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