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.  *      https://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.nio.file.Path;
  20. import java.util.ArrayList;
  21. import java.util.HashMap;
  22. import java.util.Map;
  23. import java.util.Objects;
  24. import java.util.StringTokenizer;
  25. import java.util.Vector;

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

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

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

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

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

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

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

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

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

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

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

  79.         return cl;
  80.     }

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

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

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

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

  142.         if (lastTokenHasBeenQuoted || current.length() != 0) {
  143.             list.add(current.toString());
  144.         }

  145.         if (state == inQuote || state == inDoubleQuote) {
  146.             throw new IllegalArgumentException("Unbalanced quotes in " + toProcess);
  147.         }

  148.         final String[] args = new String[list.size()];
  149.         return list.toArray(args);
  150.     }

  151.     /**
  152.      * The arguments of the command.
  153.      */
  154.     private final Vector<Argument> arguments = new Vector<>();

  155.     /**
  156.      * The program to execute.
  157.      */
  158.     private final String executable;

  159.     /**
  160.      * A map of name value pairs used to expand command line arguments.
  161.      */
  162.     private Map<String, ?> substitutionMap; // This can contain values other than Strings.

  163.     /**
  164.      * Tests whether a file was used to set the executable.
  165.      */
  166.     private final boolean isFile;

  167.     /**
  168.      * Copy constructor.
  169.      *
  170.      * @param other the instance to copy.
  171.      */
  172.     public CommandLine(final CommandLine other) {
  173.         this.executable = other.getExecutable();
  174.         this.isFile = other.isFile();
  175.         this.arguments.addAll(other.arguments);

  176.         if (other.getSubstitutionMap() != null) {
  177.             this.substitutionMap = new HashMap<>(other.getSubstitutionMap());
  178.         }
  179.     }

  180.     /**
  181.      * Constructs a command line without any arguments.
  182.      *
  183.      * @param executable the executable file.
  184.      */
  185.     public CommandLine(final File executable) {
  186.         this.isFile = true;
  187.         this.executable = toCleanExecutable(executable.getAbsolutePath());
  188.     }

  189.     /**
  190.      * Constructs a command line without any arguments.
  191.      *
  192.      * @param executable the executable file.
  193.      * @since 1.5.0
  194.      */
  195.     public CommandLine(final Path executable) {
  196.         this.isFile = true;
  197.         this.executable = toCleanExecutable(executable.toAbsolutePath().toString());
  198.     }

  199.     /**
  200.      * Constructs a command line without any arguments.
  201.      *
  202.      * @param executable the executable.
  203.      * @throws NullPointerException     on null input.
  204.      * @throws IllegalArgumentException on empty input.
  205.      */
  206.     public CommandLine(final String executable) {
  207.         this.isFile = false;
  208.         this.executable = toCleanExecutable(executable);
  209.     }

  210.     /**
  211.      * Add a single argument. Handles quoting.
  212.      *
  213.      * @param argument The argument to add.
  214.      * @return The command line itself.
  215.      * @throws IllegalArgumentException If argument contains both single and double quotes.
  216.      */
  217.     public CommandLine addArgument(final String argument) {
  218.         return addArgument(argument, true);
  219.     }

  220.     /**
  221.      * Add a single argument.
  222.      *
  223.      * @param argument      The argument to add.
  224.      * @param handleQuoting Add the argument with/without handling quoting.
  225.      * @return The command line itself.
  226.      */
  227.     public CommandLine addArgument(final String argument, final boolean handleQuoting) {

  228.         if (argument == null) {
  229.             return this;
  230.         }

  231.         // check if we can really quote the argument - if not throw an
  232.         // IllegalArgumentException
  233.         if (handleQuoting) {
  234.             StringUtils.quoteArgument(argument);
  235.         }

  236.         arguments.add(new Argument(argument, handleQuoting));
  237.         return this;
  238.     }

  239.     /**
  240.      * Add multiple arguments. Handles parsing of quotes and whitespace. Please note that the parsing can have undesired side-effects therefore it is
  241.      * recommended to build the command line incrementally.
  242.      *
  243.      * @param addArguments An string containing multiple arguments.
  244.      * @return The command line itself.
  245.      */
  246.     public CommandLine addArguments(final String addArguments) {
  247.         return addArguments(addArguments, true);
  248.     }

  249.     /**
  250.      * Add multiple arguments. Handles parsing of quotes and whitespace. Please note that the parsing can have undesired side-effects therefore it is
  251.      * recommended to build the command line incrementally.
  252.      *
  253.      * @param addArguments  An string containing multiple arguments.
  254.      * @param handleQuoting Add the argument with/without handling quoting.
  255.      * @return The command line itself.
  256.      */
  257.     public CommandLine addArguments(final String addArguments, final boolean handleQuoting) {
  258.         if (addArguments != null) {
  259.             final String[] argumentsArray = translateCommandline(addArguments);
  260.             addArguments(argumentsArray, handleQuoting);
  261.         }

  262.         return this;
  263.     }

  264.     /**
  265.      * Add multiple arguments. Handles parsing of quotes and whitespace.
  266.      *
  267.      * @param addArguments An array of arguments.
  268.      * @return The command line itself.
  269.      */
  270.     public CommandLine addArguments(final String[] addArguments) {
  271.         return addArguments(addArguments, true);
  272.     }

  273.     /**
  274.      * Add multiple arguments.
  275.      *
  276.      * @param addArguments  An array of arguments.
  277.      * @param handleQuoting Add the argument with/without handling quoting.
  278.      * @return The command line itself.
  279.      */
  280.     public CommandLine addArguments(final String[] addArguments, final boolean handleQuoting) {
  281.         if (addArguments != null) {
  282.             for (final String addArgument : addArguments) {
  283.                 addArgument(addArgument, handleQuoting);
  284.             }
  285.         }
  286.         return this;
  287.     }

  288.     /**
  289.      * Expand variables in a command line argument.
  290.      *
  291.      * @param argument the argument.
  292.      * @return the expanded string.
  293.      */
  294.     private String expandArgument(final String argument) {
  295.         final StringBuffer stringBuffer = StringUtils.stringSubstitution(argument, getSubstitutionMap(), true);
  296.         return stringBuffer.toString();
  297.     }

  298.     /**
  299.      * Gets the expanded and quoted command line arguments.
  300.      *
  301.      * @return The quoted arguments.
  302.      */
  303.     public String[] getArguments() {

  304.         Argument currArgument;
  305.         String expandedArgument;
  306.         final String[] result = new String[arguments.size()];

  307.         for (int i = 0; i < result.length; i++) {
  308.             currArgument = arguments.get(i);
  309.             expandedArgument = expandArgument(currArgument.getValue());
  310.             result[i] = currArgument.isHandleQuoting() ? StringUtils.quoteArgument(expandedArgument) : expandedArgument;
  311.         }

  312.         return result;
  313.     }

  314.     /**
  315.      * Gets the executable.
  316.      *
  317.      * @return The executable.
  318.      */
  319.     public String getExecutable() {
  320.         // Expand the executable and replace '/' and '\\' with the platform
  321.         // specific file separator char. This is safe here since we know
  322.         // that this is a platform specific command.
  323.         return StringUtils.fixFileSeparatorChar(expandArgument(executable));
  324.     }

  325.     /**
  326.      * Gets the substitution map.
  327.      *
  328.      * @return the substitution map.
  329.      */
  330.     public Map<String, ?> getSubstitutionMap() {
  331.         return substitutionMap;
  332.     }

  333.     /**
  334.      * Tests whether a file was used to set the executable.
  335.      *
  336.      * @return true whether a file was used for setting the executable.
  337.      */
  338.     public boolean isFile() {
  339.         return isFile;
  340.     }

  341.     /**
  342.      * Sets the substitutionMap to expand variables in the command line.
  343.      *
  344.      * @param substitutionMap the map
  345.      */
  346.     public void setSubstitutionMap(final Map<String, ?> substitutionMap) {
  347.         this.substitutionMap = substitutionMap;
  348.     }

  349.     /**
  350.      * Cleans the executable string. The argument is trimmed and '/' and '\\' are replaced with the platform specific file separator char
  351.      *
  352.      * @param dirtyExecutable the executable.
  353.      * @return the platform-specific executable string.
  354.      * @throws NullPointerException     on null input.
  355.      * @throws IllegalArgumentException on empty input.
  356.      */
  357.     private String toCleanExecutable(final String dirtyExecutable) {
  358.         Objects.requireNonNull(dirtyExecutable, "dirtyExecutable");
  359.         if (dirtyExecutable.trim().isEmpty()) {
  360.             throw new IllegalArgumentException("Executable cannot be empty");
  361.         }
  362.         return StringUtils.fixFileSeparatorChar(dirtyExecutable);
  363.     }

  364.     /**
  365.      * Stringify operator returns the command line as a string. Parameters are correctly quoted when containing a space or left untouched if the are already
  366.      * quoted.
  367.      *
  368.      * @return the command line as single string.
  369.      */
  370.     @Override
  371.     public String toString() {
  372.         return "[" + String.join(", ", toStrings()) + "]";
  373.     }

  374.     /**
  375.      * Converts the command line as an array of strings.
  376.      *
  377.      * @return The command line as an string array.
  378.      */
  379.     public String[] toStrings() {
  380.         final String[] result = new String[arguments.size() + 1];
  381.         result[0] = getExecutable();
  382.         System.arraycopy(getArguments(), 0, result, 1, result.length - 1);
  383.         return result;
  384.     }
  385. }