Blog

04
February 2015

Gavin Pickin

How to write your very own CLI Commands in CFML

CFML Language, CFML Server, CommandBox, Lucee, Node.js, Server Admin, Tools and IDEs


I bet half of my audience just tuned out, seeing CFML in the title, but this is easier than writing Node as a Javascript Developer… of course you can prove me wrong. In case you have been sleeping under a rock, I’ll let you know that CommandBox went gold today… which apparently means it made it to 1.0. Its not like musical going gold, where you sold so many copies, going gold meant you made it to 1.0, and your api is solid enough to use in production or something. CommandBox is a CLI, a CFML REPL, Package Manager, and includes a tiny Java Servlet Container, which means you can even spin up a server in any directory. Ortus Solution listened to the cries for a CLI and Package manager, and with Forgebox being extended to other any CFML library or module, not just Ortus / Coldbox libraries and modules, the CFML community finally has it.

I will not go into all the details of what it can do… to be honest there are some great videos already, including the cool Snake game that is built in, great documentation and more coming all the time. There are lots of content on the COOLness of it, let me show you what I have been playing with.

Command Box Demo - released June 2014
Coldbox Developer Week 2014 - Package Management & Automation with CommandBox
CommandBox Site on Ortus Solution’s Website
CommandBox Documentation

Disclosure time, before Feb 1 of 2015… I had only watched the videos, and downloaded the beta, tried out a couple of the simple commands and the REPL. I had not tired to write a recipe, or command, or anything under the covers… so everything I am about to show you, I picked up through the docs, the google group, and maybe some twittering.

My last few blog posts have been about the new CFML engine, LUCEE, and how to configure Lucee and Tomcat, configure apache, and get up and running quickly. I have even had a request to help others to get their dev environments setup so they can avoid conf headaches, so I thought to myself, maybe I can write some commands for this. I encourage you to download CommandBox, and code along with me.

Now, if you are running commandbox from terminal, outside of the commandbox shell, you can write a type a command like this

$ box server list

 

If you run just box, it will load up the CommandBox shell, and then you can enter commands like

$ server list

 

without needing to prefix it with box for each command.

I am going to assume you ran box first, now we do not need to prefix it.
The command we just used is a built in command, and you can see all the server commands are namespaced in the server namespace. For this example I’m going to make a set of commands in the kiwiSays namespace. Our goal is to make some commands like this

$ kiwiSays addWebsite param1 param2
$ kiwiSays startLucee
$ kiwiSays stopLucee

 

So, how do we make our own commands in CommandBox?

CommandBox when run, installs into your user directory, in a hidden folder… called CommandBox. Inside that folder, is a folder for you to add your own commands, called Commands. Anything you put in there, will get read on start up of CommandBox and be available to run. Folders create namespaces… so if we want to make a namespace called kiwiSays, we make a folder in /Users/Gavin/.CommandBox/Commands/kiwiSays/

Inside of that folder, we put any actual commands we want to run. Of course, we do not have to namespace them, but that would be a mess. If we want to have a few levels deep, we could too, but we don’t want to make things too difficult… so lets just leave it as this.
Now, a command is just a cfc… so lets add 3 cfcs into our folder

/Users/Gavin/.CommandBox/Commands/kiwiSays/
addWebsite.cfc
startLucee.cfc
stopLucee.cfc

Inside of those 3 cfcs, to be commands, we simple need to do 2 things… make sure the component extended the BaseCommand, and has a function called run().
As simple as

component extends="commandbox.system.BaseCommand" {
     function run() {
          print.line( ‘Hello World’);
     }

}

 

You can copy that template into each of the 3 cfcs, and modify the comments that print.line prints to the command line, and we can restart CommandBox, and try them out.

$ kiwiSays addWebsite
$ kiwiSays startLucee
$ kiwiSays stopLucee

 

Tada, you built your first commands. Awesome, my job is done.

Wait, you want to learn more than that? Ok, he’s a little more.

