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 »
19 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 »
11,241 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 »
19 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 »
11,241 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


Sep 19, 2012 at 4:58 AM // reply »
1 Comments

I don't understand any web code.so sorry.


Apr 12, 2013 at 5:11 AM // reply »
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.


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
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 22, 2013 at 4:43 AM
How Do You Use The ColdFusion CFParam Tag?
'<cfparam>' or 'isDefined()and <cfset>' performs the same task.Is there any difference? ... read »
May 21, 2013 at 7:46 PM
Using Plupload For Drag & Drop File Uploads In ColdFusion
No luck. At least I have uncovered the cause, URLScan 3.1. Here is what I see in the IIS log when a file is over 30mb. 2013-05-21 23:29:05 10.105.45.128 GET /plupload/assets/jquery/jquery-1.8. ... read »
May 21, 2013 at 6:12 PM
Using Plupload For Drag & Drop File Uploads In ColdFusion
Ben, I did not see you after Pete Freitag's Lockdown session at cfObjective but he said that IIS sets file size limits at 30MB by default which just happened to be the threshold for file size when ... read »
May 21, 2013 at 11:51 AM
Ask Ben: Parsing Very Large XML Documents In ColdFusion
Looking at my first ever XML document that I have to parse and put into MS SQL 2000 with CF8. I get it to list the desired Field name, many times over, and have a long list of this field name displa ... read »
May 21, 2013 at 9:25 AM
Turning Off and On Identity Column in SQL Server
you are awesome..i am lucky to get this blog between such a garbage one....Thanks, Prashant ... read »
May 20, 2013 at 4:38 PM
Using A Dynamic Column Name With ValueList() In ColdFusion
@Dana, Your confusion is well founded, since this is a very confusing features. In fact, it ONLY works if you use array notation. Meaning, that this: arrayToList( query[ "columnName" ] ) ... read »
May 20, 2013 at 4:34 PM
Using A Dynamic Column Name With ValueList() In ColdFusion
I was thinking chicken and the egg, I wouldn't have expected it to work in the valuelist going in I guess. Maybe I just need a beer, long day :) ... read »
May 20, 2013 at 4:29 PM
Using A Dynamic Column Name With ValueList() In ColdFusion
@Dana, That's if you're trying to reference a specific row. In this case, we're trying to reference the entire query column as one cohesive value. So, you are correct that if you wanted to output a ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools