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,232