Here we explain in greater details how to use sh.js, and to a lesser extent, how it works.
The following tutorial aims people who are at least novice at shell scripting and know their way in node.js. In particular, you should be familiar with:
ls / | grep etc
find . -name "*.js" > results
&&
or ||
require
require
Depending on your installation, there are two ways you can require
sh.js.
If you installed with npm, simply require sh
:
var sh = require('sh');
sh('echo hello');
If you installed with git, you need to require a path:
var sh = require('/path/to/shjs/sh');
sh('echo hello');
Examples in this tutorial may have a file name indicating you can run it in the examples
directory.
In a terminal, head to the examples
directory, and run ./00-hello.example.js
At its core, sh.js launches programs:
// examples/01-ls.example.js
sh('ls');
This spawns a new process that executes the ls
program.
That returns a function that you may call:
// examples/02-ls_grep.example.js
var f = sh('ls');
f('grep \\.example\\.js$');
By calling it, you tell sh.js that you want ls
' output to be used as grep
's input. You can pipe like that as many times as you want:
// examples/03-multi_pipe.example.js
sh('echo hello')('cat')('cat')('cat')('cat');
That is the equivalent to the following Bash command:
echo hello | cat | cat | cat | cat
Now here's how to redirect standard output to a file:
// examples/04-find_files.example.js
sh('find . -size -100c').file('100c_files');
Bash equivalent:
find . -size -100c > 100c_files
Alternatively, there is an .append()
method that appends to a file instead of (over)writting it (like >>
).
You can also cache the output of a command, and receive it in an callback:
1
2
3
4
5
// examples/05-count_find_results.example.js
sh('cat 100c_files')('wc -l').result(function(count) {
console.log('found %d file(s) smaller than 100 bytes', Number(count));
});
Bash equivalent:
echo found `cat 100c_files | wc -l` file\(s\) smaller than 100 bytes
I hope you can see the pattern here: anywhere piping is syntactically correct, .file()
, .append()
and .result()
are correct too:
1
2
3
4
5
6
7
8
9
10
11
sh('echo hello')('cat');
// ^^^^^^^ piping is correct
sh('echo hello').file('hello.dump');
// ^^^^^^ therefore .file() is correct too
sh('echo hello').append('hello.dump');
// ^^^^^^^^ and so is .append()
sh('echo hello').result(function(output) { console.log(output); });
// ^^^^^^^^ as well as .result()
Piping stderr is quite similar to stdout, you need to interleave .err
:
// examples/06-pipe_err_to_null.example.js
sh('find /var -type s').err.file('/dev/null');
Bash equivalent:
find /var -type s 2> /dev/null
That will find socket files and discard errors messages.
Note: see the section on advanced piping to redirect both stdout and stderr.
Typing commands with sh.js is meant to feel almost like a standard shell.
You can quote arguments:
1
2
3
4
5
6
// examples/07-quoting.example.js
sh('echo "hello world"');
// Output:
// hello world
You can escape characters:
1
2
3
4
5
6
// examples/08-escaping.example.js
sh('echo hello\\ \\ world \\" \\\' \\\\');
// Output:
// hello world " ' \
In case quoting or escaping are not satisfying, you may pass an array of arguments instead:
sh(['echo', 'hello world " \' \\']);
Finally, to avoid getting stuck because of a bug, you can access the parser like so:
1
2
3
4
5
6
7
8
// examples/09-parser.example.js
var parser = require('../sh.js')._internal.parser;
console.log(parser.parse('echo hello\\ \\ world \\" \\\' \\\\'));
// Output:
// [ 'echo', 'hello world', '"', '\'', '\' ]
The .parse()
method returns an array with the arguments as it interprets them. If you spot a bug, please report it.
sh.js is non-blocking. Every call returns immediately after some minor computations. For instance, consider:
1
2
3
4
5
6
7
8
9
// examples/10-concurrent.example.js
sh('date');
sh('sleep 2');
sh('date');
// Output:
// jeudi 18 novembre 2010, 20:30:54 (UTC+0100)
// jeudi 18 novembre 2010, 20:30:54 (UTC+0100)
Unlike in Bash, both dates are the same because calls to sh.js do not block. Both date
commands are run concurrently. This is useful to run tasks in parallel.
Bash equivalent:
date &
sleep 2 &
date &
If you want to run commands sequentially, use .then()
:
1
2
3
4
5
6
7
8
9
// examples/11-sequential.example.js
sh('date')
.then('sleep 2')
.then('date');
// Output:
// jeudi 18 novembre 2010, 20:33:07 (UTC+0100)
// jeudi 18 novembre 2010, 20:33:09 (UTC+0100)
This does not block the execution of your program, but it schedules the next command only after the previous one exits. Notice dates are not the same, unlike before.
Bash equivalent:
date
sleep 2
date
Concurrent and sequential commands are not mutually exclusive, you can use both:
1
2
3
4
5
6
7
8
9
10
// examples/12-concurrent_sequential.example.js
var s = sh('sleep 1');
s.and('sleep 2').and('date');
s.and('sleep 2').and('date');
// Output:
// jeudi 18 novembre 2010, 20:35:46 (UTC+0100)
// jeudi 18 novembre 2010, 20:35:46 (UTC+0100)
sleep 2
commands are run after the sleep 1
command, so that's sequential. But notice the dates are the same because sleep 2
commands are run concurrently.
Bash equivalent:
sleep 1 && ( ( sleep 2 && date ) & ( sleep 2 && date ) & )
.and()
and .or()
Using .and()
and .or()
methods, you can handle the exit of the previous process depending on its exit status. Take the following example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// examples/13-gzip_zeros.example.js
var s = sh('dd if=/dev/zero count=10000 bs=50K')('gzip').file('zeros.gz');
s.then('echo compression stopped');
s.or('echo compression failed');
s.and('echo compression succeeded');
// Output:
// 10000+0 enregistrements lus
// 10000+0 enregistrements écrits
// 512000000 octets (512 MB) copiés, 4,00177 s, 128 MB/s
// compression stopped
// compression succeeded
That copies 500 million null bytes from /dev/zero
and gzip
s them to zeros.gz
.
.then()
method prints "compression stopped" as soon as gzip exits..or()
method prints "compression failed" as soon as gzip exits and if gzip returns a status not equal to zero (not the case here)..and()
method prints "compression succeeded" as soon as gzip exits and if gzip returns a status of 0
.Bash equivalent:
1
2
3
4
5
6
7
dd if=/dev/zero count=10000 bs=50K | gzip > zeros.gz
status=$?
echo compression stopped
test $status || echo compression failed
test $status && echo compression succeeded
.and()
, .or()
and .then()
all accept functions as arguments, so you can run JavaScript instead of a program:
1
2
3
4
5
// examples/14-callback.example.js
sh('sleep 2').and(function() {
console.log('woke up!');
});
.cd()
and .define()
If you want to run commands in a particular directory, call .cd()
before them in a sequence:
1
2
3
4
5
// examples/15-ls_cd_ls.example.js
sh('ls -l')
.and.cd('..')
.and('ls -l');
Pay attention on how to call .cd()
. Here we call it as a method of .and
.
You may also use it as a method of .or
, .then
or sh
like below:
sh('ls -l').or.cd('..').and('ls -l');
sh('ls -l').then.cd('..').and('ls -l');
sh.cd('..').and('ls -l');
Now if you want to set an environment variable, here's how:
// examples/16-grep_my_var.example.js
sh.define('MY_VAR', 123).and('env')('grep MY_VAR');
You may also set several variables in one call:
1
2
3
4
5
6
7
// examples/17-grep_my_vars.example.js
sh.define({
'MY_VAR1': 123,
'MY_VAR2': 'abc'
})
.and('env')('grep MY_VAR');
Finally, to unset a variable, just set it to sh.UNSET
:
1
2
3
4
5
// examples/18-unset_my_var.example.js
sh.define('MY_VAR', 123)
.and.define('MY_VAR', sh.UNSET)
.and('env')('grep MY_VAR');
By the way, once you cd
to a directory or set environment variable, you can store the shell in a JavaScript variable, and reuse it later:
1
2
3
4
5
6
7
// examples/19-store_shell.example.js
var sh1 = sh.cd('/').and;
var sh2 = sh.cd('/var').and;
sh1('ls');
sh2('ls');
Don't forget the .and
however.
There is a restriction in that .cd()
or .define()
cannot be in a pipeline like so:
// this doesn't work for now
sh.cd('abcd').file('/dev/null').and('ls -l');
I hope that will work soon.
Note: the main challenge of making sh.js was to find the right syntax. Suggestions are welcome.
On the one hand, a process has three standard streams and you may want to start several processes upon its exit. On the other hand, JavaScript doesn't have lots of idioms to embed such syntax.
We're trying to reproduce the following Bash command (this uses process substitution which is not part of the standard):
1
2
3
4
5
6
7
8
9
10
11
12
13
ls / non_existent_file
2> >( sed s/non_existent/NON_EXISTENT/ ) \
> >( grep etc )
# Output
# etc
# ls: ne peut accéder NON_EXISTENT_file: Aucun fichier ou dossier de ce type
# Note: everyone is more familiar with the following, but for the purpose of
# writing shell scripts in JavaScript, the syntax poses the same problems
# as above:
ls / non_existent_file > out_stream 2> err_stream
So I've found four ways that I believe do make sense, three of which are implemented, the other one being removed from the code:
This syntax was removed. It looked like:
// This was removed, it won't work
sh('ls / non_existent_file')
.pipe('grep etc')
.err('sed s/non_existent/NON_EXISTENT/');
The reason this was removed is I found it too complicated when there is a lot of recursion. It felt awkward because .pipe()
and .err()
break easily if they are not properly ordered.
By declaring a variable, you can make several method calls on the first command:
1
2
3
4
5
6
// examples/20-pipes_with_variables.example.js
var l = sh('ls / non_existent_file');
l('grep etc');
l.err('sed s/non_existent/NON_EXISTENT/');
Pros:
Cons:
Passing a closure after the command string will run it and identify the piping:
1
2
3
4
5
6
// examples/21-pipes_in_closures.example.js
sh('ls / non_existent_file', function() {
this.out('grep etc');
this.err('sed s/non_existent/NON_EXISTENT/');
});
Pros:
very nice in CoffeeScript:
sh 'ls / non_existent_file', ->
@out 'grep etc'
@err 'sed s/non_existent/NON_EXISTENT/'
Cons:
In this syntax, you use one argument for each stream to tell the command how you want to pipe it
1
2
3
4
5
6
// examples/22-branch_arguments.example.js
sh('ls / non_existent_file',
sh.out('grep etc'),
sh.err('sed s/non_existent/NON_EXISTENT/')
);
Pros:
CoffeeScript-friendly:
sh 'ls / non_existent_file',
sh.out 'grep etc'
sh.err 'sed s/non_existent/NON_EXISTENT/'
Cons:
Of course, the idioms above are not mutally exclusive.
1
2
3
4
5
6
7
8
9
10
11
12
13
// examples/23-advanced.example.js
var l = sh('ls / non_existent_file',
sh.out('grep etc'),
sh.and('echo ls succeeded'),
function() {
this.err.file('/dev/null');
this.or('echo ls failed');
this.then('echo ls finished (1)');
}
);
l.then('echo ls finished (2)');
Here we combine:
/dev/null
line (7)If you want to redirect stderr to stdout, like with &>
or |&
in traditional shells, pass sh.OO
after the command:
sh('./configure', sh.OO).file('output+errors');
That's double-capital-O, as in "Output Only".
sh.OO
must come before a closure if you use one:
sh('./configure', sh.OO, function(c) {
c.and('echo configure succeeded');
c.or('echo configure failed');
}).file('output+errors');
If you want a clean environment, use sh.ENV
:
1
2
3
4
5
6
7
8
9
// examples/24-clean-env.example.js
sh.define(sh.ENV, {
'MY_VAR': 123
})
.and('env');
// Output: the environment is empty except for the variable we just defined
// MY_VAR=123
That tutorial covered mostly everything. This is a work in progress. Every piece of feedback will be greatly appreciated (use my email in git log
).