Skip to main content
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Matthew Orstad
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Matthew Orstad

Building Executable Scripts For The Mac OSX Command Line With Node.js

By
Published in Comments (10)

Recently, I learned that there was a whole world of programming that could be performed at the command line of the Mac OSX terminal. In a previous post, I made a very simple Bash script that could be used to simplify the execution of a different, more complex command. Today, I wanted to explore the use of Node.js as an alternate interpretor for command line programming. And, since today is Valentine's Day, I thought I'd make it all about Love!

It seems that any text file on the Mac OSX file system can be turned into an executable if you change its permissions to include "x" - execute:

chmod +x someTextFile

Once this permission is applied, the given text file can be invoked as an executable from the command line:

./someTextFile

The mode in which the file gets interpreted is determined by the first line of the text file:

#! /bin/bash

This "hashbang" (aka shebang, aka pound-bang) tells the execution context which interpretor to use for the rest of the file content. In the above line (and in my previous post), I am using the Bash interpretor to execute the file. For this post, however, I want to use the Node.js interpretor to execute the file. To do this, all I have to do is replaced the Bash path with the Node path:

#! /usr/local/bin/node

Simple enough! Let's get to the code then. For this Valentine's Day, I thought it would be fun to create a Node.js-powered executable that would randomly select from a list of "loving phrases"; and then, have it copy the selected phrase to the clipboard. Furthermore, I thought it would be fun to involve some command-line arguments that would allow for the pool of phrases to be filtered prior to selection.

So, the following command would select from all possible phrases:

./love

... whereas the following commands would each select from a filtered list of phrases:

./love --general
./love --sappy
./love --naughty
./love --playful

Ok, let's take look at the code:

NOTE: I have left the ".js" file extension on this file so that GitHub would color-code it properly. Since the Node.js interpretor is being supplied using the hashbang, there's no real need for the file to actually have an extension.

love.js - Our Node.js-Powered Command Line Executable

#! /usr/local/bin/node

// ^-- Tell the terminal which interpreter to use for the execution
// ^-- of this script. For our purposes, we'll be using the Node.js
// ^-- interpretor (which was installed in the local BIN directory
// ^-- using HomeBrew.


// ---------------------------------------------------------- //
// ---------------------------------------------------------- //
// ---------------------------------------------------------- //
// ---------------------------------------------------------- //


// Include the system utility library (for output).
var util = require( "util" );

// Include the library for spinning up child processes. We'll need
// this to execute the "copy to clipboard" command.
var childProcess = require( "child_process" );


// ---------------------------------------------------------- //
// ---------------------------------------------------------- //


// Build up a list of loving things to say to your loved one. Each
// phrase can be tagged for use in command-line filtering.
var phrases = [];

// Create some tags to be used for filtering.
phrases.GENERAL = 1;
phrases.SAPPY = 2;
phrases.NAUGHTY = 3;
phrases.PLAYFUL = 4;

// Populate the phrases.
phrases.push({
	text: "I love you so much!",
	tag: phrases.GENERAL
});

phrases.push({
	text: "Sometimes, I have to pinch myself, I'm so happy with you!",
	tag: phrases.SAPPY
});

phrases.push({
	text: "You are the love of my life!",
	tag: phrases.SAPPY
});

phrases.push({
	text: "I love you more than a kitten loves milk!",
	tag: phrases.PLAYFUL
});

phrases.push({
	text: "Hey shmoopy, I miss you.", // For Seinfeld fans.
	tag: phrases.PLAYFUL
});

phrases.push({
	text: "I can't stop thinking about your body!",
	tag: phrases.NAUGHTY
});

phrases.push({
	text: "I was just thinking of you and it made me smile :)",
	tag: phrases.GENERAL
});

phrases.push({
	text: "I wanna dip you in pudding and eat you for dessert!",
	tag: phrases.NAUGHTY
});


// ---------------------------------------------------------- //
// ---------------------------------------------------------- //


// Now that we have our phrase library populated, let's see if we
// need to filter the list using a command-line argument. By default,
// we'll be selecting from all of the phrases.
var filterArgument = (process.argv[ 2 ] || "");
var filterTag = null;

