001package org.unix4j.unix.grep;
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.Grep;
017
018/**
019 * Arguments and options for the {@link Grep grep} command.
020 */
021public final class GrepArguments implements Arguments<GrepArguments> {
022        
023        private final GrepOptions options;
024
025        
026        // operand: <regexp>
027        private String regexp;
028        private boolean regexpIsSet = false;
029        
030        // operand: <pattern>
031        private java.util.regex.Pattern pattern;
032        private boolean patternIsSet = false;
033        
034        // operand: <paths>
035        private String[] paths;
036        private boolean pathsIsSet = false;
037        
038        // operand: <files>
039        private java.io.File[] files;
040        private boolean filesIsSet = false;
041        
042        // operand: <inputs>
043        private org.unix4j.io.Input[] inputs;
044        private boolean inputsIsSet = false;
045        
046        // operand: <args>
047        private String[] args;
048        private boolean argsIsSet = false;
049        
050        /**
051         * Constructor to use if no options are specified.
052         */
053        public GrepArguments() {
054                this.options = GrepOptions.EMPTY;
055        }
056
057        /**
058         * Constructor with option set containing the selected command options.
059         * 
060         * @param options the selected options
061         * @throws NullPointerException if the argument is null
062         */
063        public GrepArguments(GrepOptions options) {
064                if (options == null) {
065                        throw new NullPointerException("options argument cannot be null");
066                }
067                this.options = options;
068        }
069        
070        /**
071         * Returns the options set containing the selected command options. Returns
072         * an empty options set if no option has been selected.
073         * 
074         * @return set with the selected options
075         */
076        public GrepOptions getOptions() {
077                return options;
078        }
079
080        /**
081         * Constructor string arguments encoding options and arguments, possibly
082         * also containing variable expressions. 
083         * 
084         * @param args string arguments for the command
085         * @throws NullPointerException if args is null
086         */
087        public GrepArguments(String... args) {
088                this();
089                this.args = args;
090                this.argsIsSet = true;
091        }
092        private Object[] resolveVariables(VariableContext context, String... unresolved) {
093                final Object[] resolved = new Object[unresolved.length];
094                for (int i = 0; i < resolved.length; i++) {
095                        final String expression = unresolved[i];
096                        if (Arg.isVariable(expression)) {
097                                resolved[i] = resolveVariable(context, expression);
098                        } else {
099                                resolved[i] = expression;
100                        }
101                }
102                return resolved;
103        }
104        private <V> V convertList(ExecutionContext context, String operandName, Class<V> operandType, List<Object> values) {
105                if (values.size() == 1) {
106                        final Object value = values.get(0);
107                        return convert(context, operandName, operandType, value);
108                }
109                return convert(context, operandName, operandType, values);
110        }
111
112        private Object resolveVariable(VariableContext context, String variable) {
113                final Object value = context.getValue(variable);
114                if (value != null) {
115                        return value;
116                }
117                throw new IllegalArgumentException("cannot resolve variable " + variable + 
118                                " in command: grep " + this);
119        }
120        private <V> V convert(ExecutionContext context, String operandName, Class<V> operandType, Object value) {
121                final ValueConverter<V> converter = context.getValueConverterFor(operandType);
122                final V convertedValue;
123                if (converter != null) {
124                        convertedValue = converter.convert(value);
125                } else {
126                        if (GrepOptions.class.equals(operandType)) {
127                                convertedValue = operandType.cast(GrepOptions.CONVERTER.convert(value));
128                        } else {
129                                convertedValue = null;
130                        }
131                }
132                if (convertedValue != null) {
133                        return convertedValue;
134                }
135                throw new IllegalArgumentException("cannot convert --" + operandName + 
136                                " value '" + value + "' into the type " + operandType.getName() + 
137                                " for grep command");
138        }
139        
140        @Override
141        public GrepArguments getForContext(ExecutionContext context) {
142                if (context == null) {
143                        throw new NullPointerException("context cannot be null");
144                }
145                if (!argsIsSet || args.length == 0) {
146                        //nothing to resolve
147                        return this;
148                }
149
150                //check if there is at least one variable
151                boolean hasVariable = false;
152                for (final String arg : args) {
153                        if (arg != null && arg.startsWith("$")) {
154                                hasVariable = true;
155                                break;
156                        }
157                }
158                //resolve variables
159                final Object[] resolvedArgs = hasVariable ? resolveVariables(context.getVariableContext(), this.args) : this.args;
160                
161                //convert now
162                final List<String> defaultOperands = Arrays.asList("regexp", "paths");
163                final Map<String, List<Object>> map = ArgsUtil.parseArgs("options", defaultOperands, resolvedArgs);
164                final GrepOptions.Default options = new GrepOptions.Default();
165                final GrepArguments argsForContext = new GrepArguments(options);
166                for (final Map.Entry<String, List<Object>> e : map.entrySet()) {
167                        if ("regexp".equals(e.getKey())) {
168                                        
169                                final String value = convertList(context, "regexp", String.class, e.getValue());  
170                                argsForContext.setRegexp(value);
171                        } else if ("pattern".equals(e.getKey())) {
172                                        
173                                final java.util.regex.Pattern value = convertList(context, "pattern", java.util.regex.Pattern.class, e.getValue());  
174                                argsForContext.setPattern(value);
175                        } else if ("paths".equals(e.getKey())) {
176                                        
177                                final String[] value = convertList(context, "paths", String[].class, e.getValue());  
178                                argsForContext.setPaths(value);
179                        } else if ("files".equals(e.getKey())) {
180                                        
181                                final java.io.File[] value = convertList(context, "files", java.io.File[].class, e.getValue());  
182                                argsForContext.setFiles(value);
183                        } else if ("inputs".equals(e.getKey())) {
184                                        
185                                final org.unix4j.io.Input[] value = convertList(context, "inputs", org.unix4j.io.Input[].class, e.getValue());  
186                                argsForContext.setInputs(value);
187                        } else if ("args".equals(e.getKey())) {
188                                throw new IllegalStateException("invalid operand '" + e.getKey() + "' in grep command args: " + Arrays.toString(args));
189                        } else if ("options".equals(e.getKey())) {
190                                        
191                                final GrepOptions value = convertList(context, "options", GrepOptions.class, e.getValue());  
192                                options.setAll(value);
193                        } else {
194                                throw new IllegalStateException("invalid operand '" + e.getKey() + "' in grep command args: " + Arrays.toString(args));
195                        }
196                }
197                return argsForContext;
198        }
199        
200        /**
201         * Returns the {@code <regexp>} operand value (variables are NOT resolved): Lines will be printed which match the given regular expression. The 
202                        {@code regexp} string is surrounded with ".*" on both sides unless
203                        the {@code --wholeLine} option is specified. If the 
204                        {@code --fixedStrings} option is used, plain string comparison is
205                        used instead of regular expression matching.
206         * 
207         * @return the {@code <regexp>} operand value (variables are not resolved)
208         * @throws IllegalStateException if this operand has never been set
209         * @see #getRegexp(ExecutionContext)
210         */
211        public String getRegexp() {
212                if (regexpIsSet) {
213                        return regexp;
214                }
215                throw new IllegalStateException("operand has not been set: " + regexp);
216        }
217        /**
218         * Returns the {@code <regexp>} (variables are resolved): Lines will be printed which match the given regular expression. The 
219                        {@code regexp} string is surrounded with ".*" on both sides unless
220                        the {@code --wholeLine} option is specified. If the 
221                        {@code --fixedStrings} option is used, plain string comparison is
222                        used instead of regular expression matching.
223         * 
224         * @param context the execution context used to resolve variables
225         * @return the {@code <regexp>} operand value after resolving variables
226         * @throws IllegalStateException if this operand has never been set
227         * @see #getRegexp()
228         */
229        public String getRegexp(ExecutionContext context) {
230                final String value = getRegexp();
231                if (Arg.isVariable(value)) {
232                        final Object resolved = resolveVariable(context.getVariableContext(), value);
233                        final String converted = convert(context, "regexp", String.class, resolved);
234                        return converted;
235                }
236                return value;
237        }
238
239        /**
240         * Returns true if the {@code <regexp>} operand has been set. 
241         * <p>
242         * Note that this method returns true even if {@code null} was passed to the
243         * {@link #setRegexp(String)} method.
244         * 
245         * @return      true if the setter for the {@code <regexp>} operand has 
246         *                      been called at least once
247         */
248        public boolean isRegexpSet() {
249                return regexpIsSet;
250        }
251        /**
252         * Sets {@code <regexp>}: Lines will be printed which match the given regular expression. The 
253                        {@code regexp} string is surrounded with ".*" on both sides unless
254                        the {@code --wholeLine} option is specified. If the 
255                        {@code --fixedStrings} option is used, plain string comparison is
256                        used instead of regular expression matching.
257         * 
258         * @param regexp the value for the {@code <regexp>} operand
259         */
260        public void setRegexp(String regexp) {
261                this.regexp = regexp;
262                this.regexpIsSet = true;
263        }
264        /**
265         * Returns the {@code <pattern>} operand value: Lines will be printed which match the given pattern.
266         * 
267         * @return the {@code <pattern>} operand value (variables are not resolved)
268         * @throws IllegalStateException if this operand has never been set
269         * 
270         */
271        public java.util.regex.Pattern getPattern() {
272                if (patternIsSet) {
273                        return pattern;
274                }
275                throw new IllegalStateException("operand has not been set: " + pattern);
276        }
277
278        /**
279         * Returns true if the {@code <pattern>} operand has been set. 
280         * <p>
281         * Note that this method returns true even if {@code null} was passed to the
282         * {@link #setPattern(java.util.regex.Pattern)} method.
283         * 
284         * @return      true if the setter for the {@code <pattern>} operand has 
285         *                      been called at least once
286         */
287        public boolean isPatternSet() {
288                return patternIsSet;
289        }
290        /**
291         * Sets {@code <pattern>}: Lines will be printed which match the given pattern.
292         * 
293         * @param pattern the value for the {@code <pattern>} operand
294         */
295        public void setPattern(java.util.regex.Pattern pattern) {
296                this.pattern = pattern;
297                this.patternIsSet = true;
298        }
299        /**
300         * Returns the {@code <paths>} operand value: Path names of the input files to be searched for the pattern;
301                        wildcards * and ? are supported; relative paths are resolved on the
302            basis of the current working directory.
303         * 
304         * @return the {@code <paths>} operand value (variables are not resolved)
305         * @throws IllegalStateException if this operand has never been set
306         * 
307         */
308        public String[] getPaths() {
309                if (pathsIsSet) {
310                        return paths;
311                }
312                throw new IllegalStateException("operand has not been set: " + paths);
313        }
314
315        /**
316         * Returns true if the {@code <paths>} operand has been set. 
317         * <p>
318         * Note that this method returns true even if {@code null} was passed to the
319         * {@link #setPaths(String[])} method.
320         * 
321         * @return      true if the setter for the {@code <paths>} operand has 
322         *                      been called at least once
323         */
324        public boolean isPathsSet() {
325                return pathsIsSet;
326        }
327        /**
328         * Sets {@code <paths>}: Path names of the input files to be searched for the pattern;
329                        wildcards * and ? are supported; relative paths are resolved on the
330            basis of the current working directory.
331         * 
332         * @param paths the value for the {@code <paths>} operand
333         */
334        public void setPaths(String... paths) {
335                this.paths = paths;
336                this.pathsIsSet = true;
337        }
338        /**
339         * Returns the {@code <files>} operand value: The input files to be searched for the pattern; relative paths are
340                        not resolved (use the string paths argument to enable relative path
341                        resolving based on the current working directory).
342         * 
343         * @return the {@code <files>} operand value (variables are not resolved)
344         * @throws IllegalStateException if this operand has never been set
345         * 
346         */
347        public java.io.File[] getFiles() {
348                if (filesIsSet) {
349                        return files;
350                }
351                throw new IllegalStateException("operand has not been set: " + files);
352        }
353
354        /**
355         * Returns true if the {@code <files>} operand has been set. 
356         * <p>
357         * Note that this method returns true even if {@code null} was passed to the
358         * {@link #setFiles(java.io.File[])} method.
359         * 
360         * @return      true if the setter for the {@code <files>} operand has 
361         *                      been called at least once
362         */
363        public boolean isFilesSet() {
364                return filesIsSet;
365        }
366        /**
367         * Sets {@code <files>}: The input files to be searched for the pattern; relative paths are
368                        not resolved (use the string paths argument to enable relative path
369                        resolving based on the current working directory).
370         * 
371         * @param files the value for the {@code <files>} operand
372         */
373        public void setFiles(java.io.File... files) {
374                this.files = files;
375                this.filesIsSet = true;
376        }
377        /**
378         * Returns the {@code <inputs>} operand value: The inputs to be searched for the pattern.
379         * 
380         * @return the {@code <inputs>} operand value (variables are not resolved)
381         * @throws IllegalStateException if this operand has never been set
382         * 
383         */
384        public org.unix4j.io.Input[] getInputs() {
385                if (inputsIsSet) {
386                        return inputs;
387                }
388                throw new IllegalStateException("operand has not been set: " + inputs);
389        }
390
391        /**
392         * Returns true if the {@code <inputs>} operand has been set. 
393         * <p>
394         * Note that this method returns true even if {@code null} was passed to the
395         * {@link #setInputs(org.unix4j.io.Input[])} method.
396         * 
397         * @return      true if the setter for the {@code <inputs>} operand has 
398         *                      been called at least once
399         */
400        public boolean isInputsSet() {
401                return inputsIsSet;
402        }
403        /**
404         * Sets {@code <inputs>}: The inputs to be searched for the pattern.
405         * 
406         * @param inputs the value for the {@code <inputs>} operand
407         */
408        public void setInputs(org.unix4j.io.Input... inputs) {
409                this.inputs = inputs;
410                this.inputsIsSet = true;
411        }
412        /**
413         * Returns the {@code <args>} operand value: String arguments defining the options and operands for the command. 
414                        Options can be specified by acronym (with a leading dash "-") or by 
415                        long name (with two leading dashes "--"). Operands other than the
416                        default "--pattern" and "--paths" operands have to be prefixed with
417                        the operand name (e.g. "--files" for subsequent file operand values).
418         * 
419         * @return the {@code <args>} operand value (variables are not resolved)
420         * @throws IllegalStateException if this operand has never been set
421         * 
422         */
423        public String[] getArgs() {
424                if (argsIsSet) {
425                        return args;
426                }
427                throw new IllegalStateException("operand has not been set: " + args);
428        }
429
430        /**
431         * Returns true if the {@code <args>} operand has been set. 
432         * 
433         * @return      true if the setter for the {@code <args>} operand has 
434         *                      been called at least once
435         */
436        public boolean isArgsSet() {
437                return argsIsSet;
438        }
439        
440        /**
441         * Returns true if the {@code --}{@link GrepOption#ignoreCase ignoreCase} option
442         * is set. The option is also known as {@code -}i option.
443         * <p>
444         * Description: Match lines ignoring the case when comparing the strings, also known
445                        from Unix with its acronym 'i'.
446         * 
447         * @return true if the {@code --ignoreCase} or {@code -i} option is set
448         */
449        public boolean isIgnoreCase() {
450                return getOptions().isSet(GrepOption.ignoreCase);
451        }
452        /**
453         * Returns true if the {@code --}{@link GrepOption#invertMatch invertMatch} option
454         * is set. The option is also known as {@code -}v option.
455         * <p>
456         * Description: Invert the match result, that is, a non-matching line is written to
457                        the output and a matching line is not. This option is also known 
458                        from Unix with its acronym 'v'.
459         * 
460         * @return true if the {@code --invertMatch} or {@code -v} option is set
461         */
462        public boolean isInvertMatch() {
463                return getOptions().isSet(GrepOption.invertMatch);
464        }
465        /**
466         * Returns true if the {@code --}{@link GrepOption#fixedStrings fixedStrings} option
467         * is set. The option is also known as {@code -}F option.
468         * <p>
469         * Description: Use fixed-strings matching instead of regular expressions. This is
470                        usually faster than the standard regexp version.
471                        <p>
472                        (This option is ignored if a {@code pattern} operand is specified
473                        instead of the {@code regexp} string).
474         * 
475         * @return true if the {@code --fixedStrings} or {@code -F} option is set
476         */
477        public boolean isFixedStrings() {
478                return getOptions().isSet(GrepOption.fixedStrings);
479        }
480        /**
481         * Returns true if the {@code --}{@link GrepOption#lineNumber lineNumber} option
482         * is set. The option is also known as {@code -}n option.
483         * <p>
484         * Description: Prefix each line of output with the line number within its input
485                        file.
486         * 
487         * @return true if the {@code --lineNumber} or {@code -n} option is set
488         */
489        public boolean isLineNumber() {
490                return getOptions().isSet(GrepOption.lineNumber);
491        }
492        /**
493         * Returns true if the {@code --}{@link GrepOption#count count} option
494         * is set. The option is also known as {@code -}c option.
495         * <p>
496         * Description: Suppress normal output; instead print a count of matching lines for
497                        each input file. With the {@code -v}, {@code --invertMatch} option,
498                        count non-matching lines.
499         * 
500         * @return true if the {@code --count} or {@code -c} option is set
501         */
502        public boolean isCount() {
503                return getOptions().isSet(GrepOption.count);
504        }
505        /**
506         * Returns true if the {@code --}{@link GrepOption#matchingFiles matchingFiles} option
507         * is set. The option is also known as {@code -}l option.
508         * <p>
509         * Description: Suppress normal output; instead print the name of each input file
510                        from which output would normally have been printed. The scanning
511                        will stop on the first match.
512         * 
513         * @return true if the {@code --matchingFiles} or {@code -l} option is set
514         */
515        public boolean isMatchingFiles() {
516                return getOptions().isSet(GrepOption.matchingFiles);
517        }
518        /**
519         * Returns true if the {@code --}{@link GrepOption#wholeLine wholeLine} option
520         * is set. The option is also known as {@code -}x option.
521         * <p>
522         * Description: Select only those matches that exactly match the whole line
523                        excluding the terminating line ending.
524                        <p>
525                        (This option is ignored if a {@code pattern} operand is specified
526                        instead of the {@code regexp} string).
527         * 
528         * @return true if the {@code --wholeLine} or {@code -x} option is set
529         */
530        public boolean isWholeLine() {
531                return getOptions().isSet(GrepOption.wholeLine);
532        }
533
534        @Override
535        public String toString() {
536                // ok, we have options or arguments or both
537                final StringBuilder sb = new StringBuilder();
538
539                if (argsIsSet) {
540                        for (String arg : args) {
541                                if (sb.length() > 0) sb.append(' ');
542                                sb.append(arg);
543                        }
544                } else {
545                
546                        // first the options
547                        if (options.size() > 0) {
548                                sb.append(DefaultOptionSet.toString(options));
549                        }
550                        // operand: <regexp>
551                        if (regexpIsSet) {
552                                if (sb.length() > 0) sb.append(' ');
553                                sb.append("--").append("regexp");
554                                sb.append(" ").append(toString(getRegexp()));
555                        }
556                        // operand: <pattern>
557                        if (patternIsSet) {
558                                if (sb.length() > 0) sb.append(' ');
559                                sb.append("--").append("pattern");
560                                sb.append(" ").append(toString(getPattern()));
561                        }
562                        // operand: <paths>
563                        if (pathsIsSet) {
564                                if (sb.length() > 0) sb.append(' ');
565                                sb.append("--").append("paths");
566                                sb.append(" ").append(toString(getPaths()));
567                        }
568                        // operand: <files>
569                        if (filesIsSet) {
570                                if (sb.length() > 0) sb.append(' ');
571                                sb.append("--").append("files");
572                                sb.append(" ").append(toString(getFiles()));
573                        }
574                        // operand: <inputs>
575                        if (inputsIsSet) {
576                                if (sb.length() > 0) sb.append(' ');
577                                sb.append("--").append("inputs");
578                                sb.append(" ").append(toString(getInputs()));
579                        }
580                        // operand: <args>
581                        if (argsIsSet) {
582                                if (sb.length() > 0) sb.append(' ');
583                                sb.append("--").append("args");
584                                sb.append(" ").append(toString(getArgs()));
585                        }
586                }
587                
588                return sb.toString();
589        }
590        private static String toString(Object value) {
591                if (value != null && value.getClass().isArray()) {
592                        return ArrayUtil.toString(value);
593                }
594                return String.valueOf(value);
595        }
596}