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.HashMap;
025 import java.util.Iterator;
026 import java.util.StringTokenizer;
027 import java.util.Vector;
028 import java.util.Map;
029
030 /**
031 * CommandLine objects help handling command lines specifying processes to
032 * execute. The class can be used to a command line by an application.
033 */
034 public class CommandLine {
035
036 /**
037 * The arguments of the command.
038 */
039 private final Vector arguments = new Vector();
040
041 /**
042 * The program to execute.
043 */
044 private final String executable;
045
046 /**
047 * A map of name value pairs used to expand command line arguments
048 */
049 private Map substitutionMap;
050
051 /**
052 * Was a file being used to set the executable?
053 */
054 private final boolean isFile;
055
056 /**
057 * Create a command line from a string.
058 *
059 * @param line the first element becomes the executable, the rest the arguments
060 * @return the parsed command line
061 * @throws IllegalArgumentException If line is null or all whitespace
062 */
063 public static CommandLine parse(final String line) {
064 return parse(line, null);
065 }
066
067 /**
068 * Create a command line from a string.
069 *
070 * @param line the first element becomes the executable, the rest the arguments
071 * @param substitutionMap the name/value pairs used for substitution
072 * @return the parsed command line
073 * @throws IllegalArgumentException If line is null or all whitespace
074 */
075 public static CommandLine parse(final String line, Map substitutionMap) {
076
077 if (line == null) {
078 throw new IllegalArgumentException("Command line can not be null");
079 } else if (line.trim().length() == 0) {
080 throw new IllegalArgumentException("Command line can not be empty");
081 } else {
082 String[] tmp = translateCommandline(line);
083
084 CommandLine cl = new CommandLine(tmp[0]);
085 cl.setSubstitutionMap(substitutionMap);
086 for (int i = 1; i < tmp.length; i++) {
087 cl.addArgument(tmp[i]);
088 }
089
090 return cl;
091 }
092 }
093
094 /**
095 * Create a command line without any arguments.
096 *
097 * @param executable the executable
098 */
099 public CommandLine(String executable) {
100 this.isFile=false;
101 this.executable=getExecutable(executable);
102 }
103
104 /**
105 * Create a command line without any arguments.
106 *
107 * @param executable the executable file
108 */
109 public CommandLine(File executable) {
110 this.isFile=true;
111 this.executable=getExecutable(executable.getAbsolutePath());
112 }
113
114 /**
115 * Copy constructor.
116 *
117 * @param other the instance to copy
118 */
119 public CommandLine(CommandLine other)
120 {
121 this.executable = other.getExecutable();
122 this.isFile = other.isFile();
123 this.arguments.addAll(other.arguments);
124
125 if(other.getSubstitutionMap() != null)
126 {
127 this.substitutionMap = new HashMap();
128 Iterator iterator = other.substitutionMap.keySet().iterator();
129 while(iterator.hasNext())
130 {
131 Object key = iterator.next();
132 this.substitutionMap.put(key, other.getSubstitutionMap().get(key));
133 }
134 }
135 }
136
137 /**
138 * Returns the executable.
139 *
140 * @return The executable
141 */
142 public String getExecutable() {
143 // Expand the executable and replace '/' and '\\' with the platform
144 // specific file separator char. This is safe here since we know
145 // that this is a platform specific command.
146 return StringUtils.fixFileSeparatorChar(expandArgument(executable));
147 }
148
149 /**
150 * Was a file being used to set the executable?
151 *
152 * @return true if a file was used for setting the executable
153 */
154 public boolean isFile(){
155 return isFile;
156 }
157
158 /**
159 * Add multiple arguments. Handles parsing of quotes and whitespace.
160 *
161 * @param arguments An array of 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.
170 *
171 * @param arguments An array of arguments
172 * @param handleQuoting Add the argument with/without handling quoting
173 * @return The command line itself
174 */
175 public CommandLine addArguments(final String[] arguments, boolean handleQuoting) {
176 if (arguments != null) {
177 for (int i = 0; i < arguments.length; i++) {
178 addArgument(arguments[i], handleQuoting);
179 }
180 }
181
182 return this;
183 }
184
185 /**
186 * Add multiple arguments. Handles parsing of quotes and whitespace.
187 * Please note that the parsing can have undesired side-effects therefore
188 * it is recommended to build the command line incrementally.
189 *
190 * @param arguments An string containing multiple arguments.
191 * @return The command line itself
192 */
193 public CommandLine addArguments(final String arguments) {
194 return this.addArguments(arguments, true);
195 }
196
197 /**
198 * Add multiple arguments. Handles parsing of quotes and whitespace.
199 * Please note that the parsing can have undesired side-effects therefore
200 * it is recommended to build the command line incrementally.
201 *
202 * @param arguments An string containing multiple arguments.
203 * @param handleQuoting Add the argument with/without handling quoting
204 * @return The command line itself
205 */
206 public CommandLine addArguments(final String arguments, boolean handleQuoting) {
207 if (arguments != null) {
208 String[] argumentsArray = translateCommandline(arguments);
209 addArguments(argumentsArray, handleQuoting);
210 }
211
212 return this;
213 }
214
215 /**
216 * Add a single argument. Handles quoting.
217 *
218 * @param argument The argument to add
219 * @return The command line itself
220 * @throws IllegalArgumentException If argument contains both single and double quotes
221 */
222 public CommandLine addArgument(final String argument) {
223 return this.addArgument(argument, true);
224 }
225
226 /**
227 * Add a single argument.
228 *
229 * @param argument The argument to add
230 * @param handleQuoting Add the argument with/without handling quoting
231 * @return The command line itself
232 */
233 public CommandLine addArgument(final String argument, boolean handleQuoting) {
234
235 if (argument == null)
236 {
237 return this;
238 }
239
240 // check if we can really quote the argument - if not throw an
241 // IllegalArgumentException
242 if (handleQuoting)
243 {
244 StringUtils.quoteArgument(argument);
245 }
246
247 arguments.add(new Argument(argument, handleQuoting));
248 return this;
249 }
250
251 /**
252 * Returns the expanded and quoted command line arguments.
253 *
254 * @return The quoted arguments
255 */
256 public String[] getArguments() {
257
258 Argument currArgument;
259 String expandedArgument;
260 String[] result = new String[arguments.size()];
261
262 for(int i=0; i<result.length; i++) {
263 currArgument = (Argument) arguments.get(i);
264 expandedArgument = expandArgument(currArgument.getValue());
265 result[i] = (currArgument.isHandleQuoting() ? StringUtils.quoteArgument(expandedArgument) : expandedArgument);
266 }
267
268 return result;
269 }
270
271 /**
272 * @return the substitution map
273 */
274 public Map getSubstitutionMap() {
275 return substitutionMap;
276 }
277
278 /**
279 * Set the substitutionMap to expand variables in the
280 * command line.
281 *
282 * @param substitutionMap the map
283 */
284 public void setSubstitutionMap(Map substitutionMap) {
285 this.substitutionMap = substitutionMap;
286 }
287
288 /**
289 * Returns the command line as an array of strings.
290 *
291 * @return The command line as an string array
292 */
293 public String[] toStrings() {
294 final String[] result = new String[arguments.size() + 1];
295 result[0] = this.getExecutable();
296 System.arraycopy(getArguments(), 0, result, 1, result.length-1);
297 return result;
298 }
299
300 /**
301 * Stringify operator returns the command line as a string.
302 * Parameters are correctly quoted when containing a space or
303 * left untouched if the are already quoted.
304 *
305 * @return the command line as single string
306 */
307 public String toString() {
308 return StringUtils.toString(toStrings(), " ");
309 }
310
311 // --- Implementation ---------------------------------------------------
312
313 /**
314 * Expand variables in a command line argument.
315 *
316 * @param argument the argument
317 * @return the expanded string
318 */
319 private String expandArgument(final String argument) {
320 StringBuffer stringBuffer = StringUtils.stringSubstitution(argument, this.getSubstitutionMap(), true);
321 return stringBuffer.toString();
322 }
323
324 /**
325 * Crack a command line.
326 *
327 * @param toProcess
328 * the command line to process
329 * @return the command line broken into strings. An empty or null toProcess
330 * parameter results in a zero sized array
331 */
332 private static String[] translateCommandline(final String toProcess) {
333 if (toProcess == null || toProcess.length() == 0) {
334 // no command? no string
335 return new String[0];
336 }
337
338 // parse with a simple finite state machine
339
340 final int normal = 0;
341 final int inQuote = 1;
342 final int inDoubleQuote = 2;
343 int state = normal;
344 StringTokenizer tok = new StringTokenizer(toProcess, "\"\' ", true);
345 Vector v = new Vector();
346 StringBuffer current = new StringBuffer();
347 boolean lastTokenHasBeenQuoted = false;
348
349 while (tok.hasMoreTokens()) {
350 String nextTok = tok.nextToken();
351 switch (state) {
352 case inQuote:
353 if ("\'".equals(nextTok)) {
354 lastTokenHasBeenQuoted = true;
355 state = normal;
356 } else {
357 current.append(nextTok);
358 }
359 break;
360 case inDoubleQuote:
361 if ("\"".equals(nextTok)) {
362 lastTokenHasBeenQuoted = true;
363 state = normal;
364 } else {
365 current.append(nextTok);
366 }
367 break;
368 default:
369 if ("\'".equals(nextTok)) {
370 state = inQuote;
371 } else if ("\"".equals(nextTok)) {
372 state = inDoubleQuote;
373 } else if (" ".equals(nextTok)) {
374 if (lastTokenHasBeenQuoted || current.length() != 0) {
375 v.addElement(current.toString());
376 current = new StringBuffer();
377 }
378 } else {
379 current.append(nextTok);
380 }
381 lastTokenHasBeenQuoted = false;
382 break;
383 }
384 }
385
386 if (lastTokenHasBeenQuoted || current.length() != 0) {
387 v.addElement(current.toString());
388 }
389
390 if (state == inQuote || state == inDoubleQuote) {
391 throw new IllegalArgumentException("Unbalanced quotes in "
392 + toProcess);
393 }
394
395 String[] args = new String[v.size()];
396 v.copyInto(args);
397 return args;
398 }
399
400 /**
401 * Get the executable - the argument is trimmed and '/' and '\\' are
402 * replaced with the platform specific file separator char
403 *
404 * @param executable the executable
405 * @return the platform-specific executable string
406 */
407 private String getExecutable(final String executable) {
408 if (executable == null) {
409 throw new IllegalArgumentException("Executable can not be null");
410 } else if(executable.trim().length() == 0) {
411 throw new IllegalArgumentException("Executable can not be empty");
412 } else {
413 return StringUtils.fixFileSeparatorChar(executable);
414 }
415 }
416
417 /**
418 * Encapsulates a command line argument.
419 */
420 class Argument {
421
422 private final String value;
423 private final boolean handleQuoting;
424
425 private Argument(String value, boolean handleQuoting)
426 {
427 this.value = value.trim();
428 this.handleQuoting = handleQuoting;
429 }
430
431 private String getValue()
432 {
433 return value;
434 }
435
436 private boolean isHandleQuoting()
437 {
438 return handleQuoting;
439 }
440 }
441 }