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