All pages
Powered by GitBook
1 of 36

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Developing For CommandBox

CommandBox is written in CFML, which puts any CFML developer in a position to easily build upon it for their own needs. CommandBox is not only a CLI tool and collection of pre-built commands, but also an extensible framework for building your own CFML-based automations. The core is modular such that anyone can extend it themselves or via the help of community modules hosted on ForgeBox, or any other installation endpoint.

Modularity

The unit of re-use in CommandBox is the module. A module is nothing more than a folder containing some settings and code that follows a convention. Modules mean that you can develop your own additions and customizations to the tool without needing to modify the core code.

Read about Developing Modules here.

Custom Commands

CommandBox's expressive CLI gets its power from commands. You can create your own commands by packaging simple CFCs inside of modules. This is a powerful way to create custom, reusable CFML scripts that interact with the user, automate hard tasks, and can be shared with other developers. Commands are better than stand alone CFML files because they have very well-defined parameters, validation, and WireBox DI.

Read about Developing Commands here.

Event Model

Customizing the internals of CommandBox is achieved via an event model known as interceptors. What this means is that at pre-defined points in the lifecylce of the shell, command execution, or web server starting, the CLI broadcasts events that you can listen to. This lets you provide custom error handling, special server handling, or modify command output. Interceptors are packaged up in modules and can be combined with custom commands and config settings for fully-configurable shell add-ons.

Read about Developing Interceptors here.

Modules

The unit of re-use in CommandBox is the module. A module is a folder containing some settings and code that follows a convention. Modules allow you you to develop your own additions and customizations to the tool without needing to modify the core code. If you have worked with the ColdBox MVC framework, CommandBox modules are very similar to ColdBox modules. If not, don't worry, they're very simple to learn.

Build Your First Module

A module is a folder that minimally contains a ModuleConfig.cfc file inside it. User modules are stored in the ~/.CommandBox/cfml/modules/ directory on your hard drive. Let's navigate to that folder and create a new sub directory called test. Now inside of our new folder, create a new file called ModuleConfig.cfc and place the following in it.

~/.CommandBox/cfml/modules/test/ModuleConfig.cfc

component {
    function configure(){}

    function onCLIStart() {
      shell.callCommand( 'upgrade' );
    }
}

This module runs the upgrade command every time the CLI starts up to check and see if you have the latest version. Pretty cool, huh?

Use Intercept Data

Let's take it a step further. Since this upgrade check could get annoying, let's only run it if we're starting the shell in interactive mode (with the blinking cursor awaiting input). That way, one-off commands from the OS shell that exit right away won't trigger the update check. Our onCLIStart() method gets an argument called intercept data that has a flag for this.

function onCLIStart( interceptData ) {
  if( interceptData.shellType == 'interactive' ) {
    shell.callCommand( 'upgrade' );
  }
}

Use A Config Setting

One final improvement. Create a module setting called checkForUpdates which defaults to true. Users can set it to false to disable the update check. Here is the final version of the ModuleConfig.cfc:

component {
    function configure(){
        settings = {
            checkForUpdates=true
        };
    }

    function onCLIStart( interceptData ) {
        if( interceptData.shellType == 'interactive' && settings.checkForUpdates ) {
            shell.callCommand( 'upgrade' );
        }
    }
}

Now run the following command to override our module's default setting and turn off the update check.

config set modules.test.checkforupdates=false

Reload the shell and you'll see the update check is gone.

Share The Love

Now that you've written your first module, you can share it with the world. The following commands run from the root folder of your module will turn it into a package:

package init name="Update Checker" version=1.0.0 slug="commandbox-update-checker"
package set type=commandbox-modules

Drop the contents of your test folder in a Github repo and now anyone can install it in their own CommandBox installation with the install command using the Git endpoint.

Aliases

A command can go by more than one name. For instance, the dir command can also be called as ls, ll, or directory. Set a comma-delimited list of aliases in your component declaration like so:

component aliases="ls,ll,directory" {
}

Command aliases don't even have to be in the same namespace. For instance, the server start command has an alias of just start. This can be very powerful as you can install a command that "overwrites" a built-in command by aliasing itself as that command.

With power comes responsibility though. Make sure you don't distribute a command that accidentally stomps all over another command. Unless your command name is decidedly unique, it's probably best for most custom commands to use a namespace to avoid conflicts.

User Settings

The defaults for a module is stored in a settings struct in the ModuleConfig.cfc for that module. However, users can override those settings without needing to touch the core module code. This is made possible by special namespace in the CommandBox config settings called modules.

Consider the following module settings:

/modules/TestModule/ModuleConfig.cfc

component{
    function configure(){
        settings = {
            mySetting = 'isCool',
            somethingEnabled = true
        };
    }
}

The following config set commands will create config settings that override those. The pattern is modules.moduleName.settingName.

config set modules.TestModule.mySetting=overridden
config set modules.TestModule.somethingEnabled=false

When a module is loaded, the config settings (that exist) for that module are also loaded and overwrite the defaults from ModuleConfig.cfc. You can easily see what settings are set for our TestModule like so:

config show modules.TestModule

Lifecycle Methods

Every module follows a sequence of steps when it is loaded and unloaded. Modules are automatically loaded for you when CommandBox starts up, but here are some ways for a module to affect how it loads.

Life-cycle Events

There are two life-cycle callback events you can declare in your ModuleConfig.cfc:

  • onLoad() - Called when the module is loaded and activated

  • onUnLoad() - Called when the module is unloaded from memory

This gives you great hooks for you to do bootup and shutdown procedures for this specific module.

function onLoad(){
    log.info('Module loaded successfully.' );
}

function onUnLoad(){
    log.info('Module unloaded successfully.' );
}

Custom Events

The ModuleConfig.cfc object itself is an interceptor so you can declare all of the CLI's interception points in the configuration object and they will be registered as interceptors.

function preCommand( interceptData ){
    // I just intercepted ALL Commands in the CLI
    log.info('The command executed is #interceptData.CommandInfo.commandString#');
}

Using Parameters

Regardless of whether your command is called with named parameters, positional parameters or boolean flags, you'll access them the same way: via the standard CFML arguments scope. The user will be prompted for required parameters if they haven't provided them, and the defaults you configured will also work just like you expect.

If the parameters were escaped when typed into the command line, you will receive the final unescaped version in your command.

Dynamic Parameters

Users can pass named or positional parameters that aren't declared, and they will come through the arguments scope. Named parameters will be accessable as arguments.name, and positional parameters as arguments[ 1 ], arguments.[ 2 ], etc.

This can allow for powerful commands like package set that allows users to set any box.json property they want.

package set foo=bar

File System Paths As Parameters

If your command accepts a file or folder path from the user, you'll want to resolve that path before you use it. To do this, use the resolvePath() method that is available to all commands via the BaseCommand class. (This method wraps the resolvePath() method of the fileSystemUtil object that is injected into all commands.) The method resolvePath() will make the file system path canonical and absolute. This ensures you have a fully qualified path to work with even if a user might passed a folder relative to their current working directory passed something like ../../.

component extends="commandbox.system.BaseCommand" {

    function run( String directory )  {
        // This will make each directory canonical and absolute
        arguments.directory = resolvePath( arguments.directory );
        return arguments.directory;
    }
}

If you run that command and pass a full file path such as C:\sandbox\testSite, you would get that exact same path back as the output.

However, if you changed the interactive shell to the C:\sandbox directory and then ran the command with testsite as the input, the relative path would now still resolve to C:\sandbox\testSite.

If, from the same directory, you passed testsite/foo/bar/../../, you would still get C:\sandbox\testSite as the path.

Error Handling

onException

Announced any time an unhandled exception is thrown.

interceptData

  • exception - Error struct

CLI Lifecycle

onCLIStart

Announced when shell first starts, but before any commands are run or output has been flushed to the console.

interceptData

  • shellType - The string interactive if starting the interactive shell, or command if running a one-off command and exiting

  • args - An array of arguments provided from the OS when box was executed.

  • banner - A string containing the CommandBox banner text that displays when in interactive mode.

This fires every time the reload command runs and a fresh shell is created.

onCLIExit

Announced right before the shell exits and control is returned back to the OS. This fires every time the reload command runs right before the shell is destroyed and re-created.

Module Lifecycle

preModuleLoad

Announced before each module that is loaded.

