Tuesday 3 November 2015

ColdFusion 2016: the long-awaited CLI

G'day:
Another thing Adobe showed at CFCamp - and I have had clearance to write about - is that ColdFusion finally has a CLI. About bloody time. It's about... well... 15yrs overdue. Well to be fair to Allaire, ColdFusion 5 could run files from the command-line, but this functionality was omitted from ColdFusion between then and now.

Having a command-line is dead useful, because it means one doesn't need to have a web server running to run CFML code. Indeed one doesn't even have to have a ColdFusion server running to run CFML code! This is handy for scripting, or - for me - testing quick code snippets.

Here it is in action:

Microsoft Windows [Version 6.1.7601]
Copyright (c) 2009 Microsoft Corporation.  All rights reserved.

D:\src\cfml\scopes\server>CF server_dump.cfm
struct

coldfusion:
        [struct]
        InstallKit: Native Windows
        appserver: JRun4
        expiration: {ts '2015-11-15 08:18:28'}
        productlevel: Evaluation
        productname: ColdFusion Server
        productversion: 11,0,01,296113
        rootdir: C:\apps\adobe\ColdFusion\2016\express\cfusion
        supportedlocales: [snip]
        updatelevel: 01
os:
        [struct]
        additionalinformation: [empty string]
        arch: amd64
        buildnumber: [empty string]
        name: Windows 7
        version: 6.1

D:\src\cfml\scopes\server>


Note one good thing here right off the bat: <cfdump> detects it's being run via the CLI and doesn't piss about with all it's mark-up, it just does a text-only dump.

Note also one other interesting thing: JRun. Huh? I thought we were done with that? Anyway, let's not worry about that too much. Although I will raise it with Adobe.

Command line arguments


The ColdFusion CLI has two types of command line arguments. Firstly there are the familiar space-separated ones:


D:\src\CF2016\cli\args>cf ordered.cfm tahi rua toru wha
All arguments - array

1) tahi
2) rua
3) toru
4) wha


First argument: tahi
D:\src\CF2016\cli\args>

And also named ones:

D:\src\CF2016\cli\args>cf named.cfm one=tahi two=rua three=toru four=wha
All arguments - struct

four: wha
one: tahi
three: toru
two: rua

'two' argument: rua
D:\src\CF2016\cli\args>

The code for each of these examples demonstrates the new CLI functionality regaridng input and output:

<cfscript>
// ordered.cfm
writeDump(var=CLI.getArgs(), label="All arguments");
CLI.writeLn("");
CLI.writeLn("First argument: " & CLI.getArg(1));
</cfscript>

<cfscript>
// named.cfm
writeDump(var=CLI.getNamedArgs(), label="All arguments");
CLI.writeLn("");
CLI.writeLn("'two' argument: " & CLI.getNamedArg("two"));
</cfscript>

So ColdFusion has a new CLI object exposed when running on the CLI. Note that this is not a scope, it is an object. It has methods (as demonstrated above):

  • getArgs() - returns all space-separated arguments
  • getArg(n) - returns the nth argument
  • getNamedArgs() - returns all the name/value pairs (separated by =)
  • getNamedArg(name) - returns the value of the named argument
  • writeLn() - writes a string to standard out, followed by a new line
  • write() - as above, but without the new line


There's a few other methods I've not looked at yet, as demonstrated in this dump:


D:\src\CF2016\cli\cliObjectTests>cf dumpCli.cfm
object of coldfusion.runtime.CLIBridge

Class Name: coldfusion.runtime.CLIBridge
Methods:
        addArg(java.lang.String) returns void
        clone() returns java.lang.Object
        getArg(int) returns java.lang.Object
        getArgs() returns coldfusion.runtime.Array
        getNamedArg(java.lang.String) returns java.lang.Object
        getNamedArgs() returns coldfusion.runtime.Struct
        putNamedArg(java.lang.String, java.lang.Object) returns void
        read() returns java.lang.Object
        write(java.lang.Object) returns void
        writeError(java.lang.String) returns void
        writeln(java.lang.Object) returns void
D:\src\CF2016\cli\cliObjectTests>

Application lifecycle


CLI calls respect Application.cfc, but only the application-level event handlers, as demonstrated here:

// Application.cfc

