Why writing CLI scripts in CommandBox is better than Node

Why writing CLI scripts in CommandBox is better than Node

Posted by Brad Wood
Mar 03, 2017 06:47:00 UTC

 

 

So this post is a response to some blog comments on this post I made today extolling the features of creating native CLI scripts in CFML with CommandBox.  I mentioned in passing that Node is a popular tool for this, but I felt CommandBox had improved the mousetrap so to speak. I also acknowledged by bais as the lead developer of CommandBox, but I stand by my comments. Adam Cameron, in his eternal quest to keep me honest was a little incredulous though, and wanted to see some better proof of my claims.  Instead of elaborating in the comment section, and since I'm well into the "opinion" territory, I decided to respond in a blog post on my personal blog.  

  1. I know the blog title says CommandBox is better at this than Node.  That was click bait really-- I'm not claiming any wholesale better experience, but I certainly think we do a few things better.
  2. I am a CommandBox guru, and while I'm well-versed in Node, I could easily be missing some things, so I welcome additions and corrections.
  3. This isn't any kind of dig on npm or what it does at all.  Just covering the ways we've improved the experience in my opinion.

Modules vs packages

Packages are a core tenant of the Node ecosystem and this is true in the Box domain as well.  I describe modules as a "smart" package as they have some architectural differences.  The first is the module lifecycle.  Node packages are installed by being dropped into a folder and they just sit there.  They don't do anything though as their mere existence is not enough to affect your application.  They must be explicitly loaded via require() and then invoked.  Modules are packages that are proactively loaded by the framework. (This true of both ColdBox and CommandBox modules)  They have an onLoad and onUnload lifecycle and employ conventions to contribute back to the parent app automatically in the form of:

  1. Models registered in WireBox. These WireBox mappings even have the ability override, extend, or decorate core models with custom ones
  2. Commands are registered with the CommandService which makes them immediately available in tab completion and help.
  3. Interceptors (or event listeners) are attached to the application which allows a module to put its "hooks" into the core API simply by being installed
  4. Config settings are registered with CommandBox and it's ConfigService which are immediately available in the config set and config show tab completion.

Now there are some benefits in the much-simpler "require" method of loading Node modules such as avoiding chicken/egg dependencies and not being loaded until they are used, but I believe it really limits what you can do with them.

Interceptors

The interceptor pattern is foundational to all the Box products, and frankly no other CFML frameworks have really embraced this concept that I've seen.  A ColdBox site, a ContentBox CMS, and a CommandBox CLI have dozens of hooks where you can tap into core behaviors and attach listeners to perform related tasks, or even modify the behavior.  To name a few from CommandBox:

  • onCLIStart
  • onCLIExit
  • preCommand
  • onServerStart
  • onServerStop
  • onException
  • onInstall
  • prePublish

Now, npm and CommandBox both have the concept of package scripts which is a simple way to respond to package-centric events, but package scripts are rather limited.  In npm, they are just a OS shell command that's fired in a process that has read only access to some data via environment variables.  In CommandBox package scripts are run inside the interactive shell which gives them better access to some of our goodies like commands (which npm has no concept of) and CFML functions from the commandline.  

At at the end of the day, package scripts are only run in the context of a given package though.  Interceptors (which you register via a CommandBox module) are another level of CLI-wide integration.  They are not just an OS shell command, but native CFML code that runs inside of CommandBox which direct access to the interceptdata, which it can modify on the fly to actually affect the core event.  For instance, onCLIStart can customize the startup banner.  onInstall can introduce framework-specific conventions for installation paths that actually affects how the install runs.  And onServerStart can listen to all servers and perform tasks like editing your host file.  As far as I know, npm doesn't offer this sort of extensibility of the core.  If you think about it, it can't really-- Node packages simply aren't smart enough to "register" themselves like CommandBox modules do.

Command Architecture

So this is broad, but the entire way commands are defined in CommandBox is just better in my opinion.  Firstly, npm doesn't have a concept of commands really-- it's all bare metal execution of .js files with various riggings to aliases and symlinks to polish their execution.  It works, and fairly well, but executable scripts are basically just .js file. (legacy JavaScript has no class as a first class citizen)  This is the equivalent of using .cfm files in CFML.  They provide no object oriented base to develop on, no encapsulation, and no API.  In fact, the most common way that people create CLI tools with npm involves shebang scripts (which CommandBox supports as well) but than means it doesn't even work out of the box on Windows without additional trickery.  

Commands in CommandBox are a CFC.  They extend a base class that provides handy functionality (this happens even if you leave off the "extends").  They use simple conventions and annotations to declare aliases, help text, tab completion details, and dependency injection.  To a modern developer this just makes way more sense.  Even the way we allow a module to package multiple commands in a folder that, by convention, creates a namespace of commands is unique to CommandBox.  

Command Architecture - Help

There are modules in npm to assist with command help, but it does not come out of the box, does not follow a standard format, and requires more work.  Each Node script can implement their own help shortcut which leads you to the same issues you have with bash when you have to type ?, then /?, then -help, then --help before you finally figure out how the author of this tool decided to implement their help.  CommandBox's help is a consistent first class citizen of the CLI and it is available for every single last command with zero additional code.  The world's simplest command will at least have a help screen that shows you aliases, parameter types and names, and any commands in a nested namespace.  That's with zero work.

 Adding to your command's help is natural because we parse the metadata right from the CFC so your component and argument annotations are picked up.  That means if you write good, commented code, you have automatic help.  The closest you can get in npm is to manually require a library and tell it what you want the help to have.  If you fail to do this, you have no help.