So, lets make your code a little better… in the addWebsite command, we want to ask for a websiteURL, and a websitePath.
So lets do that.

//addWebsite.cfc
component extends="commandbox.system.BaseCommand" {
function run( ){
        var websiteURL = ask('Please enter URL: ');
        var websitePath = ask('Please enter file path: ');
               print.line(‘The URL is: ‘ & websiteURL);
               print.line(‘The Path is: ‘ & websitePath);
     }
}

 

Now, when you reset CommandBox and run the following command
$ kiwiSays addWebsite

We get this output, I added the responses as it prompted me, line by line.

$ kiwiSays addWebsite
Please enter URL: www.gpickin.com
Please enter file path: /www/www.gpickin.com
The URL is: www.gpickin.com
The Path is: /www/www.gpickin.com

 

Now, thats pretty cool, we can use the Ask function to ask for info… but it would be nice if we could use tab completion in our paths, right?
Well, unfortunately, you can’t do that in the Ask function currently… but… you can at the command line… so lets change those 2 asks, into arguments.

// addWebsite.cfc
component extends="commandbox.system.BaseCommand" {
      function run( required string websiteURL, required string websitePath ){
           print.line('The URL is: ' & websiteURL);
           print.line('The Path is: ' & websitePath);
     }
}

 

So restart CommandBox and run our command with the 2 arguments

$ kiwiSays addWebsite www.gpickin.com /www/www.gpickin.com
The URL is: www.gpickin.com
The Path is: /www/www.gpickin.com

 

Very cool, and if you try it, tab completion works.
Note: you can do ordered arguments, or named, so thats pretty slick.

EDIT: I just found out that they have some built in tools to help with paths, so I will go ahead and add this to the code, to resolve the path, so you can use relative or full paths.
arguments.websitePath = fileSystemUtil.resolvePath( arguments.websitePath );

What if people don’t know you need to add the arguments, and run it without them… will it error?
No, CommandBox sees they are required, and prompts you for them… slick or what?
So lets see what that looks like.

$ kiwiSays addWebsite
Enter websiteURLwww.gpickin.com
Enter websitePath/www/www.gpickin.com
The URL is: www.gpickin.com
The Path is: /www/www.gpickin.com

 

Well, that is cool, but its ugly, its showing the variable name, no spacing… wouldn’t it be cool if… yes, yes it would be, and it is. You can use javadoc annotations to give hints to the arguments, and those same hints power the awesome HELP features too. This is how we add that.

component extends="commandbox.system.BaseCommand" {
        /**
* @websiteURL.hint The Website URL
* @websitePath.hint Path to the Website Directory
*/
    function run( required string websiteURL, required string websitePath ){
        arguments.websitePath = fileSystemUtil.resolvePath( arguments.websitePath );
        print.line('The URL is: ' & websiteURL);
        print.line('The Path is: ' & websitePath);
    }
}

 

Now, restart and run again, and lets see what that does.

$ kiwiSays addWebsite
Enter websiteURL (The Website URL) :www.gpickin.com
Enter websitePath (Path to the Website Directory) :/www/www.gpickin.com
The URL is: www.gpickin.com
The Path is: /www/www.gpickin.com

 

That is much better. It still shows the argument name, but thats ok, if its a terrible argument name, change it… but the hint works.
What about the help I mentioned… lets see how that looks.

$ kiwiSays addWebsite help
**************************************************
* CommandBox Help for kiwiSays addWebsite
**************************************************

kiwiSays addWebsite

Arguments:
required string websiteURL (The Website URL)
required string websitePath (Path to the Website Directory)

 

Ok, what if they want help for the whole namespace?

$ kiwiSays help
**************************************************
* CommandBox Help for kiwiSays
**************************************************
Here is a list of commands in this namespace:
kiwiSays addWebsite
kiwiSays startLucee
kiwiSays stopLucee

 

To get further help on any of the items above, type "help command name".
Impressed yet?

