001    /*
002     $Id: GroovyMain.java,v 1.24 2005/07/18 18:00:32 dierk Exp $
003    
004     Copyright 2003 (C) James Strachan and Bob Mcwhirter. All Rights Reserved.
005    
006     Redistribution and use of this software and associated documentation
007     ("Software"), with or without modification, are permitted provided
008     that the following conditions are met:
009    
010     1. Redistributions of source code must retain copyright
011     statements and notices.  Redistributions must also contain a
012     copy of this document.
013    
014     2. Redistributions in binary form must reproduce the
015     above copyright notice, this list of conditions and the
016     following disclaimer in the documentation and/or other
017     materials provided with the distribution.
018    
019     3. The name "groovy" must not be used to endorse or promote
020     products derived from this Software without prior written
021     permission of The Codehaus.  For written permission,
022     please contact info@codehaus.org.
023    
024     4. Products derived from this Software may not be called "groovy"
025     nor may "groovy" appear in their names without prior written
026     permission of The Codehaus. "groovy" is a registered
027     trademark of The Codehaus.
028    
029     5. Due credit should be given to The Codehaus -
030     http://groovy.codehaus.org/
031    
032     THIS SOFTWARE IS PROVIDED BY THE CODEHAUS AND CONTRIBUTORS
033     ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
034     NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
035     FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
036     THE CODEHAUS OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
037     INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
038     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
039     SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
040     HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
041     STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
042     ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
043     OF THE POSSIBILITY OF SUCH DAMAGE.
044    
045     */
046    package groovy.ui;
047    
048    import groovy.lang.GroovyShell;
049    import groovy.lang.MetaClass;
050    import groovy.lang.Script;
051    
052    import java.io.BufferedReader;
053    import java.io.File;
054    import java.io.FileInputStream;
055    import java.io.FileNotFoundException;
056    import java.io.FileReader;
057    import java.io.FileWriter;
058    import java.io.IOException;
059    import java.io.InputStreamReader;
060    import java.io.PrintWriter;
061    import java.util.Iterator;
062    import java.util.List;
063    
064    import org.apache.commons.cli.CommandLine;
065    import org.apache.commons.cli.CommandLineParser;
066    import org.apache.commons.cli.HelpFormatter;
067    import org.apache.commons.cli.OptionBuilder;
068    import org.apache.commons.cli.Options;
069    import org.apache.commons.cli.ParseException;
070    import org.apache.commons.cli.PosixParser;
071    import org.codehaus.groovy.control.CompilationFailedException;
072    import org.codehaus.groovy.control.CompilerConfiguration;
073    import org.codehaus.groovy.runtime.InvokerHelper;
074    import org.codehaus.groovy.runtime.InvokerInvocationException;
075    
076    /**
077     * A Command line to execute groovy.
078     *
079     * @author Jeremy Rayner
080     * @author Yuri Schimke
081     * @version $Revision: 1.24 $
082     */
083    public class GroovyMain {
084        // arguments to the script
085        private List args;
086    
087        // is this a file on disk
088        private boolean isScriptFile;
089    
090        // filename or content of script
091        private String script;
092    
093        // process args as input files
094        private boolean processFiles;
095    
096        // edit input files in place
097        private boolean editFiles;
098    
099        // automatically output the result of each script
100        private boolean autoOutput;
101    
102        // process sockets
103        private boolean processSockets;
104    
105        // port to listen on when processing sockets
106        private int port;
107    
108        // backup input files with extension
109        private String backupExtension;
110    
111        // do you want full stack traces in script exceptions?
112        private boolean debug = false;
113    
114        // Compiler configuration, used to set the encodings of the scripts/classes
115        private CompilerConfiguration conf = new CompilerConfiguration();
116    
117        /**
118         * Main CLI interface.
119         *
120         * @param args all command line args.
121         */
122        public static void main(String args[]) {
123            MetaClass.setUseReflection(true);
124    
125            Options options = buildOptions();
126    
127            try {
128                CommandLine cmd = parseCommandLine(options, args);
129    
130                if (cmd.hasOption('h')) {
131                    HelpFormatter formatter = new HelpFormatter();
132                    formatter.printHelp("groovy", options);
133                } else if (cmd.hasOption('v')) {
134                    String version = InvokerHelper.getVersion();
135                    System.out.println("Groovy Version: " + version + " JVM: " + System.getProperty("java.vm.version"));
136                } else {
137                    // If we fail, then exit with an error so scripting frameworks can catch it
138                    if (!process(cmd)) {
139                        System.exit(1);
140                    }
141                }
142            } catch (ParseException pe) {
143                System.out.println("error: " + pe.getMessage());
144                HelpFormatter formatter = new HelpFormatter();
145                formatter.printHelp("groovy", options);
146            }
147        }
148    
149        /**
150         * Parse the command line.
151         *
152         * @param options the options parser.
153         * @param args    the command line args.
154         * @return parsed command line.
155         * @throws ParseException if there was a problem.
156         */
157        private static CommandLine parseCommandLine(Options options, String[] args) throws ParseException {
158            CommandLineParser parser = new PosixParser();
159            CommandLine cmd = parser.parse(options, args, true);
160            return cmd;
161        }
162    
163        /**
164         * Build the options parser.  Has to be synchronized because of the way Options are constructed.
165         *
166         * @return an options parser.
167         */
168        private static synchronized Options buildOptions() {
169            Options options = new Options();
170    
171            options.addOption(
172                OptionBuilder.hasArg(false)
173                .withDescription("usage information")
174                .withLongOpt("help")
175                .create('h'));
176            options.addOption(
177                OptionBuilder.hasArg(false)
178                .withDescription("debug mode will print out full stack traces")
179                .withLongOpt("debug")
180                .create('d'));
181            options.addOption(
182                OptionBuilder.hasArg(false)
183                .withDescription("display the Groovy and JVM versions")
184                .withLongOpt("version")
185                .create('v'));
186            options.addOption(
187                OptionBuilder.withArgName("charset")
188                .hasArg()
189                .withDescription("specify the encoding of the files")
190                .withLongOpt("encoding")
191                .create('c'));
192            options.addOption(
193                OptionBuilder.withArgName("script")
194                .hasArg()
195                .withDescription("specify a command line script")
196                .create('e'));
197            options.addOption(
198                OptionBuilder.withArgName("extension")
199                .hasOptionalArg()
200                .withDescription("modify files in place, create backup if extension is given (e.g. \'.bak\')")
201                .create('i'));
202            options.addOption(
203                OptionBuilder.hasArg(false)
204                .withDescription("process files line by line")
205                .create('n'));
206            options.addOption(
207                OptionBuilder.hasArg(false)
208                .withDescription("process files line by line and print result")
209                .create('p'));
210            options.addOption(
211                OptionBuilder.withArgName("port")
212                .hasOptionalArg()
213                .withDescription("listen on a port and process inbound lines")
214                .create('l'));
215            return options;
216        }
217    
218        /**
219         * Process the users request.
220         *
221         * @param line the parsed command line.
222         * @throws ParseException if invalid options are chosen
223         */
224        private static boolean process(CommandLine line) throws ParseException {
225            GroovyMain main = new GroovyMain();
226    
227            List args = line.getArgList();
228    
229            // add the ability to parse scripts with a specified encoding
230            if (line.hasOption('c')) {
231                main.conf.setSourceEncoding(line.getOptionValue("encoding"));
232            }
233    
234            main.isScriptFile = !line.hasOption('e');
235            main.debug = line.hasOption('d');
236            main.conf.setDebug(main.debug);
237            main.processFiles = line.hasOption('p') || line.hasOption('n');
238            main.autoOutput = line.hasOption('p');
239            main.editFiles = line.hasOption('i');
240            if (main.editFiles) {
241                main.backupExtension = line.getOptionValue('i');
242            }
243    
244            if (main.isScriptFile) {
245                if (args.isEmpty())
246                    throw new ParseException("neither -e or filename provided");
247    
248                main.script = (String) args.remove(0);
249                if (main.script.endsWith(".java"))
250                    throw new ParseException("error: cannot compile file with .java extension: " + main.script);
251            } else {
252                main.script = line.getOptionValue('e');
253            }
254    
255            main.processSockets = line.hasOption('l');
256            if (main.processSockets) {
257                String p = line.getOptionValue('l', "1960"); // default port to listen to
258                main.port = new Integer(p).intValue();
259            }
260            main.args = args;
261    
262            return main.run();
263        }
264    
265    
266        /**
267         * Run the script.
268         */
269        private boolean run() {
270            try {
271                if (processSockets) {
272                    processSockets();
273                } else if (processFiles) {
274                    processFiles();
275                } else {
276                    processOnce();
277                }
278                return true;
279            } catch (CompilationFailedException e) {
280                System.err.println(e);
281                return false;
282            } catch (Throwable e) {
283                if (e instanceof InvokerInvocationException) {
284                    InvokerInvocationException iie = (InvokerInvocationException) e;
285                    e = iie.getCause();
286                }
287                System.err.println("Caught: " + e);
288                if (debug) {
289                    e.printStackTrace();
290                } else {
291                    StackTraceElement[] stackTrace = e.getStackTrace();
292                    for (int i = 0; i < stackTrace.length; i++) {
293                        StackTraceElement element = stackTrace[i];
294                        String fileName = element.getFileName();
295                        if (fileName!=null && !fileName.endsWith(".java")) {
296                            System.err.println("\tat " + element);
297                        }
298                    }
299                }
300                return false;
301            }
302        }
303    
304        /**
305         * Process Sockets.
306         */
307        private void processSockets() throws CompilationFailedException, IOException {
308            GroovyShell groovy = new GroovyShell(conf);
309            //check the script is currently valid before starting a server against the script
310            if (isScriptFile) {
311                groovy.parse(new FileInputStream(huntForTheScriptFile(script)));
312            } else {
313                groovy.parse(script);
314            }
315            new GroovySocketServer(groovy, isScriptFile, script, autoOutput, port);
316        }
317    
318        /**
319         * Hunt for the script file, doesn't bother if it is named precisely.
320         *
321         * Tries in this order:
322         * - actual supplied name
323         * - name.groovy
324         * - name.gvy
325         * - name.gy
326         * - name.gsh
327         */
328        public File huntForTheScriptFile(String scriptFileName) {
329            File scriptFile = new File(scriptFileName);
330            String[] standardExtensions = {".groovy",".gvy",".gy",".gsh"};
331            int i = 0;
332            while (i < standardExtensions.length && !scriptFile.exists()) {
333                scriptFile = new File(scriptFileName + standardExtensions[i]);
334                i++;
335            }
336            // if we still haven't found the file, point back to the originally specified filename
337            if (!scriptFile.exists()) {
338                scriptFile = new File(scriptFileName);
339            }
340            return scriptFile;
341        }
342    
343        /**
344         * Process the input files.
345         */
346        private void processFiles() throws CompilationFailedException, IOException {
347            GroovyShell groovy = new GroovyShell(conf);
348    
349            Script s = null;
350    
351            if (isScriptFile) {
352                s = groovy.parse(huntForTheScriptFile(script));
353            } else {
354                s = groovy.parse(script, "main");
355            }
356    
357            if (args.isEmpty()) {
358                BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
359                PrintWriter writer = new PrintWriter(System.out);
360    
361                try {
362                    processReader(s, reader, writer);
363                } finally {
364                    reader.close();
365                    writer.close();
366                }
367    
368            } else {
369                Iterator i = args.iterator();
370                while (i.hasNext()) {
371                    String filename = (String) i.next();
372                    File file = huntForTheScriptFile(filename);
373                    processFile(s, file);
374                }
375            }
376        }
377    
378        /**
379         * Process a single input file.
380         *
381         * @param s    the script to execute.
382         * @param file the input file.
383         */
384        private void processFile(Script s, File file) throws IOException {
385            if (!file.exists())
386                throw new FileNotFoundException(file.getName());
387    
388            if (!editFiles) {
389                BufferedReader reader = new BufferedReader(new FileReader(file));
390                try {
391                    PrintWriter writer = new PrintWriter(System.out);
392                    processReader(s, reader, writer);
393                    writer.flush();
394                } finally {
395                    reader.close();
396                }
397            } else {
398                File backup = null;
399                if (backupExtension == null) {
400                    backup = File.createTempFile("groovy_", ".tmp");
401                    backup.deleteOnExit();
402                } else {
403                    backup = new File(file.getPath() + backupExtension);
404                }
405                backup.delete();
406                if (!file.renameTo(backup))
407                    throw new IOException("unable to rename " + file + " to " + backup);
408    
409                BufferedReader reader = new BufferedReader(new FileReader(backup));
410                try {
411                    PrintWriter writer = new PrintWriter(new FileWriter(file));
412                    try {
413                        processReader(s, reader, writer);
414                    } finally {
415                        writer.close();
416                    }
417                } finally {
418                    reader.close();
419                }
420            }
421        }
422    
423        /**
424         * Process a script against a single input file.
425         *
426         * @param s      script to execute.
427         * @param reader input file.
428         * @param pw     output sink.
429         */
430        private void processReader(Script s, BufferedReader reader, PrintWriter pw) throws IOException {
431            String line = null;
432            s.setProperty("out", pw);
433            while ((line = reader.readLine()) != null) {
434                s.setProperty("line", line);
435                Object o = s.run();
436    
437                if (autoOutput) {
438                    pw.println(o);
439                }
440            }
441        }
442    
443        /**
444         * Process the standard, single script with args.
445         */
446        private void processOnce() throws CompilationFailedException, IOException {
447            GroovyShell groovy = new GroovyShell(conf);
448    
449            if (isScriptFile)
450                groovy.run(huntForTheScriptFile(script), args);
451            else
452                groovy.run(script, "script_from_command_line", args);
453        }
454    }