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