diff --git a/README.md b/README.md index 962db6f..dde6295 100644 --- a/README.md +++ b/README.md @@ -619,7 +619,7 @@ otherwise returns string explaining the error Examples: -``` +```javascript var foo = ShellString('hello world'); ``` @@ -627,10 +627,25 @@ Turns a regular string into a string-like object similar to what each command returns. This has special methods, like `.to()` and `.toEnd()` +### Pipes + +Examples: + +```javascript +grep('foo', 'file1.txt', 'file2.txt').sed(/o/g, 'a').to('output.txt'); +echo('files with o\'s in the name:\n' + ls().grep('o')); +cat('test.js').exec('node'); // pipe to exec() call +``` + +Commands can send their output to another command in a pipe-like fashion. +`sed`, `grep`, `cat`, `exec`, `to`, and `toEnd` can appear on the right-hand +side of a pipe. Pipes can be chained. + ## Configuration ### config.silent + Example: ```javascript @@ -645,6 +660,7 @@ Suppresses all command output if `true`, except for `echo()` calls. Default is `false`. ### config.fatal + Example: ```javascript @@ -659,6 +675,7 @@ command encounters an error. Default is `false`. This is analogous to Bash's `set -e` ### config.verbose + Example: ```javascript diff --git a/shell.js b/shell.js index 76fd189..51ff397 100644 --- a/shell.js +++ b/shell.js @@ -128,6 +128,20 @@ exports.error = _error; //@include ./src/common exports.ShellString = common.ShellString; +//@ +//@ ### Pipes +//@ +//@ Examples: +//@ +//@ ```javascript +//@ grep('foo', 'file1.txt', 'file2.txt').sed(/o/g, 'a').to('output.txt'); +//@ echo('files with o\'s in the name:\n' + ls().grep('o')); +//@ cat('test.js').exec('node'); // pipe to exec() call +//@ ``` +//@ +//@ Commands can send their output to another command in a pipe-like fashion. +//@ `sed`, `grep`, `cat`, `exec`, `to`, and `toEnd` can appear on the right-hand +//@ side of a pipe. Pipes can be chained. //@ //@ ## Configuration @@ -137,6 +151,7 @@ exports.config = common.config; //@ //@ ### config.silent +//@ //@ Example: //@ //@ ```javascript @@ -152,6 +167,7 @@ exports.config = common.config; //@ //@ ### config.fatal +//@ //@ Example: //@ //@ ```javascript @@ -167,6 +183,7 @@ exports.config = common.config; //@ //@ ### config.verbose +//@ //@ Example: //@ //@ ```javascript diff --git a/src/cat.js b/src/cat.js index 8257b72..27889b7 100644 --- a/src/cat.js +++ b/src/cat.js @@ -17,14 +17,12 @@ var fs = require('fs'); //@ containing the files if more than one file is given (a new line character is //@ introduced between each file). Wildcard `*` accepted. function _cat(options, files) { - var cat = ''; + var cat = common.readFromPipe(this); - if (!files) + if (!files && !cat) common.error('no paths given'); - if (typeof files === 'string') - files = [].slice.call(arguments, 1); - // if it's array leave it as it is + files = [].slice.call(arguments, 1); files.forEach(function(file) { if (!fs.existsSync(file)) diff --git a/src/common.js b/src/common.js index 4464a3a..1ac42f4 100644 --- a/src/common.js +++ b/src/common.js @@ -5,6 +5,7 @@ var os = require('os'); var fs = require('fs'); var glob = require('glob'); +var shell = require('..'); var _to = require('./to'); var _toEnd = require('./toEnd'); @@ -57,18 +58,27 @@ exports.error = error; //@ //@ Examples: //@ -//@ ``` +//@ ```javascript //@ var foo = ShellString('hello world'); //@ ``` //@ //@ Turns a regular string into a string-like object similar to what each //@ command returns. This has special methods, like `.to()` and `.toEnd()` -var ShellString = function (str, stderr) { - var that = new String(str); - that.to = wrap('to', _to, {idx: 1}); - that.toEnd = wrap('toEnd', _toEnd, {idx: 1}); - that.stdout = str; +var ShellString = function (stdout, stderr) { + var that; + if (stdout instanceof Array) { + that = stdout; + that.stdout = stdout.join('\n')+'\n'; + } else { + that = new String(stdout); + that.stdout = stdout; + } that.stderr = stderr; + that.to = function() {wrap('to', _to, {idx: 1}).apply(that.stdout, arguments); return that;}; + that.toEnd = function() {wrap('toEnd', _toEnd, {idx: 1}).apply(that.stdout, arguments); return that;}; + ['cat', 'sed', 'grep', 'exec'].forEach(function (cmd) { + that[cmd] = function() {return shell[cmd].apply(that.stdout, arguments);}; + }); return that; }; @@ -281,3 +291,8 @@ function wrap(cmd, fn, options) { }; } // wrap exports.wrap = wrap; + +function _readFromPipe(that) { + return that instanceof String ? that.toString() : ''; +} +exports.readFromPipe = _readFromPipe; diff --git a/src/exec.js b/src/exec.js index 9d50b54..5af5c31 100644 --- a/src/exec.js +++ b/src/exec.js @@ -12,7 +12,7 @@ var DEFAULT_MAXBUFFER_SIZE = 20*1024*1024; // (Can't do a wait loop that checks for internal Node variables/messages as // Node is single-threaded; callbacks and other internal state changes are done in the // event loop). -function execSync(cmd, opts) { +function execSync(cmd, opts, pipe) { var tempDir = _tempDir(); var stdoutFile = path.resolve(tempDir+'/'+common.randomFileName()), stderrFile = path.resolve(tempDir+'/'+common.randomFileName()), @@ -53,10 +53,6 @@ function execSync(cmd, opts) { previousStreamContent = streamContent; } - function escape(str) { - return (str+'').replace(/([\\"'])/g, "\\$1").replace(/\0/g, "\\0"); - } - if (fs.existsSync(scriptFile)) common.unlinkSync(scriptFile); if (fs.existsSync(stdoutFile)) common.unlinkSync(stdoutFile); if (fs.existsSync(stderrFile)) common.unlinkSync(stderrFile); @@ -74,23 +70,26 @@ function execSync(cmd, opts) { if (typeof child.execSync === 'function') { script = [ - "var child = require('child_process')", - " , fs = require('fs');", - "var childProcess = child.exec('"+escape(cmd)+"', "+optString+", function(err) {", - " fs.writeFileSync('"+escape(codeFile)+"', err ? err.code.toString() : '0');", - "});", - "var stdoutStream = fs.createWriteStream('"+escape(stdoutFile)+"');", - "var stderrStream = fs.createWriteStream('"+escape(stderrFile)+"');", - "childProcess.stdout.pipe(stdoutStream, {end: false});", - "childProcess.stderr.pipe(stderrStream, {end: false});", - "childProcess.stdout.pipe(process.stdout);", - "childProcess.stderr.pipe(process.stderr);", - "var stdoutEnded = false, stderrEnded = false;", - "function tryClosingStdout(){ if(stdoutEnded){ stdoutStream.end(); } }", - "function tryClosingStderr(){ if(stderrEnded){ stderrStream.end(); } }", - "childProcess.stdout.on('end', function(){ stdoutEnded = true; tryClosingStdout(); });", - "childProcess.stderr.on('end', function(){ stderrEnded = true; tryClosingStderr(); });" - ].join('\n'); + "var child = require('child_process')", + " , fs = require('fs');", + "var childProcess = child.exec("+JSON.stringify(cmd)+", "+optString+", function(err) {", + " fs.writeFileSync("+JSON.stringify(codeFile)+", err ? err.code.toString() : '0');", + "});", + "var stdoutStream = fs.createWriteStream("+JSON.stringify(stdoutFile)+");", + "var stderrStream = fs.createWriteStream("+JSON.stringify(stderrFile)+");", + "childProcess.stdout.pipe(stdoutStream, {end: false});", + "childProcess.stderr.pipe(stderrStream, {end: false});", + "childProcess.stdout.pipe(process.stdout);", + "childProcess.stderr.pipe(process.stderr);" + ].join('\n') + + (pipe ? "\nchildProcess.stdin.end("+JSON.stringify(pipe)+");\n" : '\n') + + [ + "var stdoutEnded = false, stderrEnded = false;", + "function tryClosingStdout(){ if(stdoutEnded){ stdoutStream.end(); } }", + "function tryClosingStderr(){ if(stderrEnded){ stderrStream.end(); } }", + "childProcess.stdout.on('end', function(){ stdoutEnded = true; tryClosingStdout(); });", + "childProcess.stderr.on('end', function(){ stderrEnded = true; tryClosingStderr(); });" + ].join('\n'); fs.writeFileSync(scriptFile, script); @@ -115,12 +114,13 @@ function execSync(cmd, opts) { cmd += ' > '+stdoutFile+' 2> '+stderrFile; // works on both win/unix script = [ - "var child = require('child_process')", - " , fs = require('fs');", - "var childProcess = child.exec('"+escape(cmd)+"', "+optString+", function(err) {", - " fs.writeFileSync('"+escape(codeFile)+"', err ? err.code.toString() : '0');", - "});" - ].join('\n'); + "var child = require('child_process')", + " , fs = require('fs');", + "var childProcess = child.exec("+JSON.stringify(cmd)+", "+optString+", function(err) {", + " fs.writeFileSync("+JSON.stringify(codeFile)+", err ? err.code.toString() : '0');", + "});" + ].join('\n') + + (pipe ? "\nchildProcess.stdin.end("+JSON.stringify(pipe)+");\n" : '\n'); fs.writeFileSync(scriptFile, script); @@ -163,7 +163,7 @@ function execSync(cmd, opts) { } // execSync() // Wrapper around exec() to enable echoing output to console in real time -function execAsync(cmd, opts, callback) { +function execAsync(cmd, opts, pipe, callback) { var stdout = ''; var stderr = ''; @@ -179,6 +179,9 @@ function execAsync(cmd, opts, callback) { callback(err ? err.code : 0, stdout, stderr); }); + if (pipe) + c.stdin.end(pipe); + c.stdout.on('data', function(data) { stdout += data; if (!opts.silent) @@ -233,6 +236,8 @@ function _exec(command, options, callback) { if (!command) common.error('must specify command'); + var pipe = common.readFromPipe(this); + // Callback is defined instead of options. if (typeof options === 'function') { callback = options; @@ -251,9 +256,9 @@ function _exec(command, options, callback) { try { if (options.async) - return execAsync(command, options, callback); + return execAsync(command, options, pipe, callback); else - return execSync(command, options); + return execSync(command, options, pipe); } catch (e) { common.error('internal error'); } diff --git a/src/grep.js b/src/grep.js index 8ef7d90..3c0106a 100644 --- a/src/grep.js +++ b/src/grep.js @@ -24,21 +24,26 @@ function _grep(options, regex, files) { 'l': 'nameOnly' }); - if (!files) + // Check if this is coming from a pipe + var pipe = common.readFromPipe(this); + + if (!files && !pipe) common.error('no paths given'); - if (typeof files === 'string') - files = [].slice.call(arguments, 2); + files = [].slice.call(arguments, 2); + + if (pipe) + files.unshift('-'); var grep = []; files.forEach(function(file) { - if (!fs.existsSync(file)) { + if (!fs.existsSync(file) && file !== '-') { common.error('no such file or directory: ' + file, true); return; } - var contents = fs.readFileSync(file, 'utf8'), - lines = contents.split(/\r*\n/); + var contents = file === '-' ? pipe : fs.readFileSync(file, 'utf8'); + var lines = contents.split(/\r*\n/); if (options.nameOnly) { if (contents.match(regex)) grep.push(file); diff --git a/src/ls.js b/src/ls.js index 482a388..71953b4 100644 --- a/src/ls.js +++ b/src/ls.js @@ -3,8 +3,6 @@ var fs = require('fs'); var common = require('./common'); var _cd = require('./cd'); var _pwd = require('./pwd'); -var _to = require('./to'); -var _toEnd = require('./toEnd'); //@ //@ ### ls([options,] [path, ...]) @@ -154,15 +152,7 @@ function _ls(options, paths) { }); // Add methods, to make this more compatible with ShellStrings - list.stdout = list.join('\n') + '\n'; - list.stderr = common.state.error; - list.to = function (file) { - common.wrap('to', _to, {idx: 1}).call(this.stdout, file); - }; - list.toEnd = function(file) { - common.wrap('toEnd', _toEnd, {idx: 1}).call(this.stdout, file); - }; - return list; + return new common.ShellString(list, common.state.error); } module.exports = _ls; diff --git a/src/sed.js b/src/sed.js index 7477403..a9c675a 100644 --- a/src/sed.js +++ b/src/sed.js @@ -22,6 +22,9 @@ function _sed(options, regex, replacement, files) { 'i': 'inplace' }); + // Check if this is coming from a pipe + var pipe = common.readFromPipe(this); + if (typeof replacement === 'string' || typeof replacement === 'function') replacement = replacement; // no-op else if (typeof replacement === 'number') @@ -33,21 +36,24 @@ function _sed(options, regex, replacement, files) { if (typeof regex === 'string') regex = RegExp(regex); - if (!files) + if (!files && !pipe) common.error('no files given'); - if (typeof files === 'string') - files = [].slice.call(arguments, 3); - // if it's array leave it as it is + files = [].slice.call(arguments, 3); + + if (pipe) + files.unshift('-'); var sed = []; files.forEach(function(file) { - if (!fs.existsSync(file)) { + if (!fs.existsSync(file) && file !== '-') { common.error('no such file or directory: ' + file, true); return; } - var result = fs.readFileSync(file, 'utf8').split('\n').map(function (line) { + var contents = file === '-' ? pipe : fs.readFileSync(file, 'utf8'); + var lines = contents.split(/\r*\n/); + var result = lines.map(function (line) { return line.replace(regex, replacement); }).join('\n'); diff --git a/test/pipe.js b/test/pipe.js new file mode 100644 index 0000000..e877ec5 --- /dev/null +++ b/test/pipe.js @@ -0,0 +1,85 @@ +var shell = require('..'); + +var assert = require('assert'); + +shell.config.silent = true; + +shell.rm('-rf', 'tmp'); +shell.mkdir('tmp'); + +// +// Invalids +// + +// no piped methods for commands that don't return anything +assert.throws(function() { + shell.cp('resources/file1', 'tmp/'); + assert.ok(!shell.error()); + shell.cp('resources/file1', 'tmp/').cat(); +}); + +// commands like `rm` can't be on the right side of pipes +assert.equal(typeof shell.ls('.').rm, 'undefined'); +assert.equal(typeof shell.cat('resources/file1.txt').rm, 'undefined'); + +// +// Valids +// + +// piping to cat() should return roughly the same thing +assert.strictEqual(shell.cat('resources/file1.txt').cat().toString(), + shell.cat('resources/file1.txt').toString()); + +// piping ls() into cat() converts to a string +assert.strictEqual(shell.ls('resources/').cat().toString(), + shell.ls('resources/').stdout); + +var result; +result = shell.ls('resources/').grep('file1'); +assert.equal(result + '', 'file1\nfile1.js\nfile1.txt\n'); + +result = shell.ls('resources/').cat().grep('file1'); +assert.equal(result + '', 'file1\nfile1.js\nfile1.txt\n'); + +// Equivalent to a simple grep() test case +result = shell.cat('resources/grep/file').grep(/alpha*beta/); +assert.equal(shell.error(), null); +assert.equal(result.toString(), 'alphaaaaaaabeta\nalphbeta\n'); + +// Equivalent to a simple sed() test case +var result = shell.cat('resources/grep/file').sed(/l*\.js/, ''); +assert.ok(!shell.error()); +assert.equal(result.toString(), 'alphaaaaaaabeta\nhowareyou\nalphbeta\nthis line ends in\n\n'); + +// Synchronous exec +// TODO: add windows tests +if (process.platform !== 'win32') { + // unix-specific + if (shell.which('grep').stdout) { + result = shell.cat('resources/grep/file').exec("grep 'alpha*beta'"); + assert.equal(shell.error(), null); + assert.equal(result, 'alphaaaaaaabeta\nalphbeta\n'); + } else { + console.error('Warning: Cannot verify piped exec'); + } +} else { + console.error('Warning: Cannot verify piped exec'); +} + +// Async exec +// TODO: add windows tests +if (process.platform !== 'win32') { + // unix-specific + if (shell.which('grep').stdout) { + shell.cat('resources/grep/file').exec("grep 'alpha*beta'", function(code, stdout) { + assert.equal(code, 0); + assert.equal(stdout, 'alphaaaaaaabeta\nalphbeta\n'); + shell.exit(123); + }); + } else { + console.error('Warning: Cannot verify piped exec'); + } +} else { + console.error('Warning: Cannot verify piped exec'); + shell.exit(123); +}