├── .gitignore ├── .travis.yml ├── README.md ├── lib ├── ast2js │ ├── _environment.js │ ├── _execCommands.js │ ├── _redirects.js │ ├── _spawnStream.js │ ├── command.js │ ├── commandSubstitution.js │ ├── glob.js │ ├── ifElse.js │ ├── index.js │ ├── literal.js │ ├── pipe.js │ ├── processSubstitution.js │ ├── redirectFd.js │ ├── until-loop.js │ ├── variable.js │ ├── variableAssignment.js │ ├── variableSubstitution.js │ └── while-loop.js ├── builtins.js ├── completer.js └── index.js ├── package.json ├── server.js └── test ├── fixture.txt └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "6.1" 5 | - "5.11" 6 | - "iojs" 7 | env: 8 | - CXX=g++-4.8 9 | addons: 10 | apt: 11 | sources: 12 | - ubuntu-toolchain-r-test 13 | packages: 14 | - g++-4.8 15 | after_script: 16 | - npm run coveralls 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/piranna/nsh.svg?branch=master)](https://travis-ci.org/piranna/nsh) 2 | [![Coverage Status](https://coveralls.io/repos/github/piranna/nsh/badge.svg?branch=master)](https://coveralls.io/github/piranna/nsh?branch=master) 3 | 4 | # Node SHell 5 | 6 | [![Built for NodeOS](http://i.imgur.com/pIJu2TS.png)](http://nodeos.github.io) 7 | 8 | `nsh` is a basic POSIX compliant shell that will run without having `bash` or 9 | another process tidy things up first. It's also `require()`able and embedable on 10 | other projects like [blesh](https://github.com/piranna/blesh), and has a 11 | collection of basic commands as build-in functions running on the same shell 12 | process powered by [Coreutils.js](https://github.com/piranna/Coreutils.js). 13 | -------------------------------------------------------------------------------- /lib/ast2js/_environment.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var environment = 4 | { 5 | '?': 0, 6 | PS1: '\\w > ', 7 | PS2: '> ', 8 | PS4: '+ ' 9 | } 10 | 11 | 12 | // 13 | // Regular access 14 | // 15 | 16 | const handler = 17 | { 18 | ownKeys: function() 19 | { 20 | return Object.keys(environment) 21 | }, 22 | 23 | get: function(_, prop) 24 | { 25 | var value = environment[prop] 26 | 27 | if(value === undefined) value = process.env[prop] 28 | if(value == null) value = '' 29 | 30 | return value 31 | }, 32 | 33 | set: function(_, prop, value) 34 | { 35 | if(value === undefined) return this.deleteProperty(_, prop) 36 | 37 | // Exported environment variable 38 | const env = process.env 39 | if(env[prop] !== undefined) 40 | env[prop] = value 41 | 42 | // Local environment variable 43 | else 44 | environment[prop] = value 45 | 46 | return true 47 | }, 48 | 49 | deleteProperty: function(_, prop) 50 | { 51 | // Local environment variable 52 | var value = environment[prop] 53 | if(value !== undefined) 54 | delete environment[prop] 55 | 56 | // Exported environment variable 57 | else 58 | delete process.env[prop] 59 | 60 | return true 61 | } 62 | } 63 | 64 | 65 | exports = new Proxy({}, handler) 66 | 67 | 68 | // 69 | // Stacked environment variables 70 | // 71 | 72 | exports.push = function() 73 | { 74 | process.env = {__proto__: process.env} 75 | environment = {__proto__: environment} 76 | } 77 | 78 | exports.pop = function() 79 | { 80 | process.env = process.env.__proto__ 81 | environment = environment.__proto__ 82 | } 83 | 84 | 85 | module.exports = exports 86 | -------------------------------------------------------------------------------- /lib/ast2js/_execCommands.js: -------------------------------------------------------------------------------- 1 | const eachSeries = require('async/eachSeries') 2 | 3 | 4 | module.exports = execCommands 5 | 6 | const ast2js = require('./index') 7 | const environment = require('./_environment') 8 | 9 | 10 | // Always calculate dynamic `$PATH` based on the original one 11 | const npmPath = require('npm-path').bind(null, {env:{PATH:process.env.PATH}}) 12 | 13 | 14 | function execCommands(stdio, commands, callback) 15 | { 16 | eachSeries(commands, function(command, callback) 17 | { 18 | // `$PATH` is dynamic based on current directory and any command could 19 | // change it, so we update it previously to exec any of them 20 | npmPath() 21 | 22 | command.stdio = stdio 23 | 24 | ast2js(command, function(error, command) 25 | { 26 | if(error) return callback(error) 27 | 28 | if(command == null) return callback() 29 | 30 | command.once('error', callback) 31 | .once('exit', function(code, signal) 32 | { 33 | environment['?'] = code 34 | environment['??'] = signal 35 | 36 | this.removeListener('error', callback) 37 | 38 | callback(code || signal) 39 | }) 40 | }) 41 | }, 42 | callback) 43 | } 44 | -------------------------------------------------------------------------------- /lib/ast2js/_redirects.js: -------------------------------------------------------------------------------- 1 | var reduce = require('async').reduce 2 | 3 | var ast2js = require('./index') 4 | 5 | 6 | function filterPipes(item) 7 | { 8 | return item.type === 'pipe' 9 | } 10 | 11 | function getSrcFd(stdio, fd) 12 | { 13 | var result = stdio[fd] 14 | 15 | if(typeof result === 'string') return fd 16 | 17 | return result 18 | } 19 | 20 | function setOutput(item) 21 | { 22 | item.command.output = this 23 | } 24 | 25 | 26 | function iterator(stdio, redirect, callback) 27 | { 28 | ast2js(redirect, function(error, value) 29 | { 30 | if(error) return callback(error) 31 | 32 | var type = redirect.type 33 | switch(type) 34 | { 35 | case 'duplicateFd': 36 | stdio[redirect.destFd] = getSrcFd(stdio, redirect.srcFd) 37 | break; 38 | 39 | case 'moveFd': 40 | stdio[redirect.dest] = getSrcFd(stdio, redirect.fd) 41 | stdio[redirect.fd] = 'ignore' 42 | break; 43 | 44 | case 'pipe': 45 | stdio[1] = value 46 | break; 47 | 48 | case 'redirectFd': 49 | stdio[redirect.fd] = value 50 | 51 | if(redirect.op === '&>' 52 | || redirect.op === '&>>') 53 | stdio[2] = value 54 | break; 55 | 56 | default: 57 | return callback('Unknown redirect type "'+type+'"') 58 | } 59 | 60 | callback(null, stdio) 61 | }) 62 | } 63 | 64 | 65 | function redirects(stdio, array, callback) 66 | { 67 | array.filter(filterPipes).forEach(setOutput, stdio.stdout) 68 | 69 | reduce(array, stdio.slice(), iterator, callback) 70 | } 71 | 72 | 73 | module.exports = redirects 74 | -------------------------------------------------------------------------------- /lib/ast2js/_spawnStream.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events') 2 | const spawn = require('child_process').spawn 3 | const stream = require('stream') 4 | 5 | const Duplex = stream.Duplex 6 | const Readable = stream.Readable 7 | const Writable = stream.Writable 8 | 9 | 10 | function noop(){} 11 | 12 | /** 13 | * Node.js `spawn` only accept streams with a file descriptor as stdio, so use 14 | * pipes instead and connect the given streams to them. 15 | */ 16 | function wrapStdio(command, argv, options) 17 | { 18 | argv = argv || [] 19 | options = options || {} 20 | 21 | var stdio = options.stdio 22 | if(!stdio) options.stdio = stdio = [] 23 | 24 | var stdin = stdio[0] 25 | var stdout = stdio[1] 26 | var stderr = stdio[2] 27 | 28 | // Wrap stdio 29 | if(typeof stdin === 'string' || typeof stdin === 'number' 30 | || stdin && stdin.constructor.name === 'ReadStream') 31 | stdin = null 32 | else 33 | stdio[0] = 'pipe' 34 | 35 | if(typeof stdout === 'string' || typeof stdout === 'number' 36 | || stdout && stdout.constructor.name === 'WriteStream') 37 | stdout = null 38 | else 39 | stdio[1] = 'pipe' 40 | 41 | if(typeof stderr === 'string' || typeof stderr === 'number' 42 | || stderr && stderr.constructor.name === 'WriteStream') 43 | stderr = null 44 | else 45 | stdio[2] = 'pipe' 46 | 47 | // Create child process 48 | var cp = spawn(command, argv, options) 49 | 50 | // Adjust events, pipe streams and restore stdio 51 | if(stdin != null) 52 | { 53 | stdin.pipe(cp.stdin) 54 | cp.stdin = null 55 | } 56 | if(stdout != null) 57 | { 58 | cp.stdout.pipe(stdout) 59 | cp.stdout = null 60 | } 61 | if(stderr != null) 62 | { 63 | cp.stderr.pipe(stderr) 64 | cp.stderr = null 65 | } 66 | 67 | // Return child process 68 | return cp 69 | } 70 | 71 | 72 | function spawnStream(command, argv, options) 73 | { 74 | if(argv && argv.constructor.name === 'Object') 75 | { 76 | options = argv 77 | argv = undefined 78 | } 79 | 80 | options = options || {} 81 | var stdio = options.stdio || [] 82 | 83 | var stdin = stdio[0] 84 | var stdout = stdio[1] 85 | var stderr = stdio[2] 86 | 87 | var cp = wrapStdio(command, argv, options) 88 | 89 | stdin = (stdin == null || stdin === 'pipe') ? cp.stdin : null 90 | stdout = (stdout == null || stdout === 'pipe') ? cp.stdout : null 91 | stderr = (stderr == null || stderr === 'pipe') ? cp.stderr : null 92 | 93 | var result 94 | 95 | // Both `stdin` and `stdout` are open, probably the normal case. Create a 96 | // `Duplex` object with them so command can be used as a filter 97 | if(stdin && stdout) result = Duplex() 98 | 99 | // Only `stdout` is open, use it directly 100 | else if(stdout) result = Readable() 101 | 102 | // Only `stdin` is open, ensure is always 'only' `Writable` 103 | else if(stdin) result = Writable() 104 | 105 | // Both `stdin` and `stdout` are clossed, or already redirected on `spawn` 106 | else result = new EventEmitter() 107 | 108 | // Connect stdio streams 109 | if(stdin) 110 | { 111 | result._write = stdin.write.bind(stdin) 112 | result.once('finish', stdin.end.bind(stdin)) 113 | } 114 | 115 | if(stdout) 116 | { 117 | result._read = noop 118 | stdout.on ('data', result.push.bind(result)) 119 | stdout.once('end' , result.push.bind(result, null)) 120 | } 121 | 122 | // Use child process `exit` event instead of missing `stdout` `end` event 123 | else 124 | cp.once('exit', result.emit.bind(result, 'end')) 125 | 126 | if(stderr) 127 | { 128 | // Expose `stderr` so it can be used later. 129 | result.stderr = stderr 130 | 131 | // Redirect `stderr` from piped command to our own `stderr`, since there's 132 | // no way to redirect it to `process.stderr` by default as it should be. 133 | // This way we can at least fetch the error messages someway instead of 134 | // lost them... 135 | var out_stderr = stdout && stdout.stderr 136 | if(out_stderr) out_stderr.pipe(stderr) 137 | } 138 | 139 | // Propagate process events 140 | cp.once('error', result.emit.bind(result, 'error')) 141 | cp.once('exit' , result.emit.bind(result, 'exit' )) 142 | 143 | return result 144 | } 145 | 146 | 147 | module.exports = spawnStream 148 | -------------------------------------------------------------------------------- /lib/ast2js/command.js: -------------------------------------------------------------------------------- 1 | const Domain = require('domain').Domain 2 | const Duplex = require('stream').Duplex 3 | const Readable = require('stream').Readable 4 | 5 | const flatten = require('array-flatten') 6 | const map = require('async').map 7 | const ToStringStream = require('to-string-stream') 8 | 9 | const ast2js = require('./index') 10 | const redirects = require('./_redirects') 11 | const spawnStream = require('./_spawnStream') 12 | 13 | const builtins = require('../builtins') 14 | 15 | 16 | function noop(){} 17 | 18 | function wrapStdio(command, argv, options) 19 | { 20 | var stdio = options.stdio 21 | 22 | var stdin = stdio[0] 23 | var stdout = stdio[1] 24 | var stderr = stdio[2] 25 | 26 | // Create a `stderr` stream for `error` events if none is defined 27 | if(stderr === 'pipe') 28 | { 29 | stderr = new Readable({objectMode: true}) 30 | stderr._read = noop 31 | } 32 | 33 | 34 | // Put `error` events on the `stderr` stream 35 | var d = new Domain() 36 | .on('error', stderr.push.bind(stderr)) 37 | // [ToDo] Close `stderr` when command finish 38 | 39 | // Run the builtin command 40 | command = d.run(command.bind(options.env), argv) 41 | 42 | // TODO stdin === 'ignore' 43 | if(stdin !== 'pipe') 44 | { 45 | if(command.writeable) 46 | stdin.pipe(command) 47 | 48 | stdin = null 49 | } 50 | 51 | // TODO stdout === 'ignore' 52 | if(stdout !== 'pipe') 53 | { 54 | if(command.readable) 55 | { 56 | if(stdout.objectMode) 57 | command.pipe(stdout) 58 | else 59 | command.pipe(new ToStringStream()).pipe(stdout) 60 | } 61 | 62 | stdout = null 63 | } 64 | 65 | var result 66 | 67 | // TODO Check exactly what values can be `stdin` and `stdout`, probably we 68 | // have here a lot of garbage code 69 | if(stdin && stdout) 70 | { 71 | result = Duplex() 72 | result._read = noop 73 | result._write = stdin.write.bind(stdin) 74 | 75 | result.on('finish', stdin.end.bind(stdin)) 76 | 77 | stdout 78 | .on('data', result.push.bind(result)) 79 | .on('end' , result.emit.bind(result, 'end')) 80 | } 81 | 82 | else if(stdin) 83 | { 84 | result = stdin 85 | 86 | command.once('finish', result.emit.bind(result, 'end')) 87 | } 88 | 89 | else if(stdout) 90 | { 91 | result = command 92 | 93 | stdout.once('end', result.emit.bind(result, 'end')) 94 | } 95 | 96 | else 97 | result = command 98 | 99 | // Expose `stderr` so it can be used later. 100 | if(stderr !== stdio[2]) result.stderr = stderr 101 | 102 | // Emulate process events 103 | result.once('end', result.emit.bind(result, 'exit', 0, null)) 104 | 105 | return result 106 | } 107 | 108 | 109 | function command(item, callback) 110 | { 111 | // Command 112 | ast2js(item.command, function(error, command) 113 | { 114 | if(error) return callback(error) 115 | 116 | // Arguments 117 | map(item.args, ast2js, function(error, argv) 118 | { 119 | if(error) return callback(error) 120 | 121 | // Globs return an array, flat it 122 | argv = flatten(argv) 123 | 124 | // Redirects 125 | redirects(item.stdio, item.redirects, function(error, stdio) 126 | { 127 | if(error) return callback(error) 128 | 129 | // Create command 130 | var env = item.env 131 | env.__proto__ = process.env 132 | 133 | var options = 134 | { 135 | env: env, 136 | stdio: stdio 137 | } 138 | 139 | // Builtins 140 | var builtin = builtins[command] 141 | if(builtin) return callback(null, wrapStdio(builtin, argv, options)) 142 | 143 | // External commands 144 | try 145 | { 146 | command = spawnStream(command, argv, options) 147 | } 148 | catch(error) 149 | { 150 | if(error.code === 'EACCES') error = command+': is a directory' 151 | 152 | return callback(error) 153 | } 154 | 155 | callback(null, command) 156 | }) 157 | }) 158 | }) 159 | } 160 | 161 | 162 | module.exports = command 163 | -------------------------------------------------------------------------------- /lib/ast2js/commandSubstitution.js: -------------------------------------------------------------------------------- 1 | var Writable = require('stream').Writable 2 | 3 | var environment = require('./_environment') 4 | var execCommands = require('./_execCommands') 5 | 6 | 7 | function commandSubstitution(item, callback) 8 | { 9 | var buffer = [] 10 | 11 | var output = new Writable() 12 | output._write = function(chunk, _, done) 13 | { 14 | buffer.push(chunk) 15 | done() 16 | } 17 | 18 | // Protect environment variables 19 | environment.push() 20 | 21 | execCommands({output: output}, item.commands, function(error) 22 | { 23 | // Restore environment variables 24 | environment.pop() 25 | 26 | // Restore (possible) changed current dir 27 | process.chdir(environment['PWD']) 28 | 29 | if(error) return callback(error) 30 | 31 | callback(null, buffer.join('')) 32 | }) 33 | } 34 | 35 | 36 | module.exports = commandSubstitution 37 | -------------------------------------------------------------------------------- /lib/ast2js/glob.js: -------------------------------------------------------------------------------- 1 | var globFunc = require('glob') 2 | 3 | 4 | function glob(item, callback) 5 | { 6 | globFunc(item.value, callback) 7 | } 8 | 9 | 10 | module.exports = glob 11 | -------------------------------------------------------------------------------- /lib/ast2js/ifElse.js: -------------------------------------------------------------------------------- 1 | var detectSeries = require('async').detectSeries 2 | 3 | var ast2js = require('./index') 4 | 5 | 6 | function ifElse(item, callback) 7 | { 8 | function runTest(item, callback2) 9 | { 10 | ast2js(item.test, function(error, result) 11 | { 12 | if(error) return callback(error) 13 | 14 | callback2(result) 15 | }) 16 | } 17 | 18 | function execBody(block) 19 | { 20 | if(block) return ast2js(block.body, callback) 21 | 22 | if(item.elseBody) return ast2js(item.elseBody, callback) 23 | 24 | callback() 25 | } 26 | 27 | 28 | runTest(item, function(value) 29 | { 30 | if(value) return ast2js(item.body, callback) 31 | 32 | detectSeries(item.elifBlocks || [], runTest, execBody) 33 | }) 34 | } 35 | 36 | 37 | module.exports = ifElse 38 | -------------------------------------------------------------------------------- /lib/ast2js/index.js: -------------------------------------------------------------------------------- 1 | function noop(item, callback) 2 | { 3 | callback() 4 | } 5 | 6 | 7 | function ast2js(item, callback) 8 | { 9 | ast2js[item.type](item, callback) 10 | } 11 | 12 | 13 | module.exports = ast2js 14 | 15 | 16 | ast2js.command = require('./command') 17 | ast2js.commandSubstitution = require('./commandSubstitution') 18 | ast2js.duplicateFd = noop // Processed on `_redirects` file 19 | ast2js.glob = require('./glob') 20 | ast2js.ifElse = require('./ifElse') 21 | ast2js.literal = require('./literal') 22 | ast2js.moveFd = noop // Processed on `_redirects` file 23 | ast2js.pipe = require('./pipe') 24 | ast2js.processSubstitution = require('./processSubstitution') 25 | ast2js.redirectFd = require('./redirectFd') 26 | ast2js['until-loop'] = require('./until-loop') 27 | ast2js.variable = require('./variable') 28 | ast2js.variableAssignment = require('./variableAssignment') 29 | ast2js.variableSubstitution = require('./variableSubstitution') 30 | ast2js['while-loop'] = require('./while-loop') 31 | -------------------------------------------------------------------------------- /lib/ast2js/literal.js: -------------------------------------------------------------------------------- 1 | function literal(item, callback) 2 | { 3 | callback(null, item.value) 4 | } 5 | 6 | 7 | module.exports = literal 8 | -------------------------------------------------------------------------------- /lib/ast2js/pipe.js: -------------------------------------------------------------------------------- 1 | var command = require('./command') 2 | 3 | 4 | function pipe(item, callback) 5 | { 6 | command(item.command, callback) 7 | } 8 | 9 | 10 | module.exports = pipe 11 | -------------------------------------------------------------------------------- /lib/ast2js/processSubstitution.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const tmpdir = require('os').tmpdir 3 | 4 | const mkfifo = require('mkfifo').mkfifo 5 | const uuid = require('uuid').v4 6 | 7 | const environment = require('./_environment') 8 | const execCommands = require('./_execCommands') 9 | 10 | 11 | function processSubstitution(item, callback) 12 | { 13 | const path = tmpdir()+'/'+uuid() 14 | 15 | mkfifo(path, 0600, function(error) 16 | { 17 | if(error) return callback(error) 18 | 19 | // Protect environment variables 20 | environment.push() 21 | 22 | function onExecuted(error) 23 | { 24 | stream.close() 25 | 26 | // Restore environment variables 27 | environment.pop() 28 | 29 | // Restore (possible) changed current dir 30 | process.chdir(environment['PWD']) 31 | 32 | if(error) console.trace(error) 33 | } 34 | 35 | 36 | if(item.readWrite === '<') 37 | var stream = fs.createWriteStream(path) 38 | .on('open', function() 39 | { 40 | execCommands({output: this}, item.commands, onExecuted) 41 | }) 42 | 43 | else 44 | var stream = fs.createReadStream(path) 45 | .on('open', function() 46 | { 47 | execCommands({input: this}, item.commands, onExecuted) 48 | }) 49 | 50 | stream.on('close', fs.unlink.bind(null, path)) 51 | 52 | callback(null, path) 53 | }) 54 | } 55 | 56 | 57 | module.exports = processSubstitution 58 | -------------------------------------------------------------------------------- /lib/ast2js/redirectFd.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | 3 | var ast2js = require('./index') 4 | 5 | 6 | function redirectFd(item, callback) 7 | { 8 | function onError(error) 9 | { 10 | this.removeListener('open', onOpen) 11 | 12 | callback(error) 13 | } 14 | 15 | function onOpen() 16 | { 17 | this.removeListener('error', onError) 18 | 19 | callback(null, this) 20 | } 21 | 22 | 23 | ast2js(item.filename, function(error, filename) 24 | { 25 | if(error) return callback(error) 26 | 27 | var result 28 | 29 | var op = item.op 30 | switch(op) 31 | { 32 | case '<': 33 | result = fs.createReadStream(filename) 34 | break 35 | 36 | case '>': 37 | case '&>': 38 | result = fs.createWriteStream(filename, {flags: 'wx'}) 39 | break 40 | 41 | case '>|': 42 | result = fs.createWriteStream(filename) 43 | break 44 | 45 | case '>>': 46 | case '&>>': 47 | result = fs.createWriteStream(filename, {flags: 'a'}) 48 | break 49 | 50 | default: 51 | return callback('Unknown redirectFd op "'+op+'"') 52 | } 53 | 54 | result.once('error', onError) 55 | result.once('open' , onOpen) 56 | }) 57 | } 58 | 59 | 60 | module.exports = redirectFd 61 | -------------------------------------------------------------------------------- /lib/ast2js/until-loop.js: -------------------------------------------------------------------------------- 1 | var during = require('async').during 2 | 3 | var ast2js = require('./index') 4 | 5 | 6 | function until_loop(item, callback) 7 | { 8 | function test(callback) 9 | { 10 | ast2js(item.test, function(error, value) 11 | { 12 | callback(error, !value) 13 | }) 14 | } 15 | 16 | during(test, 17 | ast2js.bind(null, item.body), 18 | callback) 19 | } 20 | 21 | 22 | module.exports = until_loop 23 | -------------------------------------------------------------------------------- /lib/ast2js/variable.js: -------------------------------------------------------------------------------- 1 | const environment = require('./_environment') 2 | 3 | 4 | function variable(item, callback) 5 | { 6 | callback(null, environment[item.name]) 7 | } 8 | 9 | 10 | module.exports = variable 11 | -------------------------------------------------------------------------------- /lib/ast2js/variableAssignment.js: -------------------------------------------------------------------------------- 1 | var ast2js = require('./index') 2 | var environment = require('./_environment') 3 | 4 | 5 | function variableAssignment(item, callback) 6 | { 7 | ast2js(item.value, function(error, value) 8 | { 9 | if(error) return callback(error) 10 | 11 | environment[item.name] = value 12 | 13 | callback() 14 | }) 15 | } 16 | 17 | 18 | module.exports = variableAssignment 19 | -------------------------------------------------------------------------------- /lib/ast2js/variableSubstitution.js: -------------------------------------------------------------------------------- 1 | const environment = require('./_environment') 2 | 3 | 4 | function variableSubstitution(item, callback) 5 | { 6 | callback(null, environment[item.expression]) 7 | } 8 | 9 | 10 | module.exports = variableSubstitution 11 | -------------------------------------------------------------------------------- /lib/ast2js/while-loop.js: -------------------------------------------------------------------------------- 1 | var during = require('async').during 2 | 3 | var ast2js = require('./index') 4 | 5 | 6 | function while_loop(item, callback) 7 | { 8 | during(ast2js.bind(null, item.test), 9 | ast2js.bind(null, item.body), 10 | callback) 11 | } 12 | 13 | 14 | module.exports = while_loop 15 | -------------------------------------------------------------------------------- /lib/builtins.js: -------------------------------------------------------------------------------- 1 | const coreutils = require('coreutils.js') 2 | 3 | const environment = require('./ast2js/_environment') 4 | 5 | 6 | // `cd` is a special case, since it needs to change the shell environment, 7 | // that's why we overwrite it 8 | 9 | const coreutils_cd = coreutils.cd 10 | 11 | function cd(argv) 12 | { 13 | return coreutils_cd(argv, environment) 14 | } 15 | 16 | coreutils.cd = cd 17 | 18 | 19 | module.exports = coreutils 20 | -------------------------------------------------------------------------------- /lib/completer.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | const pc = require('lib-pathcomplete') 4 | const ps = require('lib-pathsearch') 5 | const reduce = require('async/reduce') 6 | 7 | const builtins = require('./builtins') 8 | const environment = require('./ast2js/_environment') 9 | 10 | const constants = fs.constants 11 | const stat = fs.stat 12 | 13 | 14 | const EMPTY_ITEM = 1 15 | const EMPTY_ENV_VAR = 2 16 | 17 | 18 | // 19 | // Helper functions 20 | // 21 | 22 | function filterEnvVars(item) 23 | { 24 | return item && !item.includes('=') 25 | } 26 | 27 | function filterNames(name) 28 | { 29 | return name.substr(0, this.length) === this.toString() 30 | } 31 | 32 | function getEnvVars(item) 33 | { 34 | var vars = Object.getOwnPropertyNames(environment).filter(filterNames, item) 35 | var env = Object.keys(process.env) .filter(filterNames, item) 36 | 37 | if(vars.length && env.length) vars.push('') 38 | 39 | return vars.concat(env) 40 | } 41 | 42 | function isExecutable(stats) 43 | { 44 | const mode = stats.mode 45 | 46 | return mode & constants.S_IXUSR && stats.uid === process.getuid() 47 | || mode & constants.S_IXGRP && stats.gid === process.getgid() 48 | || mode & constants.S_IXOTH 49 | } 50 | 51 | function mapEnvVarsAsign(name) 52 | { 53 | if(name) name += '=' 54 | 55 | return name 56 | } 57 | 58 | function mapEnvVarsRef(name) 59 | { 60 | if(name) name = '$'+name 61 | 62 | return name 63 | } 64 | 65 | 66 | // 67 | // Completer functions 68 | // 69 | 70 | function envVar(item, callback) 71 | { 72 | const key = item.substr(1) 73 | 74 | if(!key) return callback(EMPTY_ENV_VAR) 75 | 76 | var result = getEnvVars(key).map(mapEnvVarsRef) 77 | 78 | // if there is only one environment variable, append a space after it 79 | if(result.length === 1) result[0] += ' ' 80 | 81 | callback(null, [result, item]) 82 | } 83 | 84 | function relativePath(item, is_arg, callback) 85 | { 86 | pc(item, function(err, arr, info) 87 | { 88 | if(err) return callback(err) 89 | 90 | // [Hack] Add `.` and `..` entries 91 | if(info.file === '.' ) arr.unshift('.', '..') 92 | if(info.file === '..') arr.unshift('..') 93 | 94 | reduce(arr, {}, function(memo, name, callback) 95 | { 96 | const path = info.dir + name 97 | 98 | stat(path, function(error, stats) 99 | { 100 | if(error) return callback(error) 101 | 102 | memo[path] = stats 103 | 104 | callback(null, memo) 105 | }) 106 | }, 107 | function(error, stats) 108 | { 109 | if(error) return callback(error) 110 | 111 | // user is typing the command, autocomplete it only against the 112 | // executables and directories in the current directory 113 | if(!is_arg) 114 | arr = arr.filter(function(name) 115 | { 116 | const stat = stats[info.dir + name] 117 | 118 | return stat.isDirectory() || isExecutable(stat) 119 | }) 120 | 121 | arr = arr.map(function(item) 122 | { 123 | // If completion is a directory, append a slash 124 | if(stats[info.dir + item].isDirectory()) item += '/' 125 | 126 | return item 127 | }) 128 | 129 | // There's just only one completion and it's not a directory, append it a 130 | // space 131 | if(arr.length === 1 && arr[0][arr[0].length-1] !== '/') arr[0] += ' ' 132 | 133 | callback(null, [arr, info.file]) 134 | }) 135 | }) 136 | } 137 | 138 | 139 | /** 140 | * auto-complete handler 141 | */ 142 | function completer(line, callback) 143 | { 144 | const split = line.split(/\s+/) 145 | const item = split.pop() 146 | const is_arg = split.filter(filterEnvVars).length 147 | 148 | // avoid crazy auto-completions when the item is empty 149 | if(!item && !is_arg) return callback(EMPTY_ITEM) 150 | 151 | // Environment variables 152 | if(item[0] === '$') return envVar(item, callback) 153 | 154 | // Relative paths and arguments 155 | if(item[0] === '.' || is_arg) return relativePath(item, is_arg, callback) 156 | 157 | // Commands & environment variables 158 | ps(item, environment.PATH.split(':'), function(err, execs) 159 | { 160 | if(err) return callback(err) 161 | 162 | // Builtins 163 | var names = Object.keys(builtins).filter(filterNames, item) 164 | 165 | // Environment variables 166 | var envVars = getEnvVars(item).map(mapEnvVarsAsign) 167 | 168 | // Current directory 169 | relativePath(item, true, function(err, entries) 170 | { 171 | if(err) return callback(err) 172 | 173 | entries = entries[0] 174 | 175 | // Compose result 176 | if(names.length && execs.length) names.push('') 177 | var result = names.concat(execs) 178 | 179 | if(result.length && envVars.length) result.push('') 180 | result = result.concat(envVars) 181 | 182 | if(result.length && entries.length) result.push('') 183 | result = result.concat(entries) 184 | 185 | // if there is only one executable, append a space after it 186 | const result0 = result[0] 187 | const type = result0[result0.length-1] 188 | if(result.length === 1 && type !== '=' && type !== '/') result[0] += ' ' 189 | 190 | callback(null, [result, item]) 191 | }) 192 | }) 193 | } 194 | 195 | 196 | module.exports = completer 197 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const inherits = require('util').inherits 2 | const Interface = require('readline').Interface 3 | 4 | const decode = require('decode-prompt') 5 | const parse = require('shell-parse') 6 | 7 | const _completer = require('./completer') 8 | const environment = require('./ast2js/_environment') 9 | const execCommands = require('./ast2js/_execCommands') 10 | 11 | 12 | function onError(error) 13 | { 14 | console.error(error) 15 | 16 | return this.prompt() 17 | } 18 | 19 | 20 | function Nsh(stdio, completer, terminal) 21 | { 22 | if(!(this instanceof Nsh)) return new Nsh(stdio, completer, terminal) 23 | 24 | const stdin = stdio[0] 25 | 26 | Nsh.super_.call(this, stdin, stdio[1], 27 | (completer || completer === false) ? completer : _completer, 28 | terminal) 29 | 30 | 31 | var self = this 32 | 33 | var input = '' 34 | 35 | function execCommandsCallback(error) 36 | { 37 | if(stdin.setRawMode) stdin.setRawMode(true) 38 | stdin.resume() 39 | 40 | if(error) console.error(error) 41 | 42 | self.prompt() 43 | } 44 | 45 | this.on('line', function(line) 46 | { 47 | input += line 48 | 49 | if(input === '') return this.prompt() 50 | 51 | try 52 | { 53 | var commands = parse(input) 54 | } 55 | catch(error) 56 | { 57 | if(error.constructor !== parse.SyntaxError) return onError.call(this, error) 58 | 59 | line = input.slice(error.offset) 60 | 61 | try 62 | { 63 | parse(line, 'continuationStart') 64 | } 65 | catch(error) 66 | { 67 | return onError.call(this, error) 68 | } 69 | 70 | return this.prompt(true) 71 | } 72 | 73 | if(stdin.setRawMode) stdin.setRawMode(false) 74 | stdin.pause() 75 | 76 | execCommands(stdio, commands, execCommandsCallback) 77 | }) 78 | 79 | 80 | // 81 | // Public API 82 | // 83 | 84 | /** 85 | * 86 | */ 87 | this.prompt = function(smallPrompt) 88 | { 89 | if(smallPrompt) 90 | var ps = environment['PS2'] 91 | 92 | else 93 | { 94 | input = '' 95 | 96 | var ps = environment['PS1'] 97 | } 98 | 99 | this.setPrompt(decode(ps, {env: environment})) 100 | 101 | // HACK Are these ones needed for builtins? We should remove them 102 | this.line = '' 103 | this.clearLine() 104 | 105 | Interface.prototype.prompt.call(this) 106 | } 107 | 108 | 109 | // Start acceoting commands 110 | this.prompt() 111 | } 112 | inherits(Nsh, Interface) 113 | 114 | 115 | Nsh.eval = function(stdio, line, callback) 116 | { 117 | try 118 | { 119 | var commands = parse(line) 120 | } 121 | catch(error) 122 | { 123 | return callback(error) 124 | } 125 | 126 | execCommands(stdio, commands, callback) 127 | } 128 | 129 | 130 | module.exports = Nsh 131 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bin-nsh", 3 | "version": "0.5.2", 4 | "description": "Node SHell", 5 | "author": "Jacob Groundwater ", 6 | "contributors": [ 7 | "Jesús Leganés Combarro 'piranna' " 8 | ], 9 | "license": "MIT", 10 | "main": "lib", 11 | "bin": { 12 | "nsh": "server.js" 13 | }, 14 | "scripts": { 15 | "coveralls": "easy-coveralls", 16 | "test": "mocha" 17 | }, 18 | "dependencies": { 19 | "array-flatten": "^2.1.1", 20 | "async": "^2.4.0", 21 | "concat-stream": "^1.6.0", 22 | "coreutils.js": "github:piranna/coreutils.js", 23 | "decode-prompt": "^0.0.2", 24 | "glob": "~7.1.1", 25 | "lib-pathcomplete": "piranna/node-lib-pathcomplete", 26 | "lib-pathsearch": "piranna/node-lib-pathsearch", 27 | "mkdirp": "~0.5.1", 28 | "mkfifo": "^1.2.5", 29 | "npm-path": "^2.0.3", 30 | "shell-parse": "^0.0.2", 31 | "to-string-stream": "^0.1.0", 32 | "uuid": "^3.0.1" 33 | }, 34 | "devDependencies": { 35 | "concat-stream": "^1.6.0", 36 | "easy-coveralls": "0.0.1", 37 | "mocha": "^3.4.1", 38 | "string-to-stream": "^1.1.0", 39 | "tmp": "0.0.31" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const readFile = require('fs').readFile 4 | 5 | const concat = require('concat-stream') 6 | const eachSeries = require('async/eachSeries') 7 | 8 | const Nsh = require('.') 9 | 10 | 11 | const PROFILE_FILES = ['/etc/profile', process.env.HOME+'/.profile'] 12 | const stdio = [process.stdin, process.stdout, process.stderr] 13 | 14 | 15 | function eval(data) 16 | { 17 | // Re-adjust arguments 18 | process.argv = process.argv.concat(argv) 19 | 20 | Nsh.eval(stdio, data, function(error) 21 | { 22 | if(error) onerror(error) 23 | }) 24 | } 25 | 26 | function interactiveShell() 27 | { 28 | Nsh(stdio) 29 | .on('SIGINT', function() 30 | { 31 | this.write('^C') 32 | this.clearLine() 33 | 34 | this.prompt() 35 | }) 36 | } 37 | 38 | function loadCommands() 39 | { 40 | switch(argv[0]) 41 | { 42 | case '-c': 43 | argv.shift() 44 | 45 | const command_string = argv.shift() 46 | if(!command_string) return onerror('-c requires an argument') 47 | 48 | const command_name = argv.shift() 49 | if(command_name) process.argv[0] = command_name 50 | 51 | return eval(command_string) 52 | 53 | case '-s': 54 | argv.shift() 55 | 56 | return process.stdin.pipe(concat(function(data) 57 | { 58 | eval(data.toString()) 59 | })) 60 | .on('error', onerror) 61 | break 62 | 63 | default: 64 | const command_file = argv.shift() 65 | if(command_file) 66 | return readFile(command_file, 'utf-8', function(error, data) 67 | { 68 | if(error) return onerror(error) 69 | 70 | eval(data) 71 | }) 72 | } 73 | 74 | 75 | // 76 | // Start an interactive shell 77 | // 78 | 79 | // Re-adjust arguments 80 | process.argv = process.argv.concat(argv) 81 | 82 | const ENV = process.env.ENV 83 | if(!ENV) return interactiveShell() 84 | 85 | readFile(ENV, 'utf-8', function(error, data) 86 | { 87 | if(error) return onerror(error) 88 | 89 | Nsh.eval(stdio, data, function(error) 90 | { 91 | if(error) return onerror(error) 92 | 93 | interactiveShell() 94 | }) 95 | }) 96 | } 97 | 98 | function onerror(error) 99 | { 100 | console.error(process.argv0+': '+error) 101 | process.exit(2) 102 | } 103 | 104 | 105 | // Get arguments 106 | 107 | const argv = process.argv.slice(2) 108 | process.argv = [process.argv[1]] 109 | 110 | if(argv[0] != '-l') return loadCommands() 111 | 112 | 113 | // Login shell 114 | 115 | argv.shift() 116 | 117 | eachSeries(PROFILE_FILES, function(file, callback) 118 | { 119 | readFile(file, 'utf-8', function(error, data) 120 | { 121 | if(error) return callback(error.code !== 'ENOENT' ? error : null) 122 | 123 | Nsh.eval(stdio, data, callback) 124 | }) 125 | }, 126 | function(error) 127 | { 128 | if(error) return onerror(error) 129 | 130 | loadCommands() 131 | }) 132 | -------------------------------------------------------------------------------- /test/fixture.txt: -------------------------------------------------------------------------------- 1 | asdf 2 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const fs = require('fs') 3 | const tty = require('tty') 4 | 5 | const concat = require('concat-stream') 6 | const str = require('string-to-stream') 7 | const tmp = require('tmp').file 8 | 9 | const spawnStream = require('../lib/ast2js/_spawnStream') 10 | 11 | 12 | describe('spawnStream', function() 13 | { 14 | it('no pipes', function(done) 15 | { 16 | var expected = ['aa','ab','bb'] 17 | 18 | const stdin = str(expected.join('\n')) 19 | const stdout = concat(function(data) 20 | { 21 | expected.shift() 22 | expected = expected.join('\n') 23 | 24 | assert.strictEqual(data.toString(), expected) 25 | }) 26 | 27 | const result = spawnStream('grep', ['b']) 28 | 29 | assert.strictEqual(result.constructor.name, 'Duplex') 30 | assert.ok(result.readable) 31 | assert.ok(result.writable) 32 | 33 | stdin.pipe(result).pipe(stdout) 34 | 35 | result.on('end', done) 36 | }) 37 | 38 | it('ignore stdio', function() 39 | { 40 | var stdio = ['ignore', 'ignore'] 41 | 42 | var result = spawnStream('ls', {stdio: stdio}) 43 | 44 | assert.strictEqual(result.constructor.name, 'EventEmitter') 45 | assert.ok(!result.readable) 46 | assert.ok(!result.writable) 47 | }) 48 | 49 | it('set a command as `stdin` of another', function(done) 50 | { 51 | var expected = ['aa','ab','bb'] 52 | 53 | const argv = [expected.join('\n'), '-e'] 54 | const echo = spawnStream('echo', argv, {stdio: ['ignore']}) 55 | 56 | assert.strictEqual(echo.constructor.name, 'Readable') 57 | assert.ok(echo.readable) 58 | assert.ok(!echo.writable) 59 | 60 | const stdout = concat(function(data) 61 | { 62 | expected.shift() 63 | expected = expected.join('\n') 64 | 65 | assert.strictEqual(data.toString(), expected+'\n') 66 | 67 | done() 68 | }) 69 | 70 | const grep = spawnStream('grep', ['b'], {stdio: [echo, stdout]}) 71 | 72 | assert.strictEqual(grep.constructor.name, 'EventEmitter') 73 | assert.ok(!grep.readable) 74 | assert.ok(!grep.writable) 75 | }) 76 | 77 | it('set a command as `stdout` of another', function(done) 78 | { 79 | var expected = ['aa','ab','bb'] 80 | 81 | const stdout = concat(function(data) 82 | { 83 | expected.shift() 84 | expected = expected.join('\n') 85 | 86 | assert.strictEqual(data.toString(), expected+'\n') 87 | 88 | done() 89 | }) 90 | 91 | const grep = spawnStream('grep', ['b'], {stdio: [null, stdout]}) 92 | 93 | assert.strictEqual(grep.constructor.name, 'Writable') 94 | assert.ok(!grep.readable) 95 | assert.ok(grep.writable) 96 | 97 | const argv = [expected.join('\n'), '-e'] 98 | const echo = spawnStream('echo', argv, {stdio: ['ignore', grep]}) 99 | 100 | assert.strictEqual(echo.constructor.name, 'EventEmitter') 101 | assert.ok(!echo.readable) 102 | assert.ok(!echo.writable) 103 | 104 | echo.stderr.pipe(process.stderr) 105 | }) 106 | 107 | describe('pipe regular streams', function() 108 | { 109 | it('pipe stdin', function(done) 110 | { 111 | var expected = ['aa','ab','bb'] 112 | 113 | const stdin = str(expected.join('\n')) 114 | const stdout = concat(function(data) 115 | { 116 | expected.shift() 117 | expected = expected.join('\n') 118 | 119 | assert.strictEqual(data.toString(), expected) 120 | 121 | done() 122 | }) 123 | 124 | const grep = spawnStream('grep', ['b'], {stdio: [stdin]}) 125 | 126 | assert.strictEqual(grep.constructor.name, 'Readable') 127 | assert.ok(grep.readable) 128 | assert.ok(!grep.writable) 129 | 130 | grep.pipe(stdout) 131 | }) 132 | 133 | it('pipe stdout', function(done) 134 | { 135 | const expected = 'asdf' 136 | 137 | const stdout = concat(function(data) 138 | { 139 | assert.strictEqual(data.toString(), expected+'\n') 140 | 141 | done() 142 | }) 143 | 144 | const echo = spawnStream('echo', [expected], {stdio: [null, stdout]}) 145 | 146 | assert.strictEqual(echo.constructor.name, 'Writable') 147 | assert.ok(!echo.readable) 148 | assert.ok(echo.writable) 149 | 150 | echo.stderr.pipe(process.stderr) 151 | }) 152 | 153 | it('fully piped', function(done) 154 | { 155 | var expected = ['aa','ab','bb'] 156 | 157 | const stdin = str(expected.join('\n')) 158 | const stdout = concat(function(data) 159 | { 160 | expected.shift() 161 | expected = expected.join('\n') 162 | 163 | assert.strictEqual(data.toString(), expected) 164 | 165 | done() 166 | }) 167 | 168 | var grep = spawnStream('grep', ['b'], {stdio: [stdin, stdout]}) 169 | 170 | assert.strictEqual(grep.constructor.name, 'EventEmitter') 171 | assert.ok(!grep.readable) 172 | assert.ok(!grep.writable) 173 | }) 174 | }) 175 | 176 | describe('pipe handler streams', function() 177 | { 178 | it('pipe stdin', function(done) 179 | { 180 | const expected = 'asdf' 181 | 182 | var stdin = new tty.ReadStream() 183 | const stdout = concat(function(data) 184 | { 185 | expected.shift() 186 | expected = expected.join('\n') 187 | 188 | assert.strictEqual(data.toString(), expected) 189 | 190 | done() 191 | }) 192 | 193 | var result = spawnStream('ls', {stdio: [stdin]}) 194 | 195 | assert.strictEqual(result.constructor.name, 'Readable') 196 | assert.ok(result.readable) 197 | assert.ok(!result.writable) 198 | 199 | result.resume() 200 | result.on('end', done) 201 | }) 202 | 203 | it('pipe stdout', function(done) 204 | { 205 | const expected = 'asdf' 206 | 207 | const stdout = new tty.WriteStream() 208 | 209 | const echo = spawnStream('echo', [expected], {stdio: [null, stdout]}) 210 | 211 | assert.strictEqual(echo.constructor.name, 'Writable') 212 | assert.ok(!echo.readable) 213 | assert.ok(echo.writable) 214 | 215 | echo.on('end', done) 216 | }) 217 | 218 | it('fully piped', function(done) 219 | { 220 | const stdin = new tty.ReadStream() 221 | const stdout = new tty.WriteStream() 222 | 223 | const result = spawnStream('ls', {stdio: [stdin, stdout]}) 224 | 225 | assert.strictEqual(result.constructor.name, 'EventEmitter') 226 | assert.ok(!result.readable) 227 | assert.ok(!result.writable) 228 | 229 | result.on('end', function() 230 | { 231 | 232 | done() 233 | }) 234 | }) 235 | }) 236 | }) 237 | 238 | describe('file descriptors', function() 239 | { 240 | it('pipe stdin', function(done) 241 | { 242 | fs.open('test/fixture.txt', 'r', function(err, fd) 243 | { 244 | if(err) return done(err) 245 | 246 | function clean(err1) 247 | { 248 | fs.close(fd, function(err2) 249 | { 250 | done(err1 || err2) 251 | }) 252 | } 253 | 254 | const expected = 'asdf' 255 | 256 | const stdout = concat(function(data) 257 | { 258 | assert.strictEqual(data.toString(), expected+'\n') 259 | 260 | clean() 261 | }) 262 | 263 | const echo = spawnStream('echo', [expected], {stdio: [fd]}) 264 | 265 | assert.strictEqual(echo.constructor.name, 'Readable') 266 | assert.ok(echo.readable) 267 | assert.ok(!echo.writable) 268 | 269 | echo.pipe(stdout) 270 | }) 271 | }) 272 | 273 | it('pipe stdout', function(done) 274 | { 275 | const expected = 'asdf' 276 | 277 | tmp(function(err, path, fd, cleanupCallback) 278 | { 279 | if(err) return done(err) 280 | 281 | function clean(err) 282 | { 283 | cleanupCallback() 284 | done(err) 285 | } 286 | 287 | const echo = spawnStream('echo', [expected], {stdio: [null, fd]}) 288 | 289 | assert.strictEqual(echo.constructor.name, 'Writable') 290 | assert.ok(!echo.readable) 291 | assert.ok(echo.writable) 292 | 293 | echo.on('end', function() 294 | { 295 | fs.readFile(path, 'utf-8', function(err, data) 296 | { 297 | if(err) return clean(err) 298 | 299 | assert.strictEqual(data, expected+'\n') 300 | 301 | clean() 302 | }) 303 | }) 304 | }) 305 | }) 306 | 307 | it('fully piped', function(done) 308 | { 309 | fs.open('test/fixture.txt', 'r', function(err, fdStdin) 310 | { 311 | if(err) return done(err) 312 | 313 | function clean1(err1) 314 | { 315 | fs.close(fdStdin, function(err2) 316 | { 317 | done(err1 || err2) 318 | }) 319 | } 320 | 321 | tmp(function(err, path, fdStdout, cleanupCallback) 322 | { 323 | if(err) return clean1(err) 324 | 325 | function clean2(err) 326 | { 327 | cleanupCallback() 328 | clean1(err) 329 | } 330 | 331 | const expected = 'asdf' 332 | 333 | const stdio = [fdStdin, fdStdout] 334 | const echo = spawnStream('echo', [expected], {stdio}) 335 | 336 | assert.strictEqual(echo.constructor.name, 'EventEmitter') 337 | assert.ok(!echo.readable) 338 | assert.ok(!echo.writable) 339 | 340 | echo.on('end', function() 341 | { 342 | fs.readFile(path, 'utf-8', function(err, data) 343 | { 344 | if(err) return clean2(err) 345 | 346 | assert.strictEqual(data, expected+'\n') 347 | 348 | clean2() 349 | }) 350 | }) 351 | }) 352 | }) 353 | }) 354 | }) 355 | 356 | 357 | // describe('command', function() 358 | // { 359 | // it('', function(done) 360 | // { 361 | // const item = 362 | // { 363 | // command:, 364 | // args: [], 365 | // stdio:, 366 | // redirects: [], 367 | // env: {} 368 | // } 369 | // 370 | // command(item, function(command) 371 | // { 372 | // 373 | // }) 374 | // }) 375 | // }) 376 | 377 | // if('inception', function() 378 | // { 379 | // 380 | // }) 381 | --------------------------------------------------------------------------------