Node shell scripting

January 2nd, 2017

You want some menial job automated? Some research-y task done? Parse some CSV files, do something with the data? Batch process? Well why not use the language already know instead of learning bash, shell, php scripting or whatever?

Nodejs

Grab the latest version from https://nodejs.org/en/download/current/ and let's go!

Check, check, 1, 2, 3

Open your favorite command prompt/terminal/shell (Mac users, go to Spotlight search, type "Terminal"), then type:

$ node -v

(I use $ to denote something you type in the terminal, you need not type it.)

You should see the version of the Node you just installed displayed in the terminal.

v

Hello script

Now let's create the simplest script and make sure it works.

$ touch hello.js
$ open hello.js

In your editor of choice, type the following in your hello.js file:

console.log(__dirname);

Save the file, now run it:

$ node hello.js

Ta-da!

hello

As you can see

  • You run a shell script with $ node scriptname.js
  • You can print results back to the user with console.log
  • You can figure out the directory where your script is located with the constant __dirname. BTW, similarly __filename gives you the full path and the name of the script, in this example /Users/stoyanstefanov/Desktop/nodeshell/hello.js

__dirname is useful when you have for example a bunch of files you want to fiddle with in a subdirectory. You don't want to limit where your script can be run from. So it's best to avoid relative paths but always start with __dirname.

Read a directory

Now, imagine you need a script that does something with a list of files you conveniently put in data subdirectory together with your script.

dir

Let's list all the files in this directory using a new script you can call dirlist.js.

$ touch dirlist.js

You start by including two libraries (modules): fs (as in "file system") ans path.

const fs = require('fs');
const path = require('path');

path helps you deal with differences in Windows vs Unix slashes and backslashes and so on. It's alwayts best if you avoid string concatenation and use path's methods instead to make your scripts more reusable and resilient.

fs gives you a number of methods to read, write, delete files and directories.

For the purpose of listing a directory you can use fs.readdir or fs.readdirSync. Let's go with the second. Why? Well, it's simpler. You'll notice several methods have synchonous and asynchronous versions. The async versions are definitely better because they don't block your scripts with one-after-the-other synchronous operations. They tend to make your scripts run faster because you can do several things in parallel. However they are a bit more complicated because you need to provide a callback function to be notified when the async operation is complete.

To keep things simple, sync all the way!

And so function that gives you an array of files in a directory is:

const readDir = fs.readdirSync;

Which directory to list?

const dataDir = path.resolve(__dirname, 'data');

Using resolve you can concatenate the current directory and the data subdirectory without any backslashes, etc.

Now, reading the directory gives you an array and you can simply print the name of each file in the directory in a forEach loop.

readDir(dataDir).forEach(f => console.log(f));

So the whole script is:

const fs = require('fs');
const path = require('path');

const readDir = fs.readdirSync;

const dataDir = path.resolve(__dirname, 'data');

readDir(dataDir).forEach(f => console.log(f));

And the script in action....

$ node dirlist.js 
1.txt
hello.txt
what.cvs

You probably want the full path to each file if you're going to read it, so just another path.resolve() call should do the trick:

readDir(dataDir).forEach(f => 
  console.log(path.resolve(dataDir, f))
);

dirlist

Filtering out things you don't care about

Say you run dirlist.js and spits out something like:

$ node dirlist.js 
/Users/stoyanstefanov/Desktop/nodeshell/data/.DS_Store
/Users/stoyanstefanov/Desktop/nodeshell/data/1.txt
/Users/stoyanstefanov/Desktop/nodeshell/data/hello.txt
/Users/stoyanstefanov/Desktop/nodeshell/data/moar-data
/Users/stoyanstefanov/Desktop/nodeshell/data/what.cvs

Darn Dot-files

There's a .DS_Store file in there, some Max OS garbage you've no use for. Let's remove it from the list of files. You can simply check if the file name starts with a dot (.), like so...

Before:

readDir(dataDir).forEach(f => 
  console.log(path.resolve(dataDir, f))
);

After:

readDir(dataDir).forEach(f => {
  if (f.startsWith('.')) {
    return; // no .DS_Store etc, thank you
  }
  const file = path.resolve(dataDir, f);
  console.log(file);
});

Files only

How about removing the moar-data directory from the list? You only care about files. There's a stat() method in the fs module that can help here. More specifically, the sync version, statSync().

const stat = fs.statSync;

Calling this method on a file gives you properties (such as file creation time) and methods (such as isDirectory()). Here's how you can filter out directories:

readDir(dataDir).forEach(f => {
  if (f.startsWith('.')) {
    return; // no .DS_Store etc, thank you
  }

  const file = path.resolve(dataDir, f);
  const stats = stat(file);
  
  if (stats.isDirectory()) {
    return; // files only shall pass
  }

  console.log(file);
});

