ES6 Command-Line Parsing
ES6 Command-Line Parsing
ECMA Script has come a really long way and continues to add more great features. A lot of them borrowed from functional languages and libraries. Typically there isn’t a lot of need for command-line argument parsing in this manner, there’s a lot of great tools and libraries that already exist for handling more complex situations. There are times when you just want to write a quick little CLI tool to do one-off work. Without futher ado, here’s how to do it.
Desired input
Let’s take a look at what we want the startup to look like. Typically you can run something like
1
node app 2 3
The problem is that when you use the default node tools, process.argv
, to get the agruments passed in, you read everything, including node
and app
in the above example. The startup parameters aren’t very clear either. What are 2 and 3? If you wrote the code, I would hope you would know, but maybe you inherited this or you wrote this tool for other devs on your team. What about them?
Doesn’t this look nicer?
1
node app --base 2 --exponent 3
Just by looking at the parameters, you have a good idea of what might happen. Also, what we’re going to implement, the order of the parameters doesn’t matter, other than they will be expected to be paired in such a way that it always follows the convention of name followed by value.
Without parsing
Command:
1
node app 2 3
Code in app.js
:
1
2
3
4
5
6
7
8
9
const inputs = process.argv.slice(2)
console.info(
`${inputs[0]} raised to the power of ${inputs[1]} = ${Math.pow(
inputs[0],
inputs[1]
)}`
)
process.exit(0)
Parsing function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function parseArgs(args) {
try {
return Object.assign(
...args
.slice(2)
.reduce((l, r) => {
if (!Array.isArray(l)) l = [[l, r]]
else if (r.startsWith('--')) l.push([r])
else l[l.length - 1].push(r)
return l
})
.map(p => ({ [p[0].substring(2)]: p[1] }))
)
} catch (e) {
console.error('Unknown argument passed', e)
return {}
}
}
Let’s break this down.
-
We’re using the spread operator
...
onargs
to expandargs
and iterate. Essentially we’re ensuringargs
is an array. So we can perform the next set of operations and also for the very last step below. -
.slice(2)
is being used to drop the first 2 arguments, which would benode
andapp
. -
Now we can use
.reduce(...)
. Reduce takes a function that takes two arguments and returns a single output. Many times you will see it depicted as taking aleft
andright
argument, but it’s reallyaccumulator
andvalue
. You merge thevalue
to theaccumulator
. I say merge, because you’re reducing. The operation that reduces thevalue
into theaccumulator
is entirely up to you, add, subtract, multiply,Array.prototype.push
are all examples. -
The function we are using to reduce will start off by taking the first two elements of the array,
l
will get index 0 andr
will get index 1 on the very first run. Herel
is a value and not an array, so we turnl
into a multi-dimensional array of[[l, r]]
, in our example[['--base', '2']]
. The second iterationl
will be the multi-dimensional array andr
will be the next flag, in our example'--exponent'
. So far the accumulator will look like[['--base', '2'], ['--exponent']]
. For the last pass through we call.push()
on the last array in our multi-dimensional array. This gives us a result of[['--base', '2'], ['--exponent', '3']]
. If you have more, the process repeats until the end of the array. You can see here that it’s vital, and expected that you pass in paired parameters. Having a straggler without a name won’t work with this code. -
Next we’re calling
.map()
on the resulting multi-dimensional array. The map function performs an operation on each element of the array and returns the result. The end result is an equally sized array with the mapped values. For ours we are simply converting each array pair into an object. You’ll see thesubstring()
that’s removing the expected leading--
on each name. The array['--base', '2']
becomes the object{ base: '2' }
. Leaving you with an array of objects. -
The last piece of the puzzle is the
Object.assign
that everything is wrapped in. Notice that we’re using the spread operator at the beginning. This allows us to spread the object assign over each element in the array. Leading to one object{ base: '2', exponent: '3' }
, which we return as the result of the function, or an empty object if there was an error.
All together
Your app.js
should look like this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function parseArgs(args) {
try {
return Object.assign(
...args
.slice(2)
.reduce((l, r) => {
if (!Array.isArray(l)) l = [[l, r]]
else if (r.startsWith('--')) l.push([r])
else l[l.length - 1].push(r)
return l
})
.map(p => ({ [p[0].substring(2)]: p[1] }))
)
} catch (e) {
console.error('Unknown argument passed', e)
return {}
}
}
const opts = parseArgs(process.argv)
console.info(
`${opts.base} raised to the power of ${opts.exponent} = ${Math.pow(
opts.base,
opts.exponent
)}`
)
process.exit(0)
And call it
1
2
$ node app --base 2 --exponent 3
2 raised to the power of 3 = 8
or
1
2
$ node app --exponent 3 --base 2
2 raised to the power of 3 = 8
opts
will now have the value of { base: '2', exponent: '3' }
no matter which way you ran the command line. It’s also a lot easier to read in the code instead of trying to figure out which variable is in what index.
I also have a Gist available for easy commenting and copying.