Placing Things into Other Things


Lone Star Ruby Conf


@cowboyd

The FrontSide

July 18, 2013

A confession

I'm not going to talk about placing things into other things

"New Shit has come to light"

-- Jeffrey Lebowski

Instead, I'm going to talk to you about the CLI

(comand line interface)

Yes. CLI.

The CLI is the Developer UX

To all the gems I've loved before

Slop, Trollop, Mixlib, Map, Optitron, Rake, Thor, GLI, and even good old OptionParser

When it's good, it's good

And then comes the hangover

What went wrong?

Output

I have given a name to to my pain

and it is puts()

and ui.info()

and shell.say()

how do you share calls to puts()?

How do you share code that formats text?

Example: Help


$ jenkins -h nodes
usage: jenkins nodes

    --host [HOST]          	  connect to jenkins server on this host
    --port [PORT]          	  connect to jenkins server on this port
    --ssl                  	  connect to jenkins server with ssl
    --username [USERNAME]  	  connect to jenkins server with username
    --password [PASSWORD]  	  connect to jenkins server with password
                             

layout?


class CLI < Thor
  module Formatting
    module ClassMethods
      def task_help(shell, task_name)
        meth = normalize_task_name(task_name)
        task = all_tasks[meth]
        handle_no_task_error(meth) unless task
      
        shell.say "usage: #{banner(task)}"
        shell.say
        class_options_help(shell, nil => task.options.map { |_, o| o })
      end
      ...
    end
  end
end
                            

Embed streams into other streams

  • with an indentation prefix
  • with control characters

Capistrano is beautiful... Until it's ugly


** [out :: 198.61.173.225] .
** [out :: 198.61.173.225] .
** [out :: 198.61.173.225] .
** [out :: 198.61.173.225] Using rake (10.1.0)
** [out :: 198.61.173.225]
** [out :: 198.61.173.225] Using i18n (0.6.1)
** [out :: 198.61.173.225]
** [out :: 198.61.173.225] Using multi_json (1.7.7)
** [out :: 198.61.173.225]
** [out :: 198.61.173.225] Using activesupport (3.2.13)
** [out :: 198.61.173.225]
** [out :: 198.61.173.225] Using builder (3.0.4)
                            

Do progress bars need to be hard?


Fetching: rumm-0.0.18.gem (100%)
Successfully installed rumm-0.0.18

What went wrong?

Decoding and Validation

Strings are the squishy goo of the software

We don't want goo, we want objects

'home/cowboyd' => #<Pathname:/home/cowboyd>

decoder: Pathname()

'198.61.173.225' =>#<IPAddr: IPv4:198.61.173.225>

decoder: IPAddr.new()

'cowboyd@thefrontside.net' => #<User id: 1>

decoder: User.find_by_email()

Well formed objects aren't enough

They need to be valid in the bounded context in which they are being used

Decoding and Validation are separate concerns

Chomsky's classic example

Colorless green dreams sleep furiously

this is well-formed nonsense

Objects can be well formed nonsense too

Well formed. But valid?

#<Pathname:/home/cowboyd>

  • .exist?
  • .owned?

Current tools are scant help with Decode/Validate

As a community, we have a horrible bias towards the output side of things to the neglect of the input

smart data == smart programs

No Plugin Architecure

Only so much program can fit into a single program

every sufficiently complex CLI will need this

  • think rubygems, vagrant, heroku

every sufficiently complex program will need this

  • Rails Engines
  • Jenkins Plugins
  • Textmate Bundles

Ok, time for plugins. What do I need?

  • discovery (search)
  • download, verify and install
  • register/activate at runtime
  • configure
  • uninstall

UUUUUUUHHHUUUUUUUUGGGGGGG

I could go on and on whinging...

Suffice it to say

OptParser::DSL != App Framework

Suffice it to say

Option parsing is a solved problem. It's the other 98% of the application that sucks

Fast Forward to 6 weeks ago

All Purpose CLI for Rackspace

(contains every gotcha and then some)

Big API, large surface area

Plugins required

What to do?

Attack!

Output

Contextualize the streams!

Step 1. Middlewares

Middlewares?

Yes. Middlewares.

What's in the middle?


class Command
  attr_reader :argv, :input, :output, :log, :env

  def initialize(argv, input, output, log, env)
    @argv, @input, @output, @log, @env = argv, input, output, log, env
  end
end

A Command

The API


def call(command)
  # do stuff before
  yield command #invoke next middleware
  # do stuff after
end

Each middleware controls all downstream streams

Emoji!


def call(cmd)
  yield Command.new(cmd.argv, cmd.input, EmojiFilter.new(cmd.output), cmd.env)
end

Step 2. Templates

Templates?

Yes. Templates

Achieve re-use with layouts and partials

Help Redux


class CLI < Thor
  module Formatting
    module ClassMethods
      def task_help(shell, task_name)
        meth = normalize_task_name(task_name)
        task = all_tasks[meth]
        handle_no_task_error(meth) unless task
      
        shell.say "usage: #{banner(task)}"
        shell.say
        class_options_help(shell, nil => task.options.map { |_, o| o })
      end
      ...
    end
  end
end
                            

You decide


usage: <%= banner task %>

<% indent 4 do %>
  <%= render 'options' %>
<% end %>

Templates are for streaming!

  • templates further contextualize output
  • streaming components

Progress bars are for templates!


Fetching: <%= gem.filename %> <%= gem.progress %>
Successfully installed <%= gem.name %>

Templates: Use 'em!

Decode / Validate

One word: Forms

Yes. forms.

Platonic Forms, not Web Forms

What is the essence of your input?

Create Loadbalancer

app/forms/loadbalancers/create_form.rb

class Loadbalancers::CreateForm < MVCLI::Form
  input :name, String, default: -> { naming.generate_name 'l', 'b' }

  input :port, Integer, default: 80, decode: ->(s) { Integer s }

  input :protocol, String, default: 'HTTP', decode: :upcase

  validates(:port) {|port| port >=0 && port < 65535 }
  validates(:protocol) {|p| ['HTTP','HTTPS','TCP', 'UDP'].member? p }
end

deriving an option parser for this is left as an exercise for the reader

What's going on here?

  • templates
  • validations
  • middlewares
  • Rack-Like API

Are we re-inventing Rails?

Kinda. Yeah.

and that's awesome.

Co-Anatomy

  • stdin => request.body
  • stdout => response.body
  • argv => request.uri
  • exit_status => response.status_code
  • ENV => request.headers

Convergent Design

Rumm routing

app/routes.rb

macro /(-v|--version)/ => "version"
match 'version' => proc {|cmd| cmd.output << "#{Rumm::VERSION}\n" }

match 'login' => 'authentication#login'
match 'logout' => 'authentication#logout'

resources :servers do
  resources :attachments
end
resources :loadbalancers do
  resources :nodes
end
resources :dbinstances do
  resources :users
  resources :databases
end
resources :containers do
  resources :files
end
resources :volumes

describes about 50 commands!

MVCLI

https://github.com/cowboyd/mvcli

Feels so good man

  • templating
  • routing
  • middlewares
  • models/controllers

Because a command is a process, not an object

Thank You!