# ShellJS - Unix shell commands for Node.js [](http://travis-ci.org/arturadib/shelljs)
-_This project is young and experimental. Use at your own risk._
++ _This project is young and experimental. Use at your own risk._
++ _Major API change as of v0.0.4: `ls()` and `find()` now return arrays._
ShellJS is a **portable** (Windows included) implementation of Unix shell commands on top of the Node.js API. You can use it to eliminate your shell script's dependency on Unix while still keeping its familiar and powerful commands.
// Replace macros in each .js file
cd('lib');
-for (file in ls('*.js')) {
+ls('*.js').forEach(function(file) {
sed('-i', 'BUILD_VERSION', 'v0.1.2', file);
sed('-i', /.*REMOVE_THIS_LINE.*\n/, '', file);
sed('-i', /.*REPLACE_LINE_WITH_MACRO.*\n/, cat('macro.js'), file);
-}
+});
cd('..');
// Run external tool synchronously
cd(__dirname);
mkdir('docs');
cd('lib');
- for (file in ls('*.js')) {
+ ls('*.js').forEach(function(file){
var text = grep('//@', file); // extract special comments
text.replace('//@', ''); // remove comment tags
text.to('docs/my_docs.md');
- }
+ });
}
```
ls('-R', ['/users/me', '/tmp']); // same as above
```
-Returns list of files in the given path, or in current directory if no path provided.
-For convenient iteration via `for (file in ls())`, the format returned is a hash object:
-`{ 'file1':null, 'dir1/file2':null, ...}`.
+Returns array of files in the given path, or in current directory if no path provided.
#### find(path [,path ...])
#### find(path_array)
```javascript
find('src', 'lib');
find(['src', 'lib']); // same as above
-for (file in find('.')) {
-if (!file.match(/\.js$/))
-continue;
-// all files at this point end in '.js'
-}
+find('.').filter(function(file) { return file.match(/\.js$/); })
```
-Returns list of all files (however deep) in the given paths. For convenient iteration
-via `for (file in find(...))`, the format returned is a hash object:
-`{ 'file1':null, 'dir1/file2':null, ...}`.
+Returns array of all files (however deep) in the given paths.
-The main difference with respect to `ls('-R', path)` is that the resulting file names
+The main difference from `ls('-R', path)` is that the resulting file names
include the base directories, e.g. `lib/resources/file1` instead of just `file1`.
#### cp('[options ,] source [,source ...], dest')
Searches and returns string containing a writeable, platform-dependent temporary directory.
Follows Python's [tempfile algorithm](http://docs.python.org/library/tempfile.html#tempfile.tempdir).
-#### exists(path [, path ...])
-#### exists(path_array)
-Returns true if all the given paths exist.
-
#### error()
Tests if error occurred in the last command. Returns `null` if no error occurred,
otherwise returns string explaining the error
+#### silent([state])
+Example:
+
+```javascript
+var silentState = silent();
+silent(true);
+/* ... */
+silent(silentState); // restore old silent state
+```
+
+Suppresses all output if `state = true`. Returns state if no arguments given.
+
+## Deprecated
+
+
+#### exists(path [, path ...])
+#### exists(path_array)
+
+_This function is being deprecated. Use `test()` instead._
+
+Returns true if all the given paths exist.
+
#### verbose()
-Enables all output (default)
-#### silent()
-Suppresses all output, except for explict `echo()` calls
+_This function is being deprecated. Use `silent(false) instead.`_
+
+Enables all output (default)
//@ ls('-R', ['/users/me', '/tmp']); // same as above
//@ ```
//@
-//@ Returns list of files in the given path, or in current directory if no path provided.
-//@ For convenient iteration via `for (file in ls())`, the format returned is a hash object:
-//@ `{ 'file1':null, 'dir1/file2':null, ...}`.
+//@ Returns array of files in the given path, or in current directory if no path provided.
function _ls(options, paths) {
options = parseOptions(options, {
'R': 'recursive',
else if (typeof paths === 'string')
paths = [].slice.call(arguments, 1);
- var hash = {};
+ var list = [];
- function pushHash(file, query) {
+ // Conditionally pushes file to list - returns true if pushed, false otherwise
+ // (e.g. prevents hidden files to be included unless explicitly told so)
+ function pushFile(file, query) {
// hidden file?
if (path.basename(file)[0] === '.') {
// not explicitly asking for hidden files?
if (!options.all && !(path.basename(query)[0] === '.' && path.basename(query).length > 1))
- return;
+ return false;
}
- hash[file] = null;
+ if (platform === 'win')
+ file = file.replace(/\\/g, '/');
+
+ list.push(file);
+ return true;
}
paths.forEach(function(p) {
if (fs.existsSync(p)) {
// Simple file?
if (fs.statSync(p).isFile()) {
- pushHash(p, p);
+ pushFile(p, p);
return; // continue
}
if (fs.statSync(p).isDirectory()) {
// Iterate over p contents
fs.readdirSync(p).forEach(function(file) {
- pushHash(file, p);
-
- // Recursive
- var oldDir = _pwd();
- _cd('', p);
- if (fs.statSync(file).isDirectory() && options.recursive)
- hash = extend(hash, _ls('-R', file+'/*'));
- _cd('', oldDir);
+ if (!pushFile(file, p))
+ return;
+
+ // Recursive?
+ if (options.recursive) {
+ var oldDir = _pwd();
+ _cd('', p);
+ if (fs.statSync(file).isDirectory())
+ list = list.concat(_ls('-R'+(options.all?'a':''), file+'/*'));
+ _cd('', oldDir);
+ }
});
return; // continue
}
// Escape special regular expression chars
var regexp = basename.replace(/(\^|\$|\(|\)|\<|\>|\[|\]|\{|\}|\.|\+|\?)/g, '\\$1');
// Translates wildcard into regex
- regexp = '^' + regexp.replace(/\*/g, '.*');
+ regexp = '^' + regexp.replace(/\*/g, '.*') + '$';
// Iterate over directory contents
fs.readdirSync(dirname).forEach(function(file) {
if (file.match(new RegExp(regexp))) {
- pushHash(path.normalize(dirname+'/'+file), basename);
-
- // Recursive
- var pp = dirname + '/' + file;
- if (fs.statSync(pp).isDirectory() && options.recursive)
- hash = extend(hash, _ls('-R', pp+'/*'));
- }
+ if (!pushFile(path.normalize(dirname+'/'+file), basename))
+ return;
+
+ // Recursive?
+ if (options.recursive) {
+ var pp = dirname + '/' + file;
+ if (fs.statSync(pp).isDirectory())
+ list = list.concat(_ls('-R'+(options.all?'a':''), pp+'/*'));
+ } // recursive
+ } // if file matches
}); // forEach
return;
}
error('no such file or directory: ' + p, true);
});
- return hash;
+ return list;
};
exports.ls = wrap('ls', _ls);
//@ ```javascript
//@ find('src', 'lib');
//@ find(['src', 'lib']); // same as above
-//@ for (file in find('.')) {
-//@ if (!file.match(/\.js$/))
-//@ continue;
-//@ // all files at this point end in '.js'
-//@ }
+//@ find('.').filter(function(file) { return file.match(/\.js$/); });
//@ ```
//@
-//@ Returns list of all files (however deep) in the given paths. For convenient iteration
-//@ via `for (file in find(...))`, the format returned is a hash object:
-//@ `{ 'file1':null, 'dir1/file2':null, ...}`.
+//@ Returns array of all files (however deep) in the given paths.
//@
//@ The main difference from `ls('-R', path)` is that the resulting file names
//@ include the base directories, e.g. `lib/resources/file1` instead of just `file1`.
else if (typeof paths === 'string')
paths = [].slice.call(arguments, 1);
- var hash = {};
+ var list = [];
+
+ function pushFile(file) {
+ if (platform === 'win')
+ file = file.replace(/\\/g, '/');
+ list.push(file);
+ }
// why not simply do ls('-R', paths)? because the output wouldn't give the base dirs
// to get the base dir in the output, we need instead ls('-R', 'dir/*') for every directory
- paths.forEach(function(file){
- hash[file] = null;
+ paths.forEach(function(file) {
+ pushFile(file);
if (fs.statSync(file).isDirectory()) {
- for (subfile in _ls('-Ra', file+'/*'))
- hash[subfile] = null;
+ _ls('-Ra', file+'/*').forEach(function(subfile) {
+ pushFile(subfile);
+ });
}
});
- return hash;
+ return list;
}
exports.find = wrap('find', _find);
// Remove simple file
if (fs.statSync(file).isFile()) {
- fs.unlinkSync(file);
+
+ // Do not check for file writing permissions
+ if (options.force) {
+ _unlinkSync(file);
+ return;
+ }
+
+ if (isWriteable(file))
+ _unlinkSync(file);
+ else
+ error('permission denied: '+file, true);
+
return;
- }
+ } // simple file
// Path is an existing directory, but no -r flag given
if (fs.statSync(file).isDirectory() && !options.recursive) {
// Recursively remove existing directory
if (fs.statSync(file).isDirectory() && options.recursive) {
- rmdirSyncRecursive(file);
+ rmdirSyncRecursive(file, options.force);
}
}); // forEach(file)
}; // rm
if (!fs.existsSync( path.dirname(file) ))
error('no such file or directory: ' + path.dirname(file));
- fs.writeFileSync(file, this.toString(), 'utf8');
+ try {
+ fs.writeFileSync(file, this.toString(), 'utf8');
+ } catch(e) {
+ error('could not write to file (code '+e.code+'): '+file, true);
+ }
};
// In the future, when Proxies are default, we can add methods like `.to()` to primitive strings.
// For now, this is a dummy function to bookmark places we need such strings
}
options = extend({
- silent: false,
+ silent: state.silent,
async: false
}, options);
//@ Follows Python's [tempfile algorithm](http://docs.python.org/library/tempfile.html#tempfile.tempdir).
exports.tempdir = wrap('tempdir', tempDir);
+
+//@
+//@ #### error()
+//@ Tests if error occurred in the last command. Returns `null` if no error occurred,
+//@ otherwise returns string explaining the error
+exports.error = function() {
+ return state.error;
+}
+
+//@
+//@ #### silent([state])
+//@ Example:
+//@
+//@ ```javascript
+//@ var silentState = silent();
+//@ silent(true);
+//@ /* ... */
+//@ silent(silentState); // restore old silent state
+//@ ```
+//@
+//@ Suppresses all output if `state = true`. Returns state if no arguments given.
+exports.silent = function(_state) {
+ if (typeof _state !== 'boolean')
+ return state.silent;
+
+ state.silent = _state;
+}
+
+
+//@
+//@ ## Deprecated
+//@
+
+
+
+
//@
//@ #### exists(path [, path ...])
//@ #### exists(path_array)
+//@
+//@ _This function is being deprecated. Use `test()` instead._
+//@
//@ Returns true if all the given paths exist.
function _exists(options, paths) {
+ deprecate('exists', 'Use test() instead.');
+
if (!paths)
error('no paths given');
};
exports.exists = wrap('exists', _exists);
-//@
-//@ #### error()
-//@ Tests if error occurred in the last command. Returns `null` if no error occurred,
-//@ otherwise returns string explaining the error
-exports.error = function() {
- return state.error;
-}
//@
//@ #### verbose()
+//@
+//@ _This function is being deprecated. Use `silent(false) instead.`_
+//@
//@ Enables all output (default)
exports.verbose = function() {
- state.silent = false;
-}
+ deprecate('verbose', 'Use silent(false) instead.');
-//@
-//@ #### silent()
-//@ Suppresses all output, except for explict `echo()` calls
-exports.silent = function() {
- state.silent = true;
+ state.silent = false;
}
-
-
-
-
////////////////////////////////////////////////////////////////////////////////////////////////
//
// Auxiliary functions (internal use only)
console.log.apply(this, arguments);
}
+function deprecate(what, msg) {
+ console.log('*** ShellJS.'+what+': This function is deprecated.', msg);
+}
+
function write(msg) {
if (!state.silent)
process.stdout.write(msg);
var BUF_LENGTH = 64*1024,
buf = new Buffer(BUF_LENGTH),
- fdr = fs.openSync(srcFile, 'r'),
- fdw = fs.openSync(destFile, 'w'),
bytesRead = BUF_LENGTH,
- pos = 0;
+ pos = 0,
+ fdr = null,
+ fdw = null;
+
+ try {
+ fdr = fs.openSync(srcFile, 'r');
+ } catch(e) {
+ error('copyFileSync: could not read src file ('+srcFile+')');
+ }
+
+ try {
+ fdw = fs.openSync(destFile, 'w');
+ } catch(e) {
+ error('copyFileSync: could not write to dest file (code='+e.code+'):'+destFile);
+ }
while (bytesRead === BUF_LENGTH) {
bytesRead = fs.readSync(fdr, buf, 0, BUF_LENGTH, pos);
//
// Licensed under the MIT License
// http://www.opensource.org/licenses/mit-license.php
-function rmdirSyncRecursive(dir) {
+function rmdirSyncRecursive(dir, force) {
var files;
files = fs.readdirSync(dir);
// Loop through and delete everything in the sub-tree after checking it
for(var i = 0; i < files.length; i++) {
- var currFile = fs.lstatSync(dir + "/" + files[i]);
+ var file = dir + "/" + files[i],
+ currFile = fs.lstatSync(file);
- if(currFile.isDirectory()) // Recursive function back to the beginning
- rmdirSyncRecursive(dir + "/" + files[i]);
+ if(currFile.isDirectory()) { // Recursive function back to the beginning
+ rmdirSyncRecursive(file, force);
+ }
- else if(currFile.isSymbolicLink()) // Unlink symlinks
- fs.unlinkSync(dir + "/" + files[i]);
+ else if(currFile.isSymbolicLink()) { // Unlink symlinks
+ if (force || isWriteable(file))
+ _unlinkSync(file);
+ }
else // Assume it's a file - perhaps a try/catch belongs here?
- fs.unlinkSync(dir + "/" + files[i]);
+ if (force || isWriteable(file))
+ _unlinkSync(file);
}
// Now that we know everything in the sub-tree has been deleted, we can delete the main directory.
// Huzzah for the shopkeep.
- return fs.rmdirSync(dir);
+
+ var result;
+ try {
+ result = fs.rmdirSync(dir);
+ } catch(e) {
+ if (e.code === 'ENOTEMPTY')
+ error('directory not empty: ' + dir, true);
+ }
+
+ return result;
}; // rmdirSyncRecursive
// Recursively creates 'dir'
var testFile = dir+'/'+randomFileName();
try {
fs.writeFileSync(testFile, ' ');
- fs.unlinkSync(testFile);
+ _unlinkSync(testFile);
return dir;
} catch (e) {
return false;
// Wrapper around exec() to enable echoing output to console in real time
function execAsync(cmd, opts, callback) {
- var output = '';
+ var output = '',
+ silent = 'silent' in opts ? opts.silent : state.silent;
var c = child.exec(cmd, {env: process.env}, function(err) {
if (callback)
c.stdout.on('data', function(data) {
output += data;
- if (!opts.silent)
+ if (!silent)
write(data);
});
c.stderr.on('data', function(data) {
output += data;
- if (!opts.silent)
+ if (!silent)
write(data);
});
}
function execSync(cmd, opts) {
var stdoutFile = path.resolve(tempDir()+'/'+randomFileName()),
codeFile = path.resolve(tempDir()+'/'+randomFileName()),
- scriptFile = path.resolve(tempDir()+'/'+randomFileName());
+ scriptFile = path.resolve(tempDir()+'/'+randomFileName()),
+ sleepFile = path.resolve(tempDir()+'/'+randomFileName());
var options = extend({
- silent: false
+ silent: state.silent
}, opts);
var previousStdoutContent = '';
// Echoes stdout changes from running process, if not silent
function updateStdout() {
- if (state.silent || options.silent || !fs.existsSync(stdoutFile))
+ if (options.silent || !fs.existsSync(stdoutFile))
return;
var stdoutContent = fs.readFileSync(stdoutFile, 'utf8');
fs.writeFileSync('"+escape(codeFile)+"', err ? err.code.toString() : '0'); \
});";
- if (fs.existsSync(scriptFile)) fs.unlinkSync(scriptFile);
- if (fs.existsSync(stdoutFile)) fs.unlinkSync(stdoutFile);
- if (fs.existsSync(codeFile)) fs.unlinkSync(codeFile);
+ if (fs.existsSync(scriptFile)) _unlinkSync(scriptFile);
+ if (fs.existsSync(stdoutFile)) _unlinkSync(stdoutFile);
+ if (fs.existsSync(codeFile)) _unlinkSync(codeFile);
fs.writeFileSync(scriptFile, script);
child.exec('node '+scriptFile, {
});
// The wait loop
- while (!fs.existsSync(codeFile)) { updateStdout(); };
- while (!fs.existsSync(stdoutFile)) { updateStdout(); };
+ // sleepFile is used as a dummy I/O op to mitigate unnecessary CPU usage
+ // (tried many I/O sync ops, writeFileSync() seems to be only one that is effective in reducing
+ // CPU usage, though apparently not so much on Windows)
+ while (!fs.existsSync(codeFile)) { updateStdout(); fs.writeFileSync(sleepFile, 'a'); };
+ while (!fs.existsSync(stdoutFile)) { updateStdout(); fs.writeFileSync(sleepFile, 'a'); };
// At this point codeFile exists, but it's not necessarily flushed yet.
// Keep reading it until it is.
var stdout = fs.readFileSync(stdoutFile, 'utf8');
- fs.unlinkSync(scriptFile);
- fs.unlinkSync(stdoutFile);
- fs.unlinkSync(codeFile);
+ _unlinkSync(scriptFile);
+ _unlinkSync(stdoutFile);
+ _unlinkSync(codeFile);
+ _unlinkSync(sleepFile);
// True if successful, false if not
var obj = {
list.forEach(function(listEl) {
// Wildcard present?
if (listEl.search(/\*/) > -1) {
- for (file in _ls('', listEl))
+ _ls('', listEl).forEach(function(file) {
expanded.push(file);
+ });
} else {
expanded.push(listEl);
}
return target;
}
+
+// Normalizes _unlinkSync() across platforms to match Unix behavior, i.e.
+// file can be unlinked even if it's read-only, see joyent/node#3006
+function _unlinkSync(file) {
+ try {
+ fs.unlinkSync(file);
+ } catch(e) {
+ // Try to override file permission
+ if (e.code === 'EPERM') {
+ fs.chmodSync(file, '0666');
+ fs.unlinkSync(file);
+ } else {
+ throw e;
+ }
+ }
+}
+
+// Hack to determine if file has write permissions for current user
+// Avoids having to check user, group, etc, but it's probably slow
+function isWriteable(file) {
+ var writePermission = true;
+ try {
+ var __fd = fs.openSync(file, 'a');
+ fs.closeSync(__fd);
+ } catch(e) {
+ writePermission = false;
+ }
+
+ return writePermission;
+}