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    
019    package org.apache.commons.exec;
020    
021    import org.apache.commons.exec.util.StringUtils;
022    
023    import java.io.File;
024    import java.util.HashMap;
025    import java.util.Iterator;
026    import java.util.StringTokenizer;
027    import java.util.Vector;
028    import 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     */
034    public 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    }