interceptData

  • moduleLocation - Path to the module

  • moduleName - Name of the module

postModuleLoad

Announced after each module that is loaded.

interceptData

  • moduleLocation - Path to the module

  • moduleName - Name of the module

  • moduleConfig - Struct representing the configuration data for the module.

preModuleUnLoad

Announced before each module that is unloaded.

interceptData

  • moduleName - Name of the module

postModuleUnload

Announced after each module that is unloaded.

interceptData

  • moduleName - Name of the module

Conventions

Module Conventions

What makes modules so easy to use is the convention-over-configuration approach that lets you use expected file and folder names instead of manually configuring things. Here are some of the high-level conventions that modules use.

Configuration

The settings for a module is stored in the ModuleConfig.cfc file in the root of the module. This CFC is automatically created and its settings extracted when the module is loaded. It has lifecycle methods like onLoad() and onUnLoad() that will be called automatically if they exist. This CFC will also be loaded up as an interceptor, so any method names that match interception points will be registered. .

Models

CommandBox ships with WireBox, which is the glue that holds everything together. Your custom modules can contain as many models as they like. Place your CFCs (beans, services, etc) in the models folder and WireBox will automatically map them as modelName@moduleName so they can easily be injected into any other model, interceptor, or command CFC.

Commands

One of the primary purposes of modules is to serve as a delivery mechanism for custom commands. All command CFCs should be placed in the commands folder inside your module. Any CFCs there will automatically be registered when the module is loaded, and will be available in the help command, as well as tab-completion, etc. To create namespace commands, create extra sub folders inside the commands folder that match the name of the namespace.

Interceptors

Any interceptor CFCs packaged inside a module should go in an interceptors folder. Interceptors are not loaded automatically though. You'll need to reference the component path in your ModuleConfig.cfc. The path that points to the interceptors folder can be resolved in your config file as #moduleMapping#.interceptors.MyInterceptorName.

Modules

Wait, what-- modules can have their own modules?? Yep, we call that "module inception" and it's one of the most powerful features of modular development. If your module depends on another CommandBox module, then specify that dependency in your module's box.json and it will automatically be installed for you inside your modules folder.

Loading Ad hoc Jars

If you need to use a 3rd party jar, we recommend you use the extra parameters to the createObject() function, which allows you to specify a list of jars to load from (this is a Lucee-specific feature). Read up on the context parameter here:

There are some scenarios however that don't work. One is if you need to use the createDynamicProxy() BIF to create CFC instances that implement Java classes that exist in an ad hoc jar. Lucee currently requires those classes to be loaded by the Lucee system classloader.

As such, CommandBox gives you a mechanism to load jars into the system class loader that was used to classload Lucee so those classes are available everywhere. This is a much better and portable solution to dropping the jars in the ~/.CommandBox/lib folder and restarting the shell.

classLoad()

To load up your custom jars on the fly, call the classLoad() method which is available in any custom command or Task Runner.

You can pass either an array or list of:

  • Directories

    • All jar and class files will be loaded from each directory recursively

  • Jar files

    • Each jar file will be loaded

  • Class files - Each class file will be loaded

Note, paths need to be absolute when you pass them in! Here's some more examples.

Public Properties

You can control how CommandBox loads your module by setting optional settings in the this scope.

  • this.autoMapModels - Will automatically map all model objects under the models folder in WireBox using @modulename as part of the alias.

  • this.modelNamespace - The name of the namespace to use when registering models in WireBox. Defaults to name of the module.

  • this.cfmapping - The CF mapping that will be registered for you that points to the root of the module. Defaults to name of the module.

  • this.disabled - You can manually disable a module from loading and registering.

  • this.dependencies - An array of dependent module names. All dependencies will be registered and activated FIRST before the module declaring them

Error handling

When a command throws an unhandled exception, any output in its print buffer will be flushed to the screen. Then the error message accompanied by a tag stack will be output to the screen and the user will be returned to the prompt.

If an unexpected error happens inside a command that is non-recoverable, do not attempt to try/catch it unless you can improve the error message to something more useful. Generally speaking, just let the error bubble up and be handled by CommandBox for consistency and simplicity.

Controlled Errors

If there are expected situations such as a file not existing, that you know might go wrong, we wholeheartedly recommend checking for these situations and using the error() method to alert the user.

Errors returned from the error() method will not contain any stack traces, etc.

You can set a detail and an error code as well. The error code will be used as the exit code for the command.

The CFML exception that is thrown from the error() method will contain the exit code in the errorcode part of the cfcatch struct. The Exit code is also available as a system setting called exitCode.

Verbose Errors in the Shell

When an unhandled exception happens in the shell, only the tag context is shown. You can enable full stack trace output like so:

Watchers

CommandBox has a powerful utility that can be used to watch a folder of files for changes and fire arbitrary code when changes happen. The utility will block execution of the command until the user stops it with Ctrl+C. To use a watcher in your command, there is a method called watch() in the base command class that you can call. It has a nice DSL of chainable methods to configure it.

Here's a rundown of the methods used above in the DSL.

  • paths( ... ) - Receives a comma-delimtied list of globbing patterns to watch for changes. (defaults to **)

  • inDirectory( ... ) - Set the base directory that the file globs are relative to. (defaults to current working directory)

  • withDelay( ... ) - Set the number of milliseconds between polling the file system. (defaults to 500 ms)

  • onChange( ... ) - Pass a closure to be executed when a change has occurred.

  • start() - Starts the watcher. Always call this at the end of the DSL chain

FusionReactor for the CLI

If you are working on CommandBox custom commands or Task Runners and have a FusionReactor license, you can use that license to track what commands are running at the CLI, including HTTP requests and database calls. This is super handy for debugging your code at the CLI. CommandBox even reports its startup times to FusionReactor

In order to set this up, you'll need the fusionreactor.jar in a folder somewhere. Note, you'll probably get errors if you try to start more than once instance of the shell while FR is enabled. Also note this has nothing to do with using FR for a web server. We have a module for that-- this is JUST for using FR from the CLI!

JVM Args

How you set the JVM args for the actual CLI process differs based on your OS. For Windows, we'll use a little feature of the Launch4J binary and create a file called box.l4j.ini placed in the same directory as the box.exe file. Place one JVM arg on each line:

Any backslashes need to be escaped. You can modify the port to your liking as well. For Mac or Linux, you can pass extra JVM args into the CLI process by setting an environment variable in bash prior to running box.

You can set the debugger lib and license the same in *nix, I just omitted it above for brevity.

Web UI

When CommandBox first starts, you should see some extra output in the console from FusionReactor. Now you can navigate to the port you set on localhost in a browser and se the Transactions menu. Don't look under Web Requests as you won't find anything there. You want Transactions! Running commands will show up in the activity page and completed commands will show in the history.

All of the standard FR functions you're used to including memory/CPU tracking JDBC requests, stack traces, the profiler and debugger are all available to you! This works because FusionReactor has an API that CommandBox uses to report what it's doing automatically.

You even get sweet trans breakdowns. Here is a coldbox create app command that runs several HTTP calls and a nested package set command.

component{
    // Module Properties
    this.autoMapModels = true;
    this.modelNamespace = "test";
    this.cfmapping = "test";
    this.dependencies = [ "otherModule", "coolModule" ];

  function configure(){}
}
error( "I don't like your tone of voice" );
error( "We're sorry, but happy hour ended 20 minutes ago.", "Sux 2B U", 123 );
> !git status
fatal: not a git repository (or any of the parent directories): .git
Command returned failing exit code [128]

> echo ${exitCode}
128
config set verboseErrors=true
watch()
    .paths( '**.cfc' )
    .inDirectory( getCWD() )
    .withDelay( 5000 )
    .onChange( function() {

        print.line( 'Something changed!' );
        command( 'testbox run' )
            .run();

    } )
    .start();
/modules/ModuleName/ModuleConfig.cfc
/modules/ModuleName/models/
/modules/ModuleName/commands/
/modules/ModuleName/interceptors/
/modules/ModuleName/modules/
Read more about module configuration here
Read more about command configuration here
Read more about interceptor configuration here

Custom Interception Points

You have the ability to create your own interception points on top of the core ones in CommandBox. This can be handy to employ your own event listener model within a module-- which could be comprised of multiple models and commands. Interceptors that listen to custom interception points work the exact same as interceptors that listen to core interception points. In fact, the same interceptor file can listen to both.

