• Save
Crafting Beautiful CLI Applications in Ruby
Upcoming SlideShare
Loading in...5
×
 

Crafting Beautiful CLI Applications in Ruby

on

  • 7,748 views

Shishir Das (@shishirdas) and Nikhil Mungel (@hyfather) presented this at RubyConf India 2012.

Shishir Das (@shishirdas) and Nikhil Mungel (@hyfather) presented this at RubyConf India 2012.

Statistics

Views

Total Views
7,748
Views on SlideShare
7,626
Embed Views
122

Actions

Likes
13
Downloads
0
Comments
0

6 Embeds 122

http://rubylive.fr 92
http://news.humancoders.com 10
http://www.linkedin.com 8
https://twitter.com 8
http://www.hanrss.com 2
https://www.linkedin.com 2

Accessibility

Categories

Upload Details

Uploaded via as Apple Keynote

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • task oriented v/s feature oriented\n(ssh is feature / rails is tasks)\n\nOther Examples -- knife & cucumber\nTODO: SSH\n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • bundle is a bad example\n\nrails/git/knife are good examples\n\n
  • \n
  • \n
  • all commands should be reversible, those that are not should be called out.\n\nSometimes irreversibility is unavoidable >> rm -rf --no-preserve-root /\n\n
  • make an example for rails generate /destroy\n
  • make an example for rails generate /destroy\n
  • make an example for rails generate /destroy\n
  • make an example for rails generate /destroy\n
  • Assume defaults wherever you can.\nIn case of risky operations don’t assume anything.\n\nE. g. -- package managers assume latest version\n
  • \n
  • \n
  • \n
  • git add\n> did you mean `git add .’ ?\n
  • \n
  • \n
  • vi modes\nbehavior should not depend on hidden state\n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • git is a very mature cli. no piping or redirection normally used. but it honors everything.\nwrite errors to stderr\n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • knife, rails, gem, bundle\n
  • \n
  • Input - command line -> Option Parsing\n\nOutput - status code, stdout, stderr\n
  • Input - command line -> Option Parsing\n\nOutput - status code, stdout, stderr\n
  • Input - command line -> Option Parsing\n\nOutput - status code, stdout, stderr\n
  • Input - command line -> Option Parsing\n\nOutput - status code, stdout, stderr\n
  • Input - command line -> Option Parsing\n\nOutput - status code, stdout, stderr\n
  • Input - command line -> Option Parsing\n\nOutput - status code, stdout, stderr\n
  • Input - command line -> Option Parsing\n\nOutput - status code, stdout, stderr\n
  • Input - command line -> Option Parsing\n\nOutput - status code, stdout, stderr\n
  • Input - command line -> Option Parsing\n\nOutput - status code, stdout, stderr\n
  • Input - command line -> Option Parsing\n\nOutput - status code, stdout, stderr\n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • Enforces OO\nAutomatic rake style tasks\n
  • Enforces OO\nAutomatic rake style tasks\n
  • Enforces OO\nAutomatic rake style tasks\n
  • Convention -- \nfirst arg to the description becomes the input \nsecond arg is the banner\n
  • Convention -- \nfirst arg to the description becomes the input \nsecond arg is the banner\n
  • Convention -- \nfirst arg to the description becomes the input \nsecond arg is the banner\n
  • Option Parsing\nInbuilt support for help and banners\nSupports standard invocations of help on the shell\n
  • Option Parsing\nInbuilt support for help and banners\nSupports standard invocations of help on the shell\n
  • \n
  • \n
  • \n
  • \n
  • Input - mostly handled by the third party library.\nTesting that would be testing the gem. Not a good idea.\n\n
  • Input - mostly handled by the third party library.\nTesting that would be testing the gem. Not a good idea.\n\n
  • Standard Ruby classes. Libraries like test/unit, rspec.\nMocking and proxy layers for 3rd party services etc.\nLike any other app.\n\n
  • \n
  • Mocking\n  mocha works out well. \n  For filesystem, MockFS lets you mock the entire file system. \n\nTesting CLI apps that manipulate filesystem. Mocking is good. But if we mock every call to FileUtils, test becomes very tightly coupled. So even if behaviour doesn't change but the command changes the test breaks.\n - one use FakeFS\n\n
  • Isolate environment of its own.\nIf its is cheap and scriptable to spin up the environment. Then we can have behaviour testing \n  - powerful machines.\n  - strong virtualizations (inbuilt)\nvagrant, lxc, openvz\n
  • 2 distinct sections -- your and subprocesses\n\nbackticks and system ruby calls not versatile. Doesn’t give you full control over the Input/Output/Error stream\nMixLib::Shellout and POpen3 are better alternatives.\nRespect exit status 0 -for success, rest all failures while writing your CLI.\nYour CLI should write error to stderr and not stdout. ruby provides $stdout, $stdin, $stderr\n\n
  • \n
  • \n
  • \n
  • Compatible with windows.\nUses the select(2) system call. \nGives abstractions over umask, cwd etc.\n
  • Compatible with windows.\nUses the select(2) system call. \nGives abstractions over umask, cwd etc.\n
  • Drop in plugins eg. vagrant, knife\nPick up all .rb files from a predetermined location.\n\nYour CLI app can then be easily extended\nWhen writing an app, you don’t know all the possible use cases\n\n
  • Provide hooks where arbitrary code can be run on failure and success of steps.\nEnsure that you support all executable files and not only .rb files.\n\nCan be filename based (git) or configurable.\n
  • STDOUT could be the default. But should be configurable to a file. \n\nLog at correct level. Apply to all apps but worth mentioning.\n\nyou may want to support -v and -vv for falling back to :info or :debug and -q falls back fatal.\n
  • Give URL in sliduments\n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n

Crafting Beautiful CLI Applications in Ruby Crafting Beautiful CLI Applications in Ruby Presentation Transcript

  • RubyConf India 2012Pune Crafting Beautiful Command Line Applications using Ruby Shishir Nikhil @shishirdas @hyfather
  • Shishir Das@shishirdasThoughtWorks
  • Nikhil Mungel@hyfatherThoughtWorks
  • Why the command line?
  • Why the command line?
  • eyheroku knife puppet pg_ctl mysql rails git
  • bash zsh powershell eyheroku knife puppet pg_ctl mysql rails git
  • eyheroku knife puppet pg_ctl mysql rails git Developers and QAs
  • Lightweight
  • Lightweight
  • Scriptable
  • Consistent UX
  • Consistent UX *
  • What makes a CLI app good?
  • The Luxury of Ignorance
  • ~ > rails
  • ~ > railsUsage: rails COMMAND [ARGS]The most common rails commands are: generate Generate new code (short-cut alias: "g") console Start the Rails console (short-cut alias: "c") server Start the Rails server (short-cut alias: "s")~ >
  • ~ > rails generate
  • ~ > rails generateUsage: rails generate GENERATOR [args] [options]Please choose a generator below.Rails: assets controller generator helper integration_test mailer migration model observer performance_test plugin~ >
  • ~ > ssh
  • ~ > sshusage: ssh [-1246AaCfgKkMNnqsTtVvXxYy] [-b bind_address] [-ccipher_spec] [-D [bind_address:]port] [-F configfile] [-I pkcs11] [-i identity_file] [-L [bind_address:]port:host:hostport] [-l login_name] [-O ctl_cmd] [-o option] [-p port] [-R [bind_address:]port:host:hostport] [-S ctl_path] [-W host:port] [-w local_tun[:remote_tun]] [user@]hostname [command]~ >
  • Least Astonishment
  • ~ > bundle
  • ~ > bundleFetching source index for https://rubygems.org/Using rake (0.9.2.2)Installing i18n (0.6.0)Installing multi_json (1.0.4)Installing activesupport (3.2.1)Installing builder (3.0.0)Installing activemodel (3.2.1)Installing erubis (2.7.0)~ >
  • Reversibility
  • ~ > git commit -am "Added a new framework"
  • ~ > git commit -am "Added a new framework"[master b4a2130] Added a new framework 2 files changed, 1012 insertions(+), 529 deletions(-)~ >
  • ~ > git commit -am "Added a new framework"[master b4a2130] Added a new framework 2 files changed, 1012 insertions(+), 529 deletions(-)~ > git reset HEAD^
  • ~ > git commit -am "Added a new framework"[master b4a2130] Added a new framework 2 files changed, 1012 insertions(+), 529 deletions(-)~ > git reset HEAD^Unstaged changes after reset:M javascript/framework.jsM javascripts/support.js~ >
  • Config Files
  • ~ > knife cookbook upload apache2 -o /User/foo/cookbooks --server-url http://chef-server:4000 --key /etc/chef/my.key --color~ >
  • ~ > knife cookbook upload apache2 -o /User/foo/cookbooks --server-url http://chef-server:4000 --key /etc/chef/my.key --color~ >~ > cat knife.rb log_level                :infolog_location             STDOUTnode_name                blitzclient_key               /Users/shishir/.chef/blitz.pemvalidation_client_name   chef-validatorvalidation_key           /etc/chef/validation.pemchef_server_url          http://10.10.100.202:4000cache_type               BasicFilecache_options( :path => /Users/shishir/.chef/checksums )~ >
  • ~ > knife cookbook upload apache2 -o /User/foo/cookbooks --server-url http://chef-server:4000 --key /etc/chef/my.key --color~ >~ > cat knife.rb log_level                :infolog_location             STDOUTnode_name                blitzclient_key               /Users/shishir/.chef/blitz.pemvalidation_client_name   chef-validatorvalidation_key           /etc/chef/validation.pemchef_server_url          http://10.10.100.202:4000cache_type               BasicFilecache_options( :path => /Users/shishir/.chef/checksums )~ >~ > knife cookbook upload apache2
  • Graceful Failure
  • ~ > git pull
  • ~ > git pullYou asked me to pull without telling me which branch youwant to merge with, and branch.master.merge inyour configuration file does not tell me, either. Pleasespecify which branch you want to use on the command line andtry again (e.g. git pull <repository> <refspec>).See git-pull(1) for details.If you often merge with the same branch, you may want touse something like the following in your configuration file: [branch "master"] remote = <nickname> merge = <remote-ref> [remote "<nickname>"] url = <url> fetch = <refspec>See git-config(1) for details.
  • No hidden states
  • Confirmations should be scriptable
  • ~ > gem uninstall rspec
  • ~ > gem uninstall rspecYou have requested to uninstall the gem: rspec-2.8.0cucumber-1.1.4 depends on [rspec (>= 2.7.0)]If you remove this gems, one or more dependencies will not be met.Continue with Uninstall? [Yn] n
  • ~ > gem uninstall rspecYou have requested to uninstall the gem: rspec-2.8.0cucumber-1.1.4 depends on [rspec (>= 2.7.0)]If you remove this gems, one or more dependencies will not be met.Continue with Uninstall? [Yn] n~ > gem uninstall -I rspec
  • ~ > gem uninstall rspecYou have requested to uninstall the gem: rspec-2.8.0cucumber-1.1.4 depends on [rspec (>= 2.7.0)]If you remove this gems, one or more dependencies will not be met.Continue with Uninstall? [Yn] n~ > gem uninstall -I rspecSuccessfully uninstalled rspec-2.8.0
  • Honor Piping
  • IO#tty?
  • Why Ruby?
  • Scripting Language
  • Easy textmanipulation
  • Good Abstractions
  • Plethora of gems to help you
  • Examples to learn from
  • Structure of CLI apps
  • Input Execution Output
  • Input Execution Output STDIO = UI/UX
  • Input
  • Input STDIN
  • Input ARGV/ENV STDIN
  • Input ARGV/ENV STDIN
  • Input ARGV/ENV STDIN
  • Input OptionParser ARGV/ENV STDIN
  • Input OptionParser ARGV/ENV STDIN
  • Input Libraries OptionParser ARGV/ENV STDIN
  • options = {}OptionParser.new do |opts| opts.banner = "Usage: example.rb [options]" opts.on("-v", "--[no-]verbose", "Run verbosely") do |v| options[:verbose] = v endend.parse!
  • options = {}OptionParser.new do |opts| opts.banner = "Usage: example.rb [options]" opts.on("-v", "--[no-]verbose", "Run verbosely") do |v| options[:verbose] = v endend.parse!~ > ./opt.rb --helpUsage: example.rb [options]   -v, --[no-]verbose               Run verbosely
  • options = {}OptionParser.new do |opts| opts.banner = "Usage: example.rb [options]" opts.on("-v", "--[no-]verbose", "Run verbosely") do |v| options[:verbose] = v endend.parse!~ > ./opt.rb --helpUsage: example.rb [options]   -v, --[no-]verbose               Run verbosely
  • options = {}OptionParser.new do |opts| opts.banner = "Usage: example.rb [options]" opts.on("-v", "--[no-]verbose", "Run verbosely") do |v| options[:verbose] = v endend.parse!~ > ./opt.rb --helpUsage: example.rb [options]   -v, --[no-]verbose               Run verbosely
  • options = {}OptionParser.new do |opts| opts.banner = "Usage: example.rb [options]" opts.on("-v", "--[no-]verbose", "Run verbosely") do |v| options[:verbose] = v endend.parse!~ > ./opt.rb --helpUsage: example.rb [options]   -v, --[no-]verbose               Run verbosely
  • The Mixlib Suite
  • class MyCLIApp include Mixlib::CLI option :config_file, :short => "-c CONFIG", :description => "Configuration file", :required => trueend~ > ./mycliapp.rb --helpUsage: ./mix.rb (options) -c, --config CONFIG Configuration file (required) -h, --help Show this message
  • class MyCLIApp include Mixlib::CLI option :config_file, :short => "-c CONFIG", :description => "Configuration file", :required => trueend~ > ./mycliapp.rb --helpUsage: ./mix.rb (options) -c, --config CONFIG Configuration file (required) -h, --help Show this message
  • class MyCLIApp include Mixlib::CLI option :config_file, :short => "-c CONFIG", :description => "Configuration file", :required => trueend~ > ./mycliapp.rb --helpUsage: ./mix.rb (options) -c, --config CONFIG Configuration file (required) -h, --help Show this message
  • config.rbserver_url “http://server.remote”username “elvis”password “hotdog”
  • config.rbserver_url “http://server.remote”username “elvis”password “hotdog”class MyConfig extend(Mixlib::Config) server_url http://server.local username king password burgerendMyConfig.from_file(‘config.rb’)irb> MyConfig.server_url# ‘http://server.remote’
  • config.rbserver_url “http://server.remote”username “elvis”password “hotdog”class MyConfig extend(Mixlib::Config) server_url http://server.local username king password burgerendMyConfig.from_file(‘config.rb’)irb> MyConfig.server_url# ‘http://server.remote’
  • Thor
  • class Test < Thor desc " FILE", "an example task" method_option :delete, :aliases => "-d", :desc => "Delete the file" def example(file) endend
  • class Test < Thor desc " FILE", "an example task" method_option :delete, :aliases => "-d", :desc => "Delete the file" def example(file) endend~ > myapp help test:exampleUsage: thor test:example FILEOptions: -d, [--delete=DELETE] # Delete the file after parsing itan example task
  • class Test < Thor desc " FILE", "an example task" method_option :delete, :aliases => "-d", :desc => "Delete the file" def example(file) endend~ > myapp help test:exampleUsage: thor test:example FILEOptions: -d, [--delete=DELETE] # Delete the file after parsing itan example task
  • class Test < Thor desc " FILE", "an example task" method_option :delete, :aliases => "-d", :desc => "Delete the file" def example(file) endend~ > myapp help test:exampleUsage: thor test:example FILEOptions: -d, [--delete=DELETE] # Delete the file after parsing itan example task
  • class Test < Thor desc " FILE", "an example task" method_option :delete, :aliases => "-d", :desc => "Delete the file" def example(file) endend~ > myapp help test:exampleUsage: thor test:example FILEOptions: -d, [--delete=DELETE] # Delete the file after parsing itan example task
  • class Test < Thor desc " FILE", "an example task" method_option :delete, :aliases => "-d", :desc => "Delete the file" def example(file) endend~ > myapp help test:exampleUsage: thor test:example FILEOptions: -d, [--delete=DELETE] # Delete the file after parsing itan example task
  • class Test < Thor desc " FILE", "an example task" method_option :delete, :aliases => "-d", :desc => "Delete the file" def example(file) endend~ > myapp help test:exampleUsage: thor test:example FILEOptions: -d, [--delete=DELETE] # Delete the file after parsing itan example task
  • class Test < Thor desc " FILE", "an example task" method_option :delete, :aliases => "-d", :desc => "Delete the file" def example(file) endend~ > myapp help test:exampleUsage: thor test:example FILEOptions: -d, [--delete=DELETE] # Delete the file after parsing itan example task
  • Testing
  • Input Execution Output
  • Input Execution Output
  • Input Execution OutputMostly third party libraries
  • Input Execution OutputTest::Unit, rspec etc.
  • Input Execution Output System
  • SystemFile System Network Process
  • Isolated Environments ContainerUser Application File Memory Processes System
  • Streams & Signals
  • STDOUTSTDIN App STDERR
  • STDOUTSTDIN App STDERR
  • STDOUTSTDIN App STDERR STDOUT & STDERR STDIN App
  • Mixlib::Shellout> ls = Mixlib::ShellOut.new("ls")> ls.run_command> ls.stdout“init.elnREADME.mdn”
  • Mixlib::Shellout> ls = Mixlib::ShellOut.new("ls")> ls.run_command> ls.stdout“init.elnREADME.mdn”
  • Plugin Architecture
  • Hooks
  • Logging
  • GNU CLI standards‘--version’ and ‘--help’Input/Output Files -- -O for Output Files
  • “CLI apps could bethe first consumers ” of your services.
  • “CLI apps could bethe first consumers ” of your services.
  • “CLI apps could bethe first consumers ” of your services. Testing Development Cheap No rich UI, functional Fast tests
  • Ideas CLI app for a REST API: Github SCM Plugins for Knife Transform Rake scripts into first class CLI appsShishir Nikhil@shishirdas @hyfather
  • Questions Comments SuggestionsShishir Nikhil@shishirdas @hyfather