You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1421 lines
37 KiB
1421 lines
37 KiB
// |
|
// ShellJS |
|
// Unix shell commands on top of Node's API |
|
// |
|
// Copyright (c) 2012 Artur Adib |
|
// http://github.com/arturadib/shelljs |
|
// |
|
|
|
var fs = require('fs'), |
|
path = require('path'), |
|
util = require('util'), |
|
vm = require('vm'), |
|
child = require('child_process'), |
|
os = require('os'); |
|
|
|
// Node shims for < v0.7 |
|
fs.existsSync = fs.existsSync || path.existsSync; |
|
|
|
var state = { |
|
error: null, |
|
fatal: false, |
|
silent: false, |
|
currentCmd: 'shell.js', |
|
tempDir: null |
|
}, |
|
platform = os.type().match(/^Win/) ? 'win' : 'unix'; |
|
|
|
|
|
//@ |
|
//@ All commands run synchronously, unless otherwise stated. |
|
//@ |
|
|
|
|
|
//@ |
|
//@ #### cd('dir') |
|
//@ Changes to directory `dir` for the duration of the script |
|
function _cd(options, dir) { |
|
if (!dir) |
|
error('directory not specified'); |
|
|
|
if (!fs.existsSync(dir)) |
|
error('no such file or directory: ' + dir); |
|
|
|
if (fs.existsSync(dir) && !fs.statSync(dir).isDirectory()) |
|
error('not a directory: ' + dir); |
|
|
|
process.chdir(dir); |
|
}; |
|
exports.cd = wrap('cd', _cd); |
|
|
|
//@ |
|
//@ #### pwd() |
|
//@ Returns the current directory. |
|
function _pwd(options) { |
|
var pwd = path.resolve(process.cwd()); |
|
return ShellString(pwd); |
|
}; |
|
exports.pwd = wrap('pwd', _pwd); |
|
|
|
|
|
//@ |
|
//@ #### ls([options ,] path [,path ...]) |
|
//@ #### ls([options ,] path_array) |
|
//@ Available options: |
|
//@ |
|
//@ + `-R`: recursive |
|
//@ + `-a`: all files (include files beginning with `.`) |
|
//@ |
|
//@ Examples: |
|
//@ |
|
//@ ```javascript |
|
//@ ls('projs/*.js'); |
|
//@ ls('-R', '/users/me', '/tmp'); |
|
//@ ls('-R', ['/users/me', '/tmp']); // same as above |
|
//@ ``` |
|
//@ |
|
//@ 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', |
|
'a': 'all' |
|
}); |
|
|
|
if (!paths) |
|
paths = ['.']; |
|
else if (typeof paths === 'object') |
|
paths = paths; // assume array |
|
else if (typeof paths === 'string') |
|
paths = [].slice.call(arguments, 1); |
|
|
|
var list = []; |
|
|
|
// 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 false; |
|
} |
|
|
|
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()) { |
|
pushFile(p, p); |
|
return; // continue |
|
} |
|
|
|
// Simple dir? |
|
if (fs.statSync(p).isDirectory()) { |
|
// Iterate over p contents |
|
fs.readdirSync(p).forEach(function(file) { |
|
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 |
|
} |
|
} |
|
|
|
// p does not exist - possible wildcard present |
|
|
|
var basename = path.basename(p); |
|
var dirname = path.dirname(p); |
|
// Wildcard present on an existing dir? (e.g. '/tmp/*.js') |
|
if (basename.search(/\*/) > -1 && fs.existsSync(dirname) && fs.statSync(dirname).isDirectory) { |
|
// Escape special regular expression chars |
|
var regexp = basename.replace(/(\^|\$|\(|\)|\<|\>|\[|\]|\{|\}|\.|\+|\?)/g, '\\$1'); |
|
// Translates wildcard into regex |
|
regexp = '^' + regexp.replace(/\*/g, '.*') + '$'; |
|
// Iterate over directory contents |
|
fs.readdirSync(dirname).forEach(function(file) { |
|
if (file.match(new RegExp(regexp))) { |
|
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 list; |
|
}; |
|
exports.ls = wrap('ls', _ls); |
|
|
|
|
|
//@ |
|
//@ #### find(path [,path ...]) |
|
//@ #### find(path_array) |
|
//@ Examples: |
|
//@ |
|
//@ ```javascript |
|
//@ find('src', 'lib'); |
|
//@ find(['src', 'lib']); // same as above |
|
//@ find('.').filter(function(file) { return file.match(/\.js$/); }); |
|
//@ ``` |
|
//@ |
|
//@ 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`. |
|
function _find(options, paths) { |
|
if (!paths) |
|
error('no path specified'); |
|
else if (typeof paths === 'object') |
|
paths = paths; // assume array |
|
else if (typeof paths === 'string') |
|
paths = [].slice.call(arguments, 1); |
|
|
|
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) { |
|
pushFile(file); |
|
|
|
if (fs.statSync(file).isDirectory()) { |
|
_ls('-Ra', file+'/*').forEach(function(subfile) { |
|
pushFile(subfile); |
|
}); |
|
} |
|
}); |
|
|
|
return list; |
|
} |
|
exports.find = wrap('find', _find); |
|
|
|
|
|
//@ |
|
//@ #### cp('[options ,] source [,source ...], dest') |
|
//@ #### cp('[options ,] source_array, dest') |
|
//@ Available options: |
|
//@ |
|
//@ + `-f`: force |
|
//@ + `-r, -R`: recursive |
|
//@ |
|
//@ Examples: |
|
//@ |
|
//@ ```javascript |
|
//@ cp('file1', 'dir1'); |
|
//@ cp('-Rf', '/tmp/*', '/usr/local/*', '/home/tmp'); |
|
//@ cp('-Rf', ['/tmp/*', '/usr/local/*'], '/home/tmp'); // same as above |
|
//@ ``` |
|
//@ |
|
//@ Copies files. The wildcard `*` is accepted. |
|
function _cp(options, sources, dest) { |
|
options = parseOptions(options, { |
|
'f': 'force', |
|
'R': 'recursive', |
|
'r': 'recursive' |
|
}); |
|
|
|
// Get sources, dest |
|
if (arguments.length < 3) { |
|
error('missing <source> and/or <dest>'); |
|
} else if (arguments.length > 3) { |
|
sources = [].slice.call(arguments, 1, arguments.length - 1); |
|
dest = arguments[arguments.length - 1]; |
|
} else if (typeof sources === 'string') { |
|
sources = [sources]; |
|
} else if ('length' in sources) { |
|
sources = sources; // no-op for array |
|
} else { |
|
error('invalid arguments'); |
|
} |
|
|
|
// Dest is not existing dir, but multiple sources given |
|
if ((!fs.existsSync(dest) || !fs.statSync(dest).isDirectory()) && sources.length > 1) |
|
error('dest is not a directory (too many sources)'); |
|
|
|
// Dest is an existing file, but no -f given |
|
if (fs.existsSync(dest) && fs.statSync(dest).isFile() && !options.force) |
|
error('dest file already exists: ' + dest); |
|
|
|
sources = expand(sources); |
|
|
|
sources.forEach(function(src) { |
|
if (!fs.existsSync(src)) { |
|
error('no such file or directory: '+src, true); |
|
return; // skip file |
|
} |
|
|
|
// If here, src exists |
|
|
|
if (fs.statSync(src).isDirectory()) { |
|
if (!options.recursive) { |
|
// Non-Recursive |
|
log(src + ' is a directory (not copied)'); |
|
} else { |
|
// Recursive |
|
// 'cp /a/source dest' should create 'source' in 'dest' |
|
var newDest = dest+'/'+path.basename(src), |
|
checkDir = fs.statSync(src); |
|
try { |
|
fs.mkdirSync(newDest, checkDir.mode); |
|
} catch (e) { |
|
//if the directory already exists, that's okay |
|
if (e.code !== 'EEXIST') throw e; |
|
} |
|
cpdirSyncRecursive(src, newDest, {force: options.force}); |
|
} |
|
return; // done with dir |
|
} |
|
|
|
// If here, src is a file |
|
|
|
// When copying to '/path/dir': |
|
// thisDest = '/path/dir/file1' |
|
var thisDest = dest; |
|
if (fs.existsSync(dest) && fs.statSync(dest).isDirectory()) |
|
thisDest = path.normalize(dest + '/' + path.basename(src)); |
|
|
|
if (fs.existsSync(thisDest) && !options.force) { |
|
error('dest file already exists: ' + thisDest, true); |
|
return; // skip file |
|
} |
|
|
|
copyFileSync(src, thisDest); |
|
}); // forEach(src) |
|
}; // cp |
|
exports.cp = wrap('cp', _cp); |
|
|
|
//@ |
|
//@ #### rm([options ,] file [, file ...]) |
|
//@ #### rm([options ,] file_array) |
|
//@ Available options: |
|
//@ |
|
//@ + `-f`: force |
|
//@ + `-r, -R`: recursive |
|
//@ |
|
//@ Examples: |
|
//@ |
|
//@ ```javascript |
|
//@ rm('-rf', '/tmp/*'); |
|
//@ rm('some_file.txt', 'another_file.txt'); |
|
//@ rm(['some_file.txt', 'another_file.txt']); // same as above |
|
//@ ``` |
|
//@ |
|
//@ Removes files. The wildcard `*` is accepted. |
|
function _rm(options, files) { |
|
options = parseOptions(options, { |
|
'f': 'force', |
|
'r': 'recursive', |
|
'R': 'recursive' |
|
}); |
|
if (!files) |
|
error('no paths given'); |
|
|
|
if (typeof files === 'string') |
|
files = [].slice.call(arguments, 1); |
|
// if it's array leave it as it is |
|
|
|
files = expand(files); |
|
|
|
files.forEach(function(file) { |
|
if (!fs.existsSync(file)) { |
|
// Path does not exist, no force flag given |
|
if (!options.force) |
|
error('no such file or directory: '+file, true); |
|
|
|
return; // skip file |
|
} |
|
|
|
// If here, path exists |
|
|
|
// Remove simple file |
|
if (fs.statSync(file).isFile()) { |
|
|
|
// 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) { |
|
error('path is a directory', true); |
|
return; // skip path |
|
} |
|
|
|
// Recursively remove existing directory |
|
if (fs.statSync(file).isDirectory() && options.recursive) { |
|
rmdirSyncRecursive(file, options.force); |
|
} |
|
}); // forEach(file) |
|
}; // rm |
|
exports.rm = wrap('rm', _rm); |
|
|
|
//@ |
|
//@ #### mv(source [, source ...], dest') |
|
//@ #### mv(source_array, dest') |
|
//@ Available options: |
|
//@ |
|
//@ + `f`: force |
|
//@ |
|
//@ Examples: |
|
//@ |
|
//@ ```javascript |
|
//@ mv('-f', 'file', 'dir/'); |
|
//@ mv('file1', 'file2', 'dir/'); |
|
//@ mv(['file1', 'file2'], 'dir/'); // same as above |
|
//@ ``` |
|
//@ |
|
//@ Moves files. The wildcard `*` is accepted. |
|
function _mv(options, sources, dest) { |
|
options = parseOptions(options, { |
|
'f': 'force' |
|
}); |
|
|
|
// Get sources, dest |
|
if (arguments.length < 3) { |
|
error('missing <source> and/or <dest>'); |
|
} else if (arguments.length > 3) { |
|
sources = [].slice.call(arguments, 1, arguments.length - 1); |
|
dest = arguments[arguments.length - 1]; |
|
} else if (typeof sources === 'string') { |
|
sources = [sources]; |
|
} else if ('length' in sources) { |
|
sources = sources; // no-op for array |
|
} else { |
|
error('invalid arguments'); |
|
} |
|
|
|
sources = expand(sources); |
|
|
|
// Dest is not existing dir, but multiple sources given |
|
if ((!fs.existsSync(dest) || !fs.statSync(dest).isDirectory()) && sources.length > 1) |
|
error('dest is not a directory (too many sources)'); |
|
|
|
// Dest is an existing file, but no -f given |
|
if (fs.existsSync(dest) && fs.statSync(dest).isFile() && !options.force) |
|
error('dest file already exists: ' + dest); |
|
|
|
sources.forEach(function(src) { |
|
if (!fs.existsSync(src)) { |
|
error('no such file or directory: '+src, true); |
|
return; // skip file |
|
} |
|
|
|
// If here, src exists |
|
|
|
// When copying to '/path/dir': |
|
// thisDest = '/path/dir/file1' |
|
var thisDest = dest; |
|
if (fs.existsSync(dest) && fs.statSync(dest).isDirectory()) |
|
thisDest = path.normalize(dest + '/' + path.basename(src)); |
|
|
|
if (fs.existsSync(thisDest) && !options.force) { |
|
error('dest file already exists: ' + thisDest, true); |
|
return; // skip file |
|
} |
|
|
|
if (path.resolve(src) === path.dirname(path.resolve(thisDest))) { |
|
error('cannot move to self: '+src, true); |
|
return; // skip file |
|
} |
|
|
|
fs.renameSync(src, thisDest); |
|
}); // forEach(src) |
|
}; // mv |
|
exports.mv = wrap('mv', _mv); |
|
|
|
//@ |
|
//@ #### mkdir([options ,] dir [, dir ...]) |
|
//@ #### mkdir([options ,] dir_array) |
|
//@ Available options: |
|
//@ |
|
//@ + `p`: full path (will create intermediate dirs if necessary) |
|
//@ |
|
//@ Examples: |
|
//@ |
|
//@ ```javascript |
|
//@ mkdir('-p', '/tmp/a/b/c/d', '/tmp/e/f/g'); |
|
//@ mkdir('-p', ['/tmp/a/b/c/d', '/tmp/e/f/g']); // same as above |
|
//@ ``` |
|
//@ |
|
//@ Creates directories. |
|
function _mkdir(options, dirs) { |
|
options = parseOptions(options, { |
|
'p': 'fullpath' |
|
}); |
|
if (!dirs) |
|
error('no paths given'); |
|
|
|
if (typeof dirs === 'string') |
|
dirs = [].slice.call(arguments, 1); |
|
// if it's array leave it as it is |
|
|
|
dirs.forEach(function(dir) { |
|
if (fs.existsSync(dir)) { |
|
if (!options.fullpath) |
|
error('path already exists: ' + dir, true); |
|
return; // skip dir |
|
} |
|
|
|
// Base dir does not exist, and no -p option given |
|
var baseDir = path.dirname(dir); |
|
if (!fs.existsSync(baseDir) && !options.fullpath) { |
|
error('no such file or directory: ' + baseDir, true); |
|
return; // skip dir |
|
} |
|
|
|
if (options.fullpath) |
|
mkdirSyncRecursive(dir); |
|
else |
|
fs.mkdirSync(dir, 0777); |
|
}); |
|
}; // mkdir |
|
exports.mkdir = wrap('mkdir', _mkdir); |
|
|
|
//@ |
|
//@ #### test(expression) |
|
//@ Available expression primaries: |
|
//@ |
|
//@ + `'-d', 'path'`: true if path is a directory |
|
//@ + `'-f', 'path'`: true if path is a regular file |
|
//@ |
|
//@ Examples: |
|
//@ |
|
//@ ```javascript |
|
//@ if (test('-d', path)) { /* do something with dir */ }; |
|
//@ if (!test('-f', path)) continue; // skip if it's a regular file |
|
//@ ``` |
|
//@ |
|
//@ Evaluates expression using the available primaries and returns corresponding value. |
|
function _test(options, path) { |
|
if (!path) |
|
error('no path given'); |
|
|
|
// hack - only works with unary primaries |
|
options = parseOptions(options, { |
|
'd': 'directory', |
|
'f': 'file' |
|
}); |
|
if (!options.directory && !options.file) |
|
error('could not interpret expression'); |
|
|
|
if (options.directory) |
|
return fs.existsSync(path) && fs.statSync(path).isDirectory(); |
|
|
|
if (options.file) |
|
return fs.existsSync(path) && fs.statSync(path).isFile(); |
|
}; // test |
|
exports.test = wrap('test', _test); |
|
|
|
|
|
//@ |
|
//@ #### cat(file [, file ...]) |
|
//@ #### cat(file_array) |
|
//@ |
|
//@ Examples: |
|
//@ |
|
//@ ```javascript |
|
//@ var str = cat('file*.txt'); |
|
//@ var str = cat('file1', 'file2'); |
|
//@ var str = cat(['file1', 'file2']); // same as above |
|
//@ ``` |
|
//@ |
|
//@ Returns a string containing the given file, or a concatenated string |
|
//@ 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 = ''; |
|
|
|
if (!files) |
|
error('no paths given'); |
|
|
|
if (typeof files === 'string') |
|
files = [].slice.call(arguments, 1); |
|
// if it's array leave it as it is |
|
|
|
files = expand(files); |
|
|
|
files.forEach(function(file) { |
|
if (!fs.existsSync(file)) |
|
error('no such file or directory: ' + file); |
|
|
|
cat += fs.readFileSync(file, 'utf8') + '\n'; |
|
}); |
|
|
|
if (cat[cat.length-1] === '\n') |
|
cat = cat.substring(0, cat.length-1); |
|
|
|
return ShellString(cat); |
|
}; |
|
exports.cat = wrap('cat', _cat); |
|
|
|
//@ |
|
//@ #### 'string'.to(file) |
|
//@ |
|
//@ Examples: |
|
//@ |
|
//@ ```javascript |
|
//@ cat('input.txt').to('output.txt'); |
|
//@ ``` |
|
//@ |
|
//@ Analogous to the redirection operator `>` in Unix, but works with JavaScript strings (such as |
|
//@ those returned by `cat`, `grep`, etc). _Like Unix redirections, `to()` will overwrite any existing file!_ |
|
function _to(options, file) { |
|
if (!file) |
|
error('wrong arguments'); |
|
|
|
if (!fs.existsSync( path.dirname(file) )) |
|
error('no such file or directory: ' + path.dirname(file)); |
|
|
|
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 |
|
function ShellString(str) { |
|
return str; |
|
} |
|
String.prototype.to = wrap('to', _to); |
|
|
|
//@ |
|
//@ #### sed([options ,] search_regex, replace_str, file) |
|
//@ Available options: |
|
//@ |
|
//@ + `-i`: Replace contents of 'file' in-place. _Note that no backups will be created!_ |
|
//@ |
|
//@ Examples: |
|
//@ |
|
//@ ```javascript |
|
//@ sed('-i', 'PROGRAM_VERSION', 'v0.1.3', 'source.js'); |
|
//@ sed(/.*DELETE_THIS_LINE.*\n/, '', 'source.js'); |
|
//@ ``` |
|
//@ |
|
//@ Reads an input string from `file` and performs a JavaScript `replace()` on the input |
|
//@ using the given search regex and replacement string. Returns the new string after replacement. |
|
function _sed(options, regex, replacement, file) { |
|
options = parseOptions(options, { |
|
'i': 'inplace' |
|
}); |
|
|
|
if (typeof replacement === 'string') |
|
replacement = replacement; // no-op |
|
else if (typeof replacement === 'number') |
|
replacement = replacement.toString(); // fallback |
|
else |
|
error('invalid replacement string'); |
|
|
|
if (!file) |
|
error('no file given'); |
|
|
|
if (!fs.existsSync(file)) |
|
error('no such file or directory: ' + file); |
|
|
|
var result = fs.readFileSync(file, 'utf8').replace(regex, replacement); |
|
if (options.inplace) |
|
fs.writeFileSync(file, result, 'utf8'); |
|
|
|
return ShellString(result); |
|
}; |
|
exports.sed = wrap('sed', _sed); |
|
|
|
//@ |
|
//@ #### grep(regex_filter, file [, file ...]) |
|
//@ #### grep(regex_filter, file_array) |
|
//@ |
|
//@ Examples: |
|
//@ |
|
//@ ```javascript |
|
//@ grep('GLOBAL_VARIABLE', '*.js'); |
|
//@ ``` |
|
//@ |
|
//@ Reads input string from given files and returns a string containing all lines of the |
|
//@ file that match the given `regex_filter`. Wildcard `*` accepted. |
|
function _grep(options, regex, files) { |
|
if (!files) |
|
error('no paths given'); |
|
|
|
if (typeof files === 'string') |
|
files = [].slice.call(arguments, 2); |
|
// if it's array leave it as it is |
|
|
|
files = expand(files); |
|
|
|
var grep = ''; |
|
files.forEach(function(file) { |
|
if (!fs.existsSync(file)) { |
|
error('no such file or directory: ' + file, true); |
|
return; |
|
} |
|
|
|
var contents = fs.readFileSync(file, 'utf8'), |
|
lines = contents.split(/\r*\n/); |
|
lines.forEach(function(line) { |
|
if (line.match(regex)) |
|
grep += line + '\n'; |
|
}); |
|
}); |
|
|
|
return ShellString(grep); |
|
}; |
|
exports.grep = wrap('grep', _grep); |
|
|
|
|
|
//@ |
|
//@ #### which(command) |
|
//@ |
|
//@ Examples: |
|
//@ |
|
//@ ```javascript |
|
//@ var nodeExec = which('node'); |
|
//@ ``` |
|
//@ |
|
//@ Searches for `command` in the system's PATH. On Windows looks for `.exe`, `.cmd`, and `.bat` extensions. |
|
//@ Returns string containing the absolute path to the command. |
|
function _which(options, cmd) { |
|
if (!cmd) |
|
error('must specify command'); |
|
|
|
var pathEnv = process.env.path || process.env.Path || process.env.PATH, |
|
pathArray = splitPath(pathEnv), |
|
where = null; |
|
|
|
// No relative/absolute paths provided? |
|
if (cmd.search(/\//) === -1) { |
|
// Search for command in PATH |
|
pathArray.forEach(function(dir) { |
|
if (where) |
|
return; // already found it |
|
|
|
var attempt = path.resolve(dir + '/' + cmd); |
|
if (fs.existsSync(attempt)) { |
|
where = attempt; |
|
return; |
|
} |
|
|
|
if (platform === 'win') { |
|
var baseAttempt = attempt; |
|
attempt = baseAttempt + '.exe'; |
|
if (fs.existsSync(attempt)) { |
|
where = attempt; |
|
return; |
|
} |
|
attempt = baseAttempt + '.cmd'; |
|
if (fs.existsSync(attempt)) { |
|
where = attempt; |
|
return; |
|
} |
|
attempt = baseAttempt + '.bat'; |
|
if (fs.existsSync(attempt)) { |
|
where = attempt; |
|
return; |
|
} |
|
} // if 'win' |
|
}); |
|
} |
|
|
|
// Command not found anywhere? |
|
if (!fs.existsSync(cmd) && !where) |
|
return null; |
|
|
|
where = where || path.resolve(cmd); |
|
|
|
return ShellString(where); |
|
}; |
|
exports.which = wrap('which', _which); |
|
|
|
//@ |
|
//@ #### echo(string [,string ...]) |
|
//@ |
|
//@ Examples: |
|
//@ |
|
//@ ```javascript |
|
//@ echo('hello world'); |
|
//@ var str = echo('hello world'); |
|
//@ ``` |
|
//@ |
|
//@ Prints string to stdout, and returns string with additional utility methods |
|
//@ like `.to()`. |
|
function _echo(options) { |
|
var messages = [].slice.call(arguments, 1); |
|
console.log.apply(this, messages); |
|
return ShellString(messages.join(' ')); |
|
}; |
|
exports.echo = wrap('echo', _echo); |
|
|
|
//@ |
|
//@ #### exit(code) |
|
//@ Exits the current process with the given exit code. |
|
exports.exit = process.exit; |
|
|
|
//@ |
|
//@ #### env['VAR_NAME'] |
|
//@ Object containing environment variables (both getter and setter). Shortcut to process.env. |
|
exports.env = process.env; |
|
|
|
//@ |
|
//@ #### exec(command [, options] [, callback]) |
|
//@ Available options (all `false` by default): |
|
//@ |
|
//@ + `async`: Asynchronous execution. Needs callback. |
|
//@ + `silent`: Do not echo program output to console. |
|
//@ |
|
//@ Examples: |
|
//@ |
|
//@ ```javascript |
|
//@ var version = exec('node --version', {silent:true}).output; |
|
//@ ``` |
|
//@ |
|
//@ Executes the given `command` _synchronously_, unless otherwise specified. |
|
//@ When in synchronous mode returns the object `{ code:..., output:... }`, containing the program's |
|
//@ `output` (stdout + stderr) and its exit `code`. Otherwise the `callback` gets the |
|
//@ arguments `(code, output)`. |
|
//@ |
|
//@ **Note:** For long-lived processes, it's best to run `exec()` asynchronously as |
|
//@ the current synchronous implementation uses a lot of CPU. This should be getting |
|
//@ fixed soon. |
|
function _exec(command, options, callback) { |
|
if (!command) |
|
error('must specify command'); |
|
|
|
if (typeof options === 'function') { |
|
callback = options; |
|
options = {}; |
|
} |
|
|
|
options = extend({ |
|
silent: state.silent, |
|
async: false |
|
}, options); |
|
|
|
if (options.async) |
|
execAsync(command, options, callback); |
|
else |
|
return execSync(command, options); |
|
}; |
|
exports.exec = wrap('exec', _exec, {notUnix:true}); |
|
|
|
|
|
|
|
|
|
//@ |
|
//@ ## Non-Unix commands |
|
//@ |
|
|
|
|
|
|
|
|
|
|
|
|
|
//@ |
|
//@ #### tempdir() |
|
//@ 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). |
|
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 command output if `state = true`, except for `echo()` calls. |
|
//@ 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'); |
|
|
|
if (typeof paths === 'string') |
|
paths = [].slice.call(arguments, 1); |
|
// if it's array leave it as it is |
|
|
|
var exists = true; |
|
paths.forEach(function(p) { |
|
if (!fs.existsSync(p)) |
|
exists = false; |
|
}); |
|
|
|
return exists; |
|
}; |
|
exports.exists = wrap('exists', _exists); |
|
|
|
|
|
//@ |
|
//@ #### verbose() |
|
//@ |
|
//@ _This function is being deprecated. Use `silent(false) instead.`_ |
|
//@ |
|
//@ Enables all output (default) |
|
exports.verbose = function() { |
|
deprecate('verbose', 'Use silent(false) instead.'); |
|
|
|
state.silent = false; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////////// |
|
// |
|
// Auxiliary functions (internal use only) |
|
// |
|
|
|
function log() { |
|
if (!state.silent) |
|
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); |
|
} |
|
|
|
// Shows error message. Throws unless '_continue = true'. |
|
function error(msg, _continue) { |
|
if (state.error === null) |
|
state.error = ''; |
|
state.error += state.currentCmd + ': ' + msg + '\n'; |
|
|
|
log(state.error); |
|
|
|
if (!_continue) |
|
throw ''; |
|
} |
|
|
|
// Returns {'alice': true, 'bob': false} when passed: |
|
// parseOptions('-a', {'a':'alice', 'b':'bob'}); |
|
function parseOptions(str, map) { |
|
if (!map) |
|
error('parseOptions() internal error: no map given'); |
|
|
|
// All options are false by default |
|
var options = {}; |
|
for (letter in map) |
|
options[map[letter]] = false; |
|
|
|
if (!str) |
|
return options; // defaults |
|
|
|
if (typeof str !== 'string') |
|
error('parseOptions() internal error: wrong str'); |
|
|
|
// e.g. match[1] = 'Rf' for str = '-Rf' |
|
var match = str.match(/^\-(.+)/); |
|
if (!match) |
|
return options; |
|
|
|
// e.g. chars = ['R', 'f'] |
|
var chars = match[1].split(''); |
|
|
|
chars.forEach(function(char) { |
|
if (char in map) |
|
options[map[char]] = true; |
|
else |
|
error('option not recognized: '+char); |
|
}); |
|
|
|
return options; |
|
} |
|
|
|
// Common wrapper for all Unix-like commands |
|
function wrap(cmd, fn, options) { |
|
return function() { |
|
var retValue = null; |
|
|
|
state.currentCmd = cmd; |
|
state.error = null; |
|
|
|
try { |
|
var args = [].slice.call(arguments, 0); |
|
|
|
if (options && options.notUnix) { |
|
retValue = fn.apply(this, args); |
|
} else { |
|
if (args.length === 0 || typeof args[0] !== 'string' || args[0][0] !== '-') |
|
args.unshift(''); // only add dummy option if '-option' not already present |
|
retValue = fn.apply(this, args); |
|
} |
|
} catch (e) { |
|
if (!state.error) { |
|
// If state.error hasn't been set it's an error thrown by Node, not us - probably a bug... |
|
console.log('shell.js: internal error'); |
|
console.log(e.stack || e); |
|
process.exit(1); |
|
} |
|
if (state.fatal) |
|
throw e; |
|
} |
|
|
|
state.currentCmd = 'shell.js'; |
|
return retValue; |
|
} |
|
} // wrap |
|
|
|
// Buffered file copy, synchronous |
|
// (Using readFileSync() + writeFileSync() could easily cause a memory overflow |
|
// with large files) |
|
function copyFileSync(srcFile, destFile) { |
|
if (!fs.existsSync(srcFile)) |
|
error('copyFileSync: no such file or directory: ' + srcFile); |
|
|
|
var BUF_LENGTH = 64*1024, |
|
buf = new Buffer(BUF_LENGTH), |
|
bytesRead = BUF_LENGTH, |
|
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); |
|
fs.writeSync(fdw, buf, 0, bytesRead); |
|
pos += bytesRead; |
|
} |
|
|
|
fs.closeSync(fdr); |
|
fs.closeSync(fdw); |
|
} |
|
|
|
// Recursively copies 'sourceDir' into 'destDir' |
|
// Adapted from https://github.com/ryanmcgrath/wrench-js |
|
// |
|
// Copyright (c) 2010 Ryan McGrath |
|
// Copyright (c) 2012 Artur Adib |
|
// |
|
// Licensed under the MIT License |
|
// http://www.opensource.org/licenses/mit-license.php |
|
function cpdirSyncRecursive(sourceDir, destDir, opts) { |
|
if (!opts) opts = {}; |
|
|
|
/* Create the directory where all our junk is moving to; read the mode of the source directory and mirror it */ |
|
var checkDir = fs.statSync(sourceDir); |
|
try { |
|
fs.mkdirSync(destDir, checkDir.mode); |
|
} catch (e) { |
|
//if the directory already exists, that's okay |
|
if (e.code !== 'EEXIST') throw e; |
|
} |
|
|
|
var files = fs.readdirSync(sourceDir); |
|
|
|
for(var i = 0; i < files.length; i++) { |
|
var currFile = fs.lstatSync(sourceDir + "/" + files[i]); |
|
|
|
if (currFile.isDirectory()) { |
|
/* recursion this thing right on back. */ |
|
cpdirSyncRecursive(sourceDir + "/" + files[i], destDir + "/" + files[i], opts); |
|
} else if (currFile.isSymbolicLink()) { |
|
var symlinkFull = fs.readlinkSync(sourceDir + "/" + files[i]); |
|
fs.symlinkSync(symlinkFull, destDir + "/" + files[i]); |
|
} else { |
|
/* At this point, we've hit a file actually worth copying... so copy it on over. */ |
|
if (fs.existsSync(destDir + "/" + files[i]) && !opts.force) { |
|
log('skipping existing file: ' + files[i]); |
|
} else { |
|
copyFileSync(sourceDir + "/" + files[i], destDir + "/" + files[i]); |
|
} |
|
} |
|
|
|
} // for files |
|
}; // cpdirSyncRecursive |
|
|
|
// Recursively removes 'dir' |
|
// Adapted from https://github.com/ryanmcgrath/wrench-js |
|
// |
|
// Copyright (c) 2010 Ryan McGrath |
|
// Copyright (c) 2012 Artur Adib |
|
// |
|
// Licensed under the MIT License |
|
// http://www.opensource.org/licenses/mit-license.php |
|
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 file = dir + "/" + files[i], |
|
currFile = fs.lstatSync(file); |
|
|
|
if(currFile.isDirectory()) { // Recursive function back to the beginning |
|
rmdirSyncRecursive(file, force); |
|
} |
|
|
|
else if(currFile.isSymbolicLink()) { // Unlink symlinks |
|
if (force || isWriteable(file)) |
|
_unlinkSync(file); |
|
} |
|
|
|
else // Assume it's a file - perhaps a try/catch belongs here? |
|
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. |
|
|
|
var result; |
|
try { |
|
result = fs.rmdirSync(dir); |
|
} catch(e) { |
|
error('could not remove directory (code '+e.code+'): ' + dir, true); |
|
} |
|
|
|
return result; |
|
}; // rmdirSyncRecursive |
|
|
|
// Recursively creates 'dir' |
|
function mkdirSyncRecursive(dir) { |
|
var baseDir = path.dirname(dir); |
|
|
|
// Base dir exists, no recursion necessary |
|
if (fs.existsSync(baseDir)) { |
|
fs.mkdirSync(dir, 0777); |
|
return; |
|
} |
|
|
|
// Base dir does not exist, go recursive |
|
mkdirSyncRecursive(baseDir); |
|
|
|
// Base dir created, can create dir |
|
fs.mkdirSync(dir, 0777); |
|
}; |
|
|
|
// e.g. 'makerjs_a5f185d0443ca...' |
|
function randomFileName() { |
|
function randomHash(count) { |
|
if (count === 1) |
|
return parseInt(16*Math.random()).toString(16); |
|
else { |
|
var hash = ''; |
|
for (var i=0; i<count; i++) |
|
hash += randomHash(1); |
|
return hash; |
|
} |
|
} |
|
|
|
return 'makerjs_'+randomHash(20); |
|
} |
|
|
|
// Returns false if 'dir' is not a writeable directory, 'dir' otherwise |
|
function writeableDir(dir) { |
|
if (!dir || !fs.existsSync(dir)) |
|
return false; |
|
|
|
if (!fs.statSync(dir).isDirectory()) |
|
return false; |
|
|
|
var testFile = dir+'/'+randomFileName(); |
|
try { |
|
fs.writeFileSync(testFile, ' '); |
|
_unlinkSync(testFile); |
|
return dir; |
|
} catch (e) { |
|
return false; |
|
} |
|
} |
|
|
|
// Cross-platform method for getting an available temporary directory. |
|
// Follows the algorithm of Python's tempfile.tempdir |
|
// http://docs.python.org/library/tempfile.html#tempfile.tempdir |
|
function tempDir() { |
|
if (state.tempDir) |
|
return state.tempDir; // from cache |
|
|
|
state.tempDir = writeableDir(process.env['TMPDIR']) || |
|
writeableDir(process.env['TEMP']) || |
|
writeableDir(process.env['TMP']) || |
|
writeableDir(process.env['Wimp$ScrapDir']) || // RiscOS |
|
writeableDir('C:\\TEMP') || // Windows |
|
writeableDir('C:\\TMP') || // Windows |
|
writeableDir('\\TEMP') || // Windows |
|
writeableDir('\\TMP') || // Windows |
|
writeableDir('/tmp') || |
|
writeableDir('/var/tmp') || |
|
writeableDir('/usr/tmp') || |
|
writeableDir('.'); // last resort |
|
|
|
return state.tempDir; |
|
} |
|
|
|
// Wrapper around exec() to enable echoing output to console in real time |
|
function execAsync(cmd, opts, callback) { |
|
var output = ''; |
|
|
|
var options = extend({ |
|
silent: state.silent |
|
}, opts); |
|
|
|
var c = child.exec(cmd, {env: process.env}, function(err) { |
|
if (callback) |
|
callback(err ? err.code : 0, output); |
|
}); |
|
|
|
c.stdout.on('data', function(data) { |
|
output += data; |
|
if (!options.silent) |
|
process.stdout.write(data); |
|
}); |
|
|
|
c.stderr.on('data', function(data) { |
|
output += data; |
|
if (!options.silent) |
|
process.stdout.write(data); |
|
}); |
|
} |
|
|
|
// Hack to run child_process.exec() synchronously (sync avoids callback hell) |
|
// Uses a custom wait loop that checks for a flag file, created when the child process is done. |
|
// (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) { |
|
var stdoutFile = path.resolve(tempDir()+'/'+randomFileName()), |
|
codeFile = path.resolve(tempDir()+'/'+randomFileName()), |
|
scriptFile = path.resolve(tempDir()+'/'+randomFileName()), |
|
sleepFile = path.resolve(tempDir()+'/'+randomFileName()); |
|
|
|
var options = extend({ |
|
silent: state.silent |
|
}, opts); |
|
|
|
var previousStdoutContent = ''; |
|
// Echoes stdout changes from running process, if not silent |
|
function updateStdout() { |
|
if (options.silent || !fs.existsSync(stdoutFile)) |
|
return; |
|
|
|
var stdoutContent = fs.readFileSync(stdoutFile, 'utf8'); |
|
// No changes since last time? |
|
if (stdoutContent.length <= previousStdoutContent.length) |
|
return; |
|
|
|
process.stdout.write(stdoutContent.substr(previousStdoutContent.length)); |
|
previousStdoutContent = stdoutContent; |
|
} |
|
|
|
function escape(str) { |
|
str = str.replace(/\'/g, '"'); |
|
str = str.replace(/\\/g, '\\\\'); |
|
return str; |
|
} |
|
|
|
cmd += ' > '+stdoutFile+' 2>&1'; // works on both win/unix |
|
|
|
var script = |
|
"var child = require('child_process'), \ |
|
fs = require('fs'); \ |
|
child.exec('"+escape(cmd)+"', {env: process.env}, function(err) { \ |
|
fs.writeFileSync('"+escape(codeFile)+"', err ? err.code.toString() : '0'); \ |
|
});"; |
|
|
|
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, { |
|
env: process.env, |
|
cwd: exports.pwd() |
|
}); |
|
|
|
// The wait loop |
|
// 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 code = parseInt(''); |
|
while (isNaN(code)) |
|
code = parseInt(fs.readFileSync(codeFile, 'utf8')); |
|
|
|
var stdout = fs.readFileSync(stdoutFile, 'utf8'); |
|
|
|
// No biggie if we can't erase the files now -- they're in a temp dir anyway |
|
try { _unlinkSync(scriptFile); } catch(e) {}; |
|
try { _unlinkSync(stdoutFile); } catch(e) {}; |
|
try { _unlinkSync(codeFile); } catch(e) {}; |
|
try { _unlinkSync(sleepFile); } catch(e) {}; |
|
|
|
// True if successful, false if not |
|
var obj = { |
|
code: code, |
|
output: stdout |
|
}; |
|
return obj; |
|
} // execSync() |
|
|
|
// Expands wildcards with matching file names. For a given array of file names 'list', returns |
|
// another array containing all file names as per ls(list[i]). |
|
// For example: expand(['file*.js']) = ['file1.js', 'file2.js', ...] |
|
// (if the files 'file1.js', 'file2.js', etc, exist in the current dir) |
|
function expand(list) { |
|
var expanded = []; |
|
list.forEach(function(listEl) { |
|
// Wildcard present? |
|
if (listEl.search(/\*/) > -1) { |
|
_ls('', listEl).forEach(function(file) { |
|
expanded.push(file); |
|
}); |
|
} else { |
|
expanded.push(listEl); |
|
} |
|
}); |
|
return expanded; |
|
} |
|
|
|
// Cross-platform method for splitting environment PATH variables |
|
function splitPath(p) { |
|
if (!p) |
|
return []; |
|
|
|
if (platform === 'win') |
|
return p.split(';'); |
|
else |
|
return p.split(':'); |
|
} |
|
|
|
// extend(target_obj, source_obj1 [, source_obj2 ...]) |
|
// Shallow extend, e.g.: |
|
// aux.extend({a:1}, {b:2}, {c:3}) |
|
// returns {a:1, b:2, c:3} |
|
function extend(target) { |
|
var sources = [].slice.call(arguments, 1); |
|
sources.forEach(function(source) { |
|
for (key in source) |
|
target[key] = source[key]; |
|
}); |
|
|
|
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; |
|
}
|
|
|