    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}