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 java.io.File;
022import java.util.ArrayList;
023import java.util.HashMap;
024import java.util.Iterator;
025import java.util.Map;
026import java.util.StringTokenizer;
027import java.util.Vector;
028
029import org.apache.commons.exec.util.StringUtils;
030
031/**
032 * CommandLine objects help handling command lines specifying processes to
033 * execute. The class can be used to a command line by an application.
034 *
035 * @version $Id: CommandLine.java 1613094 2014-07-24 12:20:14Z ggregory $
036 */
037public class CommandLine {
038
039    /**
040     * The arguments of the command.
041     */
042    private final Vector<Argument> arguments = new Vector<Argument>();
043
044    /**
045     * The program to execute.
046     */
047    private final String executable;
048
049    /**
050     * A map of name value pairs used to expand command line arguments
051     */
052    private Map<String, ?> substitutionMap; // N.B. This can contain values other than Strings
053
054    /**
055     * Was a file being used to set the executable?
056     */
057    private final boolean isFile;
058
059    /**
060     * Create a command line from a string.
061     * 
062     * @param line the first element becomes the executable, the rest the arguments
063     * @return the parsed command line
064     * @throws IllegalArgumentException If line is null or all whitespace
065     */
066    public static CommandLine parse(final String line) {
067        return parse(line, null);
068    }
069
070    /**
071     * Create a command line from a string.
072     *
073     * @param line the first element becomes the executable, the rest the arguments
074     * @param substitutionMap the name/value pairs used for substitution
075     * @return the parsed command line
076     * @throws IllegalArgumentException If line is null or all whitespace
077     */
078    public static CommandLine parse(final String line, final Map<String, ?> substitutionMap) {
079                
080        if (line == null) {
081            throw new IllegalArgumentException("Command line can not be null");
082        } else if (line.trim().length() == 0) {
083            throw new IllegalArgumentException("Command line can not be empty");
084        } else {
085            final String[] tmp = translateCommandline(line);
086
087            final CommandLine cl = new CommandLine(tmp[0]);
088            cl.setSubstitutionMap(substitutionMap);
089            for (int i = 1; i < tmp.length; i++) {
090                cl.addArgument(tmp[i]);
091            }
092
093            return cl;
094        }
095    }
096
097    /**
098     * Create a command line without any arguments.
099     *
100     * @param executable the executable
101     */
102    public CommandLine(final String executable) {
103        this.isFile=false;
104        this.executable=toCleanExecutable(executable);
105    }
106
107    /**
108     * Create a command line without any arguments.
109     *
110     * @param  executable the executable file
111     */
112    public CommandLine(final File executable) {
113        this.isFile=true;
114        this.executable=toCleanExecutable(executable.getAbsolutePath());
115    }
116
117    /**
118     * Copy constructor.
119     *
120     * @param other the instance to copy
121     */
122    public CommandLine(final CommandLine other)
123    {
124        this.executable = other.getExecutable();
125        this.isFile = other.isFile();
126        this.arguments.addAll(other.arguments);
127
128        if (other.getSubstitutionMap() != null)
129        {
130            final Map<String, Object> omap = new HashMap<String, Object>();
131            this.substitutionMap = omap;
132            final Iterator<String> iterator = other.substitutionMap.keySet().iterator();
133            while (iterator.hasNext())
134            {
135                final String key = iterator.next();
136                omap.put(key, other.getSubstitutionMap().get(key));
137            }
138        }
139    }
140
141    /**
142     * Returns the executable.
143     * 
144     * @return The executable
145     */
146    public String getExecutable() {
147        // Expand the executable and replace '/' and '\\' with the platform
148        // specific file separator char. This is safe here since we know
149        // that this is a platform specific command.
150        return StringUtils.fixFileSeparatorChar(expandArgument(executable));
151    }
152
153    /**
154     * Was a file being used to set the executable?
155     *
156     * @return true if a file was used for setting the executable 
157     */
158    public boolean isFile() {
159        return isFile;
160    }
161
162    /**
163     * Add multiple arguments. Handles parsing of quotes and whitespace.
164     * 
165     * @param addArguments An array of arguments
166     * @return The command line itself
167     */
168    public CommandLine addArguments(final String[] addArguments) {
169        return this.addArguments(addArguments, true);
170    }
171
172    /**
173     * Add multiple arguments.
174     *
175     * @param addArguments An array of arguments
176     * @param handleQuoting Add the argument with/without handling quoting
177     * @return The command line itself
178     */
179    public CommandLine addArguments(final String[] addArguments, final boolean handleQuoting) {
180        if (addArguments != null) {
181            for (final String addArgument : addArguments) {
182                addArgument(addArgument, handleQuoting);
183            }
184        }
185
186        return this;
187    }
188
189    /**
190     * Add multiple arguments. Handles parsing of quotes and whitespace.
191     * Please note that the parsing can have undesired side-effects therefore
192     * it is recommended to build the command line incrementally.
193     * 
194     * @param addArguments An string containing multiple arguments. 
195     * @return The command line itself
196     */
197    public CommandLine addArguments(final String addArguments) {
198        return this.addArguments(addArguments, true);
199    }
200
201    /**
202     * Add multiple arguments. Handles parsing of quotes and whitespace.
203     * Please note that the parsing can have undesired side-effects therefore
204     * it is recommended to build the command line incrementally.
205     *
206     * @param addArguments An string containing multiple arguments.
207     * @param handleQuoting Add the argument with/without handling quoting
208     * @return The command line itself
209     */
210    public CommandLine addArguments(final String addArguments, final boolean handleQuoting) {
211        if (addArguments != null) {
212            final String[] argumentsArray = translateCommandline(addArguments);
213            addArguments(argumentsArray, handleQuoting);
214        }
215
216        return this;
217    }
218
219    /**
220     * Add a single argument. Handles quoting.
221     *
222     * @param argument The argument to add
223     * @return The command line itself
224     * @throws IllegalArgumentException If argument contains both single and double quotes
225     */
226    public CommandLine addArgument(final String argument) {
227        return this.addArgument(argument, true);
228    }
229
230   /**
231    * Add a single argument.
232    *
233    * @param argument The argument to add
234    * @param handleQuoting Add the argument with/without handling quoting
235    * @return The command line itself
236    */
237   public CommandLine addArgument(final String argument, final boolean handleQuoting) {
238
239       if (argument == null)
240       {
241           return this;
242       }
243
244       // check if we can really quote the argument - if not throw an
245       // IllegalArgumentException
246       if (handleQuoting)
247       {
248           StringUtils.quoteArgument(argument);
249       }
250
251       arguments.add(new Argument(argument, handleQuoting));
252       return this;
253   }
254
255    /**
256     * Returns the expanded and quoted command line arguments.
257     *  
258     * @return The quoted arguments
259     */
260    public String[] getArguments() {
261
262        Argument currArgument;
263        String expandedArgument;
264        final String[] result = new String[arguments.size()];
265
266        for (int i=0; i<result.length; i++) {
267            currArgument = arguments.get(i);
268            expandedArgument = expandArgument(currArgument.getValue());
269            result[i] = currArgument.isHandleQuoting() ? StringUtils.quoteArgument(expandedArgument) : expandedArgument;
270        }
271
272        return result;
273    }
274
275    /**
276     * @return the substitution map
277     */
278    public Map<String, ?> getSubstitutionMap() {
279        return substitutionMap;
280    }
281
282    /**
283     * Set the substitutionMap to expand variables in the
284     * command line.
285     * 
286     * @param substitutionMap the map
287     */
288    public void setSubstitutionMap(final Map<String, ?> substitutionMap) {
289        this.substitutionMap = substitutionMap;
290    }
291
292    /**
293     * Returns the command line as an array of strings.
294     *
295     * @return The command line as an string array
296     */
297    public String[] toStrings() {
298        final String[] result = new String[arguments.size() + 1];
299        result[0] = this.getExecutable();
300        System.arraycopy(getArguments(), 0, result, 1, result.length-1);
301        return result;
302    }
303
304    /**
305     * Stringify operator returns the command line as a string.
306     * Parameters are correctly quoted when containing a space or
307     * left untouched if the are already quoted. 
308     *
309     * @return the command line as single string
310     */
311    @Override
312    public String toString() {
313        return "[" + StringUtils.toString(toStrings(), ", ") + "]";
314    }
315
316    // --- Implementation ---------------------------------------------------
317
318    /**
319     * Expand variables in a command line argument.
320     *
321     * @param argument the argument
322     * @return the expanded string
323     */
324    private String expandArgument(final String argument) {
325        final StringBuffer stringBuffer = StringUtils.stringSubstitution(argument, this.getSubstitutionMap(), true);
326        return stringBuffer.toString();
327    }
328
329    /**
330     * Crack a command line.
331     *
332     * @param toProcess
333     *            the command line to process
334     * @return the command line broken into strings. An empty or null toProcess
335     *         parameter results in a zero sized array
336     */
337    private static String[] translateCommandline(final String toProcess) {
338        if (toProcess == null || toProcess.length() == 0) {
339            // no command? no string
340            return new String[0];
341        }
342
343        // parse with a simple finite state machine
344
345        final int normal = 0;
346        final int inQuote = 1;
347        final int inDoubleQuote = 2;
348        int state = normal;
349        final StringTokenizer tok = new StringTokenizer(toProcess, "\"\' ", true);
350        final ArrayList<String> list = new ArrayList<String>();
351        StringBuilder current = new StringBuilder();
352        boolean lastTokenHasBeenQuoted = false;
353
354        while (tok.hasMoreTokens()) {
355            final String nextTok = tok.nextToken();
356            switch (state) {
357            case inQuote:
358                if ("\'".equals(nextTok)) {
359                    lastTokenHasBeenQuoted = true;
360                    state = normal;
361                } else {
362                    current.append(nextTok);
363                }
364                break;
365            case inDoubleQuote:
366                if ("\"".equals(nextTok)) {
367                    lastTokenHasBeenQuoted = true;
368                    state = normal;
369                } else {
370                    current.append(nextTok);
371                }
372                break;
373            default:
374                if ("\'".equals(nextTok)) {
375                    state = inQuote;
376                } else if ("\"".equals(nextTok)) {
377                    state = inDoubleQuote;
378                } else if (" ".equals(nextTok)) {
379                    if (lastTokenHasBeenQuoted || current.length() != 0) {
380                        list.add(current.toString());
381                        current = new StringBuilder();
382                    }
383                } else {
384                    current.append(nextTok);
385                }
386                lastTokenHasBeenQuoted = false;
387                break;
388            }
389        }
390
391        if (lastTokenHasBeenQuoted || current.length() != 0) {
392            list.add(current.toString());
393        }
394
395        if (state == inQuote || state == inDoubleQuote) {
396            throw new IllegalArgumentException("Unbalanced quotes in "
397                    + toProcess);
398        }
399
400        final String[] args = new String[list.size()];
401        return list.toArray(args);
402    }
403
404    /**
405     * Cleans the executable string. The argument is trimmed and '/' and '\\' are
406     * replaced with the platform specific file separator char
407     *
408     * @param dirtyExecutable the executable
409     * @return the platform-specific executable string
410     */
411    private String toCleanExecutable(final String dirtyExecutable) {
412        if (dirtyExecutable == null) {
413            throw new IllegalArgumentException("Executable can not be null");
414        } else if (dirtyExecutable.trim().length() == 0) {
415            throw new IllegalArgumentException("Executable can not be empty");
416        } else {
417            return StringUtils.fixFileSeparatorChar(dirtyExecutable);
418        }
419    }
420
421    /**
422     * Encapsulates a command line argument.
423     */
424    class Argument {
425
426        private final String value;
427        private final boolean handleQuoting;
428
429        private Argument(final String value, final boolean handleQuoting)
430        {
431            this.value = value.trim();
432            this.handleQuoting = handleQuoting;
433        }
434
435        private String getValue()
436        {
437            return value;
438        }
439
440        private boolean isHandleQuoting()
441        {
442            return handleQuoting;
443        }
444    }
445}