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