Now the list contains interesting files only:

$ node dirlist.js 
/Users/stoyanstefanov/Desktop/nodeshell/data/1.txt
/Users/stoyanstefanov/Desktop/nodeshell/data/hello.txt
/Users/stoyanstefanov/Desktop/nodeshell/data/what.cvs

Tip: stats.size gives you file size.

txt files only

And what if this script only works with text files? Your friend is path.extname(file).

if (path.extname(file) !== '.txt') {
  return; // actually text files only
}

Tip: explore what path.parse() can do for you. E.g.

console.log(
  require('path').parse(__filename)
);

You should see something like:

{
  root: '/',
  dir: '/Users/stoyanstefanov/Desktop/nodeshell',
  base: 'dirlist.js',
  ext: '.js',
  name: 'dirlist' 
}

Recursive directory listing

What if the subdirectory moar-data contains additional files you're interested in? Let's list your main directory and its subdirectories and their subdirectories. In other words, list recursively.

All you need to do is put your forEach loop into a function listFiles so it an be reused. This function takes any directory as input (listFiles(dir)), lists it and if it finds that one of the items in the list is another directory (stats.isDirectory()) it navigates into the new directory, instead of ignoring it like before.

const fs = require('fs');
const path = require('path');

const readDir = fs.readdirSync;
const stat = fs.statSync;

const dataDir = path.resolve(__dirname, 'data');

function listFiles(dir) {
  
  readDir(dir).forEach(f => {
    if (f.startsWith('.')) {
      return; // no .DS_Store etc, thank you
    }

    const file = path.resolve(dir, f);
    const stats = stat(file);
  
    if (stats.isDirectory()) {
      return listFiles(file);
    }
    
    if (path.extname(file) !== '.txt') {
      return; // actually text files only
    }
    
    console.log(file);      
  });
}

listFiles(dataDir);

The script in action:

$ node dirlist-recursive.js 
/Users/stoyanstefanov/Desktop/nodeshell/data/1.txt
/Users/stoyanstefanov/Desktop/nodeshell/data/hello.txt
/Users/stoyanstefanov/Desktop/nodeshell/data/moar-data/2.txt
/Users/stoyanstefanov/Desktop/nodeshell/data/moar-data/bye.txt
/Users/stoyanstefanov/Desktop/nodeshell/data/moar-data/even-more/nomore.txt

Reading files

/* TODO */

Parsing file contents

/* TODO */

Creating directories

/* TODO */

Writing files

/* TODO */

process

Where am I?

process.cwd()

I'm done!

process.exit(1)

Executing other command-line tools

/* TODO */

The Web

Dealing with URLs

/* TODO */

cURL

/* TODO */

Scraping with PhantomJS

/* TODO */

Reading JSON files

/* TODO */

Writing JSON files

Using third party modules

/* TODO */

Taking command line arguments

Arguments array

/* TODO */

commander

Project: DIY unit testing scripts

/* TODO */

Publishing your script to NPM

/* TODO */

js4php: thanks!

April 15th, 2013

JS4PHP book is almost a wrap! It started as "well, I have this blog post, then I have these slides from this conference, how hard could it be to make it into a book form?" Pretty hard, turns out. I'm surprised every time. Takes a while, you get distracted by something shinier and so on... No such thing as "quick book project", for me, at least.

