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: <comparator> 035 private java.util.Comparator<? super org.unix4j.line.Line> comparator; 036 private boolean comparatorIsSet = false; 037 038 // operand: <args> 039 private String[] args; 040 private boolean argsIsSet = false; 041 042 /** 043 * Constructor to use if no options are specified. 044 */ 045 public SortArguments() { 046 this.options = SortOptions.EMPTY; 047 } 048 049 /** 050 * Constructor with option set containing the selected command options. 051 * 052 * @param options the selected options 053 * @throws NullPointerException if the argument is null 054 */ 055 public SortArguments(SortOptions options) { 056 if (options == null) { 057 throw new NullPointerException("options argument cannot be null"); 058 } 059 this.options = options; 060 } 061 062 /** 063 * Returns the options set containing the selected command options. Returns 064 * an empty options set if no option has been selected. 065 * 066 * @return set with the selected options 067 */ 068 public SortOptions getOptions() { 069 return options; 070 } 071 072 /** 073 * Constructor string arguments encoding options and arguments, possibly 074 * also containing variable expressions. 075 * 076 * @param args string arguments for the command 077 * @throws NullPointerException if args is null 078 */ 079 public SortArguments(String... args) { 080 this(); 081 this.args = args; 082 this.argsIsSet = true; 083 } 084 private Object[] resolveVariables(VariableContext context, String... unresolved) { 085 final Object[] resolved = new Object[unresolved.length]; 086 for (int i = 0; i < resolved.length; i++) { 087 final String expression = unresolved[i]; 088 if (Arg.isVariable(expression)) { 089 resolved[i] = resolveVariable(context, expression); 090 } else { 091 resolved[i] = expression; 092 } 093 } 094 return resolved; 095 } 096 private <V> V convertList(ExecutionContext context, String operandName, Class<V> operandType, List<Object> values) { 097 if (values.size() == 1) { 098 final Object value = values.get(0); 099 return convert(context, operandName, operandType, value); 100 } 101 return convert(context, operandName, operandType, values); 102 } 103 104 private Object resolveVariable(VariableContext context, String variable) { 105 final Object value = context.getValue(variable); 106 if (value != null) { 107 return value; 108 } 109 throw new IllegalArgumentException("cannot resolve variable " + variable + 110 " in command: sort " + this); 111 } 112 private <V> V convert(ExecutionContext context, String operandName, Class<V> operandType, Object value) { 113 final ValueConverter<V> converter = context.getValueConverterFor(operandType); 114 final V convertedValue; 115 if (converter != null) { 116 convertedValue = converter.convert(value); 117 } else { 118 if (SortOptions.class.equals(operandType)) { 119 convertedValue = operandType.cast(SortOptions.CONVERTER.convert(value)); 120 } else { 121 convertedValue = null; 122 } 123 } 124 if (convertedValue != null) { 125 return convertedValue; 126 } 127 throw new IllegalArgumentException("cannot convert --" + operandName + 128 " value '" + value + "' into the type " + operandType.getName() + 129 " for sort command"); 130 } 131 132 @Override 133 public SortArguments getForContext(ExecutionContext context) { 134 if (context == null) { 135 throw new NullPointerException("context cannot be null"); 136 } 137 if (!argsIsSet || args.length == 0) { 138 //nothing to resolve 139 return this; 140 } 141 142 //check if there is at least one variable 143 boolean hasVariable = false; 144 for (final String arg : args) { 145 if (arg != null && arg.startsWith("$")) { 146 hasVariable = true; 147 break; 148 } 149 } 150 //resolve variables 151 final Object[] resolvedArgs = hasVariable ? resolveVariables(context.getVariableContext(), this.args) : this.args; 152 153 //convert now 154 final List<String> defaultOperands = Arrays.asList("paths"); 155 final Map<String, List<Object>> map = ArgsUtil.parseArgs("options", defaultOperands, resolvedArgs); 156 final SortOptions.Default options = new SortOptions.Default(); 157 final SortArguments argsForContext = new SortArguments(options); 158 for (final Map.Entry<String, List<Object>> e : map.entrySet()) { 159 if ("paths".equals(e.getKey())) { 160 161 final String[] value = convertList(context, "paths", String[].class, e.getValue()); 162 argsForContext.setPaths(value); 163 } else if ("files".equals(e.getKey())) { 164 165 final java.io.File[] value = convertList(context, "files", java.io.File[].class, e.getValue()); 166 argsForContext.setFiles(value); 167 } else if ("comparator".equals(e.getKey())) { 168 @SuppressWarnings("unchecked") 169 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()); 170 argsForContext.setComparator(value); 171 } else if ("args".equals(e.getKey())) { 172 throw new IllegalStateException("invalid operand '" + e.getKey() + "' in sort command args: " + Arrays.toString(args)); 173 } else if ("options".equals(e.getKey())) { 174 175 final SortOptions value = convertList(context, "options", SortOptions.class, e.getValue()); 176 options.setAll(value); 177 } else { 178 throw new IllegalStateException("invalid operand '" + e.getKey() + "' in sort command args: " + Arrays.toString(args)); 179 } 180 } 181 return argsForContext; 182 } 183 184 /** 185 * Returns the {@code <paths>} operand value: Pathnames of the files to be sorted, merged, or checked; wildcards * 186 and ? are supported; relative paths are resolved on the 187 basis of the current working directory. 188 * 189 * @return the {@code <paths>} operand value (variables are not resolved) 190 * @throws IllegalStateException if this operand has never been set 191 * 192 */ 193 public String[] getPaths() { 194 if (pathsIsSet) { 195 return paths; 196 } 197 throw new IllegalStateException("operand has not been set: " + paths); 198 } 199 200 /** 201 * Returns true if the {@code <paths>} operand has been set. 202 * <p> 203 * Note that this method returns true even if {@code null} was passed to the 204 * {@link #setPaths(String[])} method. 205 * 206 * @return true if the setter for the {@code <paths>} operand has 207 * been called at least once 208 */ 209 public boolean isPathsSet() { 210 return pathsIsSet; 211 } 212 /** 213 * Sets {@code <paths>}: Pathnames of the files to be sorted, merged, or checked; wildcards * 214 and ? are supported; relative paths are resolved on the 215 basis of the current working directory. 216 * 217 * @param paths the value for the {@code <paths>} operand 218 */ 219 public void setPaths(String... paths) { 220 this.paths = paths; 221 this.pathsIsSet = true; 222 } 223 /** 224 * Returns the {@code <files>} operand value: The files to be sorted or merged; relative paths are not resolved 225 (use the string paths argument to enable relative path resolving 226 based on the current working directory). 227 * 228 * @return the {@code <files>} operand value (variables are not resolved) 229 * @throws IllegalStateException if this operand has never been set 230 * 231 */ 232 public java.io.File[] getFiles() { 233 if (filesIsSet) { 234 return files; 235 } 236 throw new IllegalStateException("operand has not been set: " + files); 237 } 238 239 /** 240 * Returns true if the {@code <files>} operand has been set. 241 * <p> 242 * Note that this method returns true even if {@code null} was passed to the 243 * {@link #setFiles(java.io.File[])} method. 244 * 245 * @return true if the setter for the {@code <files>} operand has 246 * been called at least once 247 */ 248 public boolean isFilesSet() { 249 return filesIsSet; 250 } 251 /** 252 * Sets {@code <files>}: The files to be sorted or merged; relative paths are not resolved 253 (use the string paths argument to enable relative path resolving 254 based on the current working directory). 255 * 256 * @param files the value for the {@code <files>} operand 257 */ 258 public void setFiles(java.io.File... files) { 259 this.files = files; 260 this.filesIsSet = true; 261 } 262 /** 263 * Returns the {@code <comparator>} operand value: The comparator to use for the line comparisons. 264 * 265 * @return the {@code <comparator>} operand value (variables are not resolved) 266 * @throws IllegalStateException if this operand has never been set 267 * 268 */ 269 public java.util.Comparator<? super org.unix4j.line.Line> getComparator() { 270 if (comparatorIsSet) { 271 return comparator; 272 } 273 throw new IllegalStateException("operand has not been set: " + comparator); 274 } 275 276 /** 277 * Returns true if the {@code <comparator>} operand has been set. 278 * <p> 279 * Note that this method returns true even if {@code null} was passed to the 280 * {@link #setComparator(java.util.Comparator)} method. 281 * 282 * @return true if the setter for the {@code <comparator>} operand has 283 * been called at least once 284 */ 285 public boolean isComparatorSet() { 286 return comparatorIsSet; 287 } 288 /** 289 * Sets {@code <comparator>}: The comparator to use for the line comparisons. 290 * 291 * @param comparator the value for the {@code <comparator>} operand 292 */ 293 public void setComparator(java.util.Comparator<? super org.unix4j.line.Line> comparator) { 294 this.comparator = comparator; 295 this.comparatorIsSet = true; 296 } 297 /** 298 * Returns the {@code <args>} operand value: String arguments defining the options and operands for the command. 299 Options can be specified by acronym (with a leading dash "-") or by 300 long name (with two leading dashes "--"). Operands other than the 301 default "--paths" operand have to be prefixed with the operand 302 name (e.g. "--comparator" for a subsequent comparator operand value). 303 * 304 * @return the {@code <args>} operand value (variables are not resolved) 305 * @throws IllegalStateException if this operand has never been set 306 * 307 */ 308 public String[] getArgs() { 309 if (argsIsSet) { 310 return args; 311 } 312 throw new IllegalStateException("operand has not been set: " + args); 313 } 314 315 /** 316 * Returns true if the {@code <args>} operand has been set. 317 * 318 * @return true if the setter for the {@code <args>} operand has 319 * been called at least once 320 */ 321 public boolean isArgsSet() { 322 return argsIsSet; 323 } 324 325 /** 326 * Returns true if the {@code --}{@link SortOption#check check} option 327 * is set. The option is also known as {@code -}c option. 328 * <p> 329 * Description: Checks that the single input file is ordered as specified by the 330 arguments and the collating sequence of the current locale. No 331 output is produced; only the exit code is affected. 332 * 333 * @return true if the {@code --check} or {@code -c} option is set 334 */ 335 public boolean isCheck() { 336 return getOptions().isSet(SortOption.check); 337 } 338 /** 339 * Returns true if the {@code --}{@link SortOption#merge merge} option 340 * is set. The option is also known as {@code -}m option. 341 * <p> 342 * Description: Merge only; the input file are assumed to be already sorted. 343 * 344 * @return true if the {@code --merge} or {@code -m} option is set 345 */ 346 public boolean isMerge() { 347 return getOptions().isSet(SortOption.merge); 348 } 349 /** 350 * Returns true if the {@code --}{@link SortOption#unique unique} option 351 * is set. The option is also known as {@code -}u option. 352 * <p> 353 * Description: Unique: suppress all but one in each set of lines having equal keys. 354 If used with the {@code -c} option, checks that there are no lines 355 with duplicate keys, in addition to checking that the input file is 356 sorted. 357 * 358 * @return true if the {@code --unique} or {@code -u} option is set 359 */ 360 public boolean isUnique() { 361 return getOptions().isSet(SortOption.unique); 362 } 363 /** 364 * Returns true if the {@code --}{@link SortOption#ignoreLeadingBlanks ignoreLeadingBlanks} option 365 * is set. The option is also known as {@code -}b option. 366 * <p> 367 * Description: Ignore leading blanks. 368 (This option is ignored if a comparator operand is present). 369 * 370 * @return true if the {@code --ignoreLeadingBlanks} or {@code -b} option is set 371 */ 372 public boolean isIgnoreLeadingBlanks() { 373 return getOptions().isSet(SortOption.ignoreLeadingBlanks); 374 } 375 /** 376 * Returns true if the {@code --}{@link SortOption#dictionaryOrder dictionaryOrder} option 377 * is set. The option is also known as {@code -}d option. 378 * <p> 379 * Description: Consider only blanks and alphanumeric characters. 380 (This option is ignored if a comparator operand is present). 381 * 382 * @return true if the {@code --dictionaryOrder} or {@code -d} option is set 383 */ 384 public boolean isDictionaryOrder() { 385 return getOptions().isSet(SortOption.dictionaryOrder); 386 } 387 /** 388 * Returns true if the {@code --}{@link SortOption#ignoreCase ignoreCase} option 389 * is set. The option is also known as {@code -}f option. 390 * <p> 391 * Description: Consider all lowercase characters that have uppercase equivalents to 392 be the uppercase equivalent for the purposes of comparison. 393 (This option is ignored if a comparator operand is present). 394 * 395 * @return true if the {@code --ignoreCase} or {@code -f} option is set 396 */ 397 public boolean isIgnoreCase() { 398 return getOptions().isSet(SortOption.ignoreCase); 399 } 400 /** 401 * Returns true if the {@code --}{@link SortOption#numericSort numericSort} option 402 * is set. The option is also known as {@code -}n option. 403 * <p> 404 * Description: Sort numerically; the number begins each line and consists of 405 optional blanks, an optional minus sign, and zero or more digits 406 possibly separated by thousands separators, optionally followed by a 407 decimal-point character and zero or more digits. An empty number is 408 treated as '0'. The current local specifies the decimal-point 409 character and thousands separator. 410 <p> 411 Comparison is exact; there is no rounding error. 412 <p> 413 Neither a leading '+' nor exponential notation is recognized. To 414 compare such strings numerically, use the 415 {@code -genericNumericSort (-g)} option. 416<p> 417 (This option is ignored if a comparator operand is present). 418 * 419 * @return true if the {@code --numericSort} or {@code -n} option is set 420 */ 421 public boolean isNumericSort() { 422 return getOptions().isSet(SortOption.numericSort); 423 } 424 /** 425 * Returns true if the {@code --}{@link SortOption#generalNumericSort generalNumericSort} option 426 * is set. The option is also known as {@code -}g option. 427 * <p> 428 * Description: Sort numerically, using the standard {@link Double#parseDouble(String)} 429 function to convert a trimmed line to a double-precision floating 430 point number. This allows floating point numbers to be specified in 431 scientific notation, like 1.0e-34 and 10e100. 432 <p> 433 Uses the following collating sequence: Lines that cannot be parsed 434 because they do not represent valid double values (in alpha-numeric 435 order); "-Infinity"; finite numbers in ascending numeric order 436 (with -0 < +0); "Infinity"; "NaN". 437<p> 438 This option is usually slower than {@code -numeric-sort (-n)} and it 439 can lose information when converting to floating point. 440 <p> 441 (This option is ignored if a comparator operand is present). 442 * 443 * @return true if the {@code --generalNumericSort} or {@code -g} option is set 444 */ 445 public boolean isGeneralNumericSort() { 446 return getOptions().isSet(SortOption.generalNumericSort); 447 } 448 /** 449 * Returns true if the {@code --}{@link SortOption#humanNumericSort humanNumericSort} option 450 * is set. The option is also known as {@code -}h option. 451 * <p> 452 * Description: Sort numerically, first by numeric sign (negative, zero, or 453 positive); then by SI suffix (either empty, or 'k' or 'K', or one 454 of 'MGTPEZY', in that order); and finally by numeric value. For 455 example, '1023M' sorts before '1G' because 'M' (mega) precedes 'G' 456 (giga) as an SI suffix. 457 <p> 458 This option sorts values that are consistently scaled to the nearest 459 suffix, regardless of whether suffixes denote powers of 1000 or 460 1024, and it therefore sorts the output of any single invocation of 461 the {@code ls} command that are invoked with the --human-readable 462 option. 463 <p> 464 The syntax for numbers is the same as for the 465 {@code --numericSort (-n)} option; the SI suffix must immediately 466 follow the number. 467<p> 468 (This option is ignored if a comparator operand is present). 469 * 470 * @return true if the {@code --humanNumericSort} or {@code -h} option is set 471 */ 472 public boolean isHumanNumericSort() { 473 return getOptions().isSet(SortOption.humanNumericSort); 474 } 475 /** 476 * Returns true if the {@code --}{@link SortOption#monthSort monthSort} option 477 * is set. The option is also known as {@code -}M option. 478 * <p> 479 * Description: An initial string, consisting of any amount of blanks, followed by a 480 month name abbreviation, is folded to UPPER case and compared in the 481 order: (unknown) < 'JAN' < ... < 'DEC'. The current locale 482 determines the month spellings. 483 * 484 * @return true if the {@code --monthSort} or {@code -M} option is set 485 */ 486 public boolean isMonthSort() { 487 return getOptions().isSet(SortOption.monthSort); 488 } 489 /** 490 * Returns true if the {@code --}{@link SortOption#versionSort versionSort} option 491 * is set. The option is also known as {@code -}V option. 492 * <p> 493 * Description: Sort by version name and number. It behaves like a standard sort, 494 except that each sequence of decimal digits is treated numerically 495 as an index/version number. 496 <p> 497 (This option is ignored if a comparator operand is present). 498 * 499 * @return true if the {@code --versionSort} or {@code -V} option is set 500 */ 501 public boolean isVersionSort() { 502 return getOptions().isSet(SortOption.versionSort); 503 } 504 /** 505 * Returns true if the {@code --}{@link SortOption#reverse reverse} option 506 * is set. The option is also known as {@code -}r option. 507 * <p> 508 * Description: Reverse the sense of comparisons. 509 * 510 * @return true if the {@code --reverse} or {@code -r} option is set 511 */ 512 public boolean isReverse() { 513 return getOptions().isSet(SortOption.reverse); 514 } 515 516 @Override 517 public String toString() { 518 // ok, we have options or arguments or both 519 final StringBuilder sb = new StringBuilder(); 520 521 if (argsIsSet) { 522 for (String arg : args) { 523 if (sb.length() > 0) sb.append(' '); 524 sb.append(arg); 525 } 526 } else { 527 528 // first the options 529 if (options.size() > 0) { 530 sb.append(DefaultOptionSet.toString(options)); 531 } 532 // operand: <paths> 533 if (pathsIsSet) { 534 if (sb.length() > 0) sb.append(' '); 535 sb.append("--").append("paths"); 536 sb.append(" ").append(toString(getPaths())); 537 } 538 // operand: <files> 539 if (filesIsSet) { 540 if (sb.length() > 0) sb.append(' '); 541 sb.append("--").append("files"); 542 sb.append(" ").append(toString(getFiles())); 543 } 544 // operand: <comparator> 545 if (comparatorIsSet) { 546 if (sb.length() > 0) sb.append(' '); 547 sb.append("--").append("comparator"); 548 sb.append(" ").append(toString(getComparator())); 549 } 550 // operand: <args> 551 if (argsIsSet) { 552 if (sb.length() > 0) sb.append(' '); 553 sb.append("--").append("args"); 554 sb.append(" ").append(toString(getArgs())); 555 } 556 } 557 558 return sb.toString(); 559 } 560 private static String toString(Object value) { 561 if (value != null && value.getClass().isArray()) { 562 return ArrayUtil.toString(value); 563 } 564 return String.valueOf(value); 565 } 566}