001    /*
002     $Id: GroovyMain.java,v 1.22 2005/06/14 21:15:20 blackdrag 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.22 $
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(OptionBuilder.hasArg(false).withDescription("usage information").withLongOpt("help").create('h'));
172    
173            options.addOption(OptionBuilder.hasArg(false).withDescription("debug mode will print out full stack traces").withLongOpt("debug").create('d'));
174    
175            options.addOption(OptionBuilder.hasArg(false).withDescription("display the Groovy and JVM versions").withLongOpt("version").create('v'));
176    
177            options.addOption(OptionBuilder.withArgName("charset").hasArg().withDescription("specify the encoding of the files").withLongOpt("encoding").create('c'));
178    
179            options.addOption(OptionBuilder.withArgName("script").hasArg().withDescription("specify a command line script").create('e'));
180    
181            options.addOption(OptionBuilder.withArgName("extension").hasOptionalArg().withDescription("modify files in place").create('i'));
182    
183            options.addOption(OptionBuilder.hasArg(false).withDescription("process files line by line").create('n'));
184    
185            options.addOption(OptionBuilder.hasArg(false).withDescription("process files line by line and print result").create('p'));
186    
187            options.addOption(OptionBuilder.withArgName("port").hasOptionalArg().withDescription("listen on a port and process inbound lines").create('l'));
188            return options;
189        }
190    
191        /**
192         * Process the users request.
193         *
194         * @param line the parsed command line.
195         * @throws ParseException if invalid options are chosen
196         */
197        private static boolean process(CommandLine line) throws ParseException {
198            GroovyMain main = new GroovyMain();
199    
200            List args = line.getArgList();
201    
202            // add the ability to parse scripts with a specified encoding
203            if (line.hasOption('c')) {
204                main.conf.setSourceEncoding(line.getOptionValue("encoding"));
205            }
206    
207            main.isScriptFile = !line.hasOption('e');
208            main.debug = line.hasOption('d');
209            main.conf.setDebug(main.debug);
210            main.processFiles = line.hasOption('p') || line.hasOption('n');
211            main.autoOutput = line.hasOption('p');
212            main.editFiles = line.hasOption('i');
213            if (main.editFiles) {
214                main.backupExtension = line.getOptionValue('i');
215            }
216    
217            if (main.isScriptFile) {
218                if (args.isEmpty())
219                    throw new ParseException("neither -e or filename provided");
220    
221                main.script = (String) args.remove(0);
222                if (main.script.endsWith(".java"))
223                    throw new ParseException("error: cannot compile file with .java extension: " + main.script);
224            } else {
225                main.script = line.getOptionValue('e');
226            }
227    
228            main.processSockets = line.hasOption('l');
229            if (main.processSockets) {
230                String p = line.getOptionValue('l', "1960"); // default port to listen to
231                main.port = new Integer(p).intValue();
232            }
233            main.args = args;
234    
235            return main.run();
236        }
237    
238    
239        /**
240         * Run the script.
241         */
242        private boolean run() {
243            try {
244                if (processSockets) {
245                    processSockets();
246                } else if (processFiles) {
247                    processFiles();
248                } else {
249                    processOnce();
250                }
251                return true;
252            } catch (CompilationFailedException e) {
253                System.err.println(e);
254                return false;
255            } catch (Throwable e) {
256                if (e instanceof InvokerInvocationException) {
257                    InvokerInvocationException iie = (InvokerInvocationException) e;
258                    e = iie.getCause();
259                }
260                System.err.println("Caught: " + e);
261                if (debug) {
262                    e.printStackTrace();
263                } else {
264                    StackTraceElement[] stackTrace = e.getStackTrace();
265                    for (int i = 0; i < stackTrace.length; i++) {
266                        StackTraceElement element = stackTrace[i];
267                        String fileName = element.getFileName();
268                        if (fileName!=null && !fileName.endsWith(".java")) {
269                            System.err.println("\tat " + element);
270                        }
271                    }
272                }
273                return false;
274            }
275        }
276    
277        /**
278         * Process Sockets.
279         */
280        private void processSockets() throws CompilationFailedException, IOException {
281            GroovyShell groovy = new GroovyShell(conf);
282            //check the script is currently valid before starting a server against the script
283            if (isScriptFile) {
284                groovy.parse(new FileInputStream(huntForTheScriptFile(script)));
285            } else {
286                groovy.parse(script);
287            }
288            new GroovySocketServer(groovy, isScriptFile, script, autoOutput, port);
289        }
290    
291        /**
292         * Hunt for the script file, doesn't bother if it is named precisely.
293         *
294         * Tries in this order:
295         * - actual supplied name
296         * - name.groovy
297         * - name.gvy
298         * - name.gy
299         * - name.gsh
300         */
301        public File huntForTheScriptFile(String scriptFileName) {
302            File scriptFile = new File(scriptFileName);
303            String[] standardExtensions = {".groovy",".gvy",".gy",".gsh"};
304            int i = 0;
305            while (i < standardExtensions.length && !scriptFile.exists()) {
306                scriptFile = new File(scriptFileName + standardExtensions[i]);
307                i++;
308            }
309            // if we still haven't found the file, point back to the originally specified filename
310            if (!scriptFile.exists()) {
311                scriptFile = new File(scriptFileName);
312            }
313            return scriptFile;
314        }
315    
316        /**
317         * Process the input files.
318         */
319        private void processFiles() throws CompilationFailedException, IOException {
320            GroovyShell groovy = new GroovyShell(conf);
321    
322            Script s = null;
323    
324            if (isScriptFile) {
325                s = groovy.parse(huntForTheScriptFile(script));
326            } else {
327                s = groovy.parse(script, "main");
328            }
329    
330            if (args.isEmpty()) {
331                BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
332                PrintWriter writer = new PrintWriter(System.out);
333    
334                processReader(s, reader, writer);
335            } else {
336                Iterator i = args.iterator();
337                while (i.hasNext()) {
338                    String filename = (String) i.next();
339                    File file = huntForTheScriptFile(filename);
340                    processFile(s, file);
341                }
342            }
343        }
344    
345        /**
346         * Process a single input file.
347         *
348         * @param s    the script to execute.
349         * @param file the input file.
350         */
351        private void processFile(Script s, File file) throws IOException {
352            if (!file.exists())
353                throw new FileNotFoundException(file.getName());
354    
355            if (!editFiles) {
356                BufferedReader reader = new BufferedReader(new FileReader(file));
357                try {
358                    PrintWriter writer = new PrintWriter(System.out);
359                    processReader(s, reader, writer);
360                    writer.flush();
361                } finally {
362                    reader.close();
363                }
364            } else {
365                File backup = null;
366                if (backupExtension == null) {
367                    backup = File.createTempFile("groovy_", ".tmp");
368                    backup.deleteOnExit();
369                } else {
370                    backup = new File(file.getPath() + backupExtension);
371                    backup.delete();
372                }
373                if (!file.renameTo(backup))
374                    throw new IOException("unable to rename " + file + " to " + backup);
375    
376                BufferedReader reader = new BufferedReader(new FileReader(backup));
377                try {
378                    PrintWriter writer = new PrintWriter(new FileWriter(file));
379                    try {
380                        processReader(s, reader, writer);
381                    } finally {
382                        writer.close();
383                    }
384                } finally {
385                    reader.close();
386                }
387            }
388        }
389    
390        /**
391         * Process a script against a single input file.
392         *
393         * @param s      script to execute.
394         * @param reader input file.
395         * @param pw     output sink.
396         */
397        private void processReader(Script s, BufferedReader reader, PrintWriter pw) throws IOException {
398            String line = null;
399            s.setProperty("out", pw);
400            while ((line = reader.readLine()) != null) {
401                s.setProperty("line", line);
402                Object o = s.run();
403    
404                if (autoOutput) {
405                    pw.println(o);
406                }
407            }
408        }
409    
410        /**
411         * Process the standard, single script with args.
412         */
413        private void processOnce() throws CompilationFailedException, IOException {
414            GroovyShell groovy = new GroovyShell(conf);
415    
416            if (isScriptFile)
417                groovy.run(huntForTheScriptFile(script), args);
418            else
419                groovy.run(script, "script_from_command_line", args);
420        }
421    }