001package org.unix4j.unix.sort; 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.Sort; 017 018/** 019 * Arguments and options for the {@link Sort sort} command. 020 */ 021public final class SortArguments implements Arguments<SortArguments> { 022 023 private final SortOptions options; 024 025 026 // operand: <paths> 027 private String[] paths; 028 private boolean pathsIsSet = false; 029 030 // operand: <files> 031 private java.io.File[] files; 032 private boolean filesIsSet = false; 033 034 // operand: <inputs> 035 private org.unix4j.io.Input[] inputs; 036 private boolean inputsIsSet = false; 037 038 // operand: <comparator> 039 private java.util.Comparator<? super org.unix4j.line.Line> comparator; 040 private boolean comparatorIsSet = 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 SortArguments() { 050 this.options = SortOptions.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 SortArguments(SortOptions 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 SortOptions 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 SortArguments(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: sort " + 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 (SortOptions.class.equals(operandType)) { 123 convertedValue = operandType.cast(SortOptions.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 sort command"); 134 } 135 136 @Override 137 public SortArguments 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("paths"); 159 final Map<String, List<Object>> map = ArgsUtil.parseArgs("options", defaultOperands, resolvedArgs); 160 final SortOptions.Default options = new SortOptions.Default(); 161 final SortArguments argsForContext = new SortArguments(options); 162 for (final Map.Entry<String, List<Object>> e : map.entrySet()) { 163 if ("paths".equals(e.getKey())) { 164 165 final String[] value = convertList(context, "paths", String[].class, e.getValue()); 166 argsForContext.setPaths(value); 167 } else if ("files".equals(e.getKey())) { 168 169 final java.io.File[] value = convertList(context, "files", java.io.File[].class, e.getValue()); 170 argsForContext.setFiles(value); 171 } else if ("inputs".equals(e.getKey())) { 172 173 final org.unix4j.io.Input[] value = convertList(context, "inputs", org.unix4j.io.Input[].class, e.getValue()); 174 argsForContext.setInputs(value); 175 } else if ("comparator".equals(e.getKey())) { 176 @SuppressWarnings("unchecked") 177 final java.util.Comparator<? super org.unix4j.line.Line> value = convertList(context, "comparator", (Class<java.util.Comparator<? super org.unix4j.line.Line>>)(Class<?>)java.util.Comparator.class, e.getValue()); 178 argsForContext.setComparator(value); 179 } else if ("args".equals(e.getKey())) { 180 throw new IllegalStateException("invalid operand '" + e.getKey() + "' in sort command args: " + Arrays.toString(args)); 181 } else if ("options".equals(e.getKey())) { 182 183 final SortOptions value = convertList(context, "options", SortOptions.class, e.getValue()); 184 options.setAll(value); 185 } else { 186 throw new IllegalStateException("invalid operand '" + e.getKey() + "' in sort command args: " + Arrays.toString(args)); 187 } 188 } 189 return argsForContext; 190 } 191 192 /** 193 * Returns the {@code <paths>} operand value: Path names of the files to be sorted, merged, or checked; wildcards * 194 and ? are supported; relative paths are resolved on the 195 basis of the current working directory. 196 * 197 * @return the {@code <paths>} operand value (variables are not resolved) 198 * @throws IllegalStateException if this operand has never been set 199 * 200 */ 201 public String[] getPaths() { 202 if (pathsIsSet) { 203 return paths; 204 } 205 throw new IllegalStateException("operand has not been set: " + paths); 206 } 207 208 /** 209 * Returns true if the {@code <paths>} operand has been set. 210 * <p> 211 * Note that this method returns true even if {@code null} was passed to the 212 * {@link #setPaths(String[])} method. 213 * 214 * @return true if the setter for the {@code <paths>} operand has 215 * been called at least once 216 */ 217 public boolean isPathsSet() { 218 return pathsIsSet; 219 } 220 /** 221 * Sets {@code <paths>}: Path names of the files to be sorted, merged, or checked; wildcards * 222 and ? are supported; relative paths are resolved on the 223 basis of the current working directory. 224 * 225 * @param paths the value for the {@code <paths>} operand 226 */ 227 public void setPaths(String... paths) { 228 this.paths = paths; 229 this.pathsIsSet = true; 230 } 231 /** 232 * Returns the {@code <files>} operand value: The files to be sorted or merged; relative paths are not resolved 233 (use the string paths argument to enable relative path resolving 234 based on the current working directory). 235 * 236 * @return the {@code <files>} operand value (variables are not resolved) 237 * @throws IllegalStateException if this operand has never been set 238 * 239 */ 240 public java.io.File[] getFiles() { 241 if (filesIsSet) { 242 return files; 243 } 244 throw new IllegalStateException("operand has not been set: " + files); 245 } 246 247 /** 248 * Returns true if the {@code <files>} operand has been set. 249 * <p> 250 * Note that this method returns true even if {@code null} was passed to the 251 * {@link #setFiles(java.io.File[])} method. 252 * 253 * @return true if the setter for the {@code <files>} operand has 254 * been called at least once 255 */ 256 public boolean isFilesSet() { 257 return filesIsSet; 258 } 259 /** 260 * Sets {@code <files>}: The files to be sorted or merged; relative paths are not resolved 261 (use the string paths argument to enable relative path resolving 262 based on the current working directory). 263 * 264 * @param files the value for the {@code <files>} operand 265 */ 266 public void setFiles(java.io.File... files) { 267 this.files = files; 268 this.filesIsSet = true; 269 } 270 /** 271 * Returns the {@code <inputs>} operand value: The inputs to be sorted or merged. 272 * 273 * @return the {@code <inputs>} operand value (variables are not resolved) 274 * @throws IllegalStateException if this operand has never been set 275 * 276 */ 277 public org.unix4j.io.Input[] getInputs() { 278 if (inputsIsSet) { 279 return inputs; 280 } 281 throw new IllegalStateException("operand has not been set: " + inputs); 282 } 283 284 /** 285 * Returns true if the {@code <inputs>} operand has been set. 286 * <p> 287 * Note that this method returns true even if {@code null} was passed to the 288 * {@link #setInputs(org.unix4j.io.Input[])} method. 289 * 290 * @return true if the setter for the {@code <inputs>} operand has 291 * been called at least once 292 */ 293 public boolean isInputsSet() { 294 return inputsIsSet; 295 } 296 /** 297 * Sets {@code <inputs>}: The inputs to be sorted or merged. 298 * 299 * @param inputs the value for the {@code <inputs>} operand 300 */ 301 public void setInputs(org.unix4j.io.Input... inputs) { 302 this.inputs = inputs; 303 this.inputsIsSet = true; 304 } 305 /** 306 * Returns the {@code <comparator>} operand value: The comparator to use for the line comparisons. 307 * 308 * @return the {@code <comparator>} operand value (variables are not resolved) 309 * @throws IllegalStateException if this operand has never been set 310 * 311 */ 312 public java.util.Comparator<? super org.unix4j.line.Line> getComparator() { 313 if (comparatorIsSet) { 314 return comparator; 315 } 316 throw new IllegalStateException("operand has not been set: " + comparator); 317 } 318 319 /** 320 * Returns true if the {@code <comparator>} operand has been set. 321 * <p> 322 * Note that this method returns true even if {@code null} was passed to the 323 * {@link #setComparator(java.util.Comparator)} method. 324 * 325 * @return true if the setter for the {@code <comparator>} operand has 326 * been called at least once 327 */ 328 public boolean isComparatorSet() { 329 return comparatorIsSet; 330 } 331 /** 332 * Sets {@code <comparator>}: The comparator to use for the line comparisons. 333 * 334 * @param comparator the value for the {@code <comparator>} operand 335 */ 336 public void setComparator(java.util.Comparator<? super org.unix4j.line.Line> comparator) { 337 this.comparator = comparator; 338 this.comparatorIsSet = true; 339 } 340 /** 341 * Returns the {@code <args>} operand value: String arguments defining the options and operands for the command. 342 Options can be specified by acronym (with a leading dash "-") or by 343 long name (with two leading dashes "--"). Operands other than the 344 default "--paths" operand have to be prefixed with the operand 345 name (e.g. "--comparator" for a subsequent comparator operand value). 346 * 347 * @return the {@code <args>} operand value (variables are not resolved) 348 * @throws IllegalStateException if this operand has never been set 349 * 350 */ 351 public String[] getArgs() { 352 if (argsIsSet) { 353 return args; 354 } 355 throw new IllegalStateException("operand has not been set: " + args); 356 } 357 358 /** 359 * Returns true if the {@code <args>} operand has been set. 360 * 361 * @return true if the setter for the {@code <args>} operand has 362 * been called at least once 363 */ 364 public boolean isArgsSet() { 365 return argsIsSet; 366 } 367 368 /** 369 * Returns true if the {@code --}{@link SortOption#check check} option 370 * is set. The option is also known as {@code -}c option. 371 * <p> 372 * Description: Checks that the single input file is ordered as specified by the 373 arguments and the collating sequence of the current locale. No 374 output is produced; only the exit code is affected. 375 * 376 * @return true if the {@code --check} or {@code -c} option is set 377 */ 378 public boolean isCheck() { 379 return getOptions().isSet(SortOption.check); 380 } 381 /** 382 * Returns true if the {@code --}{@link SortOption#merge merge} option 383 * is set. The option is also known as {@code -}m option. 384 * <p> 385 * Description: Merge only; the input file are assumed to be already sorted. 386 * 387 * @return true if the {@code --merge} or {@code -m} option is set 388 */ 389 public boolean isMerge() { 390 return getOptions().isSet(SortOption.merge); 391 } 392 /** 393 * Returns true if the {@code --}{@link SortOption#unique unique} option 394 * is set. The option is also known as {@code -}u option. 395 * <p> 396 * Description: Unique: suppress all but one in each set of lines having equal keys. 397 If used with the {@code -c} option, checks that there are no lines 398 with duplicate keys, in addition to checking that the input file is 399 sorted. 400 * 401 * @return true if the {@code --unique} or {@code -u} option is set 402 */ 403 public boolean isUnique() { 404 return getOptions().isSet(SortOption.unique); 405 } 406 /** 407 * Returns true if the {@code --}{@link SortOption#ignoreLeadingBlanks ignoreLeadingBlanks} option 408 * is set. The option is also known as {@code -}b option. 409 * <p> 410 * Description: Ignore leading blanks. 411 (This option is ignored if a comparator operand is present). 412 * 413 * @return true if the {@code --ignoreLeadingBlanks} or {@code -b} option is set 414 */ 415 public boolean isIgnoreLeadingBlanks() { 416 return getOptions().isSet(SortOption.ignoreLeadingBlanks); 417 } 418 /** 419 * Returns true if the {@code --}{@link SortOption#dictionaryOrder dictionaryOrder} option 420 * is set. The option is also known as {@code -}d option. 421 * <p> 422 * Description: Consider only blanks and alphanumeric characters. 423 (This option is ignored if a comparator operand is present). 424 * 425 * @return true if the {@code --dictionaryOrder} or {@code -d} option is set 426 */ 427 public boolean isDictionaryOrder() { 428 return getOptions().isSet(SortOption.dictionaryOrder); 429 } 430 /** 431 * Returns true if the {@code --}{@link SortOption#ignoreCase ignoreCase} option 432 * is set. The option is also known as {@code -}f option. 433 * <p> 434 * Description: Consider all lowercase characters that have uppercase equivalents to 435 be the uppercase equivalent for the purposes of comparison. 436 (This option is ignored if a comparator operand is present). 437 * 438 * @return true if the {@code --ignoreCase} or {@code -f} option is set 439 */ 440 public boolean isIgnoreCase() { 441 return getOptions().isSet(SortOption.ignoreCase); 442 } 443 /** 444 * Returns true if the {@code --}{@link SortOption#numericSort numericSort} option 445 * is set. The option is also known as {@code -}n option. 446 * <p> 447 * Description: Sort numerically; the number begins each line and consists of 448 optional blanks, an optional minus sign, and zero or more digits 449 possibly separated by thousands separators, optionally followed by a 450 decimal-point character and zero or more digits. An empty number is 451 treated as '0'. The current local specifies the decimal-point 452 character and thousands separator. 453 <p> 454 Comparison is exact; there is no rounding error. 455 <p> 456 Neither a leading '+' nor exponential notation is recognized. To 457 compare such strings numerically, use the 458 {@code -genericNumericSort (-g)} option. 459<p> 460 (This option is ignored if a comparator operand is present). 461 * 462 * @return true if the {@code --numericSort} or {@code -n} option is set 463 */ 464 public boolean isNumericSort() { 465 return getOptions().isSet(SortOption.numericSort); 466 } 467 /** 468 * Returns true if the {@code --}{@link SortOption#generalNumericSort generalNumericSort} option 469 * is set. The option is also known as {@code -}g option. 470 * <p> 471 * Description: Sort numerically, using the standard {@link Double#parseDouble(String)} 472 function to convert a trimmed line to a double-precision floating 473 point number. This allows floating point numbers to be specified in 474 scientific notation, like 1.0e-34 and 10e100. 475 <p> 476 Uses the following collating sequence: Lines that cannot be parsed 477 because they do not represent valid double values (in alpha-numeric 478 order); "-Infinity"; finite numbers in ascending numeric order 479 (with -0 < +0); "Infinity"; "NaN". 480<p> 481 This option is usually slower than {@code -numeric-sort (-n)} and it 482 can lose information when converting to floating point. 483 <p> 484 (This option is ignored if a comparator operand is present). 485 * 486 * @return true if the {@code --generalNumericSort} or {@code -g} option is set 487 */ 488 public boolean isGeneralNumericSort() { 489 return getOptions().isSet(SortOption.generalNumericSort); 490 } 491 /** 492 * Returns true if the {@code --}{@link SortOption#humanNumericSort humanNumericSort} option 493 * is set. The option is also known as {@code -}h option. 494 * <p> 495 * Description: Sort numerically, first by numeric sign (negative, zero, or 496 positive); then by SI suffix (either empty, or 'k' or 'K', or one 497 of 'MGTPEZY', in that order); and finally by numeric value. For 498 example, '1023M' sorts before '1G' because 'M' (mega) precedes 'G' 499 (giga) as an SI suffix. 500 <p> 501 This option sorts values that are consistently scaled to the nearest 502 suffix, regardless of whether suffixes denote powers of 1000 or 503 1024, and it therefore sorts the output of any single invocation of 504 the {@code ls} command that are invoked with the --human-readable 505 option. 506 <p> 507 The syntax for numbers is the same as for the 508 {@code --numericSort (-n)} option; the SI suffix must immediately 509 follow the number. 510<p> 511 (This option is ignored if a comparator operand is present). 512 * 513 * @return true if the {@code --humanNumericSort} or {@code -h} option is set 514 */ 515 public boolean isHumanNumericSort() { 516 return getOptions().isSet(SortOption.humanNumericSort); 517 } 518 /** 519 * Returns true if the {@code --}{@link SortOption#monthSort monthSort} option 520 * is set. The option is also known as {@code -}M option. 521 * <p> 522 * Description: An initial string, consisting of any amount of blanks, followed by a 523 month name abbreviation, is folded to UPPER case and compared in the 524 order: (unknown) < 'JAN' < ... < 'DEC'. The current locale 525 determines the month spellings. 526 * 527 * @return true if the {@code --monthSort} or {@code -M} option is set 528 */ 529 public boolean isMonthSort() { 530 return getOptions().isSet(SortOption.monthSort); 531 } 532 /** 533 * Returns true if the {@code --}{@link SortOption#versionSort versionSort} option 534 * is set. The option is also known as {@code -}V option. 535 * <p> 536 * Description: Sort by version name and number. It behaves like a standard sort, 537 except that each sequence of decimal digits is treated numerically 538 as an index/version number. 539 <p> 540 (This option is ignored if a comparator operand is present). 541 * 542 * @return true if the {@code --versionSort} or {@code -V} option is set 543 */ 544 public boolean isVersionSort() { 545 return getOptions().isSet(SortOption.versionSort); 546 } 547 /** 548 * Returns true if the {@code --}{@link SortOption#reverse reverse} option 549 * is set. The option is also known as {@code -}r option. 550 * <p> 551 * Description: Reverse the sense of comparisons. 552 * 553 * @return true if the {@code --reverse} or {@code -r} option is set 554 */ 555 public boolean isReverse() { 556 return getOptions().isSet(SortOption.reverse); 557 } 558 559 @Override 560 public String toString() { 561 // ok, we have options or arguments or both 562 final StringBuilder sb = new StringBuilder(); 563 564 if (argsIsSet) { 565 for (String arg : args) { 566 if (sb.length() > 0) sb.append(' '); 567 sb.append(arg); 568 } 569 } else { 570 571 // first the options 572 if (options.size() > 0) { 573 sb.append(DefaultOptionSet.toString(options)); 574 } 575 // operand: <paths> 576 if (pathsIsSet) { 577 if (sb.length() > 0) sb.append(' '); 578 sb.append("--").append("paths"); 579 sb.append(" ").append(toString(getPaths())); 580 } 581 // operand: <files> 582 if (filesIsSet) { 583 if (sb.length() > 0) sb.append(' '); 584 sb.append("--").append("files"); 585 sb.append(" ").append(toString(getFiles())); 586 } 587 // operand: <inputs> 588 if (inputsIsSet) { 589 if (sb.length() > 0) sb.append(' '); 590 sb.append("--").append("inputs"); 591 sb.append(" ").append(toString(getInputs())); 592 } 593 // operand: <comparator> 594 if (comparatorIsSet) { 595 if (sb.length() > 0) sb.append(' '); 596 sb.append("--").append("comparator"); 597 sb.append(" ").append(toString(getComparator())); 598 } 599 // operand: <args> 600 if (argsIsSet) { 601 if (sb.length() > 0) sb.append(' '); 602 sb.append("--").append("args"); 603 sb.append(" ").append(toString(getArgs())); 604 } 605 } 606 607 return sb.toString(); 608 } 609 private static String toString(Object value) { 610 if (value != null && value.getClass().isArray()) { 611 return ArrayUtil.toString(value); 612 } 613 return String.valueOf(value); 614 } 615}