001/* 
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 *  contributor license agreements.  See the NOTICE file distributed with
004 *  this work for additional information regarding copyright ownership.
005 *  The ASF licenses this file to You under the Apache License, Version 2.0
006 *  (the "License"); you may not use this file except in compliance with
007 *  the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 *  Unless required by applicable law or agreed to in writing, software
012 *  distributed under the License is distributed on an "AS IS" BASIS,
013 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 *  See the License for the specific language governing permissions and
015 *  limitations under the License.
016 *
017 */
018
019package org.apache.commons.exec;
020
021import org.apache.commons.exec.util.StringUtils;
022
023import java.io.File;
024import java.util.HashMap;
025import java.util.Iterator;
026import java.util.StringTokenizer;
027import java.util.Vector;
028import java.util.Map;
029
030/**
031 * CommandLine objects help handling command lines specifying processes to
032 * execute. The class can be used to a command line by an application.
033 */
034public class CommandLine {
035
036    /**
037     * The arguments of the command.
038     */
039    private final Vector arguments = new Vector();
040
041    /**
042     * The program to execute.
043     */
044    private final String executable;
045
046    /**
047     * A map of name value pairs used to expand command line arguments
048     */
049    private Map substitutionMap;
050
051    /**
052     * Was a file being used to set the executable?
053     */
054    private final boolean isFile;
055
056    /**
057     * Create a command line from a string.
058     * 
059     * @param line the first element becomes the executable, the rest the arguments
060     * @return the parsed command line
061     * @throws IllegalArgumentException If line is null or all whitespace
062     */
063    public static CommandLine parse(final String line) {
064        return parse(line, null);
065    }
066
067    /**
068     * Create a command line from a string.
069     *
070     * @param line the first element becomes the executable, the rest the arguments
071     * @param substitutionMap the name/value pairs used for substitution
072     * @return the parsed command line
073     * @throws IllegalArgumentException If line is null or all whitespace
074     */
075    public static CommandLine parse(final String line, Map substitutionMap) {
076                
077        if (line == null) {
078            throw new IllegalArgumentException("Command line can not be null");
079        } else if (line.trim().length() == 0) {
080            throw new IllegalArgumentException("Command line can not be empty");
081        } else {
082            String[] tmp = translateCommandline(line);
083
084            CommandLine cl = new CommandLine(tmp[0]);
085            cl.setSubstitutionMap(substitutionMap);
086            for (int i = 1; i < tmp.length; i++) {
087                cl.addArgument(tmp[i]);
088            }
089
090            return cl;
091        }
092    }
093
094    /**
095     * Create a command line without any arguments.
096     *
097     * @param executable the executable
098     */
099    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}