UNPKG

nscript

Version:
234 lines (213 loc) 7.93 kB
#!/usr/bin/env node /* * nscript: javascript shell scripts for the masses * * (c) 2014 - Michel Weststrate */ /* GLOBAL exports,module */ // FEATURE: support travis build integration var Fiber = require('fibers'); var path = require('path'); var utils = require('./utils.js'); var program = require('commander'); var fs = require('fs'); /** * Runs a function using nscript. Params will be aliased @see nscript.alias based on their name, except for the first one, which will be replaced by nscript itself. * @param {function} func */ var runNscriptFunction = module.exports = function(func, callback) { //parse and args if (typeof func !== "function") throw new Error("Not a function: " + func + ", the script file should be in the form 'module.exports = function(shell) { }' or 'require(\"nscript\")(function(shell) { })"); var args = utils.extractFunctionArgumentNames(func); args = injectArguments(args, [].concat(scriptArgs)); //invoke new Fiber(function() { var retValue; var gotException = true; try { retValue = func.apply(null, args); if (shell.verbose()) console.warn("Finished in " + process.uptime() + " seconds"); gotException = false; } catch (e) { if (callback) callback(e); else { console.error("Uncaught exception in nscript function: " + e); console.error(e.stack); } } if (!gotException && callback) callback(null, retValue); }).run(); }; runNscriptFunction.nscript = runNscriptFunction; // For normal module export: import {nscript} from "nscript" /* * Local imports after defining module.exports */ var shell = require('./shell.js'); var repl = require('./repl.js'); var scriptArgs = process.argv.slice(2); //remove node, scriptfile var injectArguments = runNscriptFunction.injectArguments = function(argNames, varArgs) { var argValues = new Array(argNames.length); var secondPass = false; var validOptions = [];//[path.basename(process.argv[2])]; var argsRequired = -1; var parseParams = !!argNames.filter(function(x){return x.indexOf("$") === 0; }).length; // FEATURE: support predefined --verbose --change-dir --help --version function onArg(argName, index) { var idxMatch = argName.match(/^\$(\d+)$/); var paramMatch = argName.match(/^\$\$([A-Za-z0-9_-]+)$/); var flagMatch = argName.match(/^\$([A-Za-z0-9_-]+)$/); var paramName, idx; //always pass in shell as first if (index === 0) { if (secondPass) argValues[index] = shell; } //$args returns all remaining args else if (argName === "$args") { if (secondPass) argValues[index] = varArgs; } //$3 returns the 3th vararg else if (idxMatch) { if (secondPass) argValues[index] = varArgs[idxMatch[1]]; else argsRequired = Math.max(argsRequired, idxMatch[1]); } //$$myArg should parse --my-arg value else if (paramMatch) { if (!secondPass) { paramName = utils.hyphenate(paramMatch[1]); validOptions.push("[" + paramName + " <value>]"); idx = varArgs.indexOf(paramName); if (idx != -1) { if (idx >= varArgs.length - 1) throw "Missing a value for option " + paramName; argValues[index] = varArgs[idx + 1]; varArgs.splice(idx, 2); } } } //$myFlag should parse --my-flag to true else if (flagMatch) { if (!secondPass) { paramName = utils.hyphenate(flagMatch[1]); validOptions.push("[" + paramName + "]"); idx = varArgs.indexOf(paramName); argValues[index] = idx != -1; if (idx != -1) varArgs.splice(idx, 1); } } else if (argName.indexOf('$') === 0) throw new Error("Invalid parametername in nscript function: '" + argName + "', please check the nscript docs for valid parameter names"); else if (secondPass) argValues[index] = shell.alias(argName); } varArgs = utils.normalizeCliFlags(varArgs); //parse all params and flags argNames.forEach(onArg); for(var i = 0; i <= argsRequired; i++) validOptions.push("[arg]"); if (parseParams) { //remaining values should not be flags varArgs.forEach(function(arg) { //script variadic argument values should not start with a hyphen. Rly? yeah, try to touch or git add a file named '-p' for exampe :-P if (arg.indexOf("-") === 0) throw "Invalid option '" + arg + "'. \nUsage: " + validOptions.join(" "); }); // FEATURE: introduce required unnamed arguments? -> $$0 makes are mandatory, in contrast to $0 //if (varArgs.length <= argsRequired) // throw "Missing arguments. Expected at least " + (argsRequired + 1) + " argument(s), found: '" + varArgs.join(' ') + "'"; } //variadic arguments can only be determined reliable after parsing the named args secondPass = true; argNames.forEach(onArg); return argValues; }; /** * Runs a file that contains a nscript script * @param {string} scriptFile */ function runScriptFile(scriptFile) { //node gets the node arguments, the nscript arguments and the actual script args combined. Slice all node and nscript args away! scriptArgs = scriptArgs.slice(scriptArgs.indexOf(scriptFile) + 1); if (shell.verbose()) console.warn("Starting nscript " + scriptFile + scriptArgs.join(" ")); runNscriptFunction(require(path.resolve(process.cwd(), scriptFile))); //nscript scripts should always export a single function that is the main } function touchScript(scriptFile, local) { if (fs.existsSync(scriptFile)) throw new Error("File '" + scriptFile + "' already exists"); console.log("Generating default script in '" + scriptFile + "' " + (local?"[using local nscript]":"")); var demoFunc = [ "function(shell, echo, $0) {", "\t/* This script is powered by 'nscript', see the docs at https://github.com/mweststrate/nscript */", "\techo(\"Hello\", $0 || \"world\")", "}" ].join("\n"); shell.write( scriptFile, local ? "require(nscript)(" + demoFunc + ")" : "module.exports = " + demoFunc ); makeExecutable(scriptFile, local); } function makeExecutable(scriptFile, local) { if (!fs.existsSync(scriptFile)) throw new Error("Filed doesn't exist: " + scriptFile); if (process.platform === 'windows') { console.log("Generating executable script in '" + scriptFile + ".bat' " + (local?"[using local nscript]":"")); shell.write(scriptFile + ".bat", (local ? "node " : "nscript ") + path.basename(scriptFile) + " %+"); } else { console.log("Marking script as executable: '" + scriptFile + "' " + (local?"[using local nscript]":"")); shell.nscript(function(shell, cp, chmod, rm, echo, cat) { var contents = shell.read(scriptFile); if (contents[0] != '#' || contents[1] != '!') contents = (local ? "#!/usr/bin/env node\n" : "#!/usr/bin/nscript\n") + contents; shell.write(scriptFile, contents); chmod("+x", scriptFile); }); } } var version = exports.version = require('../package.json').version; if (!module.parent) { program .version(version) .usage('[options] <file>') .option('-C, --chdir <path>', 'change the working directory') .option('-v, --verbose', 'start in verbose mode') .option('--touch <path>', 'create a new nscript file at the specified location and make it executable') .option('-x <path>', 'make sure the nscript file at the specified location is executable') .option('--local', 'in combination with --touch or -x; do not use global nscript, but the one provided in the embedding npm package'); program.parse(process.argv); if (program.chdir) shell.cd(program.chdir); if (program.verbose) shell.verbose(true); if (program.touch) touchScript(program.touch, program.local); else if (program.X) //MWE: unsure why X is upercased here... makeExecutable(program.X, program.local); else if (process.argv.length > 2) { var script = program.args[0]; try { runScriptFile(script); } catch (e) { if (shell.verbose()) throw e; //propage exception to the console for stack else console.error("" + e); process.exit(8); //same as node exceptions } } else { shell.useGlobals(); repl.start(); } }