CommandLine.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  *  contributor license agreements.  See the NOTICE file distributed with
  4.  *  this work for additional information regarding copyright ownership.
  5.  *  The ASF licenses this file to You under the Apache License, Version 2.0
  6.  *  (the "License"); you may not use this file except in compliance with
  7.  *  the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  *  Unless required by applicable law or agreed to in writing, software
  12.  *  distributed under the License is distributed on an "AS IS" BASIS,
  13.  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  *  See the License for the specific language governing permissions and
  15.  *  limitations under the License.
  16.  */

  17. package org.apache.commons.exec;

  18. import java.io.File;
  19. import java.util.ArrayList;
  20. import java.util.HashMap;
  21. import java.util.Map;
  22. import java.util.Objects;
  23. import java.util.StringTokenizer;
  24. import java.util.Vector;

  25. import org.apache.commons.exec.util.StringUtils;

  26. /**
  27.  * CommandLine objects help handling command lines specifying processes to execute. The class can be used to a command line by an application.
  28.  */
  29. public class CommandLine {

  30.     /**
  31.      * Encapsulates a command line argument.
  32.      */
  33.     static final class Argument {

  34.         private final String value;
  35.         private final boolean handleQuoting;

  36.         private Argument(final String value, final boolean handleQuoting) {
  37.             this.value = value.trim();
  38.             this.handleQuoting = handleQuoting;
  39.         }

  40.         private String getValue() {
  41.             return value;
  42.         }

  43.         private boolean isHandleQuoting() {
  44.             return handleQuoting;
  45.         }
  46.     }

  47.     /**
  48.      * Create a command line from a string.
  49.      *
  50.      * @param line the first element becomes the executable, the rest the arguments.
  51.      * @return the parsed command line.
  52.      * @throws IllegalArgumentException If line is null or all whitespace.
  53.      */
  54.     public static CommandLine parse(final String line) {
  55.         return parse(line, null);
  56.     }

  57.     /**
  58.      * Create a command line from a string.
  59.      *
  60.      * @param line            the first element becomes the executable, the rest the arguments.
  61.      * @param substitutionMap the name/value pairs used for substitution.
  62.      * @return the parsed command line.
  63.      * @throws IllegalArgumentException If line is null or all whitespace.
  64.      */
  65.     public static CommandLine parse(final String line, final Map<String, ?> substitutionMap) {

  66.         if (line == null) {
  67.             throw new IllegalArgumentException("Command line can not be null");
  68.         }
  69.         if (line.trim().isEmpty()) {
  70.             throw new IllegalArgumentException("Command line can not be empty");
  71.         }
  72.         final String[] tmp = translateCommandline(line);

  73.         final CommandLine cl = new CommandLine(tmp[0]);
  74.         cl.setSubstitutionMap(substitutionMap);
  75.         for (int i = 1; i < tmp.length; i++) {
  76.             cl.addArgument(tmp[i]);
  77.         }

  78.         return cl;
  79.     }

  80.     /**
  81.      * Crack a command line.
  82.      *
  83.      * @param toProcess the command line to process.
  84.      * @return the command line broken into strings. An empty or null toProcess parameter results in a zero sized array.
  85.      */
  86.     private static String[] translateCommandline(final String toProcess) {
  87.         if (toProcess == null || toProcess.trim().isEmpty()) {
  88.             // no command? no string
  89.             return new String[0];
  90.         }

  91.         // parse with a simple finite state machine.

  92.         final int normal = 0;
  93.         final int inQuote = 1;
  94.         final int inDoubleQuote = 2;
  95.         int state = normal;
  96.         final StringTokenizer tok = new StringTokenizer(toProcess, "\"\' ", true);
  97.         final ArrayList<String> list = new ArrayList<>();
  98.         StringBuilder current = new StringBuilder();
  99.         boolean lastTokenHasBeenQuoted = false;

  100.         while (tok.hasMoreTokens()) {
  101.             final String nextTok = tok.nextToken();
  102.             switch (state) {
  103.             case inQuote:
  104.                 if ("\'".equals(nextTok)) {
  105.                     lastTokenHasBeenQuoted = true;
  106.                     state = normal;
  107.                 } else {
  108.                     current.append(nextTok);
  109.                 }
  110.                 break;
  111.             case inDoubleQuote:
  112.                 if ("\"".equals(nextTok)) {
  113.                     lastTokenHasBeenQuoted = true;
  114.                     state = normal;
  115.                 } else {
  116.                     current.append(nextTok);
  117.                 }
  118.                 break;
  119.             default:
  120.                 if ("\'".equals(nextTok)) {
  121.                     state = inQuote;
  122.                 } else if ("\"".equals(nextTok)) {
  123.                     state = inDoubleQuote;
  124.                 } else if (" ".equals(nextTok)) {
  125.                     if (lastTokenHasBeenQuoted || current.length() != 0) {
  126.                         list.add(current.toString());
  127.                         current = new StringBuilder();
  128.                     }
  129.                 } else {
  130.                     current.append(nextTok);
  131.                 }
  132.                 lastTokenHasBeenQuoted = false;
  133.                 break;
  134.             }
  135.         }

  136.         if (lastTokenHasBeenQuoted || current.length() != 0) {
  137.             list.add(current.toString());
  138.         }

  139.         if (state == inQuote || state == inDoubleQuote) {
  140.             throw new IllegalArgumentException("Unbalanced quotes in " + toProcess);
  141.         }

  142.         final String[] args = new String[list.size()];
  143.         return list.toArray(args);
  144.     }

  145.     /**
  146.      * The arguments of the command.
  147.      */
  148.     private final Vector<Argument> arguments = new Vector<>();

  149.     /**
  150.      * The program to execute.
  151.      */
  152.     private final String executable;

  153.     /**
  154.      * A map of name value pairs used to expand command line arguments.
  155.      */
  156.     private Map<String, ?> substitutionMap; // N.B. This can contain values other than Strings.

  157.     /**
  158.      * Tests whether a file was used to set the executable.
  159.      */
  160.     private final boolean isFile;

  161.     /**
  162.      * Copy constructor.
  163.      *
  164.      * @param other the instance to copy.
  165.      */
  166.     public CommandLine(final CommandLine other) {
  167.         this.executable = other.getExecutable();
  168.         this.isFile = other.isFile();
  169.         this.arguments.addAll(other.arguments);

  170.         if (other.getSubstitutionMap() != null) {
  171.             this.substitutionMap = new HashMap<>(other.getSubstitutionMap());
  172.         }
  173.     }

  174.     /**
  175.      * Create a command line without any arguments.
  176.      *
  177.      * @param executable the executable file.
  178.      */
  179.     public CommandLine(final File executable) {
  180.         this.isFile = true;
  181.         this.executable = toCleanExecutable(executable.getAbsolutePath());
  182.     }

  183.     /**
  184.      * Create a command line without any arguments.
  185.      *
  186.      * @param executable the executable.
  187.      * @throws NullPointerException     on null input.
  188.      * @throws IllegalArgumentException on empty input.
  189.      */
  190.     public CommandLine(final String executable) {
  191.         this.isFile = false;
  192.         this.executable = toCleanExecutable(executable);
  193.     }

  194.     /**
  195.      * Add a single argument. Handles quoting.
  196.      *
  197.      * @param argument The argument to add.
  198.      * @return The command line itself.
  199.      * @throws IllegalArgumentException If argument contains both single and double quotes.
  200.      */
  201.     public CommandLine addArgument(final String argument) {
  202.         return addArgument(argument, true);
  203.     }

  204.     /**
  205.      * Add a single argument.
  206.      *
  207.      * @param argument      The argument to add.
  208.      * @param handleQuoting Add the argument with/without handling quoting.
  209.      * @return The command line itself.
  210.      */
  211.     public CommandLine addArgument(final String argument, final boolean handleQuoting) {

  212.         if (argument == null) {
  213.             return this;
  214.         }

  215.         // check if we can really quote the argument - if not throw an
  216.         // IllegalArgumentException
  217.         if (handleQuoting) {
  218.             StringUtils.quoteArgument(argument);
  219.         }

  220.         arguments.add(new Argument(argument, handleQuoting));
  221.         return this;
  222.     }

  223.     /**
  224.      * Add multiple arguments. Handles parsing of quotes and whitespace. Please note that the parsing can have undesired side-effects therefore it is
  225.      * recommended to build the command line incrementally.
  226.      *
  227.      * @param addArguments An string containing multiple arguments.
  228.      * @return The command line itself.
  229.      */
  230.     public CommandLine addArguments(final String addArguments) {
  231.         return addArguments(addArguments, true);
  232.     }

  233.     /**
  234.      * Add multiple arguments. Handles parsing of quotes and whitespace. Please note that the parsing can have undesired side-effects therefore it is
  235.      * recommended to build the command line incrementally.
  236.      *
  237.      * @param addArguments  An string containing multiple arguments.
  238.      * @param handleQuoting Add the argument with/without handling quoting.
  239.      * @return The command line itself.
  240.      */
  241.     public CommandLine addArguments(final String addArguments, final boolean handleQuoting) {
  242.         if (addArguments != null) {
  243.             final String[] argumentsArray = translateCommandline(addArguments);
  244.             addArguments(argumentsArray, handleQuoting);
  245.         }

  246.         return this;
  247.     }

  248.     /**
  249.      * Add multiple arguments. Handles parsing of quotes and whitespace.
  250.      *
  251.      * @param addArguments An array of arguments.
  252.      * @return The command line itself.
  253.      */
  254.     public CommandLine addArguments(final String[] addArguments) {
  255.         return addArguments(addArguments, true);
  256.     }

  257.     /**
  258.      * Add multiple arguments.
  259.      *
  260.      * @param addArguments  An array of arguments.
  261.      * @param handleQuoting Add the argument with/without handling quoting.
  262.      * @return The command line itself.
  263.      */
  264.     public CommandLine addArguments(final String[] addArguments, final boolean handleQuoting) {
  265.         if (addArguments != null) {
  266.             for (final String addArgument : addArguments) {
  267.                 addArgument(addArgument, handleQuoting);
  268.             }
  269.         }
  270.         return this;
  271.     }

  272.     /**
  273.      * Expand variables in a command line argument.
  274.      *
  275.      * @param argument the argument.
  276.      * @return the expanded string.
  277.      */
  278.     private String expandArgument(final String argument) {
  279.         final StringBuffer stringBuffer = StringUtils.stringSubstitution(argument, getSubstitutionMap(), true);
  280.         return stringBuffer.toString();
  281.     }

  282.     /**
  283.      * Gets the expanded and quoted command line arguments.
  284.      *
  285.      * @return The quoted arguments.
  286.      */
  287.     public String[] getArguments() {

  288.         Argument currArgument;
  289.         String expandedArgument;
  290.         final String[] result = new String[arguments.size()];

  291.         for (int i = 0; i < result.length; i++) {
  292.             currArgument = arguments.get(i);
  293.             expandedArgument = expandArgument(currArgument.getValue());
  294.             result[i] = currArgument.isHandleQuoting() ? StringUtils.quoteArgument(expandedArgument) : expandedArgument;
  295.         }

  296.         return result;
  297.     }

  298.     /**
  299.      * Gets the executable.
  300.      *
  301.      * @return The executable.
  302.      */
  303.     public String getExecutable() {
  304.         // Expand the executable and replace '/' and '\\' with the platform
  305.         // specific file separator char. This is safe here since we know
  306.         // that this is a platform specific command.
  307.         return StringUtils.fixFileSeparatorChar(expandArgument(executable));
  308.     }

  309.     /**
  310.      * Gets the substitution map.
  311.      *
  312.      * @return the substitution map.
  313.      */
  314.     public Map<String, ?> getSubstitutionMap() {
  315.         return substitutionMap;
  316.     }

  317.     /**
  318.      * Tests whether a file was used to set the executable.
  319.      *
  320.      * @return true whether a file was used for setting the executable.
  321.      */
  322.     public boolean isFile() {
  323.         return isFile;
  324.     }

  325.     /**
  326.      * Sets the substitutionMap to expand variables in the command line.
  327.      *
  328.      * @param substitutionMap the map
  329.      */
  330.     public void setSubstitutionMap(final Map<String, ?> substitutionMap) {
  331.         this.substitutionMap = substitutionMap;
  332.     }

  333.     /**
  334.      * Cleans the executable string. The argument is trimmed and '/' and '\\' are replaced with the platform specific file separator char
  335.      *
  336.      * @param dirtyExecutable the executable.
  337.      * @return the platform-specific executable string.
  338.      * @throws NullPointerException     on null input.
  339.      * @throws IllegalArgumentException on empty input.
  340.      */
  341.     private String toCleanExecutable(final String dirtyExecutable) {
  342.         Objects.requireNonNull(dirtyExecutable, "dirtyExecutable");
  343.         if (dirtyExecutable.trim().isEmpty()) {
  344.             throw new IllegalArgumentException("Executable can not be empty");
  345.         }
  346.         return StringUtils.fixFileSeparatorChar(dirtyExecutable);
  347.     }

  348.     /**
  349.      * Stringify operator returns the command line as a string. Parameters are correctly quoted when containing a space or left untouched if the are already
  350.      * quoted.
  351.      *
  352.      * @return the command line as single string.
  353.      */
  354.     @Override
  355.     public String toString() {
  356.         return "[" + String.join(", ", toStrings()) + "]";
  357.     }

  358.     /**
  359.      * Converts the command line as an array of strings.
  360.      *
  361.      * @return The command line as an string array.
  362.      */
  363.     public String[] toStrings() {
  364.         final String[] result = new String[arguments.size() + 1];
  365.         result[0] = getExecutable();
  366.         System.arraycopy(getArguments(), 0, result, 1, result.length - 1);
  367.         return result;
  368.     }
  369. }