Merge pull request #379 from shelljs/feat-head-sort-commands

New commands: sort(), head(), and tail()
This commit is contained in:
Nate Fischer 2016-03-31 21:22:11 -07:00
commit 2984b40b12
18 changed files with 763 additions and 2 deletions

View File

@ -302,6 +302,36 @@ containing the files if more than one file is given (a new line character is
introduced between each file).
### head([{'-n', \<num\>},] file [, file ...])
### head([{'-n', \<num\>},] file_array)
Examples:
```javascript
var str = head({'-n', 1}, 'file*.txt');
var str = head('file1', 'file2');
var str = head(['file1', 'file2']); // same as above
```
Output the first 10 lines of a file (or the first `<num>` if `-n` is
specified)
### tail([{'-n', \<num\>},] file [, file ...])
### tail([{'-n', \<num\>},] file_array)
Examples:
```javascript
var str = tail({'-n', 1}, 'file*.txt');
var str = tail('file1', 'file2');
var str = tail(['file1', 'file2']); // same as above
```
Output the last 10 lines of a file (or the last `<num>` if `-n` is
specified)
### ShellString.prototype.to(file)
Examples:
@ -344,6 +374,24 @@ Reads an input string from `files` and performs a JavaScript `replace()` on the
using the given search regex and replacement string or function. Returns the new string after replacement.
### sort([options,] file [, file ...])
### sort([options,] file_array)
Available options:
+ `-r`: Reverse the result of comparisons
+ `-n`: Compare according to numerical value
Examples:
```javascript
sort('foo.txt', 'bar.txt');
sort('-r', 'foo.txt');
```
Return the contents of the files, sorted line-by-line. Sorting multiple
files mixes their content, just like unix sort does.
### grep([options,] regex_filter, file [, file ...])
### grep([options,] regex_filter, file_array)
Available options:

View File

@ -58,6 +58,14 @@ exports.test = common.wrap('test', _test);
var _cat = require('./src/cat');
exports.cat = common.wrap('cat', _cat, {idx: 1});
//@include ./src/head
var _head = require('./src/head');
exports.head = common.wrap('head', _head, {idx: 1});
//@include ./src/tail
var _tail = require('./src/tail');
exports.tail = common.wrap('tail', _tail, {idx: 1});
// The below commands have been moved to common.ShellString(), and are only here
// for generating the docs
//@include ./src/to
@ -67,6 +75,10 @@ exports.cat = common.wrap('cat', _cat, {idx: 1});
var _sed = require('./src/sed');
exports.sed = common.wrap('sed', _sed, {idx: 3});
//@include ./src/sort
var _sort = require('./src/sort');
exports.sort = common.wrap('sort', _sort, {idx: 1});
//@include ./src/grep
var _grep = require('./src/grep');
exports.grep = common.wrap('grep', _grep, {idx: 2});

View File