But it's almost ready to go, O'Reilly folks are optimistic it will be done in time for the Fluent conference in May (where I'm not speaking btw). The price on Amazon really makes me happy, I think $7.90 for a tech book is unheard of.

JavaScript for PHP developers

So anyway, just wanted to say thanks to people that helped with the book along the way.

Thanks

First and foremost, big thanks and gratitude to my second-time reviewers who helped me with "JavaScript Patterns" before, the Three Musketeers and d'Artagnan, the ever-so-amazing Andrea Giammarchi, Asen Bozhilov, Dmitry Soshnikov and Juriy "kangax" Zaytsev. As with the previous book they helped me tremendously with their deep knowledge and experience with JavaScript, their attention to detail and technical accuracy above all. You'll see little notes in the book following bold general statements, these notes often come from one of these guys saying: "hmm, this is not entirely correct and not always true, because...". I am forever in debt to these four ridiculously talented developers who also happen to be great and friendly people.

Many thanks to Chris Shiflett and Sean Coates. They made sure the PHP side of things make sense, but what's more, this whole book started as a post on their PHPAdvent (now WebAdvent.org) blog, followed by a talk at the ConFoo conference which Sean helps organize.

Next, thanks to the Facebook engineers that hang out on the JavaScript group. I posted an early draft there asking for comments. Three people even went through the whole thing and gave me invaluable feedback and further nitpicking, which is the best quality in a technical reviewer. Thanks to Alok Menghrajani, James Ide and Alex Himel.

Finally, thanks to Randy Owens who read the "early release" of the book and meticulously filed tens of errata reports.

Unit testing in AsciiDoc

February 12th, 2013

While finishing off this book, which I choose to write in AsciiDoc format, I really appreciate the time I took to write myself a handy little script for testing the little code examples. Because, something always gets wrong in the final steps of editing and is nice to have some automated assurance that you didn't break something stupid.

AsciiDoc or MarkDown or some other text setup is how technical books should be written. Or any books, really. Having clear plain text lets you focus on the content and not wrestle with Word formatting. And it's also easy to parse and test.

Code in AsciiDoc

The code blocks in AsciiDoc are easy to spot. They look like:

[source,js]
----------------------------------------------------------------------
var a = 0xff;
a === 255; //:: true
----------------------------------------------------------------------

Testing and linting

All you have to do in a little unit testing utility is to extract these clearly defined blocks and run them to make sure there are no silly syntax errors, creeping in in the very last edits.

But why stop with a syntax check? Why not also run jslint and also instrument and run the code and see any expected values. Asserts, in testspeak.

In my setup I did just that: lint with jshint, instrument the places where I know the expected values, lint these too (why the hell not?), run them and assert the expected values.

Output

Here's how the output of the utility looks like:

$ node scripts/test.js 
passed: 362, skipped: 43
linted: 317, nolints: 45

Since this is a book with some educational bad examples, not everything can be linted. Or run for that matter.

JSHint

I choose JSHint over JSLint as it allows more flexibility to relax the rules. Also it's a node module I can import.

var jslint = require('jshint').JSHINT;

These are my lint options:

// JSHint options need to be more relaxed because the book also
// points out bad patterns
var lintopts = {
  indent: 2,   // 2 spaces for indentation
  trailing: true, // disallow spaces at the end of a line
  white: true,
  plusplus: false, // allows ++ and --
  browser: true,   // assumes some common browser globals exist
                   // such as `document`
  node: true,  // assumes the code can run in node.js
               // and globals such as `global` are defined
  expr: true,  // ok to have expressions that seemingly do nothing
               // such as `a; // true` which the samples use to show
               // result values
  loopfunc: true, // allows definition of a function in a loop
                  // for educational purposes (in the part about closures)
  newcap: false,  // allows calling constructors (capitalized functions) 
                  // without `new`, again just for educational purposes
  proto: true,    // allows using `__proto__` which is great for understanding
                  // prototypes, although it's not supported in all browsers
};

And the lint function that lints a snippet of code:

// lint a snippet
function lint(snip) {
  // lint the snippet with all the options and have it assume
  // assert objext exists
  if (!jslint(snip, lintopts, {assert: false})) {
    log('------');
    log(snip);
    log('------');
    log(jslint.errors[0]);
    process.exit();    
  }
}

Instrumenting and executing a snippet

Execution is simple:

// run a snippet
function exec(snip) {
  // muck some stuff up and zap log()
  var mock = "function define(){}; function alert(){}; console.log = function(){};";
  try {
    eval(snip + mock);
    passed++;
  } catch (e) {
    log('------');
    log(snip);
    log('------');
    log(e.message);
    process.exit();
  }
}

The instrumentation is a little more interesting.

I often write snippets like:

// assign
var a = 1;
// test
a; // 1

Now I need a little bit of marker that will tell the instrumentation that a; is a piece of code to execute and 1 is the expected value. I came up with //::

So the above becomes:

var a = 1;
a; //:: 1

I also like to add some more explanation besides the returned value. I use ,, for that. So:

var a = 1;
a; //:: 1,, as you'd expect

Instrumented, this becomes:

var a = 1;
assert.deepEqual("a;",  1, "Error line #2");

The line is the line in the book with all code and prose, so it's easy to find.

The special markup like //:: and ,, gets stripped by another script that I run before commit.

A few other features are: support for NaN which doesn't deepEqual to anything and expecting errors with assert.throws(..)

So I can write and test code like:

sum(21, 21); //:: 42
plum(21, 21); //:: Error:: plum() is not defined

(:: that follow Error is the same as ,,)

Also:

Number("3,14"); //:: NaN

So here's the code instrumentation:

// Add asserts
function prep(l, n) {
  var parts = l.split(/;\s*\/\/::/); // "//::" separates expression to execute from its result
  var nonspace = parts[0].match(/\S/);
  var spaces = nonspace === null ? "" : Array(nonspace.index + 1).join(" ");
  parts[0] = parts[0].trim();
  if (parts[1]) {
    var r = parts[1].split(/\s*(,,|::)\s*/)[0].trim(); // the result may have ,, or ::, ignore what's on the right
                                                       // e.g. //:: true,, of course!
                                                       // e.g. //:: ReferenceError::Invalid whatever
    if (r.indexOf('Error') !== -1) {
      // expect //:: Error to throw
      return spaces + 'assert.throws(function () {' + parts[0] + '; }, ' + r + ', "error line #' + n + '");';
    }
    if (r === 'NaN') {
      // special NaN case
      return spaces + 'assert(isNaN(' + parts[0] + '), true, "error line #' + n + '");'
    }
    // usual
    return spaces + 'assert.deepEqual(' + parts[0] + ', ' + r + ', "error line #' + n + '");';
  }
  return l;
}

Main

Dependencies, locals, options and the main parser loop is how it all begins/ends:

// dependencies where I can see them
var assert = require('assert');
var fs = require('fs');
var jslint = require('jshint').JSHINT;
 
// buncha local vars
var snip, rawsnip.....;
 
// short
var log = console.log;
 
 
// JSHint options 
var lintopts = {
  indent: 2,   // 2 spaces for indentation
  // ....
};
 
// read the book one line at a time
fs.readFileSync('book.asc').toString().split('\n').forEach(function(src, num) {
 
  // src is a line in the book
  // num is the line number
 
});

There are a few additional features at snippet-level:

Ability to continue from a previous snippet using --//-- at the top of the snippet delimiter

Let's declare a variable:

[source,js]
----------------------------------------------------------------------
var a = 1;
----------------------------------------------------------------------

And then another one:

[source,js]
--------------------------------------------------------------------//--
var b = 2;
----------------------------------------------------------------------

And let's sum

[source,js]
--------------------------------------------------------------------//--
a + b; //:: 3
----------------------------------------------------------------------

Ability to skip a non-working snippet using ////

[source,js]
----------------------------------------------------------------------////--
var 1v;  // invalid
----------------------------------------------------------------------

Ability to run in non-strict mode (because strict is default) using ++

[source,js]
----------------------------------------------------------------------++--
var a = 012;
a === 10; //:: true
----------------------------------------------------------------------

nolint option

[source,js]
----------------------------------------------------------------------
/*nolint*/
assoc["one"]; //:: 1
----------------------------------------------------------------------

Cleanup before commit

Cleaning up all instrumentation markers and hints for the lint and the tests (gist):

var clean = require('fs').readFileSync('book.asc').toString().split('\n').filter(function(line) {
  if (line.indexOf('/*nolint*/') === 0 || line.indexOf('/*global') === 0) {
    return false;
  }
  return true;
})
  .join('\n')
  .replace(/--\+\+--/g, '--')
  .replace(/--\/\/--/g, '--')
  .replace(/--\/\/\/\/--/g, '--');
 
console.log(clean);

Github gist

Here's the test.js script in its entirety.

Optional parameters

February 11th, 2013

JavaScript has no syntax that allows you to have a default value for a function parameter as you often do in most other languages. This is scheduled for a future version of ECMAScript, but for now you have to take care of this yourself inside the body of your function.

There are several patterns that do the job, but here's a new one. It was suggested to me by Andrea "WebReflection" Giammarchi in his technical review of the upcoming JS4PHP book.

Andrea doesn't remember blogging about this pattern and I don't remember ever seeing it. So here goes.

Say you have a function with all 4 default parameters, mimicking for example PHP's declaration:
function sum($a = 1, $b = 2, $c = 3, $d = 4) ...

function sum(a, b, c, d) {
  // note no `break` needed
  switch (arguments.length) {
    case 0: a = 1;
    case 1: b = 2;
    case 2: c = 3;
    case 3: d = 4;
  }
  return a + b + c + d;
}

Test:

sum();            // 10
sum(1);           // 10
sum(11);          // 20
sum(1, 2, 3, 24); // 30
sum(11, 22);      // 40

Obviously this doesn't work when you have an optional param, followed by a required one, but that's just bad design.

Thoughts?

Shim sniffing

June 4th, 2012

Extending native objects and prototypes is bad. If not vile, mean and Jesuitic.

// Noooooo!
Array.prototype.map = function() {
  // stuff
};

Unless it's desirable, for example for adding ECMAScript5 methods in legacy browsers.

In which case we do something like:

if (!Array.prototype.map) {
  Array.prototype.map = function() {
    // stuff
  };
}

If we're paranoid enough we can even try to protect from somebody defining map as something unexpected like true or "the peaches are this way":

if (typeof Array.prototype.map !== "function") {
  Array.prototype.map = function() {
    // stuff
  };
}

(Although that ultimately breaks the other developer's map to the peach trees)

But in a hostile dog-eat-dog cut-throat environment (in other words when you provide or consume a library), you trust no one. What if that other smartass JS loads before your badass JS and defines map() in a way that is not really ES5-compliant and your code doesn't work anymore?

You can always trust browsers though. If Webkit implements map() you can relax that it should probably work OK. Otherwise you'd want to go ahead with your shim.

Luckily that's easy to do in JavaScript. When you call toString() of a native function it should return a string with a function that has a body of [native code]

So for example in Chrome's console:

> Array.prototype.map.toString();
"function map() { [native code] }"

A proper check is ever-so-slightly painful because browsers seem to be a little frivolous with white spaces and new lines. Testing:

Array.prototype.map.toString().replace(/\s/g, '*');
// "*function*map()*{*****[native*code]*}*"  // IE
// "function*map()*{*****[native*code]*}" // FF
// "function*map()*{*[native*code]*}" // Chrome

Simply stripping all \s will give you something more workable:

Array.prototype.map.toString().replace(/\s/g, '');
// "functionmap(){[nativecode]}"

You can opt in for a reusable shim() function so you don't have to repeat all that !Array.prototype... jazz. It can take an object to augment (e.g. Array.prototype), a property to add (e.g. 'map') and a function that implements the shim.

function shim(o, prop, fn) {
  var nbody = "function" + prop + "(){[nativecode]}";
  if (o.hasOwnProperty(prop) &&
      o[prop].toString().replace(/\s/g, '') === nbody) {
    // native!
    return true;
  }
  // shim
  o[prop] = fn;
}

Testing:

// this is native, cool
shim(
  Array.prototype, 'map',
  function(){/*...*/}
); // true
 
//  this is new
shim(
  Array.prototype, 'mapzer',
  function(){alert(this)}
);
 
[1,2,3].mapzer(); // alerts 1,2,3

p.s. And then
there's JJD's! (backstory)

The ridiculous case of adding a script element

September 10th, 2011

Adding a script element to a page should be a no-brainer. Yet, it's ridiculously unreliable in the wild - when you don't have any idea of the surrounding markup.

You know the drill - create a script element, point its src to a URL and add it to the page so that script file can be downloaded in a non-blocking manner.

Creating the script is nice and easy:

var js = document.createElement('script');
js.src = 'myscript.js';

The problem is how to add it to the page.

1. to the head

Probably the most common approach is to append to the head of the document:

document.getElementsByTagName('head')[0].appendChild(js);

You get all head elements (there should be only one) and you add the script there so the result is

<html>
  <head>
    <script>
    ...

But what if there's no head in the document? Remember, we want solid robust code that always works.

Well turns out that most browsers will create the head element even if the tag is not there.

Most, but not all as Steve Souders' browserscope test shows. Exceptions include browsers like Opera 8 but also Android 1.6 and one iPhone 3 - hardly old and negligible.

So head is out.

2. add to the body

This is even shorter:

document.body.appendChild(js);

what if there's no <body> tag? Well my test shows that all tested browsers will create a body element. Even one that has a working appendChild() method. Now, while some of those that don't create head, do create body, I'm still a little uncomfortable that I couldn't find a single browser that doesn't create body. Makes me feel a little worried about the testing and data collecting.

But even if we assume that in all browsers, document.body always exist, there's still a problem. IE7 and the dreaded "Operation aborted" error.

If you modify BODY while loading the page and from a script that is not a direct child of body, but nested in another element, you get Operation aborted and nothing works.

Surprisingly simple page fails miserably:

<html>
  <body>
      <div>
          <script>
          var js = document.createElement('script'); 
          js.async = true;
          js.src = "http://tools.w3clubs.com/pagr/1.sleep-2.js";
          document.body.appendChild(js);
          </script>
      </div>
  </body>
</html>

See it live in IE7 and be amazed

Now operation aborted may be solved with a defer, but maybe not in IE9.

Another reason not to attempt anything with body is if the script is included in the head. At execution time we have not reached <body> yet, so document.body doesn't exist. Demo.

3. use documentElement

document.documentElement is the html doc itself. Now that's got to exist no matter what.

So you go like

var html = document.documentElement;
html.insertBefore(js, html.firstChild);

And it works!

But what if the firstChild is a comment before the head? This makes something kinda weird:

 
<html>
  <script>
  <!-- comment -->
  <head>
    ..

Script obviously has no place there, but my test page worked in ie6789, and recent versions of FF, Chrome, Safari, Opera.

But looks like there are other browsers where this comment thing fails as reported(btw, good to read the whole post and all the comments) by Google Analytics folks who say they have received complaints when they used to do that. There might be other cases or browsers I didn't try. So reluctantly, we move on.

4. hook to the first script

Ahaa, if you're running a script, then this script must either be inline <script>bla</script> or external <script src="meh.js">. Either way, there's gotta be at least one script tag! Wo-hoo!

So the final solution is:

var first = document.getElementsByTagName('script')[0];
first.parentNode.insertBefore(js, first);

No matter where the first script might be, we glue out new one right above it.

Drawback is that looking for script nodes might be a little more expensive than looking for document.documentElement or document.body or the single match of getElemenetsByTagName('head')

There's still a case when there might not be a script element at all. Ah, impossible, since we're running a script there must be a script element! Well (and thanks to @kangax who pointed this to me while he was reviewing JavaScript Patterns!) here's one example:

<body onload="alert('Look ma, executing script without a script tag!')">

Overall while not completely foolproof, this is as close as you can get to being able to achieve the simple task - add a new script node. Isn't web development just magnificent?

(Funny thing if you use this only to load a script asynchronously: in order to load a script (file), you need a script that refers to a script, possibly itself. JavaScript all around.)

Once again the whole snippet:

var js = document.createElement('script');
js.src = 'myscript.js';
var first = document.getElementsByTagName('script')[0];
first.parentNode.insertBefore(js, first);

“defer” for IE7′s operation aborted error

September 9th, 2011

IE has this "operation aborted" problem (MSDN article) when you try to change your parent while it's not ready.

This is the example they give that causes this error:

<html>
  <body>
    <div>
      <script>
        document.body.innerHTML+="sample text";
      </script>
    </div>
  </body>
</html>

Turns out you can easily solve it by just adding a defer attribute to the script. This works just fine:

<html>
  <body>
    <div>
      <script defer>
        document.body.innerHTML+="sample text";
      </script>
    </div>
  </body>
</html>

Demo page here

BTW, this operation aborted may bite you if you decide to append a script node to the body, like

var js = document.createElement('script');
js.async = true;
js.src = "http://tools.w3clubs.com/pagr/1.sleep-2.js";
document.body.appendChild(js); 

So it's not a good idea to append to the body like this when you don't control the page you're adding this code to, although document.body seems to be always available even when the page doesn't have a <body> tag

Scripting Photoshop with JavaScript

August 20th, 2011

Did you know you can script common Photoshop tasks with JavaScript? Now you do :)

IDE even

When you install PS it comes with a tool called ExtendScript Toolkit, which is an IDE to write scripts - with debugger, console to try stuff out etc.

To launch I just type "Extend" in Spotlight search. I'm guessing it's got to be somewhere in Program Files\Adobe on Windows too.

Here it is:

Getting started

As you can see on the screenshot, it's easy to get started.

app; // the Photoshop application
app.documents; // collection of open files/documents
app.documents[0]; // the first file
app.documents[0].name; // the first file's filename
app.documents[0].layers.length // how many layers in the doc

Running your script

There's a green "play" button in the IDE, it runs your script.

The directive:

#target photoshop

tells the engine that this is a photoshop script. Apparently you can script other Adobe products too.

If you don't have photoshop open, this directive will open it for you. Then it's all you - open a file, hide a layer, save, close, etc

Also if you save your script file with a .jsx extension, you can put in on the desktop and double-click it. Easy as pie.

And finally, if you put it in the appropriate directory (PSDIR/Presets/Scripts), it will show up in Photoshop menus under File -> Scripts

Docs

A *Yahoo* search will find the docs for you right here:
http://www.adobe.com/devnet/photoshop/scripting.html

There's a scripting guide (nice) and a JavaScript reference (the exact object/method names).

There are also VBScript and AppleScript references in case you insist on writing non-portable scripts :)

