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: <args>
043        private String[] args;
044        private boolean argsIsSet = false;
045        
046        /**
047         * Constructor to use if no options are specified.
048         */
049        public GrepArguments() {
050                this.options = GrepOptions.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 GrepArguments(GrepOptions 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 GrepOptions 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 GrepArguments(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: grep " + 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 (GrepOptions.class.equals(operandType)) {
123                                convertedValue = operandType.cast(GrepOptions.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 grep command");
134        }
135        
136        @Override
137        public GrepArguments 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("regexp", "paths");
159                final Map<String, List<Object>> map = ArgsUtil.parseArgs("options", defaultOperands, resolvedArgs);
160                final GrepOptions.Default options = new GrepOptions.Default();
161                final GrepArguments argsForContext = new GrepArguments(options);
162                for (final Map.Entry<String, List<Object>> e : map.entrySet()) {
163                        if ("regexp".equals(e.getKey())) {
164                                        
165                                final String value = convertList(context, "regexp", String.class, e.getValue());  
166                                argsForContext.setRegexp(value);
167                        } else if ("pattern".equals(e.getKey())) {
168                                        
169                                final java.util.regex.Pattern value = convertList(context, "pattern", java.util.regex.Pattern.class, e.getValue());  
170                                argsForContext.setPattern(value);
171                        } else if ("paths".equals(e.getKey())) {
172                                        
173                                final String[] value = convertList(context, "paths", String[].class, e.getValue());  
174                                argsForContext.setPaths(value);
175                        } else if ("files".equals(e.getKey())) {
176                                        
177                                final java.io.File[] value = convertList(context, "files", java.io.File[].class, e.getValue());  
178                                argsForContext.setFiles(value);
179                        } else if ("args".equals(e.getKey())) {
180                                throw new IllegalStateException("invalid operand '" + e.getKey() + "' in grep command args: " + Arrays.toString(args));
181                        } else if ("options".equals(e.getKey())) {
182                                        
183                                final GrepOptions value = convertList(context, "options", GrepOptions.class, e.getValue());  
184                                options.setAll(value);
185                        } else {
186                                throw new IllegalStateException("invalid operand '" + e.getKey() + "' in grep command args: " + Arrays.toString(args));
187                        }
188                }
189                return argsForContext;
190        }
191        
192        /**
193         * Returns the {@code <regexp>} operand value (variables are NOT resolved): Lines will be printed which match the given regular expression. The 
194                        {@code regexp} string is surrounded with ".*" on both sides unless
195                        the {@code --wholeLine} option is specified. If the 
196                        {@code --fixedStrings} option is used, plain string comparison is
197                        used instead of regular expression matching.
198         * 
199         * @return the {@code <regexp>} operand value (variables are not resolved)
200         * @throws IllegalStateException if this operand has never been set
201         * @see #getRegexp(ExecutionContext)
202         */
203        public String getRegexp() {
204                if (regexpIsSet) {
205                        return regexp;
206                }
207                throw new IllegalStateException("operand has not been set: " + regexp);
208        }
209        /**
210         * Returns the {@code <regexp>} (variables are resolved): Lines will be printed which match the given regular expression. The 
211                        {@code regexp} string is surrounded with ".*" on both sides unless
212                        the {@code --wholeLine} option is specified. If the 
213                        {@code --fixedStrings} option is used, plain string comparison is
214                        used instead of regular expression matching.
215         * 
216         * @param context the execution context used to resolve variables
217         * @return the {@code <regexp>} operand value after resolving variables
218         * @throws IllegalStateException if this operand has never been set
219         * @see #getRegexp()
220         */
221        public String getRegexp(ExecutionContext context) {
222                final String value = getRegexp();
223                if (Arg.isVariable(value)) {
224                        final Object resolved = resolveVariable(context.getVariableContext(), value);
225                        final String converted = convert(context, "regexp", String.class, resolved);
226                        return converted;
227                }
228                return value;
229        }
230
231        /**
232         * Returns true if the {@code <regexp>} operand has been set. 
233         * <p>
234         * Note that this method returns true even if {@code null} was passed to the
235         * {@link #setRegexp(String)} method.
236         * 
237         * @return      true if the setter for the {@code <regexp>} operand has 
238         *                      been called at least once
239         */
240        public boolean isRegexpSet() {
241                return regexpIsSet;
242        }
243        /**
244         * Sets {@code <regexp>}: Lines will be printed which match the given regular expression. The 
245                        {@code regexp} string is surrounded with ".*" on both sides unless
246                        the {@code --wholeLine} option is specified. If the 
247                        {@code --fixedStrings} option is used, plain string comparison is
248                        used instead of regular expression matching.
249         * 
250         * @param regexp the value for the {@code <regexp>} operand
251         */
252        public void setRegexp(String regexp) {
253                this.regexp = regexp;
254                this.regexpIsSet = true;
255        }
256        /**
257         * Returns the {@code <pattern>} operand value: Lines will be printed which match the given pattern.
258         * 
259         * @return the {@code <pattern>} operand value (variables are not resolved)
260         * @throws IllegalStateException if this operand has never been set
261         * 
262         */
263        public java.util.regex.Pattern getPattern() {
264                if (patternIsSet) {
265                        return pattern;
266                }
267                throw new IllegalStateException("operand has not been set: " + pattern);
268        }
269
270        /**
271         * Returns true if the {@code <pattern>} operand has been set. 
272         * <p>
273         * Note that this method returns true even if {@code null} was passed to the
274         * {@link #setPattern(java.util.regex.Pattern)} method.
275         * 
276         * @return      true if the setter for the {@code <pattern>} operand has 
277         *                      been called at least once
278         */
279        public boolean isPatternSet() {
280                return patternIsSet;
281        }
282        /**
283         * Sets {@code <pattern>}: Lines will be printed which match the given pattern.
284         * 
285         * @param pattern the value for the {@code <pattern>} operand
286         */
287        public void setPattern(java.util.regex.Pattern pattern) {
288                this.pattern = pattern;
289                this.patternIsSet = true;
290        }
291        /**
292         * Returns the {@code <paths>} operand value: Pathnames of the input files to be searched for the pattern;
293                        wildcards * and ? are supported; relative paths are resolved on the
294            basis of the current working directory.
295         * 
296         * @return the {@code <paths>} operand value (variables are not resolved)
297         * @throws IllegalStateException if this operand has never been set
298         * 
299         */
300        public String[] getPaths() {
301                if (pathsIsSet) {
302                        return paths;
303                }
304                throw new IllegalStateException("operand has not been set: " + paths);
305        }
306
307        /**
308         * Returns true if the {@code <paths>} operand has been set. 
309         * <p>
310         * Note that this method returns true even if {@code null} was passed to the
311         * {@link #setPaths(String[])} method.
312         * 
313         * @return      true if the setter for the {@code <paths>} operand has 
314         *                      been called at least once
315         */
316        public boolean isPathsSet() {
317                return pathsIsSet;
318        }
319        /**
320         * Sets {@code <paths>}: Pathnames of the input files to be searched for the pattern;
321                        wildcards * and ? are supported; relative paths are resolved on the
322            basis of the current working directory.
323         * 
324         * @param paths the value for the {@code <paths>} operand
325         */
326        public void setPaths(String... paths) {
327                this.paths = paths;
328                this.pathsIsSet = true;
329        }
330        /**
331         * Returns the {@code <files>} operand value: The input files to be searched for the pattern; relative paths are 
332                        not resolved (use the string paths argument to enable relative path 
333                        resolving based on the current working directory).
334         * 
335         * @return the {@code <files>} operand value (variables are not resolved)
336         * @throws IllegalStateException if this operand has never been set
337         * 
338         */
339        public java.io.File[] getFiles() {
340                if (filesIsSet) {
341                        return files;
342                }
343                throw new IllegalStateException("operand has not been set: " + files);
344        }
345
346        /**
347         * Returns true if the {@code <files>} operand has been set. 
348         * <p>
349         * Note that this method returns true even if {@code null} was passed to the
350         * {@link #setFiles(java.io.File[])} method.
351         * 
352         * @return      true if the setter for the {@code <files>} operand has 
353         *                      been called at least once
354         */
355        public boolean isFilesSet() {
356                return filesIsSet;
357        }
358        /**
359         * Sets {@code <files>}: The input files to be searched for the pattern; relative paths are 
360                        not resolved (use the string paths argument to enable relative path 
361                        resolving based on the current working directory).
362         * 
363         * @param files the value for the {@code <files>} operand
364         */
365        public void setFiles(java.io.File... files) {
366                this.files = files;
367                this.filesIsSet = true;
368        }
369        /**
370         * Returns the {@code <args>} operand value: String arguments defining the options and operands for the command. 
371                        Options can be specified by acronym (with a leading dash "-") or by 
372                        long name (with two leading dashes "--"). Operands other than the
373                        default "--pattern" and "--paths" operands have to be prefixed with
374                        the operand name (e.g. "--files" for subsequent file operand values).
375         * 
376         * @return the {@code <args>} operand value (variables are not resolved)
377         * @throws IllegalStateException if this operand has never been set
378         * 
379         */
380        public String[] getArgs() {
381                if (argsIsSet) {
382                        return args;
383                }
384                throw new IllegalStateException("operand has not been set: " + args);
385        }
386
387        /**
388         * Returns true if the {@code <args>} operand has been set. 
389         * 
390         * @return      true if the setter for the {@code <args>} operand has 
391         *                      been called at least once
392         */
393        public boolean isArgsSet() {
394                return argsIsSet;
395        }
396        
397        /**
398         * Returns true if the {@code --}{@link GrepOption#ignoreCase ignoreCase} option
399         * is set. The option is also known as {@code -}i option.
400         * <p>
401         * Description: Match lines ignoring the case when comparing the strings, also known
402                        from Unix with its acronym 'i'.
403         * 
404         * @return true if the {@code --ignoreCase} or {@code -i} option is set
405         */
406        public boolean isIgnoreCase() {
407                return getOptions().isSet(GrepOption.ignoreCase);
408        }
409        /**
410         * Returns true if the {@code --}{@link GrepOption#invertMatch invertMatch} option
411         * is set. The option is also known as {@code -}v option.
412         * <p>
413         * Description: Invert the match result, that is, a non-matching line is written to
414                        the output and a matching line is not. This option is also known 
415                        from Unix with its acronym 'v'.
416         * 
417         * @return true if the {@code --invertMatch} or {@code -v} option is set
418         */
419        public boolean isInvertMatch() {
420                return getOptions().isSet(GrepOption.invertMatch);
421        }
422        /**
423         * Returns true if the {@code --}{@link GrepOption#fixedStrings fixedStrings} option
424         * is set. The option is also known as {@code -}F option.
425         * <p>
426         * Description: Use fixed-strings matching instead of regular expressions. This is
427                        usually faster than the standard regexp version.
428                        <p>
429                        (This option is ignored if a {@code pattern} operand is specified
430                        instead of the {@code regexp} string).
431         * 
432         * @return true if the {@code --fixedStrings} or {@code -F} option is set
433         */
434        public boolean isFixedStrings() {
435                return getOptions().isSet(GrepOption.fixedStrings);
436        }
437        /**
438         * Returns true if the {@code --}{@link GrepOption#lineNumber lineNumber} option
439         * is set. The option is also known as {@code -}n option.
440         * <p>
441         * Description: Prefix each line of output with the line number within its input
442                        file.
443         * 
444         * @return true if the {@code --lineNumber} or {@code -n} option is set
445         */
446        public boolean isLineNumber() {
447                return getOptions().isSet(GrepOption.lineNumber);
448        }
449        /**
450         * Returns true if the {@code --}{@link GrepOption#count count} option
451         * is set. The option is also known as {@code -}c option.
452         * <p>
453         * Description: Suppress normal output; instead print a count of matching lines for
454                        each input file. With the {@code -v}, {@code --invertMatch} option,
455                        count non-matching lines.
456         * 
457         * @return true if the {@code --count} or {@code -c} option is set
458         */
459        public boolean isCount() {
460                return getOptions().isSet(GrepOption.count);
461        }
462        /**
463         * Returns true if the {@code --}{@link GrepOption#matchingFiles matchingFiles} option
464         * is set. The option is also known as {@code -}l option.
465         * <p>
466         * Description: Suppress normal output; instead print the name of each input file
467                        from which output would normally have been printed. The scanning
468                        will stop on the first match.
469         * 
470         * @return true if the {@code --matchingFiles} or {@code -l} option is set
471         */
472        public boolean isMatchingFiles() {
473                return getOptions().isSet(GrepOption.matchingFiles);
474        }
475        /**
476         * Returns true if the {@code --}{@link GrepOption#wholeLine wholeLine} option
477         * is set. The option is also known as {@code -}x option.
478         * <p>
479         * Description: Select only those matches that exactly match the whole line
480                        excluding the terminating line ending.
481                        <p>
482                        (This option is ignored if a {@code pattern} operand is specified
483                        instead of the {@code regexp} string).
484         * 
485         * @return true if the {@code --wholeLine} or {@code -x} option is set
486         */
487        public boolean isWholeLine() {
488                return getOptions().isSet(GrepOption.wholeLine);
489        }
490
491        @Override
492        public String toString() {
493                // ok, we have options or arguments or both
494                final StringBuilder sb = new StringBuilder();
495
496                if (argsIsSet) {
497                        for (String arg : args) {
498                                if (sb.length() > 0) sb.append(' ');
499                                sb.append(arg);
500                        }
501                } else {
502                
503                        // first the options
504                        if (options.size() > 0) {
505                                sb.append(DefaultOptionSet.toString(options));
506                        }
507                        // operand: <regexp>
508                        if (regexpIsSet) {
509                                if (sb.length() > 0) sb.append(' ');
510                                sb.append("--").append("regexp");
511                                sb.append(" ").append(toString(getRegexp()));
512                        }
513                        // operand: <pattern>
514                        if (patternIsSet) {
515                                if (sb.length() > 0) sb.append(' ');
516                                sb.append("--").append("pattern");
517                                sb.append(" ").append(toString(getPattern()));
518                        }
519                        // operand: <paths>
520                        if (pathsIsSet) {
521                                if (sb.length() > 0) sb.append(' ');
522                                sb.append("--").append("paths");
523                                sb.append(" ").append(toString(getPaths()));
524                        }
525                        // operand: <files>
526                        if (filesIsSet) {
527                                if (sb.length() > 0) sb.append(' ');
528                                sb.append("--").append("files");
529                                sb.append(" ").append(toString(getFiles()));
530                        }
531                        // operand: <args>
532                        if (argsIsSet) {
533                                if (sb.length() > 0) sb.append(' ');
534                                sb.append("--").append("args");
535                                sb.append(" ").append(toString(getArgs()));
536                        }
537                }
538                
539                return sb.toString();
540        }
541        private static String toString(Object value) {
542                if (value != null && value.getClass().isArray()) {
543                        return ArrayUtil.toString(value);
544                }
545                return String.valueOf(value);
546        }
547}