@ -101,7 +101,7 @@ var ShellString = function (stdout, stderr, code) {
that.code = code;
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) {
['cat', 'head', 'sed', 'sort', 'tail', 'grep', 'exec'].forEach(function (cmd) {
that[cmd] = function() {return shell[cmd].apply(that.stdout, arguments);};
});
return that;

96
src/head.js Normal file
View File

@ -0,0 +1,96 @@
var common = require('./common');
var fs = require('fs');
// This reads n or more lines, or the entire file, whichever is less.
function readSomeLines(file, numLines) {
var BUF_LENGTH = 64*1024,
buf = new Buffer(BUF_LENGTH),
bytesRead = BUF_LENGTH,
pos = 0,
fdr = null;
try {
fdr = fs.openSync(file, 'r');
} catch(e) {
common.error('cannot read file: ' + file);
}
var numLinesRead = 0;
var ret = '';
while (bytesRead === BUF_LENGTH && numLinesRead < numLines) {
bytesRead = fs.readSync(fdr, buf, 0, BUF_LENGTH, pos);
var bufStr = buf.toString('utf8', 0, bytesRead);
numLinesRead += bufStr.split('\n').length - 1;
ret += bufStr;
pos += bytesRead;
}
fs.closeSync(fdr);
return ret;
}
//@
//@ ### head([{'-n', \<num\>},] file [, file ...])
//@ ### head([{'-n', \<num\>},] file_array)
//@
//@ Examples:
//@
//@ ```javascript
//@ var str = head({'-n', 1}, 'file*.txt');
//@ var str = head('file1', 'file2');
//@ var str = head(['file1', 'file2']); // same as above
//@ ```
//@
//@ Output the first 10 lines of a file (or the first `<num>` if `-n` is
//@ specified)
function _head(options, files) {
options = common.parseOptions(options, {
'n': 'numLines'
});
var head = [];
var pipe = common.readFromPipe(this);
if (!files && !pipe)
common.error('no paths given');
var idx = 1;
if (options.numLines === true) {
idx = 2;
options.numLines = Number(arguments[1]);
} else if (options.numLines === false) {
options.numLines = 10;
}
files = [].slice.call(arguments, idx);
if (pipe)
files.unshift('-');
var shouldAppendNewline = false;
files.forEach(function(file) {
if (!fs.existsSync(file) && file !== '-') {
common.error('no such file or directory: ' + file, true);
return;
}
var contents;
if (file === '-')
contents = pipe;
else if (options.numLines < 0) {
contents = fs.readFileSync(file, 'utf8');
} else {
contents = readSomeLines(file, options.numLines);
}
var lines = contents.split('\n');
var hasTrailingNewline = (lines[lines.length-1] === '');
if (hasTrailingNewline)
lines.pop();
shouldAppendNewline = (hasTrailingNewline || options.numLines < lines.length);
head = head.concat(lines.slice(0, options.numLines));
});
if (shouldAppendNewline)
head.push(''); // to add a trailing newline once we join
return new common.ShellString(head.join('\n'), common.state.error, common.state.errorCode);
}
module.exports = _head;

87
src/sort.js Normal file
View File

@ -0,0 +1,87 @@
var common = require('./common');
var fs = require('fs');
// parse out the number prefix of a line
function parseNumber (str) {
var match = str.match(/^\s*(\d*)\s*(.*)$/);
return {num: Number(match[1]), value: match[2]};
}
// compare two strings case-insensitively, but examine case for strings that are
// case-insensitive equivalent
function unixCmp(a, b) {
var aLower = a.toLowerCase();
var bLower = b.toLowerCase();
return (aLower === bLower ?
-1 * a.localeCompare(b) : // unix sort treats case opposite how javascript does
aLower.localeCompare(bLower));
}
// compare two strings in the fashion that unix sort's -n option works
function numericalCmp(a, b) {
var objA = parseNumber(a);
var objB = parseNumber(b);
if (objA.hasOwnProperty('num') && objB.hasOwnProperty('num')) {
return ((objA.num !== objB.num) ?
(objA.num - objB.num) :
unixCmp(objA.value, objB.value));
} else {
return unixCmp(objA.value, objB.value);
}
}
//@
//@ ### sort([options,] file [, file ...])
//@ ### sort([options,] file_array)
//@ Available options:
//@
//@ + `-r`: Reverse the result of comparisons
//@ + `-n`: Compare according to numerical value
//@
//@ Examples:
//@
//@ ```javascript
//@ sort('foo.txt', 'bar.txt');
//@ sort('-r', 'foo.txt');
//@ ```
//@
//@ Return the contents of the files, sorted line-by-line. Sorting multiple
//@ files mixes their content, just like unix sort does.
function _sort(options, files) {
options = common.parseOptions(options, {
'r': 'reverse',
'n': 'numerical'
});
// Check if this is coming from a pipe
var pipe = common.readFromPipe(this);
if (!files && !pipe)
common.error('no files given');
files = [].slice.call(arguments, 1);
if (pipe)
files.unshift('-');
var lines = [];
files.forEach(function(file) {
if (!fs.existsSync(file) && file !== '-') {
// exit upon any sort of error
common.error('no such file or directory: ' + file);
}
var contents = file === '-' ? pipe : fs.readFileSync(file, 'utf8');
lines = lines.concat(contents.trimRight().split(/\r*\n/));
});
var sorted;
sorted = lines.sort(options.numerical ? numericalCmp : unixCmp);
if (options.reverse)
sorted = sorted.reverse();
return new common.ShellString(sorted.join('\n')+'\n', common.state.error, common.state.errorCode);
}
module.exports = _sort;

65
src/tail.js Normal file
View File

@ -0,0 +1,65 @@
var common = require('./common');
var fs = require('fs');
//@
//@ ### tail([{'-n', \<num\>},] file [, file ...])
//@ ### tail([{'-n', \<num\>},] file_array)
//@
//@ Examples:
//@
//@ ```javascript
//@ var str = tail({'-n', 1}, 'file*.txt');
//@ var str = tail('file1', 'file2');
//@ var str = tail(['file1', 'file2']); // same as above
//@ ```
//@
//@ Output the last 10 lines of a file (or the last `<num>` if `-n` is
//@ specified)
function _tail(options, files) {
options = common.parseOptions(options, {
'n': 'numLines'
});
var tail = [];
var pipe = common.readFromPipe(this);
if (!files && !pipe)
common.error('no paths given');
var idx = 1;
if (options.numLines === true) {
idx = 2;
options.numLines = Number(arguments[1]);
} else if (options.numLines === false) {
options.numLines = 10;
}
options.numLines = -1 * Math.abs(options.numLines);
files = [].slice.call(arguments, idx);
if (pipe)
files.unshift('-');
var shouldAppendNewline = false;
files.forEach(function(file) {
if (!fs.existsSync(file) && file !== '-') {
common.error('no such file or directory: ' + file, true);
return;
}
var contents = file === '-' ? pipe : fs.readFileSync(file, 'utf8');
var lines = contents.split('\n');
if (lines[lines.length-1] === '') {
lines.pop();
shouldAppendNewline = true;
} else {
shouldAppendNewline = false;
}
tail = tail.concat(lines.slice(options.numLines));
});
if (shouldAppendNewline)
tail.push(''); // to add a trailing newline once we join
return new common.ShellString(tail.join('\n'), common.state.error, common.state.errorCode);
}
module.exports = _tail;

View File

@ -52,10 +52,11 @@ child.exec(JSON.stringify(process.execPath)+' '+file, function(err, stdout) {
// Expands to directories by default
var result = common.expand(['resources/*a*']);
assert.equal(result.length, 4);
assert.equal(result.length, 5);
assert.ok(result.indexOf('resources/a.txt') > -1);
assert.ok(result.indexOf('resources/badlink') > -1);
assert.ok(result.indexOf('resources/cat') > -1);
assert.ok(result.indexOf('resources/head') > -1);
assert.ok(result.indexOf('resources/external') > -1);
// Check to make sure options get passed through (nodir is an example)

103
test/head.js Normal file
View File

@ -0,0 +1,103 @@
var shell = require('..');
var assert = require('assert');
var fs = require('fs');
shell.config.silent = true;
shell.rm('-rf', 'tmp');
shell.mkdir('tmp');
var result;
//
// Invalids
//
result = shell.head();
assert.ok(shell.error());
assert.equal(result.code, 1);
assert.equal(fs.existsSync('/asdfasdf'), false); // sanity check
result = shell.head('/adsfasdf'); // file does not exist
assert.ok(shell.error());
assert.equal(result.code, 1);
//
// Valids
//
var topOfFile1 = ['file1 1', 'file1 2', 'file1 3', 'file1 4', 'file1 5',
'file1 6', 'file1 7', 'file1 8', 'file1 9', 'file1 10',
'file1 11', 'file1 12', 'file1 13', 'file1 14', 'file1 15',
'file1 16', 'file1 17', 'file1 18', 'file1 19', 'file1 20'];
var topOfFile2 = ['file2 1', 'file2 2', 'file2 3', 'file2 4', 'file2 5',
'file2 6', 'file2 7', 'file2 8', 'file2 9', 'file2 10',
'file2 11', 'file2 12', 'file2 13', 'file2 14', 'file2 15',
'file2 16', 'file2 17', 'file2 18', 'file2 19', 'file2 20'];
// simple
result = shell.head('resources/head/file1.txt');
assert.equal(shell.error(), null);
assert.equal(result.code, 0);
assert.equal(result, topOfFile1.slice(0, 10).join('\n')+'\n');
// multiple files
result = shell.head('resources/head/file2.txt', 'resources/head/file1.txt');
assert.equal(shell.error(), null);
assert.equal(result.code, 0);
assert.equal(result, topOfFile2.slice(0, 10).concat(
topOfFile1.slice(0, 10)
).join('\n')+'\n');
// multiple files, array syntax
result = shell.head(['resources/head/file2.txt', 'resources/head/file1.txt']);
assert.equal(shell.error(), null);
assert.equal(result.code, 0);
assert.equal(result, topOfFile2.slice(0, 10).concat(
topOfFile1.slice(0, 10)
).join('\n')+'\n');
// reading more lines than are in the file (no trailing newline)
result = shell.head('resources/file2', 'resources/file1');
assert.equal(shell.error(), null);
assert.equal(result.code, 0);
assert.equal(result, 'test2\ntest1'); // these files only have one line (no \n)
// reading more lines than are in the file (with trailing newline)
result = shell.head('resources/head/shortfile2', 'resources/head/shortfile1');
assert.equal(shell.error(), null);
assert.equal(result.code, 0);
assert.equal(result, 'short2\nshort1\n'); // these files only have one line (with \n)
// Globbed file
result = shell.head('resources/head/file?.txt');
assert.equal(shell.error(), null);
assert.equal(result.code, 0);
assert.equal(result, topOfFile1.slice(0, 10).concat(
topOfFile2.slice(0, 10)
).join('\n')+'\n');
// With `'-n' <num>` option
result = shell.head('-n', 4, 'resources/head/file2.txt', 'resources/head/file1.txt');
assert.equal(shell.error(), null);
assert.equal(result.code, 0);
assert.equal(result, topOfFile2.slice(0, 4).concat(
topOfFile1.slice(0, 4)
).join('\n')+'\n');
// With `{'-n': <num>}` option
result = shell.head({'-n': 4}, 'resources/head/file2.txt', 'resources/head/file1.txt');
assert.equal(shell.error(), null);
assert.equal(result.code, 0);
assert.equal(result, topOfFile2.slice(0, 4).concat(
topOfFile1.slice(0, 4)
).join('\n')+'\n');
// negative values (-num) are the same as (numLines - num)
result = shell.head('-n', -46, 'resources/head/file1.txt');
assert.equal(shell.error(), null);
assert.equal(result.code, 0);
assert.equal(result, 'file1 1\nfile1 2\nfile1 3\nfile1 4\n');
shell.exit(123);

View File

@ -0,0 +1,50 @@
file1 1
file1 2
file1 3
file1 4
file1 5
file1 6
file1 7
file1 8
file1 9
file1 10
file1 11
file1 12
file1 13
file1 14
file1 15
file1 16
file1 17
file1 18
file1 19
file1 20
file1 21
file1 22
file1 23
file1 24
file1 25
file1 26
file1 27
file1 28
file1 29
file1 30
file1 31
file1 32
file1 33
file1 34
file1 35
file1 36
file1 37
file1 38
file1 39
file1 40
file1 41
file1 42
file1 43
file1 44
file1 45
file1 46
file1 47
file1 48
file1 49
file1 50

View File

@ -0,0 +1,50 @@
file2 1
file2 2
file2 3
file2 4
file2 5
file2 6
file2 7
file2 8
file2 9
file2 10
file2 11
file2 12
file2 13
file2 14
file2 15
file2 16
file2 17
file2 18
file2 19
file2 20
file2 21
file2 22
file2 23
file2 24
file2 25
file2 26
file2 27
file2 28
file2 29
file2 30
file2 31
file2 32
file2 33
file2 34
file2 35
file2 36
file2 37
file2 38
file2 39
file2 40
file2 41
file2 42
file2 43
file2 44
file2 45
file2 46
file2 47
file2 48
file2 49
file2 50

View File

@ -0,0 +1 @@
short1

View File

@ -0,0 +1 @@
short2

12
test/resources/sort/file1 Normal file
View File

@ -0,0 +1,12 @@
22
symbolic
46 integers
melt
admiral
aardvark
scanner
Dynamite
Witness
12345
blackwater
5 numbers

12
test/resources/sort/file2 Normal file
View File

@ -0,0 +1,12 @@
admiral
scanner
5 numbers
Witness
46 integers
12345
Dynamite
blackwater
aardvark
22
symbolic
melt

View File

@ -0,0 +1,12 @@
12345
22
46 integers
5 numbers
aardvark
admiral
blackwater
Dynamite
melt
scanner
symbolic
Witness

View File

@ -0,0 +1,12 @@
aardvark
admiral
blackwater
Dynamite
melt
scanner
symbolic
Witness
5 numbers
22
46 integers
12345

94
test/sort.js Normal file
View File

@ -0,0 +1,94 @@
var shell = require('..');
var assert = require('assert'),
fs = require('fs');
shell.config.silent = true;
shell.rm('-rf', 'tmp');
shell.mkdir('tmp');
var result;
var doubleSorted = shell.cat('resources/sort/sorted')
.trimRight()
.split('\n')
.reduce(function(prev, cur) {
return prev.concat([cur, cur]);
}, [])
.join('\n') + '\n';
//
// Invalids
//
result = shell.sort();
assert.ok(shell.error());
assert.ok(result.code);
assert.equal(fs.existsSync('/asdfasdf'), false); // sanity check
result = shell.sort('/adsfasdf'); // file does not exist
assert.ok(shell.error());
assert.ok(result.code);
//
// Valids
//
// simple
result = shell.sort('resources/sort/file1');
assert.equal(shell.error(), null);
assert.equal(result.code, 0);
assert.equal(result + '', shell.cat('resources/sort/sorted'));
// simple
result = shell.sort('resources/sort/file2');
assert.equal(shell.error(), null);
assert.equal(result.code, 0);
assert.equal(result + '', shell.cat('resources/sort/sorted'));
// multiple files
result = shell.sort('resources/sort/file2', 'resources/sort/file1');
assert.equal(shell.error(), null);
assert.equal(result.code, 0);
assert.equal(result + '', doubleSorted);
// multiple files, array syntax
result = shell.sort(['resources/sort/file2', 'resources/sort/file1']);
assert.equal(shell.error(), null);
assert.equal(result.code, 0);
assert.equal(result + '', doubleSorted);
// Globbed file
result = shell.sort('resources/sort/file?');
assert.equal(shell.error(), null);
assert.equal(result.code, 0);
assert.equal(result + '', doubleSorted);
// With '-n' option
result = shell.sort('-n', 'resources/sort/file2');
assert.equal(shell.error(), null);
assert.equal(result.code, 0);
assert.equal(result + '', shell.cat('resources/sort/sortedDashN'));
// With '-r' option
result = shell.sort('-r', 'resources/sort/file2');
assert.equal(shell.error(), null);
assert.equal(result.code, 0);
assert.equal(result + '', shell.cat('resources/sort/sorted')
.trimRight()
.split('\n')
.reverse()
.join('\n') + '\n');
// With '-rn' option
result = shell.sort('-rn', 'resources/sort/file2');
assert.equal(shell.error(), null);
assert.equal(result.code, 0);
assert.equal(result + '', shell.cat('resources/sort/sortedDashN')
.trimRight()
.split('\n')
.reverse()
.join('\n') + '\n');
shell.exit(123);

105
test/tail.js Normal file
View File

@ -0,0 +1,105 @@
var shell = require('..');
var assert = require('assert');
var fs = require('fs');
shell.config.silent = true;
shell.rm('-rf', 'tmp');
shell.mkdir('tmp');
var result;
//
// Invalids
//
result = shell.tail();
assert.ok(shell.error());
assert.equal(result.code, 1);
assert.equal(fs.existsSync('/asdfasdf'), false); // sanity check
result = shell.tail('/adsfasdf'); // file does not exist
assert.ok(shell.error());
assert.equal(result.code, 1);
//
// Valids
//
var bottomOfFile1 = ['file1 50', 'file1 49', 'file1 48', 'file1 47', 'file1 46',
'file1 45', 'file1 44', 'file1 43', 'file1 42', 'file1 41',
'file1 40', 'file1 39', 'file1 38', 'file1 37', 'file1 36',
'file1 35', 'file1 34', 'file1 33', 'file1 32', 'file1 31'];
var bottomOfFile2 = ['file2 50', 'file2 49', 'file2 48', 'file2 47', 'file2 46',
'file2 45', 'file2 44', 'file2 43', 'file2 42', 'file2 41',
'file2 40', 'file2 39', 'file2 38', 'file2 37', 'file2 36',
'file2 35', 'file2 34', 'file2 33', 'file2 32', 'file2 31'];
// simple
result = shell.tail('resources/head/file1.txt');
assert.equal(shell.error(), null);
assert.equal(result.code, 0);
assert.equal(result, bottomOfFile1.slice(0, 10).reverse().join('\n')+'\n');
// multiple files
result = shell.tail('resources/head/file2.txt', 'resources/head/file1.txt');
assert.equal(shell.error(), null);
assert.equal(result.code, 0);
assert.equal(result, bottomOfFile2.slice(0, 10).reverse().concat(
bottomOfFile1.slice(0, 10).reverse()
).join('\n')+'\n');
// multiple files, array syntax
result = shell.tail(['resources/head/file2.txt', 'resources/head/file1.txt']);
assert.equal(shell.error(), null);
assert.equal(result.code, 0);
assert.equal(result, bottomOfFile2.slice(0, 10).reverse().concat(
bottomOfFile1.slice(0, 10).reverse()
).join('\n')+'\n');
// reading more lines than are in the file (no trailing newline)
result = shell.tail('resources/file2', 'resources/file1');
assert.equal(shell.error(), null);
assert.equal(result.code, 0);
assert.equal(result, 'test2\ntest1'); // these files only have one line (no \n)
// reading more lines than are in the file (with trailing newline)
result = shell.tail('resources/head/shortfile2', 'resources/head/shortfile1');
assert.equal(shell.error(), null);
assert.equal(result.code, 0);
assert.equal(result, 'short2\nshort1\n'); // these files only have one line (with \n)
// Globbed file
result = shell.tail('resources/head/file?.txt');
assert.equal(shell.error(), null);
assert.equal(result.code, 0);
assert.equal(result, bottomOfFile1.slice(0, 10).reverse().concat(
bottomOfFile2.slice(0, 10).reverse()
).join('\n')+'\n');
// With `'-n' <num>` option
result = shell.tail('-n', 4, 'resources/head/file2.txt', 'resources/head/file1.txt');
assert.equal(shell.error(), null);
assert.equal(result.code, 0);
assert.equal(result, bottomOfFile2.slice(0, 4).reverse().concat(
bottomOfFile1.slice(0, 4).reverse()
).join('\n')+'\n');
// With `{'-n': <num>}` option
result = shell.tail({'-n': 4}, 'resources/head/file2.txt', 'resources/head/file1.txt');
assert.equal(shell.error(), null);
assert.equal(result.code, 0);
assert.equal(result, bottomOfFile2.slice(0, 4).reverse().concat(
bottomOfFile1.slice(0, 4).reverse()
).join('\n')+'\n');
// negative values are the same as positive values
result = shell.tail('-n', -4, 'resources/head/file2.txt', 'resources/head/file1.txt');
assert.equal(shell.error(), null);
assert.equal(result.code, 0);
assert.equal(result, bottomOfFile2.slice(0, 4).reverse().concat(
bottomOfFile1.slice(0, 4).reverse()
).join('\n')+'\n');
shell.exit(123);