001package org.unix4j.unix.cut; 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.Cut; 017 018/** 019 * Arguments and options for the {@link Cut cut} command. 020 */ 021public final class CutArguments implements Arguments<CutArguments> { 022 023 private final CutOptions options; 024 025 026 // operand: <delimiter> 027 private String delimiter; 028 private boolean delimiterIsSet = false; 029 030 // operand: <outputDelimiter> 031 private char outputDelimiter; 032 private boolean outputDelimiterIsSet = false; 033 034 // operand: <indexes> 035 private int[] indexes; 036 private boolean indexesIsSet = false; 037 038 // operand: <range> 039 private org.unix4j.util.Range range; 040 private boolean rangeIsSet = 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 CutArguments() { 050 this.options = CutOptions.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 CutArguments(CutOptions 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 CutOptions 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 CutArguments(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: cut " + 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 (CutOptions.class.equals(operandType)) { 123 convertedValue = operandType.cast(CutOptions.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 cut command"); 134 } 135 136 @Override 137 public CutArguments 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("range"); 159 final Map<String, List<Object>> map = ArgsUtil.parseArgs("options", defaultOperands, resolvedArgs); 160 final CutOptions.Default options = new CutOptions.Default(); 161 final CutArguments argsForContext = new CutArguments(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 ("outputDelimiter".equals(e.getKey())) { 168 169 final char value = convertList(context, "outputDelimiter", char.class, e.getValue()); 170 argsForContext.setOutputDelimiter(value); 171 } else if ("indexes".equals(e.getKey())) { 172 173 final int[] value = convertList(context, "indexes", int[].class, e.getValue()); 174 argsForContext.setIndexes(value); 175 } else if ("range".equals(e.getKey())) { 176 177 final org.unix4j.util.Range value = convertList(context, "range", org.unix4j.util.Range.class, e.getValue()); 178 argsForContext.setRange(value); 179 } else if ("args".equals(e.getKey())) { 180 throw new IllegalStateException("invalid operand '" + e.getKey() + "' in cut command args: " + Arrays.toString(args)); 181 } else if ("options".equals(e.getKey())) { 182 183 final CutOptions value = convertList(context, "options", CutOptions.class, e.getValue()); 184 options.setAll(value); 185 } else { 186 throw new IllegalStateException("invalid operand '" + e.getKey() + "' in cut command args: " + Arrays.toString(args)); 187 } 188 } 189 return argsForContext; 190 } 191 192 /** 193 * Returns the {@code <delimiter>} operand value (variables are NOT resolved): use as the output delimiter the default is to use the input delimiter 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): use as the output delimiter the default is to use the input delimiter 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>}: use as the output delimiter the default is to use the input delimiter 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 <outputDelimiter>} operand value: use as the output delimiter the default is to use the input delimiter 246 * 247 * @return the {@code <outputDelimiter>} operand value (variables are not resolved) 248 * @throws IllegalStateException if this operand has never been set 249 * 250 */ 251 public char getOutputDelimiter() { 252 if (outputDelimiterIsSet) { 253 return outputDelimiter; 254 } 255 throw new IllegalStateException("operand has not been set: " + outputDelimiter); 256 } 257 258 /** 259 * Returns true if the {@code <outputDelimiter>} operand has been set. 260 * <p> 261 * Note that this method returns true even if {@code null} was passed to the 262 * {@link #setOutputDelimiter(char)} method. 263 * 264 * @return true if the setter for the {@code <outputDelimiter>} operand has 265 * been called at least once 266 */ 267 public boolean isOutputDelimiterSet() { 268 return outputDelimiterIsSet; 269 } 270 /** 271 * Sets {@code <outputDelimiter>}: use as the output delimiter the default is to use the input delimiter 272 * 273 * @param outputDelimiter the value for the {@code <outputDelimiter>} operand 274 */ 275 public void setOutputDelimiter(char outputDelimiter) { 276 this.outputDelimiter = outputDelimiter; 277 this.outputDelimiterIsSet = true; 278 } 279 /** 280 * Returns the {@code <indexes>} operand value: select these chars/field based on the given indexes. Indexes are 1 based. i.e. the first character/field on a line has an index of 1. 281 * 282 * @return the {@code <indexes>} operand value (variables are not resolved) 283 * @throws IllegalStateException if this operand has never been set 284 * 285 */ 286 public int[] getIndexes() { 287 if (indexesIsSet) { 288 return indexes; 289 } 290 throw new IllegalStateException("operand has not been set: " + indexes); 291 } 292 293 /** 294 * Returns true if the {@code <indexes>} operand has been set. 295 * <p> 296 * Note that this method returns true even if {@code null} was passed to the 297 * {@link #setIndexes(int[])} method. 298 * 299 * @return true if the setter for the {@code <indexes>} operand has 300 * been called at least once 301 */ 302 public boolean isIndexesSet() { 303 return indexesIsSet; 304 } 305 /** 306 * Sets {@code <indexes>}: select these chars/field based on the given indexes. Indexes are 1 based. i.e. the first character/field on a line has an index of 1. 307 * 308 * @param indexes the value for the {@code <indexes>} operand 309 */ 310 public void setIndexes(int... indexes) { 311 this.indexes = indexes; 312 this.indexesIsSet = true; 313 } 314 /** 315 * Returns the {@code <range>} operand value: select only these fields 316 * 317 * @return the {@code <range>} operand value (variables are not resolved) 318 * @throws IllegalStateException if this operand has never been set 319 * 320 */ 321 public org.unix4j.util.Range getRange() { 322 if (rangeIsSet) { 323 return range; 324 } 325 throw new IllegalStateException("operand has not been set: " + range); 326 } 327 328 /** 329 * Returns true if the {@code <range>} operand has been set. 330 * <p> 331 * Note that this method returns true even if {@code null} was passed to the 332 * {@link #setRange(org.unix4j.util.Range)} method. 333 * 334 * @return true if the setter for the {@code <range>} operand has 335 * been called at least once 336 */ 337 public boolean isRangeSet() { 338 return rangeIsSet; 339 } 340 /** 341 * Sets {@code <range>}: select only these fields 342 * 343 * @param range the value for the {@code <range>} operand 344 */ 345 public void setRange(org.unix4j.util.Range range) { 346 this.range = range; 347 this.rangeIsSet = true; 348 } 349 /** 350 * Returns the {@code <args>} operand value: String arguments defining the options and operands for the command. 351 Options can be specified by acronym (with a leading dash "-") or by 352 long name (with two leading dashes "--"). Operands other than the 353 default "--range" operand have to be prefixed with the operand name 354 (e.g. "--indexes" for subsequent index operand values). 355 * 356 * @return the {@code <args>} operand value (variables are not resolved) 357 * @throws IllegalStateException if this operand has never been set 358 * 359 */ 360 public String[] getArgs() { 361 if (argsIsSet) { 362 return args; 363 } 364 throw new IllegalStateException("operand has not been set: " + args); 365 } 366 367 /** 368 * Returns true if the {@code <args>} operand has been set. 369 * 370 * @return true if the setter for the {@code <args>} operand has 371 * been called at least once 372 */ 373 public boolean isArgsSet() { 374 return argsIsSet; 375 } 376 377 /** 378 * Returns true if the {@code --}{@link CutOption#chars chars} option 379 * is set. The option is also known as {@code -}c option. 380 * <p> 381 * Description: The list specifies character positions. 382 * 383 * @return true if the {@code --chars} or {@code -c} option is set 384 */ 385 public boolean isChars() { 386 return getOptions().isSet(CutOption.chars); 387 } 388 /** 389 * Returns true if the {@code --}{@link CutOption#fields fields} option 390 * is set. The option is also known as {@code -}f option. 391 * <p> 392 * Description: The list specifies fields, separated in the input by the field 393 delimiter character (see the -d option.) Output fields are 394 separated by a single occurrence of the field delimiter character. 395 * 396 * @return true if the {@code --fields} or {@code -f} option is set 397 */ 398 public boolean isFields() { 399 return getOptions().isSet(CutOption.fields); 400 } 401 402 @Override 403 public String toString() { 404 // ok, we have options or arguments or both 405 final StringBuilder sb = new StringBuilder(); 406 407 if (argsIsSet) { 408 for (String arg : args) { 409 if (sb.length() > 0) sb.append(' '); 410 sb.append(arg); 411 } 412 } else { 413 414 // first the options 415 if (options.size() > 0) { 416 sb.append(DefaultOptionSet.toString(options)); 417 } 418 // operand: <delimiter> 419 if (delimiterIsSet) { 420 if (sb.length() > 0) sb.append(' '); 421 sb.append("--").append("delimiter"); 422 sb.append(" ").append(toString(getDelimiter())); 423 } 424 // operand: <outputDelimiter> 425 if (outputDelimiterIsSet) { 426 if (sb.length() > 0) sb.append(' '); 427 sb.append("--").append("outputDelimiter"); 428 sb.append(" ").append(toString(getOutputDelimiter())); 429 } 430 // operand: <indexes> 431 if (indexesIsSet) { 432 if (sb.length() > 0) sb.append(' '); 433 sb.append("--").append("indexes"); 434 sb.append(" ").append(toString(getIndexes())); 435 } 436 // operand: <range> 437 if (rangeIsSet) { 438 if (sb.length() > 0) sb.append(' '); 439 sb.append("--").append("range"); 440 sb.append(" ").append(toString(getRange())); 441 } 442 // operand: <args> 443 if (argsIsSet) { 444 if (sb.length() > 0) sb.append(' '); 445 sb.append("--").append("args"); 446 sb.append(" ").append(toString(getArgs())); 447 } 448 } 449 450 return sb.toString(); 451 } 452 private static String toString(Object value) { 453 if (value != null && value.getClass().isArray()) { 454 return ArrayUtil.toString(value); 455 } 456 return String.valueOf(value); 457 } 458}