Register

To register your custom interception point with InterceptorService, place the following config in your module's ModuleConfig.cfc. This module is registering a custom interception point called onCustomEvent.

component{

  function configure(){

    interceptorSettings = {
        customInterceptionPoints = 'onCustomEvent'
    };

  }
}

Announce

It's up to you to decide when to announce this event and what intercept data you want to provide to it. To announce, use the announceInterception method in the InterceptorService. Here's a model that shows how:

component{
    property name="InterceptorService" inject="InterceptorService";

    function doSomethingAmazing(){

        interceptorService.announceInterception(
            state='onCustomEvent',
            interceptData={
                data='foo',
                moreData='bar'
            }
        );

    }
}

Injection DSL

Everything inside CommandBox is built via Wirebox. This means your models, commands, and interceptors can all take advantage of Wirebox's dependency injection. All you need to do is add a property to the top of your CFC that tells WireBox what to inject.

Core Models

There are a number of automatically-mapped objects inside WireBox. Here are some examples:

property name='shell'           inject='commandbox';
property name='tempDir'         inject='tempDir@constants';
property name='formatterUtil'   inject='formatter';
property name='ModuleService'   inject='ModuleService';
property name='ConfigService'   inject='ConfigService';

Custom Models

Any CFCs in the models folder of any module will automatically be mapped in WireBox with the pattern modelname@modulename. Therefore, if you create a module called "SuperDuper" that has a CFC called models/AmazingService.cfc, you could inject it in your module's custom command like so:

property name='AmazingService' inject='AmazingService@SuperDuper';

Module Config and Settings

If a module's service or command wants to access configuration data or module settings, they can be injected using these injection DSLs.

property name='allModuleConfigs'    inject='commandbox:moduleConfig';
property name='moduleConfig'        inject='commandbox:moduleConfig:moduleName';
property name='moduleSettings'      inject='commandbox:moduleSettings:moduleName';
property name='mySetting'           inject='commandbox:moduleSettings:moduleName:mySetting';
property name='myDeepSetting'       inject='commandbox:moduleSettings:moduleName:mySetting';

CommandBox Config

You can also inject Commandbox config settings into your CFCs. Only inject a specific config setting if you expect it to exist. If you need to check for its existence (and provide a default), then inject the entire settings struct. You can also inject nested keys of a complex config setting directly like the last line shows.

property name='allConfigSettings'   inject='commandbox:ConfigSettings';
property name='myConfigSetting'     inject='commandbox:ConfigSettings:myConfigSetting';
property name='myDeepConfigSetting' inject='commandbox:ConfigSettings:myConfigSetting.nested.keys.here';

box Injection Namespace

In order to provide compatibility for generic modules to work the same between Coldbox MVC and CommandBox CLI, we have introduced a new injection namespace called box which is simply an alias for commandbox. All of the examples above such as

property name='moduleSettings' inject='commandbox:moduleSettings:moduleName';

can also be written as

property name='moduleSettings' inject='box:moduleSettings:moduleName';

and will allow your module to work the same when installed to ColdBox MVC or CommandBox CLI. note the box injection namespace was added to CommandBox in version 4.7.0 and added to ColdBox MVC in version 5.4.0 so using it will preclude the use of your module on older versions of either platform.

Reloading Changes

CFProperty injections are aggressively caches on disk to survive restarts. If you change the injections on a command CFC, you will need to run the reload command (aliased as r) to reprocess metadata on your CFCs.

Shell integration

Your command might need to get information about its environment or perhaps proxy to other commands. Here is a handful of useful methods available to all commands.

getCWD()

This method will return the Current Working Directory that the user has changed to via the cd command. The path will be expanded and fully qualified.

shell.clearScreen()

This method on the shell object will clear all text off the screen and redraw the CommandBox prompt.

shell.getTermWidth()

This shell method returns the number of characters wide that the terminal is. Can be useful for outputting long lines and making sure they won't wrap.

shell.getTermHeight()

This shell method returns the number of characters tall the terminal is. Can be useful for outputting ASCII art.

runCommand()

Warning runCommand() is now deprecated. Please use the Command() DSL instead.

runCommand() will run another command from the shell inline and wait for it to complete. Any output from the command you run will be sent to the console. You must pass the command and any parameters in exactly as you would enter in the interactive shell which includes escaping any special characters.

runCommand( 'echo "Greetings planet"' );

If your passing through a value that may or may not need escaping, there is a method in the parser that will help you.

runCommand( 'echo "#getInstance( 'parser' ).escapeArg( untrustedVariable )#"' );

By default, commands run via runCommand() will send their output to the console. It is possible to capture that output for your own purposes.

// Run echo and capture its output
var result =  runCommand( command='echo hello', returnOutput=true );
// Force it to echo in green text
print.green( result ).toConsole();

Note You won't be able to capture any output that's already flushed directly to the console.

Interceptors

Interceptors are a powerful event-driven model inside CommandBox that allows you to listen to broadcasts from within the core code to respond to events and even influence execution. Interceptors are packaged within CommandBox Modules and can be for your own use, just debugging, or to share with the world.

To create a simple interceptor, re-create the simple folder structure below inside your CommandBox installation directory.

~/.CommandBox/cfml/modules
+-- myModule/
    |-- ModuleConfig.cfc
    +-- interceptors/
        +-- MyInterceptor.cfc

Creating An Interceptor

An interceptor is a CFC that has one or more methods whose names match the name of an interception point that is broadcast. Interceptors are packaged within CommandBox modules. When the module is loaded, the InterceptorService will register your interceptor and take care of calling it when necessary.

Here is an example of an interceptor. It listens to the postCommand event and upper cases all output from the command before it is returned to the console. It also listens to onException to perform some additional error processing.

MyInterceptor.cfc

component {

    // This runs after every command execution
    function postCommand( interceptData ) {
        // Overwrite the results with an upper case version of itself
        interceptData.results = ucase( interceptData.results );
    }

    // This runs after every error
    function onException( interceptData ) {
        // Write the last exception to a file
        fileWrite( '/commandbox-home/logs/lastError.txt', serializeJSON( interceptData.exception ) );
    }

}

Registering An Interceptor

Now that we have our interceptor above, how do we register it? This code in our module's config file registers the interceptor so it will listen to the announcements it has declared.

ModuleConfig.cfc

component{
    function configure(){

        // Declare our interceptors to listen
        interceptors = [
            { class='#moduleMapping#.interceptors.MyInterceptor' }
        ];

    }
}

Watchers

CommandBox has a powerful utility that can be used to watch a folder of files for changes and fire arbitrary code when changes happen. The utility will block execution of the command until the user stops it with Ctrl+C. To use a watcher in your command, there is a method called watch() in the base command class that you can call. It has a nice DSL of chainable methods to configure it.

watch()
    .paths( '**.cfc' )
    .inDirectory( getCWD() )
    .withDelay( 5000 )
    .onChange( function() {

        print.line( 'Something changed!' );
        command( 'testbox run' )
            .run();

    } )
    .start();

Here's a rundown of the methods used above in the DSL.

  • paths( ... ) - Receives a comma-delimtied list of globbing patterns to watch for changes. (defaults to **)

  • inDirectory( ... ) - Set the base directory that the file globs are relative to. (defaults to current working directory)

  • withDelay( ... ) - Set the number of milliseconds between polling the file system. (defaults to 500 ms)

  • onChange( ... ) - Pass a closure to be executed when a change has occurred.

  • start() - Starts the watcher. Always call this at the end of the DSL chain

classLoad( 'D:/amqp-client-5.1.2.jar' );
classLoad( 'C:/myLibs,C:/otherLibs' );
classLoad( [ 'C:/myLibs', 'C:/otherLibs' ] );
classLoad( 'C:/myLibs/myLib.jar,C:/otherLibs/other.class' );
classLoad( [ 'C:/myLibs/myLib.jar', 'C:/otherLibs/other.class' ] );
-javaagent:C:\\fusionreactor\\fusionreactor.jar=name=CommandBox-CLI,address=8088
-agentpath:C:\\fusionreactor\\frjvmti_x64.dll
-Dfrlicense=XXXXX-XXXXX-XXXXX-XXXXX-XXXXX
BOX_JAVA_ARGS="-javaagent:/fusionreactor/fusionreactor.jar=name=CommandBox-CLI,address=8088"
export BOX_JAVA_ARGS
box

