001package org.unix4j.unix.xargs; 002 003import java.util.List; 004import java.util.Map; 005import java.util.Arrays; 006 007import org.unix4j.command.Arguments; 008import org.unix4j.context.ExecutionContext; 009import org.unix4j.convert.ValueConverter; 010import org.unix4j.option.DefaultOptionSet; 011import org.unix4j.util.ArgsUtil; 012import org.unix4j.util.ArrayUtil; 013import org.unix4j.variable.Arg; 014import org.unix4j.variable.VariableContext; 015 016import org.unix4j.unix.Xargs; 017 018/** 019 * Arguments and options for the {@link Xargs xargs} command. 020 */ 021public final class XargsArguments implements Arguments<XargsArguments> { 022 023 private final XargsOptions options; 024 025 026 // operand: <delimiter> 027 private String delimiter; 028 private boolean delimiterIsSet = false; 029 030 // operand: <eof> 031 private String eof; 032 private boolean eofIsSet = false; 033 034 // operand: <maxLines> 035 private long maxLines; 036 private boolean maxLinesIsSet = false; 037 038 // operand: <maxArgs> 039 private int maxArgs; 040 private boolean maxArgsIsSet = false; 041 042 // operand: <args> 043 private String[] args; 044 private boolean argsIsSet = false; 045 046 /** 047 * Constructor to use if no options are specified. 048 */ 049 public XargsArguments() { 050 this.options = XargsOptions.EMPTY; 051 } 052 053 /** 054 * Constructor with option set containing the selected command options. 055 * 056 * @param options the selected options 057 * @throws NullPointerException if the argument is null 058 */ 059 public XargsArguments(XargsOptions options) { 060 if (options == null) { 061 throw new NullPointerException("options argument cannot be null"); 062 } 063 this.options = options; 064 } 065 066 /** 067 * Returns the options set containing the selected command options. Returns 068 * an empty options set if no option has been selected. 069 * 070 * @return set with the selected options 071 */ 072 public XargsOptions getOptions() { 073 return options; 074 } 075 076 /** 077 * Constructor string arguments encoding options and arguments, possibly 078 * also containing variable expressions. 079 * 080 * @param args string arguments for the command 081 * @throws NullPointerException if args is null 082 */ 083 public XargsArguments(String... args) { 084 this(); 085 this.args = args; 086 this.argsIsSet = true; 087 } 088 private Object[] resolveVariables(VariableContext context, String... unresolved) { 089 final Object[] resolved = new Object[unresolved.length]; 090 for (int i = 0; i < resolved.length; i++) { 091 final String expression = unresolved[i]; 092 if (Arg.isVariable(expression)) { 093 resolved[i] = resolveVariable(context, expression); 094 } else { 095 resolved[i] = expression; 096 } 097 } 098 return resolved; 099 } 100 private <V> V convertList(ExecutionContext context, String operandName, Class<V> operandType, List<Object> values) { 101 if (values.size() == 1) { 102 final Object value = values.get(0); 103 return convert(context, operandName, operandType, value); 104 } 105 return convert(context, operandName, operandType, values); 106 } 107 108 private Object resolveVariable(VariableContext context, String variable) { 109 final Object value = context.getValue(variable); 110 if (value != null) { 111 return value; 112 } 113 throw new IllegalArgumentException("cannot resolve variable " + variable + 114 " in command: xargs " + this); 115 } 116 private <V> V convert(ExecutionContext context, String operandName, Class<V> operandType, Object value) { 117 final ValueConverter<V> converter = context.getValueConverterFor(operandType); 118 final V convertedValue; 119 if (converter != null) { 120 convertedValue = converter.convert(value); 121 } else { 122 if (XargsOptions.class.equals(operandType)) { 123 convertedValue = operandType.cast(XargsOptions.CONVERTER.convert(value)); 124 } else { 125 convertedValue = null; 126 } 127 } 128 if (convertedValue != null) { 129 return convertedValue; 130 } 131 throw new IllegalArgumentException("cannot convert --" + operandName + 132 " value '" + value + "' into the type " + operandType.getName() + 133 " for xargs command"); 134 } 135 136 @Override 137 public XargsArguments getForContext(ExecutionContext context) { 138 if (context == null) { 139 throw new NullPointerException("context cannot be null"); 140 } 141 if (!argsIsSet || args.length == 0) { 142 //nothing to resolve 143 return this; 144 } 145 146 //check if there is at least one variable 147 boolean hasVariable = false; 148 for (final String arg : args) { 149 if (arg != null && arg.startsWith("$")) { 150 hasVariable = true; 151 break; 152 } 153 } 154 //resolve variables 155 final Object[] resolvedArgs = hasVariable ? resolveVariables(context.getVariableContext(), this.args) : this.args; 156 157 //convert now 158 final List<String> defaultOperands = Arrays.asList("maxArgs"); 159 final Map<String, List<Object>> map = ArgsUtil.parseArgs("options", defaultOperands, resolvedArgs); 160 final XargsOptions.Default options = new XargsOptions.Default(); 161 final XargsArguments argsForContext = new XargsArguments(options); 162 for (final Map.Entry<String, List<Object>> e : map.entrySet()) { 163 if ("delimiter".equals(e.getKey())) { 164 165 final String value = convertList(context, "delimiter", String.class, e.getValue()); 166 argsForContext.setDelimiter(value); 167 } else if ("eof".equals(e.getKey())) { 168 169 final String value = convertList(context, "eof", String.class, e.getValue()); 170 argsForContext.setEof(value); 171 } else if ("maxLines".equals(e.getKey())) { 172 173 final long value = convertList(context, "maxLines", long.class, e.getValue()); 174 argsForContext.setMaxLines(value); 175 } else if ("maxArgs".equals(e.getKey())) { 176 177 final int value = convertList(context, "maxArgs", int.class, e.getValue()); 178 argsForContext.setMaxArgs(value); 179 } else if ("args".equals(e.getKey())) { 180 throw new IllegalStateException("invalid operand '" + e.getKey() + "' in xargs command args: " + Arrays.toString(args)); 181 } else if ("options".equals(e.getKey())) { 182 183 final XargsOptions value = convertList(context, "options", XargsOptions.class, e.getValue()); 184 options.setAll(value); 185 } else { 186 throw new IllegalStateException("invalid operand '" + e.getKey() + "' in xargs command args: " + Arrays.toString(args)); 187 } 188 } 189 return argsForContext; 190 } 191 192 /** 193 * Returns the {@code <delimiter>} operand value (variables are NOT resolved): Input items are terminated by the specified characters. 194 * 195 * @return the {@code <delimiter>} operand value (variables are not resolved) 196 * @throws IllegalStateException if this operand has never been set 197 * @see #getDelimiter(ExecutionContext) 198 */ 199 public String getDelimiter() { 200 if (delimiterIsSet) { 201 return delimiter; 202 } 203 throw new IllegalStateException("operand has not been set: " + delimiter); 204 } 205 /** 206 * Returns the {@code <delimiter>} (variables are resolved): Input items are terminated by the specified characters. 207 * 208 * @param context the execution context used to resolve variables 209 * @return the {@code <delimiter>} operand value after resolving variables 210 * @throws IllegalStateException if this operand has never been set 211 * @see #getDelimiter() 212 */ 213 public String getDelimiter(ExecutionContext context) { 214 final String value = getDelimiter(); 215 if (Arg.isVariable(value)) { 216 final Object resolved = resolveVariable(context.getVariableContext(), value); 217 final String converted = convert(context, "delimiter", String.class, resolved); 218 return converted; 219 } 220 return value; 221 } 222 223 /** 224 * Returns true if the {@code <delimiter>} operand has been set. 225 * <p> 226 * Note that this method returns true even if {@code null} was passed to the 227 * {@link #setDelimiter(String)} method. 228 * 229 * @return true if the setter for the {@code <delimiter>} operand has 230 * been called at least once 231 */ 232 public boolean isDelimiterSet() { 233 return delimiterIsSet; 234 } 235 /** 236 * Sets {@code <delimiter>}: Input items are terminated by the specified characters. 237 * 238 * @param delimiter the value for the {@code <delimiter>} operand 239 */ 240 public void setDelimiter(String delimiter) { 241 this.delimiter = delimiter; 242 this.delimiterIsSet = true; 243 } 244 /** 245 * Returns the {@code <eof>} operand value (variables are NOT resolved): If the end of file string occurs as a line of input, the rest of the 246 input is ignored. 247 * 248 * @return the {@code <eof>} operand value (variables are not resolved) 249 * @throws IllegalStateException if this operand has never been set 250 * @see #getEof(ExecutionContext) 251 */ 252 public String getEof() { 253 if (eofIsSet) { 254 return eof; 255 } 256 throw new IllegalStateException("operand has not been set: " + eof); 257 } 258 /** 259 * Returns the {@code <eof>} (variables are resolved): If the end of file string occurs as a line of input, the rest of the 260 input is ignored. 261 * 262 * @param context the execution context used to resolve variables 263 * @return the {@code <eof>} operand value after resolving variables 264 * @throws IllegalStateException if this operand has never been set 265 * @see #getEof() 266 */ 267 public String getEof(ExecutionContext context) { 268 final String value = getEof(); 269 if (Arg.isVariable(value)) { 270 final Object resolved = resolveVariable(context.getVariableContext(), value); 271 final String converted = convert(context, "eof", String.class, resolved); 272 return converted; 273 } 274 return value; 275 } 276 277 /** 278 * Returns true if the {@code <eof>} operand has been set. 279 * <p> 280 * Note that this method returns true even if {@code null} was passed to the 281 * {@link #setEof(String)} method. 282 * 283 * @return true if the setter for the {@code <eof>} operand has 284 * been called at least once 285 */ 286 public boolean isEofSet() { 287 return eofIsSet; 288 } 289 /** 290 * Sets {@code <eof>}: If the end of file string occurs as a line of input, the rest of the 291 input is ignored. 292 * 293 * @param eof the value for the {@code <eof>} operand 294 */ 295 public void setEof(String eof) { 296 this.eof = eof; 297 this.eofIsSet = true; 298 } 299 /** 300 * Returns the {@code <maxLines>} operand value: Use at most maxLines nonblank input lines per command invocation. 301 * 302 * @return the {@code <maxLines>} operand value (variables are not resolved) 303 * @throws IllegalStateException if this operand has never been set 304 * 305 */ 306 public long getMaxLines() { 307 if (maxLinesIsSet) { 308 return maxLines; 309 } 310 throw new IllegalStateException("operand has not been set: " + maxLines); 311 } 312 313 /** 314 * Returns true if the {@code <maxLines>} operand has been set. 315 * <p> 316 * Note that this method returns true even if {@code null} was passed to the 317 * {@link #setMaxLines(long)} method. 318 * 319 * @return true if the setter for the {@code <maxLines>} operand has 320 * been called at least once 321 */ 322 public boolean isMaxLinesSet() { 323 return maxLinesIsSet; 324 } 325 /** 326 * Sets {@code <maxLines>}: Use at most maxLines nonblank input lines per command invocation. 327 * 328 * @param maxLines the value for the {@code <maxLines>} operand 329 */ 330 public void setMaxLines(long maxLines) { 331 this.maxLines = maxLines; 332 this.maxLinesIsSet = true; 333 } 334 /** 335 * Returns the {@code <maxArgs>} operand value: Use at most maxArgs arguments per command invocation. 336 * 337 * @return the {@code <maxArgs>} operand value (variables are not resolved) 338 * @throws IllegalStateException if this operand has never been set 339 * 340 */ 341 public int getMaxArgs() { 342 if (maxArgsIsSet) { 343 return maxArgs; 344 } 345 throw new IllegalStateException("operand has not been set: " + maxArgs); 346 } 347 348 /** 349 * Returns true if the {@code <maxArgs>} operand has been set. 350 * <p> 351 * Note that this method returns true even if {@code null} was passed to the 352 * {@link #setMaxArgs(int)} method. 353 * 354 * @return true if the setter for the {@code <maxArgs>} operand has 355 * been called at least once 356 */ 357 public boolean isMaxArgsSet() { 358 return maxArgsIsSet; 359 } 360 /** 361 * Sets {@code <maxArgs>}: Use at most maxArgs arguments per command invocation. 362 * 363 * @param maxArgs the value for the {@code <maxArgs>} operand 364 */ 365 public void setMaxArgs(int maxArgs) { 366 this.maxArgs = maxArgs; 367 this.maxArgsIsSet = true; 368 } 369 /** 370 * Returns the {@code <args>} operand value: String arguments defining the options and operands for the command. 371 Options can be specified by acronym (with a leading dash "-") or by 372 long name (with two leading dashes "--"). Operands other than the 373 default "--maxArgs" operand have to be prefixed with the operand 374 name (e.g. "--maxLines" for a subsequent line count operand value). 375 * 376 * @return the {@code <args>} operand value (variables are not resolved) 377 * @throws IllegalStateException if this operand has never been set 378 * 379 */ 380 public String[] getArgs() { 381 if (argsIsSet) { 382 return args; 383 } 384 throw new IllegalStateException("operand has not been set: " + args); 385 } 386 387 /** 388 * Returns true if the {@code <args>} operand has been set. 389 * 390 * @return true if the setter for the {@code <args>} operand has 391 * been called at least once 392 */ 393 public boolean isArgsSet() { 394 return argsIsSet; 395 } 396 397 /** 398 * Returns true if the {@code --}{@link XargsOption#delimiter0 delimiter0} option 399 * is set. The option is also known as {@code -}z option. 400 * <p> 401 * Description: Input items are terminated by a null character instead of by 402 whitespace, and the quotes and backslash are not special (every 403 character is taken literally). Disables the end of file string, 404 which is treated like any other argument. Useful when input items 405 might contain white space, quote marks, or backslashes. The find 406 --print0 option produces input suitable for this mode. 407 <p> 408 (This option is ignored if an explicit delimiter operand is specified). 409 * 410 * @return true if the {@code --delimiter0} or {@code -z} option is set 411 */ 412 public boolean isDelimiter0() { 413 return getOptions().isSet(XargsOption.delimiter0); 414 } 415 /** 416 * Returns true if the {@code --}{@link XargsOption#exactArgs exactArgs} option 417 * is set. The option is also known as {@code -}x option. 418 * <p> 419 * Description: Terminate immediately if {@code maxArgs} is specified but the found 420 number of variable items is less than {@code maxArgs}. 421<p> 422 (This option is ignored if no {@code maxArgs} operand is specified). 423 * 424 * @return true if the {@code --exactArgs} or {@code -x} option is set 425 */ 426 public boolean isExactArgs() { 427 return getOptions().isSet(XargsOption.exactArgs); 428 } 429 /** 430 * Returns true if the {@code --}{@link XargsOption#noRunIfEmpty noRunIfEmpty} option 431 * is set. The option is also known as {@code -}r option. 432 * <p> 433 * Description: If the standard input does not contain any nonblanks, do not run the 434 command. Normally, the command is run once even if there is no 435 input. 436 * 437 * @return true if the {@code --noRunIfEmpty} or {@code -r} option is set 438 */ 439 public boolean isNoRunIfEmpty() { 440 return getOptions().isSet(XargsOption.noRunIfEmpty); 441 } 442 /** 443 * Returns true if the {@code --}{@link XargsOption#verbose verbose} option 444 * is set. The option is also known as {@code -}t option. 445 * <p> 446 * Description: Print the command line on the standard error output before executing 447 it. 448 * 449 * @return true if the {@code --verbose} or {@code -t} option is set 450 */ 451 public boolean isVerbose() { 452 return getOptions().isSet(XargsOption.verbose); 453 } 454 455 @Override 456 public String toString() { 457 // ok, we have options or arguments or both 458 final StringBuilder sb = new StringBuilder(); 459 460 if (argsIsSet) { 461 for (String arg : args) { 462 if (sb.length() > 0) sb.append(' '); 463 sb.append(arg); 464 } 465 } else { 466 467 // first the options 468 if (options.size() > 0) { 469 sb.append(DefaultOptionSet.toString(options)); 470 } 471 // operand: <delimiter> 472 if (delimiterIsSet) { 473 if (sb.length() > 0) sb.append(' '); 474 sb.append("--").append("delimiter"); 475 sb.append(" ").append(toString(getDelimiter())); 476 } 477 // operand: <eof> 478 if (eofIsSet) { 479 if (sb.length() > 0) sb.append(' '); 480 sb.append("--").append("eof"); 481 sb.append(" ").append(toString(getEof())); 482 } 483 // operand: <maxLines> 484 if (maxLinesIsSet) { 485 if (sb.length() > 0) sb.append(' '); 486 sb.append("--").append("maxLines"); 487 sb.append(" ").append(toString(getMaxLines())); 488 } 489 // operand: <maxArgs> 490 if (maxArgsIsSet) { 491 if (sb.length() > 0) sb.append(' '); 492 sb.append("--").append("maxArgs"); 493 sb.append(" ").append(toString(getMaxArgs())); 494 } 495 // operand: <args> 496 if (argsIsSet) { 497 if (sb.length() > 0) sb.append(' '); 498 sb.append("--").append("args"); 499 sb.append(" ").append(toString(getArgs())); 500 } 501 } 502 503 return sb.toString(); 504 } 505 private static String toString(Object value) { 506 if (value != null && value.getClass().isArray()) { 507 return ArrayUtil.toString(value); 508 } 509 return String.valueOf(value); 510 } 511}