My latest project is broken up into a Ruby core (for the server) and a number of front-end modules for things like themes and dashboard design. I needed a way for developers working on the front-end modules (which are structured as Node/npm compatible packages) to be able to install the built files into the core project. Grunt tasks only work in their root directory and the complexity of the tasks called for something that could be extracted out of the projects and be reuseable. So I’m writing a Node CLI utility. The first problem that needed to be solved was option parsing. This is a story of the state of option parsers in Node, how to write one yourself, and why “
eval() is evil” is misunderstood.
Node has a lot of option parsers available but they were all either too bloated or too simple. There was no equivalent of Ruby’s Trollop. Not wanting to get too fancy from the start and knowing that for now we only have a few simple commands we need to run, I decided to write my own option parser. It is a ton easier than some will have you believe. If you need a quick, project-specific parser, feel free to use this as your template. Now, I know you should never write an option parser, a server, or an operating system but I don’t care. Its becoming a great learning experience and it might actually end up being a viable solution.
Step 1: What are my options?
The first thing I did was decide what my options were. At this point there’s only a single option –
install. How will we parse our options? Let’s start by defining them. In my index.js file I started with this code:
1 2 3
Those variables grab the command line arguments and assign them semantic names.
userInput is the string of text the user entered into the terminal with all the gross stuff (interpreter and path to script like
node /home/apps/myScript.js) picked off using
.slice(2). The remaining string is all the stuff from ARGV we want. So now
command is the first thing typed after the name of our app,
mode is the second, and everything after that are options, flags, and values.
doStuff() and you have a text input that asks users “What function do you want to run?”, if the user entered “doStuff” into that text box we could do something like
var input = getElementByTagName('input'); eval(input)(); and our
doStuff() function will run. Now, using
eval() in this way really is evil and I recommend that you for get I ever told you about this. I just wanted to illustrate the concept.
A word on eval
Before we get into using
eval() in our option parser, I want to clarify why there’s such hatred for the function. The
eval() method has its place. There’s a reason that it exists at all. It’s just that the use cases for it are few and far between. Using
eval() on user input is asking for trouble. You just don’t do it the same way you should never run system commands from server-side languages taken from user input. Even when you validate and sanitize the input you still run the risk of something slipping through. So the best practice is to avoid it at all costs. No matter what the situation, before you reach for
eval() think about how you can achieve your goal a different way even if it makes your life harder and you have to refactor a lot of code and some of the user experience. It’s usually worth it in the long run.
So why would we ever use
eval()? In this case we think about the risk. What can go wrong in a command line utility?
- User enters a string that can be eval’d into something harmful
- Another utility calls our utility using this option parser and passes it something that can be eval’d into something harmful
Okay, that sounds scary but how likely is it? Not very. We’re talking about a command line utility being used on the client, not on a server. The utlity is meant to be used by the user who installs it and not required or called on by any other programs. If a user wants to enter something malicious into our utility, that would be dumb. After all, if you require a user to enter commands on the command line then why run it through our options parser at all? If you have terminal access to a computer then just run your malicious commands directly. Why would a user do something malicious to their own system anyway? As far as another utility calling our option parser or another utility that uses our option parser, well, that’s just silly. Again, if you’ve got a utility that’s running in the terminal then just make your utility do malicious things directly. There’s no need for it to go through another tool. If there’s a tool that uses this option parsing technique and it’s installed on a server, how would it ever be called? Your server side scripts should not be calling the system in general and if they really need to then they shouldn’t be doing so based on user input.
The bottom line here is that we’re talking about a command line utility. The danger of
eval() comes from the ability to execute code on a system through that system’s shell. So really, being concerned that a shell command is able to execute other shell commands, besides sounding like Inception, is a non-problem. The real issue here is trusting whoever has shell access at all. Again, a CLI app on its own can do no more damage than a user entering malicious commands on their own.
Now that we have our inputs stored as variables, let’s define some options we want to be able to process. Here’s what our definition would look like:
1 2 3
So we just have on command listed so far but here’s what this does. Our
opt object contains keys whose values are arrays. That sounds confusing but it’s really simple. Let’s take it line by line:
'install'is the key that defines the function we’re going to call. We enclose it in single quotes to pass it on to the evil
eval()which we’ll talk about later
- The array that this key holds as its value is indexed. Right now we’re not doing anything with it but in the future we’re going to use this array to define how the validator should work. The indices are defined as follows:
– The type of value we expect. In this case it’s a string
– The default value of the option. In this case, it’s just a command so there’s no default
– The short option. We use ‘i’ but you can use any string (like
-iif you like to use dashes for flags)
– The help text associated with the option. At some point you should be able to type
utility-name command --helpand this text will show.
Now we need to parse the input from the terminal and run the correct function. Here’s that function:
1 2 3 4 5 6
Here we’re just iterating over all the keys in our opt array and if the
command we captured earlier matches a key then we run the function by that same name. That’s where the
eval() method comes in. We then pass the rest of the input to the function we’re running so that function can take over processing the remaining arguments.
install function. This isn’t part of the option parser but if you were to put this whole script together and run it you’d need a function to call to see it run. So if our CLI utility had an
install action it would run when we ran
my-cli-app install --some --more --options.
1 2 3 4
As you can see, the function takes the remaining user input and does its job from there.
Now, there’s a whole lot missing from this. There’s no validation implemented and it doesn’t know the difference between a command and an option nor does it validate or sanitize it. But this is a starting point. I’m going to be using this for the basis of my option parser and if I find it can be applied more broadly to other CLI apps then I’ll be extracting it out into its own NPM package.