From 1c2bb7b036db496a024a5f1f53b769bde067e267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sun, 22 Jun 2014 00:56:30 +0200 Subject: [PATCH 01/94] Start using REPL instead of plain readline --- nsh.js | 212 ++++++++------------------------------------------ shellUtils.js | 155 ++++++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+), 178 deletions(-) create mode 100644 shellUtils.js diff --git a/nsh.js b/nsh.js index b4007ad..2df9850 100755 --- a/nsh.js +++ b/nsh.js @@ -1,190 +1,46 @@ #!/usr/bin/env node -var fs = require('fs'); -var glob = require('glob'); -var path = require('path'); -var rl = require('readline'); -var cp = require('child_process'); -var parse = require('lib-cmdparse'); -var ps = require('lib-pathsearch'); -var pc = require('lib-pathcomplete'); +var rl = require('readline'); -var state = { - "?" : null -} - -var execinfo_path = process.env.PATH.split(':'); - -// auto-complete handler -function completer (line, callback) { - var split = line.split(/\s+/); - var item = split.pop(); - var outs = []; +var repl = require("repl"); - // avoid crazy auto-completions when the line is empty - if (!line) return callback(1); +var shellUtils = require('./shellUtils'); +var completer = shellUtils.completer; +var readline = shellUtils.readline; - // user is attempting to type a relative directory - var is_rel = item[0] === '.'; - - // user is typing first command - var is_first = split.length === 0; - - // if this is the first token on the line - // autocomplete it against commands in the search path - if (!is_rel && is_first) ps(item, execinfo_path, function (err, execs) { - // if there is only one executable, append a space after it - if (execs.length === 1) execs[0] = execs[0] + ' '; - callback(err, [execs, item]); - }); - - // if this is - else pc(item, function (err, arr, info) { - // if there is only one completion, and it's a directory - // automatically append a '/' to the completion - if (arr.length === 1) { - if (fs.statSync(info.dir + arr[0]).isDirectory()) - callback(err, [[arr[0] + '/'], info.file]); - else callback(err, [arr, info.file]); - } - else callback(err, [arr, info.file]); - }); - -} -var iface = rl.createInterface({ - input : process.stdin, - output : process.stdout, - completer : completer -}); - -// visually indicate closed pipe with ^D -// otherwise exiting nested shells is confusing -iface.on('close', function () { - process.stdout.write('^D\n'); -}); - -// handle ^C like bash -iface.on('SIGINT', function () { - process.stdout.write('^C'); - iface.clearLine(); - prompt(); -}); - -process.on('SIGINT', function () { - // ignore -}); - -function readline(line){ - line = interpolate(line.trim(), process.env); - line = interpolate(line, state); - - if (line && line.length > 0) { - if (line.substring(0,2) === 'cd'){ - // cd is a native command - var dir = line.substring(2).trim(); - if (dir.length === 0) dir=process.env.HOME; - - // should not crash process with a bad 'cd' - try { - process.chdir(dir); - } catch (e) { - console.log(e); - } - - setImmediate(prompt); - }else{ - // other commands - run(line); +// Main method +if(!module.parent){ + function prompt(){ + var prefix; + try { + prefix = process.cwd(); + } catch (e) { + prefix = "(none)"; } - } else { - setImmediate(prompt); - } -} - -process.on('close',function(){ - process.exit(0); -}); - -// replace $VARs with environment variables -function interpolate(string, replace){ - return string.replace(/\$[^\s]+/g, function (key){ - var name = key.substring(1); - - var out; - if(replace[name] || replace[name] === 0){ - out = replace[name]; - } else { - out = key; + return prefix + " # "; + }; + + var replserver = repl.start( + { + prompt: prompt(), + eval: function(cmd, context, filename, callback) + { + // Hack for REPL + if(cmd[0] == '(') return callback(new SyntaxError); + + readline(cmd, function(error, result) + { + callback(error, result); + }); } - - return out; - }); -} - -function run(line){ - // allow for setting environment variables - // on the command line - var stanza = parse(line); - - // fallback to current environment - stanza.envs.__proto__ = process.env; - - // We must stop reading STDIN because we will soon - // be the background process group, which will raise - // errors when attempting to read/write to the TTY driver - process.stdin.setRawMode(false) - process.stdin.pause(); - - // Sub-Process - var args = stanza.args; - var exec = stanza.exec; - var proc = cp.spawn(exec,args,{ - cwd: process.cwd(), - env: stanza.envs, - - // Inerit the terminal - stdio: 'inherit' }); - // Have this shell resume control after the sub-process exists - function res(){ - process.stdin.setRawMode(true) - process.stdin.resume(); - prompt(); - } + var displayPrompt = replserver.displayPrompt; + replserver.displayPrompt = function(){ + replserver.prompt = prompt(); + displayPrompt.call(this); + }; - // catch exit code - function end(code, signal){ - // the $? variable should contain the exit code - state["?"] = code; - state["??"] = signal; - - res(); - } - - // catch errors - function err(err){ - res(); - } - - proc.on('error', err); - proc.on('exit', end); -} - -function prompt(){ - var prefix; - try { - prefix = process.cwd(); - } catch (e) { - prefix = "(none)"; - } - iface.question(prefix + " # ", function (line) { - readline(line); - }); -} - -// Main method -if(!module.parent){ - prompt(); + replserver.complete = completer; } diff --git a/shellUtils.js b/shellUtils.js new file mode 100644 index 0000000..bf56c7e --- /dev/null +++ b/shellUtils.js @@ -0,0 +1,155 @@ +var cp = require('child_process'); +var fs = require('fs'); + +var parse = require('lib-cmdparse'); +var pc = require('lib-pathcomplete'); +var ps = require('lib-pathsearch'); + + +var execinfo_path = process.env.PATH.split(':'); + +var state = { + "?" : null +} + + +function readline(line, callback){ + line = line.trim(); + line = interpolate(line, process.env); + line = interpolate(line, state); + + if (line && line.length > 0) { + if (line.substring(0,2) === 'cd'){ + // cd is a native command + var dir = line.substring(2).trim(); + if (dir.length === 0) dir=process.env.HOME; + + // should not crash process with a bad 'cd' + try { + process.chdir(dir); + } catch (e) { + return callback(e); +// console.log(e); + } + + callback(); + }else{ + // other commands + run(line, callback); + } + } else { + callback(); + } +} + +// replace $VARs with environment variables +function interpolate(string, replace){ + return string.replace(/\$[^\s]+/g, function (key){ + var name = key.substring(1); + + var out; + if(replace[name] || replace[name] === 0){ + out = replace[name]; + } else { + out = key; + } + + return out; + }); +} + +function run(line, callback){ + // allow for setting environment variables + // on the command line + var stanza = parse(line); + + // fallback to current environment + stanza.envs.__proto__ = process.env; + + // We must stop reading STDIN because we will soon + // be the background process group, which will raise + // errors when attempting to read/write to the TTY driver + process.stdin.setRawMode(false) + process.stdin.pause(); + + // Sub-Process + var args = stanza.args; + var exec = stanza.exec; + var proc = cp.spawn(exec,args,{ + cwd: process.cwd(), + env: stanza.envs, + + // Inerit the terminal + stdio: 'inherit' + }); + + // Have this shell resume control after the sub-process exists + function res(){ + process.stdin.setRawMode(true) + process.stdin.resume(); + + callback(); + } + + // catch exit code + function end(code, signal){ + // the $? variable should contain the exit code + state["?"] = code; + state["??"] = signal; + + res(); + } + + // catch errors + function err(err){ + res(); + } + + proc.on('error', err); + proc.on('exit', end); +} + + +// auto-complete handler +function completer (line, callback) { + var split = line.split(/\s+/); + var item = split.pop(); + var outs = []; + + // avoid crazy auto-completions when the line is empty + if (!line) return callback(1); + + // user is attempting to type a relative directory + var is_rel = item[0] === '.'; + + // user is typing first command + var is_first = split.length === 0; + + // if this is the first token on the line + // autocomplete it against commands in the search path + if (!is_rel && is_first) + ps(item, execinfo_path, function (err, execs) { + // if there is only one executable, append a space after it + if (execs.length === 1) + execs[0] += ' '; + + callback(err, [execs, item]); + }); + + // if this is not the first token on the line + // autocomplete it against items in current dir + else + pc(item, function (err, arr, info) { + // if there is only one completion, and it's a directory + // automatically append a '/' to the completion + if (arr.length === 1 + && fs.statSync(info.dir + arr[0]).isDirectory()) + arr[0] += '/'; + + callback(err, [arr, info.file]); + }); +} + + +exports.completer = completer; +exports.readline = readline; From 42f8bd8ccd3473734eb0db353c4fa9dcccd42279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sun, 22 Jun 2014 16:44:11 +0200 Subject: [PATCH 02/94] Accept both Javascript statements and command line instructions And it feels really natural... ;-) --- nsh.js | 56 +++++++++++++++++++++++++++++++++++++++++----------- package.json | 3 ++- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/nsh.js b/nsh.js index 2df9850..21038ce 100755 --- a/nsh.js +++ b/nsh.js @@ -1,8 +1,8 @@ #!/usr/bin/env node -var rl = require('readline'); +var repl = require('repl'); -var repl = require("repl"); +var parallel = require('async').parallel; var shellUtils = require('./shellUtils'); var completer = shellUtils.completer; @@ -23,18 +23,28 @@ if(!module.parent){ var replserver = repl.start( { - prompt: prompt(), - eval: function(cmd, context, filename, callback) + prompt: prompt() + }); + + var jsEval = replserver.eval; + replserver.eval = function(cmd, context, filename, callback) + { + // Javascript + jsEval.call(this, cmd, context, filename, function(error, result) { - // Hack for REPL - if(cmd[0] == '(') return callback(new SyntaxError); + if(!error) return callback(null, result); + + // Shell + if(cmd[0] == '(') cmd = cmd.substring(1, cmd.length-1); - readline(cmd, function(error, result) + readline(cmd, function(error2, result) { - callback(error, result); + if(error2) return callback(error); + + callback(null, result); }); - } - }); + }); + }; var displayPrompt = replserver.displayPrompt; replserver.displayPrompt = function(){ @@ -42,5 +52,29 @@ if(!module.parent){ displayPrompt.call(this); }; - replserver.complete = completer; + var jsComplete = replserver.complete.bind(replserver); + replserver.complete = function(line, callback) + { + parallel( + [ + function(callback) + { + jsComplete(line, callback) + }, + function(callback) + { + completer(line, callback) + } + ], + function(error, results) + { + if(error) return callback(error); + + var execs = results[0][0].concat(results[1][0]); + var item = results[0][1]; + + callback(null, [execs, item]); + } + ) + }; } diff --git a/package.json b/package.json index a768606..ce4a585 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,9 @@ "nsh": "nsh.js" }, "dependencies": { - "lib-cmdparse": "~0.1.0", + "async": "^0.9.0", "glob": "~3.2.7", + "lib-cmdparse": "~0.1.0", "lib-pathcomplete": "0.0.1", "lib-pathsearch": "~0.1.0", "mkdirp": "~0.3.5" From 74401517d7592d00bae5551675655390235bc91c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=27piranna?= Date: Tue, 24 Nov 2015 19:29:08 +0100 Subject: [PATCH 03/94] Clean-up --- bin/nsh.js | 70 +++++++++++++++++++++ lib/completer.js | 51 ++++++++++++++++ lib/index.js | 6 ++ lib/readline.js | 113 ++++++++++++++++++++++++++++++++++ nsh.js | 80 ------------------------ package.json | 3 +- shellUtils.js | 155 ----------------------------------------------- 7 files changed, 242 insertions(+), 236 deletions(-) create mode 100644 bin/nsh.js create mode 100644 lib/completer.js create mode 100644 lib/index.js create mode 100644 lib/readline.js delete mode 100755 nsh.js delete mode 100644 shellUtils.js diff --git a/bin/nsh.js b/bin/nsh.js new file mode 100644 index 0000000..f3253d8 --- /dev/null +++ b/bin/nsh.js @@ -0,0 +1,70 @@ +#!/usr/bin/env node + +var repl = require('repl'); + +var parallel = require('async').parallel; + +var nsh = require('..'); +var completer = nsh.completer; +var readline = nsh.readline; + + +function prompt(){ + var prefix; + try { + prefix = process.cwd(); + } catch (e) { + prefix = "(none)"; + } + return prefix + " > "; +}; + +var replserver = repl.start( +{ + prompt: prompt() +}); + +var jsEval = replserver.eval; +replserver.eval = function(cmd, context, filename, callback) +{ + // Javascript + jsEval.call(this, cmd, context, filename, function(error, result) + { + if(!error) return callback(null, result); + + // Shell + if(cmd[0] == '(') cmd = cmd.substring(1, cmd.length-1); + + readline(cmd, function(error2, result) + { + if(error2) return callback(error); + + callback(null, result); + }); + }); +}; + +var displayPrompt = replserver.displayPrompt; +replserver.displayPrompt = function(){ + replserver.prompt = prompt(); + displayPrompt.call(this); +}; + +var jsComplete = replserver.complete.bind(replserver); +replserver.complete = function(line, callback) +{ + parallel( + [ + jsComplete.bind(jsComplete, line), + completer.bind(undefined, line) + ], + function(error, results) + { + if(error) return callback(error); + + var execs = results[0][0].concat(results[1][0]); + var item = results[0][1]; + + callback(null, [execs, item]); + }) +}; diff --git a/lib/completer.js b/lib/completer.js new file mode 100644 index 0000000..1a9896c --- /dev/null +++ b/lib/completer.js @@ -0,0 +1,51 @@ +var fs = require('fs') + +var pc = require('lib-pathcomplete') +var ps = require('lib-pathsearch') + + +var execinfo_path = process.env.PATH.split(':'); + + +// auto-complete handler +function completer (line, callback) { + var split = line.split(/\s+/); + var item = split.pop(); + var outs = []; + + // avoid crazy auto-completions when the line is empty + if (!line) return callback(1); + + // user is attempting to type a relative directory + var is_rel = item[0] === '.'; + + // user is typing first command + var is_first = split.length === 0; + + // if this is the first token on the line + // autocomplete it against commands in the search path + if (!is_rel && is_first) + ps(item, execinfo_path, function (err, execs) { + // if there is only one executable, append a space after it + if (execs.length === 1) + execs[0] += ' '; + + callback(err, [execs, item]); + }); + + // if this is not the first token on the line + // autocomplete it against items in current dir + else + pc(item, function (err, arr, info) { + // if there is only one completion, and it's a directory + // automatically append a '/' to the completion + if (arr.length === 1 + && fs.statSync(info.dir + arr[0]).isDirectory()) + arr[0] += '/'; + + callback(err, [arr, info.file]); + }); +} + + +module.exports = completer diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..acb207a --- /dev/null +++ b/lib/index.js @@ -0,0 +1,6 @@ +var completer = require('./completer') +var readline = require('./readline') + + +exports.completer = completer +exports.readline = readline diff --git a/lib/readline.js b/lib/readline.js new file mode 100644 index 0000000..e588bc2 --- /dev/null +++ b/lib/readline.js @@ -0,0 +1,113 @@ +var cp = require('child_process'); + +var parse = require('lib-cmdparse'); + + +var state = { + "?" : null +} + + +function cd(line, callback) +{ + var dir = line.trim() || process.env.HOME + + // should not crash process with a bad 'cd' + try { + process.chdir(dir); + } catch (e) { + return callback(e); +// console.log(e); + } + + callback(); +} + + +function readline(line, callback){ + line = line.trim(); + line = interpolate(line, process.env); + line = interpolate(line, state); + + if(line && line.length) + { + // cd is a native command + if(line.substring(0,2) === 'cd') + return cd(line.substring(2), callback) + + // other commands + return run(line, callback) + } + + callback() +} + +// replace $VARs with environment variables +function interpolate(string, replace){ + return string.replace(/\$[^\s]+/g, function (key){ + var name = key.substring(1); + + var out; + if(replace[name] || replace[name] === 0){ + out = replace[name]; + } else { + out = key; + } + + return out; + }); +} + +function run(line, callback){ + // allow for setting environment variables + // on the command line + var stanza = parse(line); + + // fallback to current environment + stanza.envs.__proto__ = process.env; + + // We must stop reading STDIN because we will soon + // be the background process group, which will raise + // errors when attempting to read/write to the TTY driver + process.stdin.setRawMode(false) + process.stdin.pause(); + + // Sub-Process + var args = stanza.args; + var exec = stanza.exec; + var proc = cp.spawn(exec,args,{ + cwd: process.cwd(), + env: stanza.envs, + + // Inerit the terminal + stdio: 'inherit' + }); + + // Have this shell resume control after the sub-process exists + function res(){ + process.stdin.setRawMode(true) + process.stdin.resume(); + + callback(); + } + + // catch exit code + function end(code, signal){ + // the $? variable should contain the exit code + state["?"] = code; + state["??"] = signal; + + res(); + } + + // catch errors + function err(err){ + res(); + } + + proc.on('error', err); + proc.on('exit', end); +} + + +module.exports = readline diff --git a/nsh.js b/nsh.js deleted file mode 100755 index 21038ce..0000000 --- a/nsh.js +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env node - -var repl = require('repl'); - -var parallel = require('async').parallel; - -var shellUtils = require('./shellUtils'); -var completer = shellUtils.completer; -var readline = shellUtils.readline; - - -// Main method -if(!module.parent){ - function prompt(){ - var prefix; - try { - prefix = process.cwd(); - } catch (e) { - prefix = "(none)"; - } - return prefix + " # "; - }; - - var replserver = repl.start( - { - prompt: prompt() - }); - - var jsEval = replserver.eval; - replserver.eval = function(cmd, context, filename, callback) - { - // Javascript - jsEval.call(this, cmd, context, filename, function(error, result) - { - if(!error) return callback(null, result); - - // Shell - if(cmd[0] == '(') cmd = cmd.substring(1, cmd.length-1); - - readline(cmd, function(error2, result) - { - if(error2) return callback(error); - - callback(null, result); - }); - }); - }; - - var displayPrompt = replserver.displayPrompt; - replserver.displayPrompt = function(){ - replserver.prompt = prompt(); - displayPrompt.call(this); - }; - - var jsComplete = replserver.complete.bind(replserver); - replserver.complete = function(line, callback) - { - parallel( - [ - function(callback) - { - jsComplete(line, callback) - }, - function(callback) - { - completer(line, callback) - } - ], - function(error, results) - { - if(error) return callback(error); - - var execs = results[0][0].concat(results[1][0]); - var item = results[0][1]; - - callback(null, [execs, item]); - } - ) - }; -} diff --git a/package.json b/package.json index ce4a585..4fbd2e3 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,9 @@ "description": "Node/No Shell", "author": "Jacob Groundwater ", "license": "MIT", + "main": "lib", "bin": { - "nsh": "nsh.js" + "nsh": "bin/nsh.js" }, "dependencies": { "async": "^0.9.0", diff --git a/shellUtils.js b/shellUtils.js deleted file mode 100644 index bf56c7e..0000000 --- a/shellUtils.js +++ /dev/null @@ -1,155 +0,0 @@ -var cp = require('child_process'); -var fs = require('fs'); - -var parse = require('lib-cmdparse'); -var pc = require('lib-pathcomplete'); -var ps = require('lib-pathsearch'); - - -var execinfo_path = process.env.PATH.split(':'); - -var state = { - "?" : null -} - - -function readline(line, callback){ - line = line.trim(); - line = interpolate(line, process.env); - line = interpolate(line, state); - - if (line && line.length > 0) { - if (line.substring(0,2) === 'cd'){ - // cd is a native command - var dir = line.substring(2).trim(); - if (dir.length === 0) dir=process.env.HOME; - - // should not crash process with a bad 'cd' - try { - process.chdir(dir); - } catch (e) { - return callback(e); -// console.log(e); - } - - callback(); - }else{ - // other commands - run(line, callback); - } - } else { - callback(); - } -} - -// replace $VARs with environment variables -function interpolate(string, replace){ - return string.replace(/\$[^\s]+/g, function (key){ - var name = key.substring(1); - - var out; - if(replace[name] || replace[name] === 0){ - out = replace[name]; - } else { - out = key; - } - - return out; - }); -} - -function run(line, callback){ - // allow for setting environment variables - // on the command line - var stanza = parse(line); - - // fallback to current environment - stanza.envs.__proto__ = process.env; - - // We must stop reading STDIN because we will soon - // be the background process group, which will raise - // errors when attempting to read/write to the TTY driver - process.stdin.setRawMode(false) - process.stdin.pause(); - - // Sub-Process - var args = stanza.args; - var exec = stanza.exec; - var proc = cp.spawn(exec,args,{ - cwd: process.cwd(), - env: stanza.envs, - - // Inerit the terminal - stdio: 'inherit' - }); - - // Have this shell resume control after the sub-process exists - function res(){ - process.stdin.setRawMode(true) - process.stdin.resume(); - - callback(); - } - - // catch exit code - function end(code, signal){ - // the $? variable should contain the exit code - state["?"] = code; - state["??"] = signal; - - res(); - } - - // catch errors - function err(err){ - res(); - } - - proc.on('error', err); - proc.on('exit', end); -} - - -// auto-complete handler -function completer (line, callback) { - var split = line.split(/\s+/); - var item = split.pop(); - var outs = []; - - // avoid crazy auto-completions when the line is empty - if (!line) return callback(1); - - // user is attempting to type a relative directory - var is_rel = item[0] === '.'; - - // user is typing first command - var is_first = split.length === 0; - - // if this is the first token on the line - // autocomplete it against commands in the search path - if (!is_rel && is_first) - ps(item, execinfo_path, function (err, execs) { - // if there is only one executable, append a space after it - if (execs.length === 1) - execs[0] += ' '; - - callback(err, [execs, item]); - }); - - // if this is not the first token on the line - // autocomplete it against items in current dir - else - pc(item, function (err, arr, info) { - // if there is only one completion, and it's a directory - // automatically append a '/' to the completion - if (arr.length === 1 - && fs.statSync(info.dir + arr[0]).isDirectory()) - arr[0] += '/'; - - callback(err, [arr, info.file]); - }); -} - - -exports.completer = completer; -exports.readline = readline; From 2b1ac64e9e136be22f33fb4b1fffe94572bc86c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=27piranna?= Date: Tue, 24 Nov 2015 20:18:36 +0100 Subject: [PATCH 04/94] Use NshRepl class --- bin/nsh.js | 68 ++---------------------------------------- lib/index.js | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 83 insertions(+), 68 deletions(-) diff --git a/bin/nsh.js b/bin/nsh.js index f3253d8..ca467fa 100644 --- a/bin/nsh.js +++ b/bin/nsh.js @@ -1,70 +1,6 @@ #!/usr/bin/env node -var repl = require('repl'); +var NshRepl = require('..') -var parallel = require('async').parallel; -var nsh = require('..'); -var completer = nsh.completer; -var readline = nsh.readline; - - -function prompt(){ - var prefix; - try { - prefix = process.cwd(); - } catch (e) { - prefix = "(none)"; - } - return prefix + " > "; -}; - -var replserver = repl.start( -{ - prompt: prompt() -}); - -var jsEval = replserver.eval; -replserver.eval = function(cmd, context, filename, callback) -{ - // Javascript - jsEval.call(this, cmd, context, filename, function(error, result) - { - if(!error) return callback(null, result); - - // Shell - if(cmd[0] == '(') cmd = cmd.substring(1, cmd.length-1); - - readline(cmd, function(error2, result) - { - if(error2) return callback(error); - - callback(null, result); - }); - }); -}; - -var displayPrompt = replserver.displayPrompt; -replserver.displayPrompt = function(){ - replserver.prompt = prompt(); - displayPrompt.call(this); -}; - -var jsComplete = replserver.complete.bind(replserver); -replserver.complete = function(line, callback) -{ - parallel( - [ - jsComplete.bind(jsComplete, line), - completer.bind(undefined, line) - ], - function(error, results) - { - if(error) return callback(error); - - var execs = results[0][0].concat(results[1][0]); - var item = results[0][1]; - - callback(null, [execs, item]); - }) -}; +NshRepl() diff --git a/lib/index.js b/lib/index.js index acb207a..5fb0f99 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,6 +1,85 @@ +var inherits = require('util').inherits +var ReplServer = require('repl').ReplServer + +var parallel = require('async').parallel + var completer = require('./completer') var readline = require('./readline') -exports.completer = completer -exports.readline = readline +function pathPrompt() +{ + var prefix + + try + { + prefix = process.cwd() + } + catch (e) + { + prefix = "(none)" + } + + return prefix + " > " +} + + +function NshRepl(prompt, stream, eval_, useGlobal, ignoreUndefined) +{ + if(!(this instanceof NshRepl)) + return new NshRepl(prompt, stream, eval_, useGlobal, ignoreUndefined) + + prompt = prompt || pathPrompt() + + NshRepl.super_.call(this, prompt, stream, eval_, useGlobal, ignoreUndefined) + + + var jsEval = this.eval + this.eval = function(cmd, context, filename, callback) + { + // Javascript + jsEval.call(this, cmd, context, filename, function(error, result) + { + if(!error) return callback(null, result); + + // Shell + if(cmd[0] == '(') cmd = cmd.substring(1, cmd.length-1); + + readline(cmd, function(error2, result) + { + if(error2) return callback(error); + + callback(null, result); + }); + }); + }; +} +inherits(NshRepl, ReplServer) + + +NshRepl.prototype.displayPrompt = function() +{ + this.prompt = prompt() + NshRepl.super_.displayPrompt.call(this) +} + +NshRepl.prototype.complete = function(line, callback) +{ + parallel( + [ + NshRepl.super_.complete.bind(this, line), + completer.bind(undefined, line) + ], + function(error, results) + { + if(error) return callback(error) + + var execs = results[0][0].concat(results[1][0]) + var item = results[0][1] || results[1][1] + + callback(null, [execs, item]) + }) +} + + +module.exports = NshRepl From 38ea4cd010b12a465f5242739c01867daf8e548f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=27piranna?= Date: Tue, 24 Nov 2015 21:00:07 +0100 Subject: [PATCH 05/94] Use npm-path --- lib/index.js | 18 +++++++++++------- lib/readline.js | 18 +++++++++++------- package.json | 3 ++- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/lib/index.js b/lib/index.js index 5fb0f99..97c3f97 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,6 +1,7 @@ var inherits = require('util').inherits var ReplServer = require('repl').ReplServer +var npmPath = require('npm-path') var parallel = require('async').parallel var completer = require('./completer') @@ -34,25 +35,28 @@ function NshRepl(prompt, stream, eval_, useGlobal, ignoreUndefined) NshRepl.super_.call(this, prompt, stream, eval_, useGlobal, ignoreUndefined) + npmPath() + + var jsEval = this.eval this.eval = function(cmd, context, filename, callback) { // Javascript jsEval.call(this, cmd, context, filename, function(error, result) { - if(!error) return callback(null, result); + if(!error) return callback(null, result) // Shell - if(cmd[0] == '(') cmd = cmd.substring(1, cmd.length-1); + if(cmd[0] == '(') cmd = cmd.substring(1, cmd.length-1) readline(cmd, function(error2, result) { - if(error2) return callback(error); + if(error2) return callback(error) - callback(null, result); - }); - }); - }; + callback(null, result) + }) + }) + } } inherits(NshRepl, ReplServer) diff --git a/lib/readline.js b/lib/readline.js index e588bc2..8624c84 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -1,6 +1,7 @@ var cp = require('child_process'); -var parse = require('lib-cmdparse'); +var npmPath = require('npm-path') +var parse = require('lib-cmdparse'); var state = { @@ -13,14 +14,17 @@ function cd(line, callback) var dir = line.trim() || process.env.HOME // should not crash process with a bad 'cd' - try { - process.chdir(dir); - } catch (e) { - return callback(e); -// console.log(e); + try + { + process.chdir(dir) + } + catch(e) + { + return callback(e) } - callback(); + // Update $PATH + npmPath(callback) } diff --git a/package.json b/package.json index 4fbd2e3..863131e 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "lib-cmdparse": "~0.1.0", "lib-pathcomplete": "0.0.1", "lib-pathsearch": "~0.1.0", - "mkdirp": "~0.3.5" + "mkdirp": "~0.3.5", + "npm-path": "^1.0.2" } } From 2d1eff575ffe0ba910ac28934ac61cc54190db33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Wed, 27 Jan 2016 01:37:13 +0100 Subject: [PATCH 06/94] first version rewritten using `shell-parse` module --- .gitignore | 1 + bin/nsh.js | 6 -- lib/ast2js/command.js | 125 +++++++++++++++++++++++++++++++ lib/ast2js/glob.js | 10 +++ lib/ast2js/ifElse.js | 40 ++++++++++ lib/ast2js/index.js | 38 ++++++++++ lib/ast2js/literal.js | 7 ++ lib/ast2js/pipe.js | 17 +++++ lib/ast2js/redirectFd.js | 45 +++++++++++ lib/ast2js/until-loop.js | 22 ++++++ lib/ast2js/variable.js | 15 ++++ lib/ast2js/variableAssignment.js | 22 ++++++ lib/ast2js/while-loop.js | 14 ++++ lib/completer.js | 57 ++++++++------ package.json | 17 +++-- server.js | 87 +++++++++++++++++++++ 16 files changed, 486 insertions(+), 37 deletions(-) delete mode 100644 bin/nsh.js create mode 100644 lib/ast2js/command.js create mode 100644 lib/ast2js/glob.js create mode 100644 lib/ast2js/ifElse.js create mode 100644 lib/ast2js/index.js create mode 100644 lib/ast2js/literal.js create mode 100644 lib/ast2js/pipe.js create mode 100644 lib/ast2js/redirectFd.js create mode 100644 lib/ast2js/until-loop.js create mode 100644 lib/ast2js/variable.js create mode 100644 lib/ast2js/variableAssignment.js create mode 100644 lib/ast2js/while-loop.js create mode 100644 server.js diff --git a/.gitignore b/.gitignore index 3c3629e..93f1361 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +npm-debug.log diff --git a/bin/nsh.js b/bin/nsh.js deleted file mode 100644 index ca467fa..0000000 --- a/bin/nsh.js +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env node - -var NshRepl = require('..') - - -NshRepl() diff --git a/lib/ast2js/command.js b/lib/ast2js/command.js new file mode 100644 index 0000000..4ea8f72 --- /dev/null +++ b/lib/ast2js/command.js @@ -0,0 +1,125 @@ +var spawn = require('child_process').spawn +var Stream = require('stream') + +var async = require('async') + +var ast2js = require('./index') + + +function spawnStream(command, argv, options) +{ + var cp = spawn(command, argv, options) + + var stdin = cp.stdin + var stdout = cp.stdout + var stderr = cp.sterr + + var result + + // Both `stdin` and `stdout` are open, probably the normal case. + // Create a `Duplex` object with them so command can be used as a filter. + if(stdin && stdout) + { + result = Stream.Duplex( + { + read: function(n){}, + write: function(chunk, encoding, next) + { + stdin.write(chunk) + next() + } + }) + + stdout + .on('data', function(data) + { + result.push(data) + }) + .on('close', result.emit.bind(result, 'close')) + } + + // Only one of `stdin` or `stdout` are open, use it directly. + else if(stdin) result = stdin + else if(stdout) result = stdout + + // Both `stdin` and `stdout` are clossed. + // This should never happen, but who knows... + else result = new Stream() + + // Expose `stderr` so it can be used later. + if(stderr) result.stderr = stderr + + cp.on('error', result.emit.bind(result, 'error')) + cp.on('exit' , result.emit.bind(result, 'exit' )) + + return result +} + + +function command(item, callback) +{ + // Command + ast2js(item.command, function(error, command) + { + if(error) return callback(error) + + // Arguments + async.map(item.args, ast2js, function(error, argv) + { + if(error) return callback(error) + + // Redirects + var stdio = [] + + async.eachSeries(item.redirects, function(redirect, callback) + { + ast2js(redirect, function(error, value) + { + if(error) return callback(error) + + var type = redirect.type + switch(type) + { + case 'duplicateFd': + stdio[redirect.destFd] = stdio[redirect.srcFd] + break; + + case 'redirectFd': + stdio[redirect.fd] = value + break; + + case 'moveFd': + stdio[redirect.dest] = stdio[redirect.fd] + stdio[redirect.fd] = 'ignore' + break; + + default: + return callback('Unknown redirect type "'+type+'"') + } + + callback() + }) + }, + + // Create command + function(error) + { + if(error) return callback(error) + + var env = item.env + env.__proto__ = process.env + + var options = + { + env: env, + stdio: stdio + } + + callback(null, spawnStream(command, argv, options)) + }) + }) + }) +} + + +module.exports = command diff --git a/lib/ast2js/glob.js b/lib/ast2js/glob.js new file mode 100644 index 0000000..115b4cf --- /dev/null +++ b/lib/ast2js/glob.js @@ -0,0 +1,10 @@ +var globFunc = require("glob") + + +function glob(item, callback) +{ + globFunc(item.value, callback) +} + + +module.exports = glob diff --git a/lib/ast2js/ifElse.js b/lib/ast2js/ifElse.js new file mode 100644 index 0000000..2b221b2 --- /dev/null +++ b/lib/ast2js/ifElse.js @@ -0,0 +1,40 @@ +var async = require('async') + +var ast2js = require('./index') + + +function ifElse(item, callback) +{ + ast2js(item.test, function(error, value) + { + if(error) return callback(error) + + if(value) return ast2js(item.body, callback) + + var elifBlocks = item.elifBlocks || [] + + function runTests(item, callback2) + { + ast2js(item.test, function(error, result) + { + if(error) return callback(error) + + callback2(result) + }) + } + + function execBody(block) + { + if(block) return ast2js(block.body, callback) + + if(item.elseBody) return ast2js(item.elseBody, callback) + + callback() + } + + async.detectSeries(elifBlocks, runTests, execBody) + }) +} + + +module.exports = ifElse diff --git a/lib/ast2js/index.js b/lib/ast2js/index.js new file mode 100644 index 0000000..decc56e --- /dev/null +++ b/lib/ast2js/index.js @@ -0,0 +1,38 @@ +function noop(item, callback) +{ + callback() +} + +function notImplemented(item, callback) +{ + var error = new Error("'"+item.type+"' not implemented") + error.item = item + + throw error +} + + +function ast2js(item, callback) +{ + ast2js[item.type](item, callback) +} + + +module.exports = ast2js + + +ast2js.command = require('./command') +ast2js.commandSubstitution = notImplemented +ast2js.duplicateFd = noop +ast2js.glob = require('./glob') +ast2js.ifElse = require('./ifElse') +ast2js.literal = require('./literal') +ast2js.moveFd = noop +ast2js.pipe = require('./pipe') +ast2js.processSubstitution = notImplemented +ast2js.redirectFd = require('./redirectFd') +ast2js['until-loop'] = require('./until-loop') +ast2js.variable = require('./variable') +ast2js.variableAssignment = require('./variableAssignment') +ast2js.variableSubstitution = notImplemented +ast2js['while-loop'] = require('./while-loop') diff --git a/lib/ast2js/literal.js b/lib/ast2js/literal.js new file mode 100644 index 0000000..bc67434 --- /dev/null +++ b/lib/ast2js/literal.js @@ -0,0 +1,7 @@ +function literal(item, callback) +{ + callback(null, item.value) +} + + +module.exports = literal diff --git a/lib/ast2js/pipe.js b/lib/ast2js/pipe.js new file mode 100644 index 0000000..aa691be --- /dev/null +++ b/lib/ast2js/pipe.js @@ -0,0 +1,17 @@ +var ast2js = require('./index') + + +function pipe(item, callback) +{ + ast2js(item.command, function(error, value) + { + if(error) return callback(error) + + value.fd = 1 + + callback(null, value) + }) +} + + +module.exports = pipe diff --git a/lib/ast2js/redirectFd.js b/lib/ast2js/redirectFd.js new file mode 100644 index 0000000..9af367d --- /dev/null +++ b/lib/ast2js/redirectFd.js @@ -0,0 +1,45 @@ +var fs = require('fs') + +var ast2js = require('./index') + + +function redirectFd(item, callback) +{ + ast2js(item.filename, function(error, filename) + { + if(error) return callback(error) + + var result + + var op = item.op + switch(op) + { + case '<': + result = fs.createReadStream(filename) + break; + + case '>': + result = fs.createWriteStream(filename) + break; + + case '>>': + result = fs.createWriteStream(filename, 'a') + break; + + case '>|': + case '&>': + case '&>>': + return callback('redirectFd "'+op+'" op not implemented') + + default: + return callback('Unknown redirectFd op "'+op+'"') + } + + result.fd = item.fd + + callback(null, result) + }) +} + + +module.exports = redirectFd diff --git a/lib/ast2js/until-loop.js b/lib/ast2js/until-loop.js new file mode 100644 index 0000000..9139a9b --- /dev/null +++ b/lib/ast2js/until-loop.js @@ -0,0 +1,22 @@ +var during = require('async').during + +var ast2js = require('./index') + + +function until_loop(item, callback) +{ + function test(callback) + { + ast2js(item.test, function(error, value) + { + callback(error, !value) + }) + } + + during(test, + ast2js.bind(null, item.body), + callback) +} + + +module.exports = until_loop diff --git a/lib/ast2js/variable.js b/lib/ast2js/variable.js new file mode 100644 index 0000000..6a752bb --- /dev/null +++ b/lib/ast2js/variable.js @@ -0,0 +1,15 @@ +var ast2js = require('./index') + + +function variable(item, callback) +{ + ast2js(item.name, function(error, key) + { + if(error) return callback(error) + + callback(null, global[key]) + }) +} + + +module.exports = variable diff --git a/lib/ast2js/variableAssignment.js b/lib/ast2js/variableAssignment.js new file mode 100644 index 0000000..4ef3ece --- /dev/null +++ b/lib/ast2js/variableAssignment.js @@ -0,0 +1,22 @@ +var ast2js = require('./index') + + +function variableAssignment(item, callback) +{ + ast2js(item.name, function(error, key) + { + if(error) return callback(error) + + ast2js(item.value, function(error, value) + { + if(error) return callback(error) + + global[key] = value + + callback() + }) + }) +} + + +module.exports = variableAssignment diff --git a/lib/ast2js/while-loop.js b/lib/ast2js/while-loop.js new file mode 100644 index 0000000..cd7e6ba --- /dev/null +++ b/lib/ast2js/while-loop.js @@ -0,0 +1,14 @@ +var during = require('async').during + +var ast2js = require('./index') + + +function while_loop(item, callback) +{ + during(ast2js.bind(null, item.test), + ast2js.bind(null, item.body), + callback) +} + + +module.exports = while_loop diff --git a/lib/completer.js b/lib/completer.js index 1a9896c..71b4792 100644 --- a/lib/completer.js +++ b/lib/completer.js @@ -4,47 +4,56 @@ var pc = require('lib-pathcomplete') var ps = require('lib-pathsearch') -var execinfo_path = process.env.PATH.split(':'); +var execinfo_path = process.env.PATH.split(':') // auto-complete handler -function completer (line, callback) { - var split = line.split(/\s+/); - var item = split.pop(); - var outs = []; +function completer(line, callback) +{ + var split = line.split(/\s+/) + var item = split.pop() + var outs = [] // avoid crazy auto-completions when the line is empty - if (!line) return callback(1); + if (!line) return callback(1) // user is attempting to type a relative directory - var is_rel = item[0] === '.'; + var is_rel = item[0] === '.' // user is typing first command - var is_first = split.length === 0; + var is_first = !split.length // if this is the first token on the line // autocomplete it against commands in the search path - if (!is_rel && is_first) - ps(item, execinfo_path, function (err, execs) { + if(!is_rel && is_first) + return ps(item, execinfo_path, function(err, execs) + { + if(err) return callback(err) + // if there is only one executable, append a space after it - if (execs.length === 1) - execs[0] += ' '; + if(execs.length === 1) execs[0] += ' ' - callback(err, [execs, item]); - }); + callback(null, [execs, item]) + }) // if this is not the first token on the line // autocomplete it against items in current dir - else - pc(item, function (err, arr, info) { - // if there is only one completion, and it's a directory - // automatically append a '/' to the completion - if (arr.length === 1 - && fs.statSync(info.dir + arr[0]).isDirectory()) - arr[0] += '/'; - - callback(err, [arr, info.file]); - }); + pc(item, function(err, arr, info) + { + if(err) return callback(err) + + // if there is only one completion, and it's a directory + // automatically append a '/' to the completion + if(arr.length === 1) + return fs.stat(info.dir + arr[0], function(error, stats) + { + if(stats.isDirectory()) arr[0] += '/' + + callback(null, [arr, info.file]) + }) + + callback(null, [arr, info.file]); + }) } diff --git a/package.json b/package.json index 863131e..4a644c8 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,23 @@ { "name": "bin-nsh", - "version": "0.4.0", + "version": "0.5.0", "description": "Node/No Shell", "author": "Jacob Groundwater ", + "contributors": [ + "Jesús Leganés Combarro 'piranna' " + ], "license": "MIT", "main": "lib", "bin": { - "nsh": "bin/nsh.js" + "nsh": "server.js" }, "dependencies": { - "async": "^0.9.0", - "glob": "~3.2.7", - "lib-cmdparse": "~0.1.0", + "async": "^1.5.2", + "glob": "~6.0.4", "lib-pathcomplete": "0.0.1", "lib-pathsearch": "~0.1.0", - "mkdirp": "~0.3.5", - "npm-path": "^1.0.2" + "mkdirp": "~0.5.1", + "npm-path": "^1.0.2", + "shell-parse": "0.0.2" } } diff --git a/server.js b/server.js new file mode 100644 index 0000000..f715004 --- /dev/null +++ b/server.js @@ -0,0 +1,87 @@ +#!/usr/bin/env node + +var createInterface = require('readline').createInterface +var Readable = require('stream').Readable + +var eachSeries = require('async').eachSeries +var parse = require('shell-parse') + +var ast2js = require('./lib/ast2js').command + + +function execCommand(command, callback) +{ + ast2js(command, function(error, command) + { + if(error) return callback(error) + + var stdin = new Readable() + stdin._read = function(){} + var push = stdin.push.bind(stdin) + + stdin.pipe(command).pipe(process.stdout) + + process.stdin.on('data', push) + + command.on('close', function(code) + { + process.stdin.removeListener('data', push) + + stdin.unpipe(command).unpipe(process.stdout) + + callback(code) + }) + }) +} + + +var rl = createInterface( +{ + input: process.stdin, + output: process.stdout, +// completer: +}) + + +var input = '' + +function prompt(smallPrompt) +{ + input = '' + + rl.setPrompt(smallPrompt ? '> ' : process.cwd()+'> ') + rl.prompt() +} + + +prompt() + +rl.on('line', function(line) +{ + input += line + + if(input === '') return prompt() + + try + { + var ast = parse(input) + } + catch(err) + { + if(err.constructor !== parse.SyntaxError) throw err + + line = input.slice(err.offset) + + try + { + parse(line, 'continuationStart') + return prompt(true) + } + catch(err) + { + throw err + } + } + + eachSeries(ast, execCommand, prompt) +}) From 26a5f2de74c9214d8ff39af9827e839e728654a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Thu, 28 Jan 2016 00:42:17 +0100 Subject: [PATCH 07/94] enable completer --- server.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server.js b/server.js index f715004..28572db 100644 --- a/server.js +++ b/server.js @@ -6,7 +6,8 @@ var Readable = require('stream').Readable var eachSeries = require('async').eachSeries var parse = require('shell-parse') -var ast2js = require('./lib/ast2js').command +var ast2js = require('./lib/ast2js').command +var completer = require('./lib/completer') function execCommand(command, callback) @@ -39,7 +40,7 @@ var rl = createInterface( { input: process.stdin, output: process.stdout, -// completer: + completer: completer }) From 4c791067e1f482eb80f7e9990d590a999d50909a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Thu, 28 Jan 2016 00:43:21 +0100 Subject: [PATCH 08/94] simplified creation of command stdin --- server.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server.js b/server.js index 28572db..3caa15c 100644 --- a/server.js +++ b/server.js @@ -10,14 +10,15 @@ var ast2js = require('./lib/ast2js').command var completer = require('./lib/completer') +function noop(){} + function execCommand(command, callback) { ast2js(command, function(error, command) { if(error) return callback(error) - var stdin = new Readable() - stdin._read = function(){} + var stdin = new Readable({read: noop}) var push = stdin.push.bind(stdin) stdin.pipe(command).pipe(process.stdout) From de08a2173990e6a3d03cf76c7bc630a38cb43266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Thu, 28 Jan 2016 01:41:39 +0100 Subject: [PATCH 09/94] fixed variables management --- lib/ast2js/variable.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/ast2js/variable.js b/lib/ast2js/variable.js index 6a752bb..ab4c6fe 100644 --- a/lib/ast2js/variable.js +++ b/lib/ast2js/variable.js @@ -3,12 +3,9 @@ var ast2js = require('./index') function variable(item, callback) { - ast2js(item.name, function(error, key) - { - if(error) return callback(error) + var name = item.name - callback(null, global[key]) - }) + callback(null, global[name] || process.env[name]) } From 8fcf117ffeeb484a562b721692a5229cc025e5a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Thu, 28 Jan 2016 01:42:36 +0100 Subject: [PATCH 10/94] use readline input and output instead of process stdin and stdout --- server.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) mode change 100644 => 100755 server.js diff --git a/server.js b/server.js old mode 100644 new mode 100755 index 3caa15c..01c9775 --- a/server.js +++ b/server.js @@ -18,18 +18,19 @@ function execCommand(command, callback) { if(error) return callback(error) + var input = rl.input + var output = rl.output + var stdin = new Readable({read: noop}) var push = stdin.push.bind(stdin) - stdin.pipe(command).pipe(process.stdout) - - process.stdin.on('data', push) + stdin.pipe(command).pipe(output) + input.on('data', push) command.on('close', function(code) { - process.stdin.removeListener('data', push) - - stdin.unpipe(command).unpipe(process.stdout) + input.removeListener('data', push) + stdin.unpipe(command).unpipe(output) callback(code) }) From d885de35f0acff8e0676ff7bb364378de6f18ce4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Thu, 28 Jan 2016 01:54:45 +0100 Subject: [PATCH 11/94] use `end` event instead of `close` (more compatible with functions) --- lib/ast2js/command.js | 11 +++++------ server.js | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/ast2js/command.js b/lib/ast2js/command.js index 4ea8f72..740a423 100644 --- a/lib/ast2js/command.js +++ b/lib/ast2js/command.js @@ -6,6 +6,8 @@ var async = require('async') var ast2js = require('./index') +function noop(){} + function spawnStream(command, argv, options) { var cp = spawn(command, argv, options) @@ -22,7 +24,7 @@ function spawnStream(command, argv, options) { result = Stream.Duplex( { - read: function(n){}, + read: noop, write: function(chunk, encoding, next) { stdin.write(chunk) @@ -31,11 +33,8 @@ function spawnStream(command, argv, options) }) stdout - .on('data', function(data) - { - result.push(data) - }) - .on('close', result.emit.bind(result, 'close')) + .on('data', result.push.bind(result)) + .on('end' , result.emit.bind(result,'end')) } // Only one of `stdin` or `stdout` are open, use it directly. diff --git a/server.js b/server.js index 01c9775..f9a293f 100755 --- a/server.js +++ b/server.js @@ -27,7 +27,7 @@ function execCommand(command, callback) stdin.pipe(command).pipe(output) input.on('data', push) - command.on('close', function(code) + command.on('end', function(code) { input.removeListener('data', push) stdin.unpipe(command).unpipe(output) From cbc580eb06460ad7faca27f44e3e78655f59a820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Thu, 28 Jan 2016 20:07:50 +0100 Subject: [PATCH 12/94] Dynamic $PATH & pretty completer --- package.json | 4 ++-- server.js | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 4a644c8..b2cd17a 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,9 @@ "async": "^1.5.2", "glob": "~6.0.4", "lib-pathcomplete": "0.0.1", - "lib-pathsearch": "~0.1.0", + "lib-pathsearch": "piranna/node-lib-pathsearch", "mkdirp": "~0.5.1", - "npm-path": "^1.0.2", + "npm-path": "piranna/npm-path", "shell-parse": "0.0.2" } } diff --git a/server.js b/server.js index f9a293f..50e0202 100755 --- a/server.js +++ b/server.js @@ -9,11 +9,15 @@ var parse = require('shell-parse') var ast2js = require('./lib/ast2js').command var completer = require('./lib/completer') +var npmPath = require('npm-path').bind(null, {env:{PATH:process.env.PATH}}) + function noop(){} function execCommand(command, callback) { + npmPath() + ast2js(command, function(error, command) { if(error) return callback(error) From 5270666322210cba139b60321b5f8ca108a54b4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Thu, 28 Jan 2016 20:16:19 +0100 Subject: [PATCH 13/94] prevent crashing when command not found --- server.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server.js b/server.js index 50e0202..78d9f98 100755 --- a/server.js +++ b/server.js @@ -38,6 +38,12 @@ function execCommand(command, callback) callback(code) }) + .on('error', function(error) + { + if(error.code !== 'ENOENT') throw error + + console.error(error.path+': not found') + }) }) } From ee2be5c5773957b34999c5d298d72ef9ab846172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Thu, 28 Jan 2016 23:20:52 +0100 Subject: [PATCH 14/94] Fixed variableAssigment --- lib/ast2js/variableAssignment.js | 11 +++-------- server.js | 4 +++- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/lib/ast2js/variableAssignment.js b/lib/ast2js/variableAssignment.js index 4ef3ece..f0d4f85 100644 --- a/lib/ast2js/variableAssignment.js +++ b/lib/ast2js/variableAssignment.js @@ -3,18 +3,13 @@ var ast2js = require('./index') function variableAssignment(item, callback) { - ast2js(item.name, function(error, key) + ast2js(item.value, function(error, value) { if(error) return callback(error) - ast2js(item.value, function(error, value) - { - if(error) return callback(error) + global[item.name] = value - global[key] = value - - callback() - }) + callback() }) } diff --git a/server.js b/server.js index 78d9f98..cbc4997 100755 --- a/server.js +++ b/server.js @@ -6,7 +6,7 @@ var Readable = require('stream').Readable var eachSeries = require('async').eachSeries var parse = require('shell-parse') -var ast2js = require('./lib/ast2js').command +var ast2js = require('./lib/ast2js') var completer = require('./lib/completer') var npmPath = require('npm-path').bind(null, {env:{PATH:process.env.PATH}}) @@ -22,6 +22,8 @@ function execCommand(command, callback) { if(error) return callback(error) + if(command == null) return callback() + var input = rl.input var output = rl.output From 45f6da63f3592136dee0e098302770edb7538b67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Thu, 28 Jan 2016 23:44:05 +0100 Subject: [PATCH 15/94] variableSubstitution --- lib/ast2js/index.js | 2 +- lib/ast2js/variableSubstitution.js | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 lib/ast2js/variableSubstitution.js diff --git a/lib/ast2js/index.js b/lib/ast2js/index.js index decc56e..cb7e1df 100644 --- a/lib/ast2js/index.js +++ b/lib/ast2js/index.js @@ -34,5 +34,5 @@ ast2js.redirectFd = require('./redirectFd') ast2js['until-loop'] = require('./until-loop') ast2js.variable = require('./variable') ast2js.variableAssignment = require('./variableAssignment') -ast2js.variableSubstitution = notImplemented +ast2js.variableSubstitution = require('./variableSubstitution') ast2js['while-loop'] = require('./while-loop') diff --git a/lib/ast2js/variableSubstitution.js b/lib/ast2js/variableSubstitution.js new file mode 100644 index 0000000..e866bb6 --- /dev/null +++ b/lib/ast2js/variableSubstitution.js @@ -0,0 +1,12 @@ +var ast2js = require('./index') + + +function variableSubstitution(item, callback) +{ + var value = item.expression + + callback(null, global[value] || process.env[value]) +} + + +module.exports = variableSubstitution From fb54ec85bd03bf7fbce825604faa414bfeb3ef7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sun, 31 Jan 2016 13:12:51 +0100 Subject: [PATCH 16/94] Made it compatible with Node.js v0.12 --- lib/ast2js/command.js | 14 ++++++-------- server.js | 3 ++- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/ast2js/command.js b/lib/ast2js/command.js index 740a423..12149fa 100644 --- a/lib/ast2js/command.js +++ b/lib/ast2js/command.js @@ -22,15 +22,13 @@ function spawnStream(command, argv, options) // Create a `Duplex` object with them so command can be used as a filter. if(stdin && stdout) { - result = Stream.Duplex( + result = Stream.Duplex() + result._read = noop + result._write = function(chunk, encoding, next) { - read: noop, - write: function(chunk, encoding, next) - { - stdin.write(chunk) - next() - } - }) + stdin.write(chunk) + next() + } stdout .on('data', result.push.bind(result)) diff --git a/server.js b/server.js index cbc4997..aaf702d 100755 --- a/server.js +++ b/server.js @@ -27,7 +27,8 @@ function execCommand(command, callback) var input = rl.input var output = rl.output - var stdin = new Readable({read: noop}) + var stdin = new Readable() + stdin._read = noop var push = stdin.push.bind(stdin) stdin.pipe(command).pipe(output) From 6b47e7289758c5937cc920e79a42999f54e5d227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sun, 31 Jan 2016 13:16:04 +0100 Subject: [PATCH 17/94] Move processing of redirections to own file --- lib/ast2js/command.js | 43 +++++----------------------------- lib/ast2js/redirects.js | 52 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 37 deletions(-) create mode 100644 lib/ast2js/redirects.js diff --git a/lib/ast2js/command.js b/lib/ast2js/command.js index 12149fa..42500e2 100644 --- a/lib/ast2js/command.js +++ b/lib/ast2js/command.js @@ -1,9 +1,10 @@ var spawn = require('child_process').spawn var Stream = require('stream') -var async = require('async') +var map = require('async').map -var ast2js = require('./index') +var ast2js = require('./index') +var redirects = require('./redirects') function noop(){} @@ -61,48 +62,16 @@ function command(item, callback) if(error) return callback(error) // Arguments - async.map(item.args, ast2js, function(error, argv) + map(item.args, ast2js, function(error, argv) { if(error) return callback(error) // Redirects - var stdio = [] - - async.eachSeries(item.redirects, function(redirect, callback) - { - ast2js(redirect, function(error, value) - { - if(error) return callback(error) - - var type = redirect.type - switch(type) - { - case 'duplicateFd': - stdio[redirect.destFd] = stdio[redirect.srcFd] - break; - - case 'redirectFd': - stdio[redirect.fd] = value - break; - - case 'moveFd': - stdio[redirect.dest] = stdio[redirect.fd] - stdio[redirect.fd] = 'ignore' - break; - - default: - return callback('Unknown redirect type "'+type+'"') - } - - callback() - }) - }, - - // Create command - function(error) + redirects(item.redirects, function(error, stdio) { if(error) return callback(error) + // Create command var env = item.env env.__proto__ = process.env diff --git a/lib/ast2js/redirects.js b/lib/ast2js/redirects.js new file mode 100644 index 0000000..6ebf584 --- /dev/null +++ b/lib/ast2js/redirects.js @@ -0,0 +1,52 @@ +var eachSeries = require('async').eachSeries + +var ast2js = require('./index') + + +function redirects(array, callback) +{ + var stdio = ['pipe', 'pipe', 'pipe'] + + eachSeries(array, function(redirect, callback) + { + ast2js(redirect, function(error, value) + { + if(error) return callback(error) + + var type = redirect.type + switch(type) + { + case 'duplicateFd': + stdio[redirect.destFd] = stdio[redirect.srcFd] + break; + + case 'moveFd': + stdio[redirect.dest] = stdio[redirect.fd] + stdio[redirect.fd] = 'ignore' + break; + + case 'pipe': + stdio[1] = value + break; + + case 'redirectFd': + stdio[redirect.fd] = value + break; + + default: + return callback('Unknown redirect type "'+type+'"') + } + + callback() + }) + }, + function(error) + { + if(error) return callback(error) + + callback(null, stdio) + }) +} + + +module.exports = redirects From 6b7fc97cb8c6882d5f71882a9e9054b49df87657 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sun, 31 Jan 2016 13:16:48 +0100 Subject: [PATCH 18/94] Clean-up of pipe and redirectFd --- lib/ast2js/pipe.js | 9 +-------- lib/ast2js/redirectFd.js | 6 ++---- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/lib/ast2js/pipe.js b/lib/ast2js/pipe.js index aa691be..71a58c0 100644 --- a/lib/ast2js/pipe.js +++ b/lib/ast2js/pipe.js @@ -3,14 +3,7 @@ var ast2js = require('./index') function pipe(item, callback) { - ast2js(item.command, function(error, value) - { - if(error) return callback(error) - - value.fd = 1 - - callback(null, value) - }) + ast2js(item.command, callback) } diff --git a/lib/ast2js/redirectFd.js b/lib/ast2js/redirectFd.js index 9af367d..7206120 100644 --- a/lib/ast2js/redirectFd.js +++ b/lib/ast2js/redirectFd.js @@ -23,7 +23,7 @@ function redirectFd(item, callback) break; case '>>': - result = fs.createWriteStream(filename, 'a') + result = fs.createWriteStream(filename, {flags: 'a'}) break; case '>|': @@ -35,9 +35,7 @@ function redirectFd(item, callback) return callback('Unknown redirectFd op "'+op+'"') } - result.fd = item.fd - - callback(null, result) + result.once('open', callback.bind(null, null, result)) }) } From 7ea396479629a282f99e0ef471e1e9a7fb508de5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sun, 31 Jan 2016 16:14:21 +0100 Subject: [PATCH 19/94] Use non pipe stdin on the returned Duplex object --- lib/ast2js/command.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/ast2js/command.js b/lib/ast2js/command.js index 42500e2..bba7e60 100644 --- a/lib/ast2js/command.js +++ b/lib/ast2js/command.js @@ -13,9 +13,13 @@ function spawnStream(command, argv, options) { var cp = spawn(command, argv, options) - var stdin = cp.stdin - var stdout = cp.stdout - var stderr = cp.sterr + var stdin = options.stdio[0] + var stdout = options.stdio[1] + var stderr = options.stdio[2] + + stdin = cp.stdin || (stdin !== 'ignore' && stdin .writable ? stdin : null) + stdout = cp.stdout || (stdout !== 'ignore' && stdout.readable ? stdout : null) + stderr = cp.stderr || (stderr !== 'ignore' && stderr.readable ? stderr : null) var result @@ -24,12 +28,8 @@ function spawnStream(command, argv, options) if(stdin && stdout) { result = Stream.Duplex() - result._read = noop - result._write = function(chunk, encoding, next) - { - stdin.write(chunk) - next() - } + result._read = noop + result._write = stdin.write.bind(stdin) stdout .on('data', result.push.bind(result)) From 806985bf3592c610239ab61f8d36e39ce72de106 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sun, 31 Jan 2016 16:36:48 +0100 Subject: [PATCH 20/94] allow piping from stdout of one process to stdin of another --- lib/ast2js/command.js | 45 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/lib/ast2js/command.js b/lib/ast2js/command.js index bba7e60..a6f3967 100644 --- a/lib/ast2js/command.js +++ b/lib/ast2js/command.js @@ -11,15 +11,48 @@ function noop(){} function spawnStream(command, argv, options) { + var stdio = options.stdio + + var stdin = stdio[0] + var stdout = stdio[1] + var stderr = stdio[2] + + if(typeof stdin !== 'string' && stdin.fd == null) + stdio[0] = 'pipe' + else + stdin = null + + if(typeof stdout !== 'string' && stdout.fd == null) + stdio[1] = 'pipe' + else + stdout = null + + if(typeof stderr !== 'string' && stderr.fd == null) + stdio[2] = 'pipe' + else + stderr = null + var cp = spawn(command, argv, options) - var stdin = options.stdio[0] - var stdout = options.stdio[1] - var stderr = options.stdio[2] + if(stdin) + { + cp.stdin .pipe(stdin ) + cp.stdin = null + } + if(stdout) + { + cp.stdout.pipe(stdout) + cp.stdout = null + } + if(stderr) + { + cp.stderr.pipe(stderr) + cp.stderr = null + } - stdin = cp.stdin || (stdin !== 'ignore' && stdin .writable ? stdin : null) - stdout = cp.stdout || (stdout !== 'ignore' && stdout.readable ? stdout : null) - stderr = cp.stderr || (stderr !== 'ignore' && stderr.readable ? stderr : null) + stdin = cp.stdin || (stdin && stdin !== 'ignore' && stdin .writable ? stdin : null) + stdout = cp.stdout || (stdout && stdout !== 'ignore' && stdout.readable ? stdout : null) + stderr = cp.stderr || (stderr && stderr !== 'ignore' && stderr.readable ? stderr : null) var result From 5ce5baa8a0928b6fba450adc9e5556941893c3f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sun, 31 Jan 2016 16:41:15 +0100 Subject: [PATCH 21/94] Moved `spawnStream()` to own file --- lib/ast2js/command.js | 88 ++------------------------------------- lib/ast2js/spawnStream.js | 86 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 85 deletions(-) create mode 100644 lib/ast2js/spawnStream.js diff --git a/lib/ast2js/command.js b/lib/ast2js/command.js index a6f3967..b85b8b2 100644 --- a/lib/ast2js/command.js +++ b/lib/ast2js/command.js @@ -1,90 +1,8 @@ -var spawn = require('child_process').spawn -var Stream = require('stream') - var map = require('async').map -var ast2js = require('./index') -var redirects = require('./redirects') - - -function noop(){} - -function spawnStream(command, argv, options) -{ - var stdio = options.stdio - - var stdin = stdio[0] - var stdout = stdio[1] - var stderr = stdio[2] - - if(typeof stdin !== 'string' && stdin.fd == null) - stdio[0] = 'pipe' - else - stdin = null - - if(typeof stdout !== 'string' && stdout.fd == null) - stdio[1] = 'pipe' - else - stdout = null - - if(typeof stderr !== 'string' && stderr.fd == null) - stdio[2] = 'pipe' - else - stderr = null - - var cp = spawn(command, argv, options) - - if(stdin) - { - cp.stdin .pipe(stdin ) - cp.stdin = null - } - if(stdout) - { - cp.stdout.pipe(stdout) - cp.stdout = null - } - if(stderr) - { - cp.stderr.pipe(stderr) - cp.stderr = null - } - - stdin = cp.stdin || (stdin && stdin !== 'ignore' && stdin .writable ? stdin : null) - stdout = cp.stdout || (stdout && stdout !== 'ignore' && stdout.readable ? stdout : null) - stderr = cp.stderr || (stderr && stderr !== 'ignore' && stderr.readable ? stderr : null) - - var result - - // Both `stdin` and `stdout` are open, probably the normal case. - // Create a `Duplex` object with them so command can be used as a filter. - if(stdin && stdout) - { - result = Stream.Duplex() - result._read = noop - result._write = stdin.write.bind(stdin) - - stdout - .on('data', result.push.bind(result)) - .on('end' , result.emit.bind(result,'end')) - } - - // Only one of `stdin` or `stdout` are open, use it directly. - else if(stdin) result = stdin - else if(stdout) result = stdout - - // Both `stdin` and `stdout` are clossed. - // This should never happen, but who knows... - else result = new Stream() - - // Expose `stderr` so it can be used later. - if(stderr) result.stderr = stderr - - cp.on('error', result.emit.bind(result, 'error')) - cp.on('exit' , result.emit.bind(result, 'exit' )) - - return result -} +var ast2js = require('./index') +var redirects = require('./redirects') +var spawnStream = require('./spawnStream') function command(item, callback) diff --git a/lib/ast2js/spawnStream.js b/lib/ast2js/spawnStream.js new file mode 100644 index 0000000..d895c34 --- /dev/null +++ b/lib/ast2js/spawnStream.js @@ -0,0 +1,86 @@ +var spawn = require('child_process').spawn +var Stream = require('stream') + + +function noop(){} + + +function spawnStream(command, argv, options) +{ + var stdio = options.stdio + + var stdin = stdio[0] + var stdout = stdio[1] + var stderr = stdio[2] + + if(typeof stdin !== 'string' && stdin.fd == null) + stdio[0] = 'pipe' + else + stdin = null + + if(typeof stdout !== 'string' && stdout.fd == null) + stdio[1] = 'pipe' + else + stdout = null + + if(typeof stderr !== 'string' && stderr.fd == null) + stdio[2] = 'pipe' + else + stderr = null + + var cp = spawn(command, argv, options) + + if(stdin) + { + cp.stdin .pipe(stdin ) + cp.stdin = null + } + if(stdout) + { + cp.stdout.pipe(stdout) + cp.stdout = null + } + if(stderr) + { + cp.stderr.pipe(stderr) + cp.stderr = null + } + + stdin = cp.stdin || (stdin && stdin !== 'ignore' && stdin .writable ? stdin : null) + stdout = cp.stdout || (stdout && stdout !== 'ignore' && stdout.readable ? stdout : null) + stderr = cp.stderr || (stderr && stderr !== 'ignore' && stderr.readable ? stderr : null) + + var result + + // Both `stdin` and `stdout` are open, probably the normal case. + // Create a `Duplex` object with them so command can be used as a filter. + if(stdin && stdout) + { + result = Stream.Duplex() + result._read = noop + result._write = stdin.write.bind(stdin) + + stdout + .on('data', result.push.bind(result)) + .on('end' , result.emit.bind(result,'end')) + } + + // Only one of `stdin` or `stdout` are open, use it directly. + else if(stdin) result = stdin + else if(stdout) result = stdout + + // Both `stdin` and `stdout` are clossed. + // This should never happen, but who knows... + else result = new Stream() + + // Expose `stderr` so it can be used later. + if(stderr) result.stderr = stderr + + cp.on('error', result.emit.bind(result, 'error')) + cp.on('exit' , result.emit.bind(result, 'exit' )) + + return result +} + + +module.exports = spawnStream From 51205c2d92f8470dbc78e7e8014c746c194cb3e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Mon, 1 Feb 2016 02:29:08 +0100 Subject: [PATCH 22/94] private wrapStdio() function for spawnStream() --- lib/ast2js/spawnStream.js | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/lib/ast2js/spawnStream.js b/lib/ast2js/spawnStream.js index d895c34..e257db8 100644 --- a/lib/ast2js/spawnStream.js +++ b/lib/ast2js/spawnStream.js @@ -4,8 +4,11 @@ var Stream = require('stream') function noop(){} - -function spawnStream(command, argv, options) +/** + * Node.js v0.12 don't accept as stdio streams without a file descriptor, so I + * use the original `spawn()` ones and pipe them to the desired ones + */ +function wrapStdio(command, argv, options) { var stdio = options.stdio @@ -32,8 +35,8 @@ function spawnStream(command, argv, options) if(stdin) { - cp.stdin .pipe(stdin ) - cp.stdin = null + stdin.pipe(cp.stdin) + cp.stdin = null } if(stdout) { @@ -46,6 +49,20 @@ function spawnStream(command, argv, options) cp.stderr = null } + return cp +} + + +function spawnStream(command, argv, options) +{ + var cp = wrapStdio(command, argv, options) + + var stdio = options.stdio + + var stdin = stdio[0] + var stdout = stdio[1] + var stderr = stdio[2] + stdin = cp.stdin || (stdin && stdin !== 'ignore' && stdin .writable ? stdin : null) stdout = cp.stdout || (stdout && stdout !== 'ignore' && stdout.readable ? stdout : null) stderr = cp.stderr || (stderr && stderr !== 'ignore' && stderr.readable ? stderr : null) From 0a4a98e07d9b6c1664d89ee22def7e16067fdf84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Mon, 1 Feb 2016 16:32:23 +0100 Subject: [PATCH 23/94] process Control+C on the shell --- server.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server.js b/server.js index aaf702d..0436290 100755 --- a/server.js +++ b/server.js @@ -101,3 +101,11 @@ rl.on('line', function(line) eachSeries(ast, execCommand, prompt) }) + +rl.on('SIGINT', function() +{ + this.write('^C') + this.clearLine() + + prompt() +}) From 06a1ca468b389ac1e42294b18b630d13b5227200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Wed, 3 Feb 2016 01:32:01 +0100 Subject: [PATCH 24/94] Close correctly stdin of piped commands --- lib/ast2js/spawnStream.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/ast2js/spawnStream.js b/lib/ast2js/spawnStream.js index e257db8..8ae0d8b 100644 --- a/lib/ast2js/spawnStream.js +++ b/lib/ast2js/spawnStream.js @@ -16,6 +16,7 @@ function wrapStdio(command, argv, options) var stdout = stdio[1] var stderr = stdio[2] + // Wrap stdio if(typeof stdin !== 'string' && stdin.fd == null) stdio[0] = 'pipe' else @@ -31,8 +32,10 @@ function wrapStdio(command, argv, options) else stderr = null + // Create child process var cp = spawn(command, argv, options) + // Adjust events, pipe streams and restore stdio if(stdin) { stdin.pipe(cp.stdin) @@ -49,23 +52,24 @@ function wrapStdio(command, argv, options) cp.stderr = null } + // Return child process return cp } function spawnStream(command, argv, options) { - var cp = wrapStdio(command, argv, options) - var stdio = options.stdio var stdin = stdio[0] var stdout = stdio[1] var stderr = stdio[2] - stdin = cp.stdin || (stdin && stdin !== 'ignore' && stdin .writable ? stdin : null) - stdout = cp.stdout || (stdout && stdout !== 'ignore' && stdout.readable ? stdout : null) - stderr = cp.stderr || (stderr && stderr !== 'ignore' && stderr.readable ? stderr : null) + var cp = wrapStdio(command, argv, options) + + stdin = cp.stdin || (stdin && stdin .writable ? stdin : null) + stdout = cp.stdout || (stdout && stdout.readable ? stdout : null) + stderr = cp.stderr || (stderr && stderr.readable ? stderr : null) var result @@ -77,9 +81,11 @@ function spawnStream(command, argv, options) result._read = noop result._write = stdin.write.bind(stdin) + result.on('finish', stdin.end.bind(stdin)) + stdout .on('data', result.push.bind(result)) - .on('end' , result.emit.bind(result,'end')) + .on('end' , result.emit.bind(result, 'end')) } // Only one of `stdin` or `stdout` are open, use it directly. @@ -87,7 +93,7 @@ function spawnStream(command, argv, options) else if(stdout) result = stdout // Both `stdin` and `stdout` are clossed. - // This should never happen, but who knows... + // This could never happen, but who knows... else result = new Stream() // Expose `stderr` so it can be used later. From d1e17633ced02fc06ab0670b45fd22cc894157fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Wed, 3 Feb 2016 01:45:06 +0100 Subject: [PATCH 25/94] Moved execCommand() to own file & improved stdio streams management --- lib/execCommand.js | 63 ++++++++++++++++++++++++++++++++++++++++++++++ server.js | 45 ++------------------------------- 2 files changed, 65 insertions(+), 43 deletions(-) create mode 100644 lib/execCommand.js diff --git a/lib/execCommand.js b/lib/execCommand.js new file mode 100644 index 0000000..a339fb7 --- /dev/null +++ b/lib/execCommand.js @@ -0,0 +1,63 @@ +var tty = require('tty') + +var ast2js = require('./ast2js') + + +var npmPath = require('npm-path').bind(null, {env:{PATH:process.env.PATH}}) + + +function noop(){} + + +function execCommand(rl, command, callback) +{ + npmPath() + + ast2js(command, function(error, command) + { + if(error) return callback(error) + + if(command == null) return callback() + + if(command.writable) + { + var stdin = tty.ReadStream(rl.input.fd) + + rl.input.pause() + + stdin.pipe(command) + } + + if(command.readable) + { + var stdout = tty.WriteStream(rl.output.fd) + + command.pipe(stdout) + } + + command.on('end', function(code) + { + if(stdin) + { + stdin.end() // This in needed to don'l loose chars, not sure why... + stdin.unpipe(command) + + rl.input.resume() + } + + if(stdout) + command.unpipe(stdout) + + callback(code) + }) + .on('error', function(error) + { + if(error.code !== 'ENOENT') throw error + + console.error(error.path+': not found') + }) + }) +} + + +module.exports = execCommand diff --git a/server.js b/server.js index 0436290..d84ff56 100755 --- a/server.js +++ b/server.js @@ -1,55 +1,12 @@ #!/usr/bin/env node var createInterface = require('readline').createInterface -var Readable = require('stream').Readable var eachSeries = require('async').eachSeries var parse = require('shell-parse') -var ast2js = require('./lib/ast2js') var completer = require('./lib/completer') -var npmPath = require('npm-path').bind(null, {env:{PATH:process.env.PATH}}) - - -function noop(){} - -function execCommand(command, callback) -{ - npmPath() - - ast2js(command, function(error, command) - { - if(error) return callback(error) - - if(command == null) return callback() - - var input = rl.input - var output = rl.output - - var stdin = new Readable() - stdin._read = noop - var push = stdin.push.bind(stdin) - - stdin.pipe(command).pipe(output) - input.on('data', push) - - command.on('end', function(code) - { - input.removeListener('data', push) - stdin.unpipe(command).unpipe(output) - - callback(code) - }) - .on('error', function(error) - { - if(error.code !== 'ENOENT') throw error - - console.error(error.path+': not found') - }) - }) -} - var rl = createInterface( { @@ -58,6 +15,8 @@ var rl = createInterface( completer: completer }) +var execCommand = require('./lib/execCommand').bind(null, rl) + var input = '' From a6f14e1ca8c10a69c6220b6b1828e71f54cef662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Thu, 4 Feb 2016 22:22:36 +0100 Subject: [PATCH 26/94] Support for `&>` and `&>>` --- lib/ast2js/redirectFd.js | 4 ++-- lib/ast2js/redirects.js | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/ast2js/redirectFd.js b/lib/ast2js/redirectFd.js index 7206120..cde9a01 100644 --- a/lib/ast2js/redirectFd.js +++ b/lib/ast2js/redirectFd.js @@ -19,16 +19,16 @@ function redirectFd(item, callback) break; case '>': + case '&>': result = fs.createWriteStream(filename) break; case '>>': + case '&>>': result = fs.createWriteStream(filename, {flags: 'a'}) break; case '>|': - case '&>': - case '&>>': return callback('redirectFd "'+op+'" op not implemented') default: diff --git a/lib/ast2js/redirects.js b/lib/ast2js/redirects.js index 6ebf584..8b4cfcf 100644 --- a/lib/ast2js/redirects.js +++ b/lib/ast2js/redirects.js @@ -31,6 +31,10 @@ function redirects(array, callback) case 'redirectFd': stdio[redirect.fd] = value + + if(redirect.fd === '&>' + || redirect.fd === '&>>') + stdio[2] = value break; default: From 09ca1f13e200c56cac00a0413e673640e32052a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Thu, 4 Feb 2016 22:34:18 +0100 Subject: [PATCH 27/94] Use `reduce()` to calculate redirections --- lib/ast2js/redirects.js | 79 +++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 42 deletions(-) diff --git a/lib/ast2js/redirects.js b/lib/ast2js/redirects.js index 8b4cfcf..c297055 100644 --- a/lib/ast2js/redirects.js +++ b/lib/ast2js/redirects.js @@ -1,56 +1,51 @@ -var eachSeries = require('async').eachSeries +var reduce = require('async').reduce var ast2js = require('./index') -function redirects(array, callback) +function iterator(stdio, redirect, callback) { - var stdio = ['pipe', 'pipe', 'pipe'] - - eachSeries(array, function(redirect, callback) - { - ast2js(redirect, function(error, value) - { - if(error) return callback(error) - - var type = redirect.type - switch(type) - { - case 'duplicateFd': - stdio[redirect.destFd] = stdio[redirect.srcFd] - break; - - case 'moveFd': - stdio[redirect.dest] = stdio[redirect.fd] - stdio[redirect.fd] = 'ignore' - break; - - case 'pipe': - stdio[1] = value - break; - - case 'redirectFd': - stdio[redirect.fd] = value - - if(redirect.fd === '&>' - || redirect.fd === '&>>') - stdio[2] = value - break; - - default: - return callback('Unknown redirect type "'+type+'"') - } - - callback() - }) - }, - function(error) + ast2js(redirect, function(error, value) { if(error) return callback(error) + var type = redirect.type + switch(type) + { + case 'duplicateFd': + stdio[redirect.destFd] = stdio[redirect.srcFd] + break; + + case 'moveFd': + stdio[redirect.dest] = stdio[redirect.fd] + stdio[redirect.fd] = 'ignore' + break; + + case 'pipe': + stdio[1] = value + break; + + case 'redirectFd': + stdio[redirect.fd] = value + + if(redirect.fd === '&>' + || redirect.fd === '&>>') + stdio[2] = value + break; + + default: + return callback('Unknown redirect type "'+type+'"') + } + callback(null, stdio) }) } +function redirects(array, callback) +{ + reduce(array, ['pipe', 'pipe', 'pipe'], iterator, callback) +} + + module.exports = redirects From cfd47713202f80981eaf002774cb753cd4cee0a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Fri, 5 Feb 2016 01:07:12 +0100 Subject: [PATCH 28/94] Use EventEmitter as default instead of old Stream to prevent `.pipe()` bugs --- lib/ast2js/spawnStream.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/ast2js/spawnStream.js b/lib/ast2js/spawnStream.js index 8ae0d8b..1b60f3f 100644 --- a/lib/ast2js/spawnStream.js +++ b/lib/ast2js/spawnStream.js @@ -1,5 +1,6 @@ -var spawn = require('child_process').spawn -var Stream = require('stream') +var EventEmitter = require('events') +var spawn = require('child_process').spawn +var Duplex = require('stream').Duplex function noop(){} @@ -77,7 +78,7 @@ function spawnStream(command, argv, options) // Create a `Duplex` object with them so command can be used as a filter. if(stdin && stdout) { - result = Stream.Duplex() + result = Duplex() result._read = noop result._write = stdin.write.bind(stdin) @@ -94,7 +95,7 @@ function spawnStream(command, argv, options) // Both `stdin` and `stdout` are clossed. // This could never happen, but who knows... - else result = new Stream() + else result = new EventEmitter() // Expose `stderr` so it can be used later. if(stderr) result.stderr = stderr From b26f6de847b97acdd0ea556516d68dbb96976002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Fri, 5 Feb 2016 01:08:17 +0100 Subject: [PATCH 29/94] Allow exec builtins & added `cd` working one --- lib/ast2js/command.js | 54 +++++++++++++++++++++++++++++++++++++++++++ lib/builtins.js | 46 ++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 lib/builtins.js diff --git a/lib/ast2js/command.js b/lib/ast2js/command.js index b85b8b2..b114115 100644 --- a/lib/ast2js/command.js +++ b/lib/ast2js/command.js @@ -1,9 +1,56 @@ +var Duplex = require('stream').Duplex + var map = require('async').map var ast2js = require('./index') var redirects = require('./redirects') var spawnStream = require('./spawnStream') +var builtins = require('../builtins') + + +function noop(){} + +function connectStdio(command, stdio) +{ + var stdin = stdio[0] + var stdout = stdio[1] + var stderr = stdio[2] + + if(stdin !== 'pipe') + stdin.pipe(command) + else + stdin = null + + if(stdout !== 'pipe') + command.pipe(stdout) + else + stdout = null + + var result = command + + if(stdin || stdout) + { + stdin = stdin || command + stdout = stdout || command + + result = Duplex() + result._read = noop + result._write = stdin.write.bind(stdin) + + result.on('finish', stdin.end.bind(stdin)) + + stdout + .on('data', result.push.bind(result)) + .on('end' , result.emit.bind(result, 'end')) + } + + // Expose `stderr` so it can be used later. + if(stderr) result.stderr = stderr + + return result +} + function command(item, callback) { @@ -26,6 +73,13 @@ function command(item, callback) var env = item.env env.__proto__ = process.env + var builtin = builtins[command] + if(builtin) + { + command = builtin.call(env, argv) + return callback(null, connectStdio(command, stdio)) + } + var options = { env: env, diff --git a/lib/builtins.js b/lib/builtins.js new file mode 100644 index 0000000..6bba747 --- /dev/null +++ b/lib/builtins.js @@ -0,0 +1,46 @@ +var Readable = require('stream').Readable + + +function noop(){} + + +function cd(argv) +{ + var env = process.env // Special case, needs to change shell global environment + + var result = new Readable({objectMode: true}) + result._read = noop + + var dir = argv[0] || env.HOME + + var showNewPwd = false + if(dir === '-') + { + dir = env.OLDPWD + + if(!dir) return result.emit('error', 'cd: OLDPWD not defined') + + showNewPwd = true + } + +// dir.replace(/^~\//, env.HOME+'/') +// dir = path.resolve(env.PWD, dir) + + process.chdir(dir) + + env.OLDPWD = env.PWD + env.PWD = dir + + if(showNewPwd) + { + dir.type = 'cd' + result.push(dir+'\n') + } + + result.push(null) + + return result +} + + +exports.cd = cd From c7417b3210057b556d6cd8570ecd4dd27bbf9e04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sat, 6 Feb 2016 00:45:13 +0100 Subject: [PATCH 30/94] use `stderr`, including errors from builtins --- lib/ast2js/command.js | 31 +++++++++++++++++++++++++------ lib/ast2js/spawnStream.js | 18 ++++++++++++++++-- lib/builtins.js | 14 +++++++++++--- lib/execCommand.js | 15 ++++++++++++--- 4 files changed, 64 insertions(+), 14 deletions(-) diff --git a/lib/ast2js/command.js b/lib/ast2js/command.js index b114115..023b28b 100644 --- a/lib/ast2js/command.js +++ b/lib/ast2js/command.js @@ -1,4 +1,6 @@ -var Duplex = require('stream').Duplex +var Domain = require('domain').Domain +var Duplex = require('stream').Duplex +var Readable = require('stream').Readable var map = require('async').map @@ -15,7 +17,6 @@ function connectStdio(command, stdio) { var stdin = stdio[0] var stdout = stdio[1] - var stderr = stdio[2] if(stdin !== 'pipe') stdin.pipe(command) @@ -45,9 +46,6 @@ function connectStdio(command, stdio) .on('end' , result.emit.bind(result, 'end')) } - // Expose `stderr` so it can be used later. - if(stderr) result.stderr = stderr - return result } @@ -76,7 +74,28 @@ function command(item, callback) var builtin = builtins[command] if(builtin) { - command = builtin.call(env, argv) + var d = new Domain() + + // Put `error` events on the `stderr` stream + var stderr = stdio[2] + if(stderr === 'pipe') + { + stderr = new Readable({objectMode: true}) + stderr._read = noop + } + + d.on('error', stderr.push.bind(stderr)) + // [ToDo] Close `stderr` when command finish + + // Run the builtin + d.run(function() + { + command = builtin.call(env, argv) + }) + + // Expose `stderr` so it can be used later. + if(stderr !== stdio[2]) command.stderr = stderr + return callback(null, connectStdio(command, stdio)) } diff --git a/lib/ast2js/spawnStream.js b/lib/ast2js/spawnStream.js index 1b60f3f..400bc25 100644 --- a/lib/ast2js/spawnStream.js +++ b/lib/ast2js/spawnStream.js @@ -97,8 +97,22 @@ function spawnStream(command, argv, options) // This could never happen, but who knows... else result = new EventEmitter() - // Expose `stderr` so it can be used later. - if(stderr) result.stderr = stderr + if(stderr) + { + // Expose `stderr` so it can be used later. + result.stderr = stderr + + // Redirect `stderr` from piped command to our own `stderr`, since there's + // no way to redirect it to `process.stderr` by default as it should be. + // This way we can at least fetch the error messages someway instead of lost + // them... + var out_stderr = stdout.stderr + if(out_stderr) + { + out_stderr.pipe(stderr) + out_stderr.once('end', out_stderr.unpipe.bind(out_stderr, stderr)) + } + } cp.on('error', result.emit.bind(result, 'error')) cp.on('exit' , result.emit.bind(result, 'exit' )) diff --git a/lib/builtins.js b/lib/builtins.js index 6bba747..9bd4cd4 100644 --- a/lib/builtins.js +++ b/lib/builtins.js @@ -6,11 +6,14 @@ function noop(){} function cd(argv) { - var env = process.env // Special case, needs to change shell global environment + // `cd` is a special case, since it needs to change the global environment + var env = process.env var result = new Readable({objectMode: true}) result._read = noop + argv = argv || [] + var dir = argv[0] || env.HOME var showNewPwd = false @@ -18,7 +21,13 @@ function cd(argv) { dir = env.OLDPWD - if(!dir) return result.emit('error', 'cd: OLDPWD not defined') + if(!dir) + { + result.emit('error', 'cd: OLDPWD not defined\n') + + result.push(null) + return result + } showNewPwd = true } @@ -38,7 +47,6 @@ function cd(argv) } result.push(null) - return result } diff --git a/lib/execCommand.js b/lib/execCommand.js index a339fb7..6c6f32d 100644 --- a/lib/execCommand.js +++ b/lib/execCommand.js @@ -35,18 +35,27 @@ function execCommand(rl, command, callback) command.pipe(stdout) } + var command_stderr = command.stderr + if(command_stderr) + { + var stderr = tty.WriteStream(process.stderr.fd) + + command_stderr.pipe(stderr) + } + command.on('end', function(code) { if(stdin) { stdin.end() // This in needed to don'l loose chars, not sure why... - stdin.unpipe(command) + stdin.unpipe(this) rl.input.resume() } - if(stdout) - command.unpipe(stdout) + if(stdout) this.unpipe(stdout) + + if(stderr) command_stderr.unpipe(stderr) callback(code) }) From 7233c9eb64a22b46c92c4d986f0b4fcd2f151185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sat, 6 Feb 2016 13:25:03 +0100 Subject: [PATCH 31/94] Add underscore on private files names --- lib/ast2js/{redirects.js => _redirects.js} | 0 lib/ast2js/{spawnStream.js => _spawnStream.js} | 0 lib/ast2js/command.js | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) rename lib/ast2js/{redirects.js => _redirects.js} (100%) rename lib/ast2js/{spawnStream.js => _spawnStream.js} (100%) diff --git a/lib/ast2js/redirects.js b/lib/ast2js/_redirects.js similarity index 100% rename from lib/ast2js/redirects.js rename to lib/ast2js/_redirects.js diff --git a/lib/ast2js/spawnStream.js b/lib/ast2js/_spawnStream.js similarity index 100% rename from lib/ast2js/spawnStream.js rename to lib/ast2js/_spawnStream.js diff --git a/lib/ast2js/command.js b/lib/ast2js/command.js index 023b28b..1715327 100644 --- a/lib/ast2js/command.js +++ b/lib/ast2js/command.js @@ -5,8 +5,8 @@ var Readable = require('stream').Readable var map = require('async').map var ast2js = require('./index') -var redirects = require('./redirects') -var spawnStream = require('./spawnStream') +var redirects = require('./_redirects') +var spawnStream = require('./_spawnStream') var builtins = require('../builtins') From 45ed5933bfebf517662ee018d042f289b08acfeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sat, 6 Feb 2016 14:35:28 +0100 Subject: [PATCH 32/94] exec commands globally instead of iterate over them independently --- lib/ast2js/_execCommands.js | 107 ++++++++++++++++++++++++++++++++++++ lib/execCommand.js | 72 ------------------------ server.js | 15 +++-- 3 files changed, 116 insertions(+), 78 deletions(-) create mode 100644 lib/ast2js/_execCommands.js delete mode 100644 lib/execCommand.js diff --git a/lib/ast2js/_execCommands.js b/lib/ast2js/_execCommands.js new file mode 100644 index 0000000..2459192 --- /dev/null +++ b/lib/ast2js/_execCommands.js @@ -0,0 +1,107 @@ +var tty = require('tty') +var Writable = require('stream').Writable + +var eachSeries = require('async').eachSeries + + +module.exports = execCommands + +var ast2js = require('./index') + + +var npmPath = require('npm-path').bind(null, {env:{PATH:process.env.PATH}}) + + +function noop(){} + + +var input + +function execCommands(rl, commands, callback) +{ + input = rl.input || input + + var output = rl.output + + // Backup cwd + var env = process.env + + var pwd = env.PWD + var oldPwd = env.OLDPWD + + eachSeries(commands, function(command, callback) + { + npmPath() + + ast2js(command, function(error, command) + { + if(error) return callback(error) + + if(command == null) return callback() + + if(command.writable) + { + var stdin = tty.ReadStream(input.fd) + + input.pause() + stdin.pipe(command) + } + + if(command.readable) + { + var fd = output.fd + if(fd != null) + var stdout = tty.WriteStream(fd) + else + { + var stdout = new Writable() + stdout._write = output._write.bind(output) + } + + command.pipe(stdout) + } + + var command_stderr = command.stderr + if(command_stderr) + { + var stderr = tty.WriteStream(process.stderr.fd) + + command_stderr.pipe(stderr) + } + + command.on('end', function(code) + { + if(stdin) + { + stdin.end() // This in needed to don'l loose chars, not sure why... + stdin.unpipe(this) + + input.resume() + } + if(stdout) this .unpipe(stdout) + if(stderr) command_stderr.unpipe(stderr) + + callback(code) + }) + .on('error', function(error) + { + if(error.code !== 'ENOENT') throw error + + console.error(error.path+': not found') + }) + }) + }, + function(error) + { + // Restore (possible) changed cwd + process.chdir(pwd) + env.PWD = pwd + + if(oldPwd) + env.OLDPWD = oldPwd + else + delete env.OLDPWD + + callback(error) + }) +} diff --git a/lib/execCommand.js b/lib/execCommand.js deleted file mode 100644 index 6c6f32d..0000000 --- a/lib/execCommand.js +++ /dev/null @@ -1,72 +0,0 @@ -var tty = require('tty') - -var ast2js = require('./ast2js') - - -var npmPath = require('npm-path').bind(null, {env:{PATH:process.env.PATH}}) - - -function noop(){} - - -function execCommand(rl, command, callback) -{ - npmPath() - - ast2js(command, function(error, command) - { - if(error) return callback(error) - - if(command == null) return callback() - - if(command.writable) - { - var stdin = tty.ReadStream(rl.input.fd) - - rl.input.pause() - - stdin.pipe(command) - } - - if(command.readable) - { - var stdout = tty.WriteStream(rl.output.fd) - - command.pipe(stdout) - } - - var command_stderr = command.stderr - if(command_stderr) - { - var stderr = tty.WriteStream(process.stderr.fd) - - command_stderr.pipe(stderr) - } - - command.on('end', function(code) - { - if(stdin) - { - stdin.end() // This in needed to don'l loose chars, not sure why... - stdin.unpipe(this) - - rl.input.resume() - } - - if(stdout) this.unpipe(stdout) - - if(stderr) command_stderr.unpipe(stderr) - - callback(code) - }) - .on('error', function(error) - { - if(error.code !== 'ENOENT') throw error - - console.error(error.path+': not found') - }) - }) -} - - -module.exports = execCommand diff --git a/server.js b/server.js index d84ff56..d74cb07 100755 --- a/server.js +++ b/server.js @@ -2,10 +2,10 @@ var createInterface = require('readline').createInterface -var eachSeries = require('async').eachSeries -var parse = require('shell-parse') +var parse = require('shell-parse') -var completer = require('./lib/completer') +var completer = require('./lib/completer') +var execCommands = require('./lib/ast2js/_execCommands') var rl = createInterface( @@ -15,8 +15,6 @@ var rl = createInterface( completer: completer }) -var execCommand = require('./lib/execCommand').bind(null, rl) - var input = '' @@ -58,7 +56,12 @@ rl.on('line', function(line) } } - eachSeries(ast, execCommand, prompt) + execCommands(this, ast, function(error) + { + if(error) console.error(error) + + prompt() + }) }) rl.on('SIGINT', function() From 750185aa260af8e61734bef66c3b970c9618bfc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sat, 6 Feb 2016 14:36:06 +0100 Subject: [PATCH 33/94] commandSubstitution --- lib/ast2js/commandSubstitution.js | 26 ++++++++++++++++++++++++++ lib/ast2js/index.js | 7 +++---- 2 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 lib/ast2js/commandSubstitution.js diff --git a/lib/ast2js/commandSubstitution.js b/lib/ast2js/commandSubstitution.js new file mode 100644 index 0000000..a4bfb75 --- /dev/null +++ b/lib/ast2js/commandSubstitution.js @@ -0,0 +1,26 @@ +var Writable = require('stream').Writable + +var execCommands = require('./_execCommands') + + +function commandSubstitution(item, callback) +{ + var buffer = [] + + var output = new Writable() + output._write = function(chunk, _, done) + { + buffer.push(chunk) + done() + } + + execCommands({output: output}, item.commands, function(error) + { + if(error) return callback(error) + + callback(null, buffer.join('')) + }) +} + + +module.exports = commandSubstitution diff --git a/lib/ast2js/index.js b/lib/ast2js/index.js index cb7e1df..83a79e2 100644 --- a/lib/ast2js/index.js +++ b/lib/ast2js/index.js @@ -7,7 +7,6 @@ function notImplemented(item, callback) { var error = new Error("'"+item.type+"' not implemented") error.item = item - throw error } @@ -22,12 +21,12 @@ module.exports = ast2js ast2js.command = require('./command') -ast2js.commandSubstitution = notImplemented -ast2js.duplicateFd = noop +ast2js.commandSubstitution = require('./commandSubstitution') +ast2js.duplicateFd = noop // Processed on `redirects` file ast2js.glob = require('./glob') ast2js.ifElse = require('./ifElse') ast2js.literal = require('./literal') -ast2js.moveFd = noop +ast2js.moveFd = noop // Processed on `redirects` file ast2js.pipe = require('./pipe') ast2js.processSubstitution = notImplemented ast2js.redirectFd = require('./redirectFd') From f9731a3b4774519b1bf9b7873319f7675e28c59a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sat, 6 Feb 2016 21:43:50 +0100 Subject: [PATCH 34/94] Only restore `$CWD` for commandSubstitution --- lib/ast2js/_execCommands.js | 22 +++------------------- lib/ast2js/commandSubstitution.js | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/lib/ast2js/_execCommands.js b/lib/ast2js/_execCommands.js index 2459192..50be144 100644 --- a/lib/ast2js/_execCommands.js +++ b/lib/ast2js/_execCommands.js @@ -23,14 +23,10 @@ function execCommands(rl, commands, callback) var output = rl.output - // Backup cwd - var env = process.env - - var pwd = env.PWD - var oldPwd = env.OLDPWD - eachSeries(commands, function(command, callback) { + // `$PATH` is dynamic based on current directory and any command could + // change it, so we update it previously to exec any of them npmPath() ast2js(command, function(error, command) @@ -91,17 +87,5 @@ function execCommands(rl, commands, callback) }) }) }, - function(error) - { - // Restore (possible) changed cwd - process.chdir(pwd) - env.PWD = pwd - - if(oldPwd) - env.OLDPWD = oldPwd - else - delete env.OLDPWD - - callback(error) - }) + callback) } diff --git a/lib/ast2js/commandSubstitution.js b/lib/ast2js/commandSubstitution.js index a4bfb75..a6d29ce 100644 --- a/lib/ast2js/commandSubstitution.js +++ b/lib/ast2js/commandSubstitution.js @@ -14,8 +14,23 @@ function commandSubstitution(item, callback) done() } + // Backup cwd + var env = process.env + + var pwd = env.PWD + var oldPwd = env.OLDPWD + execCommands({output: output}, item.commands, function(error) { + // Restore (possible) changed cwd + process.chdir(pwd) + env.PWD = pwd + + if(oldPwd) + env.OLDPWD = oldPwd + else + delete env.OLDPWD + if(error) return callback(error) callback(null, buffer.join('')) From 4f2f23b00789b725d7cc4eb87400c222201e394e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sat, 6 Feb 2016 21:45:58 +0100 Subject: [PATCH 35/94] Notify error if directory don't exists --- lib/builtins.js | 14 +++++++++++++- lib/readline.js | 19 ------------------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/lib/builtins.js b/lib/builtins.js index 9bd4cd4..400cb5f 100644 --- a/lib/builtins.js +++ b/lib/builtins.js @@ -35,7 +35,19 @@ function cd(argv) // dir.replace(/^~\//, env.HOME+'/') // dir = path.resolve(env.PWD, dir) - process.chdir(dir) + try + { + process.chdir(dir) + } + catch(error) + { + if(error.code !== 'ENOENT') throw error + + result.emit('error', 'cd: '+dir+': no such file or directory\n') + + result.push(null) + return result + } env.OLDPWD = env.PWD env.PWD = dir diff --git a/lib/readline.js b/lib/readline.js index 8624c84..7f6c2ea 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -9,25 +9,6 @@ var state = { } -function cd(line, callback) -{ - var dir = line.trim() || process.env.HOME - - // should not crash process with a bad 'cd' - try - { - process.chdir(dir) - } - catch(e) - { - return callback(e) - } - - // Update $PATH - npmPath(callback) -} - - function readline(line, callback){ line = line.trim(); line = interpolate(line, process.env); From 67dc17f0f5fec246ebcb0f97e17e587e683625cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sat, 6 Feb 2016 23:58:16 +0100 Subject: [PATCH 36/94] output status --- lib/ast2js/_execCommands.js | 9 +++++++-- lib/ast2js/command.js | 1 + lib/ast2js/variable.js | 7 ++++++- lib/readline.js | 15 +-------------- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/lib/ast2js/_execCommands.js b/lib/ast2js/_execCommands.js index 50be144..b08529d 100644 --- a/lib/ast2js/_execCommands.js +++ b/lib/ast2js/_execCommands.js @@ -65,7 +65,7 @@ function execCommands(rl, commands, callback) command_stderr.pipe(stderr) } - command.on('end', function(code) + command.on('end', function() { if(stdin) { @@ -77,7 +77,7 @@ function execCommands(rl, commands, callback) if(stdout) this .unpipe(stdout) if(stderr) command_stderr.unpipe(stderr) - callback(code) + callback() }) .on('error', function(error) { @@ -85,6 +85,11 @@ function execCommands(rl, commands, callback) console.error(error.path+': not found') }) + .on('exit', function(code, signal) + { + global['?' ] = code + global['??'] = signal + }) }) }, callback) diff --git a/lib/ast2js/command.js b/lib/ast2js/command.js index 1715327..3d01bfc 100644 --- a/lib/ast2js/command.js +++ b/lib/ast2js/command.js @@ -44,6 +44,7 @@ function connectStdio(command, stdio) stdout .on('data', result.push.bind(result)) .on('end' , result.emit.bind(result, 'end')) + .on('end' , result.emit.bind(result, 'exit', 0, null)) } return result diff --git a/lib/ast2js/variable.js b/lib/ast2js/variable.js index ab4c6fe..1ff3f10 100644 --- a/lib/ast2js/variable.js +++ b/lib/ast2js/variable.js @@ -5,7 +5,12 @@ function variable(item, callback) { var name = item.name - callback(null, global[name] || process.env[name]) + var value = global[name] + + if(value == null) value = process.env[name] + if(value == null) value = '' + + callback(null, value) } diff --git a/lib/readline.js b/lib/readline.js index 7f6c2ea..ba1ce42 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -2,11 +2,7 @@ var cp = require('child_process'); var npmPath = require('npm-path') var parse = require('lib-cmdparse'); - - -var state = { - "?" : null -} +var parse = require('lib-cmdparse') function readline(line, callback){ @@ -76,15 +72,6 @@ function run(line, callback){ callback(); } - // catch exit code - function end(code, signal){ - // the $? variable should contain the exit code - state["?"] = code; - state["??"] = signal; - - res(); - } - // catch errors function err(err){ res(); From 0e517e8cac879741af0da5e5b0c8bb5f7ba01608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sun, 7 Feb 2016 00:46:50 +0100 Subject: [PATCH 37/94] auto-complete of builtins --- lib/completer.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/completer.js b/lib/completer.js index 71b4792..06fec1d 100644 --- a/lib/completer.js +++ b/lib/completer.js @@ -3,6 +3,8 @@ var fs = require('fs') var pc = require('lib-pathcomplete') var ps = require('lib-pathsearch') +var builtins = require('./builtins') + var execinfo_path = process.env.PATH.split(':') @@ -30,6 +32,19 @@ function completer(line, callback) { if(err) return callback(err) + // Builtins + var names = Object.keys(builtins).filter(function(name) + { + return name.substr(0, item.length) === item + }) + + if(names.length) + { + if(execs.length) execs.unshift('') + + execs.unshift.apply(execs, names) + } + // if there is only one executable, append a space after it if(execs.length === 1) execs[0] += ' ' From de817fb0fa2b64ffa1a0469f56c0ca4c1d9de8ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sun, 7 Feb 2016 01:26:54 +0100 Subject: [PATCH 38/94] capture `EACCES` error when executing a command --- lib/ast2js/command.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/ast2js/command.js b/lib/ast2js/command.js index 3d01bfc..df67b4a 100644 --- a/lib/ast2js/command.js +++ b/lib/ast2js/command.js @@ -106,7 +106,18 @@ function command(item, callback) stdio: stdio } - callback(null, spawnStream(command, argv, options)) + try + { + command = spawnStream(command, argv, options) + } + catch(error) + { + if(error.code === 'EACCES') error = command+': is a directory' + + return callback(error) + } + + callback(null, command) }) }) }) From e39ed40c18fe905fc2d6bfa77acc52e9e21504fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sun, 7 Feb 2016 01:27:17 +0100 Subject: [PATCH 39/94] Completer for global and environment variables --- lib/completer.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/lib/completer.js b/lib/completer.js index 06fec1d..5b21a02 100644 --- a/lib/completer.js +++ b/lib/completer.js @@ -25,6 +25,37 @@ function completer(line, callback) // user is typing first command var is_first = !split.length + // Global and environment variables + if(item === '$') return callback(2) + if(item[0] === '$') + { + var item2 = item.substr(1) + + var vars = Object.keys(global) + .filter(function(name) + { + return name.substr(0, item.length-1) === item2 + }) + .map(function(name) + { + return '$'+name + }) + + var env = Object.keys(process.env) + .filter(function(name) + { + return name.substr(0, item.length-1) === item2 + }) + .map(function(name) + { + return '$'+name + }) + + if(vars.length && env.length) vars.push('') + + return callback(null, [vars.concat(env), item]) + } + // if this is the first token on the line // autocomplete it against commands in the search path if(!is_rel && is_first) From def19e8aaee9afef512b520e48e109065225401a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sun, 7 Feb 2016 13:15:31 +0100 Subject: [PATCH 40/94] Unified `variable` and `variableSubstitution` (they are the same in shell) --- lib/ast2js/index.js | 6 ++++-- lib/ast2js/variable.js | 5 +---- lib/ast2js/variableSubstitution.js | 12 ------------ 3 files changed, 5 insertions(+), 18 deletions(-) delete mode 100644 lib/ast2js/variableSubstitution.js diff --git a/lib/ast2js/index.js b/lib/ast2js/index.js index 83a79e2..a25d4ca 100644 --- a/lib/ast2js/index.js +++ b/lib/ast2js/index.js @@ -20,6 +20,8 @@ function ast2js(item, callback) module.exports = ast2js +var variable = require('./variable') + ast2js.command = require('./command') ast2js.commandSubstitution = require('./commandSubstitution') ast2js.duplicateFd = noop // Processed on `redirects` file @@ -31,7 +33,7 @@ ast2js.pipe = require('./pipe') ast2js.processSubstitution = notImplemented ast2js.redirectFd = require('./redirectFd') ast2js['until-loop'] = require('./until-loop') -ast2js.variable = require('./variable') +ast2js.variable = variable ast2js.variableAssignment = require('./variableAssignment') -ast2js.variableSubstitution = require('./variableSubstitution') +ast2js.variableSubstitution = variable ast2js['while-loop'] = require('./while-loop') diff --git a/lib/ast2js/variable.js b/lib/ast2js/variable.js index 1ff3f10..246f5f4 100644 --- a/lib/ast2js/variable.js +++ b/lib/ast2js/variable.js @@ -1,9 +1,6 @@ -var ast2js = require('./index') - - function variable(item, callback) { - var name = item.name + var name = item.name || item.expression var value = global[name] diff --git a/lib/ast2js/variableSubstitution.js b/lib/ast2js/variableSubstitution.js deleted file mode 100644 index e866bb6..0000000 --- a/lib/ast2js/variableSubstitution.js +++ /dev/null @@ -1,12 +0,0 @@ -var ast2js = require('./index') - - -function variableSubstitution(item, callback) -{ - var value = item.expression - - callback(null, global[value] || process.env[value]) -} - - -module.exports = variableSubstitution From 820afcfc65e1b655f2ed3385d178e89924bae8f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sun, 7 Feb 2016 14:17:02 +0100 Subject: [PATCH 41/94] Update environment variables if they are defined there --- lib/ast2js/variable.js | 2 +- lib/ast2js/variableAssignment.js | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/ast2js/variable.js b/lib/ast2js/variable.js index 246f5f4..681d4d6 100644 --- a/lib/ast2js/variable.js +++ b/lib/ast2js/variable.js @@ -4,7 +4,7 @@ function variable(item, callback) var value = global[name] - if(value == null) value = process.env[name] + if(value === undefined) value = process.env[name] if(value == null) value = '' callback(null, value) diff --git a/lib/ast2js/variableAssignment.js b/lib/ast2js/variableAssignment.js index f0d4f85..d755d0a 100644 --- a/lib/ast2js/variableAssignment.js +++ b/lib/ast2js/variableAssignment.js @@ -7,7 +7,16 @@ function variableAssignment(item, callback) { if(error) return callback(error) - global[item.name] = value + var name = item.name + + // Exported environment variable + var env = process.env + if(env[name] !== undefined) + env[name] = value + + // Local environment variable + else + global[name] = value callback() }) From 773908dd587cce91a539659c1e8faaaea98b642e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sun, 7 Feb 2016 14:59:01 +0100 Subject: [PATCH 42/94] Not use global scope to store environment variables --- lib/ast2js/_environment.js | 30 ++++++++++++++++++++++++++++++ lib/ast2js/_execCommands.js | 7 ++++--- lib/ast2js/variable.js | 12 ++++-------- lib/ast2js/variableAssignment.js | 14 +++----------- lib/builtins.js | 2 +- lib/completer.js | 7 ++++--- 6 files changed, 46 insertions(+), 26 deletions(-) create mode 100644 lib/ast2js/_environment.js diff --git a/lib/ast2js/_environment.js b/lib/ast2js/_environment.js new file mode 100644 index 0000000..a0533aa --- /dev/null +++ b/lib/ast2js/_environment.js @@ -0,0 +1,30 @@ +var environment = {} + + +exports.keys = function() +{ + return Object.keys(environment) +} + +exports.get = function(key) +{ + var value = environment[key] + + if(value === undefined) value = process.env[key] + if(value == null) value = '' + + return value +} + +exports.set = function(key, value) +{ + var env = process.env + + // Exported environment variable + if(env[key] !== undefined) + env[key] = value + + // Local environment variable + else + environment[key] = value +} diff --git a/lib/ast2js/_execCommands.js b/lib/ast2js/_execCommands.js index b08529d..4cc959a 100644 --- a/lib/ast2js/_execCommands.js +++ b/lib/ast2js/_execCommands.js @@ -6,7 +6,8 @@ var eachSeries = require('async').eachSeries module.exports = execCommands -var ast2js = require('./index') +var ast2js = require('./index') +var environment = require('./_environment') var npmPath = require('npm-path').bind(null, {env:{PATH:process.env.PATH}}) @@ -87,8 +88,8 @@ function execCommands(rl, commands, callback) }) .on('exit', function(code, signal) { - global['?' ] = code - global['??'] = signal + environment.set('?' , code) + environment.set('??', signal) }) }) }, diff --git a/lib/ast2js/variable.js b/lib/ast2js/variable.js index 681d4d6..87d6699 100644 --- a/lib/ast2js/variable.js +++ b/lib/ast2js/variable.js @@ -1,13 +1,9 @@ -function variable(item, callback) -{ - var name = item.name || item.expression - - var value = global[name] +var environment = require('./_environment') - if(value === undefined) value = process.env[name] - if(value == null) value = '' - callback(null, value) +function variable(item, callback) +{ + callback(null, environment.get(item.name || item.expression)) } diff --git a/lib/ast2js/variableAssignment.js b/lib/ast2js/variableAssignment.js index d755d0a..b39cb0d 100644 --- a/lib/ast2js/variableAssignment.js +++ b/lib/ast2js/variableAssignment.js @@ -1,4 +1,5 @@ -var ast2js = require('./index') +var ast2js = require('./index') +var environment = require('./_environment') function variableAssignment(item, callback) @@ -7,16 +8,7 @@ function variableAssignment(item, callback) { if(error) return callback(error) - var name = item.name - - // Exported environment variable - var env = process.env - if(env[name] !== undefined) - env[name] = value - - // Local environment variable - else - global[name] = value + environment.set(item.name, value) callback() }) diff --git a/lib/builtins.js b/lib/builtins.js index 400cb5f..562f699 100644 --- a/lib/builtins.js +++ b/lib/builtins.js @@ -6,7 +6,7 @@ function noop(){} function cd(argv) { - // `cd` is a special case, since it needs to change the global environment + // `cd` is a special case, since it needs to change the shell environment var env = process.env var result = new Readable({objectMode: true}) diff --git a/lib/completer.js b/lib/completer.js index 5b21a02..507eab5 100644 --- a/lib/completer.js +++ b/lib/completer.js @@ -3,7 +3,8 @@ var fs = require('fs') var pc = require('lib-pathcomplete') var ps = require('lib-pathsearch') -var builtins = require('./builtins') +var builtins = require('./builtins') +var environment = require('./ast2js/_environment') var execinfo_path = process.env.PATH.split(':') @@ -25,13 +26,13 @@ function completer(line, callback) // user is typing first command var is_first = !split.length - // Global and environment variables + // Environment variables if(item === '$') return callback(2) if(item[0] === '$') { var item2 = item.substr(1) - var vars = Object.keys(global) + var vars = environment.keys() .filter(function(name) { return name.substr(0, item.length-1) === item2 From b7222b2c5dcc5845c2f6dd52f3113205ca50b750 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Mon, 8 Feb 2016 23:35:50 +0100 Subject: [PATCH 43/94] decode prompt --- lib/ast2js/_environment.js | 9 ++++++++- lib/builtins.js | 3 ++- package.json | 1 + server.js | 17 ++++++++++++++--- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/lib/ast2js/_environment.js b/lib/ast2js/_environment.js index a0533aa..75f942d 100644 --- a/lib/ast2js/_environment.js +++ b/lib/ast2js/_environment.js @@ -1,4 +1,11 @@ -var environment = {} +var environment = +{ + '?': 0, + PS1: '\\w > ', + PS2: '> ', + PS4: '+ ' +} + exports.keys = function() diff --git a/lib/builtins.js b/lib/builtins.js index 562f699..bc7269b 100644 --- a/lib/builtins.js +++ b/lib/builtins.js @@ -1,4 +1,5 @@ var Readable = require('stream').Readable +var resolve = require('path').resolve function noop(){} @@ -33,7 +34,7 @@ function cd(argv) } // dir.replace(/^~\//, env.HOME+'/') -// dir = path.resolve(env.PWD, dir) + dir = resolve(env.PWD, dir) try { diff --git a/package.json b/package.json index b2cd17a..ebb54f9 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "async": "^1.5.2", + "decode-prompt": "0.0.2", "glob": "~6.0.4", "lib-pathcomplete": "0.0.1", "lib-pathsearch": "piranna/node-lib-pathsearch", diff --git a/server.js b/server.js index d74cb07..2bff8c4 100755 --- a/server.js +++ b/server.js @@ -2,9 +2,11 @@ var createInterface = require('readline').createInterface -var parse = require('shell-parse') +var decode = require('decode-prompt') +var parse = require('shell-parse') var completer = require('./lib/completer') +var environment = require('./lib/ast2js/_environment') var execCommands = require('./lib/ast2js/_execCommands') @@ -20,9 +22,18 @@ var input = '' function prompt(smallPrompt) { - input = '' + if(smallPrompt) + var ps = environment.get('PS2') + + else + { + input = '' + + var ps = environment.get('PS1') + } + + rl.setPrompt(decode(ps, {env: process.env})) - rl.setPrompt(smallPrompt ? '> ' : process.cwd()+'> ') rl.prompt() } From 82030652094ba6208e57822c5fee322fd6bbe52f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Tue, 9 Feb 2016 00:16:04 +0100 Subject: [PATCH 44/94] Allow globs on command arguments --- lib/ast2js/command.js | 6 +++++- lib/ast2js/glob.js | 2 +- package.json | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/ast2js/command.js b/lib/ast2js/command.js index df67b4a..f07dab9 100644 --- a/lib/ast2js/command.js +++ b/lib/ast2js/command.js @@ -2,7 +2,8 @@ var Domain = require('domain').Domain var Duplex = require('stream').Duplex var Readable = require('stream').Readable -var map = require('async').map +var flatten = require('array-flatten') +var map = require('async').map var ast2js = require('./index') var redirects = require('./_redirects') @@ -63,6 +64,9 @@ function command(item, callback) { if(error) return callback(error) + // Globs return an array, flat it + argv = flatten(argv) + // Redirects redirects(item.redirects, function(error, stdio) { diff --git a/lib/ast2js/glob.js b/lib/ast2js/glob.js index 115b4cf..83ab371 100644 --- a/lib/ast2js/glob.js +++ b/lib/ast2js/glob.js @@ -1,4 +1,4 @@ -var globFunc = require("glob") +var globFunc = require('glob') function glob(item, callback) diff --git a/package.json b/package.json index ebb54f9..39915f4 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "nsh": "server.js" }, "dependencies": { + "array-flatten": "^2.0.0", "async": "^1.5.2", "decode-prompt": "0.0.2", "glob": "~6.0.4", From 1e0abfcf4782048dc722bc758abbf2266794de4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Tue, 9 Feb 2016 00:19:49 +0100 Subject: [PATCH 45/94] optimizations --- lib/ast2js/ifElse.js | 39 +++++++++++++++++++-------------------- lib/ast2js/pipe.js | 4 ++-- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/lib/ast2js/ifElse.js b/lib/ast2js/ifElse.js index 2b221b2..2a7df57 100644 --- a/lib/ast2js/ifElse.js +++ b/lib/ast2js/ifElse.js @@ -1,38 +1,37 @@ -var async = require('async') +var detectSeries = require('async').detectSeries var ast2js = require('./index') function ifElse(item, callback) { - ast2js(item.test, function(error, value) + function runTest(item, callback2) { - if(error) return callback(error) + ast2js(item.test, function(error, result) + { + if(error) return callback(error) - if(value) return ast2js(item.body, callback) + callback2(result) + }) + } - var elifBlocks = item.elifBlocks || [] + function execBody(block) + { + if(block) return ast2js(block.body, callback) - function runTests(item, callback2) - { - ast2js(item.test, function(error, result) - { - if(error) return callback(error) + if(item.elseBody) return ast2js(item.elseBody, callback) - callback2(result) - }) - } + callback() + } - function execBody(block) - { - if(block) return ast2js(block.body, callback) - if(item.elseBody) return ast2js(item.elseBody, callback) + ast2js(item.test, function(error, value) + { + if(error) return callback(error) - callback() - } + if(value) return ast2js(item.body, callback) - async.detectSeries(elifBlocks, runTests, execBody) + detectSeries(item.elifBlocks || [], runTest, execBody) }) } diff --git a/lib/ast2js/pipe.js b/lib/ast2js/pipe.js index 71a58c0..41b71b5 100644 --- a/lib/ast2js/pipe.js +++ b/lib/ast2js/pipe.js @@ -1,9 +1,9 @@ -var ast2js = require('./index') +var command = require('./command') function pipe(item, callback) { - ast2js(item.command, callback) + command(item.command, callback) } From 0fea07887bce9d9d458d00b067efaa3d328775dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Tue, 9 Feb 2016 00:20:43 +0100 Subject: [PATCH 46/94] fixed redirections --- lib/ast2js/_redirects.js | 18 ++++++++++++++---- lib/ast2js/redirectFd.js | 31 ++++++++++++++++++++++++------- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/lib/ast2js/_redirects.js b/lib/ast2js/_redirects.js index c297055..0032612 100644 --- a/lib/ast2js/_redirects.js +++ b/lib/ast2js/_redirects.js @@ -3,6 +3,16 @@ var reduce = require('async').reduce var ast2js = require('./index') +function getSrcFd(stdio, fd) +{ + var result = stdio[fd] + + if(typeof result === 'string') return fd + + return result +} + + function iterator(stdio, redirect, callback) { ast2js(redirect, function(error, value) @@ -13,11 +23,11 @@ function iterator(stdio, redirect, callback) switch(type) { case 'duplicateFd': - stdio[redirect.destFd] = stdio[redirect.srcFd] + stdio[redirect.destFd] = getSrcFd(stdio, redirect.srcFd) break; case 'moveFd': - stdio[redirect.dest] = stdio[redirect.fd] + stdio[redirect.dest] = getSrcFd(stdio, redirect.fd) stdio[redirect.fd] = 'ignore' break; @@ -28,8 +38,8 @@ function iterator(stdio, redirect, callback) case 'redirectFd': stdio[redirect.fd] = value - if(redirect.fd === '&>' - || redirect.fd === '&>>') + if(redirect.op === '&>' + || redirect.op === '&>>') stdio[2] = value break; diff --git a/lib/ast2js/redirectFd.js b/lib/ast2js/redirectFd.js index cde9a01..2844d1a 100644 --- a/lib/ast2js/redirectFd.js +++ b/lib/ast2js/redirectFd.js @@ -5,6 +5,21 @@ var ast2js = require('./index') function redirectFd(item, callback) { + function onError(error) + { + this.removeListener('open', onOpen) + + callback(error) + } + + function onOpen() + { + this.removeListener('error', onError) + + callback(null, this) + } + + ast2js(item.filename, function(error, filename) { if(error) return callback(error) @@ -16,26 +31,28 @@ function redirectFd(item, callback) { case '<': result = fs.createReadStream(filename) - break; + break case '>': case '&>': + result = fs.createWriteStream(filename, {flags: 'wx'}) + break + + case '>|': result = fs.createWriteStream(filename) - break; + break case '>>': case '&>>': result = fs.createWriteStream(filename, {flags: 'a'}) - break; - - case '>|': - return callback('redirectFd "'+op+'" op not implemented') + break default: return callback('Unknown redirectFd op "'+op+'"') } - result.once('open', callback.bind(null, null, result)) + result.once('error', onError) + result.once('open' , onOpen) }) } From c68b412001d680095da8d50637cac349960fdc5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Tue, 9 Feb 2016 00:22:46 +0100 Subject: [PATCH 47/94] Store `$OLDPWD` as global variable as POSIX dictates --- lib/builtins.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/builtins.js b/lib/builtins.js index bc7269b..50f0d75 100644 --- a/lib/builtins.js +++ b/lib/builtins.js @@ -1,6 +1,8 @@ var Readable = require('stream').Readable var resolve = require('path').resolve +var environment = require('./ast2js/_environment') + function noop(){} @@ -20,7 +22,7 @@ function cd(argv) var showNewPwd = false if(dir === '-') { - dir = env.OLDPWD + dir = environment.get('OLDPWD') if(!dir) { @@ -50,7 +52,7 @@ function cd(argv) return result } - env.OLDPWD = env.PWD + environment.set('OLDPWD', env.PWD) env.PWD = dir if(showNewPwd) From 350cebfd7313d8f3e3451cfc0d19eed4f5a38688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Tue, 9 Feb 2016 00:24:32 +0100 Subject: [PATCH 48/94] Don't pipe `stderr` to the one of piped `stdout` if it's not available --- lib/ast2js/_spawnStream.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/ast2js/_spawnStream.js b/lib/ast2js/_spawnStream.js index 400bc25..2134483 100644 --- a/lib/ast2js/_spawnStream.js +++ b/lib/ast2js/_spawnStream.js @@ -102,15 +102,18 @@ function spawnStream(command, argv, options) // Expose `stderr` so it can be used later. result.stderr = stderr - // Redirect `stderr` from piped command to our own `stderr`, since there's - // no way to redirect it to `process.stderr` by default as it should be. - // This way we can at least fetch the error messages someway instead of lost - // them... - var out_stderr = stdout.stderr - if(out_stderr) + if(stdout) { - out_stderr.pipe(stderr) - out_stderr.once('end', out_stderr.unpipe.bind(out_stderr, stderr)) + // Redirect `stderr` from piped command to our own `stderr`, since there's + // no way to redirect it to `process.stderr` by default as it should be. + // This way we can at least fetch the error messages someway instead of + // lost them... + var out_stderr = stdout.stderr + if(out_stderr) + { + out_stderr.pipe(stderr) + out_stderr.once('end', out_stderr.unpipe.bind(out_stderr, stderr)) + } } } From c56a13749357972383ae34cf5500a5759fd14ecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Tue, 9 Feb 2016 00:32:33 +0100 Subject: [PATCH 49/94] Full backup of environment before command substitution --- lib/ast2js/_environment.js | 32 +++++++++++++++++++++++++++++++ lib/ast2js/commandSubstitution.js | 21 ++++++++------------ 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/lib/ast2js/_environment.js b/lib/ast2js/_environment.js index 75f942d..a55b15e 100644 --- a/lib/ast2js/_environment.js +++ b/lib/ast2js/_environment.js @@ -7,6 +7,9 @@ var environment = } +// +// Regular access +// exports.keys = function() { @@ -25,6 +28,8 @@ exports.get = function(key) exports.set = function(key, value) { + if(value === undefined) return exports.del(key) + var env = process.env // Exported environment variable @@ -35,3 +40,30 @@ exports.set = function(key, value) else environment[key] = value } + +exports.del = function(key) +{ + var value = environment[key] + + if(value !== undefined) + delete environment[key] + else + delete process.env[key] +} + + +// +// Stacked environment variables +// + +exports.push = function() +{ + process.env = {__proto__: process.env} + environment = {__proto__: environment} +} + +exports.pop = function() +{ + process.env = process.env.__proto__ + environment = environment.__proto__ +} diff --git a/lib/ast2js/commandSubstitution.js b/lib/ast2js/commandSubstitution.js index a6d29ce..eeacb12 100644 --- a/lib/ast2js/commandSubstitution.js +++ b/lib/ast2js/commandSubstitution.js @@ -1,5 +1,6 @@ var Writable = require('stream').Writable +var environment = require('./_environment') var execCommands = require('./_execCommands') @@ -14,22 +15,16 @@ function commandSubstitution(item, callback) done() } - // Backup cwd - var env = process.env - - var pwd = env.PWD - var oldPwd = env.OLDPWD + // Protect environment variables + environment.push() execCommands({output: output}, item.commands, function(error) { - // Restore (possible) changed cwd - process.chdir(pwd) - env.PWD = pwd - - if(oldPwd) - env.OLDPWD = oldPwd - else - delete env.OLDPWD + // Restore environment variables + environment.pop() + + // Restore (possible) changed current dir + process.chdir(process.env.PWD) if(error) return callback(error) From 0c5b82e9e60ebe735a62e6ed4e6d50b2732d722d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Tue, 9 Feb 2016 00:40:12 +0100 Subject: [PATCH 50/94] Clean-up of auto-completion of environment variables --- lib/ast2js/_environment.js | 2 +- lib/completer.js | 77 ++++++++++++++++++++++---------------- 2 files changed, 46 insertions(+), 33 deletions(-) diff --git a/lib/ast2js/_environment.js b/lib/ast2js/_environment.js index a55b15e..3f49b34 100644 --- a/lib/ast2js/_environment.js +++ b/lib/ast2js/_environment.js @@ -11,7 +11,7 @@ var environment = // Regular access // -exports.keys = function() +exports.ownKeys = function() { return Object.keys(environment) } diff --git a/lib/completer.js b/lib/completer.js index 507eab5..373ee4c 100644 --- a/lib/completer.js +++ b/lib/completer.js @@ -10,15 +10,30 @@ var environment = require('./ast2js/_environment') var execinfo_path = process.env.PATH.split(':') +function filterNames(name) +{ + return name.substr(0, this.length) === this +} + +function getEnvVars(item) +{ + var vars = environment.ownKeys() .filter(filterNames, item) + var env = Object.keys(process.env).filter(filterNames, item) + + if(vars.length && env.length) vars.push('') + + return vars.concat(env) +} + + // auto-complete handler function completer(line, callback) { + // avoid crazy auto-completions when the line is empty + if(!line) return callback(1) + var split = line.split(/\s+/) var item = split.pop() - var outs = [] - - // avoid crazy auto-completions when the line is empty - if (!line) return callback(1) // user is attempting to type a relative directory var is_rel = item[0] === '.' @@ -30,31 +45,17 @@ function completer(line, callback) if(item === '$') return callback(2) if(item[0] === '$') { - var item2 = item.substr(1) - - var vars = environment.keys() - .filter(function(name) + var result = getEnvVars(item.substr(1)).map(function(name) { - return name.substr(0, item.length-1) === item2 - }) - .map(function(name) - { - return '$'+name - }) + if(name === '') return name - var env = Object.keys(process.env) - .filter(function(name) - { - return name.substr(0, item.length-1) === item2 - }) - .map(function(name) - { return '$'+name }) - if(vars.length && env.length) vars.push('') + // if there is only one environment variable, append a space after it + if(result.length === 1) result[0] += ' ' - return callback(null, [vars.concat(env), item]) + return callback(null, [result, item]) } // if this is the first token on the line @@ -64,23 +65,31 @@ function completer(line, callback) { if(err) return callback(err) + // Environment variables + var envVars = getEnvVars(item).map(function(name) + { + if(name !== '') name += '=' + + return name + }) + // Builtins var names = Object.keys(builtins).filter(function(name) { return name.substr(0, item.length) === item }) - if(names.length) - { - if(execs.length) execs.unshift('') + if(envVars.length && names.length) envVars.push('') + if(names .length && execs.length) names .push('') - execs.unshift.apply(execs, names) - } + var result = envVars.concat(names, execs) // if there is only one executable, append a space after it - if(execs.length === 1) execs[0] += ' ' + var length = result.length + if(length === 1 && result[length-1] !== '=') + execs[0] += ' ' - callback(null, [execs, item]) + callback(null, [result, item]) }) // if this is not the first token on the line @@ -89,17 +98,21 @@ function completer(line, callback) { if(err) return callback(err) + var result = [arr, info.file] + // if there is only one completion, and it's a directory // automatically append a '/' to the completion if(arr.length === 1) return fs.stat(info.dir + arr[0], function(error, stats) { + if(error) return callback(error) + if(stats.isDirectory()) arr[0] += '/' - callback(null, [arr, info.file]) + callback(null, result) }) - callback(null, [arr, info.file]); + callback(null, result) }) } From 566af96ce491a426f8cc0b88198012d6aca87d66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Tue, 9 Feb 2016 00:43:58 +0100 Subject: [PATCH 51/94] Recover for errors and show them instead of throw them --- server.js | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/server.js b/server.js index 2bff8c4..322b2b2 100755 --- a/server.js +++ b/server.js @@ -37,6 +37,13 @@ function prompt(smallPrompt) rl.prompt() } +function onError(error) +{ + console.error(error) + + return prompt() +} + prompt() @@ -48,26 +55,27 @@ rl.on('line', function(line) try { - var ast = parse(input) + var commands = parse(input) } - catch(err) + catch(error) { - if(err.constructor !== parse.SyntaxError) throw err + if(error.constructor !== parse.SyntaxError) return onError(error) - line = input.slice(err.offset) + line = input.slice(error.offset) try { parse(line, 'continuationStart') - return prompt(true) } - catch(err) + catch(error) { - throw err + return onError(error) } + + return prompt(true) } - execCommands(this, ast, function(error) + execCommands(this, commands, function(error) { if(error) console.error(error) From a86b7b26d5495502501437e9a32f981d0ff38c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Tue, 9 Feb 2016 00:51:59 +0100 Subject: [PATCH 52/94] Removed old files --- lib/index.js | 89 ------------------------------------------------- lib/readline.js | 85 ---------------------------------------------- 2 files changed, 174 deletions(-) delete mode 100644 lib/index.js delete mode 100644 lib/readline.js diff --git a/lib/index.js b/lib/index.js deleted file mode 100644 index 97c3f97..0000000 --- a/lib/index.js +++ /dev/null @@ -1,89 +0,0 @@ -var inherits = require('util').inherits -var ReplServer = require('repl').ReplServer - -var npmPath = require('npm-path') -var parallel = require('async').parallel - -var completer = require('./completer') -var readline = require('./readline') - - -function pathPrompt() -{ - var prefix - - try - { - prefix = process.cwd() - } - catch (e) - { - prefix = "(none)" - } - - return prefix + " > " -} - - -function NshRepl(prompt, stream, eval_, useGlobal, ignoreUndefined) -{ - if(!(this instanceof NshRepl)) - return new NshRepl(prompt, stream, eval_, useGlobal, ignoreUndefined) - - prompt = prompt || pathPrompt() - - NshRepl.super_.call(this, prompt, stream, eval_, useGlobal, ignoreUndefined) - - - npmPath() - - - var jsEval = this.eval - this.eval = function(cmd, context, filename, callback) - { - // Javascript - jsEval.call(this, cmd, context, filename, function(error, result) - { - if(!error) return callback(null, result) - - // Shell - if(cmd[0] == '(') cmd = cmd.substring(1, cmd.length-1) - - readline(cmd, function(error2, result) - { - if(error2) return callback(error) - - callback(null, result) - }) - }) - } -} -inherits(NshRepl, ReplServer) - - -NshRepl.prototype.displayPrompt = function() -{ - this.prompt = prompt() - NshRepl.super_.displayPrompt.call(this) -} - -NshRepl.prototype.complete = function(line, callback) -{ - parallel( - [ - NshRepl.super_.complete.bind(this, line), - completer.bind(undefined, line) - ], - function(error, results) - { - if(error) return callback(error) - - var execs = results[0][0].concat(results[1][0]) - var item = results[0][1] || results[1][1] - - callback(null, [execs, item]) - }) -} - - -module.exports = NshRepl diff --git a/lib/readline.js b/lib/readline.js deleted file mode 100644 index ba1ce42..0000000 --- a/lib/readline.js +++ /dev/null @@ -1,85 +0,0 @@ -var cp = require('child_process'); - -var npmPath = require('npm-path') -var parse = require('lib-cmdparse'); -var parse = require('lib-cmdparse') - - -function readline(line, callback){ - line = line.trim(); - line = interpolate(line, process.env); - line = interpolate(line, state); - - if(line && line.length) - { - // cd is a native command - if(line.substring(0,2) === 'cd') - return cd(line.substring(2), callback) - - // other commands - return run(line, callback) - } - - callback() -} - -// replace $VARs with environment variables -function interpolate(string, replace){ - return string.replace(/\$[^\s]+/g, function (key){ - var name = key.substring(1); - - var out; - if(replace[name] || replace[name] === 0){ - out = replace[name]; - } else { - out = key; - } - - return out; - }); -} - -function run(line, callback){ - // allow for setting environment variables - // on the command line - var stanza = parse(line); - - // fallback to current environment - stanza.envs.__proto__ = process.env; - - // We must stop reading STDIN because we will soon - // be the background process group, which will raise - // errors when attempting to read/write to the TTY driver - process.stdin.setRawMode(false) - process.stdin.pause(); - - // Sub-Process - var args = stanza.args; - var exec = stanza.exec; - var proc = cp.spawn(exec,args,{ - cwd: process.cwd(), - env: stanza.envs, - - // Inerit the terminal - stdio: 'inherit' - }); - - // Have this shell resume control after the sub-process exists - function res(){ - process.stdin.setRawMode(true) - process.stdin.resume(); - - callback(); - } - - // catch errors - function err(err){ - res(); - } - - proc.on('error', err); - proc.on('exit', end); -} - - -module.exports = readline From 7056363801e308a62bdeae39d51ee1c4bdeceb5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Fri, 12 Feb 2016 20:14:48 +0100 Subject: [PATCH 53/94] Create class `Nsh` --- lib/index.js | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++ server.js | 87 ++----------------------------------------- 2 files changed, 107 insertions(+), 83 deletions(-) create mode 100644 lib/index.js diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..5679987 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,103 @@ +var inherits = require('util').inherits +var Interface = require('readline').Interface + +var decode = require('decode-prompt') +var parse = require('shell-parse') + +var completer = require('./completer') +var environment = require('./ast2js/_environment') +var execCommands = require('./ast2js/_execCommands') + + +function onError(error) +{ + console.error(error) + + return this.prompt() +} + + +function Nsh(input, output) +{ + if(!(this instanceof Nsh)) return new Nsh(input, output) + + Nsh.super_.call(this, input, output, completer) + + + var self = this + + var input = '' + + function execCommandsCallback(error) + { + if(error) console.error(error) + + self.prompt() + } + + this.on('line', function(line) + { + input += line + + if(input === '') return this.prompt() + + try + { + var commands = parse(input) + } + catch(error) + { + if(error.constructor !== parse.SyntaxError) return onError.call(this, error) + + line = input.slice(error.offset) + + try + { + parse(line, 'continuationStart') + } + catch(error) + { + return onError.call(this, error) + } + + return this.prompt(true) + } + + execCommands(this, commands, execCommandsCallback) + }) + + + // + // Public API + // + + var prompt = this.prompt.bind(this) + + /** + * + */ + this.prompt = function(smallPrompt) + { + if(smallPrompt) + var ps = environment.get('PS2') + + else + { + input = '' + + var ps = environment.get('PS1') + } + + this.setPrompt(decode(ps, {env: process.env})) + + prompt() + } + + + // Start acceoting commands + this.prompt() +} +inherits(Nsh, Interface) + + +module.exports = Nsh diff --git a/server.js b/server.js index 322b2b2..c08cfd8 100755 --- a/server.js +++ b/server.js @@ -1,92 +1,13 @@ #!/usr/bin/env node -var createInterface = require('readline').createInterface +var Nsh = require('./lib') -var decode = require('decode-prompt') -var parse = require('shell-parse') -var completer = require('./lib/completer') -var environment = require('./lib/ast2js/_environment') -var execCommands = require('./lib/ast2js/_execCommands') - - -var rl = createInterface( -{ - input: process.stdin, - output: process.stdout, - completer: completer -}) - - -var input = '' - -function prompt(smallPrompt) -{ - if(smallPrompt) - var ps = environment.get('PS2') - - else - { - input = '' - - var ps = environment.get('PS1') - } - - rl.setPrompt(decode(ps, {env: process.env})) - - rl.prompt() -} - -function onError(error) -{ - console.error(error) - - return prompt() -} - - -prompt() - -rl.on('line', function(line) -{ - input += line - - if(input === '') return prompt() - - try - { - var commands = parse(input) - } - catch(error) - { - if(error.constructor !== parse.SyntaxError) return onError(error) - - line = input.slice(error.offset) - - try - { - parse(line, 'continuationStart') - } - catch(error) - { - return onError(error) - } - - return prompt(true) - } - - execCommands(this, commands, function(error) - { - if(error) console.error(error) - - prompt() - }) -}) - -rl.on('SIGINT', function() +Nsh(process.stdin, process.stdout) +.on('SIGINT', function() { this.write('^C') this.clearLine() - prompt() + this.prompt() }) From 81ddac632acc7a80a15589f9d447c3811142578c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sun, 14 Feb 2016 13:46:12 +0100 Subject: [PATCH 54/94] Allow commands to detect they run on an interactive TTY terminal --- lib/ast2js/_execCommands.js | 24 +++++------------------- lib/ast2js/_redirects.js | 16 ++++++++++++++-- lib/ast2js/_spawnStream.js | 7 ++++++- lib/ast2js/command.js | 33 ++++++++++++++++++++++++++------- 4 files changed, 51 insertions(+), 29 deletions(-) diff --git a/lib/ast2js/_execCommands.js b/lib/ast2js/_execCommands.js index 4cc959a..90e7ea5 100644 --- a/lib/ast2js/_execCommands.js +++ b/lib/ast2js/_execCommands.js @@ -10,6 +10,7 @@ var ast2js = require('./index') var environment = require('./_environment') +// Always calculate dynamic `$PATH` based on the original one var npmPath = require('npm-path').bind(null, {env:{PATH:process.env.PATH}}) @@ -22,14 +23,14 @@ function execCommands(rl, commands, callback) { input = rl.input || input - var output = rl.output - eachSeries(commands, function(command, callback) { // `$PATH` is dynamic based on current directory and any command could // change it, so we update it previously to exec any of them npmPath() + command.output = rl.output + ast2js(command, function(error, command) { if(error) return callback(error) @@ -44,20 +45,6 @@ function execCommands(rl, commands, callback) stdin.pipe(command) } - if(command.readable) - { - var fd = output.fd - if(fd != null) - var stdout = tty.WriteStream(fd) - else - { - var stdout = new Writable() - stdout._write = output._write.bind(output) - } - - command.pipe(stdout) - } - var command_stderr = command.stderr if(command_stderr) { @@ -66,16 +53,15 @@ function execCommands(rl, commands, callback) command_stderr.pipe(stderr) } - command.on('end', function() + command.once('end', function() { if(stdin) { - stdin.end() // This in needed to don'l loose chars, not sure why... + stdin.end() // Flush buffered data so we don'l loose any character stdin.unpipe(this) input.resume() } - if(stdout) this .unpipe(stdout) if(stderr) command_stderr.unpipe(stderr) callback() diff --git a/lib/ast2js/_redirects.js b/lib/ast2js/_redirects.js index 0032612..2d150a4 100644 --- a/lib/ast2js/_redirects.js +++ b/lib/ast2js/_redirects.js @@ -3,6 +3,11 @@ var reduce = require('async').reduce var ast2js = require('./index') +function filterPipes(item) +{ + return item.type === 'pipe' +} + function getSrcFd(stdio, fd) { var result = stdio[fd] @@ -12,6 +17,11 @@ function getSrcFd(stdio, fd) return result } +function setOutput(item) +{ + item.command.output = this +} + function iterator(stdio, redirect, callback) { @@ -52,9 +62,11 @@ function iterator(stdio, redirect, callback) } -function redirects(array, callback) +function redirects(output, array, callback) { - reduce(array, ['pipe', 'pipe', 'pipe'], iterator, callback) + array.filter(filterPipes).forEach(setOutput, output) + + reduce(array, ['pipe', output, 'pipe'], iterator, callback) } diff --git a/lib/ast2js/_spawnStream.js b/lib/ast2js/_spawnStream.js index 2134483..5048cf4 100644 --- a/lib/ast2js/_spawnStream.js +++ b/lib/ast2js/_spawnStream.js @@ -90,7 +90,12 @@ function spawnStream(command, argv, options) } // Only one of `stdin` or `stdout` are open, use it directly. - else if(stdin) result = stdin + else if(stdin) + { + result = stdin + + cp.once('exit' , result.emit.bind(result, 'end')) + } else if(stdout) result = stdout // Both `stdin` and `stdout` are clossed. diff --git a/lib/ast2js/command.js b/lib/ast2js/command.js index f07dab9..9b5652d 100644 --- a/lib/ast2js/command.js +++ b/lib/ast2js/command.js @@ -19,23 +19,22 @@ function connectStdio(command, stdio) var stdin = stdio[0] var stdout = stdio[1] + // [ToDo] stdin === 'ignore' if(stdin !== 'pipe') stdin.pipe(command) else stdin = null + // [ToDo] stdout === 'ignore' if(stdout !== 'pipe') command.pipe(stdout) else stdout = null - var result = command + var result - if(stdin || stdout) + if(stdin && stdout) { - stdin = stdin || command - stdout = stdout || command - result = Duplex() result._read = noop result._write = stdin.write.bind(stdin) @@ -45,9 +44,29 @@ function connectStdio(command, stdio) stdout .on('data', result.push.bind(result)) .on('end' , result.emit.bind(result, 'end')) - .on('end' , result.emit.bind(result, 'exit', 0, null)) } + else if(stdin) + { + result = stdin + + var event = command.readable ? 'end' : 'finish' + command.on(event, result.emit.bind(result, 'end')) + } + + else if(stdout) + { + result = command + + var event = stdout.readable ? 'end' : 'finish' + stdout.on(event, result.emit.bind(result, 'end')) + } + + else + result = command + + result.once('end', result.emit.bind(result, 'exit', 0, null)) + return result } @@ -68,7 +87,7 @@ function command(item, callback) argv = flatten(argv) // Redirects - redirects(item.redirects, function(error, stdio) + redirects(item.output, item.redirects, function(error, stdio) { if(error) return callback(error) From a0222dba8e7059ec747f0c2fe9d765f7c8b6319c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sun, 14 Feb 2016 14:37:00 +0100 Subject: [PATCH 55/94] Isolated `wrapStdio()` function for builtins --- lib/ast2js/command.js | 61 +++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/lib/ast2js/command.js b/lib/ast2js/command.js index 9b5652d..f6f29e6 100644 --- a/lib/ast2js/command.js +++ b/lib/ast2js/command.js @@ -14,10 +14,33 @@ var builtins = require('../builtins') function noop(){} -function connectStdio(command, stdio) +function wrapStdio(command, argv, options) { + var stdio = options.stdio + var stdin = stdio[0] var stdout = stdio[1] + var stderr = stdio[2] + + // Create a `stderr` stream for `error` events if none is defined + if(stderr === 'pipe') + { + stderr = new Readable({objectMode: true}) + stderr._read = noop + } + + + // Put `error` events on the `stderr` stream + var d = new Domain() + .on('error', stderr.push.bind(stderr)) + // [ToDo] Close `stderr` when command finish + + // Run the builtin command + d.run(function() + { + command = command.call(options.env, argv) + }) + // [ToDo] stdin === 'ignore' if(stdin !== 'pipe') @@ -67,6 +90,9 @@ function connectStdio(command, stdio) result.once('end', result.emit.bind(result, 'exit', 0, null)) + // Expose `stderr` so it can be used later. + if(stderr !== stdio[2]) result.stderr = stderr + return result } @@ -95,40 +121,17 @@ function command(item, callback) var env = item.env env.__proto__ = process.env - var builtin = builtins[command] - if(builtin) - { - var d = new Domain() - - // Put `error` events on the `stderr` stream - var stderr = stdio[2] - if(stderr === 'pipe') - { - stderr = new Readable({objectMode: true}) - stderr._read = noop - } - - d.on('error', stderr.push.bind(stderr)) - // [ToDo] Close `stderr` when command finish - - // Run the builtin - d.run(function() - { - command = builtin.call(env, argv) - }) - - // Expose `stderr` so it can be used later. - if(stderr !== stdio[2]) command.stderr = stderr - - return callback(null, connectStdio(command, stdio)) - } - var options = { env: env, stdio: stdio } + // Builtins + var builtin = builtins[command] + if(builtin) return callback(null, wrapStdio(builtin, argv, options)) + + // External commands try { command = spawnStream(command, argv, options) From e59000e65fccd155027b1899ab906ae583f9b0c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sun, 14 Feb 2016 15:11:19 +0100 Subject: [PATCH 56/94] Moved stdio related operations on execCommans to `connectStdio()` function --- lib/ast2js/_execCommands.js | 96 +++++++++++++++++++++---------------- 1 file changed, 55 insertions(+), 41 deletions(-) diff --git a/lib/ast2js/_execCommands.js b/lib/ast2js/_execCommands.js index 90e7ea5..27233ac 100644 --- a/lib/ast2js/_execCommands.js +++ b/lib/ast2js/_execCommands.js @@ -13,11 +13,64 @@ var environment = require('./_environment') // Always calculate dynamic `$PATH` based on the original one var npmPath = require('npm-path').bind(null, {env:{PATH:process.env.PATH}}) +var input + function noop(){} +function setStatus(code, signal) +{ + environment.set('?' , code) + environment.set('??', signal) +} + +function restoreStdio(stdin, input, stderr, command_stderr) +{ + if(stdin) + { + stdin.end() // Flush buffered data so we don'l loose any character + stdin.unpipe(this) + + input.resume() + } + if(stderr) command_stderr.unpipe(stderr) +} + +function connectStdio(command, callback) +{ + if(command.writable) + { + var stdin = tty.ReadStream(input.fd) + + input.pause() + stdin.pipe(command) + } + + var command_stderr = command.stderr + if(command_stderr) + { + var stderr = tty.WriteStream(process.stderr.fd) + + command_stderr.pipe(stderr) + } + + command.on('error', function(error) + { + if(error.code !== 'ENOENT') throw error + + restoreStdio(stdin, input, stderr, command_stderr) + + callback(error.path+': not found') + }) + .once('end', function() + { + restoreStdio(stdin, input, stderr, command_stderr) + + callback() + }) + .once('exit', setStatus) +} -var input function execCommands(rl, commands, callback) { @@ -37,46 +90,7 @@ function execCommands(rl, commands, callback) if(command == null) return callback() - if(command.writable) - { - var stdin = tty.ReadStream(input.fd) - - input.pause() - stdin.pipe(command) - } - - var command_stderr = command.stderr - if(command_stderr) - { - var stderr = tty.WriteStream(process.stderr.fd) - - command_stderr.pipe(stderr) - } - - command.once('end', function() - { - if(stdin) - { - stdin.end() // Flush buffered data so we don'l loose any character - stdin.unpipe(this) - - input.resume() - } - if(stderr) command_stderr.unpipe(stderr) - - callback() - }) - .on('error', function(error) - { - if(error.code !== 'ENOENT') throw error - - console.error(error.path+': not found') - }) - .on('exit', function(code, signal) - { - environment.set('?' , code) - environment.set('??', signal) - }) + connectStdio(command, callback) }) }, callback) From c4baa20687b6ac27a12fb2ff98f2e98c46aaaf32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sun, 28 Feb 2016 03:58:26 +0100 Subject: [PATCH 57/94] Tests for `spawnStream` --- lib/ast2js/_spawnStream.js | 71 +++++++++++++++------------- package.json | 9 +++- test.js | 96 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 33 deletions(-) create mode 100644 test.js diff --git a/lib/ast2js/_spawnStream.js b/lib/ast2js/_spawnStream.js index 5048cf4..18fcf4e 100644 --- a/lib/ast2js/_spawnStream.js +++ b/lib/ast2js/_spawnStream.js @@ -11,27 +11,31 @@ function noop(){} */ function wrapStdio(command, argv, options) { + argv = argv || [] + options = options || {} + var stdio = options.stdio + if(!stdio) options.stdio = stdio = [] var stdin = stdio[0] var stdout = stdio[1] var stderr = stdio[2] // Wrap stdio - if(typeof stdin !== 'string' && stdin.fd == null) - stdio[0] = 'pipe' - else + if(stdin && stdin.constructor.name === 'ReadStream') stdin = null - - if(typeof stdout !== 'string' && stdout.fd == null) - stdio[1] = 'pipe' else - stdout = null + stdio[0] = 'pipe' - if(typeof stderr !== 'string' && stderr.fd == null) - stdio[2] = 'pipe' + if(stdout && stdout.constructor.name === 'WriteStream') + stdout = null else + stdio[1] = 'pipe' + + if(stderr && stderr.constructor.name === 'WriteStream') stderr = null + else + stdio[2] = 'pipe' // Create child process var cp = spawn(command, argv, options) @@ -60,7 +64,14 @@ function wrapStdio(command, argv, options) function spawnStream(command, argv, options) { - var stdio = options.stdio + if(argv && argv.constructor.name === 'Object') + { + options = argv + argv = undefined + } + + options = options || {} + var stdio = options.stdio || [] var stdin = stdio[0] var stdout = stdio[1] @@ -68,9 +79,14 @@ function spawnStream(command, argv, options) var cp = wrapStdio(command, argv, options) - stdin = cp.stdin || (stdin && stdin .writable ? stdin : null) - stdout = cp.stdout || (stdout && stdout.readable ? stdout : null) - stderr = cp.stderr || (stderr && stderr.readable ? stderr : null) + // `child_process` define `stdin` also when one is set. Is it a Node.js bug? + if(stdin && stdin .constructor.name === 'ReadStream' ) cp.stdin = null + if(stdout && stdout.constructor.name === 'WriteStream') cp.stdout = null + if(stderr && stderr.constructor.name === 'WriteStream') cp.stderr = null + + stdin = cp.stdin + stdout = cp.stdout + stderr = cp.stderr var result @@ -90,12 +106,7 @@ function spawnStream(command, argv, options) } // Only one of `stdin` or `stdout` are open, use it directly. - else if(stdin) - { - result = stdin - - cp.once('exit' , result.emit.bind(result, 'end')) - } + else if(stdin) result = stdin else if(stdout) result = stdout // Both `stdin` and `stdout` are clossed. @@ -107,24 +118,20 @@ function spawnStream(command, argv, options) // Expose `stderr` so it can be used later. result.stderr = stderr - if(stdout) - { - // Redirect `stderr` from piped command to our own `stderr`, since there's - // no way to redirect it to `process.stderr` by default as it should be. - // This way we can at least fetch the error messages someway instead of - // lost them... - var out_stderr = stdout.stderr - if(out_stderr) - { - out_stderr.pipe(stderr) - out_stderr.once('end', out_stderr.unpipe.bind(out_stderr, stderr)) - } - } + // Redirect `stderr` from piped command to our own `stderr`, since there's + // no way to redirect it to `process.stderr` by default as it should be. + // This way we can at least fetch the error messages someway instead of + // lost them... + var out_stderr = stdout && stdout.stderr + if(out_stderr) out_stderr.pipe(stderr) } cp.on('error', result.emit.bind(result, 'error')) cp.on('exit' , result.emit.bind(result, 'exit' )) + // No `stdout`, emit `end` event when child process exit + if(!stdout) cp.once('exit', result.emit.bind(result, 'end')) + return result } diff --git a/package.json b/package.json index 39915f4..a31a008 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "bin-nsh", "version": "0.5.0", - "description": "Node/No Shell", + "description": "Node SHell", "author": "Jacob Groundwater ", "contributors": [ "Jesús Leganés Combarro 'piranna' " @@ -11,6 +11,10 @@ "bin": { "nsh": "server.js" }, + "scripts": + { + "test": "mocha" + }, "dependencies": { "array-flatten": "^2.0.0", "async": "^1.5.2", @@ -21,5 +25,8 @@ "mkdirp": "~0.5.1", "npm-path": "piranna/npm-path", "shell-parse": "0.0.2" + }, + "devDependencies": { + "mocha": "^2.4.5" } } diff --git a/test.js b/test.js new file mode 100644 index 0000000..434ec04 --- /dev/null +++ b/test.js @@ -0,0 +1,96 @@ +var assert = require('assert') +var PassThrough = require('stream').PassThrough +var tty = require('tty') + +var spawnStream = require('./lib/ast2js/_spawnStream') + + +describe('spawnStream', function() +{ + it('no pipes', function() + { + var result = spawnStream('ls') + + assert.strictEqual(result.constructor.name, 'Duplex') + assert.ok(result.readable) + assert.ok(result.writable) + }) + + describe('pipe regular streams', function() + { + it('stdin', function() + { + var stdio = [new PassThrough()] + + var result = spawnStream('ls', {stdio: stdio}) + + assert.strictEqual(result.constructor.name, 'Socket') + assert.ok(result.readable) + assert.ok(!result.writable) + }) + + it('stdout', function() + { + var stdio = [null, new PassThrough()] + + var result = spawnStream('ls', {stdio: stdio}) + + assert.strictEqual(result.constructor.name, 'Socket') + assert.ok(!result.readable) + assert.ok(result.writable) + }) + + it('fully piped', function() + { + var stdio = [new PassThrough(), new PassThrough()] + + var result = spawnStream('ls', {stdio: stdio}) + + assert.strictEqual(result.constructor.name, 'EventEmitter') + assert.ok(!result.readable) + assert.ok(!result.writable) + }) + }) + + describe('pipe handler streams', function() + { + it('stdin', function() + { + var stdio = [new tty.ReadStream()] + + var result = spawnStream('ls', {stdio: stdio}) + + assert.strictEqual(result.constructor.name, 'Socket') + assert.ok(result.readable) + assert.ok(!result.writable) + }) + + it('stdout', function() + { + var stdio = [null, new tty.WriteStream()] + + var result = spawnStream('ls', {stdio: stdio}) + + assert.strictEqual(result.constructor.name, 'Socket') + assert.ok(!result.readable) + assert.ok(result.writable) + }) + + it('fully piped', function() + { + var stdio = [new tty.ReadStream(), new tty.WriteStream()] + + var result = spawnStream('ls', {stdio: stdio}) + + assert.strictEqual(result.constructor.name, 'EventEmitter') + assert.ok(!result.readable) + assert.ok(!result.writable) + }) + }) +}) + + +// if('inception', function() +// { +// +// }) From edda2b2fd3aa2b1a9a11c7f812b83cc9fabb7f3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sun, 28 Feb 2016 14:41:39 +0100 Subject: [PATCH 58/94] Tests for 'ignore stdio' and 'pipe two commands between them' --- lib/ast2js/_spawnStream.js | 9 ++++--- package.json | 4 +-- test.js | 55 +++++++++++++++++++++++++++++++++++--- 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/lib/ast2js/_spawnStream.js b/lib/ast2js/_spawnStream.js index 18fcf4e..b9a6176 100644 --- a/lib/ast2js/_spawnStream.js +++ b/lib/ast2js/_spawnStream.js @@ -22,17 +22,20 @@ function wrapStdio(command, argv, options) var stderr = stdio[2] // Wrap stdio - if(stdin && stdin.constructor.name === 'ReadStream') + if(typeof stdin === 'string' + || stdin && stdin.constructor.name === 'ReadStream') stdin = null else stdio[0] = 'pipe' - if(stdout && stdout.constructor.name === 'WriteStream') + if(typeof stdout === 'string' + || stdout && stdout.constructor.name === 'WriteStream') stdout = null else stdio[1] = 'pipe' - if(stderr && stderr.constructor.name === 'WriteStream') + if(typeof stderr === 'string' + || stderr && stderr.constructor.name === 'WriteStream') stderr = null else stdio[2] = 'pipe' diff --git a/package.json b/package.json index a31a008..eeb3281 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,7 @@ "bin": { "nsh": "server.js" }, - "scripts": - { + "scripts": { "test": "mocha" }, "dependencies": { @@ -27,6 +26,7 @@ "shell-parse": "0.0.2" }, "devDependencies": { + "concat-stream": "^1.5.1", "mocha": "^2.4.5" } } diff --git a/test.js b/test.js index 434ec04..5322bc7 100644 --- a/test.js +++ b/test.js @@ -2,6 +2,8 @@ var assert = require('assert') var PassThrough = require('stream').PassThrough var tty = require('tty') +var concat = require('concat-stream') + var spawnStream = require('./lib/ast2js/_spawnStream') @@ -16,6 +18,46 @@ describe('spawnStream', function() assert.ok(result.writable) }) + it('ignore stdio', function() + { + var stdio = ['ignore', 'ignore'] + + var result = spawnStream('ls', {stdio: stdio}) + + assert.strictEqual(result.constructor.name, 'EventEmitter') + assert.ok(!result.readable) + assert.ok(!result.writable) + }) + + it('pipe two commands between them', function(done) + { + var expected = ['aa','ab','bb'] + + var stdio = [null, concat(function(data) + { + expected.shift() + expected = expected.join('\n') + + assert.strictEqual(data.toString(), expected+'\n') + + done() + })] + + var grep = spawnStream('grep', ['b'], {stdio: stdio}) + + assert.strictEqual(grep.constructor.name, 'Socket') + assert.ok(!grep.readable) + assert.ok(grep.writable) + + var argv = ['-e', expected.join('\\n')] + + var echo = spawnStream('echo', argv, {stdio: [null, grep]}) + + assert.strictEqual(echo.constructor.name, 'Socket') + assert.ok(!echo.readable) + assert.ok(echo.writable) + }) + describe('pipe regular streams', function() { it('stdin', function() @@ -29,11 +71,18 @@ describe('spawnStream', function() assert.ok(!result.writable) }) - it('stdout', function() + it('stdout', function(done) { - var stdio = [null, new PassThrough()] + var expected = 'asdf' - var result = spawnStream('ls', {stdio: stdio}) + var stdio = [null, concat(function(data) + { + assert.strictEqual(data.toString(), expected+'\n') + + done() + })] + + var result = spawnStream('echo', [expected], {stdio: stdio}) assert.strictEqual(result.constructor.name, 'Socket') assert.ok(!result.readable) From 5a8d13c141099b7b184994e016e76ef2a7f06ed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sun, 28 Feb 2016 21:37:47 +0100 Subject: [PATCH 59/94] Improved tests --- lib/ast2js/_spawnStream.js | 18 +++-- package.json | 3 +- test.js | 144 +++++++++++++++++++++++++++---------- 3 files changed, 123 insertions(+), 42 deletions(-) diff --git a/lib/ast2js/_spawnStream.js b/lib/ast2js/_spawnStream.js index b9a6176..b554c74 100644 --- a/lib/ast2js/_spawnStream.js +++ b/lib/ast2js/_spawnStream.js @@ -1,6 +1,9 @@ var EventEmitter = require('events') var spawn = require('child_process').spawn -var Duplex = require('stream').Duplex +var stream = require('stream') + +var Duplex = stream.Duplex +var Writable = stream.Writable function noop(){} @@ -108,12 +111,19 @@ function spawnStream(command, argv, options) .on('end' , result.emit.bind(result, 'end')) } - // Only one of `stdin` or `stdout` are open, use it directly. - else if(stdin) result = stdin + // Only `stdin` is open, ensure is always 'only' writable. + else if(stdin) + { + result = Writable() + result._write = stdin.write.bind(stdin) + + result.on('finish', stdin.end.bind(stdin)) + } + + // Only `stdout` is open, use it directly. else if(stdout) result = stdout // Both `stdin` and `stdout` are clossed. - // This could never happen, but who knows... else result = new EventEmitter() if(stderr) diff --git a/package.json b/package.json index eeb3281..a848c20 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ }, "devDependencies": { "concat-stream": "^1.5.1", - "mocha": "^2.4.5" + "mocha": "^2.4.5", + "string-to-stream": "^1.0.1" } } diff --git a/test.js b/test.js index 5322bc7..084e651 100644 --- a/test.js +++ b/test.js @@ -1,8 +1,8 @@ -var assert = require('assert') -var PassThrough = require('stream').PassThrough -var tty = require('tty') +var assert = require('assert') +var tty = require('tty') var concat = require('concat-stream') +var str = require('string-to-stream') var spawnStream = require('./lib/ast2js/_spawnStream') @@ -33,7 +33,7 @@ describe('spawnStream', function() { var expected = ['aa','ab','bb'] - var stdio = [null, concat(function(data) + var stdout = concat(function(data) { expected.shift() expected = expected.join('\n') @@ -41,11 +41,11 @@ describe('spawnStream', function() assert.strictEqual(data.toString(), expected+'\n') done() - })] + }) - var grep = spawnStream('grep', ['b'], {stdio: stdio}) + var grep = spawnStream('grep', ['b'], {stdio: [null, stdout]}) - assert.strictEqual(grep.constructor.name, 'Socket') + assert.strictEqual(grep.constructor.name, 'Writable') assert.ok(!grep.readable) assert.ok(grep.writable) @@ -53,92 +53,162 @@ describe('spawnStream', function() var echo = spawnStream('echo', argv, {stdio: [null, grep]}) - assert.strictEqual(echo.constructor.name, 'Socket') + assert.strictEqual(echo.constructor.name, 'Writable') assert.ok(!echo.readable) assert.ok(echo.writable) }) describe('pipe regular streams', function() { - it('stdin', function() + it('stdin', function(done) { - var stdio = [new PassThrough()] + var expected = ['aa','ab','bb'] - var result = spawnStream('ls', {stdio: stdio}) + var stdin = str(expected.join('\n')) - assert.strictEqual(result.constructor.name, 'Socket') - assert.ok(result.readable) - assert.ok(!result.writable) + var grep = spawnStream('grep', ['b'], {stdio: [stdin]}) + + assert.strictEqual(grep.constructor.name, 'Socket') + assert.ok(grep.readable) + assert.ok(!grep.writable) + + grep.pipe(concat(function(data) + { + expected.shift() + expected = expected.join('\n') + + assert.strictEqual(data.toString(), expected+'\n') + })) + + grep.on('end', done) }) it('stdout', function(done) { var expected = 'asdf' - var stdio = [null, concat(function(data) + var stdout = concat(function(data) { assert.strictEqual(data.toString(), expected+'\n') + }) - done() - })] + var echo = spawnStream('echo', [expected], {stdio: [null, stdout]}) - var result = spawnStream('echo', [expected], {stdio: stdio}) + assert.strictEqual(echo.constructor.name, 'Writable') + assert.ok(!echo.readable) + assert.ok(echo.writable) - assert.strictEqual(result.constructor.name, 'Socket') - assert.ok(!result.readable) - assert.ok(result.writable) + echo.on('end', done) }) - it('fully piped', function() + it('fully piped', function(done) { - var stdio = [new PassThrough(), new PassThrough()] + var expected = ['aa','ab','bb'] - var result = spawnStream('ls', {stdio: stdio}) + var stdin = str(expected.join('\n')) + var stdout = concat(function(data) + { + expected.shift() + expected = expected.join('\n') - assert.strictEqual(result.constructor.name, 'EventEmitter') - assert.ok(!result.readable) - assert.ok(!result.writable) + assert.strictEqual(data.toString(), expected+'\n') + }) + + var grep = spawnStream('grep', ['b'], {stdio: [stdin, stdout]}) + + assert.strictEqual(grep.constructor.name, 'EventEmitter') + assert.ok(!grep.readable) + assert.ok(!grep.writable) + + grep.on('end', done) }) }) describe('pipe handler streams', function() { - it('stdin', function() + it('stdin', function(done) { - var stdio = [new tty.ReadStream()] + var stdin = new tty.ReadStream() - var result = spawnStream('ls', {stdio: stdio}) + var result = spawnStream('ls', {stdio: [stdin]}) assert.strictEqual(result.constructor.name, 'Socket') assert.ok(result.readable) assert.ok(!result.writable) + + stdin.on('end', function(){ + console.log('***end***') + }) + stdin.on('finish', function(){ + console.log('***finish***') + }) + result.on('end', function() + { + console.log('***end 2***') + stdin.end() + + done() + }) }) - it('stdout', function() + it('stdout', function(done) { - var stdio = [null, new tty.WriteStream()] + var stdout = new tty.WriteStream() - var result = spawnStream('ls', {stdio: stdio}) + var result = spawnStream('ls', {stdio: [null, stdout]}) - assert.strictEqual(result.constructor.name, 'Socket') + assert.strictEqual(result.constructor.name, 'Writable') assert.ok(!result.readable) assert.ok(result.writable) + + result.on('end', function() + { + + done() + }) }) - it('fully piped', function() + it('fully piped', function(done) { - var stdio = [new tty.ReadStream(), new tty.WriteStream()] + var stdin = new tty.ReadStream() + var stdout = new tty.WriteStream() - var result = spawnStream('ls', {stdio: stdio}) + var result = spawnStream('ls', {stdio: [stdin, stdout]}) assert.strictEqual(result.constructor.name, 'EventEmitter') assert.ok(!result.readable) assert.ok(!result.writable) + + result.on('end', function() + { + + done() + }) }) }) }) +// describe('command', function() +// { +// it('', function(done) +// { +// var item = +// { +// command:, +// args: [], +// stdio:, +// redirects: [], +// env: {} +// } +// +// command(item, function(command) +// { +// +// }) +// }) +// }) + // if('inception', function() // { // From 38df6c72f278652ebbc541f9fad06dc6992ae83e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sat, 5 Mar 2016 14:11:54 +0100 Subject: [PATCH 60/94] Simplified wrapping of spawn streams --- lib/ast2js/_spawnStream.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/lib/ast2js/_spawnStream.js b/lib/ast2js/_spawnStream.js index b554c74..74464ef 100644 --- a/lib/ast2js/_spawnStream.js +++ b/lib/ast2js/_spawnStream.js @@ -9,8 +9,8 @@ var Writable = stream.Writable function noop(){} /** - * Node.js v0.12 don't accept as stdio streams without a file descriptor, so I - * use the original `spawn()` ones and pipe them to the desired ones + * Node.js `spawn` only accept streams with a file descriptor as stdio, so use + * pipes instead and connect the given streams to them. */ function wrapStdio(command, argv, options) { @@ -83,16 +83,15 @@ function spawnStream(command, argv, options) var stdout = stdio[1] var stderr = stdio[2] - var cp = wrapStdio(command, argv, options) + if(stdin == null) stdin = 'pipe' + if(stdout == null) stdout = 'pipe' + if(stderr == null) stderr = 'pipe' - // `child_process` define `stdin` also when one is set. Is it a Node.js bug? - if(stdin && stdin .constructor.name === 'ReadStream' ) cp.stdin = null - if(stdout && stdout.constructor.name === 'WriteStream') cp.stdout = null - if(stderr && stderr.constructor.name === 'WriteStream') cp.stderr = null + var cp = wrapStdio(command, argv, options) - stdin = cp.stdin - stdout = cp.stdout - stderr = cp.stderr + stdin = (stdin === 'pipe') ? cp.stdin : null + stdout = (stdout === 'pipe') ? cp.stdout : null + stderr = (stderr === 'pipe') ? cp.stderr : null var result From 37d547c7b6b9a9a47eb3b27d38c3c44565c325ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Wed, 20 Jul 2016 23:19:51 +0200 Subject: [PATCH 61/94] Updated dependencies --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a848c20..8bcc247 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,10 @@ "test": "mocha" }, "dependencies": { - "array-flatten": "^2.0.0", - "async": "^1.5.2", + "array-flatten": "^2.1.0", + "async": "^2.0.0", "decode-prompt": "0.0.2", - "glob": "~6.0.4", + "glob": "~7.0.5", "lib-pathcomplete": "0.0.1", "lib-pathsearch": "piranna/node-lib-pathsearch", "mkdirp": "~0.5.1", @@ -27,7 +27,7 @@ }, "devDependencies": { "concat-stream": "^1.5.1", - "mocha": "^2.4.5", - "string-to-stream": "^1.0.1" + "mocha": "^2.5.3", + "string-to-stream": "^1.1.0" } } From 25fe9b224cafa67fc7a3b47c3cb19c2900278daa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Wed, 20 Jul 2016 23:20:06 +0200 Subject: [PATCH 62/94] Hack to exec Node.js interactively --- lib/ast2js/_execCommands.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/ast2js/_execCommands.js b/lib/ast2js/_execCommands.js index 27233ac..d54521a 100644 --- a/lib/ast2js/_execCommands.js +++ b/lib/ast2js/_execCommands.js @@ -84,6 +84,11 @@ function execCommands(rl, commands, callback) command.output = rl.output + // [Hack] Force Node.js to exec interactively + // https://github.com/nodejs/node/issues/5574 + if(command.command.value === 'node' && !command.args.length) + command.args = [{type: 'literal', value: '-i'}] + ast2js(command, function(error, command) { if(error) return callback(error) From 5e83df36e6d38344150c2ce89c36c746f570be6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Fri, 22 Jul 2016 00:12:34 +0200 Subject: [PATCH 63/94] Reused `runTest()` function for test statement on `ifElse` AST node --- lib/ast2js/ifElse.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/ast2js/ifElse.js b/lib/ast2js/ifElse.js index 2a7df57..8f7a3d3 100644 --- a/lib/ast2js/ifElse.js +++ b/lib/ast2js/ifElse.js @@ -25,10 +25,8 @@ function ifElse(item, callback) } - ast2js(item.test, function(error, value) + runTest(item, function(value) { - if(error) return callback(error) - if(value) return ast2js(item.body, callback) detectSeries(item.elifBlocks || [], runTest, execBody) From 488e05d3387870cdb5ecd9d0d3dfec717c603f4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Fri, 22 Jul 2016 00:13:00 +0200 Subject: [PATCH 64/94] processSubstitution --- lib/ast2js/index.js | 2 +- lib/ast2js/processSubstitution.js | 62 +++++++++++++++++++++++++++++++ package.json | 4 +- 3 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 lib/ast2js/processSubstitution.js diff --git a/lib/ast2js/index.js b/lib/ast2js/index.js index a25d4ca..ab4fded 100644 --- a/lib/ast2js/index.js +++ b/lib/ast2js/index.js @@ -30,7 +30,7 @@ ast2js.ifElse = require('./ifElse') ast2js.literal = require('./literal') ast2js.moveFd = noop // Processed on `redirects` file ast2js.pipe = require('./pipe') -ast2js.processSubstitution = notImplemented +ast2js.processSubstitution = require('./processSubstitution') ast2js.redirectFd = require('./redirectFd') ast2js['until-loop'] = require('./until-loop') ast2js.variable = variable diff --git a/lib/ast2js/processSubstitution.js b/lib/ast2js/processSubstitution.js new file mode 100644 index 0000000..145fc2c --- /dev/null +++ b/lib/ast2js/processSubstitution.js @@ -0,0 +1,62 @@ +const fs = require('fs') +const tmpdir = require('os').tmpdir + +const mkfifoSync = require('mkfifo').mkfifoSync +const uuid = require('uuid').v4 + +const environment = require('./_environment') +const execCommands = require('./_execCommands') + + +function processSubstitution(item, callback) +{ + const path = tmpdir()+'/'+uuid() + + try + { + mkfifoSync(path, 0600); + } + catch(e) + { + return callback(e) + } + + + // Protect environment variables + environment.push() + + function onExecuted(error) + { + stream.close() + + // Restore environment variables + environment.pop() + + // Restore (possible) changed current dir + process.chdir(process.env.PWD) + + if(error) console.trace(error) + } + + + if(item.readWrite === '<') + var stream = fs.createWriteStream(path) + .on('open', function() + { + execCommands({output: this}, item.commands, onExecuted) + }) + + else + var stream = fs.createReadStream(path) + .on('open', function() + { + execCommands({input: this}, item.commands, onExecuted) + }) + + stream.on('close', fs.unlink.bind(null, path)) + + callback(null, path) +} + + +module.exports = processSubstitution diff --git a/package.json b/package.json index 8bcc247..825fad6 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,10 @@ "lib-pathcomplete": "0.0.1", "lib-pathsearch": "piranna/node-lib-pathsearch", "mkdirp": "~0.5.1", + "mkfifo": "^0.1.5", "npm-path": "piranna/npm-path", - "shell-parse": "0.0.2" + "shell-parse": "0.0.2", + "uuid": "^2.0.2" }, "devDependencies": { "concat-stream": "^1.5.1", From 1eefb6a390e441dea1c04d063d4c6d428a2aff59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=27piranna?= Date: Fri, 22 Jul 2016 10:02:08 +0200 Subject: [PATCH 65/94] Use merged `npm-path` instead of own fork & fixed dependencies versions --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 825fad6..7e35157 100644 --- a/package.json +++ b/package.json @@ -17,14 +17,14 @@ "dependencies": { "array-flatten": "^2.1.0", "async": "^2.0.0", - "decode-prompt": "0.0.2", + "decode-prompt": "^0.0.2", "glob": "~7.0.5", - "lib-pathcomplete": "0.0.1", + "lib-pathcomplete": "^0.0.1", "lib-pathsearch": "piranna/node-lib-pathsearch", "mkdirp": "~0.5.1", "mkfifo": "^0.1.5", - "npm-path": "piranna/npm-path", - "shell-parse": "0.0.2", + "npm-path": "^2.0.2", + "shell-parse": "^0.0.2", "uuid": "^2.0.2" }, "devDependencies": { From 67074505f4388e95ae02826b2190550cc3d35105 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Fri, 22 Jul 2016 23:27:45 +0200 Subject: [PATCH 66/94] Split `variableSubstitution` from `variable` --- lib/ast2js/index.js | 17 ++++------------- lib/ast2js/variable.js | 4 ++-- lib/ast2js/variableSubstitution.js | 10 ++++++++++ 3 files changed, 16 insertions(+), 15 deletions(-) create mode 100644 lib/ast2js/variableSubstitution.js diff --git a/lib/ast2js/index.js b/lib/ast2js/index.js index ab4fded..bf52424 100644 --- a/lib/ast2js/index.js +++ b/lib/ast2js/index.js @@ -3,13 +3,6 @@ function noop(item, callback) callback() } -function notImplemented(item, callback) -{ - var error = new Error("'"+item.type+"' not implemented") - error.item = item - throw error -} - function ast2js(item, callback) { @@ -20,20 +13,18 @@ function ast2js(item, callback) module.exports = ast2js -var variable = require('./variable') - ast2js.command = require('./command') ast2js.commandSubstitution = require('./commandSubstitution') -ast2js.duplicateFd = noop // Processed on `redirects` file +ast2js.duplicateFd = noop // Processed on `_redirects` file ast2js.glob = require('./glob') ast2js.ifElse = require('./ifElse') ast2js.literal = require('./literal') -ast2js.moveFd = noop // Processed on `redirects` file +ast2js.moveFd = noop // Processed on `_redirects` file ast2js.pipe = require('./pipe') ast2js.processSubstitution = require('./processSubstitution') ast2js.redirectFd = require('./redirectFd') ast2js['until-loop'] = require('./until-loop') -ast2js.variable = variable +ast2js.variable = require('./variable') ast2js.variableAssignment = require('./variableAssignment') -ast2js.variableSubstitution = variable +ast2js.variableSubstitution = require('./variableSubstitution') ast2js['while-loop'] = require('./while-loop') diff --git a/lib/ast2js/variable.js b/lib/ast2js/variable.js index 87d6699..c40c59d 100644 --- a/lib/ast2js/variable.js +++ b/lib/ast2js/variable.js @@ -1,9 +1,9 @@ -var environment = require('./_environment') +const environment = require('./_environment') function variable(item, callback) { - callback(null, environment.get(item.name || item.expression)) + callback(null, environment.get(item.name)) } diff --git a/lib/ast2js/variableSubstitution.js b/lib/ast2js/variableSubstitution.js new file mode 100644 index 0000000..c6bf066 --- /dev/null +++ b/lib/ast2js/variableSubstitution.js @@ -0,0 +1,10 @@ +const environment = require('./_environment') + + +function variableSubstitution(item, callback) +{ + callback(null, environment.get(item.expression)) +} + + +module.exports = variableSubstitution From b09e2f2150054cae7c8f3bef0f822ee8746d90c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sun, 24 Jul 2016 02:22:14 +0200 Subject: [PATCH 67/94] Improved completer --- lib/completer.js | 206 ++++++++++++++++++++++++++++++++--------------- package.json | 4 +- 2 files changed, 143 insertions(+), 67 deletions(-) diff --git a/lib/completer.js b/lib/completer.js index 373ee4c..8061e3f 100644 --- a/lib/completer.js +++ b/lib/completer.js @@ -1,18 +1,32 @@ -var fs = require('fs') +const fs = require('fs') -var pc = require('lib-pathcomplete') -var ps = require('lib-pathsearch') +const pc = require('lib-pathcomplete') +const ps = require('lib-pathsearch') +const reduce = require('async/reduce') -var builtins = require('./builtins') -var environment = require('./ast2js/_environment') +const builtins = require('./builtins') +const environment = require('./ast2js/_environment') +const constants = fs.constants +const stat = fs.stat -var execinfo_path = process.env.PATH.split(':') +const EMPTY_ITEM = 1 +const EMPTY_ENV_VAR = 2 + + +// +// Helper functions +// + +function filterEnvVars(item) +{ + return item && !item.includes('=') +} function filterNames(name) { - return name.substr(0, this.length) === this + return name.substr(0, this.length) === this.toString() } function getEnvVars(item) @@ -25,94 +39,156 @@ function getEnvVars(item) return vars.concat(env) } +function isExecutable(stats) +{ + const mode = stats.mode -// auto-complete handler -function completer(line, callback) + return mode & constants.S_IXUSR && stats.uid === process.getuid() + || mode & constants.S_IXGRP && stats.gid === process.getgid() + || mode & constants.S_IXOTH +} + +function mapEnvVarsAsign(name) { - // avoid crazy auto-completions when the line is empty - if(!line) return callback(1) + if(name) name += '=' - var split = line.split(/\s+/) - var item = split.pop() + return name +} - // user is attempting to type a relative directory - var is_rel = item[0] === '.' +function mapEnvVarsRef(name) +{ + if(name) name = '$'+name - // user is typing first command - var is_first = !split.length + return name +} - // Environment variables - if(item === '$') return callback(2) - if(item[0] === '$') - { - var result = getEnvVars(item.substr(1)).map(function(name) - { - if(name === '') return name - return '$'+name - }) +// +// Completer functions +// + +function envVar(item, callback) +{ + const key = item.substr(1) + + if(!key) return callback(EMPTY_ENV_VAR) - // if there is only one environment variable, append a space after it - if(result.length === 1) result[0] += ' ' + var result = getEnvVars(key).map(mapEnvVarsRef) - return callback(null, [result, item]) - } + // if there is only one environment variable, append a space after it + if(result.length === 1) result[0] += ' ' - // if this is the first token on the line - // autocomplete it against commands in the search path - if(!is_rel && is_first) - return ps(item, execinfo_path, function(err, execs) + callback(null, [result, item]) +} + +function relativePath(item, is_arg, callback) +{ + pc(item, function(err, arr, info) + { + if(err) return callback(err) + + // [Hack] Add `.` and `..` entries + if(info.file === '.' ) arr.unshift('.', '..') + if(info.file === '..') arr.unshift('..') + + reduce(arr, {}, function(memo, name, callback) { - if(err) return callback(err) + const path = info.dir + name - // Environment variables - var envVars = getEnvVars(item).map(function(name) + stat(path, function(error, stats) { - if(name !== '') name += '=' + if(error) return callback(error) + + memo[path] = stats - return name + callback(null, memo) }) + }, + function(error, stats) + { + if(error) return callback(error) + + // user is typing the command, autocomplete it only against the + // executables and directories in the current directory + if(!is_arg) + arr = arr.filter(function(name) + { + const stat = stats[info.dir + name] - // Builtins - var names = Object.keys(builtins).filter(function(name) + return stat.isDirectory() || isExecutable(stat) + }) + + arr = arr.map(function(item) { - return name.substr(0, item.length) === item + // If completion is a directory, append a slash + if(stats[info.dir + item].isDirectory()) item += '/' + + return item }) - if(envVars.length && names.length) envVars.push('') - if(names .length && execs.length) names .push('') + // There's just only one completion and it's not a directory, append it a + // space + if(arr.length === 1 && arr[0][arr[0].length-1] !== '/') arr[0] += ' ' - var result = envVars.concat(names, execs) + callback(null, [arr, info.file]) + }) + }) +} - // if there is only one executable, append a space after it - var length = result.length - if(length === 1 && result[length-1] !== '=') - execs[0] += ' ' - callback(null, [result, item]) - }) +/** + * auto-complete handler + */ +function completer(line, callback) +{ + const split = line.split(/\s+/) + const item = split.pop() + const is_arg = split.filter(filterEnvVars).length - // if this is not the first token on the line - // autocomplete it against items in current dir - pc(item, function(err, arr, info) + // avoid crazy auto-completions when the item is empty + if(!item && !is_arg) return callback(EMPTY_ITEM) + + // Environment variables + if(item[0] === '$') return envVar(item, callback) + + // Relative paths and arguments + if(item[0] === '.' || is_arg) return relativePath(item, is_arg, callback) + + // Commands & environment variables + ps(item, process.env.PATH.split(':'), function(err, execs) { if(err) return callback(err) - var result = [arr, info.file] + // Builtins + var names = Object.keys(builtins).filter(filterNames, item) - // if there is only one completion, and it's a directory - // automatically append a '/' to the completion - if(arr.length === 1) - return fs.stat(info.dir + arr[0], function(error, stats) - { - if(error) return callback(error) + // Environment variables + var envVars = getEnvVars(item).map(mapEnvVarsAsign) - if(stats.isDirectory()) arr[0] += '/' + // Current directory + relativePath(item, true, function(err, entries) + { + if(err) return callback(err) - callback(null, result) - }) + entries = entries[0] - callback(null, result) + // Compose result + if(names.length && execs.length) names.push('') + var result = names.concat(execs) + + if(result.length && envVars.length) result.push('') + result = result.concat(envVars) + + if(result.length && entries.length) result.push('') + result = result.concat(entries) + + // if there is only one executable, append a space after it + const result0 = result[0] + const type = result0[result0.length-1] + if(result.length === 1 && type !== '=' && type !== '/') result[0] += ' ' + + callback(null, [result, item]) + }) }) } diff --git a/package.json b/package.json index 825fad6..500a648 100644 --- a/package.json +++ b/package.json @@ -16,10 +16,10 @@ }, "dependencies": { "array-flatten": "^2.1.0", - "async": "^2.0.0", + "async": "^2.0.1", "decode-prompt": "0.0.2", "glob": "~7.0.5", - "lib-pathcomplete": "0.0.1", + "lib-pathcomplete": "piranna/node-lib-pathcomplete", "lib-pathsearch": "piranna/node-lib-pathsearch", "mkdirp": "~0.5.1", "mkfifo": "^0.1.5", From d1278b3e005d9ebee3bdd537d862742f2fe7599c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sun, 24 Jul 2016 13:57:05 +0200 Subject: [PATCH 68/94] Support for Travis-CI and Coveralls.io --- .travis.yml | 9 +++++++++ README.md | 22 +++++++++------------- package.json | 2 ++ 3 files changed, 20 insertions(+), 13 deletions(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..c02af85 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: node_js +node_js: + - "6" + - "6.1" + - "5.11" + - "0.6" + - "iojs" +after_script: + - npm run coveralls diff --git a/README.md b/README.md index 89d06ca..a6a1eac 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,12 @@ -# No(de) Shell +[![Build Status](https://travis-ci.org/piranna/nsh.svg?branch=master)](https://travis-ci.org/piranna/nsh) +[![Coverage Status](https://coveralls.io/repos/github/piranna/nsh/badge.svg?branch=master)](https://coveralls.io/github/piranna/nsh?branch=master) - +# Node SHell -Both **no shell** or **node shell** accurately describe `nsh`. +[![Build for NodeOS](http://i.imgur.com/pIJu2TS.png)]http://nodeos.github.io) -The goal of `nsh` is to provide a basic shell that will run without having `bash` or another process tidy things up first. - -The shell needs to be able to nest without mixing up who gets what keyboard input. -The shell should also be able to run interactive programs like *vim*. - -Right now node doesn't support doing proper job control, although I have a pull-request into libuv about that. - -- https://github.com/joyent/libuv/pull/934 - -Features are welcome, but may not be added until I'm sure the basics are stable. +`nsh` is a basic POSIX compliant shell that will run without having `bash` or +another process tidy things up first. It's also `require()`able and embedable on +other projects like [blesh](https://github.com/piranna/blesh), and has a +collection of basic commands as build-in functions running on the same shell +process powered by [Coreutils.js](https://github.com/piranna/Coreutils.js). diff --git a/package.json b/package.json index 8e122f3..d5ccb50 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "nsh": "server.js" }, "scripts": { + "coveralls": "easy-coveralls", "test": "mocha" }, "dependencies": { @@ -29,6 +30,7 @@ }, "devDependencies": { "concat-stream": "^1.5.1", + "easy-coveralls": "0.0.0", "mocha": "^2.5.3", "string-to-stream": "^1.1.0" } From 6b204d7c51d91d2f040df1c783b84941e9ebd39f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sun, 24 Jul 2016 14:17:30 +0200 Subject: [PATCH 69/94] Support for c++11 --- .travis.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.travis.yml b/.travis.yml index c02af85..f5883e3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,5 +5,13 @@ node_js: - "5.11" - "0.6" - "iojs" +env: + - CXX=g++-4.8 +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 after_script: - npm run coveralls From 2acbfe2687fa325ef4c7e6d46c40054cf17c6430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sun, 24 Jul 2016 14:24:44 +0200 Subject: [PATCH 70/94] Removed support for Node.js 0.6 --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f5883e3..858c214 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ node_js: - "6" - "6.1" - "5.11" - - "0.6" - "iojs" env: - CXX=g++-4.8 From b95688ab7e641f0826b0203d8f6d4dcfadb8719e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro?= Date: Sun, 24 Jul 2016 14:29:20 +0200 Subject: [PATCH 71/94] Fix "Build for NodeOS" badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a6a1eac..5365710 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # Node SHell -[![Build for NodeOS](http://i.imgur.com/pIJu2TS.png)]http://nodeos.github.io) +[![Build for NodeOS](http://i.imgur.com/pIJu2TS.png)](http://nodeos.github.io) `nsh` is a basic POSIX compliant shell that will run without having `bash` or another process tidy things up first. It's also `require()`able and embedable on From da2d2bee34699a2ddffc22c1f7c596cac48a7c90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro?= Date: Sun, 24 Jul 2016 14:29:58 +0200 Subject: [PATCH 72/94] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5365710..4237d82 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # Node SHell -[![Build for NodeOS](http://i.imgur.com/pIJu2TS.png)](http://nodeos.github.io) +[![Built for NodeOS](http://i.imgur.com/pIJu2TS.png)](http://nodeos.github.io) `nsh` is a basic POSIX compliant shell that will run without having `bash` or another process tidy things up first. It's also `require()`able and embedable on From 0450384eef2a190db36255cd642c8c23c219e676 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sun, 24 Jul 2016 23:52:31 +0200 Subject: [PATCH 73/94] Updated `easy-coveralls` --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d5ccb50..6828a5e 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ }, "devDependencies": { "concat-stream": "^1.5.1", - "easy-coveralls": "0.0.0", + "easy-coveralls": "0.0.1", "mocha": "^2.5.3", "string-to-stream": "^1.1.0" } From b790ec7777075abef999ebcc2c2acf34950aa9e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Thu, 28 Jul 2016 23:55:26 +0200 Subject: [PATCH 74/94] Use a `Proxy` object to access the local and environment variables --- lib/ast2js/_environment.js | 80 +++++++++++++++++------------- lib/ast2js/_execCommands.js | 4 +- lib/ast2js/commandSubstitution.js | 2 +- lib/ast2js/processSubstitution.js | 2 +- lib/ast2js/variable.js | 2 +- lib/ast2js/variableAssignment.js | 2 +- lib/ast2js/variableSubstitution.js | 2 +- lib/builtins.js | 13 +++-- lib/completer.js | 6 +-- lib/index.js | 6 +-- 10 files changed, 64 insertions(+), 55 deletions(-) diff --git a/lib/ast2js/_environment.js b/lib/ast2js/_environment.js index 3f49b34..250936b 100644 --- a/lib/ast2js/_environment.js +++ b/lib/ast2js/_environment.js @@ -11,45 +11,52 @@ var environment = // Regular access // -exports.ownKeys = function() +const handler = { - return Object.keys(environment) + ownKeys: function() + { + return Object.keys(environment) + }, + + get: function(_, prop) + { + var value = environment[prop] + + if(value === undefined) value = process.env[prop] + if(value == null) value = '' + + return value + }, + + set: function(_, prop, value) + { + if(value === undefined) return this.deleteProperty(_, prop) + + // Exported environment variable + const env = process.env + if(env[prop] !== undefined) + env[prop] = value + + // Local environment variable + else + environment[prop] = value + }, + + deleteProperty: function(_, prop) + { + // Local environment variable + var value = environment[prop] + if(value !== undefined) + delete environment[prop] + + // Exported environment variable + else + delete process.env[prop] + } } -exports.get = function(key) -{ - var value = environment[key] - - if(value === undefined) value = process.env[key] - if(value == null) value = '' - - return value -} -exports.set = function(key, value) -{ - if(value === undefined) return exports.del(key) - - var env = process.env - - // Exported environment variable - if(env[key] !== undefined) - env[key] = value - - // Local environment variable - else - environment[key] = value -} - -exports.del = function(key) -{ - var value = environment[key] - - if(value !== undefined) - delete environment[key] - else - delete process.env[key] -} +exports = new Proxy({}, handler) // @@ -67,3 +74,6 @@ exports.pop = function() process.env = process.env.__proto__ environment = environment.__proto__ } + + +module.exports = exports diff --git a/lib/ast2js/_execCommands.js b/lib/ast2js/_execCommands.js index d54521a..e26092f 100644 --- a/lib/ast2js/_execCommands.js +++ b/lib/ast2js/_execCommands.js @@ -20,8 +20,8 @@ function noop(){} function setStatus(code, signal) { - environment.set('?' , code) - environment.set('??', signal) + environment['?'] = code + environment['??'] = signal } function restoreStdio(stdin, input, stderr, command_stderr) diff --git a/lib/ast2js/commandSubstitution.js b/lib/ast2js/commandSubstitution.js index eeacb12..d84baf9 100644 --- a/lib/ast2js/commandSubstitution.js +++ b/lib/ast2js/commandSubstitution.js @@ -24,7 +24,7 @@ function commandSubstitution(item, callback) environment.pop() // Restore (possible) changed current dir - process.chdir(process.env.PWD) + process.chdir(environment['PWD']) if(error) return callback(error) diff --git a/lib/ast2js/processSubstitution.js b/lib/ast2js/processSubstitution.js index 145fc2c..0fcd213 100644 --- a/lib/ast2js/processSubstitution.js +++ b/lib/ast2js/processSubstitution.js @@ -33,7 +33,7 @@ function processSubstitution(item, callback) environment.pop() // Restore (possible) changed current dir - process.chdir(process.env.PWD) + process.chdir(environment['PWD']) if(error) console.trace(error) } diff --git a/lib/ast2js/variable.js b/lib/ast2js/variable.js index c40c59d..e2938e5 100644 --- a/lib/ast2js/variable.js +++ b/lib/ast2js/variable.js @@ -3,7 +3,7 @@ const environment = require('./_environment') function variable(item, callback) { - callback(null, environment.get(item.name)) + callback(null, environment[item.name]) } diff --git a/lib/ast2js/variableAssignment.js b/lib/ast2js/variableAssignment.js index b39cb0d..7df3cdb 100644 --- a/lib/ast2js/variableAssignment.js +++ b/lib/ast2js/variableAssignment.js @@ -8,7 +8,7 @@ function variableAssignment(item, callback) { if(error) return callback(error) - environment.set(item.name, value) + environment[item.name] = value callback() }) diff --git a/lib/ast2js/variableSubstitution.js b/lib/ast2js/variableSubstitution.js index c6bf066..12a128f 100644 --- a/lib/ast2js/variableSubstitution.js +++ b/lib/ast2js/variableSubstitution.js @@ -3,7 +3,7 @@ const environment = require('./_environment') function variableSubstitution(item, callback) { - callback(null, environment.get(item.expression)) + callback(null, environment[item.expression]) } diff --git a/lib/builtins.js b/lib/builtins.js index 50f0d75..63e73b6 100644 --- a/lib/builtins.js +++ b/lib/builtins.js @@ -10,19 +10,18 @@ function noop(){} function cd(argv) { // `cd` is a special case, since it needs to change the shell environment - var env = process.env var result = new Readable({objectMode: true}) result._read = noop argv = argv || [] - var dir = argv[0] || env.HOME + var dir = argv[0] || environment.HOME var showNewPwd = false if(dir === '-') { - dir = environment.get('OLDPWD') + dir = environment['OLDPWD'] if(!dir) { @@ -35,8 +34,8 @@ function cd(argv) showNewPwd = true } -// dir.replace(/^~\//, env.HOME+'/') - dir = resolve(env.PWD, dir) +// dir.replace(/^~\//, environment.HOME+'/') + dir = resolve(environment.PWD, dir) try { @@ -52,8 +51,8 @@ function cd(argv) return result } - environment.set('OLDPWD', env.PWD) - env.PWD = dir + environment['OLDPWD'] = environment.PWD + environment.PWD = dir if(showNewPwd) { diff --git a/lib/completer.js b/lib/completer.js index 8061e3f..268c8d5 100644 --- a/lib/completer.js +++ b/lib/completer.js @@ -31,8 +31,8 @@ function filterNames(name) function getEnvVars(item) { - var vars = environment.ownKeys() .filter(filterNames, item) - var env = Object.keys(process.env).filter(filterNames, item) + var vars = Object.getOwnPropertyNames(environment).filter(filterNames, item) + var env = Object.keys(process.env) .filter(filterNames, item) if(vars.length && env.length) vars.push('') @@ -155,7 +155,7 @@ function completer(line, callback) if(item[0] === '.' || is_arg) return relativePath(item, is_arg, callback) // Commands & environment variables - ps(item, process.env.PATH.split(':'), function(err, execs) + ps(item, environment.PATH.split(':'), function(err, execs) { if(err) return callback(err) diff --git a/lib/index.js b/lib/index.js index 5679987..c7ac6bf 100644 --- a/lib/index.js +++ b/lib/index.js @@ -79,16 +79,16 @@ function Nsh(input, output) this.prompt = function(smallPrompt) { if(smallPrompt) - var ps = environment.get('PS2') + var ps = environment['PS2'] else { input = '' - var ps = environment.get('PS1') + var ps = environment['PS1'] } - this.setPrompt(decode(ps, {env: process.env})) + this.setPrompt(decode(ps, {env: environment})) prompt() } From f810a5caae38936f340b0ce6dbc8c7f57a9af589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Sat, 30 Jul 2016 00:14:10 +0200 Subject: [PATCH 75/94] Use `cd` build-in command from `coreutils.js` --- lib/ast2js/_environment.js | 6 ++++ lib/ast2js/command.js | 26 +++++++++------- lib/builtins.js | 61 ++------------------------------------ package.json | 1 + 4 files changed, 26 insertions(+), 68 deletions(-) diff --git a/lib/ast2js/_environment.js b/lib/ast2js/_environment.js index 250936b..4056abb 100644 --- a/lib/ast2js/_environment.js +++ b/lib/ast2js/_environment.js @@ -1,3 +1,5 @@ +'use strict' + var environment = { '?': 0, @@ -40,6 +42,8 @@ const handler = // Local environment variable else environment[prop] = value + + return true }, deleteProperty: function(_, prop) @@ -52,6 +56,8 @@ const handler = // Exported environment variable else delete process.env[prop] + + return true } } diff --git a/lib/ast2js/command.js b/lib/ast2js/command.js index f6f29e6..0a9f688 100644 --- a/lib/ast2js/command.js +++ b/lib/ast2js/command.js @@ -1,15 +1,16 @@ -var Domain = require('domain').Domain -var Duplex = require('stream').Duplex -var Readable = require('stream').Readable +const Domain = require('domain').Domain +const Duplex = require('stream').Duplex +const Readable = require('stream').Readable -var flatten = require('array-flatten') -var map = require('async').map +const flatten = require('array-flatten') +const map = require('async').map +const ToStringStream = require('to-string-stream') -var ast2js = require('./index') -var redirects = require('./_redirects') -var spawnStream = require('./_spawnStream') +const ast2js = require('./index') +const redirects = require('./_redirects') +const spawnStream = require('./_spawnStream') -var builtins = require('../builtins') +const builtins = require('../builtins') function noop(){} @@ -50,7 +51,12 @@ function wrapStdio(command, argv, options) // [ToDo] stdout === 'ignore' if(stdout !== 'pipe') - command.pipe(stdout) + { + if(stdout.objectMode) + command.pipe(stdout) + else + command.pipe(new ToStringStream()).pipe(stdout) + } else stdout = null diff --git a/lib/builtins.js b/lib/builtins.js index 63e73b6..c411283 100644 --- a/lib/builtins.js +++ b/lib/builtins.js @@ -1,67 +1,12 @@ -var Readable = require('stream').Readable -var resolve = require('path').resolve +const coreutils = require('coreutils.js') -var environment = require('./ast2js/_environment') - - -function noop(){} +const environment = require('./ast2js/_environment') function cd(argv) { // `cd` is a special case, since it needs to change the shell environment - - var result = new Readable({objectMode: true}) - result._read = noop - - argv = argv || [] - - var dir = argv[0] || environment.HOME - - var showNewPwd = false - if(dir === '-') - { - dir = environment['OLDPWD'] - - if(!dir) - { - result.emit('error', 'cd: OLDPWD not defined\n') - - result.push(null) - return result - } - - showNewPwd = true - } - -// dir.replace(/^~\//, environment.HOME+'/') - dir = resolve(environment.PWD, dir) - - try - { - process.chdir(dir) - } - catch(error) - { - if(error.code !== 'ENOENT') throw error - - result.emit('error', 'cd: '+dir+': no such file or directory\n') - - result.push(null) - return result - } - - environment['OLDPWD'] = environment.PWD - environment.PWD = dir - - if(showNewPwd) - { - dir.type = 'cd' - result.push(dir+'\n') - } - - result.push(null) - return result + return coreutils.cd(argv, environment) } diff --git a/package.json b/package.json index 6828a5e..7cd8658 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "mkfifo": "^0.1.5", "npm-path": "^2.0.2", "shell-parse": "^0.0.2", + "to-string-stream": "^0.1.0", "uuid": "^2.0.2" }, "devDependencies": { From 35153e9cec5dc186a85c385e4f3ec1a70307e626 Mon Sep 17 00:00:00 2001 From: Oskari Mantere Date: Sat, 29 Oct 2016 09:20:27 +0000 Subject: [PATCH 76/94] Added coreutils to dependencies --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 7cd8658..5ac2ab3 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "dependencies": { "array-flatten": "^2.1.0", "async": "^2.0.1", + "coreutils.js": "github:piranna/coreutils.js", "decode-prompt": "^0.0.2", "glob": "~7.0.5", "lib-pathcomplete": "piranna/node-lib-pathcomplete", From 553ee3d0f583f01f33869c943f9363e61ca1ffe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s-Combarro=20=27piranna?= Date: Fri, 18 Nov 2016 14:25:38 +0100 Subject: [PATCH 77/94] Update dependencies --- package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 5ac2ab3..d61c798 100644 --- a/package.json +++ b/package.json @@ -17,23 +17,23 @@ }, "dependencies": { "array-flatten": "^2.1.0", - "async": "^2.0.1", + "async": "^2.1.2", "coreutils.js": "github:piranna/coreutils.js", "decode-prompt": "^0.0.2", - "glob": "~7.0.5", + "glob": "~7.1.1", "lib-pathcomplete": "piranna/node-lib-pathcomplete", "lib-pathsearch": "piranna/node-lib-pathsearch", "mkdirp": "~0.5.1", - "mkfifo": "^0.1.5", + "mkfifo": "^1.2.5", "npm-path": "^2.0.2", "shell-parse": "^0.0.2", "to-string-stream": "^0.1.0", - "uuid": "^2.0.2" + "uuid": "^3.0.0" }, "devDependencies": { - "concat-stream": "^1.5.1", + "concat-stream": "^1.5.2", "easy-coveralls": "0.0.1", - "mocha": "^2.5.3", + "mocha": "^3.1.2", "string-to-stream": "^1.1.0" } } From b7d8273eb41e6fb8b93cb8b91daf746c106e0783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s-Combarro=20=27piranna?= Date: Sat, 10 Dec 2016 18:14:28 +0100 Subject: [PATCH 78/94] Fixed tests --- lib/ast2js/_spawnStream.js | 80 ++++---- package.json | 3 +- test.js | 215 --------------------- test/fixture.txt | 1 + test/index.js | 380 +++++++++++++++++++++++++++++++++++++ 5 files changed, 421 insertions(+), 258 deletions(-) delete mode 100644 test.js create mode 100644 test/fixture.txt create mode 100644 test/index.js diff --git a/lib/ast2js/_spawnStream.js b/lib/ast2js/_spawnStream.js index 74464ef..f921042 100644 --- a/lib/ast2js/_spawnStream.js +++ b/lib/ast2js/_spawnStream.js @@ -1,9 +1,10 @@ -var EventEmitter = require('events') -var spawn = require('child_process').spawn -var stream = require('stream') +const EventEmitter = require('events') +const spawn = require('child_process').spawn +const stream = require('stream') -var Duplex = stream.Duplex -var Writable = stream.Writable +const Duplex = stream.Duplex +const Readable = stream.Readable +const Writable = stream.Writable function noop(){} @@ -25,19 +26,19 @@ function wrapStdio(command, argv, options) var stderr = stdio[2] // Wrap stdio - if(typeof stdin === 'string' + if(typeof stdin === 'string' || typeof stdin === 'number' || stdin && stdin.constructor.name === 'ReadStream') stdin = null else stdio[0] = 'pipe' - if(typeof stdout === 'string' + if(typeof stdout === 'string' || typeof stdout === 'number' || stdout && stdout.constructor.name === 'WriteStream') stdout = null else stdio[1] = 'pipe' - if(typeof stderr === 'string' + if(typeof stderr === 'string' || typeof stderr === 'number' || stderr && stderr.constructor.name === 'WriteStream') stderr = null else @@ -47,17 +48,17 @@ function wrapStdio(command, argv, options) var cp = spawn(command, argv, options) // Adjust events, pipe streams and restore stdio - if(stdin) + if(stdin != null) { stdin.pipe(cp.stdin) cp.stdin = null } - if(stdout) + if(stdout != null) { cp.stdout.pipe(stdout) cp.stdout = null } - if(stderr) + if(stderr != null) { cp.stderr.pipe(stderr) cp.stderr = null @@ -83,47 +84,44 @@ function spawnStream(command, argv, options) var stdout = stdio[1] var stderr = stdio[2] - if(stdin == null) stdin = 'pipe' - if(stdout == null) stdout = 'pipe' - if(stderr == null) stderr = 'pipe' - var cp = wrapStdio(command, argv, options) - stdin = (stdin === 'pipe') ? cp.stdin : null - stdout = (stdout === 'pipe') ? cp.stdout : null - stderr = (stderr === 'pipe') ? cp.stderr : null + stdin = (stdin == null || stdin === 'pipe') ? cp.stdin : null + stdout = (stdout == null || stdout === 'pipe') ? cp.stdout : null + stderr = (stderr == null || stderr === 'pipe') ? cp.stderr : null var result - // Both `stdin` and `stdout` are open, probably the normal case. - // Create a `Duplex` object with them so command can be used as a filter. - if(stdin && stdout) - { - result = Duplex() - result._read = noop - result._write = stdin.write.bind(stdin) + // Both `stdin` and `stdout` are open, probably the normal case. Create a + // `Duplex` object with them so command can be used as a filter + if(stdin && stdout) result = Duplex() - result.on('finish', stdin.end.bind(stdin)) + // Only `stdout` is open, use it directly + else if(stdout) result = Readable() - stdout - .on('data', result.push.bind(result)) - .on('end' , result.emit.bind(result, 'end')) - } + // Only `stdin` is open, ensure is always 'only' `Writable` + else if(stdin) result = Writable() + + // Both `stdin` and `stdout` are clossed + else result = new EventEmitter() - // Only `stdin` is open, ensure is always 'only' writable. - else if(stdin) + // Connect stdio streams + if(stdin) { - result = Writable() result._write = stdin.write.bind(stdin) - - result.on('finish', stdin.end.bind(stdin)) + result.once('finish', stdin.end.bind(stdin)) } - // Only `stdout` is open, use it directly. - else if(stdout) result = stdout + if(stdout) + { + result._read = noop + stdout.on ('data', result.push.bind(result)) + stdout.once('end' , result.push.bind(result, null)) + } - // Both `stdin` and `stdout` are clossed. - else result = new EventEmitter() + // Use child process `exit` event instead of missing `stdout` `end` event + else + cp.once('exit', result.emit.bind(result, 'end')) if(stderr) { @@ -138,12 +136,10 @@ function spawnStream(command, argv, options) if(out_stderr) out_stderr.pipe(stderr) } + // Propagate process events cp.on('error', result.emit.bind(result, 'error')) cp.on('exit' , result.emit.bind(result, 'exit' )) - // No `stdout`, emit `end` event when child process exit - if(!stdout) cp.once('exit', result.emit.bind(result, 'end')) - return result } diff --git a/package.json b/package.json index d61c798..c6953be 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "concat-stream": "^1.5.2", "easy-coveralls": "0.0.1", "mocha": "^3.1.2", - "string-to-stream": "^1.1.0" + "string-to-stream": "^1.1.0", + "tmp": "0.0.31" } } diff --git a/test.js b/test.js deleted file mode 100644 index 084e651..0000000 --- a/test.js +++ /dev/null @@ -1,215 +0,0 @@ -var assert = require('assert') -var tty = require('tty') - -var concat = require('concat-stream') -var str = require('string-to-stream') - -var spawnStream = require('./lib/ast2js/_spawnStream') - - -describe('spawnStream', function() -{ - it('no pipes', function() - { - var result = spawnStream('ls') - - assert.strictEqual(result.constructor.name, 'Duplex') - assert.ok(result.readable) - assert.ok(result.writable) - }) - - it('ignore stdio', function() - { - var stdio = ['ignore', 'ignore'] - - var result = spawnStream('ls', {stdio: stdio}) - - assert.strictEqual(result.constructor.name, 'EventEmitter') - assert.ok(!result.readable) - assert.ok(!result.writable) - }) - - it('pipe two commands between them', function(done) - { - var expected = ['aa','ab','bb'] - - var stdout = concat(function(data) - { - expected.shift() - expected = expected.join('\n') - - assert.strictEqual(data.toString(), expected+'\n') - - done() - }) - - var grep = spawnStream('grep', ['b'], {stdio: [null, stdout]}) - - assert.strictEqual(grep.constructor.name, 'Writable') - assert.ok(!grep.readable) - assert.ok(grep.writable) - - var argv = ['-e', expected.join('\\n')] - - var echo = spawnStream('echo', argv, {stdio: [null, grep]}) - - assert.strictEqual(echo.constructor.name, 'Writable') - assert.ok(!echo.readable) - assert.ok(echo.writable) - }) - - describe('pipe regular streams', function() - { - it('stdin', function(done) - { - var expected = ['aa','ab','bb'] - - var stdin = str(expected.join('\n')) - - var grep = spawnStream('grep', ['b'], {stdio: [stdin]}) - - assert.strictEqual(grep.constructor.name, 'Socket') - assert.ok(grep.readable) - assert.ok(!grep.writable) - - grep.pipe(concat(function(data) - { - expected.shift() - expected = expected.join('\n') - - assert.strictEqual(data.toString(), expected+'\n') - })) - - grep.on('end', done) - }) - - it('stdout', function(done) - { - var expected = 'asdf' - - var stdout = concat(function(data) - { - assert.strictEqual(data.toString(), expected+'\n') - }) - - var echo = spawnStream('echo', [expected], {stdio: [null, stdout]}) - - assert.strictEqual(echo.constructor.name, 'Writable') - assert.ok(!echo.readable) - assert.ok(echo.writable) - - echo.on('end', done) - }) - - it('fully piped', function(done) - { - var expected = ['aa','ab','bb'] - - var stdin = str(expected.join('\n')) - var stdout = concat(function(data) - { - expected.shift() - expected = expected.join('\n') - - assert.strictEqual(data.toString(), expected+'\n') - }) - - var grep = spawnStream('grep', ['b'], {stdio: [stdin, stdout]}) - - assert.strictEqual(grep.constructor.name, 'EventEmitter') - assert.ok(!grep.readable) - assert.ok(!grep.writable) - - grep.on('end', done) - }) - }) - - describe('pipe handler streams', function() - { - it('stdin', function(done) - { - var stdin = new tty.ReadStream() - - var result = spawnStream('ls', {stdio: [stdin]}) - - assert.strictEqual(result.constructor.name, 'Socket') - assert.ok(result.readable) - assert.ok(!result.writable) - - stdin.on('end', function(){ - console.log('***end***') - }) - stdin.on('finish', function(){ - console.log('***finish***') - }) - result.on('end', function() - { - console.log('***end 2***') - stdin.end() - - done() - }) - }) - - it('stdout', function(done) - { - var stdout = new tty.WriteStream() - - var result = spawnStream('ls', {stdio: [null, stdout]}) - - assert.strictEqual(result.constructor.name, 'Writable') - assert.ok(!result.readable) - assert.ok(result.writable) - - result.on('end', function() - { - - done() - }) - }) - - it('fully piped', function(done) - { - var stdin = new tty.ReadStream() - var stdout = new tty.WriteStream() - - var result = spawnStream('ls', {stdio: [stdin, stdout]}) - - assert.strictEqual(result.constructor.name, 'EventEmitter') - assert.ok(!result.readable) - assert.ok(!result.writable) - - result.on('end', function() - { - - done() - }) - }) - }) -}) - - -// describe('command', function() -// { -// it('', function(done) -// { -// var item = -// { -// command:, -// args: [], -// stdio:, -// redirects: [], -// env: {} -// } -// -// command(item, function(command) -// { -// -// }) -// }) -// }) - -// if('inception', function() -// { -// -// }) diff --git a/test/fixture.txt b/test/fixture.txt new file mode 100644 index 0000000..8bd6648 --- /dev/null +++ b/test/fixture.txt @@ -0,0 +1 @@ +asdf diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..0734022 --- /dev/null +++ b/test/index.js @@ -0,0 +1,380 @@ +const assert = require('assert') +const fs = require('fs') +const tty = require('tty') + +const concat = require('concat-stream') +const str = require('string-to-stream') +const tmp = require('tmp').file + +const spawnStream = require('../lib/ast2js/_spawnStream') + + +describe('spawnStream', function() +{ + it('no pipes', function(done) + { + var expected = ['aa','ab','bb'] + + const stdin = str(expected.join('\n')) + const stdout = concat(function(data) + { + expected.shift() + expected = expected.join('\n') + + assert.strictEqual(data.toString(), expected) + }) + + const result = spawnStream('grep', ['b']) + + assert.strictEqual(result.constructor.name, 'Duplex') + assert.ok(result.readable) + assert.ok(result.writable) + + stdin.pipe(result).pipe(stdout) + + result.on('end', done) + }) + + it('ignore stdio', function() + { + var stdio = ['ignore', 'ignore'] + + var result = spawnStream('ls', {stdio: stdio}) + + assert.strictEqual(result.constructor.name, 'EventEmitter') + assert.ok(!result.readable) + assert.ok(!result.writable) + }) + + it('set a command as `stdin` of another', function(done) + { + var expected = ['aa','ab','bb'] + + const argv = [expected.join('\n'), '-e'] + const echo = spawnStream('echo', argv, {stdio: ['ignore']}) + + assert.strictEqual(echo.constructor.name, 'Readable') + assert.ok(echo.readable) + assert.ok(!echo.writable) + + const stdout = concat(function(data) + { + expected.shift() + expected = expected.join('\n') + + assert.strictEqual(data.toString(), expected+'\n') + + done() + }) + + const grep = spawnStream('grep', ['b'], {stdio: [echo, stdout]}) + + assert.strictEqual(grep.constructor.name, 'EventEmitter') + assert.ok(!grep.readable) + assert.ok(!grep.writable) + }) + + it('set a command as `stdout` of another', function(done) + { + var expected = ['aa','ab','bb'] + + const stdout = concat(function(data) + { + expected.shift() + expected = expected.join('\n') + + assert.strictEqual(data.toString(), expected+'\n') + + done() + }) + + const grep = spawnStream('grep', ['b'], {stdio: [null, stdout]}) + + assert.strictEqual(grep.constructor.name, 'Writable') + assert.ok(!grep.readable) + assert.ok(grep.writable) + + const argv = [expected.join('\n'), '-e'] + const echo = spawnStream('echo', argv, {stdio: ['ignore', grep]}) + + assert.strictEqual(echo.constructor.name, 'EventEmitter') + assert.ok(!echo.readable) + assert.ok(!echo.writable) + + echo.stderr.pipe(process.stderr) + }) + + describe('pipe regular streams', function() + { + it('pipe stdin', function(done) + { + var expected = ['aa','ab','bb'] + + const stdin = str(expected.join('\n')) + const stdout = concat(function(data) + { + expected.shift() + expected = expected.join('\n') + + assert.strictEqual(data.toString(), expected) + + done() + }) + + const grep = spawnStream('grep', ['b'], {stdio: [stdin]}) + + assert.strictEqual(grep.constructor.name, 'Readable') + assert.ok(grep.readable) + assert.ok(!grep.writable) + + grep.pipe(stdout) + }) + + it('pipe stdout', function(done) + { + const expected = 'asdf' + + const stdout = concat(function(data) + { + assert.strictEqual(data.toString(), expected+'\n') + + done() + }) + + const echo = spawnStream('echo', [expected], {stdio: [null, stdout]}) + + assert.strictEqual(echo.constructor.name, 'Writable') + assert.ok(!echo.readable) + assert.ok(echo.writable) + + echo.stderr.pipe(process.stderr) + }) + + it('fully piped', function(done) + { + var expected = ['aa','ab','bb'] + + const stdin = str(expected.join('\n')) + const stdout = concat(function(data) + { + expected.shift() + expected = expected.join('\n') + + assert.strictEqual(data.toString(), expected) + + done() + }) + + var grep = spawnStream('grep', ['b'], {stdio: [stdin, stdout]}) + + assert.strictEqual(grep.constructor.name, 'EventEmitter') + assert.ok(!grep.readable) + assert.ok(!grep.writable) + }) + }) + + describe('pipe handler streams', function() + { + it('pipe stdin', function(done) + { + const expected = 'asdf' + + var stdin = new tty.ReadStream() + const stdout = concat(function(data) + { + expected.shift() + expected = expected.join('\n') + + assert.strictEqual(data.toString(), expected) + + done() + }) + + var result = spawnStream('ls', {stdio: [stdin]}) + + assert.strictEqual(result.constructor.name, 'Readable') + assert.ok(result.readable) + assert.ok(!result.writable) + + result.resume() + result.on('end', done) + }) + + it('pipe stdout', function(done) + { + const expected = 'asdf' + + const stdout = new tty.WriteStream() + + const echo = spawnStream('echo', [expected], {stdio: [null, stdout]}) + + assert.strictEqual(echo.constructor.name, 'Writable') + assert.ok(!echo.readable) + assert.ok(echo.writable) + + echo.on('end', done) + }) + + it('fully piped', function(done) + { + const stdin = new tty.ReadStream() + const stdout = new tty.WriteStream() + + const result = spawnStream('ls', {stdio: [stdin, stdout]}) + + assert.strictEqual(result.constructor.name, 'EventEmitter') + assert.ok(!result.readable) + assert.ok(!result.writable) + + result.on('end', function() + { + + done() + }) + }) + }) +}) + +describe('file descriptors', function() +{ + it('pipe stdin', function(done) + { + fs.open('test/fixture.txt', 'r', function(err, fd) + { + if(err) return done(err) + + function clean(err1) + { + fs.close(fd, function(err2) + { + done(err1 || err2) + }) + } + + const expected = 'asdf' + + const stdout = concat(function(data) + { + assert.strictEqual(data.toString(), expected+'\n') + + clean() + }) + + const echo = spawnStream('echo', [expected], {stdio: [fd]}) + + assert.strictEqual(echo.constructor.name, 'Readable') + assert.ok(echo.readable) + assert.ok(!echo.writable) + + echo.pipe(stdout) + }) + }) + + it('pipe stdout', function(done) + { + const expected = 'asdf' + + tmp(function(err, path, fd, cleanupCallback) + { + if(err) return done(err) + + function clean(err) + { + cleanupCallback() + done(err) + } + + const echo = spawnStream('echo', [expected], {stdio: [null, fd]}) + + assert.strictEqual(echo.constructor.name, 'Writable') + assert.ok(!echo.readable) + assert.ok(echo.writable) + + echo.on('end', function() + { + fs.readFile(path, 'utf-8', function(err, data) + { + if(err) return clean(err) + + assert.strictEqual(data, expected+'\n') + + clean() + }) + }) + }) + }) + + it('fully piped', function(done) + { + fs.open('test/fixture.txt', 'r', function(err, fdStdin) + { + if(err) return done(err) + + function clean1(err1) + { + fs.close(fdStdin, function(err2) + { + done(err1 || err2) + }) + } + + tmp(function(err, path, fdStdout, cleanupCallback) + { + if(err) return clean1(err) + + function clean2(err) + { + cleanupCallback() + clean1(err) + } + + const expected = 'asdf' + + const stdio = [fdStdin, fdStdout] + const echo = spawnStream('echo', [expected], {stdio}) + + assert.strictEqual(echo.constructor.name, 'EventEmitter') + assert.ok(!echo.readable) + assert.ok(!echo.writable) + + echo.on('end', function() + { + fs.readFile(path, 'utf-8', function(err, data) + { + if(err) return clean2(err) + + assert.strictEqual(data, expected+'\n') + + clean2() + }) + }) + }) + }) + }) +}) + + +// describe('command', function() +// { +// it('', function(done) +// { +// const item = +// { +// command:, +// args: [], +// stdio:, +// redirects: [], +// env: {} +// } +// +// command(item, function(command) +// { +// +// }) +// }) +// }) + +// if('inception', function() +// { +// +// }) From 3afdbb234caf6ec1607ada4d484d226716d6ce42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s-Combarro=20=27piranna?= Date: Sun, 7 May 2017 21:35:14 +0200 Subject: [PATCH 79/94] Fixed interactive Node.js REPL --- lib/ast2js/_execCommands.js | 91 +++++++------------------------------ lib/ast2js/_redirects.js | 6 +-- lib/ast2js/command.js | 47 ++++++++++--------- lib/index.js | 37 +++++++++------ server.js | 4 +- 5 files changed, 69 insertions(+), 116 deletions(-) diff --git a/lib/ast2js/_execCommands.js b/lib/ast2js/_execCommands.js index e26092f..20ef700 100644 --- a/lib/ast2js/_execCommands.js +++ b/lib/ast2js/_execCommands.js @@ -1,93 +1,25 @@ -var tty = require('tty') -var Writable = require('stream').Writable - -var eachSeries = require('async').eachSeries +const eachSeries = require('async/eachSeries') module.exports = execCommands -var ast2js = require('./index') -var environment = require('./_environment') +const ast2js = require('./index') +const environment = require('./_environment') // Always calculate dynamic `$PATH` based on the original one -var npmPath = require('npm-path').bind(null, {env:{PATH:process.env.PATH}}) - -var input - - -function noop(){} - -function setStatus(code, signal) -{ - environment['?'] = code - environment['??'] = signal -} - -function restoreStdio(stdin, input, stderr, command_stderr) -{ - if(stdin) - { - stdin.end() // Flush buffered data so we don'l loose any character - stdin.unpipe(this) - - input.resume() - } - if(stderr) command_stderr.unpipe(stderr) -} - -function connectStdio(command, callback) -{ - if(command.writable) - { - var stdin = tty.ReadStream(input.fd) +const npmPath = require('npm-path').bind(null, {env:{PATH:process.env.PATH}}) - input.pause() - stdin.pipe(command) - } - var command_stderr = command.stderr - if(command_stderr) - { - var stderr = tty.WriteStream(process.stderr.fd) - - command_stderr.pipe(stderr) - } - - command.on('error', function(error) - { - if(error.code !== 'ENOENT') throw error - - restoreStdio(stdin, input, stderr, command_stderr) - - callback(error.path+': not found') - }) - .once('end', function() - { - restoreStdio(stdin, input, stderr, command_stderr) - - callback() - }) - .once('exit', setStatus) -} - - -function execCommands(rl, commands, callback) +function execCommands(stdio, commands, callback) { - input = rl.input || input - eachSeries(commands, function(command, callback) { // `$PATH` is dynamic based on current directory and any command could // change it, so we update it previously to exec any of them npmPath() - command.output = rl.output - - // [Hack] Force Node.js to exec interactively - // https://github.com/nodejs/node/issues/5574 - if(command.command.value === 'node' && !command.args.length) - command.args = [{type: 'literal', value: '-i'}] + command.stdio = stdio ast2js(command, function(error, command) { @@ -95,7 +27,16 @@ function execCommands(rl, commands, callback) if(command == null) return callback() - connectStdio(command, callback) + command.once('error', callback) + .once('exit', function(code, signal) + { + environment['?'] = code + environment['??'] = signal + + this.removeListener('error', callback) + + callback(code || signal) + }) }) }, callback) diff --git a/lib/ast2js/_redirects.js b/lib/ast2js/_redirects.js index 2d150a4..fe4efd9 100644 --- a/lib/ast2js/_redirects.js +++ b/lib/ast2js/_redirects.js @@ -62,11 +62,11 @@ function iterator(stdio, redirect, callback) } -function redirects(output, array, callback) +function redirects(stdio, array, callback) { - array.filter(filterPipes).forEach(setOutput, output) + array.filter(filterPipes).forEach(setOutput, stdio.stdout) - reduce(array, ['pipe', output, 'pipe'], iterator, callback) + reduce(array, stdio.slice(), iterator, callback) } diff --git a/lib/ast2js/command.js b/lib/ast2js/command.js index 0a9f688..afc6aee 100644 --- a/lib/ast2js/command.js +++ b/lib/ast2js/command.js @@ -37,31 +37,35 @@ function wrapStdio(command, argv, options) // [ToDo] Close `stderr` when command finish // Run the builtin command - d.run(function() - { - command = command.call(options.env, argv) - }) + command = d.run(command.bind(options.env), argv) - - // [ToDo] stdin === 'ignore' + // TODO stdin === 'ignore' if(stdin !== 'pipe') - stdin.pipe(command) - else + { + if(command.writeable) + stdin.pipe(command) + stdin = null + } - // [ToDo] stdout === 'ignore' + // TODO stdout === 'ignore' if(stdout !== 'pipe') { - if(stdout.objectMode) - command.pipe(stdout) - else - command.pipe(new ToStringStream()).pipe(stdout) - } - else + if(command.readable) + { + if(stdout.objectMode) + command.pipe(stdout) + else + command.pipe(new ToStringStream()).pipe(stdout) + } + stdout = null + } var result + // TODO Check exactly what values can be `stdin` and `stdout`, probably we + // have here a lot of garbage code if(stdin && stdout) { result = Duplex() @@ -79,26 +83,25 @@ function wrapStdio(command, argv, options) { result = stdin - var event = command.readable ? 'end' : 'finish' - command.on(event, result.emit.bind(result, 'end')) + command.once('finish', result.emit.bind(result, 'end')) } else if(stdout) { result = command - var event = stdout.readable ? 'end' : 'finish' - stdout.on(event, result.emit.bind(result, 'end')) + stdout.once('end', result.emit.bind(result, 'end')) } else result = command - result.once('end', result.emit.bind(result, 'exit', 0, null)) - // Expose `stderr` so it can be used later. if(stderr !== stdio[2]) result.stderr = stderr + // Emulate process events + result.once('end', result.emit.bind(result, 'exit', 0, null)) + return result } @@ -119,7 +122,7 @@ function command(item, callback) argv = flatten(argv) // Redirects - redirects(item.output, item.redirects, function(error, stdio) + redirects(item.stdio, item.redirects, function(error, stdio) { if(error) return callback(error) diff --git a/lib/index.js b/lib/index.js index c7ac6bf..a5cf538 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,12 +1,12 @@ -var inherits = require('util').inherits -var Interface = require('readline').Interface +const inherits = require('util').inherits +const Interface = require('readline').Interface -var decode = require('decode-prompt') -var parse = require('shell-parse') +const decode = require('decode-prompt') +const parse = require('shell-parse') -var completer = require('./completer') -var environment = require('./ast2js/_environment') -var execCommands = require('./ast2js/_execCommands') +const _completer = require('./completer') +const environment = require('./ast2js/_environment') +const execCommands = require('./ast2js/_execCommands') function onError(error) @@ -17,11 +17,15 @@ function onError(error) } -function Nsh(input, output) +function Nsh(stdio, completer, terminal) { - if(!(this instanceof Nsh)) return new Nsh(input, output) + if(!(this instanceof Nsh)) return new Nsh(stdio, completer, terminal) - Nsh.super_.call(this, input, output, completer) + const stdin = stdio[0] + + Nsh.super_.call(this, stdin, stdio[1], + (completer || completer === false) ? completer : _completer, + terminal) var self = this @@ -30,6 +34,9 @@ function Nsh(input, output) function execCommandsCallback(error) { + if(stdin.setRawMode) stdin.setRawMode(true) + stdin.resume() + if(error) console.error(error) self.prompt() @@ -63,7 +70,10 @@ function Nsh(input, output) return this.prompt(true) } - execCommands(this, commands, execCommandsCallback) + if(stdin.setRawMode) stdin.setRawMode(false) + stdin.pause() + + execCommands(stdio, commands, execCommandsCallback) }) @@ -71,8 +81,6 @@ function Nsh(input, output) // Public API // - var prompt = this.prompt.bind(this) - /** * */ @@ -90,7 +98,8 @@ function Nsh(input, output) this.setPrompt(decode(ps, {env: environment})) - prompt() + this.clearLine() // TODO Is this needed for builtins? We should remove it + Interface.prototype.prompt.call(this) } diff --git a/server.js b/server.js index c08cfd8..ce63595 100755 --- a/server.js +++ b/server.js @@ -1,9 +1,9 @@ #!/usr/bin/env node -var Nsh = require('./lib') +var Nsh = require('.') -Nsh(process.stdin, process.stdout) +Nsh([process.stdin, process.stdout, process.stderr]) .on('SIGINT', function() { this.write('^C') From 3e29cf9357258720e2413d4603dc812757bc394a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s-Combarro=20=27piranna?= Date: Sun, 7 May 2017 21:36:15 +0200 Subject: [PATCH 80/94] Use `coreutils.js` module by default --- lib/builtins.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/builtins.js b/lib/builtins.js index c411283..2a77a3a 100644 --- a/lib/builtins.js +++ b/lib/builtins.js @@ -3,11 +3,17 @@ const coreutils = require('coreutils.js') const environment = require('./ast2js/_environment') +// `cd` is a special case, since it needs to change the shell environment, +// that's why we overwrite it + +const coreutils_cd = coreutils.cd + function cd(argv) { - // `cd` is a special case, since it needs to change the shell environment - return coreutils.cd(argv, environment) + return coreutils_cd(argv, environment) } +coreutils.cd = cd + -exports.cd = cd +module.exports = coreutils From 570b42e4e8a3b1056c2261dab645af630f2f6f3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s-Combarro=20=27piranna?= Date: Sun, 7 May 2017 21:36:58 +0200 Subject: [PATCH 81/94] Use `once` for finishing `spawn` events --- lib/ast2js/_spawnStream.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/ast2js/_spawnStream.js b/lib/ast2js/_spawnStream.js index f921042..7da5101 100644 --- a/lib/ast2js/_spawnStream.js +++ b/lib/ast2js/_spawnStream.js @@ -102,7 +102,7 @@ function spawnStream(command, argv, options) // Only `stdin` is open, ensure is always 'only' `Writable` else if(stdin) result = Writable() - // Both `stdin` and `stdout` are clossed + // Both `stdin` and `stdout` are clossed, or already redirected on `spawn` else result = new EventEmitter() // Connect stdio streams @@ -137,8 +137,8 @@ function spawnStream(command, argv, options) } // Propagate process events - cp.on('error', result.emit.bind(result, 'error')) - cp.on('exit' , result.emit.bind(result, 'exit' )) + cp.once('error', result.emit.bind(result, 'error')) + cp.once('exit' , result.emit.bind(result, 'exit' )) return result } From 8322225f7cdcdcaf9f2c089b3912ba7375d9f61d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s-Combarro=20=27piranna?= Date: Sun, 7 May 2017 21:37:09 +0200 Subject: [PATCH 82/94] Updated dependencies --- package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index c6953be..6b5783d 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,8 @@ "test": "mocha" }, "dependencies": { - "array-flatten": "^2.1.0", - "async": "^2.1.2", + "array-flatten": "^2.1.1", + "async": "^2.4.0", "coreutils.js": "github:piranna/coreutils.js", "decode-prompt": "^0.0.2", "glob": "~7.1.1", @@ -25,15 +25,15 @@ "lib-pathsearch": "piranna/node-lib-pathsearch", "mkdirp": "~0.5.1", "mkfifo": "^1.2.5", - "npm-path": "^2.0.2", + "npm-path": "^2.0.3", "shell-parse": "^0.0.2", "to-string-stream": "^0.1.0", - "uuid": "^3.0.0" + "uuid": "^3.0.1" }, "devDependencies": { - "concat-stream": "^1.5.2", + "concat-stream": "^1.6.0", "easy-coveralls": "0.0.1", - "mocha": "^3.1.2", + "mocha": "^3.3.0", "string-to-stream": "^1.1.0", "tmp": "0.0.31" } From e52e60aad814e0bab651902ec9f451f663037ab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s-Combarro=20=27piranna?= Date: Mon, 8 May 2017 14:47:12 +0200 Subject: [PATCH 83/94] `eval` function & `-c` argument --- lib/index.js | 18 ++++++++++++++++++ server.js | 25 +++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/lib/index.js b/lib/index.js index a5cf538..836b390 100644 --- a/lib/index.js +++ b/lib/index.js @@ -109,4 +109,22 @@ function Nsh(stdio, completer, terminal) inherits(Nsh, Interface) +Nsh.eval = function(stdio, line, callback) +{ + console.log(line) + try + { + var commands = parse(line) + } + catch(error) + { + if(error.constructor !== parse.SyntaxError) return callback(error) + + callback(new SyntaxError('end of file unexpected')) + } + + execCommands(stdio, commands, callback) +} + + module.exports = Nsh diff --git a/server.js b/server.js index ce63595..236a6b4 100755 --- a/server.js +++ b/server.js @@ -1,9 +1,30 @@ #!/usr/bin/env node -var Nsh = require('.') +const Nsh = require('.') -Nsh([process.stdin, process.stdout, process.stderr]) +function onerror(error) +{ + console.error(process.argv0+': '+error) + process.exit(2) +} + + +const stdio = [process.stdin, process.stdout, process.stderr] + +const argv = process.argv.slice(2) +if(argv.shift() === '-c') +{ + if(!argv.length) return onerror('-c requires an argument') + + return Nsh.eval(stdio, argv.join(' '), function(error) + { + if(error) onerror(error) + }) +} + + +Nsh(stdio) .on('SIGINT', function() { this.write('^C') From d3ad5f69d81214f17e7f2c9a4365e5ca10de9508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s-Combarro=20=27piranna?= Date: Mon, 8 May 2017 14:47:54 +0200 Subject: [PATCH 84/94] Add `sh` symlink --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 6b5783d..e49e124 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "license": "MIT", "main": "lib", "bin": { - "nsh": "server.js" + "nsh": "server.js", + "sh": "server.js" }, "scripts": { "coveralls": "easy-coveralls", From 15ac012288876054c66c612db851fc6e1ea554de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s-Combarro=20=27piranna?= Date: Mon, 8 May 2017 14:48:03 +0200 Subject: [PATCH 85/94] 0.5.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e49e124..ab4cf31 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bin-nsh", - "version": "0.5.0", + "version": "0.5.1", "description": "Node SHell", "author": "Jacob Groundwater ", "contributors": [ From ee4ef2f27f768fc8822b9921506d487398025fa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s-Combarro=20=27piranna?= Date: Mon, 15 May 2017 01:23:52 +0200 Subject: [PATCH 86/94] Hack for NodeOS on Docker --- lib/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/index.js b/lib/index.js index a5cf538..33f439f 100644 --- a/lib/index.js +++ b/lib/index.js @@ -98,7 +98,10 @@ function Nsh(stdio, completer, terminal) this.setPrompt(decode(ps, {env: environment})) - this.clearLine() // TODO Is this needed for builtins? We should remove it + // HACK Are these ones needed for builtins? We should remove them + this.line = '' + this.clearLine() + Interface.prototype.prompt.call(this) } From aa0019b139f22c9bc64e1699844092298c31b93e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s-Combarro=20=27piranna?= Date: Mon, 15 May 2017 01:24:19 +0200 Subject: [PATCH 87/94] Add alias for `sh` command --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 6b5783d..e49e124 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "license": "MIT", "main": "lib", "bin": { - "nsh": "server.js" + "nsh": "server.js", + "sh": "server.js" }, "scripts": { "coveralls": "easy-coveralls", From 64328207f684bd879501095a490a637047b34320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s-Combarro=20=27piranna?= Date: Mon, 15 May 2017 01:24:29 +0200 Subject: [PATCH 88/94] Updated `mocha` dependency --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e49e124..6e06707 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "devDependencies": { "concat-stream": "^1.6.0", "easy-coveralls": "0.0.1", - "mocha": "^3.3.0", + "mocha": "^3.4.1", "string-to-stream": "^1.1.0", "tmp": "0.0.31" } From a764eca46182a8ee6a85a6ff2bfe8ca80afd00b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s-Combarro=20=27piranna?= Date: Mon, 15 May 2017 01:24:39 +0200 Subject: [PATCH 89/94] 0.5.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6e06707..2182f18 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bin-nsh", - "version": "0.5.0", + "version": "0.5.1", "description": "Node SHell", "author": "Jacob Groundwater ", "contributors": [ From 34162143e6456cdf685984a40ab7c836223c26de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s-Combarro=20=27piranna?= Date: Mon, 15 May 2017 19:28:27 +0200 Subject: [PATCH 90/94] Removed `sh` symlink due to conflicts with dependencies during install It's better to do the symlink manually later, so also gives the option to use a different shell --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 2182f18..447eb60 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,7 @@ "license": "MIT", "main": "lib", "bin": { - "nsh": "server.js", - "sh": "server.js" + "nsh": "server.js" }, "scripts": { "coveralls": "easy-coveralls", From 49ce3995cf1444c19a47de77b94730c7bb24b01d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s-Combarro=20=27piranna?= Date: Tue, 16 May 2017 00:57:24 +0200 Subject: [PATCH 91/94] `-s` flag & execution of script files --- package.json | 1 + server.js | 65 +++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 58 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 447eb60..987e935 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "dependencies": { "array-flatten": "^2.1.1", "async": "^2.4.0", + "concat-stream": "^1.6.0", "coreutils.js": "github:piranna/coreutils.js", "decode-prompt": "^0.0.2", "glob": "~7.1.1", diff --git a/server.js b/server.js index 236a6b4..58dce64 100755 --- a/server.js +++ b/server.js @@ -1,8 +1,26 @@ #!/usr/bin/env node +const readFile = require('fs').readFile + +const concat = require('concat-stream') + const Nsh = require('.') +const stdio = [process.stdin, process.stdout, process.stderr] + + +function eval(data) +{ + // Re-adjust arguments + process.argv = process.argv.concat(argv) + + Nsh.eval(stdio, data, function(error) + { + if(error) onerror(error) + }) +} + function onerror(error) { console.error(process.argv0+': '+error) @@ -10,20 +28,51 @@ function onerror(error) } -const stdio = [process.stdin, process.stdout, process.stderr] - const argv = process.argv.slice(2) -if(argv.shift() === '-c') +process.argv = [process.argv[1]] + +switch(argv[0]) { - if(!argv.length) return onerror('-c requires an argument') + case '-c': + argv.shift() - return Nsh.eval(stdio, argv.join(' '), function(error) - { - if(error) onerror(error) - }) + const command_string = argv.shift() + if(!command_string) return onerror('-c requires an argument') + + const command_name = argv.shift() + if(command_name) process.argv[0] = command_name + + return eval(command_string) + + case '-s': + argv.shift() + + return process.stdin.pipe(concat(function(data) + { + eval(data.toString()) + })) + .on('error', onerror) + break + + default: + const command_file = argv.shift() + if(command_file) + return readFile(command_file, function(error, data) + { + if(error) return onerror(error) + + (data) + }) } +// +// Start an interactive shell +// + +// Re-adjust arguments +process.argv = process.argv.concat(argv) + Nsh(stdio) .on('SIGINT', function() { From fea04d4d1d79df3e121c6305d0b63a75cd976ae9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s-Combarro=20=27piranna?= Date: Tue, 16 May 2017 01:02:08 +0200 Subject: [PATCH 92/94] Use async version of `mkfifo` function --- lib/ast2js/processSubstitution.js | 65 ++++++++++++++----------------- 1 file changed, 30 insertions(+), 35 deletions(-) diff --git a/lib/ast2js/processSubstitution.js b/lib/ast2js/processSubstitution.js index 0fcd213..a4e6628 100644 --- a/lib/ast2js/processSubstitution.js +++ b/lib/ast2js/processSubstitution.js @@ -1,8 +1,8 @@ const fs = require('fs') const tmpdir = require('os').tmpdir -const mkfifoSync = require('mkfifo').mkfifoSync -const uuid = require('uuid').v4 +const mkfifo = require('mkfifo').mkfifo +const uuid = require('uuid').v4 const environment = require('./_environment') const execCommands = require('./_execCommands') @@ -12,50 +12,45 @@ function processSubstitution(item, callback) { const path = tmpdir()+'/'+uuid() - try + mkfifo(path, 0600, function(error) { - mkfifoSync(path, 0600); - } - catch(e) - { - return callback(e) - } - + if(error) return callback(error) - // Protect environment variables - environment.push() + // Protect environment variables + environment.push() - function onExecuted(error) - { - stream.close() + function onExecuted(error) + { + stream.close() - // Restore environment variables - environment.pop() + // Restore environment variables + environment.pop() - // Restore (possible) changed current dir - process.chdir(environment['PWD']) + // Restore (possible) changed current dir + process.chdir(environment['PWD']) - if(error) console.trace(error) - } + if(error) console.trace(error) + } - if(item.readWrite === '<') - var stream = fs.createWriteStream(path) - .on('open', function() - { - execCommands({output: this}, item.commands, onExecuted) - }) + if(item.readWrite === '<') + var stream = fs.createWriteStream(path) + .on('open', function() + { + execCommands({output: this}, item.commands, onExecuted) + }) - else - var stream = fs.createReadStream(path) - .on('open', function() - { - execCommands({input: this}, item.commands, onExecuted) - }) + else + var stream = fs.createReadStream(path) + .on('open', function() + { + execCommands({input: this}, item.commands, onExecuted) + }) - stream.on('close', fs.unlink.bind(null, path)) + stream.on('close', fs.unlink.bind(null, path)) - callback(null, path) + callback(null, path) + }) } From 2c17358e838251ddcda4136845140e396377e753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s-Combarro=20=27piranna?= Date: Tue, 16 May 2017 19:42:29 +0200 Subject: [PATCH 93/94] login shell --- lib/index.js | 5 +- server.js | 132 +++++++++++++++++++++++++++++++++++---------------- 2 files changed, 91 insertions(+), 46 deletions(-) diff --git a/lib/index.js b/lib/index.js index b1233ec..027930e 100644 --- a/lib/index.js +++ b/lib/index.js @@ -114,16 +114,13 @@ inherits(Nsh, Interface) Nsh.eval = function(stdio, line, callback) { - console.log(line) try { var commands = parse(line) } catch(error) { - if(error.constructor !== parse.SyntaxError) return callback(error) - - callback(new SyntaxError('end of file unexpected')) + return callback(error) } execCommands(stdio, commands, callback) diff --git a/server.js b/server.js index 58dce64..b8fa2e1 100755 --- a/server.js +++ b/server.js @@ -2,12 +2,14 @@ const readFile = require('fs').readFile -const concat = require('concat-stream') +const concat = require('concat-stream') +const eachSeries = require('async/eachSeries') const Nsh = require('.') -const stdio = [process.stdin, process.stdout, process.stderr] +const PROFILE_FILES = ['/etc/profile', process.env.HOME+'/.profile'] +const stdio = [process.stdin, process.stdout, process.stderr] function eval(data) @@ -21,63 +23,109 @@ function eval(data) }) } -function onerror(error) +function interactiveShell() { - console.error(process.argv0+': '+error) - process.exit(2) + Nsh(stdio) + .on('SIGINT', function() + { + this.write('^C') + this.clearLine() + + this.prompt() + }) } +function loadCommands() +{ + switch(argv[0]) + { + case '-c': + argv.shift() -const argv = process.argv.slice(2) -process.argv = [process.argv[1]] + const command_string = argv.shift() + if(!command_string) return onerror('-c requires an argument') -switch(argv[0]) -{ - case '-c': - argv.shift() + const command_name = argv.shift() + if(command_name) process.argv[0] = command_name + + return eval(command_string) + + case '-s': + argv.shift() + + return process.stdin.pipe(concat(function(data) + { + eval(data.toString()) + })) + .on('error', onerror) + break - const command_string = argv.shift() - if(!command_string) return onerror('-c requires an argument') + default: + const command_file = argv.shift() + if(command_file) + return readFile(command_file, 'utf-8', function(error, data) + { + if(error) return onerror(error) - const command_name = argv.shift() - if(command_name) process.argv[0] = command_name + eval(data) + }) + } - return eval(command_string) - case '-s': - argv.shift() + // + // Start an interactive shell + // - return process.stdin.pipe(concat(function(data) + // Re-adjust arguments + process.argv = process.argv.concat(argv) + + const ENV = process.env.ENV + if(!ENV) return interactiveShell() + + readFile(ENV, 'utf-8', function(error, data) + { + if(error) return onerror(error) + + Nsh.eval(stdio, data, function(error) { - eval(data.toString()) - })) - .on('error', onerror) - break - - default: - const command_file = argv.shift() - if(command_file) - return readFile(command_file, function(error, data) - { - if(error) return onerror(error) + if(error) return onerror(error) - (data) - }) + interactiveShell() + }) + }) } +function onerror(error) +{ + console.error(process.argv0+': '+error) + process.exit(2) +} -// -// Start an interactive shell -// -// Re-adjust arguments -process.argv = process.argv.concat(argv) +// Get arguments -Nsh(stdio) -.on('SIGINT', function() +const argv = process.argv.slice(2) +process.argv = [process.argv[1]] + +if(argv[0] != '-l') return loadCommands() + + +// Login shell + +argv.shift() + +eachSeries(PROFILE_FILES, function(file, callback) +{ + readFile(file, 'utf-8', function(error, data) + { + if(error) return callback(error.code !== 'ENOENT' ? error : null) + + Nsh.eval(stdio, data, callback) + }) +}, +function(error) { - this.write('^C') - this.clearLine() + if(error) return onerror(error) - this.prompt() + loadCommands() }) From 4032445e63b817350e72c165327a1b9a46decad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s-Combarro=20=27piranna?= Date: Tue, 16 May 2017 19:42:48 +0200 Subject: [PATCH 94/94] 0.5.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 987e935..6928bb4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bin-nsh", - "version": "0.5.1", + "version": "0.5.2", "description": "Node SHell", "author": "Jacob Groundwater ", "contributors": [