Core Interception Points

Here is a list of all the core interception points in CommandBox that you can listen to. Some have interceptData that comes along with them, while others don't. Remember, the interceptData struct is passed by reference. This means modifying any values directly in that struct will affect how processing continues afterwards inside of CommandBox where those values are used.

Click a category for more information.

    • onCLIStart

    • onCLIExit

    • preCommand

    • preCommandParamProcess

    • postCommand

    • prePrompt

    • preProcessLine

    • postProcessLine

    • preModuleLoad

    • postModuleLoad

    • preModuleUnLoad

    • postModuleUnload

    • preServerStart

    • onServerStart

    • onServerInstall

    • onServerStop

    • preServerForget

    • postServerForget

    • onException

    • preInstall

    • onInstall

    • postInstall

    • preUninstall

    • postUninstall

    • preVersion

    • postVersion

    • prePublish

    • postPublish

    • preUnpublish

    • postUnpublish

    • onRelease

Example Project

Example Project

Here is an example CommandBox module that uses an interceptor (in the ModuleConfig.cfc) and custom commands to help manage its settings.

On Forgebox:

On GitHub:

CommandBox Banner Customizer

Ever gotten tired of the same lame ASCII art every time you start the CommandBox shell in interactive mode? Well fear not, this module is for you!

Installation

Install the module like so:

Once you've done that, there are three module settings you can override to affect how the CLI starts up.

Hide the banner

Now reload the CLI and the ASCII art is gone.

Show the banner

Now reload the CLI and the ASCII art is back.

Custom banner text

Now reload the CLI and your custom message will appear to greet you. Note this setting is ignored if the banner is off.

Custom banner file

Want to get all kinds of funky and create your own ASCII art of your cat? Save the exact text you want to output (can be multiple lines) into a file and specify the file path here:

Note this setting is ignored if the banner is off or if banner text is set. For extra credit, try including ANSI escape codes in your text file for a color-coded banner

Advanced

All these example commands do is set config settings for you. You can also control the module settings without the banner commands by directly setting the config settings like so:

You can see the current state of this modules settings at any time by using config show to print it out.

CLI Lifecycle
Command Execution Lifecycle
Module Lifecycle
Server Lifecycle
Error Handling
Package Lifecycle
CommandBox> install commandbox-banner-customizer
CommandBox> banner off
CommandBox> banner on
CommandBox> banner text "I like spam!"
CommandBox> banner file myCustomBanner.txt
CommandBox> config set modules.commandbox-banner-customizer.hidebanner=true
CommandBox> config set modules.commandbox-banner-customizer.bannertext="I like spam!"
CommandBox> config set modules.commandbox-banner-customizer.bannerfilepath="C:\\myCustomBanner.txt"
CommandBox> config show modules.commandbox-banner-customizer
https://www.forgebox.io/view/commandbox-banner-customizer
https://github.com/bdw429s/commandbox-banner-customizer

Tab Completion & Help

Tab completion and help are powered by metadata on your command CFCs. For basic command and parameter tab completion to work you don't need to do anything. CommandBox will look at the metadata of your command and just use it.

Command Help

When a user types yourCommand help, the text they see is the hint attribute on the component. The easiest way to add this is via a Javadoc-style comment at the top of your component. Feel free to include sample executions which can be wrapped in {code:bash}{code} blocks.

/**
* This is a description of what this command does!
* This is how you call it:
*
* {code:bash}
* myCommand myParam
* {code} 
* 
**/
component {
    ...
}

Parameter Help

You'll also want to provide hints for each of the parameters to the run() method. This will also be visible in the help command as well as the Command API docs.

/**
* @param1.hint Description of the first parameter
* @param2.hint Description of the second parameter
*/
function run( required String param1, Boolean param2 ) {
    ...
}

Parameter Value Completion

When a user hits tab while typing a parameter value, booleans will prompt with true/false. Parameters with directory, destination, file, or path in their names will automatically give file system suggestions.

You can provide additional hints though for your parameters to make them super user friendly.

If you have a parameter that doesn't contain one of the keywords above but you still want it to provide the user with path, directory, or file tab completion, you can add annotations to the arguments to enable this.

  • optionsFileComplete

  • optionsDirectoryComplete

Ex:

/**
* @myParam This is a parameter that will autocomplete files
* @myParam.optionsFileComplete true
* @yourParam This is a parameter that will autocomplete directories
* @yourParam.optionsDirectoryComplete true
*/

A parameter can have both the annotations for file and directory completion as well as an optionsUDF or options list specified and they will all be used. The user will be presented with the combined list of tab completion candidates.

Static Options

If a parameter has a set number of exact options, you can specify an options attribute for that parameter that is a comma-delimited list of options for tab completion to give.

/**
* @gender.hint Please tell us your gender
* @gender.options Male,Female,Unknown
*/
function run( String gender ) {
    ...
}

Options UDF

If a parameter's values are dynamic, you can specify a optionsUDF attribute for that parameters that points to a function name in the command CFC that will be called. This function needs to return an array of all possible values. CommandBox will do the work of filtering down partial matches if they're in the middle of typing.

/**
* @forgeBoxType.hint What type of package is this?
* @forgeBoxType.optionsUDF completeTypes
*/
function run( String forgeBoxType ) {
    ...
}

array function completeTypes( string paramSoFar, struct passedNamedParameters ) {
    // Return array of possible types
    return [ 'type1', 'type2', 'type2' ];
}

An example of this in action is the install command. It will auto-complete the ForgeBox slug for you. Try typing install cold and hitting tab to see what happens.

Instead of just passing back an array of strings from your options UDF, you can pass an array of structs to provide the following for each option:

  • name (required)

  • group

  • description

array function completeTypes() {
 return [
  { name:'type1', group:'Types', description:'This is Type 1' },
  { name:'type2', group:'Types', description:'This is Type 2' },
  { name:'type3', group:'Types', description:'This is Type 3' }
 ];
}

Smart Tab Complete

Your tab complete UDF is called every time the user hits tab, but sometimes the options you want to present are based on some context. There are two parameters to your UDF which give you some context.

array function completeTypes( string paramSoFar, struct passedNamedParameters ) {
   ...
}
  • paramSoFar - A string containing the text the user has typed thus far. CommandBox will automatically filter the candidates you send back, if if you're doing something complicated like making an API call to get the candidates, this allows you to do some pre-filtering of your own. This string may be empty if they haven't typed anything yet.

  • passedNamedParameters - A struct containing the parameters the user has typed already. You get name/value pairs even if the user is typing positional params. This is handy if the possible options for one parameter are based on what is being passed for another parameter. The struct may be empty, or may only contain some params. Always check existence!

Namespace help

If you have a namespace (folder) of commands, you can control what the user sees when they type namespace help by creating a command CFC called help.cfc in the root of that folder. The help command should print out whatever information you want. CommandBox will automatically call it when the users needs help for that namespace.

Command Output

Commands aren't required to output anything, but if you do, the simplest way is to simply return a string from your run() method.

function run() {
    return 'Hi Mom!';
}

The recommended method is via a print object. You can output ANSI-formatted text this way. All the text you output will be stored in a "buffer" and at the end of the command it will be output to the console, or piped into the next command if necessary;

Print Helper

The print object has an unlimited number of methods you can call on it since it uses onMissingMethod. Here are the rules.

Line Break

If the method has the word "line" in it, a new line will be added to the end of the string. If the string is empty or not provided, you'll just output a blank line.

print.line( 'I like Spam.' );
print.line();

Text Color

CommandBox supports 256 colors, but some terminals only support 16 or even 8. If you use a color that the terminal doesn't support, it will be adjusted to the next closest color. If the method has one of the names of a supported color in it, the text will be colored. Here are the basic 16 color names:

  • Black

  • Maroon

  • Green

  • Olive

  • Navy

  • Magenta

  • Cyan

  • Silver

  • Grey

  • Red

  • Lime

  • Yellow

  • Blue

  • Fuchsia

  • Aqua

  • White

print.magenta( 'I sound like a toner cartridge' );
print.greenText( "You wouldn't like me when I'm angry" );
print.somethingBlue( 'UHF ' );

To view all the color names run the system-colors command.

print.MistyRose3( 'Fancy colors' );

Background Color

