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/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..858c214
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,16 @@
+language: node_js
+node_js:
+ - "6"
+ - "6.1"
+ - "5.11"
+ - "iojs"
+env:
+ - CXX=g++-4.8
+addons:
+ apt:
+ sources:
+ - ubuntu-toolchain-r-test
+ packages:
+ - g++-4.8
+after_script:
+ - npm run coveralls
diff --git a/README.md b/README.md
index 89d06ca..4237d82 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,12 @@
-# No(de) Shell
+[](https://travis-ci.org/piranna/nsh)
+[](https://coveralls.io/github/piranna/nsh?branch=master)
-
+# Node SHell
-Both **no shell** or **node shell** accurately describe `nsh`.
+[](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/lib/ast2js/_environment.js b/lib/ast2js/_environment.js
new file mode 100644
index 0000000..4056abb
--- /dev/null
+++ b/lib/ast2js/_environment.js
@@ -0,0 +1,85 @@
+'use strict'
+
+var environment =
+{
+ '?': 0,
+ PS1: '\\w > ',
+ PS2: '> ',
+ PS4: '+ '
+}
+
+
+//
+// Regular access
+//
+
+const handler =
+{
+ 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
+
+ return true
+ },
+
+ deleteProperty: function(_, prop)
+ {
+ // Local environment variable
+ var value = environment[prop]
+ if(value !== undefined)
+ delete environment[prop]
+
+ // Exported environment variable
+ else
+ delete process.env[prop]
+
+ return true
+ }
+}
+
+
+exports = new Proxy({}, handler)
+
+
+//
+// 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__
+}
+
+
+module.exports = exports
diff --git a/lib/ast2js/_execCommands.js b/lib/ast2js/_execCommands.js
new file mode 100644
index 0000000..20ef700
--- /dev/null
+++ b/lib/ast2js/_execCommands.js
@@ -0,0 +1,43 @@
+const eachSeries = require('async/eachSeries')
+
+
+module.exports = execCommands
+
+const ast2js = require('./index')
+const environment = require('./_environment')
+
+
+// Always calculate dynamic `$PATH` based on the original one
+const npmPath = require('npm-path').bind(null, {env:{PATH:process.env.PATH}})
+
+
+function execCommands(stdio, commands, callback)
+{
+ 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.stdio = stdio
+
+ ast2js(command, function(error, command)
+ {
+ if(error) return callback(error)
+
+ if(command == null) return 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
new file mode 100644
index 0000000..fe4efd9
--- /dev/null
+++ b/lib/ast2js/_redirects.js
@@ -0,0 +1,73 @@
+var reduce = require('async').reduce
+
+var ast2js = require('./index')
+
+
+function filterPipes(item)
+{
+ return item.type === 'pipe'
+}
+
+function getSrcFd(stdio, fd)
+{
+ var result = stdio[fd]
+
+ if(typeof result === 'string') return fd
+
+ return result
+}
+
+function setOutput(item)
+{
+ item.command.output = this
+}
+
+
+function iterator(stdio, redirect, callback)
+{
+ ast2js(redirect, function(error, value)
+ {
+ if(error) return callback(error)
+
+ var type = redirect.type
+ switch(type)
+ {
+ case 'duplicateFd':
+ stdio[redirect.destFd] = getSrcFd(stdio, redirect.srcFd)
+ break;
+
+ case 'moveFd':
+ stdio[redirect.dest] = getSrcFd(stdio, redirect.fd)
+ stdio[redirect.fd] = 'ignore'
+ break;
+
+ case 'pipe':
+ stdio[1] = value
+ break;
+
+ case 'redirectFd':
+ stdio[redirect.fd] = value
+
+ if(redirect.op === '&>'
+ || redirect.op === '&>>')
+ stdio[2] = value
+ break;
+
+ default:
+ return callback('Unknown redirect type "'+type+'"')
+ }
+
+ callback(null, stdio)
+ })
+}
+
+
+function redirects(stdio, array, callback)
+{
+ array.filter(filterPipes).forEach(setOutput, stdio.stdout)
+
+ reduce(array, stdio.slice(), iterator, callback)
+}
+
+
+module.exports = redirects
diff --git a/lib/ast2js/_spawnStream.js b/lib/ast2js/_spawnStream.js
new file mode 100644
index 0000000..7da5101
--- /dev/null
+++ b/lib/ast2js/_spawnStream.js
@@ -0,0 +1,147 @@
+const EventEmitter = require('events')
+const spawn = require('child_process').spawn
+const stream = require('stream')
+
+const Duplex = stream.Duplex
+const Readable = stream.Readable
+const Writable = stream.Writable
+
+
+function noop(){}
+
+/**
+ * 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)
+{
+ 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' || typeof stdin === 'number'
+ || stdin && stdin.constructor.name === 'ReadStream')
+ stdin = null
+ else
+ stdio[0] = 'pipe'
+
+ if(typeof stdout === 'string' || typeof stdout === 'number'
+ || stdout && stdout.constructor.name === 'WriteStream')
+ stdout = null
+ else
+ stdio[1] = 'pipe'
+
+ if(typeof stderr === 'string' || typeof stderr === 'number'
+ || stderr && stderr.constructor.name === 'WriteStream')
+ stderr = null
+ else
+ stdio[2] = 'pipe'
+
+ // Create child process
+ var cp = spawn(command, argv, options)
+
+ // Adjust events, pipe streams and restore stdio
+ if(stdin != null)
+ {
+ stdin.pipe(cp.stdin)
+ cp.stdin = null
+ }
+ if(stdout != null)
+ {
+ cp.stdout.pipe(stdout)
+ cp.stdout = null
+ }
+ if(stderr != null)
+ {
+ cp.stderr.pipe(stderr)
+ cp.stderr = null
+ }
+
+ // Return child process
+ return cp
+}
+
+
+function spawnStream(command, argv, options)
+{
+ if(argv && argv.constructor.name === 'Object')
+ {
+ options = argv
+ argv = undefined
+ }
+
+ options = options || {}
+ var stdio = options.stdio || []
+
+ var stdin = stdio[0]
+ var stdout = stdio[1]
+ var stderr = stdio[2]
+
+ var cp = wrapStdio(command, argv, options)
+
+ 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()
+
+ // Only `stdout` is open, use it directly
+ else if(stdout) result = Readable()
+
+ // Only `stdin` is open, ensure is always 'only' `Writable`
+ else if(stdin) result = Writable()
+
+ // Both `stdin` and `stdout` are clossed, or already redirected on `spawn`
+ else result = new EventEmitter()
+
+ // Connect stdio streams
+ if(stdin)
+ {
+ result._write = stdin.write.bind(stdin)
+ result.once('finish', stdin.end.bind(stdin))
+ }
+
+ if(stdout)
+ {
+ result._read = noop
+ stdout.on ('data', result.push.bind(result))
+ stdout.once('end' , result.push.bind(result, null))
+ }
+
+ // Use child process `exit` event instead of missing `stdout` `end` event
+ else
+ cp.once('exit', result.emit.bind(result, 'end'))
+
+ 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 && stdout.stderr
+ if(out_stderr) out_stderr.pipe(stderr)
+ }
+
+ // Propagate process events
+ cp.once('error', result.emit.bind(result, 'error'))
+ cp.once('exit' , result.emit.bind(result, 'exit' ))
+
+ return result
+}
+
+
+module.exports = spawnStream
diff --git a/lib/ast2js/command.js b/lib/ast2js/command.js
new file mode 100644
index 0000000..afc6aee
--- /dev/null
+++ b/lib/ast2js/command.js
@@ -0,0 +1,162 @@
+const Domain = require('domain').Domain
+const Duplex = require('stream').Duplex
+const Readable = require('stream').Readable
+
+const flatten = require('array-flatten')
+const map = require('async').map
+const ToStringStream = require('to-string-stream')
+
+const ast2js = require('./index')
+const redirects = require('./_redirects')
+const spawnStream = require('./_spawnStream')
+
+const builtins = require('../builtins')
+
+
+function noop(){}
+
+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
+ command = d.run(command.bind(options.env), argv)
+
+ // TODO stdin === 'ignore'
+ if(stdin !== 'pipe')
+ {
+ if(command.writeable)
+ stdin.pipe(command)
+
+ stdin = null
+ }
+
+ // TODO stdout === 'ignore'
+ if(stdout !== 'pipe')
+ {
+ 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()
+ 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'))
+ }
+
+ else if(stdin)
+ {
+ result = stdin
+
+ command.once('finish', result.emit.bind(result, 'end'))
+ }
+
+ else if(stdout)
+ {
+ result = command
+
+ stdout.once('end', result.emit.bind(result, 'end'))
+ }
+
+ else
+ result = command
+
+ // 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
+}
+
+
+function command(item, callback)
+{
+ // Command
+ ast2js(item.command, function(error, command)
+ {
+ if(error) return callback(error)
+
+ // Arguments
+ map(item.args, ast2js, function(error, argv)
+ {
+ if(error) return callback(error)
+
+ // Globs return an array, flat it
+ argv = flatten(argv)
+
+ // Redirects
+ redirects(item.stdio, item.redirects, function(error, stdio)
+ {
+ if(error) return callback(error)
+
+ // Create command
+ var env = item.env
+ env.__proto__ = process.env
+
+ 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)
+ }
+ catch(error)
+ {
+ if(error.code === 'EACCES') error = command+': is a directory'
+
+ return callback(error)
+ }
+
+ callback(null, command)
+ })
+ })
+ })
+}
+
+
+module.exports = command
diff --git a/lib/ast2js/commandSubstitution.js b/lib/ast2js/commandSubstitution.js
new file mode 100644
index 0000000..d84baf9
--- /dev/null
+++ b/lib/ast2js/commandSubstitution.js
@@ -0,0 +1,36 @@
+var Writable = require('stream').Writable
+
+var environment = require('./_environment')
+var execCommands = require('./_execCommands')
+
+
+function commandSubstitution(item, callback)
+{
+ var buffer = []
+
+ var output = new Writable()
+ output._write = function(chunk, _, done)
+ {
+ buffer.push(chunk)
+ done()
+ }
+
+ // Protect environment variables
+ environment.push()
+
+ execCommands({output: output}, item.commands, function(error)
+ {
+ // Restore environment variables
+ environment.pop()
+
+ // Restore (possible) changed current dir
+ process.chdir(environment['PWD'])
+
+ if(error) return callback(error)
+
+ callback(null, buffer.join(''))
+ })
+}
+
+
+module.exports = commandSubstitution
diff --git a/lib/ast2js/glob.js b/lib/ast2js/glob.js
new file mode 100644
index 0000000..83ab371
--- /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..8f7a3d3
--- /dev/null
+++ b/lib/ast2js/ifElse.js
@@ -0,0 +1,37 @@
+var detectSeries = require('async').detectSeries
+
+var ast2js = require('./index')
+
+
+function ifElse(item, callback)
+{
+ function runTest(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()
+ }
+
+
+ runTest(item, function(value)
+ {
+ if(value) return ast2js(item.body, callback)
+
+ detectSeries(item.elifBlocks || [], runTest, execBody)
+ })
+}
+
+
+module.exports = ifElse
diff --git a/lib/ast2js/index.js b/lib/ast2js/index.js
new file mode 100644
index 0000000..bf52424
--- /dev/null
+++ b/lib/ast2js/index.js
@@ -0,0 +1,30 @@
+function noop(item, callback)
+{
+ callback()
+}
+
+
+function ast2js(item, callback)
+{
+ ast2js[item.type](item, callback)
+}
+
+
+module.exports = ast2js
+
+
+ast2js.command = require('./command')
+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 // Processed on `_redirects` file
+ast2js.pipe = require('./pipe')
+ast2js.processSubstitution = require('./processSubstitution')
+ast2js.redirectFd = require('./redirectFd')
+ast2js['until-loop'] = require('./until-loop')
+ast2js.variable = require('./variable')
+ast2js.variableAssignment = require('./variableAssignment')
+ast2js.variableSubstitution = require('./variableSubstitution')
+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..41b71b5
--- /dev/null
+++ b/lib/ast2js/pipe.js
@@ -0,0 +1,10 @@
+var command = require('./command')
+
+
+function pipe(item, callback)
+{
+ command(item.command, callback)
+}
+
+
+module.exports = pipe
diff --git a/lib/ast2js/processSubstitution.js b/lib/ast2js/processSubstitution.js
new file mode 100644
index 0000000..a4e6628
--- /dev/null
+++ b/lib/ast2js/processSubstitution.js
@@ -0,0 +1,57 @@
+const fs = require('fs')
+const tmpdir = require('os').tmpdir
+
+const mkfifo = require('mkfifo').mkfifo
+const uuid = require('uuid').v4
+
+const environment = require('./_environment')
+const execCommands = require('./_execCommands')
+
+
+function processSubstitution(item, callback)
+{
+ const path = tmpdir()+'/'+uuid()
+
+ mkfifo(path, 0600, function(error)
+ {
+ if(error) return callback(error)
+
+ // Protect environment variables
+ environment.push()
+
+ function onExecuted(error)
+ {
+ stream.close()
+
+ // Restore environment variables
+ environment.pop()
+
+ // Restore (possible) changed current dir
+ process.chdir(environment['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/lib/ast2js/redirectFd.js b/lib/ast2js/redirectFd.js
new file mode 100644
index 0000000..2844d1a
--- /dev/null
+++ b/lib/ast2js/redirectFd.js
@@ -0,0 +1,60 @@
+var fs = require('fs')
+
+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)
+
+ var result
+
+ var op = item.op
+ switch(op)
+ {
+ case '<':
+ result = fs.createReadStream(filename)
+ break
+
+ case '>':
+ case '&>':
+ result = fs.createWriteStream(filename, {flags: 'wx'})
+ break
+
+ case '>|':
+ result = fs.createWriteStream(filename)
+ break
+
+ case '>>':
+ case '&>>':
+ result = fs.createWriteStream(filename, {flags: 'a'})
+ break
+
+ default:
+ return callback('Unknown redirectFd op "'+op+'"')
+ }
+
+ result.once('error', onError)
+ result.once('open' , onOpen)
+ })
+}
+
+
+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..e2938e5
--- /dev/null
+++ b/lib/ast2js/variable.js
@@ -0,0 +1,10 @@
+const environment = require('./_environment')
+
+
+function variable(item, callback)
+{
+ callback(null, environment[item.name])
+}
+
+
+module.exports = variable
diff --git a/lib/ast2js/variableAssignment.js b/lib/ast2js/variableAssignment.js
new file mode 100644
index 0000000..7df3cdb
--- /dev/null
+++ b/lib/ast2js/variableAssignment.js
@@ -0,0 +1,18 @@
+var ast2js = require('./index')
+var environment = require('./_environment')
+
+
+function variableAssignment(item, callback)
+{
+ ast2js(item.value, function(error, value)
+ {
+ if(error) return callback(error)
+
+ environment[item.name] = value
+
+ callback()
+ })
+}
+
+
+module.exports = variableAssignment
diff --git a/lib/ast2js/variableSubstitution.js b/lib/ast2js/variableSubstitution.js
new file mode 100644
index 0000000..12a128f
--- /dev/null
+++ b/lib/ast2js/variableSubstitution.js
@@ -0,0 +1,10 @@
+const environment = require('./_environment')
+
+
+function variableSubstitution(item, callback)
+{
+ callback(null, environment[item.expression])
+}
+
+
+module.exports = variableSubstitution
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/builtins.js b/lib/builtins.js
new file mode 100644
index 0000000..2a77a3a
--- /dev/null
+++ b/lib/builtins.js
@@ -0,0 +1,19 @@
+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)
+{
+ return coreutils_cd(argv, environment)
+}
+
+coreutils.cd = cd
+
+
+module.exports = coreutils
diff --git a/lib/completer.js b/lib/completer.js
new file mode 100644
index 0000000..268c8d5
--- /dev/null
+++ b/lib/completer.js
@@ -0,0 +1,196 @@
+const fs = require('fs')
+
+const pc = require('lib-pathcomplete')
+const ps = require('lib-pathsearch')
+const reduce = require('async/reduce')
+
+const builtins = require('./builtins')
+const environment = require('./ast2js/_environment')
+
+const constants = fs.constants
+const stat = fs.stat
+
+
+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.toString()
+}
+
+function getEnvVars(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('')
+
+ return vars.concat(env)
+}
+
+function isExecutable(stats)
+{
+ const mode = stats.mode
+
+ return mode & constants.S_IXUSR && stats.uid === process.getuid()
+ || mode & constants.S_IXGRP && stats.gid === process.getgid()
+ || mode & constants.S_IXOTH
+}
+
+function mapEnvVarsAsign(name)
+{
+ if(name) name += '='
+
+ return name
+}
+
+function mapEnvVarsRef(name)
+{
+ if(name) name = '$'+name
+
+ return name
+}
+
+
+//
+// Completer functions
+//
+
+function envVar(item, callback)
+{
+ const key = item.substr(1)
+
+ if(!key) return callback(EMPTY_ENV_VAR)
+
+ var result = getEnvVars(key).map(mapEnvVarsRef)
+
+ // if there is only one environment variable, append a space after it
+ if(result.length === 1) result[0] += ' '
+
+ 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)
+ {
+ const path = info.dir + name
+
+ stat(path, function(error, stats)
+ {
+ if(error) return callback(error)
+
+ memo[path] = stats
+
+ 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]
+
+ return stat.isDirectory() || isExecutable(stat)
+ })
+
+ arr = arr.map(function(item)
+ {
+ // If completion is a directory, append a slash
+ if(stats[info.dir + item].isDirectory()) item += '/'
+
+ return item
+ })
+
+ // 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] += ' '
+
+ callback(null, [arr, info.file])
+ })
+ })
+}
+
+
+/**
+ * auto-complete handler
+ */
+function completer(line, callback)
+{
+ const split = line.split(/\s+/)
+ const item = split.pop()
+ const is_arg = split.filter(filterEnvVars).length
+
+ // 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, environment.PATH.split(':'), function(err, execs)
+ {
+ if(err) return callback(err)
+
+ // Builtins
+ var names = Object.keys(builtins).filter(filterNames, item)
+
+ // Environment variables
+ var envVars = getEnvVars(item).map(mapEnvVarsAsign)
+
+ // Current directory
+ relativePath(item, true, function(err, entries)
+ {
+ if(err) return callback(err)
+
+ entries = entries[0]
+
+ // 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])
+ })
+ })
+}
+
+
+module.exports = completer
diff --git a/lib/index.js b/lib/index.js
new file mode 100644
index 0000000..027930e
--- /dev/null
+++ b/lib/index.js
@@ -0,0 +1,130 @@
+const inherits = require('util').inherits
+const Interface = require('readline').Interface
+
+const decode = require('decode-prompt')
+const parse = require('shell-parse')
+
+const _completer = require('./completer')
+const environment = require('./ast2js/_environment')
+const execCommands = require('./ast2js/_execCommands')
+
+
+function onError(error)
+{
+ console.error(error)
+
+ return this.prompt()
+}
+
+
+function Nsh(stdio, completer, terminal)
+{
+ if(!(this instanceof Nsh)) return new Nsh(stdio, completer, terminal)
+
+ const stdin = stdio[0]
+
+ Nsh.super_.call(this, stdin, stdio[1],
+ (completer || completer === false) ? completer : _completer,
+ terminal)
+
+
+ var self = this
+
+ var input = ''
+
+ function execCommandsCallback(error)
+ {
+ if(stdin.setRawMode) stdin.setRawMode(true)
+ stdin.resume()
+
+ 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)
+ }
+
+ if(stdin.setRawMode) stdin.setRawMode(false)
+ stdin.pause()
+
+ execCommands(stdio, commands, execCommandsCallback)
+ })
+
+
+ //
+ // Public API
+ //
+
+ /**
+ *
+ */
+ this.prompt = function(smallPrompt)
+ {
+ if(smallPrompt)
+ var ps = environment['PS2']
+
+ else
+ {
+ input = ''
+
+ var ps = environment['PS1']
+ }
+
+ this.setPrompt(decode(ps, {env: environment}))
+
+ // HACK Are these ones needed for builtins? We should remove them
+ this.line = ''
+ this.clearLine()
+
+ Interface.prototype.prompt.call(this)
+ }
+
+
+ // Start acceoting commands
+ this.prompt()
+}
+inherits(Nsh, Interface)
+
+
+Nsh.eval = function(stdio, line, callback)
+{
+ try
+ {
+ var commands = parse(line)
+ }
+ catch(error)
+ {
+ return callback(error)
+ }
+
+ execCommands(stdio, commands, callback)
+}
+
+
+module.exports = Nsh
diff --git a/nsh.js b/nsh.js
deleted file mode 100755
index b4007ad..0000000
--- a/nsh.js
+++ /dev/null
@@ -1,190 +0,0 @@
-#!/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 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 = [];
-
- // 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] = 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);
- }
- } 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 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();
- }
-
- // 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();
-}
diff --git a/package.json b/package.json
index a768606..6928bb4 100644
--- a/package.json
+++ b/package.json
@@ -1,17 +1,41 @@
{
"name": "bin-nsh",
- "version": "0.4.0",
- "description": "Node/No Shell",
+ "version": "0.5.2",
+ "description": "Node SHell",
"author": "Jacob Groundwater ",
+ "contributors": [
+ "Jesús Leganés Combarro 'piranna' "
+ ],
"license": "MIT",
+ "main": "lib",
"bin": {
- "nsh": "nsh.js"
+ "nsh": "server.js"
+ },
+ "scripts": {
+ "coveralls": "easy-coveralls",
+ "test": "mocha"
},
"dependencies": {
- "lib-cmdparse": "~0.1.0",
- "glob": "~3.2.7",
- "lib-pathcomplete": "0.0.1",
- "lib-pathsearch": "~0.1.0",
- "mkdirp": "~0.3.5"
+ "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",
+ "lib-pathcomplete": "piranna/node-lib-pathcomplete",
+ "lib-pathsearch": "piranna/node-lib-pathsearch",
+ "mkdirp": "~0.5.1",
+ "mkfifo": "^1.2.5",
+ "npm-path": "^2.0.3",
+ "shell-parse": "^0.0.2",
+ "to-string-stream": "^0.1.0",
+ "uuid": "^3.0.1"
+ },
+ "devDependencies": {
+ "concat-stream": "^1.6.0",
+ "easy-coveralls": "0.0.1",
+ "mocha": "^3.4.1",
+ "string-to-stream": "^1.1.0",
+ "tmp": "0.0.31"
}
}
diff --git a/server.js b/server.js
new file mode 100755
index 0000000..b8fa2e1
--- /dev/null
+++ b/server.js
@@ -0,0 +1,131 @@
+#!/usr/bin/env node
+
+const readFile = require('fs').readFile
+
+const concat = require('concat-stream')
+const eachSeries = require('async/eachSeries')
+
+const Nsh = require('.')
+
+
+const PROFILE_FILES = ['/etc/profile', process.env.HOME+'/.profile']
+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 interactiveShell()
+{
+ Nsh(stdio)
+ .on('SIGINT', function()
+ {
+ this.write('^C')
+ this.clearLine()
+
+ this.prompt()
+ })
+}
+
+function loadCommands()
+{
+ switch(argv[0])
+ {
+ case '-c':
+ argv.shift()
+
+ 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, 'utf-8', function(error, data)
+ {
+ if(error) return onerror(error)
+
+ eval(data)
+ })
+ }
+
+
+ //
+ // Start an interactive shell
+ //
+
+ // 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)
+ {
+ if(error) return onerror(error)
+
+ interactiveShell()
+ })
+ })
+}
+
+function onerror(error)
+{
+ console.error(process.argv0+': '+error)
+ process.exit(2)
+}
+
+
+// Get arguments
+
+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)
+{
+ if(error) return onerror(error)
+
+ loadCommands()
+})
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()
+// {
+//
+// })