component {
    
    this.name = "test04";
    this.sessionManagement = true;

    cli.writeln("#getCurrentTemplatePath()# called");

    function onApplicationStart(){
        cli.writeln("#getFunctionCalledName()#() called");
    }

    function onSessionStart(){
        cli.writeln("#getFunctionCalledName()#() called");
    }

    function onRequestStart(){
        cli.writeln("#getFunctionCalledName()#() called");
    }

    function onRequest(){
        cli.writeln("#getFunctionCalledName()#() called");
        include arguments[1];
    }

    function onRequestEnd(){
        cli.writeln("#getFunctionCalledName()#() called");
    }

    function onSessionEnd(){
        cli.writeln("#getFunctionCalledName()#() called");
    }

    function onApplicationEnd(){
        cli.writeln("#getFunctionCalledName()#() called");
    }
}

This yields:
D:\src\CF2016\cli\applicationCfcTests>cf test.cfm
D:\src\CF2016\cli\applicationCfcTests\Application.cfc called
onApplicationStart() called
D:\src\CF2016\cli\applicationCfcTests\test.cfm
onApplicationEnd() called
D:\src\CF2016\cli\applicationCfcTests>

Also note that the application lives and dies with the script call here.

TestBox

TestBox even runs OK out of the... erm... box. It works fine if yer tests pass:


D:\src\CF2016\cli\testBox>cf runTests.cfm
_____         _   ____
 |_   _|__  ___| |_| __ )  _____  __
   | |/ _ \/ __| __|  _ \ / _ \ \/ /
   | |  __/\__ \ |_| |_) | (_) >  <
   |_|\___||___/\__|____/ \___/_/\_\ v2.2.0+@build.number@
=============================================================

Global Stats (392 ms)
=============================================================
->[Bundles/Suites/Specs: 1/2/2]
->[Pass: 2]
->[Failures: 0]
->[Errors: 0]
->[Skipped: 0]
->[Labels Applied: ]
=============================================================
Tests (78 ms)
=============================================================
->[Suites/Specs: 2/2]
->[Pass: 2]
->[Failures: 0]
->[Errors: 0]
->[Skipped: 0]
 (+)First set of tests
    (+)This is the first test (which passes) (32 ms)
(+)Second set of tests
    (+)This is the third test (which passes) (0 ms)
=============================================================
Legend:
=============================================================
(P) = Passed
(-) = Skipped
(X) = Exception/Error
(!) = FailureD:\src\CF2016\cli\testBox>


But it needs some work if there are failures:

D:\src\CF2016\cli\testBox>cf runTests.cfm
_____         _   ____
 |_   _|__  ___| |_| __ )  _____  __
   | |/ _ \/ __| __|  _ \ / _ \ \/ /
   | |  __/\__ \ |_| |_) | (_) >  <
   |_|\___||___/\__|____/ \___/_/\_\ v2.2.0+@build.number@
=============================================================

