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