Skip to main content
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Andrew Dixon
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Andrew Dixon ( @aandrewdixon )

Running CFExecute From A Given Working Directory In Lucee CFML 5.2.9.31

By on
Tags:

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:

Zip output showing relative paths generated from the working directory in Lucee CFML.

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

15,674 Comments

@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, including cd 'ing into another working directory (which was the goal of this overall post). Just a fun experiment.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel