View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    *  contributor license agreements.  See the NOTICE file distributed with
4    *  this work for additional information regarding copyright ownership.
5    *  The ASF licenses this file to You under the Apache License, Version 2.0
6    *  (the "License"); you may not use this file except in compliance with
7    *  the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   *  Unless required by applicable law or agreed to in writing, software
12   *  distributed under the License is distributed on an "AS IS" BASIS,
13   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   *  See the License for the specific language governing permissions and
15   *  limitations under the License.
16   */
17  
18  package org.apache.commons.exec;
19  
20  import java.io.File;
21  import java.nio.file.Path;
22  import java.util.ArrayList;
23  import java.util.HashMap;
24  import java.util.Map;
25  import java.util.Objects;
26  import java.util.StringTokenizer;
27  import java.util.Vector;
28  
29  import org.apache.commons.exec.util.StringUtils;
30  
31  /**
32   * CommandLine objects help handling command lines specifying processes to execute. The class can be used to a command line by an application.
33   */
34  public class CommandLine {
35  
36      /**
37       * Encapsulates a command line argument.
38       */
39      static final class Argument {
40  
41          private final String value;
42          private final boolean handleQuoting;
43  
44          private Argument(final String value, final boolean handleQuoting) {
45              this.value = value.trim();
46              this.handleQuoting = handleQuoting;
47          }
48  
49          private String getValue() {
50              return value;
51          }
52  
53          private boolean isHandleQuoting() {
54              return handleQuoting;
55          }
56      }
57  
58      /**
59       * Create a command line from a string.
60       *
61       * @param line the first element becomes the executable, the rest the arguments.
62       * @return the parsed command line.
63       * @throws IllegalArgumentException If line is null or all whitespace.
64       */
65      public static CommandLine parse(final String line) {
66          return parse(line, null);
67      }
68  
69      /**
70       * Create a command line from a string.
71       *
72       * @param line            the first element becomes the executable, the rest the arguments.
73       * @param substitutionMap the name/value pairs used for substitution.
74       * @return the parsed command line.
75       * @throws IllegalArgumentException If line is null or all whitespace.
76       */
77      public static CommandLine parse(final String line, final Map<String, ?> substitutionMap) {
78  
79          if (line == null) {
80              throw new IllegalArgumentException("Command line cannot be null");
81          }
82          if (line.trim().isEmpty()) {
83              throw new IllegalArgumentException("Command line cannot be empty");
84          }
85          final String[] tmp = translateCommandline(line);
86  
87          final CommandLine cl = new CommandLine(tmp[0]);
88          cl.setSubstitutionMap(substitutionMap);
89          for (int i = 1; i < tmp.length; i++) {
90              cl.addArgument(tmp[i]);
91          }
92  
93          return cl;
94      }
95  
96      /**
97       * Crack a command line.
98       *
99       * @param toProcess the command line to process.
100      * @return the command line broken into strings. An empty or null toProcess parameter results in a zero sized array.
101      */
102     private static String[] translateCommandline(final String toProcess) {
103         if (toProcess == null || toProcess.trim().isEmpty()) {
104             // no command? no string
105             return new String[0];
106         }
107 
108         // parse with a simple finite state machine.
109 
110         final int normal = 0;
111         final int inQuote = 1;
112         final int inDoubleQuote = 2;
113         int state = normal;
114         final StringTokenizer tok = new StringTokenizer(toProcess, "\"\' ", true);
115         final ArrayList<String> list = new ArrayList<>();
116         StringBuilder current = new StringBuilder();
117         boolean lastTokenHasBeenQuoted = false;
118 
119         while (tok.hasMoreTokens()) {
120             final String nextTok = tok.nextToken();
121             switch (state) {
122             case inQuote:
123                 if ("\'".equals(nextTok)) {
124                     lastTokenHasBeenQuoted = true;
125                     state = normal;
126                 } else {
127                     current.append(nextTok);
128                 }
129                 break;
130             case inDoubleQuote:
131                 if ("\"".equals(nextTok)) {
132                     lastTokenHasBeenQuoted = true;
133                     state = normal;
134                 } else {
135                     current.append(nextTok);
136                 }
137                 break;
138             default:
139                 switch (nextTok) {
140                 case "\'":
141                     state = inQuote;
142                     break;
143                 case "\"":
144                     state = inDoubleQuote;
145                     break;
146                 case " ":
147                     if (lastTokenHasBeenQuoted || current.length() != 0) {
148                         list.add(current.toString());
149                         current = new StringBuilder();
150                     }
151                     break;
152                 default:
153                     current.append(nextTok);
154                     break;
155                 }
156                 lastTokenHasBeenQuoted = false;
157                 break;
158             }
159         }
160 
161         if (lastTokenHasBeenQuoted || current.length() != 0) {
162             list.add(current.toString());
163         }
164 
165         if (state == inQuote || state == inDoubleQuote) {
166             throw new IllegalArgumentException("Unbalanced quotes in " + toProcess);
167         }
168 
169         final String[] args = new String[list.size()];
170         return list.toArray(args);
171     }
172 
173     /**
174      * The arguments of the command.
175      */
176     private final Vector<Argument> arguments = new Vector<>();
177 
178     /**
179      * The program to execute.
180      */
181     private final String executable;
182 
183     /**
184      * A map of name value pairs used to expand command line arguments.
185      */
186     private Map<String, ?> substitutionMap; // This can contain values other than Strings.
187 
188     /**
189      * Tests whether a file was used to set the executable.
190      */
191     private final boolean isFile;
192 
193     /**
194      * Copy constructor.
195      *
196      * @param other the instance to copy.
197      */
198     public CommandLine(final CommandLine other) {
199         this.executable = other.getExecutable();
200         this.isFile = other.isFile();
201         this.arguments.addAll(other.arguments);
202 
203         if (other.getSubstitutionMap() != null) {
204             this.substitutionMap = new HashMap<>(other.getSubstitutionMap());
205         }
206     }
207 
208     /**
209      * Constructs a command line without any arguments.
210      *
211      * @param executable the executable file.
212      */
213     public CommandLine(final File executable) {
214         this.isFile = true;
215         this.executable = toCleanExecutable(executable.getAbsolutePath());
216     }
217 
218     /**
219      * Constructs a command line without any arguments.
220      *
221      * @param executable the executable file.
222      * @since 1.5.0
223      */
224     public CommandLine(final Path executable) {
225         this.isFile = true;
226         this.executable = toCleanExecutable(executable.toAbsolutePath().toString());
227     }
228 
229     /**
230      * Constructs a command line without any arguments.
231      *
232      * @param executable the executable.
233      * @throws NullPointerException     on null input.
234      * @throws IllegalArgumentException on empty input.
235      */
236     public CommandLine(final String executable) {
237         this.isFile = false;
238         this.executable = toCleanExecutable(executable);
239     }
240 
241     /**
242      * Add a single argument. Handles quoting.
243      *
244      * @param argument The argument to add.
245      * @return The command line itself.
246      * @throws IllegalArgumentException If argument contains both single and double quotes.
247      */
248     public CommandLine addArgument(final String argument) {
249         return addArgument(argument, true);
250     }
251 
252     /**
253      * Add a single argument.
254      *
255      * @param argument      The argument to add.
256      * @param handleQuoting Add the argument with/without handling quoting.
257      * @return The command line itself.
258      */
259     public CommandLine addArgument(final String argument, final boolean handleQuoting) {
260 
261         if (argument == null) {
262             return this;
263         }
264 
265         // check if we can really quote the argument - if not throw an
266         // IllegalArgumentException
267         if (handleQuoting) {
268             StringUtils.quoteArgument(argument);
269         }
270 
271         arguments.add(new Argument(argument, handleQuoting));
272         return this;
273     }
274 
275     /**
276      * Add multiple arguments. Handles parsing of quotes and whitespace. Please note that the parsing can have undesired side-effects therefore it is
277      * recommended to build the command line incrementally.
278      *
279      * @param addArguments An string containing multiple arguments.
280      * @return The command line itself.
281      */
282     public CommandLine addArguments(final String addArguments) {
283         return addArguments(addArguments, true);
284     }
285 
286     /**
287      * Add multiple arguments. Handles parsing of quotes and whitespace. Please note that the parsing can have undesired side-effects therefore it is
288      * recommended to build the command line incrementally.
289      *
290      * @param addArguments  An string containing multiple arguments.
291      * @param handleQuoting Add the argument with/without handling quoting.
292      * @return The command line itself.
293      */
294     public CommandLine addArguments(final String addArguments, final boolean handleQuoting) {
295         if (addArguments != null) {
296             final String[] argumentsArray = translateCommandline(addArguments);
297             addArguments(argumentsArray, handleQuoting);
298         }
299 
300         return this;
301     }
302 
303     /**
304      * Add multiple arguments. Handles parsing of quotes and whitespace.
305      *
306      * @param addArguments An array of arguments.
307      * @return The command line itself.
308      */
309     public CommandLine addArguments(final String[] addArguments) {
310         return addArguments(addArguments, true);
311     }
312 
313     /**
314      * Add multiple arguments.
315      *
316      * @param addArguments  An array of arguments.
317      * @param handleQuoting Add the argument with/without handling quoting.
318      * @return The command line itself.
319      */
320     public CommandLine addArguments(final String[] addArguments, final boolean handleQuoting) {
321         if (addArguments != null) {
322             for (final String addArgument : addArguments) {
323                 addArgument(addArgument, handleQuoting);
324             }
325         }
326         return this;
327     }
328 
329     /**
330      * Expand variables in a command line argument.
331      *
332      * @param argument the argument.
333      * @return the expanded string.
334      */
335     private String expandArgument(final String argument) {
336         final StringBuffer stringBuffer = StringUtils.stringSubstitution(argument, getSubstitutionMap(), true);
337         return stringBuffer.toString();
338     }
339 
340     /**
341      * Gets the expanded and quoted command line arguments.
342      *
343      * @return The quoted arguments.
344      */
345     public String[] getArguments() {
346 
347         Argument currArgument;
348         String expandedArgument;
349         final String[] result = new String[arguments.size()];
350 
351         for (int i = 0; i < result.length; i++) {
352             currArgument = arguments.get(i);
353             expandedArgument = expandArgument(currArgument.getValue());
354             result[i] = currArgument.isHandleQuoting() ? StringUtils.quoteArgument(expandedArgument) : expandedArgument;
355         }
356 
357         return result;
358     }
359 
360     /**
361      * Gets the executable.
362      *
363      * @return The executable.
364      */
365     public String getExecutable() {
366         // Expand the executable and replace '/' and '\\' with the platform
367         // specific file separator char. This is safe here since we know
368         // that this is a platform specific command.
369         return StringUtils.fixFileSeparatorChar(expandArgument(executable));
370     }
371 
372     /**
373      * Gets the substitution map.
374      *
375      * @return the substitution map.
376      */
377     public Map<String, ?> getSubstitutionMap() {
378         return substitutionMap;
379     }
380 
381     /**
382      * Tests whether a file was used to set the executable.
383      *
384      * @return true whether a file was used for setting the executable.
385      */
386     public boolean isFile() {
387         return isFile;
388     }
389 
390     /**
391      * Sets the substitutionMap to expand variables in the command line.
392      *
393      * @param substitutionMap the map
394      */
395     public void setSubstitutionMap(final Map<String, ?> substitutionMap) {
396         this.substitutionMap = substitutionMap;
397     }
398 
399     /**
400      * Cleans the executable string. The argument is trimmed and '/' and '\\' are replaced with the platform specific file separator char
401      *
402      * @param dirtyExecutable the executable.
403      * @return the platform-specific executable string.
404      * @throws NullPointerException     on null input.
405      * @throws IllegalArgumentException on empty input.
406      */
407     private String toCleanExecutable(final String dirtyExecutable) {
408         Objects.requireNonNull(dirtyExecutable, "dirtyExecutable");
409         if (dirtyExecutable.trim().isEmpty()) {
410             throw new IllegalArgumentException("Executable cannot be empty");
411         }
412         return StringUtils.fixFileSeparatorChar(dirtyExecutable);
413     }
414 
415     /**
416      * Stringify operator returns the command line as a string. Parameters are correctly quoted when containing a space or left untouched if the are already
417      * quoted.
418      *
419      * @return the command line as single string.
420      */
421     @Override
422     public String toString() {
423         return "[" + String.join(", ", toStrings()) + "]";
424     }
425 
426     /**
427      * Converts the command line as an array of strings.
428      *
429      * @return The command line as an string array.
430      */
431     public String[] toStrings() {
432         final String[] result = new String[arguments.size() + 1];
433         result[0] = getExecutable();
434         System.arraycopy(getArguments(), 0, result, 1, result.length - 1);
435         return result;
436     }
437 }