Jesse Clark’s Blog

Dynamically-generated Cakefile Tasks

by Jesse Clark
Published

Cake is a task-runner for CoffeeScript.

It can be useful to invoke external scripts from cake, if they are written in another language or simply don't fit in the Cakefile. Suppose we have a scripts directory populated with various text-based executables, and we want to automatically define a cake task for each of those scripts, with an appropriate description, like so:

$ ls scripts
build.py
clean.sh

$ cake
cake build                    # build the project
cake clean                    # remove built files

$ cake build --help
build.py: usage info...

We can do this in the Cakefile by reading each script and extracting its first comment, then defining a task with that comment as its description:

# Define tasks for any scripts in the `scripts` directory
for basename in fs.readdirSync('./scripts') then do (basename)->
    return if basename[0] is '.'
    filename = "./scripts/#{basename}"
    contents = fs.readFileSync(filename, 'utf8')
    title = basename.replace(/\..*?$/, '')
    # use the first block comment in the file as its description
    description = ''
    for line, i in contents.split(/[\r\n]+/)
        if line.match(/^#!/) then continue
        if line.match(/-\*- mode/) then continue
        if line.match(/^\s*(#|\/\/|$)/)
            description += line.replace(/^\s*(#|\/\/)\s*/, ' ').trim()
            continue
        # stop reading when we encounter non-comment text
        break

    task(title, description, ->
        # pass command-line arguments directly in to the script
        index = process.argv.indexOf(title)
        args = process.argv.slice(index+1)
        child_process.spawn(filename, args, {
            stdio: 'inherit'
            env: env
        }).on('exit', (code, signal)->
            process.exit(code)
        )
        # prevent `cake` from invoking further tasks based on those arguments
        global.invoke = (->)
    )