loading...
Cover image for Build a command parser with pure javascript

Build a command parser with pure javascript

rallipi profile image Ralf 💥🔥☄️ Updated on ・6 min read

Are you building an application that needs to fetch user input and transform it in any way? Or are you just curious how a command line interface basically works?
Great! This little guide will get you prepared to build your own command parser that fetches input and transforms it based on given command schemes.

The techniques describes here were heavily used while developing the gymconsole app. Of course in a more complex way, but the principles stay the same.
If you're any kind into fitness or self tracking, you should definitifely check out gymconsole. https://gymconsole.app

A working version of the parser can be found here: https://codepen.io/RalliPi/pen/abOrNzZ
Feel free to add new commands and let me know when you built something cool with it.

This guide will be split into 3 main parts:

  1. Defining and structuring the supported commands
  2. Defining supported argumenttypes (the input types your commands can handle)
  3. The actual command parsing

Part 1: Defining commands

For this tutorial we will be storing our command templates in a good old array. For a real application you could store them in a database or whereever you want.
Lets create our command list:

var commands = [

];

Great now we have a list where we put every command into that we want to handle. Let's think about all the data a single commands needs to have.

  1. id or name While this isn't necessarily needed it's always good to give your templates something that makes them identifiable
  2. scheme The scheme is what makes it all work. The scheme is basically a regex that we compare to the user input to determine if the user wanted to trigger a particular command.
  3. list of arguments Most commands will handle some kind of argument. Those are the parameters that we want to transform.
  4. handler The handler is the logic that gets performed when we determined that this is the command the user wants to execute and when we identified all given arguments.

That's all we need. So let's look how such a command template will look like in practice.
Let's start with a very basic command. It will just echo everything back the user gives as argument.

var echoCommandTemplate = {
    name: "echo",
    scheme: "echo $input",
    args: [
        {
            name: "input",
            type: "string"
        }
    ],
    handler: ({input}) => {
      return input;
    }
}

A command template is just a regular javascript object. Nothing to be scared of.
First we're defining a name. Easy.
Now it gets a little more complicated. We define a list or arguments with a name and a type. In this case our command expects a single argument called input which is of type string.
We find this argument again if we look at the scheme. We're using the name of the argument to define where in the command scheme we're expecting to find our input argument.
Later our command parser will check if the user input matches "echo someinput". If it does, our parser knows, that "someinput" is a string argument called input.
The last property of the command template is the handler. It's a regular javascript function that receives all collected arguments as input parameters.

Lets add the command template to our commands list. Your code should look like this now:

var commands = [
    {
        name: "echo",
        scheme: "echo $input",
        args: [
            {
                name: "input",
                type: "string"
            }
        ],
        handler: ({input}) => {
          return input;
        }
    }
];

Part 2: Defining argument types

In our previously created command template we're using the argument "input" of type string. Now we need to tell our parser how to handle string arguments.
So lets create an argument type:

var stringArgumentType = {
    type: "string",
    replace: "([a-z]+)",
    transform: (arg) => {
      return arg
    }
}

This is probably the most complicated part of the tutorial. So let's tackle it step by step.
The type property is needed because it tells the parser which argumenttype to use for a given argument. So in our echo command, the input argument is of type "string", which tells tells the parser that it has to use the argumenttype which has "string" as its type property.
The replace property is a regex. It matches any amount of characters in a row. Eg "amfmfj" or "hello", but not "2345".
This string will replace the "\$input" part of the scheme in the commandtemplate.
In our example command "echo $input" will be replaced with "echo ([a-z]+)".
Et voila, "echo ([a-z]+)" is a regex that we can compare to the input a user gives us.
Finally the transform method tells what to do with the raw argument input. For a string it just returns the raw input again because every input we get from users is already of type string. But if we want to collect eg a number, we have to transform the string to a number manually. We will see how to do this later.

Let's define another argumentType for numbers (integers)

var numberArgumentType = {
    type: "number",
    replace: "([0-9]+)",
    transform: (arg) => {
      return parseInt(arg)
    }
  }