Global Stats (578 ms)
=============================================================
->[Bundles/Suites/Specs: 1/2/4]
->[Pass: 2]
->[Failures: 2]
->[Errors: 0]
->[Skipped: 0]
->[Labels Applied: ]
=============================================================
Tests (121 ms)
=============================================================
->[Suites/Specs: 2/4]
->[Pass: 2]
->[Failures: 2]
->[Errors: 0]
->[Skipped: 0]
 (!)First set of tests
    (+)This is the first test (which passes) (31 ms)
    (!)This is the second test (which fails) (0 ms)
        -> Failure: Expected [false] but received [true]
        -> Failure Origin: [{RAW_TRACE={        at cfAssertion2ecfc691355197$fun
cFAIL.runFunction(D:\src\frameworks\testbox\system\Assertion.cfc:16)},LINE={16},
COLUMN={0},TEMPLATE={D:\src\frameworks\testbox\system\Assertion.cfc},ID={CFTHROW
},TYPE={CFML}}, {RAW_TRACE={    at cfAssertion2ecfc691355197$funcISEQUAL.runFunc
tion(D:\src\frameworks\testbox\system\Assertion.cfc:67)},LINE={67},COLUMN={0},TE
MPLATE={D:\src\frameworks\testbox\system\Assertion.cfc},ID={CF_UDFMETHOD},TYPE={
CFML}}, {RAW_TRACE={    at cfExpectation2ecfc1724520611$funcTOBE.runFunction(D:\
src\frameworks\testbox\system\Expectation.cfc:150)},LINE={150},COLUMN={0},TEMPLA
TE={D:\src\frameworks\testbox\system\Expectation.cfc},ID={CF_TEMPLATEPROXY},TYPE
={CFML}}, {RAW_TRACE={  at cfTests2ecfc890816971$func_CF_ANONYMOUSCLOSURE_2.runF
unction(D:\src\CF2016\cli\testBox\Tests.cfc:9)},LINE={9},COLUMN={0},TEMPLATE={D:\s
rc\CF2016\cli\testBox\Tests.cfc},ID={CF_TEMPLATEPROXY},TYPE={CFML}}, {RAW_TRACE={
at cfBaseSpec2ecfc516366033$funcRUNSPEC.runFunction(D:\src\frameworks\testbox\sy
stem\BaseSpec.cfc:441)},LINE={441},COLUMN={0},TEMPLATE={D:\src\frameworks\testbo
x\system\BaseSpec.cfc},ID={CF_UDFMETHOD},TYPE={CFML}}, {RAW_TRACE={     at cfBDD
Runner2ecfc1312189979$funcTESTSUITE.runFunction(D:\src\frameworks\testbox\system
\runners\BDDRunner.cfc:164)},LINE={164},COLUMN={0},TEMPLATE={D:\src\frameworks\t
estbox\system\runners\BDDRunner.cfc},ID={CF_TEMPLATEPROXY},TYPE={CFML}}, {RAW_TR
ACE={   at cfBDDRunner2ecfc1312189979$funcRUN.runFunction(D:\src\frameworks\test
box\system\runners\BDDRunner.cfc:66)},LINE={66},COLUMN={0},TEMPLATE={D:\src\fram
eworks\testbox\system\runners\BDDRunner.cfc},ID={CF_UDFMETHOD},TYPE={CFML}}, {RA
W_TRACE={       at cfTestBox2ecfc337885211$funcTESTBUNDLE.runFunction(D:\src\fra
meworks\testbox\system\TestBox.cfc:352)},LINE={352},COLUMN={0},TEMPLATE={D:\src\
frameworks\testbox\system\TestBox.cfc},ID={CF_TEMPLATEPROXY},TYPE={CFML}}, {RAW_
TRACE={ at cfTestBox2ecfc337885211$funcRUNRAW.runFunction(D:\src\frameworks\test
box\system\TestBox.cfc:165)},LINE={165},COLUMN={0},TEMPLATE={D:\src\frameworks\t
estbox\system\TestBox.cfc},ID={CF_UDFMETHOD},TYPE={CFML}}, {RAW_TRACE={ at cfTes
tBox2ecfc337885211$funcRUN.runFunction(D:\src\frameworks\testbox\system\TestBox.
cfc:96)},LINE={96},COLUMN={0},TEMPLATE={D:\src\frameworks\testbox\system\TestBox
.cfc},ID={CF_UDFMETHOD},TYPE={CFML}}, {RAW_TRACE={      at cfrunTests2ecfm185183
0234.runPage(D:\src\CF2016\cli\testBox\runTests.cfm:2)},LINE={2},COLUMN={0},TEMPLA
TE={D:\src\CF2016\cli\testBox\runTests.cfm},ID={CF_TEMPLATEPROXY},TYPE={CFML}}]
(!)Second set of tests
    (+)This is the third test (which passes) (0 ms)
    (!)This is the fourth test (which fails) (16 ms)
        -> Failure: Expected [false] but received [true]
        -> Failure Origin: [{RAW_TRACE={        at cfAssertion2ecfc691355197$fun
cFAIL.runFunction(D:\src\frameworks\testbox\system\Assertion.cfc:16)},LINE={16},
COLUMN={0},TEMPLATE={D:\src\frameworks\testbox\system\Assertion.cfc},ID={CFTHROW
},TYPE={CFML}}, {RAW_TRACE={    at cfAssertion2ecfc691355197$funcISEQUAL.runFunc
tion(D:\src\frameworks\testbox\system\Assertion.cfc:67)},LINE={67},COLUMN={0},TE
MPLATE={D:\src\frameworks\testbox\system\Assertion.cfc},ID={CF_UDFMETHOD},TYPE={
CFML}}, {RAW_TRACE={    at cfExpectation2ecfc1724520611$funcTOBE.runFunction(D:\
src\frameworks\testbox\system\Expectation.cfc:150)},LINE={150},COLUMN={0},TEMPLA
TE={D:\src\frameworks\testbox\system\Expectation.cfc},ID={CF_TEMPLATEPROXY},TYPE
={CFML}}, {RAW_TRACE={  at cfTests2ecfc890816971$func_CF_ANONYMOUSCLOSURE_5.runF
unction(D:\src\CF2016\cli\testBox\Tests.cfc:17)},LINE={17},COLUMN={0},TEMPLATE={D:
\src\CF2016\cli\testBox\Tests.cfc},ID={CF_TEMPLATEPROXY},TYPE={CFML}}, {RAW_TRACE=
{       at cfBaseSpec2ecfc516366033$funcRUNSPEC.runFunction(D:\src\frameworks\te
stbox\system\BaseSpec.cfc:441)},LINE={441},COLUMN={0},TEMPLATE={D:\src\framework
s\testbox\system\BaseSpec.cfc},ID={CF_UDFMETHOD},TYPE={CFML}}, {RAW_TRACE={
at cfBDDRunner2ecfc1312189979$funcTESTSUITE.runFunction(D:\src\frameworks\testbo
x\system\runners\BDDRunner.cfc:164)},LINE={164},COLUMN={0},TEMPLATE={D:\src\fram
eworks\testbox\system\runners\BDDRunner.cfc},ID={CF_TEMPLATEPROXY},TYPE={CFML}},
 {RAW_TRACE={   at cfBDDRunner2ecfc1312189979$funcRUN.runFunction(D:\src\framewo
rks\testbox\system\runners\BDDRunner.cfc:66)},LINE={66},COLUMN={0},TEMPLATE={D:\
src\frameworks\testbox\system\runners\BDDRunner.cfc},ID={CF_UDFMETHOD},TYPE={CFM
L}}, {RAW_TRACE={       at cfTestBox2ecfc337885211$funcTESTBUNDLE.runFunction(D:
\src\frameworks\testbox\system\TestBox.cfc:352)},LINE={352},COLUMN={0},TEMPLATE=
{D:\src\frameworks\testbox\system\TestBox.cfc},ID={CF_TEMPLATEPROXY},TYPE={CFML}
}, {RAW_TRACE={ at cfTestBox2ecfc337885211$funcRUNRAW.runFunction(D:\src\framewo
rks\testbox\system\TestBox.cfc:165)},LINE={165},COLUMN={0},TEMPLATE={D:\src\fram
eworks\testbox\system\TestBox.cfc},ID={CF_UDFMETHOD},TYPE={CFML}}, {RAW_TRACE={
at cfTestBox2ecfc337885211$funcRUN.runFunction(D:\src\frameworks\testbox\system\
TestBox.cfc:96)},LINE={96},COLUMN={0},TEMPLATE={D:\src\frameworks\testbox\system
\TestBox.cfc},ID={CF_UDFMETHOD},TYPE={CFML}}, {RAW_TRACE={      at cfrunTests2ec
fm1851830234.runPage(D:\src\CF2016\cli\testBox\runTests.cfm:2)},LINE={2},COLUMN={0
},TEMPLATE={D:\src\CF2016\cli\testBox\runTests.cfm},ID={CF_TEMPLATEPROXY},TYPE={CF
ML}}]
=============================================================
Legend:
=============================================================
(P) = Passed
(-) = Skipped
(X) = Exception/Error
(!) = FailureD:\src\CF2016\cli\testBox>

