Building Executable Scripts For The Mac OSX Command Line With Node.js
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
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.
@Dominic,
I am not sure what you mean? Are there going to be command-line tools in Railo / ACF?
@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.
@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!
You should also checkout the optimist NodeJS module:
https://github.com/substack/node-optimist
It makes working with the command line options a lot easier.
Interesting . . . didn't know you could do this, use JavaScript to run commands on your OSX setup.
Thanks for this post.http://www.ebuysilver.com I am a vegetarian and realy like a delicious and healthy meal and will definately check this one out next time am in Durham
I don't understand any web code.so sorry.
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.
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