DO IT
WITH CFML
Building Tools with CommandBox
Matthew Clemente
box server start
server.json
.CFConfig.json
box.json
180+ config items!
All the server settings!
Complete package control
What is CommandBox?
Let's Check the Docs
- Command Line Interface (CLI)
- Package Manager
- Embedded CFML Server
- Read Eval Print Loop (REPL)
CommandBox is...
>Command Line_
Powerful
Obscure Incantations
grep
sed
xargs
awk
uniq
Dangerous
>Command Line_
box
server
start
CLI
command
namespace
HELP!
Tab-Completion
Tab-Completion
Example: Removing a Directory
execute
Executes a CFML file and outputs whatever the template outputs using cfoutput or the buffer.
execute
Helpful Commands
tokenReplace
utils
propertyFile
env
checksum
Command Line Tools
- Task Runners
- Watchers
- Custom Commands
- CommandBox Modules
this would be so much easier with ColdFusion?
Have you ever thought...
#!/bin/sh
# credit: https://medium.com/@basi/docker-environment-variables-expanded-from-secrets-8fa70617b3bc
: ${ENV_SECRETS_DIR:=/run/secrets}
env_secret_debug()
{
if [ ! -z "$ENV_SECRETS_DEBUG" ]; then
echo -e "\033[1m$@\033[0m"
fi
}
# usage: env_secret_expand VAR
# ie: env_secret_expand 'XYZ_DB_PASSWORD'
# (will check for "$XYZ_DB_PASSWORD" variable value for a placeholder that defines the
# name of the docker secret to use instead of the original value. For example:
# XYZ_DB_PASSWORD=<<SECRET:my-db.secret>>
env_secret_expand() {
var="$1"
eval val=\$$var
if secret_name=$(expr match "$val" "<<SECRET:\([^}]\+\)>>$"); then
secret="${ENV_SECRETS_DIR}/${secret_name}"
elif [[ ${var:(-5)} = '_FILE' ]]; then
suffix=${var:(-5)}
secret=$val
fi
if [ $secret ]; then
env_secret_debug "Secret file for $var: $secret"
if [ -f "$secret" ]; then
val=$(cat "${secret}")
if [ $suffix ]; then
echo "Expanding from _FILE suffix"
var=$(echo $var| cut -d'_' -f 1);
fi
export "$var"="$val"
env_secret_debug "Expanded variable: $var=$val"
else
export "$var"=""
env_secret_debug "Secret file does not exist! $secret"
fi
fi
# reset
unset secret;
unset secret_name;
unset var;
unset suffix;
unset val;
}
env_secrets_expand() {
for env_var in $(printenv | cut -f1 -d"=")
do
env_secret_expand $env_var
done
if [ ! -z "$ENV_SECRETS_DEBUG" ]; then
echo -e "\n\033[1mExpanded environment variables\033[0m"
printenv
fi
}
env_secrets_expand
Easier in ColdFusion
Task Runners
Streamline jobs in CFML with a defined API, minimal boilerplate, and helpful tooling.
?
Ready to create your first task
Ready to create your first task
?
Seriously, it's just two words.
Seriously, it's just two words.
task create --open
Print Helper
// all the examples from the docs
component {
function run() {
print.line( 'I like Spam.' );
print.line();
print.magenta( 'I sound like a toner cartridge' );
print.greenText( "You wouldn't like me when I'm angry" );
print.Blue( 'UHF ' );
print.blackOnWhiteText( 'Inverse!' );
print.greenOnRedLine( "Christmas?" );
print.color221( 'I will print PaleVioletRed1' );
print.boldText( "Don't make me turn this car around!" );
print.underscoredLine( "Have I made my point?" );
print.boldText( "Header" )
.indentedLine( "Detail 1" )
.indentedLine( "Detail 2" );
print.redOnWhiteLine( 'Ready the cannons!' );
print.boldRedOnBlueText( "Test dirt, don't wash." );
print.boldBlinkingUnderscoredBlueTextOnRedBackground( "That's just cruel" );
print.text( 'status', ( dayofweek(now()) == 5 ? 'green' : 'red' ) );
print.whiteOnRedLine( 'ERROR' )
.line()
.redLine( 'oops' )
.line();
}
}
component {
function wizard() {
var name = ask( message="Name of recipient: ", defaultResponse="Friend" );
var savePdf = confirm( "Do you want to save a PDF version?" );
var quote = multiselect()
.setQuestion( 'What quote should be included? ' )
.setOptions( [
{ value='What have you learned?', selected=true },
{ value='Lost time is never found again.' },
{ value='Time flies like an arrow; fruit flies like a banana.' }
] )
.setRequired( true )
.ask();
var days = now().daysinyear()-now().dayofyear();
var message = 'Hello #name#, there are #days# left in #year(now())#.
#quote#';
print.greenLine( message );
if( savePdf ){
cfDocument(format="PDF", filename="motivate-#name#.pdf", overwrite="true") {
writeOutput(message);
};
}
}
}
Interactivity
task create motivate wizard --open
Passing Arguments
function report( string name = 'Friend', boolean savePdf = false ) {
var days = now().daysinyear()-now().dayofyear();
var message = 'Hello #name#, there are #days# days left in #year(now())#.
What have you learned?';
print.greenLine( message );
if( savePdf ){
cfDocument(format="PDF", filename="motivate-#name#.pdf", overwrite="true") {
writeOutput(message);
};
}
}
Parameters
- Positional
- Named
- Boolean Flags
component {
property name='progressableDownloader' inject='ProgressableDownloader';
property name='progressBar' inject='ProgressBar';
function run( boolean doZip = false ){
var csvs = globber( resolvePath( '' ) & '*.csv' ).matches();
if( !csvs.len() ) {
print.boldRedLine( 'No CSV Files found' );
return;
}
var line = '';
for( var csv in csvs ) {
var dateSlug = formatterUtil.slugify( dateFormat( now(), 'long' ) );
var exportName = getFileFromPath( csv ).listFirst( '.' ).lcase();
var exportFolder = dateSlug & '-' & exportName;
var collectionDir = getCWD() & exportFolder;
command( 'mkdir' )
.params( exportFolder )
.append( 'log.txt' )
.run();
cfloop( file=csv, item='line' ) {
var imageUrl = line.listlast().replace( '"', '', 'all' );
var imageName = imageurl.listlast( '/' );
var imageDestination = collectionDir & '/' & imageName & '.jpg';
if( !isValid( 'url', imageUrl ) ) continue;
progressableDownloader.download(
imageUrl,
imageDestination,
( status ) => progressBar.update( argumentCollection = status )
);
command( 'imageToASCII' )
.params( imageDestination )
.overwrite( collectionDir & '/' & imageName & '.txt' )
.run();
}
fileMove( csv, collectionDir );
if( doZip ) {
cfzip( source=collectionDir, file='#collectionDir#.zip', overwrite='true' );
}
}
}
}
Task Example
Use Cases
- Scheduled jobs
- One-off tasks
- CI/CD
Watchers
Trigger code in response to specific file changes within a watched folder.
component {
property name='progressableDownloader' inject='ProgressableDownloader';
property name='progressBar' inject='ProgressBar';
function run( boolean doZip = false ){
watch()
.paths( '/*.csv' )
.inDirectory( resolvePath( '' ) )
.withDelay( 5000 )
.onChange( function( paths ){
print.line( 'Added: #paths.added.toList()#' )
.line( 'Removed: #paths.removed.toList()#' )
.line( 'Changed: #paths.changed.toList()#' )
.toConsole();
var line = '';
for( var csv in paths.added ) {
var dateSlug = formatterUtil.slugify( dateFormat( now(), 'long' ) );
var exportName = getFileFromPath( csv ).listFirst( '.' ).lcase();
var exportFolder = dateSlug & '-' & exportName;
var collectionDir = getCWD() & exportFolder;
command( 'mkdir' )
.params( exportFolder )
.append( 'log.txt' )
.run();
cfloop( file=csv, item='line' ) {
var imageUrl = line.listlast().replace( '"', '', 'all' );
var imageName = imageurl.listlast( '/' );
var imageDestination = collectionDir & '/' & imageName & '.jpg';
if( !isValid( 'url', imageUrl ) ) continue;
progressableDownloader.download(
imageUrl,
imageDestination,
( status ) => progressBar.update( argumentCollection = status )
);
command( 'imageToASCII' )
.params( imageDestination )
.overwrite( collectionDir & '/' & imageName & '.txt' )
.run();
}
fileMove( csv, collectionDir );
if( doZip ) {
cfzip( source=collectionDir, file='#collectionDir#.zip', overwrite='true' );
}
}
} )
.start();
}
}
Watcher Example
Custom Commands
Reusable packages of code that contain command CFCs to extend CommandBox functionality.
./YourCommandModule
├── ModuleConfig.cfc
├── box.json
└── commands
└── Hello.cfc
Structure
./YourCommandModule
├── ModuleConfig.cfc
├── box.json
└── commands
├── Hello.cfc
└── say
└── Hello.cfc
└── Goodbye.cfc
Structure
mkdir exampleCommand --cd &&
touch ModuleConfig.cfc &&
package init &&
package set name="Example Command" &&
package set slug="commandbox-example" &&
package set type="commandbox-modules" &&
mkdir commands --cd &&
touch date.cfc &&
cd ../ && !code .
Command Creation
Module Location
-
~/.CommandBox/cfml/system/modules
-
~/.CommandBox/cfml/system/modules_app
-
~/.CommandBox/cfml/modules
package link
package unlink
Package Link
Creates a symlink in the core CommandBox modules directory.
Help and Completion
/**
* Craft your own avian exclamation, like Ben Nadel.
* This is how you call it:
*
* {code:bash}
* exclaim bird=chickens
* {code}
*
**/
component {
/**
* @bird.hint Type of bird to use as an exclamation
* @bird.options Chickens,Turkeys,Flamingos,Vultures,Penguins
*/
function run( required string bird ) {
print.line()
.boldCyanLine( ' Oh my sweet #bird#! ' )
.line().toConsole();
}
}
Help and Completion
/**
* Craft your own avian exclamation, like Ben Nadel.
* This is how you call it:
*
* {code:bash}
* exclaim bird=chickens
* {code}
*
**/
component {
/**
* @bird.hint Type of bird to use as an exclamation
* @bird.options Chickens,Turkeys,Flamingos,Vultures,Penguins
*/
function run( required string bird ) {
print.line()
.boldCyanLine( ' Oh my sweet #bird#! ' )
.line().toConsole();
}
}
touch ./commands/exclaim.cfc --open
Help and Completion
/**
* Craft your own avian exclamation, like Ben Nadel.
* This is how you call it:
*
* {code:bash}
* exclaim bird=chickens
* {code}
*
**/
component {
/**
* @bird.hint Type of bird to use as an exclamation
* @bird.optionsUdf listBirds
*/
function run( required string bird ) {
print.line()
.boldCyanLine( ' Oh my sweet #bird#! ' )
.line().toConsole();
}
function listBirds( string paramSoFar, struct passedNamedParameters ) {
return [
"Albatrosses",
"Auklets",
"Bitterns",
"Blackbirds",
"Bluebirds",
"Buntings",
"Cardinals",
"Catbirds",
"Chickadees",
"Chickens",
"Cowbirds",
"Cranes",
"Crows",
"Cuckoos",
"Doves",
"Ducks",
"Eagles",
"Egrets",
"Falcons",
"Finchs",
"Flamingos",
"Goldfinches",
"Geese",
"Gulls",
"Hawks",
"Herons",
"Hummingbirds",
"Ibises",
"Jays",
"Kestrels",
"Kingfishers",
"Larks",
"Loons",
"Magpies",
"Mallards",
"Mockingbirds",
"Orioles",
"Ospreys",
"Owls",
"Pelicans",
"Penguins",
"Pheasants",
"Pigeons",
"Puffins",
"Quail",
"Ravens",
"Roadrunners",
"Robins",
"Sandpipers",
"Sapsuckers",
"Sparrows",
"Starlings",
"Storks",
"Swallows",
"Swans",
"Turkeys",
"Vultures",
"Warblers",
"Woodpeckers",
"Wrens"
];
}
}
// ModuleConfig.cfc
component {
function configure(){
settings = {
'bird': 'chickens',
'exclamation': true
}
}
}
Settings
component {
// property name='settings' inject='commandbox:moduleSettings:moduleName';
property name='settings' inject='commandbox:moduleSettings:commandbox-example';
function run(){
print.line( settings ).toConsole();
return;
}
}
Settings
config set modules.commandbox-example.bird="eagles"
config set modules.commandbox-example.exclamation=false
config show modules.commandbox-example
Settings
Digging into the APIs
ForgeBox
CommandBox Modules
box install commandbox-bullet-train
box install cfdocs
box install commandbox-cfformat
box install commandbox-update-check
Code Quality Commands
Go
Build
Something.
DO IT
WITH CFML
Building Tools with CommandBox
Matthew Clemente
Do It with CFML: Building Your Own Tools with CommandBox
By mjclemente
Do It with CFML: Building Your Own Tools with CommandBox
Presentation for Into the Box 2020
- 3,290