But this is a case of the text reporter just offering too much information in the wrong part of the report, rather than it not working. So that's pretty cool.

STDIN

One of the things I do with PHP a lot is to just run it semi-interactively. PHP has a CLI much the same as ColdFusion's, except if one doesn't specify a file, it'll read from STDIN, eg:

Microsoft Windows [Version 6.1.7601]
Copyright (c) 2009 Microsoft Corporation.  All rights reserved.

d:\src>php
<?php
$message = "G'day World";
echo $message;
^Z
G'day World
d:\src>

So I don't give PHP a file here so it listens on STDIN until it receives an EOF (CTRL-Z), then it processes the input as a file.

Note this is not interactive so it's not a REPL, but it's still bloody handy for testing small snippets of code.

Ruby's CLI is the same.

The ColdFusion CLI does not support this by default, but as it's all run out of a batch file, it's easy enough to tweak it so it does. I've just added this at the top of mine:

IF "%1"=="" (
    echo ^<cfscript^> > ____.cfm 
    type con >> ____.cfm
    echo ^</cfscript^> >> ____.cfm
    call cf.bat ____.cfm
    del ____.cfm
    exit /b
)

It's a bit jerry-built, but it works:


d:\src>cf
message = "G'day World";
CLI.writeLn(message);
^Z
G'day World
d:\src>

Note that in my example I am automatically wrapping the code in <cfscript> tags, as that's pretty much all I ever use anyhow.

That's about it, at the moment.

I'm not sure if there's anything else that's been exposed to the public that I can talk about? Hopefully I'll have some more after CFSummit next week.

--
Adam