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