Generating code

Getting started with a completely new API can be a little uphill-y walk. For me it was especially so when it comes to saving a file with all the options. Worry not, you can generate code too!

Check the scripting guide where it talks about "The ScriptListener Plug-In". It's a plugin that you already have, you just copy it to to the appropriate directory to activate it and then restart PS.

Then whatever you do in photoshop will generate code in a .log file on your desktop. Then you can go back and cleanup that file (it generates somewhat verbose code) and get the parts you need.

Example - Mojotune chords

So for this "JavaScript is Everywhere" talk I gave this year at OSCON (slides) I came up (with a friend's help) with a sample application that I ported to different environments.

The core of it is m.js (m as in mojo, m as in (data) model). It's a portable piece of JavaScript that knows a lot about guitar chords. I wanted to generate nice PNGs with the data from the chord model. So - Photoshop scripting.

The document template is shown here with three visible layers.

This is the guitar's neck and each dot is where you press. The template has a layer for each dot (each fret on each string), appropriately named:

The task is to get the chord configuration, show the corresponding layers and save the file with the chord name as filename.

So I did this manually (while recording the generated code): show a layer, save the file.

Show/hide layers

The generated code for showing a layer looked like an oddly indented piece of overhead:

/////// GENERATED - DON'T!

// =======================================================
var idShw = charIDToTypeID( "Shw " );
    var desc2 = new ActionDescriptor();
    var idnull = charIDToTypeID( "null" );
        var list1 = new ActionList();
            var ref1 = new ActionReference();
            var idLyr = charIDToTypeID( "Lyr " );
            ref1.putName( idLyr, "11" );
        list1.putReference( ref1 );
    desc2.putList( idnull, list1 );
executeAction( idShw, desc2, DialogModes.NO );

This goes out the door since it can be replaced with:

app.documents[0].layers.getByName('11').visible = true;

(11 means first fret on first string)

Save for web

The generated code for saving a PNG is even worse, but I simply reindented it and shoved in into a function and that was that.

function saveFile(filename) {
    // the following is mostly auto-generated by PS 
    var idExpr = charIDToTypeID( "Expr" );
    var desc14 = new ActionDescriptor();

    /// 200 more lines...
}

All the chords

Finally the part that I actually had to write myself.

Directives:

#target photoshop
#include "../../lib/m.js"

Notice how easy it is to include a JS file, in my case the data model m.js?

Singe var pattern:

var data, 
    i, c, v, 
    layer,
    layers = app.documents[0].layers;

The mojo object comes from m.js. How it works is not very important. The thing is it has a mojo.guitar.getChord() method. You give it a chord name and it returns an array - which fret needs to be pressed on each string.

// mojo.guitar.getChord('Am')
// returns [ignore, 0, 1, 2, 2, 0, x]

All we need to do then is loop though the array and show the layers. Then save the file (e.g. Am.png) and then hide the same layers so they don't interfere the next chord. So this is the end result which generates all the chords m.js knows about:

for (c = 0; c < mojo.prettytones.length; c++) {
    for (v = 0; v < mojo.allchords.length; v++) {
        chord = mojo.prettytones[c].split('/')[0];
        chord += mojo.allchords[v];
        data = mojo.guitar.getChord(chord);
        for (i = 1; i <= 6; i++) {
            layer = "" + i + data[i];
            layers.getByName(layer).visible = true;
        }
        chord = chord.replace('#', '-sharp');
        saveFile('~/stoyan/mojo/chords/images/' + chord + '.png');
        for (i = 1; i <= 6; i++) {
            layer = "" + i + data[i];
            layers.getByName(layer).visible = false;
        }
    }
}

Run it and that's it - a bunch of PNG images are written.

Linkies

  • mojotune.jsx - The final script
  • m.js - the data model ("Check out Guitar George, he knows all the chords" - Dire Straits)
  • images - the generated chord PNGs
  • JS everywhere: slides and code

JavaScript classes

May 5th, 2011

OK, think of it as a religious flamewar to the likes of "tabs vs. spaces for indentation". Looks like this particular war is currently (at JSConf and NodeConf) even more heated than it should be. Classes vs prototypes. For or against classes.

I personally don't care about the "winner". The thing is that classes currently don't exist in JavaScript. No such thing. However looks like they might in the next iterations of the language (see latest post from Brendan Eich). Some people miss classes so much that they start calling other things classes or come up with some approximation. Problem is, because classes don't exist, people often mean different things when they say "class".

Sometimes they mean "constructor functions". Sometimes they mean a regular object literal (singleton-type thing. Heck, "singleton" is also open for interpretations). Sometimes they mean an object or a function defined using Crockford's module pattern.

Sometimes it's some completely different home-grown (or library-provided) thingie called klass for example (in my "JavaScript Patterns" book I have one example for educational/thought-provoking purposes). It has to be klass, or _class or something weird. Because class is a reserved word. Unused, but reserved. And one day may be full of meaning. See the problem?

I avoid saying "class". It just doesn't exist. Imagine two months down the road ECMAScript comes up with classes. And they will most certainly not be the classes you may mean today (e.g. classes won't be another name for constructor functions, I'm sure).

So any written text/blog/documentation you've produced will become incorrect and even worse - misleading and confusing.

To summarize:

  • saying "class" today is confusing and takes extra effort to process (what do you mean? what was that again? it doesn't really exist and is not defined in the language, so an extra translation step is required)
  • saying "class" today will read plain wrong tomorrow, will confuse and misinform

Note:
Heck, even I have this "how to define a JavaScript class" post on my other blog. I wrote it years ago when, coming from PHP, I was curious how stuff works in JavaScript. Well I got it wrong then. But fixed it not too long ago because it was top 1 result in google and Yahoo search for "javascript class" and "javascript classes" and I didn't want to continue contributing to the confusion.

Note 2:
To my regret, I couldn't make it to JSConf (aka the best!) nor NodeConf this year (because all girls and women in my life are born in May and it's impossible to travel) so I may be a little off on the level of flamewarfare, but according to Twitter I'm not.

And the winner is…

April 5th, 2011

In the previous post I announced that one person will win a copy of the Brazilian Portuguese translation of JavaScript Patterns. So here's the winner:

@LouMintzer

He wins a signed copy of "Padrões JavaScript". Waiting for your mailing address, Lou :)