// Check to see if the use has supplied a filter.
switch (filterArgument){

	case "":

		filterTag = null;

	break;

	case "--general":

		filterTag = phrases.GENERAL;

	break;

	case "--sappy":

		filterTag = phrases.SAPPY;

	break;

	case "--naughty":

		filterTag = phrases.NAUGHTY;

	break;

	case "--playful":

		filterTag = phrases.PLAYFUL;

	break;

	// If we could not figure out what the command-line argument was,
	// then something is incorrect. Exit out.
	default:

		util.puts( "Filter not recognized." );
		util.puts( "Use: --general, --sappy, --naughty, --playful" );

		// Exit out of the process (as a failure).
		process.exit( 1 );

	break;

}


// ---------------------------------------------------------- //
// ---------------------------------------------------------- //


// It's time to select the phrase. Since we'll need to select a
// random phrase from a pool of phrases, let's factor out the
// phrases first, then we'll select them. Start by assuming that
// we will not be filtering.
var filteredPhrases = phrases;

// Check to see if we need to filter.
if (filterTag){

	// Create a list of filtered phrases for the given tag.
	filteredPhrases = phrases.filter(
		function( phrase ){

			// Only include the phrase if the filter matches.
			return( phrase.tag === filterTag );

		}
	);

}

// Now, let's generate a random index from the array.
var targetIndex = (
	Math.floor( Math.random() * 1000 ) % filteredPhrases.length
);

// Get the target phrase.
var phrase = filteredPhrases[ targetIndex ];


// ---------------------------------------------------------- //
// ---------------------------------------------------------- //


// Now that we have the phrase, we need to copy it to the clipboard.
// For this, we'll use the [pbcopy] command in a child process:
//
// $> echo "YOUR_SELECTED_PHRASE" | pbcopy
//
childProcess.exec(
	("echo \"" + phrase.text + "\" | pbcopy"),
	function( error, stdout, stderr ){

		// Check to see if the PastBoard copy worked.
		if (error){

			// Something went wrong.
			util.puts(
				"Hmm, we could not copy \"" + phrase.text +
				"\" to the clipboard."
			);

			// Output the error.
			util.puts( "ERROR: " + stderr );

		} else {

			// Woot! It all worked.
			util.puts(
				"\"" + phrase.text +
				"\" has been copied to your clipboard!"
			);

		}

		// Exit out with success!!
		process.exit( 0 );

	}
);

The code works by building up a collection of phrases, selecting one at [pseudo] random, and then copying it to the user's clipboard. For the clipboard copy, I am invoking the pbcopy command in a child process. This is the first time I have every explicitly created a child process, so I hope that it makes some sense.

Here's a snippet of terminal output based on the love.js usage:

ben$ ./love.js
"You are the love of my life!" has been copied to your clipboard!

ben$ ./love.js --sappy
"Sometimes, I have to pinch myself, I'm so happy with you!" has been copied to your clipboard!

ben$ ./love.js --playful
"Hey shmoopy, I miss you." has been copied to your clipboard!

ben$ ./love.js --general
"I was just thinking of you and it made me smile :)" has been copied to your clipboard!

ben$ ./love.js --naughty
"I wanna dip you in pudding and eat you for dessert!" has been copied to your clipboard!

ben$ ./love.js
"I was just thinking of you and it made me smile :)" has been copied to your clipboard!

Pretty cool stuff!

I don't pretend to know much about command line scripting. But, I do know a lot about JavaScript! It's pretty awesome that, with Node.js installed, we can start to leverage our existing client-side skills to enhance non-browser contexts! Badass!

Want to use code from this post? Check out the license.

Reader Comments

22 Comments

Just talking about this today and looking forward to having this for CFers in Railo 4 and future ACF versions...

dom$ ./pdfIzeMyWordDocsAndEmailThemToMe.cfm /path/to/my/docs me@me.com

Etc.

22 Comments

@Ben,

You got it (at least it was talked about for Railo 4), the ability to run CFML from the command line would be pretty darn awesome.

15,841 Comments

@Dominic,

Oh yeah, now I remember Gert saying something about that at CFUNITED - being able to export CFML code to an executable. That would pretty darn cool of they could do it! I thought it would be funny to be able to build code in Railo and then execute it in ACF using CFExecute.

We'll see where they go with that!

1 Comments

To run you need to prefix the program with "./" otherwise the spend will look for in your direction for a computer file with this name.

1 Comments

I just stumbled across this. I've been toying with node a decent amount lately so I enjoyed reading it...the sample quotes cracked me up. thanks for the tutorial

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