1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * https://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19
20 package org.apache.commons.exec;
21
22 import java.io.File;
23 import java.nio.file.Path;
24 import java.util.ArrayList;
25 import java.util.HashMap;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Objects;
29 import java.util.StringTokenizer;
30
31 import org.apache.commons.exec.util.StringUtils;
32
33 /**
34 * CommandLine objects help handling command lines specifying processes to execute. The class can be used to a command line by an application.
35 */
36 public class CommandLine {
37
38 /**
39 * Encapsulates a command line argument.
40 */
41 static final class Argument {
42
43 /**
44 * Argument value.
45 */
46 private final String value;
47
48 /**
49 * Whether to quote arguments.
50 */
51 private final boolean handleQuoting;
52
53 private Argument(final String value, final boolean handleQuoting) {
54 this.value = value.trim();
55 this.handleQuoting = handleQuoting;
56 }
57
58 private String getValue() {
59 return value;
60 }
61
62 private boolean isHandleQuoting() {
63 return handleQuoting;
64 }
65 }
66
67 /**
68 * Create a command line from a string.
69 *
70 * @param line the first element becomes the executable, the rest the arguments.
71 * @return the parsed command line.
72 * @throws IllegalArgumentException If line is null or all whitespace.
73 */
74 public static CommandLine parse(final String line) {
75 return parse(line, null);
76 }
77
78 /**
79 * Create a command line from a string.
80 *
81 * @param line the first element becomes the executable, the rest the arguments.
82 * @param substitutionMap the name/value pairs used for substitution.
83 * @return the parsed command line.
84 * @throws IllegalArgumentException If line is null or all whitespace.
85 */
86 public static CommandLine parse(final String line, final Map<String, ?> substitutionMap) {
87 if (line == null) {
88 throw new IllegalArgumentException("Command line cannot be null");
89 }
90 if (line.trim().isEmpty()) {
91 throw new IllegalArgumentException("Command line cannot be empty");
92 }
93 final String[] tmp = translateCommandline(line);
94 final CommandLine cl = new CommandLine(tmp[0]);
95 cl.setSubstitutionMap(substitutionMap);
96 for (int i = 1; i < tmp.length; i++) {
97 cl.addArgument(tmp[i]);
98 }
99 return cl;
100 }
101
102 /**
103 * Crack a command line.
104 *
105 * @param toProcess the command line to process.
106 * @return the command line broken into strings. An empty or null toProcess parameter results in a zero sized array.
107 */
108 private static String[] translateCommandline(final String toProcess) {
109 if (toProcess == null || toProcess.trim().isEmpty()) {
110 // no command? no string
111 return new String[0];
112 }
113 // parse with a simple finite state machine.
114 final int normal = 0;
115 final int inQuote = 1;
116 final int inDoubleQuote = 2;
117 int state = normal;
118 final StringTokenizer tok = new StringTokenizer(toProcess, "\"\' ", true);
119 final ArrayList<String> list = new ArrayList<>();
120 StringBuilder current = new StringBuilder();
121 boolean lastTokenHasBeenQuoted = false;
122 while (tok.hasMoreTokens()) {
123 final String nextTok = tok.nextToken();
124 switch (state) {
125 case inQuote:
126 if ("\'".equals(nextTok)) {
127 lastTokenHasBeenQuoted = true;
128 state = normal;
129 } else {
130 current.append(nextTok);
131 }
132 break;
133 case inDoubleQuote:
134 if ("\"".equals(nextTok)) {
135 lastTokenHasBeenQuoted = true;
136 state = normal;
137 } else {
138 current.append(nextTok);
139 }
140 break;
141 default:
142 switch (nextTok) {
143 case "\'":
144 state = inQuote;
145 break;
146 case "\"":
147 state = inDoubleQuote;
148 break;
149 case " ":
150 if (lastTokenHasBeenQuoted || current.length() != 0) {
151 list.add(current.toString());
152 current = new StringBuilder();
153 }
154 break;
155 default:
156 current.append(nextTok);
157 break;
158 }
159 lastTokenHasBeenQuoted = false;
160 break;
161 }
162 }
163 if (lastTokenHasBeenQuoted || current.length() != 0) {
164 list.add(current.toString());
165 }
166 if (state == inQuote || state == inDoubleQuote) {
167 throw new IllegalArgumentException("Unbalanced quotes in " + toProcess);
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 List<Argument> arguments = new ArrayList<>();
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 if (other.getSubstitutionMap() != null) {
203 this.substitutionMap = new HashMap<>(other.getSubstitutionMap());
204 }
205 }
206
207 /**
208 * Constructs a command line without any arguments.
209 *
210 * @param executable the executable file.
211 */
212 public CommandLine(final File executable) {
213 this.isFile = true;
214 this.executable = toCleanExecutable(executable.getAbsolutePath());
215 }
216
217 /**
218 * Constructs a command line without any arguments.
219 *
220 * @param executable the executable file.
221 * @since 1.5.0
222 */
223 public CommandLine(final Path executable) {
224 this.isFile = true;
225 this.executable = toCleanExecutable(executable.toAbsolutePath().toString());
226 }
227
228 /**
229 * Constructs a command line without any arguments.
230 *
231 * @param executable the executable.
232 * @throws NullPointerException on null input.
233 * @throws IllegalArgumentException on empty input.
234 */
235 public CommandLine(final String executable) {
236 this.isFile = false;
237 this.executable = toCleanExecutable(executable);
238 }
239
240 /**
241 * Add a single argument. Handles quoting.
242 *
243 * @param argument The argument to add.
244 * @return The command line itself.
245 * @throws IllegalArgumentException If argument contains both single and double quotes.
246 */
247 public CommandLine addArgument(final String argument) {
248 return addArgument(argument, true);
249 }
250
251 /**
252 * Add a single argument.
253 *
254 * @param argument The argument to add.
255 * @param handleQuoting Add the argument with/without handling quoting.
256 * @return The command line itself.
257 */
258 public CommandLine addArgument(final String argument, final boolean handleQuoting) {
259 if (argument == null) {
260 return this;
261 }
262 // check if we can really quote the argument - if not throw an
263 // IllegalArgumentException
264 if (handleQuoting) {
265 StringUtils.quoteArgument(argument);
266 }
267 arguments.add(new Argument(argument, handleQuoting));
268 return this;
269 }
270
271 /**
272 * Add multiple arguments. Handles parsing of quotes and whitespace. Please note that the parsing can have undesired side-effects therefore it is
273 * recommended to build the command line incrementally.
274 *
275 * @param addArguments A string containing multiple arguments.
276 * @return The command line itself.
277 */
278 public CommandLine addArguments(final String addArguments) {
279 return addArguments(addArguments, true);
280 }
281
282 /**
283 * Add multiple arguments. Handles parsing of quotes and whitespace. Please note that the parsing can have undesired side-effects therefore it is
284 * recommended to build the command line incrementally.
285 *
286 * @param addArguments A string containing multiple arguments.
287 * @param handleQuoting Add the argument with/without handling quoting.
288 * @return The command line itself.
289 */
290 public CommandLine addArguments(final String addArguments, final boolean handleQuoting) {
291 if (addArguments != null) {
292 final String[] argumentsArray = translateCommandline(addArguments);
293 addArguments(argumentsArray, handleQuoting);
294 }
295 return this;
296 }
297
298 /**
299 * Add multiple arguments. Handles parsing of quotes and whitespace.
300 *
301 * @param addArguments An array of arguments.
302 * @return The command line itself.
303 */
304 public CommandLine addArguments(final String[] addArguments) {
305 return addArguments(addArguments, true);
306 }
307
308 /**
309 * Add multiple arguments.
310 *
311 * @param addArguments An array of arguments.
312 * @param handleQuoting Add the argument with/without handling quoting.
313 * @return The command line itself.
314 */
315 public CommandLine addArguments(final String[] addArguments, final boolean handleQuoting) {
316 if (addArguments != null) {
317 for (final String addArgument : addArguments) {
318 addArgument(addArgument, handleQuoting);
319 }
320 }
321 return this;
322 }
323
324 /**
325 * Expand variables in a command line argument.
326 *
327 * @param argument the argument.
328 * @return the expanded string.
329 */
330 private String expandArgument(final String argument) {
331 final StringBuffer stringBuffer = StringUtils.stringSubstitution(argument, getSubstitutionMap(), true);
332 return stringBuffer.toString();
333 }
334
335 /**
336 * Gets the expanded and quoted command line arguments.
337 *
338 * @return The quoted arguments.
339 */
340 public String[] getArguments() {
341 Argument currArgument;
342 String expandedArgument;
343 final String[] result = new String[arguments.size()];
344 for (int i = 0; i < result.length; i++) {
345 currArgument = arguments.get(i);
346 expandedArgument = expandArgument(currArgument.getValue());
347 result[i] = currArgument.isHandleQuoting() ? StringUtils.quoteArgument(expandedArgument) : expandedArgument;
348 }
349 return result;
350 }
351
352 /**
353 * Gets the executable.
354 *
355 * @return The executable.
356 */
357 public String getExecutable() {
358 // Expand the executable and replace '/' and '\\' with the platform
359 // specific file separator char. This is safe here since we know
360 // that this is a platform specific command.
361 return StringUtils.fixFileSeparatorChar(expandArgument(executable));
362 }
363
364 /**
365 * Gets the substitution map.
366 *
367 * @return the substitution map.
368 */
369 public Map<String, ?> getSubstitutionMap() {
370 return substitutionMap;
371 }
372
373 /**
374 * Tests whether a file was used to set the executable.
375 *
376 * @return true whether a file was used for setting the executable.
377 */
378 public boolean isFile() {
379 return isFile;
380 }
381
382 /**
383 * Sets the substitutionMap to expand variables in the command line.
384 *
385 * @param substitutionMap the map
386 */
387 public void setSubstitutionMap(final Map<String, ?> substitutionMap) {
388 this.substitutionMap = substitutionMap;
389 }
390
391 /**
392 * Cleans the executable string. The argument is trimmed and '/' and '\\' are replaced with the platform specific file separator char
393 *
394 * @param dirtyExecutable the executable.
395 * @return the platform-specific executable string.
396 * @throws NullPointerException on null input.
397 * @throws IllegalArgumentException on empty input.
398 */
399 private String toCleanExecutable(final String dirtyExecutable) {
400 Objects.requireNonNull(dirtyExecutable, "dirtyExecutable");
401 if (dirtyExecutable.trim().isEmpty()) {
402 throw new IllegalArgumentException("Executable cannot be empty");
403 }
404 return StringUtils.fixFileSeparatorChar(dirtyExecutable);
405 }
406
407 /**
408 * Stringify operator returns the command line as a string.
409 * Parameters are correctly quoted when containing a space or left untouched if they are already
410 * quoted.
411 *
412 * @return the command line as single string.
413 */
414 @Override
415 public String toString() {
416 return "[" + String.join(", ", toStrings()) + "]";
417 }
418
419 /**
420 * Converts the command line as an array of strings.
421 *
422 * @return The command line as a string array.
423 */
424 public String[] toStrings() {
425 final String[] result = new String[arguments.size() + 1];
426 result[0] = getExecutable();
427 System.arraycopy(getArguments(), 0, result, 1, result.length - 1);
428 return result;
429 }
430 }