Update: Lou Doesn't speak Portuguese, so he gets an English copy. The second winner is:

@puresight

Update: Monty is not too proud of his Portuguese skills either, so third is

@abozhilov

Asen is an awesome JavaScripter, who actually reviewed the book (thanks a million!) and he already has a copy. Next.

@mexitek

How I picked the winner

By writing some JavaScript in the console, of course.

The winner was to be randomly picked from all those who retweet my tweet or post a comment in the announcement. So I had to collect those.

Twitter

The tweet page says there has been 27 retweets (worded a bit like 28, but looks like it's 27). The page only shows about 15 people though and I need all of them. Given how Twitter doesn't let you search older stuff, I was afraid it was too late. I had to check the API first. I was expecting I can hit a few URLs and get the data I need. Tough luck. All these auth keys, tokens, secrets and stuff got me floored.

Luckily Twitter's UI is also using the APIs. Checking the network traffic I was able to spot the request I need!

The URL is:
http://api.twitter.com/1/statuses/49323240014872576/retweeted_by.json?count=15
I only needed to change the count to something over 27, so I made it 30. Lo and behold I got the data!

The rest of the stuff I did in Safari's Web Inspector console.

Visiting the URL:
http://api.twitter.com/1/statuses/49323240014872576/retweeted_by.json?count=30

We have a JSON array as a document.

>>> var a = document.body.innerHTML
>>> a
"<pre style="word-wrap: break-word; white-space: pre-wrap;">[{"profile_link_color":"0084B4","verified":false,"not....]</pre>"