After using linux, and bash, I’m just loving how easy this is, its fast, and we have only scratched the surface. 
Lets jump ahead a little bit, and do something useful.

Thats pretty cool, simple, but cool. So now, lets output a Virtual Host for Apache. We’ll use the same arguments, but output a Virtual Host Definition.

// addWebsite.cfc
component extends="commandbox.system.BaseCommand" {
/**
* @websiteURL.hint The Website URL
* @websitePath.hint Path to the Website Directory
*/
function run( required string websiteURL, required string websitePath ){
          var apacheConf = "";
          arguments.websitePath = fileSystemUtil.resolvePath( arguments.websitePath );
          apacheConf = apacheConf & '<virtualhost :80="">';
          apacheConf = apacheConf & 'DocumentRoot "#websitePath#"';
          apacheConf = apacheConf & 'ServerName #websiteURL#';
          apacheConf = apacheConf & 'Include /www/_servers/conf/inc_lucee_conn.inc';
          apacheConf = apacheConf & '</virtualhost>';
          print.line().line( apacheConf );
     }
}

 

Just a simple Virtual Host, we use our websitePath for the DocumentRoot, we use the websiteURL for the ServerName, and I have an include in there which would contain my Lucee Connection Information (I’m planning ahead). Lets run it and see what happens.
Without arguments

$ kiwiSays addWebsite
Enter websiteURL (The Website URL) :www.gpickin.com
Enter websitePath (Path to the Website Directory) :/www/www.gpickin.com
<virtualhost *:80>
ServerAdmin myemail@mydomain.com
DocumentRoot "/www/www.gpickin.com"
ServerName www.gpickin.com
Include /www/_servers/conf/inc_lucee_conn.inc
</virtualhost>

 

With arguments and tab completion for the path :)

$ kiwiSays addWebsite  www.gpickin.com /www/www.gpickin.com
<virtualhost *:80>
ServerAdmin myemail@mydomain.com
DocumentRoot "/www/www.gpickin.com"
ServerName www.gpickin.com
Include /www/_servers/conf/inc_lucee_conn.inc
</virtualhost>

 

So, you can copy and paste that into your httpd.conf or you could put it in a virts.conf file… or if you read my previous article, I make a directory for all my individual Virtual Host conf files, and then use the following line in my httpd.conf file to include them all
//httpd.conf - bottom of the file

Include /www/apacheconfs/*.conf

So you could copy from the CLI and then paste them into a new file called www.gpickin.com.conf in that folder… but why do all that work, when the command can do that. 
How long would that take you in Node, or Bash? 
I don’t know, but I’ll tell you how long with CFML… 1… 2… done!

// addWebsite.cfc
component extends="commandbox.system.BaseCommand" {
         
        /**
* @websiteURL.hint The Website URL
* @websitePath.hint Path to the Website Directory
*/
    function run( required string websiteURL, required string websitePath ){
        var apacheConf = "";
        arguments.websitePath = fileSystemUtil.resolvePath( arguments.websitePath );
        apacheConf = apacheConf & '<virtualhost *:80>#CR#';
        apacheConf = apacheConf & 'ServerAdmin myemail@mydomain.com#CR#';
        apacheConf = apacheConf & 'DocumentRoot "#websitePath#"#CR#';
        apacheConf = apacheConf & 'ServerName #websiteURL##CR#';
        apacheConf = apacheConf & 'Include /www/_servers/conf/inc_lucee_conn.inc#CR#';
        apacheConf = apacheConf & '</virtualhost>#CR#';

        print.line().line( apacheConf );
        fileWrite( '/www/apacheconfs/# websiteURL#.conf', apacheConf );
    }
}

 

