001package org.unix4j.unix.head; 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.Head; 017 018/** 019 * Arguments and options for the {@link Head head} command. 020 */ 021public final class HeadArguments implements Arguments<HeadArguments> { 022 023 private final HeadOptions options; 024 025 026 // operand: <count> 027 private long count; 028 private boolean countIsSet = false; 029 030 // operand: <paths> 031 private String[] paths; 032 private boolean pathsIsSet = false; 033 034 // operand: <files> 035 private java.io.File[] files; 036 private boolean filesIsSet = false; 037 038 // operand: <inputs> 039 private org.unix4j.io.Input[] inputs; 040 private boolean inputsIsSet = 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 HeadArguments() { 050 this.options = HeadOptions.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 HeadArguments(HeadOptions 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 HeadOptions 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 HeadArguments(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: head " + 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 (HeadOptions.class.equals(operandType)) { 123 convertedValue = operandType.cast(HeadOptions.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 head command"); 134 } 135 136 @Override 137 public HeadArguments 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 HeadOptions.Default options = new HeadOptions.Default(); 161 final HeadArguments argsForContext = new HeadArguments(options); 162 for (final Map.Entry<String, List<Object>> e : map.entrySet()) { 163 if ("count".equals(e.getKey())) { 164 165 final long value = convertList(context, "count", long.class, e.getValue()); 166 argsForContext.setCount(value); 167 } else if ("paths".equals(e.getKey())) { 168 169 final String[] value = convertList(context, "paths", String[].class, e.getValue()); 170 argsForContext.setPaths(value); 171 } else if ("files".equals(e.getKey())) { 172 173 final java.io.File[] value = convertList(context, "files", java.io.File[].class, e.getValue()); 174 argsForContext.setFiles(value); 175 } else if ("inputs".equals(e.getKey())) { 176 177 final org.unix4j.io.Input[] value = convertList(context, "inputs", org.unix4j.io.Input[].class, e.getValue()); 178 argsForContext.setInputs(value); 179 } else if ("args".equals(e.getKey())) { 180 throw new IllegalStateException("invalid operand '" + e.getKey() + "' in head command args: " + Arrays.toString(args)); 181 } else if ("options".equals(e.getKey())) { 182 183 final HeadOptions value = convertList(context, "options", HeadOptions.class, e.getValue()); 184 options.setAll(value); 185 } else { 186 throw new IllegalStateException("invalid operand '" + e.getKey() + "' in head command args: " + Arrays.toString(args)); 187 } 188 } 189 return argsForContext; 190 } 191 192 /** 193 * Returns the {@code <count>} operand value: The first {@code count} lines of each input file are 194 copied to standard output, starting from 1 (characters instead of 195 lines if the {@code -c} option is specified). Must be a non-negative 196 integer or an exception is thrown. If {@code count} is greater than 197 the number number of lines (characters) in the input, the 198 application will not error and send the whole file to the output. 199 * 200 * @return the {@code <count>} operand value (variables are not resolved) 201 * @throws IllegalStateException if this operand has never been set 202 * 203 */ 204 public long getCount() { 205 if (countIsSet) { 206 return count; 207 } 208 throw new IllegalStateException("operand has not been set: " + count); 209 } 210 211 /** 212 * Returns true if the {@code <count>} operand has been set. 213 * <p> 214 * Note that this method returns true even if {@code null} was passed to the 215 * {@link #setCount(long)} method. 216 * 217 * @return true if the setter for the {@code <count>} operand has 218 * been called at least once 219 */ 220 public boolean isCountSet() { 221 return countIsSet; 222 } 223 /** 224 * Sets {@code <count>}: The first {@code count} lines of each input file are 225 copied to standard output, starting from 1 (characters instead of 226 lines if the {@code -c} option is specified). Must be a non-negative 227 integer or an exception is thrown. If {@code count} is greater than 228 the number number of lines (characters) in the input, the 229 application will not error and send the whole file to the output. 230 * 231 * @param count the value for the {@code <count>} operand 232 */ 233 public void setCount(long count) { 234 this.count = count; 235 this.countIsSet = true; 236 } 237 /** 238 * Returns the {@code <paths>} operand value: Path names of the input files to be filtered; wildcards * and ? are 239 supported; relative paths are resolved on the basis of the current 240 working directory. 241 * 242 * @return the {@code <paths>} operand value (variables are not resolved) 243 * @throws IllegalStateException if this operand has never been set 244 * 245 */ 246 public String[] getPaths() { 247 if (pathsIsSet) { 248 return paths; 249 } 250 throw new IllegalStateException("operand has not been set: " + paths); 251 } 252 253 /** 254 * Returns true if the {@code <paths>} operand has been set. 255 * <p> 256 * Note that this method returns true even if {@code null} was passed to the 257 * {@link #setPaths(String[])} method. 258 * 259 * @return true if the setter for the {@code <paths>} operand has 260 * been called at least once 261 */ 262 public boolean isPathsSet() { 263 return pathsIsSet; 264 } 265 /** 266 * Sets {@code <paths>}: Path names of the input files to be filtered; wildcards * and ? are 267 supported; relative paths are resolved on the basis of the current 268 working directory. 269 * 270 * @param paths the value for the {@code <paths>} operand 271 */ 272 public void setPaths(String... paths) { 273 this.paths = paths; 274 this.pathsIsSet = true; 275 } 276 /** 277 * Returns the {@code <files>} operand value: The input files to be filtered; relative paths are not resolved (use 278 the string paths argument to enable relative path resolving based on 279 the current working directory). 280 * 281 * @return the {@code <files>} operand value (variables are not resolved) 282 * @throws IllegalStateException if this operand has never been set 283 * 284 */ 285 public java.io.File[] getFiles() { 286 if (filesIsSet) { 287 return files; 288 } 289 throw new IllegalStateException("operand has not been set: " + files); 290 } 291 292 /** 293 * Returns true if the {@code <files>} operand has been set. 294 * <p> 295 * Note that this method returns true even if {@code null} was passed to the 296 * {@link #setFiles(java.io.File[])} method. 297 * 298 * @return true if the setter for the {@code <files>} operand has 299 * been called at least once 300 */ 301 public boolean isFilesSet() { 302 return filesIsSet; 303 } 304 /** 305 * Sets {@code <files>}: The input files to be filtered; relative paths are not resolved (use 306 the string paths argument to enable relative path resolving based on 307 the current working directory). 308 * 309 * @param files the value for the {@code <files>} operand 310 */ 311 public void setFiles(java.io.File... files) { 312 this.files = files; 313 this.filesIsSet = true; 314 } 315 /** 316 * Returns the {@code <inputs>} operand value: The inputs to be filtered. 317 * 318 * @return the {@code <inputs>} operand value (variables are not resolved) 319 * @throws IllegalStateException if this operand has never been set 320 * 321 */ 322 public org.unix4j.io.Input[] getInputs() { 323 if (inputsIsSet) { 324 return inputs; 325 } 326 throw new IllegalStateException("operand has not been set: " + inputs); 327 } 328 329 /** 330 * Returns true if the {@code <inputs>} operand has been set. 331 * <p> 332 * Note that this method returns true even if {@code null} was passed to the 333 * {@link #setInputs(org.unix4j.io.Input[])} method. 334 * 335 * @return true if the setter for the {@code <inputs>} operand has 336 * been called at least once 337 */ 338 public boolean isInputsSet() { 339 return inputsIsSet; 340 } 341 /** 342 * Sets {@code <inputs>}: The inputs to be filtered. 343 * 344 * @param inputs the value for the {@code <inputs>} operand 345 */ 346 public void setInputs(org.unix4j.io.Input... inputs) { 347 this.inputs = inputs; 348 this.inputsIsSet = true; 349 } 350 /** 351 * Returns the {@code <args>} operand value: String arguments defining the options and operands for the command. 352 Options can be specified by acronym (with a leading dash "-") or by 353 long name (with two leading dashes "--"). Operands other than the 354 default "--paths" operand have to be prefixed with the operand 355 name (e.g. "--count" for a subsequent count operand value). 356 * 357 * @return the {@code <args>} operand value (variables are not resolved) 358 * @throws IllegalStateException if this operand has never been set 359 * 360 */ 361 public String[] getArgs() { 362 if (argsIsSet) { 363 return args; 364 } 365 throw new IllegalStateException("operand has not been set: " + args); 366 } 367 368 /** 369 * Returns true if the {@code <args>} operand has been set. 370 * 371 * @return true if the setter for the {@code <args>} operand has 372 * been called at least once 373 */ 374 public boolean isArgsSet() { 375 return argsIsSet; 376 } 377 378 /** 379 * Returns true if the {@code --}{@link HeadOption#chars chars} option 380 * is set. The option is also known as {@code -}c option. 381 * <p> 382 * Description: The {@code count} argument is in units of characters instead of 383 lines. Starts from 1 and includes line ending characters. 384 * 385 * @return true if the {@code --chars} or {@code -c} option is set 386 */ 387 public boolean isChars() { 388 return getOptions().isSet(HeadOption.chars); 389 } 390 /** 391 * Returns true if the {@code --}{@link HeadOption#suppressHeaders suppressHeaders} option 392 * is set. The option is also known as {@code -}q option. 393 * <p> 394 * Description: Suppresses printing of headers when multiple files are being 395 examined. 396 * 397 * @return true if the {@code --suppressHeaders} or {@code -q} option is set 398 */ 399 public boolean isSuppressHeaders() { 400 return getOptions().isSet(HeadOption.suppressHeaders); 401 } 402 403 @Override 404 public String toString() { 405 // ok, we have options or arguments or both 406 final StringBuilder sb = new StringBuilder(); 407 408 if (argsIsSet) { 409 for (String arg : args) { 410 if (sb.length() > 0) sb.append(' '); 411 sb.append(arg); 412 } 413 } else { 414 415 // first the options 416 if (options.size() > 0) { 417 sb.append(DefaultOptionSet.toString(options)); 418 } 419 // operand: <count> 420 if (countIsSet) { 421 if (sb.length() > 0) sb.append(' '); 422 sb.append("--").append("count"); 423 sb.append(" ").append(toString(getCount())); 424 } 425 // operand: <paths> 426 if (pathsIsSet) { 427 if (sb.length() > 0) sb.append(' '); 428 sb.append("--").append("paths"); 429 sb.append(" ").append(toString(getPaths())); 430 } 431 // operand: <files> 432 if (filesIsSet) { 433 if (sb.length() > 0) sb.append(' '); 434 sb.append("--").append("files"); 435 sb.append(" ").append(toString(getFiles())); 436 } 437 // operand: <inputs> 438 if (inputsIsSet) { 439 if (sb.length() > 0) sb.append(' '); 440 sb.append("--").append("inputs"); 441 sb.append(" ").append(toString(getInputs())); 442 } 443 // operand: <args> 444 if (argsIsSet) { 445 if (sb.length() > 0) sb.append(' '); 446 sb.append("--").append("args"); 447 sb.append(" ").append(toString(getArgs())); 448 } 449 } 450 451 return sb.toString(); 452 } 453 private static String toString(Object value) { 454 if (value != null && value.getClass().isArray()) { 455 return ArrayUtil.toString(value); 456 } 457 return String.valueOf(value); 458 } 459}