Safari puts all in a PRE behind the scenes, so this is how we get the data:

>>> var source = $$('pre')[0].innerHTML;
>>> source
"[{"profile_link_color":"0084B4","verified":...]"

eval() it:

>>> source = eval(source)
[Object, Object...]
>>> source.length
27

Sounds right. Now let's move all usernames into a new array using the new ECMA5 forEach fancy-ness:

>>> var all = [];
>>> source.forEach(function(e){all.push(e.screen_name)})
>>> all.length
27
>>> all
["jrfaqcom", "gustavobarbosa", "gabrielsilva", ...."vishalkrsingh"]

Blog comments

I had 4 comments on the original post. WordPress puts all comments in a div with class commentlist, so this allows us to grab all comments:

>>> var comments = $$('.commentlist cite a')
>>> comments.length
4

Now let's only grab the names, they are in the href's innerHTML:

>>> var all = [];
>>> comments[0].innerHTML
"Fabiano Nunes"
>>> comments.forEach(function(e){all.push(e.innerHTML)})
TypeError: Result of expression 'comments.forEach' [undefined] is not a function.

Eh? What? Oh, the list of HREFs is not an array but a NodeList:

>>> comments
[
<a href="http://fabiano.nunes.me" rel="external nofollow" class="url">Fabiano Nunes</a>
, 
<a href="http://www.gabrielizaias.com" rel="external nofollow" class="url">Gabriel Izaias</a>
, 
<a href="http://www.jrfaq.com.br" rel="external nofollow" class="url">João Rodrigues</a>
, 
<a href="http://www.jrfaq.com.br" rel="external nofollow" class="url">João Rodrigues</a>
]
>>> comments.forEach
undefined