If the method has a valid color name preceded by the word "on", the background of the text will be that color.

  • onBlack

  • onRed

  • onGreen

  • onYellow

  • onBlue

  • onMagenta

  • onCyan

  • onWhite

  • etc...

print.blackOnWhiteText( 'Inverse!' );
print.greenOnRedLine( "Christmas?" );

Color by Numbers

When you run the system-colors command, you'll see that each of the 256 colors have a number. You can reference a color like so:

print.color221( 'I will print PaleVioletRed1' );

Text Decoration

If any of the following words appear in the method, their decoration will be added. Note, not all of these work on all ANSI consoles. Blink, for instance, doesn't seem to work in Windows.

  • bold

  • underscored

  • blinking

  • reversed - Inverse of the default terminal colors

  • concealed

  • indented

print.boldText( "Don't make me turn this car around!" );
print.underscoredLine( "Have I made my point?" );

indented isn't part of the ANSI standard but rather a nice way to indent each line of output with two spaces to help clean up nested lines of output. If the string being passed in has carriage returns, each of them will be preceded by two spaces.

print.boldText( "Header" )
    .indentedLine( "Detail 1" )
    .indentedLine( "Detail 2" )
    .indentedLine( "Detail 3" );

Mix It Up

Any combination of the above is possible. Filler words like "text" will simply be ignored so you can make your method nice and readable. Get creative, but just don't overdo it. No one wants their console to look like a rainbow puked on it.

print.redOnWhiteLine( 'Ready the cannons!' );
print.boldRedOnBlueText( "Test dirt, don't wash." );
print.boldBlinkingUnderscoredBlueTextOnRedBackground( "That's just cruel" );

Dynamic Formatting

Some times you want to apply formatting at run time. For instance, show a status green if it's good and red if it's bad. You can pass a second string to the print helper with additional formatting that will be appended to the method name.

print.text( status, ( status == 'running' ? 'green' : 'red' ) );

Depending on the value of the status variable, that line would be the same as one of the following two lines:

print.greenText( status );
print.redText( status );

Flush

If you have a command that takes a while to complete and you want to update the user right away, you can flush out everything in the buffer to the console with the .toConsole() method. Note, any text flushed to the console cannot be piped to another command.

print.Line( 'Step 1 complete' ).toConsole();
print.Line( 'Step 2 complete' ).toConsole();
print.Line( 'Step 3 complete' ).toConsole();

Chaining

All the methods in the print object can be chained together to clean up your code.

print.whiteOnRedLine( 'ERROR' )
    .line()
    .redLine( message )
    .line();

Interactivity

Some tasks simply do some processing, output their results, and finish executing. You may want to interact with the user or create a long-running that can be controlled by user input (See the snake game)

Use these methods made available to interact with your users

ask()

The ask() task will wait for the to enter any amount of text until a line break is entered. A message must be supplied that lets the user what you'd like to receive from them. The task will return their response in a string variable.

var favoriteColor = ask( 'WHAT, is your favorite color??' );

if( favoriteColor == 'red' ) {
    print.boldRedLine( 'AAAAHHHHHH!!!!' );
} else if ( favoriteColor == 'I mean blue' ) {
    print.line( 'Obscure Monty Python reference' );
}

You can mask sensitive input so it doesn't show on the screen:

response = ask( message='What is your password?', mask='*' );

You can also put default text in the buffer for a wizard-style interface where the user can simply hit "enter" to accept the visible default values.

