shargs-tutorial-git is a tutorial for getting started with shargs π¦.
See the shargs github repository for more details!
The code we will write in this tutorial is available as the git script in this repository.
To run it, first setup the dependencies:
$ git clone https://github.com/Yord/shargs-tutorial-git.git
$ cd shargs-tutorial-git
$ npm i
$ chmod +x ./gitThen execute the script:
$ ./git --helpIn this tutorial, we will build a command-line interface that resembles git.
I assume, you are already familiar with git, but even if you are not, you should be able to follow.
Let us start with installing some shargs packages:
$ npm install --save --save-exact shargs@0.26.0Note that we install a fixed version. This tutorial should work with this exact version, and might work with a newer version. However, this tutorial may not work if you use a different version.
We start with a minimal shargs parser:
#!/usr/bin/env node
const {parserSync} = require('shargs')
const parser = parserSync()
const parse = parser()
const argv = process.argv.slice(2)
const res = parse(argv)
console.log(JSON.stringify(res, null, 2))We choose to write a synchronous parser by importing parserSync.
Next, we initialize a new parser.
Calling the parser returns a parse function.
The parse function is then used to process argv argument values.
Note, that we skip the first two parameters of process.argv.
Those are always node and the file name.
Finally, we log the result to the console.
Let us now call this minimal shargs parser to see what it prints out.
$ ./git
{
"errs": [
{
"code": "CommandExpected",
"msg": "Expected a command with a string 'key' field and an 'opts' array.",
"info": {"opt": {}}
}
],
"args": {
"_": []
}
}parser reports a CommandExpected error.
The shargs documentation has a table of error codes,
where we can look up CommandExpected.
We find out, that the error is thrown in the toOpts stage.
The problem is, that we did not provide an opt parameter to parser.
Note though, that the parser did work anyway, by reporting an error, helping us along the way.
Our first parser did not parse anything, but instead reported an error.
We can make it work by adding a command:
#!/usr/bin/env node
const {parserSync} = require('shargs')
const {command} = require('shargs/opts')
const git = command('git', [])
const parser = parserSync()
const parse = parser(git)
const argv = process.argv.slice(2)
const res = parse(argv)
console.log(JSON.stringify(res, null, 2))We added a git command that has no options, yet.
git is then passed to parser.
Let us have a look at what changed.
./git --help
{
"errs": [],
"args": {
"_": ["--help"]
}
}The error is gone and we have args.
Note that git does not have options.
This is why "--help" does not mean anything to the parser.
It ends up in the rest array _, that holds all tokens that could not be interpreted.
Let us add a --help option to the git command, next:
// ...
const {flag} = require('shargs/opts')
const opts = [
flag('help', ['--help'])
]
const git = command('git', opts)
// ...We chose to make help a flag, so we can just call --help without any values.
Now we should be able to parse --help:
./git --help
{
"errs": [],
"args": {
"_": [],
"help": { "type": "flag", "count": 1 }
}
}The _ array in args is empty, and we have successfully parsed --help into the help field.
Note that while --help is the command-line argument we use, help (without --) is the field name.
This is reflected in the definition of the flag.
Shargs separates between the external API of providing an argument, and the internal API of storing the values.
--help is parsed into a weird flag object with a count field.
However, we do not need to know, how often --help has been called,
we only need to know if it has been provided at least once.
So we do not need a flag value, a bool is enough:
// ...
const {flagAsBool} = require('shargs/parser')
// ...
const stages = {
args: [flagAsBool('help')]
}
const parser = parserSync(stages)
//...We have modified parser's behavior by adding an args stage to its stages parameter.
flagAsBool transforms a flag with a given key (here 'help') into a bool.
Shargs works in seven different steps that each take one or more stages.
The args step is at the sixth position.
Let us see stages in action.
./git --help
{
"errs": [],
"args": {
"_": [],
"help": true
}
}The help field is now true.
Git has many subcommands that do various tasks.
Let us start by adding the init subcommand:
// ...
const {flag, subcommand} = require('shargs/opts')
// ...
const init = subcommand([])
const opts = [
// ...
init('init', ['init'])
]
// ...For now, init is a subcommand without any options.
Note, that we do not call it as --init, like we did with --help, but just as init.
Shargs does not force you to use a specific syntax for argument names.
You can choose any string you like, as long as it does not contain whitespaces.
Since, by convention, subcommands do not start with --, I decided for this case, to just use init as an argument.
Let us see what calling init yields.
./git init
{
"errs": [],
"args": {
"_": [],
"init": {
"_": []
}
}
}args now has an init field.
Note, that although we did not provide any other arguments and although init has no options,
the init field has a _ array in args.
This is because every command and subcommand may receive arguments it does not recognize
and thus has its own rest array _.
Like commands, subcommands may have options of their own:
// ...
const init = subcommand([
flag('quiet', ['-q', '--quiet'])
])
// ...We have added a quiet flag, that is meant to suppress unnecessary output.
Let us see if it works with init.
./git init -q
{
"errs": [],
"args": {
"_": [],
"init": {
"_": [],
"quiet": {
"type": "flag",
"count": 1
}
}
}
}Indeed, the init field has now a nested quiet field with a flag value.
Many commands count their quiet arguments and hide logging information based on how often -q was passed.
Usually, these commands let you write -qqq instead of -q -q -q.
A feature shargs calls short option groups.
They are supported by adding the splitShortOpts stage:
// ...
const {flagAsBool, splitShortOpts} = require('shargs/parser')
const stages = {
argv: [splitShortOpts],
args: [flagAsBool('help')]
}
// ...Now we should be able to define how quiet the output should be.
./git init -qqq
{
"errs": [],
"args": {
"_": [],
"init": {
"_": [],
"quiet": {
"type": "flag",
"count": 3
}
}
}
}splitShortOpts actually rewrites ./git init -qqq to ./git init -q -q -q internally.
Note that the quiet field count is 3, which is exactly the number of -q arguments.
Like help, we do not need to know in the results, that quiet was a flag.
Storing its count as a number suffices:
// ...
const {flagsAsBools, flagAsNumber, splitShortOpts} = require('shargs/parser')
const stages = {
argv: [splitShortOpts],
args: [flagAsNumber('quiet'), flagsAsBools]
}
// ...We have changed two things, here:
First, we have added the flagAsNumber stage to transform quiet.
Second, we have generalized flagAsBool to flagsAsBools,
a stage that transforms all remaining flags to bools.
./git --help init -qqq
{
"errs": [],
"args": {
"_": [],
"help": true,
"init": {
"_": [],
"quiet": 3
}
}
}The help field is still a bool, while the quiet field is just a number, now.
We have already seen that rest arrays exist, now, let us get a feeling for how they work:
./git --help init -qqq commit
{
"errs": [],
"args": {
"_": [],
"help": true,
"init": {
"_": ["commit"],
"quiet": 3
}
}
}
./git init -qqq --help commit
{
"errs": [],
"args": {
"_": ["commit"],
"help": true,
"init": {
"_": [],
"quiet": 3
}
}
}In the first case, "commit" is still considered a part of the init subcommand.
In the second case, "commit" is stored in the git command's rest array.
The reason for this difference is --help.
Upon reaching --help, the parser tries to find the token in its options.
In the second case, the parser first looks for --help in init's options.
Since it does not find an option with the args --help,
it continues searching in init's parent git.
Here, it finds the help option that has a --help argument.
However, it has left the init subcommand's scope for good and is now back in git's scope.
This is why commit is in git's rest array, while it is still in init's rest array in the first case.
Besides arguments that are passed by argument name (aka options), many commands have arguments that are passed by position. Shargs supports both, options and positional arguments:
// ...
const {flag, string, subcommand, variadicPos} = require('shargs/opts')
const commit = subcommand([
flag('all', ['-a', '--all']),
string('message', ['-m', '--message']),
variadicPos('file')
])
const opts = [
// ...
commit('commit', ['commit'])
]
// ...We have added the new commit subcommand that has three different kinds of arguments:
allis aflagthat has just an argument name (-aor--all), but no argument values.messageis astringoption that has an argument name (-mor--message) as well as an argument value (onestring).fileis a positional argument that has no argument name, only an argument value.fileis also variadic, meaning it takes not one, but any number of values.
Let us test commit.
./git commit -a -m 'First commit' package.json README.md
{
"errs": [],
"args": {
"_": [],
"commit": {
"_": [],
"all": true,
"message": "First commit",
"file": [
"package.json",
"README.md"
]
}
}
}The message field is indeed a string and file collects any number of argument values.
One thing that makes shargs special, is its support for specifying multiple subcommands.
Imagine you could write the following in real git:
./git --help init -qqq commit -a -m 'First commit' package.json README.md
{
"errs": [],
"args": {
"_": [],
"help": true,
"init": {
"_": [],
"quiet": 3
},
"commit": {
"_": [],
"all": true,
"message": "First commit",
"file": [
"package.json",
"README.md"
]
}
}
}Note that shargs does not necessarily retain the subcommand's order.
Since JavaScript objects' order is not specified and depends on the engine,
you should not rely on the object's field order.
Each command has to have a help page.
We can automatically generate a help text based on git with
shargs/usage:
// ...
const {optsList} = require('shargs/usage')
// ...
if (res.args.help) {
const help = optsList(git)()
console.log(help)
} else {
console.log(JSON.stringify(res, null, 2))
}If the args field of the parse results has a truthy help field, we log help to the console.
For now, help is just an optsList that layouts git's options.
./git --help
--help
init
commit We get a plain list of all git options.
Note, that the list is not sorted, but is presented in the order we have specified in opts.
We can enhance the usage documentation by adding descriptions
to git's options:
// ...
const opts = [
flag('help', ['--help'], {desc: 'Print this help message.'}),
init('init', ['init'], {desc: 'Create an empty Git repository or reinitialize an existing one.'}),
commit('commit', ['commit'], {desc: 'Record changes to the repository.'})
]
// ...The --help page is more helpful, now.
./git --help
--help Print this help message.
init Create an empty Git repository or reinitialize an
existing one.
commit Record changes to the repository. --help would be even more helpful, if it would also show the subcommands' options:
// ...
const {optsLists} = require('shargs/usage')
// ...
const init = subcommand([
flag('quiet', ['-q', '--quiet'], {desc: 'Only print error and warning messages.'})
])
const commit = subcommand([
flag('all', ['-a', '--all'], {desc: 'Automatically stage files that have been modified and deleted.'}),
string('message', ['-m', '--message'], {desc: 'Use <string> as the commit message.'}),
variadicPos('file', {desc: 'A list of files to commit.'})
])
// ...
const git = command('git', opts, {desc: 'A simple command-line interface for git.'})
// ...
const help = optsLists(git)()
// ...We have added descriptions to all options, now.
Another update is easy to miss:
Instead of optsList, we use optsLists (plural) to generate our usage documentation.
optsLists recursively documents all subcommand options.
./git --help
--help Print this help message.
init Create an empty Git repository or reinitialize an
existing one.
-q, --quiet Only print error and warning messages.
commit Record changes to the repository.
-a, --all Automatically stage files that have been modified and
deleted.
-m, Use <string> as the commit message.
--message=<string>
<file>... A list of files to commit. All options are now documented.
Have you recognized the line break, because --message=<string> is just a little bit too long?
Let us fix that:
// ...
string('message', ['-m', '--message'], {descArg: 'msg', desc: 'Use <msg> as the commit message.'}),
// ...We have used the descArg field to change message's value description
from <string> to <msg>.
This has given us just enough space to fix the layout.
./git --help
--help Print this help message.
init Create an empty Git repository or reinitialize an
existing one.
-q, --quiet Only print error and warning messages.
commit Record changes to the repository.
-a, --all Automatically stage files that have been modified and
deleted.
-m, --message=<msg> Use <msg> as the commit message.
<file>... A list of files to commit. We can do better than just displaying the options in a list and add some more elements to our help:
// ...
const {desc, optsLists, space, synopses, usage} = require('shargs/usage')
// ...
if (res.args.help) {
const help = usage([
synopses,
space,
optsLists,
space,
desc
])(git)()
console.log(help)
} else {
console.log(JSON.stringify(res, null, 2))
}We have also added synopses (mind the plural) and git's description to help.
usage is used to group these elements together, so we only have to pass the git command once.
./git --help
git [--help]
git init [-q|--quiet]
git commit [-a|--all] [-m|--message] [<file>...]
--help Print this help message.
init Create an empty Git repository or reinitialize an
existing one.
-q, --quiet Only print error and warning messages.
commit Record changes to the repository.
-a, --all Automatically stage files that have been modified and
deleted.
-m, --message=<msg> Use <msg> as the commit message.
<file>... A list of files to commit.
A simple command-line interface for git. The usage documentation comes together nicely!
Now that the contents of our usage documentation are complete, let us finish off by adding some styles:
// ...
const {desc, optsListsWith, space, synopses, usage} = require('shargs/usage')
// ...
const style = {
line: [{width: 50}],
cols: [{width: 20}, {width: 30}]
}
const help = usage([
synopses,
space,
optsListsWith({pad: 2}),
space,
desc
])(git)(style)
// ...First off, we change the left padding of the optsLists to be just 2
with optsListsWith.
Then, we add a style object and pass it to usage.
The style says that lines should be 50 columns wide,
while cols specifies the first column's width to be 20 and the second's to be 30.
You can read up in shargs' documentation (e.g. with optsListsWith),
what style field a component uses for its layout.
You could also invent new style fields and configure components to use those.
Let us print help one last time.
./git --help
git [--help]
git init [-q|--quiet]
git commit [-a|--all] [-m|--message] [<file>...]
--help Print this help message.
init Create an empty Git repository
or reinitialize an existing
one.
-q, --quiet Only print error and warning
messages.
commit Record changes to the
repository.
-a, --all Automatically stage files that
have been modified and
deleted.
-m, Use <msg> as the commit
--message=<msg> message.
<file>... A list of files to commit.
A simple command-line interface for git. The usage documentation is now very compact.
If you do not like it, yet, keep changing style until you are satisfied.
Please report issues in the shargs tracker!
shargs-tutorial-git is MIT licensed.