Restart CommandBox and run it, and hey presto… file created, with the #CR# it even inserted the OS's carriage return for me so it formatted nicely, you can open it up and see it. 
Awesome, simple, easy to write, easy to use… if it restarted Apache, it would be the ultimate command. So I’ll show you how to do that.
Of course, you have to make sure you can stop and start apache without having to add a password, so you could just add your user to the SuDoers file like this SuperUser.com post mentions.
http://superuser.com/questions/381337/passwordless-sudo-apachectl

Running a shell command from inside of CFML, not easy normally right? Well, we are in a turbo charged shell, like Ask and print.line() we have a lot of tools at our command, read the docs for more info, but we can run a command, so lets do that.
In a normal shell we would do this:

$ sudo apachectl restart

In commandbox’s shell, we would do the following:
$ run ‘sudo apachectl restart'

From a command running in command box’s shell, we need to do this
$ runCommand( “run ‘sudo apachectl restart’”);

So it looks like this

// addWebsite.cfc
component extends="commandbox.system.BaseCommand" {
         
        /**
* @websiteURL.hint The Website URL
* @websitePath.hint Path to the Website Directory
*/
    function run( required string websiteURL, required string websitePath ){
        var apacheConf = "";
        apacheConf = apacheConf & '<virtualhost *:80>#CR#';
        apacheConf = apacheConf & 'ServerAdmin myemail@mydomain.com#CR#';
        apacheConf = apacheConf & 'DocumentRoot "#websitePath#"#CR#';
        apacheConf = apacheConf & 'ServerName #websiteURL##CR#';
        apacheConf = apacheConf & 'Include /www/_servers/conf/inc_lucee_conn.inc#CR#';
        apacheConf = apacheConf & '</virtualhost>#CR#';

        print.line().line( apacheConf );
        fileWrite( '/www/apacheconfs/# websiteURL#.conf', apacheConf );
        runCommand( "run 'sudo apachectl restart’" );

    }
}

 

Restart commandbox, and now, we run our command, and as well as output to the screen, and write a file, it now restarts apache for us.
Its taken me a lot longer to write this post than just to build the whole thing.

Now, me being me, and you being you, you probably saw a lot of things in there that aren’t perfect. If I was going to release this command onto forge box, it wouldn’t work for most people, since i hardcoded paths, etc… so thats not very useful. You are right, but I could pass those in as params, or better yet, have defaults set in a json file that my command reads, and uses them, so you could modify the json, or, I could make a command to save settings in the json.

All good answers, but what I plan on doing is this… if you try to use a command that needs a setting, that isn’t set yet, I can ask you for it the first time, and then save it in the json for future requests, unless you override it in a one off command, or set a new default with a settings namespace of commands.

I think we did pretty good today… we have a useful tool built, in minutes, next time, we’ll build commands for startLucee and stopLucee, and we’ll call them from our addWebsite command, after producing the XML snippet we need for Tomcat.

If you don’t think that is cool enough, maybe I’ll make a new command that does the following

  • Check if the directory you enter exists, if not, create it.
  • Install ColdBox to that directory
  • Create the Apache Conf
  • Restart Apache
  • Create the Lucee XML
  • Restart Lucee
  • Launch a Browser and point it at that URL and see your new Coldbox app installed and running.

 

See the power in CommandBox yet?

Maybe you’re thinking, I don’t use ColdBox… well, you’re right… because we don’t use ColdBox for everything we do, but we use some type of template. What about some Git Integration to pull down a Template for your company… maybe choose a Starter Template of your own… the power is in your hands, and with it being as simple as CFML, but as powerful as a CLI… the options are endless.

If you didn’t follow along, try it out, its pretty cool to spin up something so quick, with no real learning curve.
Its better than I thought, and I’ve only played with it for a couple of hours… I wonder what I can build with some time.

Thanks for reading… start playing today.
 

by Brad Wood
02/05/2015 09:20:01 AM

Thanks for the clarification on 'going gold'. I couldn't figure out how I was going to get enough CommandBox listens on Spotify to justify the status! :)

by James
02/08/2015 01:05:38 PM

I tuned in because it said CFML. Nice article

Blog Search