Here's the full, unabridged code of the CommandBox "echo" command.

/**
 * Outputs the text entered.
 * .
 * {code:bash}
 * echo "Hello World!"
 * {code}
 * .
 * This can be useful in CommandBox Recipes, or to pipe arbitrary text into another command.
 * .
 * {code:bash}
 * echo "Step 3 complete" >> log.txt
 * {code}
 * 
 **/
component {

	/**
	 * @text.hint The text to output
	 **/
	function run( String text="" )  {
		return arguments.text;
	}

}

Command Architecture - Parameters

Node CLI tools are just .js files, not proper classes (JavaScript doesn't provide classical OO anyway).  Out of the box, npm does nothing to assist you in declaring your parameters.  You've got to require another library and use a DSL to declare these things.  It's a nice DSL, so it's not too bad, but a run() method with standard parameters just makes more sense to a modern CFML developer, it's an existing format your eyes are used to reading, plus you get out of the box validation when you specify argument types and CommandBox will automatically ask the user for missing parameters that you've marked as required.  This is with no additional code-- just your method signature does this.  And the best part is you can use tools like DocBox to generate API for your commands since they're just standard CFCs.  This is actually how the CommandBox API docs are generated.  

/**
 * @path.hint file or directory to delete
 * @force.hint Force deletion without asking
 * @recurse.hint Delete sub directories
 **/
function run( required path, boolean force=false, boolean recurse=false )  {}

Command Architecture - Parsing

No special code is required to access these parameters like in npm.  Instead, they come right in the arguments scope just like any other method invocation  This makes it even easier to make internal calls to other commands since your command code has nothing tying it to the specific CLI formatting.  Extra parameters passed by the user show up in the arguments scope which is a more natural pattern than what most npm CLI parsing libs do and boolean arguments are automatically recognized as flag both positive and negative:  There are a few dozen different CLI parsers for npm that each provide an approximation, but with slight variations and no experience as seamless as an arguments scope.  The following two examples automatically pass true or false into the force argument.

myCommand --force
myCommand --noForce

Speaking of parameter parsing, CommandBox is the only CLI I know of that uses a custom parser to allow you to use either positional or named parameters.  This is a natural way to invoke CF functions, and it carries over to the CLI.  It gives you the choice of quick and dirty, or better documented scripts.  

CommandBox> rm /myDir
CommandBox> rm path=/myDir

ANSI Formatting

CommandBox and npm are pretty neck and neck on this these days.  CommandBox has had a nice DSL for ANSI formatting since it's inception 3 years ago.  The top libraries for ANSI formatting are 2 to 3 years old, so they've caught up nicely, but I wanted to point out we've been doing it since before it was cool :)

Most ANSI formatters in npm use a chainable DSL which is pretty readable.  Since CommandBox uses CFML, we're able to trim down the boiler plate even more by leveraging onMissingMethod in our DSL to be more liberal with our inputs and allow multiple formatting instructions in a single method.  This is a valid line of code in CommandBox that reads like a sentence.  

print.boldBlinkingUnderscoredBlueTextOnRedBackground( 'Test' );

Most npm libraries would require you to chain at least 5 methods and wouldn't allow the filler words like 'text' which are ignored, but allowed for readability.  You may prefer one or the other, but I really like this one.

Shell interactivity

This is another place where npm and CommandBox both let you do all the basic user interactions. Prompting for text with character masks, and default values, confirming yes/no questions, and capturing Ctrl-C  I do want to point out that no boiler plate is required to do this in CommandBox.  These functions are just available to all commands. I don't care for some of the popular npm libraries for interactivity either.  It's inherently a blocking operation to request user input, but some libs clutter your code with callbacks.

Here's an example from the internet on how to ask the user a question in a Node script using a library called Vorpal:

  self = v;
  self.prompt({
    type: 'input',
    name: 'width',
    default: false,
    message: 'Max width? ',
    },
    function(result){
      if (result.width)
        width = result.width;
  });

Maybe I'm missing something really profound that all that code does, but here's the equivalent in a CommandBox command.  I consider this an improvement.

width = ask( 'Max Width? ' );

Conclusion

So there you have it.  Obviously Node/npm are very powerful and do a lot of the same things that CommandBox has been doing the last 3 years, but hopefully this helps explain the parts where I think CommandBox is making it easier to use, more robust, and more natural for CF devs to write CLI tools.  Believe it or not, I don't consider CommandBox to just be a copycat tool that does some of what npm does in regards to writing native CLI tools. I think it stands on its own.  The biggest issue now is getting people to actually use it.  It really bothers me that there's CFML developers out there writing CLI tools in Node because they don't even realize CommandBox has been doing it in their native tongue for 3 years now!

 

Comments are currently closed

Pete

Love the more in-depth explanation. Thanks for taking the time to write this up. cfml and commandbox are so powerful for the modern web developer.

Brian

You really should try out a more modern backend language. ColdFusion code is difficult to maintain, harder to develop, has a small ecosystem, and gaping security flaws. PHP, Java, Node.js, Python, etc. all have their flaws. But literally everything is better than ColdFusion.

Brad Wood

Hi Brian, I'm having a hard time telling if you're just trolling or not. In case you aren't, I couldn't disagree more with what you've said. You seem to have a circa 1999 understanding of CFML and no modern experience. I would categorically refute each of your points except, perhaps, the community size. I won't do it here though, I've already done so on this blog post:

http://www.codersrevolution.com/blog/cfml-good-discussions-and-misinformation

Site Updates

Entry Comments

Entries Search