053 Command line apps

19 Oct 2013

Command line apps provide automation, one time batch processing or even execute an app in the command line. In this episode we will see how we can create such apps in cli using a programming language of our choice such as NodeJS and Ruby. We will then move on to create a full-fledged apps using Commander (with NodeJS) and Thor (with Ruby) to create a skeleton for a started web project.

Download video: mp4

Sample code: Github

Version: Commander 4.1.5, Thor 0.18.1

Similar episodes: 002 Terminal 009 Package Managers, 019 Bash, 044 Node

Background on Command line apps

  1. Commander - Github and examples
  2. Thor - Github
  3. [Shebang](http://en.wikipedia.org/wiki/Shebang_(Unix)) - tells the operating system which program loader to use

Things to learn with shell script

  1. Why CLI?

    • one time batch processing
    • scriptable/automation
    • repeated tasks
    • loads of other packages/libraries
    • pipelining
  2. Some terms:

- **command**: E.g. `git` or `ssh` or `npm`
- **option**: E.g. `git --version` or `node --help`
  1. Steps to creating a simple shell script:

    1. create a file with Shebang and shell commands
    2. change permission
    3. symlink the file to the user path (check bash profile)
  2. create a file called myinfo

    #!/bin/bash
    
    echo $USER
    ls -al
    
  3. check permissions - current user cannot execute it

    $ ls -al myinfo
    -rw-r--r--  1 sayanee  staff  31 Oct 18 07:44 info
    
  4. change permission and check permission again!

    $ chmod u+x myinfo
    $ ls -al info
    -rwxr--r--  1 sayanee  staff  31 Oct 18 07:44 info
    
  5. execute myinfo in the same folder and in another folder

    $ myinfo
    sayanee
    total 8
    drwxr-xr-x  3 sayanee  staff  102 Oct 18 07:49 .
    drwxr-xr-x+ 5 sayanee  staff  170 Oct 18 07:21 ..
    -rwxr--r--  1 sayanee  staff   31 Oct 18 07:44 myinfo
    
    $ cd ~
    $ myinfo
    zsh: command not found: myinfo
    
  6. check your .bashrc or .zshrc to create a path or choose an existing path

    export PATH=/Users/sayanee/Sites/scripts/:$PATH
    
  7. create a symlink

    $ ln -s /path/to/original/filename /path/to/all/scripts/filename
    $ ln -s /Users/sayanee/Desktop/command-line-apps/myinfo /Users/sayanee/Sites/scripts/myinfo
    

Things to learn with Thor

  1. Install Thor

    $ gem install thor
    $ thor version
    Thor 0.18.1
    $ thor help
    
  2. create a file app with the following contents:

    #!/usr/bin/env ruby
    
    require "thor"
    
    class App < Thor
    
      desc "start", "start a project"
      def start
        puts "Started !"
      end
    end
    
    App.start ARGV
    

    remember to change permission and symlink to the path in your profile

    $ chmod u+x app
    $ ln -s /path/to/script/app /path/to/all/scripts/app
    

    execute the following in the command line:

    $ app
    $ app help
    $ app start
    
  3. passing an argument

    ...
    desc "start NAME", "start a project with a NAME"
    def start name
      puts "Started #{name}!"
    end
    ...
    

    execute the following in the command line:

    $ app start travel
    $ app start todolist
    
  4. optional arguments

    desc "start NAME", "start a project with a NAME"
    def start(name, author=nil)
        puts "Started #{name}!"
        puts "by #{author}" if author
    end
    
  5. putting in another argument

    def start(name, deadline, author=nil)
        puts "Started #{name}!"
        puts "authored by #{author}" if author
        puts "to be completed by #{deadline}"
    end
    
  6. passing as an option

    desc "start NAME", "start a project with a NAME"
    option :author
    def start(name, deadline)
      puts "Started #{name}!"
      puts "authored by #{options[:author]}" if options[:author]
      puts "to be completed by #{deadline}"
    end
    

    execute it in the command line

    app start todolist october --author "sayanee"
    
  7. passing the type for option

    desc "start NAME", "start a project with a NAME"
    option :author
    option :multiuser, :type => :boolean
    def start(name, deadline, author=nil)
        puts "Started #{name}!"
        puts "authored by #{options[:author]}" if options[:author]
        puts "to be completed by #{deadline}"
        puts "It will be multiuser!" if options[:multiuser]
    end
    

    execute it in the command line

    $ app start todolist october --author "sayanee" --multiuser false
    $ app start todolist october --author "sayanee" --multiuser true
    

Things to learn with CommanderJS

  1. Install Commander

    $ npm install -g commander
    $ commander --version
    $ commander --help
    
  2. create a new file called start, change permission and symlink it

    $ touch start
    $ ls -al start
    -rw-r--r--  1 sayanee  staff  69 Oct 18 08:06 start
    $ chmod u+x start
    $ ls -al start
    -rwxr--r--  1 sayanee  staff  69 Oct 18 08:06 start
    
  3. start file contents

    #!/usr/bin/env node
    
    console.log('Hello World in Command line ;-)');
    
  4. execute start in the command line!

    $ start
    Hello World in Command line ;-)
    
  5. add module dependencies - if npm install -g and NODE_PATH is set in the path

  6. adding version and help - file contents of start now:

    #!/usr/bin/env node
    
    var program = require('commander');
    
    program
      .version('0.0.1')
      .parse(process.argv);
    
    console.log('Hello World in Command line ;-)');
    
  7. passing in options

    ...
    
    program
      ...
      .option('-t, --html <file>', 'Add HTML file')
      .option('-c, --css <file>', 'Add CSS file')
      ...
    
    console.log('Created html file: ' + program.html);
    console.log('Created css file: ' + program.css);
    

    execute in the command line

    $ start --html index.html --css style.css
    
  8. checking options

    if (process.argv.length < 3) program.help();
    

    execute in the command line

    $ start
    $ start --html index.html --css style.css
    
  9. default options

    ...
    .option('-t, --html <file>', 'Add HTML file', 'index.html')
    ...
    if (!program.css) program.help();
    ...
    
  10. add subcommand serve, try start --help in the command line again

    ...
    
    program
      .command('serve')
      .description('Open the project in the browser')
      .action(function(){
        console.log('opened!');
      });
    
    program.parse(process.argv);
    
    if (program.css) {
      console.log('Created html file: ' + program.html);
      console.log('Created css file: ' + program.css);
    }
    
  11. check for number of arguments/ options and display the default help

    if (process.argv.length < 3) program.help();
    
  12. add command line executions with child_process - do note that error checking should be done in real projects!

    ...
    var exec = require('child_process').exec;
    ...
    program
      .command('serve')
      ...
      .action(function(){
        exec('open http://build-podcast.com');
        console.log('opened!');
      });
    ...
    
  13. create template folder and files

    └── template
        ├── _css
        └── _html
    

    contents of _html:

    <!doctype html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
      <link rel="stylesheet" href="">
      <title>My new webproject</title>
    </head>
    <body>
    
    <h1>Hi there :-)</h1>
    <h2>A new web project is created!</h2>
    
    </body>
    </html>
    

    contents of _css:

    body {
      background-color: cornflowerblue;
    }
    
  14. copy from template and create new project files

    ...
    var fs = require('fs');
    …
    program
      .command('serve')
      .description('Open the project in the browser')
      .action(function(){
        exec('python -m SimpleHTTPServer 8000');
        exec('open http://localhost:8000')
        console.log('View the new web project at http://localhost:8000');
      });
    ...
    
    var templatePath = '~/Desktop/command-line-apps/template/';
    
    if (program.css) {
      exec('cp ' + templatePath + '_css ' + program.css + ' && cp ' + templatePath + '/_html ' + program.html,
        function(error, stdout, stderr) {
          if (error) { return console.log('Error: ' + error); }
    
          fs.readFile(program.html, 'utf8', function(err, data) {
            if (err) { return console.log('Read file error: ' + err); }
    
            var newData = '<link rel="stylesheet" href="' + program.css + '">';
            data = data.replace(//g, newData);
    
            fs.writeFile(program.html, data, 'utf8', function(error) {
              if (error) { return console.log('Write file error: ' + err); }
              console.log('Created html file: ' + program.html);
              console.log('Created css file: ' + program.css);
            });
    
          });
      });
    
    }
    
  15. adding colors to the command line. ensure you already did a global install with npm install -g cli-color

    var clc = require('cli-color');
    …
    console.log('Created ' + program.html);
    console.log('Created ' + program.css);
    console.log(clc.blue(program.html + ' and ' + program.css + ' are created!'));
    

More Resources on Command line apps

  1. Tuts+ course on Command Line Apps in Ruby
  2. PragProg book on Command Line Apps in Ruby and the author's talk
  3. Slides on CLI Ruby
  4. Writing CLI apps in Node by Liang Zan
  5. CLI Tool in Node
  6. in Ruby - GLI, rainbow, gem-man, ronn
  7. Node CLI app - part 1 and part 2
  8. Building your tools with Thor
  9. Examples of projects using Commander: Jade,

Build Link of this episode

Web Tools Weekly