package com.laytonsmith.core.functions; import com.laytonsmith.PureUtilities.CommandExecutor; import com.laytonsmith.PureUtilities.Common.MutableObject; import com.laytonsmith.PureUtilities.Common.StreamUtils; import com.laytonsmith.PureUtilities.Common.StringUtils; import com.laytonsmith.PureUtilities.TermColors; import com.laytonsmith.PureUtilities.Version; import com.laytonsmith.abstraction.StaticLayer; import com.laytonsmith.annotations.api; import com.laytonsmith.annotations.core; import com.laytonsmith.annotations.noboilerplate; import com.laytonsmith.core.CHLog; import com.laytonsmith.core.CHVersion; import com.laytonsmith.core.Optimizable; import com.laytonsmith.core.Prefs; import com.laytonsmith.core.Static; import com.laytonsmith.core.constructs.CArray; import com.laytonsmith.core.constructs.CBoolean; import com.laytonsmith.core.constructs.CByteArray; import com.laytonsmith.core.constructs.CClosure; import com.laytonsmith.core.constructs.CInt; import com.laytonsmith.core.constructs.CNull; import com.laytonsmith.core.constructs.CString; import com.laytonsmith.core.constructs.CVoid; import com.laytonsmith.core.constructs.Construct; import com.laytonsmith.core.constructs.Target; import com.laytonsmith.core.environments.Environment; import com.laytonsmith.core.environments.GlobalEnv; import com.laytonsmith.core.exceptions.CRE.CRECastException; import com.laytonsmith.core.exceptions.CRE.CREIOException; import com.laytonsmith.core.exceptions.CRE.CREInsufficientPermissionException; import com.laytonsmith.core.exceptions.CRE.CREPluginInternalException; import com.laytonsmith.core.exceptions.CRE.CREShellException; import com.laytonsmith.core.exceptions.CRE.CREThrowable; import com.laytonsmith.core.exceptions.ConfigCompileException; import com.laytonsmith.core.exceptions.ConfigRuntimeException; import java.io.BufferedOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintStream; import java.io.Reader; import java.lang.reflect.Field; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.logging.Level; import java.util.logging.Logger; /** * */ @core public class Cmdline { public static String docs() { return "This class contains functions that are mostly only useful for command line scripts, but in general may be used by any script. For" + " more information on running MethodScript from the command line, see [[CommandHelper/Command_Line_Scripting|this wiki page]]."; } @api @noboilerplate public static class sys_out extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{}; } @Override public boolean isRestricted() { return true; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { String msg = Static.MCToANSIColors(args[0].val()); StreamUtils.GetSystemOut().print(msg); if (msg.contains("\033")) { //We have color codes in it, we need to reset them StreamUtils.GetSystemOut().print(TermColors.reset()); } StreamUtils.GetSystemOut().println(); return CVoid.VOID; } @Override public String getName() { return "sys_out"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "void {text} Writes the text to the system's std out. Unlike console(), this does not use anything else to format the output, though in many" + " cases they will behave the same. However, colors and other formatting characters will not \"bleed\" through, so" + " sys_out(color(RED) . 'This is red') will not cause the next line to also be red, so if you need to print multiple lines out, you should" + " manually add \\n to create your linebreaks, and only make one call to sys_out."; } @Override public CHVersion since() { return CHVersion.V3_3_1; } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("Basic usage", "#Note, this is guaranteed to print to standard out\nsys_out('Hello World!')", ":Hello World!") }; } } @api @noboilerplate public static class sys_err extends AbstractFunction { @Override public Class[] thrown() { return new Class[]{}; } @Override public boolean isRestricted() { return true; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { String msg = Static.MCToANSIColors(args[0].val()); PrintStream se = StreamUtils.GetSystemErr(); se.print(msg); if (msg.contains("\033")) { //We have color codes in it, we need to reset them se.print(TermColors.reset()); } se.println(); return CVoid.VOID; } @Override public String getName() { return "sys_err"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "void {text} Writes the text to the system's std err. Unlike console(), this does not use anything else to format the output, though in many" + " cases they will behave nearly the same. However, colors and other formatting characters will not \"bleed\" through, so" + " sys_err(color(RED) . 'This is red') will not cause the next line to also be red, so if you need to print multiple lines out, you should" + " manually add \\n to create your linebreaks, and only make one call to sys_err."; } @Override public CHVersion since() { return CHVersion.V3_3_1; } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("Basic usage", "#Note this is guaranteed to print to standard err\nsys_out('Hello World!')", ":Hello World!") }; } } @api @noboilerplate public static class print_out extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{}; } @Override public boolean isRestricted() { return true; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { String msg = Static.MCToANSIColors(args[0].val()); PrintStream so = StreamUtils.GetSystemOut(); so.print(msg); so.flush(); return CVoid.VOID; } @Override public String getName() { return "print_out"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "void {text} Writes the text to the system's std out, but does not automatically add a newline at the end." + " Unlike console(), this does not use anything else to format the output, though in many" + " cases they will behave the same. Unlike other print methdods, colors and other formatting characters WILL" + " \"bleed\" through, so" + " print_out(color(RED) . 'This is red') will also cause the next line to also be red," + " so if you need to print multiple lines out, you should manually reset the color with print_out(color(RESET))."; } @Override public Version since() { return CHVersion.V3_3_1; } } @api @noboilerplate public static class print_err extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{}; } @Override public boolean isRestricted() { return true; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { String msg = Static.MCToANSIColors(args[0].val()); StreamUtils.GetSystemErr().print(msg); StreamUtils.GetSystemErr().flush(); return CVoid.VOID; } @Override public String getName() { return "print_err"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "void {text} Writes the text to the system's std err, but does not automatically add a newline at the end." + " Unlike console(), this does not use anything else to format the output, though in many" + " cases they will behave the same. Unlike other print methdods, colors and other formatting characters WILL" + " \"bleed\" through, so" + " print_err(color(RED) . 'This is red') will also cause the next line to also be red," + " so if you need to print multiple lines out, you should manually reset the color with print_out(color(RESET))."; } @Override public Version since() { return CHVersion.V3_3_1; } } @api(environments={GlobalEnv.class}) @noboilerplate public static class exit extends AbstractFunction implements Optimizable { @Override public Class<? extends CREThrowable>[] thrown() { return null; } @Override public boolean isRestricted() { return false; } @Override public Boolean runAsync() { return false; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { int exit_code = 0; if (args.length == 1) { exit_code = Static.getInt32(args[0], t); } if (Static.InCmdLine(environment)) { System.exit(exit_code); } return new Echoes.die().exec(t, environment, args); } @Override public String getName() { return "exit"; } @Override public Integer[] numArgs() { return new Integer[]{0, 1}; } @Override public String docs() { return "void {[int]} Exits the program. If this is being run from the command line, works by exiting the interpreter, with " + " the specified exit code (defaulting to 0). If this is being run from in-game, works just like die()."; } @Override public CHVersion since() { return CHVersion.V3_3_1; } @Override public Set<OptimizationOption> optimizationOptions() { return EnumSet.of( OptimizationOption.TERMINAL ); } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("Basic usage", "#Causes the JVM to exit with an exit code of 0\nexit(0)", ""), new ExampleScript("Basic usage", "#Causes the JVM to exit with an exit code of 1\nexit(1)", ""), }; } } @api public static class sys_properties extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return null; } @Override public boolean isRestricted() { return true; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { if (args.length == 1) { String propName = args[0].val(); String prop; if(propName.startsWith("methodscript.")){ prop = getMethodScriptProperties().get(propName); } else { prop = System.getProperty(propName); } if(prop == null){ return CNull.NULL; } return new CString(prop, t); } else { CArray ca = CArray.GetAssociativeArray(t); for (String key : System.getProperties().stringPropertyNames()) { ca.set(key, System.getProperty(key)); } Map<String, String> msProps = getMethodScriptProperties(); for(String key : msProps.keySet()){ ca.set(key, msProps.get(key)); } return ca; } } private Map<String, String> getMethodScriptProperties(){ Map<String, String> map = new HashMap<>(); for(Prefs.PNames name : Prefs.PNames.values()){ map.put("methodscript.preference." + name.config(), Prefs.pref(name).toString()); } return map; } @Override public String getName() { return "sys_properties"; } @Override public Integer[] numArgs() { return new Integer[]{0, 1}; } @Override public String docs() { return "mixed {[propertyName]} If propertyName is set, that single property is returned, or null if that property doesn't exist. If propertyName is not set, an" + " associative array with all the system properties is returned. This mechanism hooks into Java's system property mechanism, and is just a wrapper for" + " that. System properties are more reliable than environmental variables, and so are preferred in cases where they exist. For more information about system" + " properties, see http://docs.oracle.com/javase/tutorial/essential/environment/sysprop.html. In addition, known preferences listed in preferences.ini" + " are also included, starting with the prefix \"methodscript.preference.\""; } @Override public CHVersion since() { return CHVersion.V3_3_1; } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("Gets all properties", "array_size(sys_properties())"), new ExampleScript("Gets a single property", "sys_properties('java.specification.vendor')"), new ExampleScript("Gets a single property", "sys_properties('methodscript.preference.debug-mode')"), }; } } @api public static class get_env extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return null; } @Override public boolean isRestricted() { return true; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { if (args.length == 1) { return new CString(System.getenv(args[0].val()), t); } else { CArray ca = CArray.GetAssociativeArray(t); for (String key : System.getenv().keySet()) { ca.set(key, System.getenv(key)); } return ca; } } @Override public String getName() { return "get_env"; } @Override public Integer[] numArgs() { return new Integer[]{0, 1}; } @Override public String docs() { return "mixed {[variableName]} Returns the environment variable specified, if variableName is set. Otherwise, returns an associative array" + " of all the environment variables."; } @Override public CHVersion since() { return CHVersion.V3_3_1; } } @api public static class set_env extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return null; } @Override public boolean isRestricted() { return true; } @Override public Boolean runAsync() { return null; } @SuppressWarnings({"BroadCatchBlock", "TooBroadCatch", "UseSpecificCatch"}) @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { //TODO: Make this more robust by having a local cache of the environment which we modify, and get_env returns from. Map<String, String> newenv = new HashMap<String, String>(System.getenv()); newenv.put(args[0].val(), args[1].val()); boolean ret; try { Class<?> processEnvironmentClass = Class.forName("java.lang.ProcessEnvironment"); Field theEnvironmentField = processEnvironmentClass.getDeclaredField("theEnvironment"); theEnvironmentField.setAccessible(true); Map<String, String> env = (Map<String, String>) theEnvironmentField.get(null); env.putAll(newenv); Field theCaseInsensitiveEnvironmentField = processEnvironmentClass.getDeclaredField("theCaseInsensitiveEnvironment"); theCaseInsensitiveEnvironmentField.setAccessible(true); Map<String, String> cienv = (Map<String, String>) theCaseInsensitiveEnvironmentField.get(null); cienv.putAll(newenv); ret = true; } catch (NoSuchFieldException e) { try { Class[] classes = Collections.class.getDeclaredClasses(); Map<String, String> env = System.getenv(); for (Class cl : classes) { if ("java.util.Collections$UnmodifiableMap".equals(cl.getName())) { Field field = cl.getDeclaredField("m"); field.setAccessible(true); Object obj = field.get(env); Map<String, String> map = (Map<String, String>) obj; map.clear(); map.putAll(newenv); } } ret = true; } catch (Exception e2) { ret = false; if(Prefs.DebugMode()){ CHLog.GetLogger().e(CHLog.Tags.GENERAL, e2, t); } } } catch (Exception e1) { ret = false; if(Prefs.DebugMode()){ CHLog.GetLogger().e(CHLog.Tags.GENERAL, e1, t); } } return CBoolean.get(ret); } @Override public String getName() { return "set_env"; } @Override public Integer[] numArgs() { return new Integer[]{2}; } @Override public String docs() { return "void {variableName, value} Sets the value of an environment variable. This only changes the environment value in this process, not system-wide." + " This uses some hackery to work, and may not be 100% reliable in all cases, and shouldn't be relied on heavily. It will" + " always work with get_env, however, so you can rely on that mechanism. The value will always be interpreted as a string, so if you are expecting" + " a particular data type on a call to get_env, you will need to manually cast the variable. Arrays will be toString'd as well, but will be accepted."; } @Override public CHVersion since() { return CHVersion.V3_3_1; } } @api @noboilerplate public static class prompt_pass extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CREInsufficientPermissionException.class, CREIOException.class}; } @Override public boolean isRestricted() { return true; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { if(!Static.InCmdLine(environment)){ throw new CREInsufficientPermissionException(getName() + " cannot be used outside of cmdline mode.", t); } boolean mask = true; if(args.length > 1){ mask = Static.getBoolean(args[1]); } String prompt = args[0].val(); Character cha = new Character((char)0); if(mask){ cha = new Character('*'); } jline.console.ConsoleReader reader = null; try { reader = new jline.console.ConsoleReader(); reader.setExpandEvents(false); return new CString(reader.readLine(Static.MCToANSIColors(prompt), cha), t); } catch (IOException ex) { throw new CREIOException(ex.getMessage(), t); } finally { if(reader != null){ reader.shutdown(); } } } @Override public String getName() { return "prompt_pass"; } @Override public Integer[] numArgs() { return new Integer[]{1, 2}; } @Override public String docs() { return "string {prompt, [mask]} Prompts the user for a password. This only works in cmdline mode. If mask is true (default)," + " then the password displays * characters for each password character they type. If mask is false, the field" + " stays blank as they type. What they type is returned once they hit enter."; } @Override public Version since() { return CHVersion.V3_3_1; } } @api @noboilerplate public static class prompt_char extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CREInsufficientPermissionException.class, CREIOException.class}; } @Override public boolean isRestricted() { return true; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { requireCmdlineMode(environment, this, t); String prompt = args[0].val(); StreamUtils.GetSystemOut().print(Static.MCToANSIColors(prompt)); StreamUtils.GetSystemOut().flush(); jline.console.ConsoleReader reader = null; try { reader = new jline.console.ConsoleReader(); reader.setExpandEvents(false); char c = (char)reader.readCharacter(); StreamUtils.GetSystemOut().println(c); return new CString(c, t); } catch (IOException ex) { throw new CREIOException(ex.getMessage(), t); } finally { if(reader != null){ reader.shutdown(); } } } @Override public String getName() { return "prompt_char"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "string {prompt} Prompts the user for a single character. They do not need to hit enter first. This only works" + " in cmdline mode."; } @Override public Version since() { return CHVersion.V3_3_1; } } @api @noboilerplate public static class prompt_line extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CREInsufficientPermissionException.class, CREIOException.class}; } @Override public boolean isRestricted() { return true; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { if(!Static.InCmdLine(environment)){ throw new CREInsufficientPermissionException(getName() + " cannot be used outside of cmdline mode.", t); } String prompt = args[0].val(); jline.console.ConsoleReader reader = null; try { reader = new jline.console.ConsoleReader(); reader.setExpandEvents(false); String line = reader.readLine(Static.MCToANSIColors(prompt)); return new CString(line, t); } catch (IOException ex) { throw new CREIOException(ex.getMessage(), t); } finally { if(reader != null){ reader.shutdown(); } } } @Override public String getName() { return "prompt_line"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "string {prompt} Prompts the user for a line. The line typed is returned once the user presses enter. This" + " only works in cmdline mode."; } @Override public Version since() { return CHVersion.V3_3_1; } } @api @noboilerplate public static class sys_beep extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CREInsufficientPermissionException.class, CREPluginInternalException.class}; } @Override public boolean isRestricted() { return true; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { java.awt.Toolkit.getDefaultToolkit().beep(); return CVoid.VOID; } @Override public String getName() { return "sys_beep"; } @Override public Integer[] numArgs() { return new Integer[]{0}; } @Override public String docs() { return "void {} Emits a system beep, on the system itself, not in game. This is only useful from cmdline."; } @Override public Version since() { return CHVersion.V3_3_1; } } @api @noboilerplate public static class clear_screen extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return null; } @Override public boolean isRestricted() { return true; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { if(Static.InCmdLine(environment)){ try { new jline.console.ConsoleReader().clearScreen(); } catch (IOException ex) { throw new CREIOException(ex.getMessage(), t); } } return CVoid.VOID; } @Override public String getName() { return "clear_screen"; } @Override public Integer[] numArgs() { return new Integer[]{0}; } @Override public String docs() { return "void {} Clears the screen. This only works from cmdline mode, nothing happens otherwise."; } @Override public Version since() { return CHVersion.V3_3_1; } } @api @noboilerplate public static class shell_adv extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CREInsufficientPermissionException.class, CREIOException.class}; } @Override public boolean isRestricted() { return true; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(final Target t, final Environment environment, Construct... args) throws ConfigRuntimeException { if(!Static.InCmdLine(environment)){ if(!Prefs.AllowShellCommands()){ throw new CREInsufficientPermissionException("Shell commands are not allowed. Enable them in preferences.ini.", t); } if(environment.getEnv(GlobalEnv.class).GetDynamicScriptingMode() && !Prefs.AllowDynamicShell()){ throw new CREInsufficientPermissionException("Shell commands are disabled from dynamic sources.", t); } } String[] command; File workingDir = null; CClosure stdout = null; CClosure stderr = null; CClosure exit = null; boolean subshell = false; if(args[0] instanceof CArray){ CArray array = (CArray) args[0]; command = new String[(int)array.size()]; for(int i = 0; i < array.size(); i++){ command[i] = array.get(i, t).val(); } } else { command = StringUtils.ArgParser(args[0].val()).toArray(new String[0]); } if(args.length > 1){ CArray options = Static.getArray(args[1], t); if(options.containsKey("workingDir") && !(options.get("workingDir", t) instanceof CNull)){ workingDir = new File(options.get("workingDir", t).val()); if(!workingDir.isAbsolute()){ workingDir = new File(t.file().getParentFile(), workingDir.getPath()); } } if(options.containsKey("stdout") && !(options.get("stdout", t) instanceof CNull)){ stdout = Static.getObject(options.get("stdout", t), t, CClosure.class); } if(options.containsKey("stderr") && !(options.get("stderr", t) instanceof CNull)){ stderr = Static.getObject(options.get("stderr", t), t, CClosure.class); } if(options.containsKey("exit") && !(options.get("exit", t) instanceof CNull)){ exit = Static.getObject(options.get("exit", t), t, CClosure.class); } if(options.containsKey("subshell")){ subshell = Static.getBoolean(options.get("subshell", t)); } } final CommandExecutor cmd = new CommandExecutor(command); cmd.setWorkingDir(workingDir); final CClosure _stdout = stdout; final CClosure _stderr = stderr; final CClosure _exit = exit; final MutableObject<StringBuilder> sbout = new MutableObject(new StringBuilder()); final MutableObject<StringBuilder> sberr = new MutableObject(new StringBuilder()); cmd.setSystemOut(new OutputStream() { @Override public void write(int b) throws IOException { if(_stdout == null){ return; } char c = (char)b; if(c == '\n' || b == -1){ try { StaticLayer.GetConvertor().runOnMainThreadAndWait(new Callable<Object>() { @Override public Object call() throws Exception { _stdout.execute(new CString(sbout.getObject(), t)); return null; } }); } catch (Exception ex) { Logger.getLogger(Cmdline.class.getName()).log(Level.SEVERE, null, ex); } sbout.setObject(new StringBuilder()); } else { sbout.getObject().append(c); } } }); cmd.setSystemErr(new OutputStream() { @Override public void write(int b) throws IOException { if(_stderr == null){ return; } char c = (char)b; if(c == '\n' || b == -1){ try { StaticLayer.GetConvertor().runOnMainThreadAndWait(new Callable<Object>() { @Override public Object call() throws Exception { _stderr.execute(new CString(sberr.getObject(), t)); return null; } }); } catch (Exception ex) { Logger.getLogger(Cmdline.class.getName()).log(Level.SEVERE, null, ex); } sberr.setObject(new StringBuilder()); } else { sberr.getObject().append(c); } } }); try { cmd.start(); } catch (IOException ex) { throw new CREIOException(ex.getMessage(), t); } Runnable run = new Runnable() { @Override public void run() { environment.getEnv(GlobalEnv.class).GetDaemonManager().activateThread(null); try { final int exitCode = cmd.waitFor(); try { cmd.getSystemOut().flush(); if(cmd.getSystemOut() != StreamUtils.GetSystemOut()){ cmd.getSystemOut().close(); } } catch (IOException ex) { Logger.getLogger(Cmdline.class.getName()).log(Level.SEVERE, null, ex); } try { cmd.getSystemErr().flush(); if(cmd.getSystemErr() != StreamUtils.GetSystemErr()){ cmd.getSystemErr().close(); } } catch (IOException ex) { Logger.getLogger(Cmdline.class.getName()).log(Level.SEVERE, null, ex); } if(_exit != null){ try { StaticLayer.GetConvertor().runOnMainThreadAndWait(new Callable<Object>() { @Override public Object call() throws Exception { _exit.execute(new CInt(exitCode, t)); return null; } }); } catch (Exception ex) { Logger.getLogger(Cmdline.class.getName()).log(Level.SEVERE, null, ex); } } } catch (InterruptedException ex) { throw ConfigRuntimeException.CreateUncatchableException(ex.getMessage(), t); } finally { environment.getEnv(GlobalEnv.class).GetDaemonManager().deactivateThread(null); } } }; if(subshell){ new Thread(run, "shell-adv-subshell (" + StringUtils.Join(command, " ") + ")").start(); } else { run.run(); } return CVoid.VOID; } @Override public String getName() { return "shell_adv"; } @Override public Integer[] numArgs() { return new Integer[]{1, 2}; } @Override public String docs() { return "void {command, [options]} Runs a shell command. <code>command</code> can either be a string or an array of string arguments," + " which are run as an external process. Requires the allow-shell-commands option to be enabled in preferences, or run from command line, otherwise" + " an InsufficientPermissionException is thrown. ---- <code>options</code> is an associative array with zero or more" + " of the following options:\n\n" + "{| border=\"1\" class=\"wikitable\" cellspacing=\"1\" cellpadding=\"1\"\n" + "|-\n| workingDir || Sets the working directory for" + " the sub process. By default null, which represents the directory of this script." + " If the path is relative, it is relative to the directory of this script.\n" + "|-\n| stdout || A closure which receives the program" + " output to stdout line by line. The closure should accept a single string, which will be a line.\n" + "|-\n| stderr || A closure which receives the program output to stderr line by line. The closure should accept a single string," + " which should be a line.\n" + "|-\n| exit || A closure which is triggered one time, and contains the process's exit code, once it terminates.\n" + "|-\n| subshell || A boolean. If true, the process will not block, and script execution will continue. If false (default)" + " script execution will halt until the process exits.\n" + "|}"; } @Override public Version since() { return CHVersion.V3_3_1; } } @api @noboilerplate public static class shell extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CREInsufficientPermissionException.class, CREShellException.class, CREIOException.class}; } @Override public boolean isRestricted() { return true; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { if(!Static.InCmdLine(environment)){ if(!Prefs.AllowShellCommands()){ throw new CREInsufficientPermissionException("Shell commands are not allowed. Enable them in preferences.ini.", t); } if(environment.getEnv(GlobalEnv.class).GetDynamicScriptingMode() && !Prefs.AllowDynamicShell()){ throw new CREInsufficientPermissionException("Shell commands are disabled from dynamic sources.", t); } } String[] command; int expectedExitCode = 0; File workingDir = null; if(args[0] instanceof CArray){ CArray array = (CArray) args[0]; command = new String[(int)array.size()]; for(int i = 0; i < array.size(); i++){ command[i] = array.get(i, t).val(); } } else { command = StringUtils.ArgParser(args[0].val()).toArray(new String[0]); } if(args.length > 1){ CArray options = Static.getArray(args[1], t); if(options.containsKey("expectedExitCode")){ expectedExitCode = Static.getInt32(options.get("expectedExitCode", t), t); } if(options.containsKey("workingDir") && !(options.get("workingDir", t) instanceof CNull)){ workingDir = new File(options.get("workingDir", t).val()); if(!workingDir.isAbsolute()){ workingDir = new File(t.file().getParentFile(), workingDir.getPath()); } } } CommandExecutor cmd = new CommandExecutor(command); final StringBuilder sout = new StringBuilder(); OutputStream out = new BufferedOutputStream(new OutputStream() { @Override public void write(int b) throws IOException { sout.append((char)b); } }); final StringBuilder serr = new StringBuilder(); OutputStream err = new BufferedOutputStream(new OutputStream() { @Override public void write(int b) throws IOException { serr.append((char)b); } }); cmd.setSystemOut(out).setSystemErr(err).setWorkingDir(workingDir); try { int exitCode = cmd.start().waitFor(); try{ if(exitCode != expectedExitCode){ err.flush(); throw new CREShellException(serr.toString(), t); } else { out.flush(); return new CString(sout.toString(), t); } } finally { out.close(); err.close(); } } catch (IOException ex) { throw new CREIOException(ex.getMessage(), t); } catch(InterruptedException ex){ throw ConfigRuntimeException.CreateUncatchableException(ex.getMessage(), t); } } @Override public String getName() { return "shell"; } @Override public Integer[] numArgs() { return new Integer[]{1, 2}; } @Override public String docs() { return "string {command, [options]} Runs a shell command. <code>command</code> can be either a string, or array of string" + " arguments. This works mostly like {{function|shell_adv}} however, it buffers then" + " returns the output for sysout once the process is completed, and throws a ShellException with the exception" + " message set to the syserr output if the" + " process exits with an exit code that isn't the expectedExitCode, which defaults to 0. This is useful for simple commands" + " that return output and don't need very complicated usage, and failures don't need to check the exact error code." + " If the underlying command throws an IOException, it is" + " passed through. Requires the allow-shell-commands option to be enabled in preferences, or run from command line, otherwise" + " an InsufficientPermissionException is thrown. Options is an associative array which expects zero or more" + " of the following options: expectedErrorCode - The expected error code indicating successful command completion. Defaults to 0." + " workingDir - Sets the working directory for the sub process. By default null, which represents the directory of this script." + " If the path is relative, it is relative to the directory of this script."; } @Override public Version since() { return CHVersion.V3_3_1; } @Override public com.laytonsmith.core.functions.ExampleScript[] examples() throws com.laytonsmith.core.exceptions.ConfigCompileException { return new com.laytonsmith.core.functions.ExampleScript[]{ new com.laytonsmith.core.functions.ExampleScript("Basic usage with array", "shell(array('grep', '-r', 'search content', '*'))", "<output of command>"), new com.laytonsmith.core.functions.ExampleScript("Basic usage with string", "shell('grep -r \"search content\" *')", "<output of command>"), new com.laytonsmith.core.functions.ExampleScript("Changing the working directory", "shell('grep -r \"search content\" *', array(workingDir: '/'))", "<output of command>"), }; } } @api public static class read_pipe_input extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CREIOException.class, CREInsufficientPermissionException.class}; } @Override public boolean isRestricted() { return true; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { if(!Static.InCmdLine(environment)){ throw new CREInsufficientPermissionException(getName() + " cannot be used outside of cmdline mode.", t); } if(System.console() != null){ throw new CREIOException(getName() + " can only be used in TTY mode.", t); } boolean binary = false; if(args.length > 0){ binary = Static.getBoolean(args[0]); } try { if (binary) { CByteArray ba = new CByteArray(t); while (true) { int b = System.in.read(); if (b < 0) { break; } ba.putByte((byte) b, null); } return ba; } else { Reader r = new InputStreamReader(System.in); StringBuilder b = new StringBuilder(); while (true) { int ch; ch = r.read(); if (ch < 0) { break; } b.append((char) ch); } return new CString(b.toString(), t); } } catch (IOException ex) { throw new CREIOException(ex.getMessage(), t, ex); } } @Override public String getName() { return "read_pipe_input"; } @Override public Integer[] numArgs() { return new Integer[]{0, 1}; } @Override public String docs() { return "mixed {[binary]} Reads the input from a process that is piped to this script. It is assumed that the" + " data piped to the script will come all at once, and it will be returned as a string (or byte_array if binary is true)." + " This can only be used in cmdline mode, and binary defaults to false. If the script isn't started in TTY mode," + " an IOException is thrown."; } @Override public Version since() { return CHVersion.V3_3_1; } } @api public static class pwd extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CREShellException.class}; } @Override public boolean isRestricted() { return true; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { File root; if(Static.InCmdLine(environment)){ root = environment.getEnv(GlobalEnv.class).GetRootFolder(); } else { if (t.file() == null) { root = null; } else { root = t.file().getParentFile(); } } if(root == null){ throw new CREShellException("Running in interpreted mode. pwd() is not available.", t); } else { try { String ret = root.getCanonicalPath(); return new CString(ret, t); } catch (IOException ex) { //This shouldn't happen, because the current working directory will only be //set programmatically. throw new RuntimeException(ex); } } } @Override public String getName() { return "pwd"; } @Override public Integer[] numArgs() { return new Integer[]{0}; } @Override public String docs() { return "string {} Returns the path to the current working directory. This is available outside cmdline mode, but" + " is probably only useful for debugging, meta, or informational purposes when not in cmdline interpreter mode," + " as the current working directory is known simply by knowing what file this is running from. When run from" + " a context where there is no working directory, a ShellException is thrown."; } @Override public Version since() { return CHVersion.V3_3_1; } } @api public static class cd extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CREIOException.class, CREInsufficientPermissionException.class}; } @Override public boolean isRestricted() { return true; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { requireCmdlineMode(environment, this, t); File cd = Static.GetFileFromArgument(args.length == 0 ? null : args[0].val(), environment, t, new File(System.getProperty("user.home"))); if(!cd.exists()){ throw new CREIOException("No such file or directory: " + cd.getPath(), t); } environment.getEnv(GlobalEnv.class).SetRootFolder(cd); return CVoid.VOID; } @Override public String getName() { return "cd"; } @Override public Integer[] numArgs() { return new Integer[]{0, 1}; } @Override public String docs() { return "void {[dir]} Changes the current working directory to the path specified, or the user's home" + " directory if omitted. This only works from cmdline mode."; } @Override public Version since() { return CHVersion.V3_3_1; } } @api public static class ls extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CREInsufficientPermissionException.class}; } @Override public boolean isRestricted() { return true; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { requireCmdlineMode(environment, this, t); CArray ca = new CArray(t); File cwd = Static.GetFileFromArgument(args.length > 0 ? args[0].val() : null, environment, t, environment.getEnv(GlobalEnv.class).GetRootFolder()); if(cwd.exists()){ for(File f : cwd.listFiles()){ ca.push(new CString(f.getName(), t), t); } } else { throw new CREIOException("No such file or directory: " + cwd.getPath(), t); } return ca; } @Override public String getName() { return "ls"; } @Override public Integer[] numArgs() { return new Integer[]{0, 1}; } @Override public String docs() { return "array {[directory]} Returns an array of files in the current working directory, including \"hidden\" files, or" + "if directory is specified, the files in that directory. This is only available in cmdline mode."; } @Override public Version since() { return CHVersion.V3_3_1; } } @api public static class set_cmdline_prompt extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CRECastException.class, CREInsufficientPermissionException.class}; } @Override public boolean isRestricted() { return true; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { requireCmdlineMode(environment, this, t); if(!(args[0] instanceof CClosure)){ throw new CRECastException("Expecting a closure for argument 1 of " + getName(), t); } environment.getEnv(GlobalEnv.class).SetCustom("cmdline_prompt", args[0]); return CVoid.VOID; } @Override public String getName() { return "set_cmdline_prompt"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "void {closure} Sets the cmdline prompt. This is only usable or useful in cmdline interpreter mode. The closure should" + " return a string, that string will be used as the prompt. The closure is called each time a prompt needs generating," + " thereby allowing for dynamic prompts."; } @Override public Version since() { return CHVersion.V3_3_1; } } @api public static class get_terminal_width extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CREInsufficientPermissionException.class}; } @Override public boolean isRestricted() { return true; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { requireCmdlineMode(environment, this, t); try { int i = new jline.console.ConsoleReader().getTerminal().getWidth(); return new CInt(i, t); } catch (IOException ex) { throw new CREIOException(ex.getMessage(), t, ex); } } @Override public String getName() { return "get_terminal_width"; } @Override public Integer[] numArgs() { return new Integer[]{0}; } @Override public String docs() { return "int {} Returns the current width of the terminal, measured in characters. This is" + " useful for determining proper layout for dynamic output. This only works in cmdline mode."; } @Override public Version since() { return CHVersion.V3_3_1; } } @api public static class user extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return null; } @Override public boolean isRestricted() { return false; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { String name = StaticLayer.GetConvertor().GetUser(environment); if(name == null){ return CNull.NULL; } else { return new CString(name, t); } } @Override public String getName() { return "user"; } @Override public Integer[] numArgs() { return new Integer[]{0}; } @Override public String docs() { return "string {} Returns the name of the current user. This is retrieved in a platform specific manner, and should" + " be cross compatible in all scripts. Null is returned if this function call is non-sensical in the" + " current platform"; } @Override public Version since() { return CHVersion.V3_3_1; } } @api public static class in_cmdline_mode extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return null; } @Override public boolean isRestricted() { return false; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { return CBoolean.GenerateCBoolean(Static.InCmdLine(environment), t); } @Override public String getName() { return "in_cmdline_mode"; } @Override public Integer[] numArgs() { return new Integer[]{0}; } @Override public String docs() { return "boolean {} Returns true if the environment is in cmdline mode. False otherwise."; } @Override public Version since() { return CHVersion.V3_3_2; } } /** * Requires cmdline mode. If not currently in cmdline mode, a proper CRE is thrown. * @param environment * @param f The function this is being called from. * @param t * @throws ConfigRuntimeException If not in cmdline mode. */ public static void requireCmdlineMode(Environment environment, Function f, Target t) throws ConfigRuntimeException { if(!Static.InCmdLine(environment)){ throw new CREInsufficientPermissionException(f.getName() + " cannot be used outside of cmdline mode.", t); } } }