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