response = ask( message='Enter installation Directory: ', defaultResponse='/etc/foo/bar` );

waitForKey()

If you just need a single character collected from a user, or perhaps any keystroke at all, use the waitForKey() method. A message must be supplied that lets the user know what you need. This method can capture a single standard character and also has some special (multi-char) return values that represent special key presses.

var keyPress = waitForKey( 'Press any key, any key.' );
print
  .line()
  .line( 'My magic tells me you pressed: #keyPress#' );

If the return is not a single character, it will be one of the following special strings:

  • key_left - Left arrow

  • key_right - Right arrow

  • key_up - Up arrow

  • key_down - Down arrow

  • back_tab - Shift-tab. (Regular tab will come through as a normal tab char)

  • key_home - Home key

  • key_end - End end

  • key_dc - Delete

  • key_ic - Insert key

  • key_npage - Page down

  • key_ppage - Page up

  • key_f1 - F1 key

  • key_f2 - F2 key

  • key_f3 - F3 key

  • key_f4 - F4 key

  • key_f5 - F5 key

  • key_f6 - F6 key

  • key_f7 - F7 key

  • key_f8 - F8 key

  • key_f9 - F9 key

  • key_f10 - F10 key

  • key_f11 - F11 key

  • key_f12 - F12 key

  • esc - Escape key

confirm()

If you want to ask the user a yes or no question, use the confirm() method. Any boolean that evaluates to true or a y will return true. Everything else will return false. This allows your users to respond with what's natural to them like yes, y, no, n, true, or false. You must pass a question into the method and you will receive a boolean back.

if( confirm( 'Do you like Pizza? [y/n]' ) ) {
    print.greenLine( 'Good for you.' );
} else {
    print.boldRedLine( 'Heretic!!' );
}

Multiselect input

Sometimes you want to collect input from the user that is constrained to a limited number of predefined options. You could have them enter via ask() as freetext, but that is more prone to errors. This is where the Multiselect input control comes in handy. It blocks just like the ask command until the user responds but allows the user to interact with it via their keyboard. Think of it like radio buttons or checkboxes. If you configure it to only allow a single response (radio buttons) then a string will come back containing the answer. If you configure it to allow multiple selections, you will receive an array of responses back, even if there was only one selection made.

Here is a simple example that uses a comma-delimited list to define the options.

var color =  multiselect()
    .setQuestion( 'What is your favorite color? ' )
    .setOptions( 'Red,Green,Blue' )
    .ask();

Here is another example that defines the options in an array. This allows you to have different text on screen from what gets returned in the response. This sets multiple responses on so an array will come back. This also sets the input as required so the user will be required to select at least one option.

var colorArray =  multiselect()
    .setQuestion( 'What is your favorite color? ' )
    .setOptions( [
        { display='Red', value='r', selected=true },
        { display='Green', value='g' },
        { display='Blue', value='b' }
    ] )
    .setMultiple( true )
    .setRequired( true )
    .ask();

Notice how the "red" option is set as selected by default. Even though the colors will show up as "Red", "Green", and "Blue, the values will come back in the array as "r", "g" and "b" in the array.

Display and value are both both required in the array of options above. If you provide at least one of the two, the other will default to the same. A keyboard shortcut will be created for each option which defaults to the first character of the display. So for instance, pressing "R" on your keyboard will select the Red option. Pressing "G" will select Green, etc. You can override the shortcut with an accessKey setting in the struct.

var response = multiselect()
	.setQuestion( 'What place would you like? ' )
	.setOptions( [
		{ value='First', accessKey=1 },
		{ value='Second', accessKey=2 },
		{ value='Third', accessKey=3 }
	] )
	.ask();

Force

Remember that while interactivity is cool, people might want to automate your tasks as part of a script that runs headlessly. Therefore you should always provide a way to skip prompts if possible.

function run( required path, Boolean force=false )  {
    if( arguments.force || confirm( "Are you sure? [y/n]" ) ) {
        fileDelete( arguments.path );
        print.redLine( "It's gone, baby." );
        return;
    }
    print.redLine( "Chickened out!" );

Running Other Commands

Many times when developing a command, you find the need to run another, existing command. To do this, we have provided you with a DSL you can use to call any command, pass parameters, and even pipe commands together.

The DSL

The DSL is a sequence of chained methods that will always start with command() and end with .run(). The run method tells the DSL that you are finished chaining methods and that the command should be executed. Here is the simplest possible example:

command( 'version' )
    .run();

This runs the version command and the output will be flushed to the console.

Here are all the possible DSL methods that we'll unpack below:

command( ... )
    .params( ... )
    .flags( ... )
    .append( ... )
    .overwrite( ... )
    .run( ... );

command()

This is required to be the first method you call. It creates an instance of the CommandDSL class and returns it. It accepts a single parameter called name which is the name of the command you wish to run. Type the name exactly as you would in the shell including the namespace, if applicable.

command( 'info' )
    .run();

command( 'server start' )
    .run();

params()

This method is used to pass parameters to your command. You can pass named or positional parameters to this method, and they will be pass along to the command in the same fashion. There is no need to escape parameter values like you would when running a command manually from the shell.

Named parameters

command( 'cp' )
    .params( path='/my/path', newPath='/my/new/path' )
    .run();

Positional parameters

command( 'cp' )
    .params( '/my/path', '/my/new/path' )
    .run();

flags()

Just like when running a command manually, flags are an optional shortcut for specifying boolean parameters. Pass in each flag as a separate argument. It is not necessary to include the -- prior to the value, but it will still work.

command( "install" )
    .params( 'coldbox' )
    .flags( 'force', '!save' )
    .run();

append() and overwrite()

You may redirect the output of a command to a file (normally accomplished by > and >>) by chaining the append() or overwrite() methods. These are mutually exclusive.

command( "cat" )
    .params( "myFile.txt" )
    .append( "myOtherFile.txt" )
    .run();

command( "echo" )
    .params( "Your new file contents" )
    .overwrite( "myFile.txt" )
    .run();

inWorkingDirectory()

Control the working directory that the command runs in if you don't want it to be the current working directory of the shell.

command( "ls" )
    .inWorkingDirectory( 'C:/' )
    .run();

pipe()

Piping is a very powerful way to combine multiple commands and is accomplished via the pipe method. This method expects to receive another CommandDSL instance. You do not need to call run() on the nested command. This example is the equivalent to echo "hello\nworld" | grep lo.

command( "echo" )
    .params( "hello#chr( 10) #world" )
    .pipe( 
        command( "grep" )
        .params( "lo" )
    )
    .run();

You can have more than one pipe() method. Each piped command will be called in order, receiving the output from the previous one.

command( "cat" )
    .params( "myFile.txt" )
    .pipe( 
        command( "grep" )
        .params( "searchString" )
    )
    .pipe( 
        command( "sed" )
        .params( "s/find/replace/g" )
    )
    .pipe( 
        command( "more" )
    )
    .run();

The above is the equivalent of

cat myFile.txt | grep searchString | sed s/find/replace/g | more

run()

Your DSL should always end with a run method. This executes the command. By default, the output will be sent to the console, however you can capture it by specifying returnOutput as true.

var output = command( "echo" )
      .params( "My name is Brad" )
      .run( returnOutput=true );

If you want to help debug the exact command that is being passed along to the shell for executing, set the echo parameter to true and the command will be echoed out prior to execution. The echoed text is not part of what gets returned or piped.

command( "version" )
    .run( echo=true );

You may want to manually pipe data into the command (which is the same as passing it as the first parameter. Do so with the piped parameter to the run method.

command( "touch" )
    .run( piped='myFile' );

If you try to pass a shell expansion into a command, it won't work since the CommandDSL escapes all your special characters. This example doesn't work because the special characters are escaped. So the exact text is printed out and it's not possible to have it evaluated.

command( 'echo' )
  .params( '${ os.name }' )
  .run();
------------------------------------------
Output: ${ os.name }

You can ask the CommandDSL to treat your parameters as 'raw' so they are just passed along. This allows them to include system setting expansions and CommandBox backtick expressions. Make sure that you escape any special chars yourself in this mode just like you would if typing the parameters from the shell.

command( 'echo' )
  .params( '${ os.name }' )
  .run( rawParams=true );
------------------------------------------
Output: Windows 7

Server Lifecycle

preServerStart

Announced before a server starts. This fires after server.json has been located but before any configuration is resolved. Use this to override any user inputs, influence how the server's details are resolved, or to modify things like hostname before ports are bound.

interceptData

  • serverDetails - A struct with the following keys used in starting the server

    • defaultName - The name of the server

    • defaultwebroot - The web root of the server

    • defaultServerConfigFile - The location of the server.json (May not exist yet)

    • serverJSON - The parsed contents of the JSON file

    • serverInfo - The serverInfo Struct (see below)

    • serverIsNew - A boolean whether this server has been started before.

  • serverProps - A struct with the parameters passed to the start command from the CLI. Omitted params will not be present.

    • See the help for the server start command to see the current list of parameters.

onServerStart

Announced as a server is starting after the configuration values have been resolved, but prior to the actual server starts. Use this to modify the settings for the server before it starts.

interceptData

  • serverInfo - A struct with the following keys used in starting the server

    • name - The name of the server

    • webroot - The path to the web root

    • serverConfigFile - The path to the server.json file (may not exist)

    • trayEnable - If tray menu is enabled

    • customServerFolder - Where the server's log files live. May be the same as serverHomeDirectory

    • debug - Whether to start Runwar in debug mode

    • trace - Whether to start Runwar in trace mode

    • console - Whether to start server in console mode

    • openbrowser - Flag to open web browser upon start

    • host - The hostname to bind the server to

    • port - The HTTP port

    • stopsocket - The socket to listen for stop connections

    • webConfigDir - Path to the Lucee web context

    • serverConfigDir - Path to the Lucee server context

    • libDirs - List of additional lib paths

    • trayEnable - If tray menu is enabled

    • trayIcon - Path to .png file for tray icon

    • trayOptions - Array of tray menu options

    • webXML - Path to web.com file

    • SSLEnable - Enable HTTPS flag

    • HTTPEnable - Enable HTTP flag

    • SSLPort - HTTPS port

    • SSLCert - SSL Certificate

    • SSLKey -SSL Key

    • SSLKeyPass - SSL Key passphrase

    • rewritesEnable - Enable URL rewrites

    • rewritesConfig - Path to custom Tuckey rewrite config file

    • heapSize - Max heap size in Megabytes

    • directoryBrowsing - Enable directory browsing

    • JVMargs - Additional JVM args to use when starting the server

    • runwarArgs - Additional Runwar options to use when starting the server

    • logdir - Path to directory for server logs

    • welcomeFiles - List of welcome files

  • serverDetails - Same as onServerStart above

  • installDetails - Same as onInstall below

onServerInstall

Announced when a server is starting and the cfengine is being installed. This gives you a chance to influence how the server is installed or to modify default settings before the server process actually starts. This is not announced for servers using a WARPath setting. It is announced every time a server is started, but you can use the installDetails.initialInstall flag to determine if this is the first time the engine is being installed for one-time tasks.

interceptData

  • serverInfo - Same as onServerStart above

  • installDetails A struct with the following keys:

    • internal - True if using the embedded jars from the CLI

    • enginename - The name of the cfengine that was installed

    • version - The version of the cfengine that was installed

    • installDir - The folder where the server is installed to

    • initialInstall - True if this is the first time the engine was installed

onServerStop

Announced before a server stop.

interceptData

  • serverInfo - Same as onServerStart above

preServerForget

Always fires before attempting to forget a server whether or not the forgetting is actually successful. Has access to all files and settings for the server.

interceptData

  • serverInfo - Same as onServerStart above

postServerForget

Fires after a successful server forget. If the forget fails, this will not fire.

interceptData

  • serverInfo - Same as onServerStart above

Package Lifecycle

preInstall

Announced prior to installing a package. If a package has additional dependencies to install, each of them will fire this interception point.

interceptData

  • installArgs - Struct containing the following keys used in installation of the package.

    • ID - The ID of the package to install

    • directory - Directory to install to. May be null, if none supplied.

    • save - Flag to save box.json dependency

    • saveDev - Flag to save box.json dev dependency

    • production - Flag to perform a production install

    • currentWorkingDirectory - Original working directory that requested installation

    • verbose - Flag to print verbose output

    • force - Flag to force installation

    • packagePathRequestingInstallation - Path to package requesting installing. This climbs the folders structure for nested dependencies.

onInstall

Announced while a package is being installed, after the package endpoint and installation directory has been resolved but before the actual installation occurs. This allows you to override things like the installation directory based on package type. Any values updated in the interceptData struct will override what the install command uses.

interceptData

  • installArgs - Same as preInstall above

  • installDirectory - Directory that the package will be installed in

  • containerBoxJSON - A struct containing the box.json of the page requesting the installation

  • artifactDescriptor - A struct containing the box.json of the package artifcat about to be installed

  • artifactPath - The folder containing the unzipped artifact, ready to be installed.

  • ignorePatterns - An array of file globbing patterns to ignore on installation

  • endpointData - A struct containing the following keys.

    • endpointName - The name of the endpoint. i.e. "forgebox" or "HTTP"

    • package - The name of the package. i.e. "cborm" or "coldbox"

    • ID - The canonical ID of the endpoint. i.e. "forgebox:coldbox" or "github:user/repo"

    • endpoint - The instance of the endpoint CFC that implements IEndpoint.

postInstall

Announced after an installation is complete. If a package has additional dependencies to install, each of them will fire this interception point. This fires even if an install is skipped due to an existing package that satisfies the dependencies, or if the package is already installed.

interceptData

  • installArgs - Same as preInstall above

preUninstall

Announced before the uninstallation of a package.

interceptData

  • uninstallArgs - Struct containing the following keys used in removal of the package

    • ID - ID of the package to uninstall

    • directory - The directory to be uninstalled from (used to find box.json)

    • save - Whether to update box.json

    • currentWorkingDirectory - Path to package requesting removal . This climbs the folders structure for nested dependencies.

postUninstall

Announced after the uninstallation of a package.

interceptData

  • uninstallArgs - Same as preUninstall above

preVersion

Announced before the new version is set in the package.

interceptData

  • versionArgs - A struct containing the following keys:

    • version - The new version about to be set

    • tagVersion - Boolean that determines whether to tag a Git repo

    • message - Commit message to use when tagging Git repo

    • directory - The working directory of the package

    • force - If true, tag a Git repo even if it isn't clean

postVersion

Announced after the new version is set in the package but before the Git repo is tagged.

interceptData

  • versionArgs - Same as preVersion above.

onRelease

Announced after a new version is set using the bump command and after the Git repo is tagged.

interceptData

  • directory - The working directory of the package

  • version - The new version about was set

prePublish

Announced prior to publishing a package to an endpoint

interceptData

  • publishArgs - A struct containing the following keys:

    • endpointName - The name of the endpoint being published to

    • directory - The directory that the package lives in

  • boxJSON - A struct containing the defaulted box.json properties for the package

postPublish

Announced after publishing a package to an endpoint

interceptData

  • publishArgs - Same as prePublish above.

  • boxJSON - Same as prePublish above.

  • preUnpublish

Announced prior to unpublishing a package from an endpoint

interceptData

  • unpublishArgs - A struct containing the following keys:

    • endpointName - The name of the endpoint being published to

    • directory - The directory that the package lives in

    • version - The version being unpublished

    • force - Boolean to skip the interactive prompt

  • boxJSON - A struct containing the defaulted box.json properties for the package

postUnpublish

Announced after unpublishing a package from an endpoint

interceptData

  • unpublishArgs - Same as preUnpublish above.

  • boxJSON - Same as preUnpublish above.

CreateObject() :: Lucee Documentationlucee_server

Configuration

The configuration for a module is contained within in the ModuleConfig.cfc that lives in the root folder. Here's an overview of the options for configuring your module.

component{
    // Module Properties
    this.autoMapModels = true;
    this.modelNamespace = "test";
    this.cfmapping = "test";
    this.dependencies = [ "otherModule", "coolModule" ];

    function configure(){

        // Settings for my module
        settings = {
            mySetting = 'isCool',
            settingsCanBe = [
                'complex',
                'values'
            ]
        };

        // Declare some interceptors to listen
        interceptors = [
            {
                class='#moduleMapping#.interceptors.TestInterceptor'
            }
        ];

        // Ad-hoc interception events I will announce myself
        interceptorSettings = {
            customInterceptionPoints = ''
        };

        // Manually map some models
        binder.map( 'foo' ).to( '#moduleMapping#.com.foo.bar' );

    }

    // Runs when module is loaded
    function onLoad(){
        log.info('Module loaded successfully.' );
    }

    // Runs when module is unloaded
    function onUnLoad(){
        log.info('Module unloaded successfully.' );
    }

    // An interceptor that listens for every command that's run.
    function preCommand( interceptData ){
        // I just intercepted ALL Commands in the CLI
        log.info('The command executed is #interceptData.CommandInfo.commandString#');
    }    

}

Commands

CommandBox is extensible via CFML by creating modules that contain command CFCs. This is a very easy and powerful way to create custom, reusable CFML scripts that interact with the user, automate hard tasks, and can be shared with other developers. Let's create a "Hello World" command.

Your First Command

To create our first command, we'll need a new module. A module can contain as many commands as you like. You can create a module by placing a folder in ~/.CommandBox/cfml/modules/ that contains a ModuleConfig.cfc file. The minimum contents of your module config is:

modules/test/ModuleConfig.cfc

component {
    function configure(){}
}

Now, create a commands folder inside your module for your command to live in. Each CFC in this folder will be registered as a command. The only requirement for a command CFC is that is has a run() method.

modules/test/commands/Hello.cfc

component {
    function run(){
        return 'Hello World!'; 
    }
}

That's it! After creating your module, run the reload command from the shell, and then the name of the new command is the same as the name of the CFC. In this case, you would run the command above like so:

hello

It would output Hello World! to the console. Anything after hello will be passed to your run() function as parameters.

Create Namespaces

To create a two-part command like say hello create CFCs that are nested in subfolders, for example: ~/.CommandBox/cfml/modules/test/commands/say/Hello.cfc The contents of the Hello.cfc would not change, but the namespace will match the folder name by convention. The namespaced command would be called like so:

say hello

There is no limit to how deeply you can nest your namespace folders. CommandBox's built in help and tab-completion will always work via conventions.

Note: The box.json file can have a key called "ignore" that allows you stipulate folders or files which should not be installed but can be kept in reserve for testing or private purposes. Any folder in your namespace that matches that pattern will not be copied during installation.

Making Changes

Commands are created and stored once for the duration that CommandBox is running. CFProperty injections are also aggressively cached on disk across restarts. If testing changes to a command in the interactive shell, use the reload command (aliased as r) to reload the shell. Your changes will immediately be available. Using the up arrow to access the shell's history can also be useful here.

WireBox DI

All CFCs including commands are created and wired via WireBox, so dependency injection and AOP are available to them. This can be handy for commands to wrap services provided by models, or to access utilities and services inside CommandBox.

This command would inject CommandBox's ArtifactService to list out all the packages being stored.

component {

    property name='artifactService' inject='artifactService'; 

    function run(){
        var results = artifactService.listArtifacts();
        for( var package in results ) {
            print.boldCyanLine( package );
        }
    }

}

Commands also have a variables.wirebox variable as well as their own getInstance() method which proxies to WireBox to get objects.

var results = getInstance( 'artifactService' ).listArtifacts();

Dynamic Parameters

Sometimes you have a command that can need an undetermined number of configuration items that all relate to the same thing. CommandBox can accept any number of named or positional parameters, but it can be hard to collect a dynamic and unknown number of parameters. An easy way to handle this is to have the user prefix each grouped parameter name with something: like this.

doWidget widgetProp:foo=blue widgetProp:bar=seven widgetProp:baz=turkey

This will create a single item in the arguments scope of your command called widgetProp which is a struct and contains the keys foo, bar, and baz. You can loop over the widgetProp struct to access the collection in a concise manner. You can have as any of these parameter groups as you want, and a struct will be created for each unique parameter prefix.

You can omit the prefix entirely and just start a collection of parameters with a colon (:) and they will be placed in a struct called args.

doWidget :foo=blue :bar=seven :baz=turkey

And the command might look like this:

function run( args={} ){
    args.each( function( k, v ) {
        print.line( '#k# has a value of #v#.' );
    } );
}

Which, given the parameters shown above, would output this:

foo has a value of blue.
bar has a value of seven.
baz has a value of turkey.

Linking Modules

CommandBox allows you to link a module directly into the CommandBox core so that you can use the module as though it's installed but without having two copies of the code. To do this, cd to the root of the module you want to link into CommandBox.

cd /path/to/my/cool/module
package link

This will create a symlink in the core CommandBox modules directory to your package and reload the shell for you so you can immediately test it out. Any further code changes you make can be tested by running the reload command again.

Unlinking

When you are done testing, you can remove the link by running the package unlink command from the root of the module you've been developing on.

cd /path/to/my/cool/module
package unlink

This will remove the symlink for you.

Installation and Locations

A module is nothing more than a folder that contains a ModuleConfig.cfc file. The only requirement for that CFC is that it contains a method called configure(). Modules can also contain models, interceptors, commands, and pretty much anything else you want to stick in them. As long as they contain a box.json in their root, they are also a package, which means they are self-describing, can install dependencies, and can be installed via the install command.

Locations

CommandBox has two default modules directories.

  • ~/.CommandBox/cfml/system/modules

  • ~/.CommandBox/cfml/modules

The first is for system modules so you shouldn't need to touch it. All the built-in commands are in those modules, so feel free to check out how they work. All user-installed modules are in the second folder. The cfml folder is the "root" of the CommandBox installation, so there will also be a box.json created in that folder once you start installing modules via the install command.

Installation

The first way to create a module is to manually create a folder in the ~/.CommandBox/cfml/modules directory and place your ModuleConfig.cfc inside of it. This is the process described in the guide on creating your first module.

If you have a package of type commandbox-modules locally, in a Git repo, or in Forgebox, you can install from any working directory. When CommandBox sees the package type, it will change the installation directory to be the user modules folder.

install /path/to/module
install githubuser/modulerepo
install forgebox-module-slug
Logo

Configure() Method

The configure() method will be run before a module is loaded.

Injected Variables

The following variables will be created for you in the variables scope of the ModuleConfig.cfc.

  • shell - The shell object (kind of the core CommandBox controller)

  • moduleMapping - The component mapping to the module

  • modulePath - The physical path to the module

  • logBox - The LogBox instance

  • log - A named logger, ready to log!

  • wirebox - Your friendly neighborhood DI engine

  • binder - The WireBox binder, handy for mapping models manually

Data Structures

The configure() method does not accept or return any data. Instead, the config CFC itself represents the data for configuring the module with data structures in the variables scope. It's the job of the configure method to ensure that these data structures are created and populated.

There is no required data, but here is the list of optional data you can set:

  • settings - A struct of custom module settings. These defaults can be overriden when the module is loaded with the CommandBox user config.

  • interceptors - An array of declared interceptor structures that should be loaded in the entire application. Each interceptor structure contains class, and optional properties.

  • interceptorSettings - A structure of settings for interceptor interactivity which includes the sub-key customInterceptionPoints, a list of custom interception points to add to the application wide interceptor service

Here is an example configure() method. Note these data structures are placed in the variables scope.

component{

  function configure(){

    // Settings for my module
    settings = {
        mySetting = 'isCool',
        settingsCanBe = [
            'complex',
            'values'
        ],
        andEven = {
            nested = {
                any = 'way'
            },
            you = 'like'
        }
    };

    // Declare some interceptors to listen
    interceptors = [
        {
            class='#moduleMapping#.interceptors.TestInterceptor'
        }, 
        {
            class='#moduleMapping#.interceptors.DoCoolThings',
            properties={
                coolnessFactor='max',
                crankItToEleven=true

            }
        }
    ];

    // Ad-hoc interception events I will announce myself
    interceptorSettings = {
        customInterceptionPoints = 'launchInitiated,velocityAcheived,singularityAcquired'
    };

    // Manually map some models
    binder.map( 'foo' ).to( '#moduleMapping#.com.foo.bar' );

  }
}

Using File Globs

If you want users to be able to pass file globbing patterns to your command, set a type of Globber for the argument.

/**
* @path.hint file or directory to interact with.
**/
function run( required Globber path )  { }

Even though the user types a string, CommandBox will hand you a CFC instance that's preloaded with the matching paths and has some nice methods to help you interact with the matching paths. The Globber object lazy loads the matches, which gives you time to affect the pattern or even the sort if you wish prior to the file system actually being hit.

Globber.count()

Use the count() method to get the total number of paths matched on the file system.

function run( required Globber path ) {
  print.line( path.count() & ' files affected!' );
}

Globber.apply( ... )

The easiest way to apply some processing to each of the file paths found is by passing a closure to the apply() method. The closure will be executed once for each result.

function run( required Globber path ) {
  path.apply( function( thisPath ){
    print.line( 'Processing file ' & thisPath );
  } );
}

Globber.matches()

To get the raw results back, use the matches() method.

Globber.asQuery()

If you want to get the results back as a query object, use the asQuery() method and then you can loop over them yourself. The query contents match what comes back from directoryList().

function run( required Globber path ) {
var myQry = path
  .asQuery()
  .matches();
}

Globber.asArray()

If you want to get the results back as an array, use the asArray() method and then you can loop over them yourself.

function run( required Globber path ) {
var myArr = path
  .asArray()
  .matches();
}

Globber.withSort( ... )

Affect the order that the results come back by setting a sort. The sort follows the same pattern as the directoryList() function, which means it can be a comma-delimited list of columns to sort on.

function run( required Globber path ) {
var myQry = path
  .asQuery()
  .withSort( 'type, name' )
  .matches();
}

Globber.getPattern()

To get the original globbing pattern that the user typed, use the getPattern() method on the Globber CFC.

function run( required Globber path ) {
  print.line( 'You typed ' & path.getPattern() );
}

Create your own Globber

You can create your own File Gobber object from any pattern using the globber() method. You can pass a comma delimited list or an array of globber patterns to the constructor.

globber( 'models/**.cfc' ).matches()

globber( 'models/**.cfc,handlers/**.cfc' ).matches()

globber( [ 'models/**.cfc', 'handlers/**.cfc' ] ).matches()

You can also add additional glob patterns to an existing Globber object.

globber( 'models/**.cfc' )
  .addPattern( 'handlers/**.cfc' )
  .matches()

Exclude Patterns

Sometimes when using a Globbing pattern, it's desirable to exclude a small number of patterns and it's cumbersome to manually include every pattern you want. You can set more than one excludes pattern to be passed to filter the matches. Excludes follow the same format as include patterns. If a pattern is both excluded and included, the exclude wins.

globber( '**.cf?' )
  .setExcludePattern( '.git/,coldbox/' )
  .matches()

Excludes, like includes, allow for a comma-delimited list or an array to be passed.

globber( '**.cf?' )
  .setExcludePattern( [ '.git/', 'coldbox/' ] )
  .matches()

The setExcludePattern() method will override any existing methods, but the addExcludePattern() method will add to the existing list.

globber( '**.cf?' )
  .setExcludePattern( '.git/' )
  .addExcludePattern( 'coldbox/' )  
  .matches()

Command Execution Lifecycle

preCommand

Announced before the execution of a command. This fires after all command parameters have been evaluated, including expressions. If piping the output of one command into another in a command chain, this will fire twice-- once for each command in the chain.

interceptData

  • commandInfo - A struct containing the following keys about the command to execute

    • commandString - A string representing the command name

    • commandReference - The instantiated Command CFC

    • parameters - An array of un-parsed parameter tokens typed in the CLI

    • closestHelpCommand - The CFC path to the most-applicable help command. Used to generate namespace help.

  • parameterInfo - A struct containing the following keys about the processed parameters for the command execution

    • positionalParameters - An array of parameter values

    • namedParameters - A struct of name/value pairs. The named parameters are always what is passed to the command's run() method.

    • flags - A struct of flags that were passed in.

preCommandParamProcess

preCommand fires after expression, system setting, and colon param expansions have already occurred. preCommandParamProcess will fire prior to that so you can affect the system prior to the time the system settings are expanded in parameter values.

  • commandInfo - Same as above

  • parameterInfo - Same as above

postCommand

Announced immediately after command execution is complete. If more than one command is piped together in a command chain, this is announced after each command in the chain.

interceptData

  • commandInfo - Same as preCommand above

  • parameterInfo - Same as preCommand above

  • results - A string that represents any output from the command that hasn't already been flushed to the console.

prePrompt

Announced prior to drawing the prompt in the interactive shell. This interception point can be used to customize the text of the prompt by modifying the prompt variable in intercept data which is an ANSI-formatted string to be output before the cursor.

interceptData

  • prompt - An ANSI-formatted string containing the prompt text. Replacing this value will override the prompt.

preProcessLine

Pre and post command fire before and after each command, but that means they fire twice for something like:

echo `package show name`

preProcessLine will fire only once for the above command after the user hits enter but before anything is processed.

interceptData

  • line - A string representing the line typed into the shell. Changing the contents of this string will override what actually gets executed.

postProcessLine

Pre and post command fire before and after each command, but that means they fire twice for something like:

echo `package show name`

postProcessLine will fire only once for the above command after the entire line has been executed. Any output is already sent to the console by the time this interception point fires.

interceptData

  • line - A string representing the line that was just executed in the shell.