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