We had to change our replace property, because we want to match numbers now. And we need to adjust the transform method, because the raw input is of type string, but we want to return a number. So we parse the input to a number.

Let's put our commands in an array to have them all available:

var argumentTypes = [
  {
    type: "string",
    replace: "([a-z]+)",
    transform: (arg) => {
      return arg
    }
  },
  {
    type: "number",
    replace: "([0-9]+)",
    transform: (arg) => {
      return parseInt(arg)
    }
  }
]

Part 3: The command parser

We now have everything we need to parse our commands. Let's write the method that does the actual command parsing:

var cmd = (input) => {
  //check all commands
  for(let c of commands){
    var reg = c.scheme
    for(let arg of c.args){
      var argumentType = argumentTypes.find(a => a.type === arg.type);
      if(argumentType == undefined){
        console.log("unsupported argumenttype")
        return;
      }
      reg = reg.replace("$" + arg.name, argumentType.replace)
    }
    var regExp = new RegExp(reg)
    var match = input.match(regExp)
    if(match){
      match.shift()
      var paramObj = {}
      for(var i = 0; i < c.args.length; i++){
        var argumentType = argumentTypes.find(a => a.type === c.args[i].type);
        paramObj[c.args[i].name] = argumentType.transform(match[i])
      }
      return c.handler(paramObj)
    }
  }
  console.log("no matching command found")
}

No worries, it's not as complicated as you might think. I will break down every little piece.
First we loop over every entry of our command list. We do this in order to find the command that matches our input.
To check if a command matches, we need to check if it's scheme matches. But before we can use the scheme, we need to replace the argument placeholders with the actual regex patterns of the argument types.
So we loop over the arguments of the command, find the appropriate argumenttype and replace the placeholders in the scheme (we know it's a placeholder if it starts with a \$ character) with the actual regex pattern of the argument type.
That's what basically transforms our readable scheme eg "echo $input" to "echo ([a-z]+)" which we can use for the actual regex check.

After we constructed the regex pattern, we match it with the user input. If this match succeeds (return value is not undefined), we know that this command matches.

All thats now left to do is extract the arguments from the regex match, transform them to the correct types and pass them to the handler method of the command.

Extracting the arguments is easy, as the match method returns an array of all matches. Element 0 is the complete input, so we can remove that (with the shift method). The other elements are the parts of the input that matched our regex groups. Before we pass them to the handler function, we bring them in a nice and consistent format:

{
  argument1name: argument1value,
  argument2name: argument2value,
}

We do this by looping over the command arguments again and constructing an object with the names as keys and the extracted values as values.
Then we pass that newly created object to the handler and we're done.

Let's see if our command parser is capable of parsing a more complicated command. Let's build a command that sums up two numbers.

var commands = [
    {
        name: "echo",
        scheme: "echo $input",
        args: [
            {
                name: "input",
                type: "string"
            }
        ],
        handler: ({input}) => {
        console.log(input)
        }
    },
    {
    name: "sum",
    scheme: "sum $val1 $val2",
    args:[
      {
        name: "val1",
        type: "number"
      },
      {
        name: "val2",
        type: "number"
      }
    ],
    handler: ({val1, val2}) => {
      return val1 + val2;
    }
  }
];

We added another command template to our commandslist. it's expecting two arguments called val1 and val2 both of type number. the handler will just sum them up and print them to the console.
There really isn't more to do than adding this little snippet to the commands list. Our parser is now able to sum numbers.

And that's it. I hope you learned something by following this tutorial.
If you like this kind of guides, just follow me on twitter. I always anounce new tutorials there. https://twitter.com/rallipi

One final note:
You might be asking why the command scheme isn't directly filled with the regex. That's because with the way we did it. Now 'everybody' can edit and create new schemes for commands and not only the developer.
https://gymconsole.app uses the same replacemement system. That makes it possible that even endusers that don't even know what a regex is are able to configure their own tracking schemes for workouts and any other metric you want to log.

Posted on Apr 9 by:

rallipi profile

Ralf 💥🔥☄️

@rallipi

Hi I'm Ralf! I love programming. fitness and helping other devs!

Discussion

markdown guide