    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    }