26 March 2016

Make your hubot coffee scripts into reusable standalone programs

In part 1 of this 2 part series on making your module.exports reusable as standalone programs in JavaScript/CoffeeScript, I explained that this pattern allows for flexible reuse of a function like main:

var main = function(args) {
  console.log(args)
}

module.exports = main

if (module.parent === null) {
  main(process.argv)
}

In this article, I’ll show how to make a hubot script that wraps the dig command into a reusable standalone program. Assuming you’re using Linux, OSX, or another BSD and you have dig installed, let’s get started.

Lets pretend that this would actually be useful, and keep in mind this is just proof of concept code. It isn’t gonna win any prizes for inventiveness, brevity, clarity, or robustness. Indeed, it will fail in a few subtle ways if you actually try to use it, so don’t!

That disclaimer aside, here’s how reusable code looks in a hubot CoffeeScript plugin:

{spawn} = require 'child_process'
util = require 'util'

dig = (args, res) ->
  if args.length > 0
    domain = args[0].replace(/http(s)?:\/\//gi, '')
    cp = spawn 'dig', [domain]
    output = ''
    cp.stdout.on 'data', (data) ->
      output += data.toString()
    cp.stderr.on 'data', (data) ->
      output += data.toString()
    cp.on 'exit', (code) ->
      if res
        res.reply output
      else
        console.log output
  else
    reply = 'Please provide a domain'
    if res
      res.reply reply
    else
      console.log reply


module.exports = (robot) ->
    robot.respond /dig/i, (res) ->
        args = res.envelope.message.text.split(' ').slice(2)
        dig args, res


if module.parent is null
    dig process.argv.slice(2)

Not much to say here. The wrapper does a little slicing and dicing and just does an A record lookup. Since some incoming messages from places like Slack or XMPP will automatically prepend domain names with http/https there’s a regex to strip that out.

Invoking via a hubot does what you’d expect:

hubot> hubot-j dig jamon.ca
hubot> Shell:
; <<>> DiG 9.8.3-P1 <<>> jamon.ca
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 57573
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;jamon.ca.			IN	A

;; ANSWER SECTION:
jamon.ca.		300	IN	A	45.32.177.243

;; Query time: 48 msec
;; SERVER: 4.2.2.2#53(4.2.2.2)
;; WHEN: Sun Mar 27 12:25:58 2016
;; MSG SIZE  rcvd: 42

And run via the command like, you get the same output:

jamon@vagrant: /tmp/hubot>>
 0 % ./node_modules/.bin/coffee scripts/dig.coffee jamon.ca

; <<>> DiG 9.8.3-P1 <<>> jamon.ca
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 54298
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;jamon.ca.			IN	A

;; ANSWER SECTION:
jamon.ca.		141	IN	A	45.32.177.243

;; Query time: 20 msec
;; SERVER: 4.2.2.2#53(4.2.2.2)
;; WHEN: Sun Mar 27 12:28:37 2016
;; MSG SIZE  rcvd: 42

The main point is this structure that makes the dig function reusable in a bot, and as a standalone program:

dig = (args, res) ->
  .....

module.exports = (robot) ->
  ....

if module.parent is null
  dig process.arvg.slice(2)

The reason that this works as I mentioned in part 1 is that the module.parent object does not exist when invoking via the command line. Here’s a full console.dir listing to demonstrate what I mean:

Module {
  id: '.',
  exports: [Function],
  parent: null,
  filename: '/tmp/hubot/scripts/test.coffee',
  loaded: false,
  children:
   [ Module {
       id: '/tmp/hubot/node_modules/coffee-script/lib/coffee-script/command.js',
       exports: [Object],
       parent: [Circular],
       filename: '/tmp/hubot/node_modules/coffee-script/lib/coffee-script/command.js',
       loaded: true,
       children: [Object],
       paths: [Object] } ],
  paths:
   [ '/tmp/hubot/scripts/node_modules',
     '/tmp/hubot/node_modules',
     '/home/vagrant/projects/node_modules',
     '/home/vagrant/node_modules',
     '/home/node_modules',
     '/node_modules' ] }

The astute reader will also note that the id attribute is set to . here. That’s another place where you could check to see how the program is invoked. I prefer checking module.parent since it is just null or not, but either will work.

And that’s about it. I’m curious to hear what others make of this pattern, if you’ve used it, any pitfalls that I might have overlooked etc.

Feel free to get in touch via the various means over to your right -> -> ->

Cheers! Jamon