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

Posted February 14, 2012 at 10:55 AM by Ben Nadel

Tags: Javascript / DHTML

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!




Reader Comments

Feb 14, 2012 at 11:43 AM // reply »
14 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.


Feb 14, 2012 at 1:27 PM // reply »
10,743 Comments

@Dominic,

I am not sure what you mean? Are there going to be command-line tools in Railo / ACF?


Feb 14, 2012 at 1:39 PM // reply »
14 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.


Feb 14, 2012 at 1:42 PM // reply »
10,743 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!


Feb 15, 2012 at 12:10 AM // reply »
1 Comments

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.


Feb 15, 2012 at 6:53 AM // reply »
148 Comments

Interesting . . . didn't know you could do this, use JavaScript to run commands on your OSX setup.


Feb 16, 2012 at 3:09 AM // reply »
1 Comments

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


Post A Comment

Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
InVision App - Prototyping Made Beautiful With Prototyping Tools Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 21, 2012 at 1:58 AM
Updated: Converting A ColdFusion Query To CSV Using QueryToCSV()
Hi Ben, why do you need to have so many double quotes when adding the field and field name to the row data? ----------------------------------------- <cfset LOCAL.RowData[ LOCAL.ColumnIndex ] = ... read »
AXL
May 21, 2012 at 1:24 AM
URL Rewriting And ColdFusion's WriteToBrowser Image Functionality (CFFileServlet)
@Mounir, Open your lower case URL Rewrite rule and add the following condition. Condition input: {REQUEST_URI} Check if input string: Does Not Match the Pattern Pattern: ^/CFFileServlet/_cf_ca ... read »
May 20, 2012 at 4:28 AM
Understanding The Complex And Circular Relationships Between Objects In JavaScript
@Will Vaughn I tried your javascript example but got this error:- foo.print is not a function ... read »
May 19, 2012 at 5:37 AM
A Graphical Explanation Of Javascript Closures In A jQuery Context
Thanks for this article, but I fear you missed an important point. If variables in the outer context change, these changes affect the inner anonymous functions as well. That means: if you change the ... read »
May 18, 2012 at 3:39 PM
Parsing CSV Data With An Input Stream And A Finite State Machine
Can you use file upload button with this? and read live? or does the file have to already be on the server saved? ... read »
May 18, 2012 at 1:06 AM
VIRGO (Aug. 23-Sept. 22): Dead On The Money!
A friend of mine and I were arguing about astrology and she told me that he believes in astrology. She hasn't provided me with any evidence that the belief makes any sense to me. She she been telling ... read »
May 17, 2012 at 11:32 PM
Using ColdFusion to Handle 404 Errors (Page Not Found) On Development Server
Very easy the configuration. I read a lot pages and I can't find the solution. I open the administrator and change this Administrator/server settings/Error Handlers/Missing Template Handler and p ... read »
May 17, 2012 at 3:13 PM
LOCAL Variables Scope Conflicts With ColdFusion Query of Queries
I never cease to be amazed that almost EVERY random CF issue I come across lands me on your site. Thank you for documenting your findings for the world. ... read »