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