/* * ExecCmd.java -- * * This file contains the Jacl implementation of the built-in Tcl "exec" * command. The exec command is not available on the Mac. * * Copyright (c) 1997 Sun Microsystems, Inc. * * See the file "license.terms" for information on usage and * redistribution of this file, and for a DISCLAIMER OF ALL * WARRANTIES. * * RCS: @(#) $Id: ExecCmd.java,v 1.2 1998/11/04 22:36:00 hylands Exp $ */ package tcl.lang; import java.util.*; import java.io.*; /* * This class implements the built-in "exec" command in Tcl. */ class ExecCmd implements Command { /* *---------------------------------------------------------------------- * * CmdProc -- * * This procedure is invoked to process the "exec" Tcl command. * See the user documentation for details on what it does. * * Results: * None. * * Side effects: * See the user documentation. * *---------------------------------------------------------------------- */ public void cmdProc( Interp interp, // The current interpreter. TclObject argv[]) // The arguments to exec. throws TclException // A standard Tcl exception. { int i, j; int firstWord; /* Index to the first non-switch arg */ int argLen = argv.length; /* No of args to copy to argStrs */ int exit; /* denotes exit status of process */ int errorBytes; /* number of bytes of process stderr */ boolean background = false; /* Indicates a bg process */ boolean keepNewline = false; /* Retains newline in pipline output */ Process p; /* The exec-ed process */ String argStr; /* Conversion of argv to a string */ String os; /* Stores the os.name property */ StringBuffer sbuf; /* * Check for a leading "-keepnewline" argument. */ for (firstWord = 1; firstWord < argLen; firstWord++) { argStr = argv[firstWord].toString(); if ((argStr.length() > 0) && (argStr.charAt(0) == '-')) { if (argStr.equals("-keepnewline")) { keepNewline = true; } else if (argStr.equals("--")) { firstWord++; break; } else { throw new TclException(interp, "bad switch \"" + argStr + "\": must be -keepnewline or --"); } } else { break; } } if (argLen <= firstWord) { throw new TclNumArgsException(interp, 1, argv, "?switches? arg ?arg ...?"); } /* * See if the command is to be run in background. * Currently this does nothing, it is just for compatibility */ if (argv[argLen-1].toString().equals("&")) { argLen--; background = true; } try { /* * It is necessary to perform system specific * operations before calling exec. For now Solaris * and Windows execs are somewhat supported, in all other cases * we simply call exec and give it our "best shot" */ if (Util.isUnix()) { p = execUnix(interp, argv, firstWord, argLen); } else if (Util.isWindows()) { p = execWin(interp, argv, firstWord, argLen); } else { p = execDefault(interp, argv, firstWord, argLen); } //note to self : buffer reading should be done in //a seperate thread and not by calling waitFor() //because a process that is waited for can block //Wait for the process to finish running, exit = p.waitFor(); //Make buffer for the results of the subprocess execution sbuf = new StringBuffer(); //read data on stdout stream into result buffer readStreamIntoBuffer(p.getInputStream(),sbuf); //if there is data on the stderr stream then append //this data onto the result StringBuffer //check for the special case where there is no error //data but the process returns an error result errorBytes = readStreamIntoBuffer(p.getErrorStream(),sbuf); if ((errorBytes == 0) && (exit != 0)) { sbuf.append("child process exited abnormally"); } //If the last character of the result buffer is a newline, then //remove the newline character (the newline would just confuse //things). Finally, we set pass the result to the interpreter. int length = sbuf.length(); if (!keepNewline && (length > 0) && (sbuf.charAt(length - 1) == '\n')) { sbuf.setLength(length - 1); } //when the subprocess writes to its stderr stream or returns //a non zero result we generate an error if ((exit != 0) || (errorBytes != 0)) { throw new TclException(interp, sbuf.toString()); } //otherwise things went well so set the result interp.setResult(sbuf.toString()); } catch (IOException e) { //if exec fails we end up catching the exception here throw new TclException(interp, "couldn't execute \"" + argv[firstWord].toString() + "\": no such file or directory"); } catch (InterruptedException e) { /* * Do Nothing... */ } } /* *---------------------------------------------------------------------- * * readStreamIntoBuffer -- * * This utility function will read the contents of an InputStream * into a StringBuffer. When done it returns the number of bytes * read from the InputStream. The assumption is an unbuffered stream * * Results: * Returns the number of bytes read from the stream to the buffer * * Side effects: * Data is read from the InputStream. * *---------------------------------------------------------------------- */ static int readStreamIntoBuffer(InputStream in, StringBuffer sbuf) { int numRead = 0; BufferedReader br = new BufferedReader(new InputStreamReader(in)); try { String line = br.readLine(); while (line != null) { sbuf.append(line); numRead += line.length(); sbuf.append('\n'); numRead++; line = br.readLine(); } } catch (IOException e) { //do nothing just return numRead } finally { try {br.close();} catch (IOException e) {} //ignore IO error } return numRead; } /* *---------------------------------------------------------------------- * * escapeWinString -- * * This utility function takes a String and returns the same String * with any % replaced by %%. This is needed so that environemental * variable substitution is not done when a windows program is called * with the exec command that uses .bat files * * Results: * Returns the same String is no % is in the string, otherwise the * String is returned with %% for every %. * * Side effects: * none. * *---------------------------------------------------------------------- */ static String escapeWinString(String str) { if (str.indexOf('%') == -1) return str; char[] arr = str.toCharArray(); StringBuffer sb = new StringBuffer(50); for (int i=0; i < arr.length; i++) { if (arr[i] == '%') { sb.append('%'); } sb.append(arr[i]); } return sb.toString(); } /* *---------------------------------------------------------------------- * * execUnix -- * * This procedure is invoked to process system specific "exec" calls for * Unix. Since new Process dosent know the current tclsh directory, we * exec the command in a Unix shell, but we force the Unix shell to cd to * the current dir first. * * Results: * Returns the Process object returned from the exec call. * * Side effects: * See the user documentation. * *---------------------------------------------------------------------- */ private Process execUnix (Interp interp, TclObject argv[], int first, int last) throws IOException, InterruptedException { String[] argStrs = new String[3]; argStrs[0] = "sh"; argStrs[1] = "-c"; StringBuffer sbuf = new StringBuffer(); sbuf.append("cd \'"); sbuf.append(interp.getWorkingDir().toString()); sbuf.append("\'; "); for (int i = first; i < last; i++) { sbuf.append('\''); sbuf.append(argv[i].toString()); sbuf.append('\''); sbuf.append(' '); } //trim off the last space char sbuf.setLength(sbuf.length() - 1); argStrs[2] = sbuf.toString(); //System.out.println("now to unix exec \"" + argStrs[2] + "\""); return Runtime.getRuntime().exec(argStrs); } /* *---------------------------------------------------------------------- * * execWin -- * * This procedure is invoked to process system specific "exec" calls for * Windows. Since new Process dosent know the current tclsh directory, a * file has to be created that cds to the current interp dir, and executes * the command there. * * Results: * Returns the new process. * * Side effects: * See the user documentation. * *---------------------------------------------------------------------- */ private Process execWin (Interp interp, TclObject argv[], int first, int last) throws IOException, InterruptedException { // when running on NT we need to write out two files // the first is in C:/TEMP/jacl1.bat and the second is C:/TEMP/jacl2.bat // we exec command.com on jacl1.bat which will invoke cmd.exe on jacl2.bat // if we are not running under NT then we just write out the jacl1.bat file // and invoke that with command.com which should work on 95 String jacl1 = "C:\\TEMP\\jacl1.bat"; String jacl2 = "C:\\TEMP\\jacl2.bat"; boolean isNT = System.getProperty("os.name").toLowerCase().equals("windows nt"); File jacl1_file = new File(jacl1); File jacl2_file = new File(jacl2); // if we are running the NT version then we need to write out to jacl2 but // if not then we write out to jacl, we also need to check to make sure jacl // exists before each exec on NT because it might get removed by the user File out_file; if (isNT) { if ( jacl1_file.exists() ) { jacl1_file.delete(); } PrintWriter jacl_out = new PrintWriter(new BufferedWriter( new FileWriter( jacl1_file ))); jacl_out.println("@echo off"); jacl_out.println("cmd.exe /C " + jacl2); jacl_out.close(); out_file = jacl2_file; } else { out_file = jacl1_file; } if ( out_file.exists() ) { out_file.delete(); } PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter( out_file ))); // now we write out the .BAT script to the current output file and run it // remove .bat file cmd echoing out.println("@echo off"); // from the path we get the "DRIVE" and the current directory String path = interp.getWorkingDir().toString(); // write out the drive id out.println( path.substring(0,2) ); // write out the path without double quotes but with % subst out.println( "cd " + escapeWinString(path.substring(2)) ); // we must take special care not to quote the program name // we must also take care to ensure that each "%" becomes a "%%" out.print( escapeWinString(argv[first].toString()) ); out.print(' '); for (int i = (first + 1); i < last; i++) { out.print('"'); out.print( escapeWinString(argv[i].toString()) ); out.print('"'); out.print(' '); } out.println(); out.close(); String[] argStrs = {"command.com", "/C", jacl1}; //System.out.println("now to win exec " + argv[first]); return Runtime.getRuntime().exec(argStrs); } /* *---------------------------------------------------------------------- * * execDefault -- * * This procedure is invoked to process the "exec" call for an unknown * a system. This happens when we do not have special exec code for a * system that the code is running under. It should never get used as * unix and windows are supported and Mac has no exec. * * Results: * Returns the new process. * * Side effects: * See the user documentation. * *---------------------------------------------------------------------- */ private Process execDefault (Interp interp, TclObject argv[], int first, int last) throws IOException, InterruptedException { //we can not do anything to change the current directory in Java String[] strv = new String[last - first]; //System.out.println("def alloc space for " + (last - first) + " Strings"); for (int i=first, j=0; i < last; j++, i++) { strv[j] = argv[i].toString(); //System.out.println("default exec arg = ->" + strv[j] + "<-"); } return Runtime.getRuntime().exec(strv); } } // end ExecCmd