So, list of nodes converted to array:

>>> comments = Array.prototype.slice.call(comments)

Now forEach is usable:

>>> comments.forEach
function forEach() {
    [native code]
}
>>> comments.forEach(function(e){all.push(e.innerHTML)})

So we have a list of all names. Lets serialize it, so it can be pasted to the other window where we had the Twitter data.

>>> all
["Fabiano Nunes", "Gabriel Izaias", "João Rodrigues", "João Rodrigues"]
>>> JSON.stringify(all)
"["Fabiano Nunes","Gabriel Izaias","João Rodrigues","João Rodrigues"]"

(Simple array join and then string split will do too in this simple example)

All together

Back to the twitter window. Deserializing the comments array:

>>> comments = JSON.parse('["Fabiano Nunes","Gabriel Izaias","João Rodrigues","João Rodrigues"]')
["Fabiano Nunes", "Gabriel Izaias", "João Rodrigues", "João Rodrigues"]

Merging the two arrays

>>> all = all.concat(comments);
["jrfaqcom", "gustavobarbosa", ...."João Rodrigues"]
>>> all.length
31

Perfect. 31 entries. Just as many as the days in March when I announced it. So let's take the 19th array element to be the winner.

But shuffle the array a bit first.

Suffle

Sorting the array by randomness. (I shuffled and reshuffled it three times, just because.)

>> all.sort(function() {return 0 - (Math.round(Math.random()))})
["ravidsrk", "anagami", "lpetrov", ...]

And the winner is:

>>> all[18]
"LouMintzer"