Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
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.
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
.
And the command might look like this:
Which, given the parameters shown above, would output this:
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.
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
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
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:
It would output Hello World!
to the console. Anything after hello
will be passed to your run()
function as parameters.
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:
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.
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.
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.
Commands also have a variables.wirebox
variable as well as their own getInstance()
method which proxies to WireBox to get objects.
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:
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.
If you want users to be able to pass file globbing patterns to your command, set a type of Globber
for the argument.
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.
Use the count()
method to get the total number of paths matched on the file system.
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.
To get the raw results back, use the matches()
method.
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()
.
If you want to get the results back as an array, use the asArray()
method and then you can loop over them yourself.
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.
To get the original globbing pattern that the user typed, use the getPattern()
method on the Globber CFC.
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.
You can also add additional glob patterns to an existing Globber object.
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.
Excludes, like includes, allow for a comma-delimited list or an array to be passed.
The setExcludePattern()
method will override any existing methods, but the addExcludePattern()
method will add to the existing list.
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.
Users can pass named or positional parameters that aren't declared, and they will come through the arguments
scope. Named parameters will be accessible 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.
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 ../../
.
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.
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
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.
You can mask sensitive input so it doesn't show on the screen:
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.
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.
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
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.
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.
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.
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.
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.
Commands aren't required to output anything, but if you do, the simplest way is to simply return a string from your run() method.
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;
The print object has an unlimited number of methods you can call on it since it uses onMissingMethod. Here are the rules.
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.
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
To view all the color names run the system-colors
command.
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...
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:
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
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.
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.
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.
Depending on the value of the status
variable, that line would be the same as one of the following two lines:
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.
All the methods in the print
object can be chained together to clean up your code.
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.
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.
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.
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:
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.
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.
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.
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
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.
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!
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.
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.
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.
This method on the shell object will clear all text off the screen and redraw the CommandBox prompt.
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.
This shell method returns the number of characters tall the terminal is. Can be useful for .
Warning runCommand() is now deprecated. Please use the 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.
If your passing through a value that may or may not need escaping, there is a method in the parser that will help you.
By default, commands run via runCommand()
will send their output to the console. It is possible to capture that output for your own purposes.
Note You won't be able to capture any output that's already flushed directly to the console.
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-delimited 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
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 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:
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:
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.
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.
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.
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.
Control the working directory that the command runs in if you don't want it to be the current working directory of the shell.
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
.
You can have more than one pipe()
method. Each piped command will be called in order, receiving the output from the previous one.
The above is the equivalent of
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
.
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.
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.
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.
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.
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.
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
.
When an unhandled exception happens in the shell, only the tag context is shown. You can enable full stack trace output like so:
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-delimited list of globbing patterns to watch for changes. (defaults to **
) Note that the "paths" here work more like .gitignore
entries and less like bash paths. Specifically:
A path with a leading slash (or backslash), will be evaluated relative to the current working directory. E.g. watch /foo
will only watch files in the directory at ./foo
, but not in directories like ./bar/foo
.
A path without a leading slash (or backslash) will be applied as a glob filter to all files within the current working directory. E.g. watch foo
will result in the entire working directory being watched, but only files matching the glob **foo
will be processed.
If your watcher seems slow, unresponsive, or is failing to notice some file change events, it is likely that you have it watching too many files. Try specifying more specific paths to the files you want to process, and use leading slashes in your arguments to avoid watching all files in the current working directory.
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
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.
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.