Skip to content

Commit

Permalink
Merge pull request #379 from shelljs/feat-head-sort-commands
Browse files Browse the repository at this point in the history
New commands: sort(), head(), and tail()
  • Loading branch information
nfischer committed Apr 1, 2016
2 parents 16cb1af + 60d6301 commit 2984b40
Show file tree
Hide file tree
Showing 18 changed files with 763 additions and 2 deletions.
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
12 changes: 12 additions & 0 deletions shell.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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});
Expand Down
2 changes: 1 addition & 1 deletion src/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
96 changes: 96 additions & 0 deletions src/head.js
Original file line number Diff line number Diff line change
@@ -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 changes: 87 additions & 0 deletions src/sort.js
Original file line number Diff line number Diff line change
@@ -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 changes: 65 additions & 0 deletions src/tail.js
Original file line number Diff line number Diff line change
@@ -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;
3 changes: 2 additions & 1 deletion test/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 2984b40

Please sign in to comment.