1 package org.apache.maven.it.util.cli;
2
3 /*
4 * The MIT License
5 *
6 * Copyright (c) 2004, The Codehaus
7 *
8 * Permission is hereby granted, free of charge, to any person obtaining a copy of
9 * this software and associated documentation files (the "Software"), to deal in
10 * the Software without restriction, including without limitation the rights to
11 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
12 * of the Software, and to permit persons to whom the Software is furnished to do
13 * so, subject to the following conditions:
14 *
15 * The above copyright notice and this permission notice shall be included in all
16 * copies or substantial portions of the Software.
17 *
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 * SOFTWARE.
25 */
26
27 /********************************************************************************
28 * CruiseControl, a Continuous Integration Toolkit
29 * Copyright (c) 2001-2003, ThoughtWorks, Inc.
30 * 651 W Washington Ave. Suite 500
31 * Chicago, IL 60661 USA
32 * All rights reserved.
33 *
34 * Redistribution and use in source and binary forms, with or without
35 * modification, are permitted provided that the following conditions
36 * are met:
37 *
38 * + Redistributions of source code must retain the above copyright
39 * notice, this list of conditions and the following disclaimer.
40 *
41 * + Redistributions in binary form must reproduce the above
42 * copyright notice, this list of conditions and the following
43 * disclaimer in the documentation and/or other materials provided
44 * with the distribution.
45 *
46 * + Neither the name of ThoughtWorks, Inc., CruiseControl, nor the
47 * names of its contributors may be used to endorse or promote
48 * products derived from this software without specific prior
49 * written permission.
50 *
51 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
52 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
53 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
54 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
55 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
56 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
57 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
58 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
59 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
60 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
61 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
62 ********************************************************************************/
63
64 /* ====================================================================
65 * Copyright 2003-2004 The Apache Software Foundation.
66 *
67 * Licensed under the Apache License, Version 2.0 (the "License");
68 * you may not use this file except in compliance with the License.
69 * You may obtain a copy of the License at
70 *
71 * http://www.apache.org/licenses/LICENSE-2.0
72 *
73 * Unless required by applicable law or agreed to in writing, software
74 * distributed under the License is distributed on an "AS IS" BASIS,
75 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
76 * See the License for the specific language governing permissions and
77 * limitations under the License.
78 * ====================================================================
79 */
80
81 import org.apache.maven.it.util.cli.shell.CommandShell;
82 import org.apache.maven.it.util.cli.shell.CmdShell;
83 import org.apache.maven.it.util.cli.shell.Shell;
84
85 import java.io.File;
86 import java.io.IOException;
87 import java.util.ArrayList;
88 import java.util.Arrays;
89 import java.util.Iterator;
90 import java.util.List;
91 import java.util.Properties;
92 import java.util.StringTokenizer;
93 import java.util.Vector;
94 import java.util.Hashtable;
95
96 /**
97 * <p/>
98 * Commandline objects help handling command lines specifying processes to
99 * execute.
100 * </p>
101 * <p/>
102 * The class can be used to define a command line as nested elements or as a
103 * helper to define a command line by an application.
104 * </p>
105 * <p/>
106 * <code>
107 * <someelement><br>
108 * <acommandline executable="/executable/to/run"><br>
109 * <argument value="argument 1" /><br>
110 * <argument line="argument_1 argument_2 argument_3" /><br>
111 * <argument value="argument 4" /><br>
112 * </acommandline><br>
113 * </someelement><br>
114 * </code>
115 * </p>
116 * <p/>
117 * The element <code>someelement</code> must provide a method
118 * <code>createAcommandline</code> which returns an instance of this class.
119 * </p>
120 *
121 * @author thomas.haas@softwired-inc.com
122 * @author <a href="mailto:stefan.bodewig@epost.de">Stefan Bodewig</a>
123 */
124 public class Commandline
125 implements Cloneable
126 {
127 protected static final String OS_NAME = "os.name";
128
129 protected static final String WINDOWS = "Windows";
130
131 protected String executable = null;
132
133 protected Vector arguments = new Vector();
134
135 protected Hashtable envVars = new Hashtable();
136
137 private File workingDir = null;
138
139 private long pid = -1;
140
141 private Shell shell;
142
143 /**
144 * Create a new command line object.
145 * Shell is autodetected from operating system
146 *
147 * @param toProcess
148 */
149 public Commandline( String toProcess )
150 {
151 super();
152 setDefaultShell();
153 String[] tmp = new String[0];
154 try
155 {
156 tmp = translateCommandline( toProcess );
157 }
158 catch ( Exception e )
159 {
160 System.err.println( "Error translating Commandline." );
161 }
162 if ( tmp != null && tmp.length > 0 )
163 {
164 setExecutable( tmp[0] );
165 for ( int i = 1; i < tmp.length; i++ )
166 {
167 createArgument().setValue( tmp[i] );
168 }
169 }
170 }
171
172 /**
173 * Create a new command line object.
174 * Shell is autodetected from operating system
175 */
176 public Commandline()
177 {
178 super();
179 setDefaultShell();
180 }
181
182 public long getPid()
183 {
184 if ( pid == -1 )
185 {
186 pid = Long.parseLong( String.valueOf( System.currentTimeMillis() ) );
187 }
188
189 return pid;
190 }
191
192 public void setPid( long pid )
193 {
194 this.pid = pid;
195 }
196
197 /**
198 * Used for nested xml command line definitions.
199 */
200 public static class Argument
201 {
202
203 private String[] parts;
204
205 /**
206 * Sets a single commandline argument.
207 *
208 * @param value a single commandline argument.
209 */
210 public void setValue( String value )
211 {
212 if ( value != null )
213 {
214 parts = new String[]{value};
215 }
216 }
217
218 /**
219 * Line to split into several commandline arguments.
220 *
221 * @param line line to split into several commandline arguments
222 */
223 public void setLine( String line )
224 {
225 if ( line == null )
226 {
227 return;
228 }
229 try
230 {
231 parts = translateCommandline( line );
232 }
233 catch ( Exception e )
234 {
235 System.err.println( "Error translating Commandline." );
236 }
237 }
238
239 /**
240 * Sets a single commandline argument to the absolute filename
241 * of the given file.
242 *
243 * @param value a single commandline argument.
244 */
245 public void setFile( File value )
246 {
247 parts = new String[]{value.getAbsolutePath()};
248 }
249
250 /**
251 * Returns the parts this Argument consists of.
252 */
253 public String[] getParts()
254 {
255 return parts;
256 }
257 }
258
259 /**
260 * Class to keep track of the position of an Argument.
261 */
262 // <p>This class is there to support the srcfile and targetfile
263 // elements of <execon> and <transform> - don't know
264 // whether there might be additional use cases.</p> --SB
265 public class Marker
266 {
267
268 private int position;
269
270 private int realPos = -1;
271
272 Marker( int position )
273 {
274 this.position = position;
275 }
276
277 /**
278 * Return the number of arguments that preceeded this marker.
279 * <p/>
280 * <p>The name of the executable - if set - is counted as the
281 * very first argument.</p>
282 */
283 public int getPosition()
284 {
285 if ( realPos == -1 )
286 {
287 realPos = ( executable == null ? 0 : 1 );
288 for ( int i = 0; i < position; i++ )
289 {
290 Argument arg = (Argument) arguments.elementAt( i );
291 realPos += arg.getParts().length;
292 }
293 }
294 return realPos;
295 }
296 }
297
298
299 /**
300 * <p>Sets the shell or command-line interpretor for the detected operating system,
301 * and the shell arguments.</p>
302 */
303 private void setDefaultShell()
304 {
305 String os = System.getProperty( OS_NAME );
306
307 //If this is windows set the shell to command.com or cmd.exe with correct arguments.
308 if ( os.indexOf( WINDOWS ) > -1 )
309 {
310 if ( os.indexOf( "95" ) > -1 || os.indexOf( "98" ) > -1 || os.indexOf( "Me" ) > -1 )
311 {
312 setShell( new CommandShell() );
313 }
314 else
315 {
316 setShell( new CmdShell() );
317 }
318 }
319 }
320
321 /**
322 * Creates an argument object.
323 * <p/>
324 * <p>Each commandline object has at most one instance of the
325 * argument class. This method calls
326 * <code>this.createArgument(false)</code>.</p>
327 *
328 * @return the argument object.
329 * @see #createArgument(boolean)
330 */
331 public Argument createArgument()
332 {
333 return this.createArgument( false );
334 }
335
336 /**
337 * Creates an argument object and adds it to our list of args.
338 * <p/>
339 * <p>Each commandline object has at most one instance of the
340 * argument class.</p>
341 *
342 * @param insertAtStart if true, the argument is inserted at the
343 * beginning of the list of args, otherwise it is appended.
344 */
345 public Argument createArgument( boolean insertAtStart )
346 {
347 Argument argument = new Argument();
348 if ( insertAtStart )
349 {
350 arguments.insertElementAt( argument, 0 );
351 }
352 else
353 {
354 arguments.addElement( argument );
355 }
356 return argument;
357 }
358
359 /**
360 * Sets the executable to run.
361 */
362 public void setExecutable( String executable )
363 {
364 if ( executable == null || executable.length() == 0 )
365 {
366 return;
367 }
368 this.executable = executable.replace( '/', File.separatorChar ).replace( '\\', File.separatorChar );
369 }
370
371 public String getExecutable()
372 {
373 return executable;
374 }
375
376 public void addArguments( String[] line )
377 {
378 for ( int i = 0; i < line.length; i++ )
379 {
380 createArgument().setValue( line[i] );
381 }
382 }
383
384 /**
385 * Add an environment variable
386 */
387 public void addEnvironment( String name,
388 String value )
389 {
390 envVars.put( name, name + "=" + value );
391 }
392
393 /**
394 * Add system environment variables
395 */
396 public void addSystemEnvironment()
397 throws Exception
398 {
399 Properties envVars = CommandLineUtils.getSystemEnvVars();
400
401 for ( Iterator i = envVars.keySet().iterator(); i.hasNext(); )
402 {
403 String key = (String) i.next();
404
405 if ( !this.envVars.containsKey( key ) )
406 {
407 this.envVars.put( key, key + "=" + envVars.getProperty( key ) );
408 }
409 }
410 }
411
412 /**
413 * Return the list of environment variables
414 */
415 public String[] getEnvironmentVariables()
416 throws CommandLineException
417 {
418 try
419 {
420 addSystemEnvironment();
421 }
422 catch ( Exception e )
423 {
424 throw new CommandLineException( "Error setting up environmental variables", e );
425 }
426
427 return (String[]) envVars.values().toArray( new String[envVars.size()] );
428 }
429
430 /**
431 * Returns the executable and all defined arguments.
432 */
433 public String[] getCommandline()
434 {
435 final String[] args = getArguments();
436 if ( executable == null )
437 {
438 return args;
439 }
440 final String[] result = new String[args.length + 1];
441 result[0] = executable;
442 System.arraycopy( args, 0, result, 1, args.length );
443 return result;
444 }
445
446 /**
447 * Returns the shell, executable and all defined arguments.
448 */
449 public String[] getShellCommandline()
450 {
451
452 if ( getShell() == null )
453 {
454 if ( executable != null )
455 {
456 List commandLine = new ArrayList();
457 commandLine.add( executable );
458 commandLine.addAll( Arrays.asList( getArguments() ) );
459 return (String[]) commandLine.toArray( new String[0] );
460 }
461 else
462 {
463 return getArguments();
464 }
465
466 }
467 else
468 {
469 return (String[]) getShell().getShellCommandLine( executable, getArguments() ).toArray( new String[0] );
470 }
471 }
472
473 /**
474 * Returns all arguments defined by <code>addLine</code>,
475 * <code>addValue</code> or the argument object.
476 */
477 public String[] getArguments()
478 {
479 Vector result = new Vector( arguments.size() * 2 );
480 for ( int i = 0; i < arguments.size(); i++ )
481 {
482 Argument arg = (Argument) arguments.elementAt( i );
483 String[] s = arg.getParts();
484 if ( s != null )
485 {
486 for ( int j = 0; j < s.length; j++ )
487 {
488 result.addElement( s[j] );
489 }
490 }
491 }
492
493 String[] res = new String[result.size()];
494 result.copyInto( res );
495 return res;
496 }
497
498 public String toString()
499 {
500 return toString( getCommandline() );
501 }
502
503 /**
504 * <p>Put quotes around the given String if necessary.</p>
505 * <p>If the argument doesn't include spaces or quotes, return it
506 * as is. If it contains double quotes, use single quotes - else
507 * surround the argument by double quotes.</p>
508 *
509 * @throws CommandLineException if the argument contains both, single
510 * and double quotes.
511 */
512 public static String quoteArgument( String argument )
513 throws CommandLineException
514 {
515 if ( argument.indexOf( '\"' ) > -1 )
516 {
517 if ( argument.indexOf( '\'' ) > -1 )
518 {
519 throw new CommandLineException( "Can't handle single and double quotes in same argument" );
520 }
521 else
522 {
523 return '\'' + argument + '\'';
524 }
525 }
526 else if ( containsAny( argument, "'<>&|*? " ) )
527 {
528 return '\"' + argument + '\"';
529 }
530 else
531 {
532 return argument;
533 }
534 }
535
536 private static boolean containsAny( String argument, String chars )
537 {
538 for ( int i = chars.length() - 1; i >= 0; i-- )
539 {
540 if ( argument.indexOf( chars.charAt( i ) ) >= 0 )
541 {
542 return true;
543 }
544 }
545 return false;
546 }
547
548 public static String toString( String[] line )
549 {
550 // empty path return empty string
551 if ( line == null || line.length == 0 )
552 {
553 return "";
554 }
555
556 // path containing one or more elements
557 final StringBuffer result = new StringBuffer();
558 for ( int i = 0; i < line.length; i++ )
559 {
560 if ( i > 0 )
561 {
562 result.append( ' ' );
563 }
564 try
565 {
566 result.append( quoteArgument( line[i] ) );
567 }
568 catch ( Exception e )
569 {
570 System.err.println( "Error quoting argument." );
571 }
572 }
573 return result.toString();
574 }
575
576 public static String[] translateCommandline( String toProcess )
577 throws Exception
578 {
579 if ( toProcess == null || toProcess.length() == 0 )
580 {
581 return new String[0];
582 }
583
584 // parse with a simple finite state machine
585
586 final int normal = 0;
587 final int inQuote = 1;
588 final int inDoubleQuote = 2;
589 int state = normal;
590 StringTokenizer tok = new StringTokenizer( toProcess, "\"\' ", true );
591 Vector v = new Vector();
592 StringBuffer current = new StringBuffer();
593
594 while ( tok.hasMoreTokens() )
595 {
596 String nextTok = tok.nextToken();
597 switch ( state )
598 {
599 case inQuote:
600 if ( "\'".equals( nextTok ) )
601 {
602 state = normal;
603 }
604 else
605 {
606 current.append( nextTok );
607 }
608 break;
609 case inDoubleQuote:
610 if ( "\"".equals( nextTok ) )
611 {
612 state = normal;
613 }
614 else
615 {
616 current.append( nextTok );
617 }
618 break;
619 default:
620 if ( "\'".equals( nextTok ) )
621 {
622 state = inQuote;
623 }
624 else if ( "\"".equals( nextTok ) )
625 {
626 state = inDoubleQuote;
627 }
628 else if ( " ".equals( nextTok ) )
629 {
630 if ( current.length() != 0 )
631 {
632 v.addElement( current.toString() );
633 current.setLength( 0 );
634 }
635 }
636 else
637 {
638 current.append( nextTok );
639 }
640 break;
641 }
642 }
643
644 if ( current.length() != 0 )
645 {
646 v.addElement( current.toString() );
647 }
648
649 if ( state == inQuote || state == inDoubleQuote )
650 {
651 throw new CommandLineException( "unbalanced quotes in " + toProcess );
652 }
653
654 String[] args = new String[v.size()];
655 v.copyInto( args );
656 return args;
657 }
658
659 public int size()
660 {
661 return getCommandline().length;
662 }
663
664 public Object clone()
665 {
666 Commandline c = new Commandline();
667 c.setExecutable( executable );
668 c.addArguments( getArguments() );
669 return c;
670 }
671
672 /**
673 * Clear out the whole command line.
674 */
675 public void clear()
676 {
677 executable = null;
678 arguments.removeAllElements();
679 }
680
681 /**
682 * Clear out the arguments but leave the executable in place for another operation.
683 */
684 public void clearArgs()
685 {
686 arguments.removeAllElements();
687 }
688
689 /**
690 * Return a marker.
691 * <p/>
692 * <p>This marker can be used to locate a position on the
693 * commandline - to insert something for example - when all
694 * parameters have been set.</p>
695 */
696 public Marker createMarker()
697 {
698 return new Marker( arguments.size() );
699 }
700
701 /**
702 * Sets execution directory.
703 */
704 public void setWorkingDirectory( String path )
705 {
706 if ( path != null )
707 {
708 workingDir = new File( path );
709 }
710 }
711
712 public File getWorkingDirectory()
713 {
714 return workingDir;
715 }
716
717 /**
718 * Executes the command.
719 */
720 public Process execute()
721 throws CommandLineException
722 {
723 Process process;
724
725 //addEnvironment( "MAVEN_TEST_ENVAR", "MAVEN_TEST_ENVAR_VALUE" );
726
727 String[] environment = getEnvironmentVariables();
728
729 try
730 {
731 if ( workingDir == null )
732 {
733 process = Runtime.getRuntime().exec( getShellCommandline(), environment );
734 }
735 else
736 {
737 if ( !workingDir.exists() )
738 {
739 throw new CommandLineException(
740 "Working directory \"" + workingDir.getPath() + "\" does not exist!" );
741 }
742 else if ( !workingDir.isDirectory() )
743 {
744 throw new CommandLineException(
745 "Path \"" + workingDir.getPath() + "\" does not specify a directory." );
746 }
747
748 process = Runtime.getRuntime().exec( getShellCommandline(), environment, workingDir );
749 }
750 }
751 catch ( IOException ex )
752 {
753 throw new CommandLineException( "Error while executing process.", ex );
754 }
755
756 return process;
757 }
758
759 public Properties getSystemEnvVars()
760 throws Exception
761 {
762 return CommandLineUtils.getSystemEnvVars();
763 }
764
765 /**
766 * Allows to set the shell to be used in this command line.
767 *
768 * @param shell
769 * @since 1.2
770 */
771 public void setShell( Shell shell )
772 {
773 this.shell = shell;
774 }
775
776 /**
777 * Get the shell to be used in this command line.
778 *
779 * @since 1.2
780 */
781 public Shell getShell()
782 {
783 return shell;
784 }
785 }