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