Running CFExecute From A Given Working Directory In Lucee CFML 5.2.9.31
When you invoke the CFExecute
tag in ColdFusion, there is no option to execute the given command from a particular working directory. That's why I recently looked at using the ProcessBuilder
class to execute commands in ColdFusion. That said, the default community response to anyone who runs into a limitation with the CFExecute
tag is generally, "Put your logic in a bash-script and then execute the bash-script.". I don't really know anything about bash scripting (the syntax looks utterly cryptic); so, I thought, it might be fun to try and create a bash script that will proxy arbitrary commands in order to execute them in a given working directory in Lucee CFML 5.2.9.31.
The goal here is to create a bash script that will take N-arguments in which the 1st argument is the working directory from which to evaluate the rest (2...N) of the arguments. So, instead of running something like:
ls -al path/to/things
... we could run something like:
my-proxy path/to ls -al things
In this case, we'd be telling the my-proxy
command to execute ls -al things
from within the path/to
working directory. To hard-code this example as a bash script, we could write something like this:
cd path/to # Change working directory.
ls -al ./things # Run ls command RELATIVE to WORKING DIRECTORY.
The hard-coded version illustrates what we're trying to do; but, we want this concept to by dynamic such that we could run any command from any directory. To this end, I've created the following bash script, execute_from_directory.sh
, through much trial and error:
#!/bin/sh
# In the current script invocation, the first argument needs to be the WORKING DIRECTORY
# from whence the rest of the script will be executed.
working_directory=$1
# Now that we have the working directory argument saved, SHIFT IT OFF the arguments list.
# This will leave us with a "$@" array that contains the REST of the arguments.
shift
# Move to the target working directory.
cd "$working_directory"
# Execute the REST of command from within the new working directory.
# --
# NOTE: The $@ is a special array in BASH that contains the input arguments used to
# invoke the current executable.
"$@"
CAUTION: Again, I have no experience with bash script. As such, please take this exploration with a grain of salt - a point of inspiration, not a source of truth!
What this is saying, as best as I think I understand it, is take the first argument as the desired working directory. Then, use the shift
command to shift all the other arguments over one (essentially shifting the first item off of the arguments array). Then, change working directory and execute the rest of the arguments from within the new working directory.
Because we are using the special arguments array notation, "$@"
, instead of hard-coding anything, we should be able to pass-in an arbitrary set of arguments. I think. Again, I have next to no experience here.
Once I created this bash-script, I had to change the permissions to allow for execution:
chmod +x execute_from_directory.sh
To test this from the command-line, outside of ColdFusion, I tried to list out the files in my images directory - path/to/my-cool-images
- using a combination of working directories and relative paths:
wwwroot# ./execute_from_directory.sh path/to ls -al ./my-cool-images
total 17740
drwxr-xr-x 11 root root 352 Apr 11 11:28 .
drwxr-xr-x 4 root root 128 Apr 15 10:07 ..
-rw-r--r-- 1 root root 1628611 Dec 1 20:04 broad-city-yas-queen.gif
-rw-r--r-- 1 root root 188287 Mar 4 14:09 cfml-state-of-the-union.jpg
-rw------- 1 root root 3469447 Jan 28 16:59 dramatic-goose.gif
-rw------- 1 root root 2991674 Dec 14 15:39 engineering-mistakes.gif
-rw-r--r-- 1 root root 531285 Dec 1 21:10 monolith-to-microservices.jpg
-rw-r--r-- 1 root root 243006 Dec 24 12:34 phoenix-project.jpg
-rw-r--r-- 1 root root 1065244 Jan 22 14:41 rob-lowe-literally.gif
-rw-r--r-- 1 root root 7482444 Mar 25 10:15 thanos-inifinity-stones.gif
-rw-r--r-- 1 root root 239090 Dec 29 13:08 unicorn-project.jpg
As you can see, I was able to execute the ls
command from within the path/to
working directory! Woot woot!
To test this from ColdFusion, I'm going to recreate my zip
storage experiment; but, instead of using the ProcessBuilder
class, I'm going to use the CFExecute
tag to run the zip
command through my execute_from_directory.sh
bash script:
<cfscript>
// Reset demo on subsequent executions.
cleanupFile( "./images.zip" );
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
// Normally, CFExecute has no sense of a "working directory" during execution.
// However, by proxying our command-line execution through a Shell Script (.sh), we
// can CD (change directory) to a given directory and then dynamically execute the
// rest of the commands.
executeFromDirectory(
// This is the WORKING DIRECTORY that will become the context for the rest of
// the script execution.
expandPath( "./path/to" ),
// This is the command that we are going to execute from the WORKING DIRECTORY.
// In this case, we will execute the ZIP command using RELATIVE PATHS that are
// relative to the above WORKING DIRECTORY.
"zip",
// These are the arguments to pass to the ZIP command.
[
// Regulate the speed of compression: 0 means NO compression. This is setting
// the compression method to STORE, as opposed to DEFLATE, which is the
// default method. This will apply to all files within the zip - if we wanted
// to target only a subset of file-types, we could have used "-n" to white-
// list a subset of the input files (ex, "-n .gif:.jpg:.jpeg:.png").
"-0",
// Recurse the input directory.
"-r",
// Define the OUTPUT file (our generated ZIP file).
expandPath( "./images.zip" ),
// Define the INPUT file - NOTE that this path is RELATIVE TO THE WORKING
// DIRECTORY! By using a relative directory, it allows us to generate a ZIP
// in which the relative paths become the entries in the resultant archive.
"./my-cool-images",
// Don't include files in zip.
"-x *.DS_Store"
]
);
echo( "<br />" );
echo( "Zip file size: " );
echo( numberFormat( getFileInfo( "./images.zip" ).size ) & " bytes" );
echo( "<br /><br />" );
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
/**
* I execute the given series of commands from the given working directory. The
* standard output is printed to the page. If an error is returned, the page request
* is aborted.
*
* @workingDirectory I am the working directory from whence to execute commands.
* @commandName I am the command to execute from the working directory.
* @commandArguments I am the arguments for the command.
*/
public void function executeFromDirectory(
required string workingDirectory,
required string commandName,
required array commandArguments
) {
// The Shell Script that's going to proxy the commands is expecting the working
// directory to be the first argument. As such, let's create a normalized set of
// arguments for our proxy that contains the working directory first, followed by
// the rest of the commands.
var normalizedArguments = [ workingDirectory ]
.append( commandName )
.append( commandArguments, true )
;
execute
name = expandPath( "./execute_from_directory.sh" ),
arguments = normalizedArguments.toList( " " )
variable = "local.successOutput"
errorVariable = "local.errorOutput"
timeout = 10
terminateOnTimeout = true
;
if ( len( errorOutput ?: "" ) ) {
dump( errorOutput );
abort;
}
echo( "<pre>" & ( successOutput ?: "" ) & "</pre>" );
}
/**
* I delete the given file if it exists.
*
* @filename I am the file being deleted.
*/
public void function cleanupFile( required string filename ) {
if ( fileExists( filename ) ) {
fileDelete( filename );
}
}
</cfscript>
As you can see, I've created an executeFromDirectory()
User-Defined Function (UDF) which takes, as its first argument, the working directory from which we are going to execute the rest of the commands. Then, instead of executing the zip
command directly, we are proxying it through our bash script.
And, when we run the above ColdFusion code, we get the following output:
Very cool! It worked! As you can see from the zip
debug output, the entries in the archive are based on the relative paths from the working directory that we passed to our proxy.
Now that I know that the ProcessBuilder
class exists, I'll probably just go with that approach in the future. That said, it was exciting (and, honestly, very frustrating) for me to write my first real bash-script to allow the CFExecute
tag to execute commands from a given working directory in Lucee CFML. Bash scripting seems.... crazy; but, it also seems something worth learning a bit more about.
Want to use code from this post? Check out the license.
Reader Comments
FYI: I added the attribute
directory
tocfexecute
which sets the working directory to the launched process. It's an experimental feature ATM but will be added officially in a future major version.https://www.linkedin.com/feed/update/urn:li:activity:6674054637471059968/
@Igal,
Woot woot woot!! This is very exciting! Thanks for digging into this :D
@All,
Here's an interesting follow-up post: using npm run-scripts from within Lucee CFML in order to leverage existing functionality; or, to just be able to have more control over the command-line tools:
www.bennadel.com/blog/3878-using-npm-run-scripts-to-execute-shell-commands-in-lucee-cfml-5-3-6-61.htm
This was fascinating to me because once your in the
npm
world, you have full control over the command-line, includingcd
'ing into another working directory (which was the goal of this overall post). Just a fun experiment.@Igal,
Woot woot! Lucee CFML 5.3.8 finally dropped! And I'm already thrilled to see the
CFExecute
tag getting that sweet, sweetdirectory
attribute:www.bennadel.com/blog/4074-using-the-directory-attribute-to-invoke-cfexecute-from-a-working-directory-in-lucee-cfml-5-3-8-189.htm
This is a much needed and much welcomed update. Probably one of the most exciting updates in 5.3.8 (for me).