Successfully reported this slideshow.
Your SlideShare is downloading. ×

Crafting Beautiful CLI Applications in Ruby

More Related Content

Related Books

Free with a 30 day trial from Scribd

See all

Related Audiobooks

Free with a 30 day trial from Scribd

See all

Crafting Beautiful CLI Applications in Ruby

  1. 1. RubyConf India 2012 Pune Crafting Beautiful Command Line Applications using Ruby Shishir Nikhil @shishirdas @hyfather
  2. 2. Shishir Das @shishirdas ThoughtWorks
  3. 3. Nikhil Mungel @hyfather ThoughtWorks
  4. 4. Why the command line?
  5. 5. Why the command line?
  6. 6. ey heroku knife puppet pg_ctl mysql rails git
  7. 7. bash zsh powershell ey heroku knife puppet pg_ctl mysql rails git
  8. 8. ey heroku knife puppet pg_ctl mysql rails git Developers and QAs
  9. 9. Lightweight
  10. 10. Lightweight
  11. 11. Scriptable
  12. 12. Consistent UX
  13. 13. Consistent UX *
  14. 14. What makes a CLI app good?
  15. 15. The Luxury of Ignorance
  16. 16. ~ > rails
  17. 17. ~ > rails Usage: 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") ~ >
  18. 18. ~ > rails generate
  19. 19. ~ > rails generate Usage: rails generate GENERATOR [args] [options] Please choose a generator below. Rails: assets controller generator helper integration_test mailer migration model observer performance_test plugin ~ >
  20. 20. ~ > ssh
  21. 21. ~ > ssh usage: ssh [-1246AaCfgKkMNnqsTtVvXxYy] [-b bind_address] [-c cipher_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] ~ >
  22. 22. Least Astonishment
  23. 23. ~ > bundle
  24. 24. ~ > bundle Fetching 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) ~ >
  25. 25. Reversibility
  26. 26. ~ > git commit -am "Added a new framework"
  27. 27. ~ > git commit -am "Added a new framework" [master b4a2130] Added a new framework 2 files changed, 1012 insertions(+), 529 deletions(-) ~ >
  28. 28. ~ > git commit -am "Added a new framework" [master b4a2130] Added a new framework 2 files changed, 1012 insertions(+), 529 deletions(-) ~ > git reset HEAD^
  29. 29. ~ > 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.js M javascripts/support.js ~ >
  30. 30. Config Files
  31. 31. ~ > knife cookbook upload apache2 -o /User/foo/cookbooks -- server-url http://chef-server:4000 --key /etc/chef/my.key --color ~ >
  32. 32. ~ > 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                :info log_location             STDOUT node_name                'blitz' client_key               '/Users/shishir/.chef/blitz.pem' validation_client_name   'chef-validator' validation_key           '/etc/chef/validation.pem' chef_server_url          'http://10.10.100.202:4000' cache_type               'BasicFile' cache_options( :path => '/Users/shishir/.chef/checksums' ) ~ >
  33. 33. ~ > 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                :info log_location             STDOUT node_name                'blitz' client_key               '/Users/shishir/.chef/blitz.pem' validation_client_name   'chef-validator' validation_key           '/etc/chef/validation.pem' chef_server_url          'http://10.10.100.202:4000' cache_type               'BasicFile' cache_options( :path => '/Users/shishir/.chef/checksums' ) ~ > ~ > knife cookbook upload apache2
  34. 34. Graceful Failure
  35. 35. ~ > git pull
  36. 36. ~ > git pull You asked me to pull without telling me which branch you want to merge with, and 'branch.master.merge' in your configuration file does not tell me, either. Please specify which branch you want to use on the command line and try again (e.g. 'git pull <repository> <refspec>'). See git-pull(1) for details. If you often merge with the same branch, you may want to use 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.
  37. 37. No hidden states
  38. 38. Confirmations should be scriptable
  39. 39. ~ > gem uninstall rspec
  40. 40. ~ > gem uninstall rspec You have requested to uninstall the gem: rspec-2.8.0 cucumber-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
  41. 41. ~ > gem uninstall rspec You have requested to uninstall the gem: rspec-2.8.0 cucumber-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
  42. 42. ~ > gem uninstall rspec You have requested to uninstall the gem: rspec-2.8.0 cucumber-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 Successfully uninstalled rspec-2.8.0
  43. 43. Honor Piping
  44. 44. IO#tty?
  45. 45. Why Ruby?
  46. 46. Scripting Language
  47. 47. Easy text manipulation
  48. 48. Good Abstractions
  49. 49. Plethora of gems to help you
  50. 50. Examples to learn from
  51. 51. Structure of CLI apps
  52. 52. Input Execution Output
  53. 53. Input Execution Output STDIO = UI/UX
  54. 54. Input
  55. 55. Input STDIN
  56. 56. Input ARGV/ENV STDIN
  57. 57. Input ARGV/ENV STDIN
  58. 58. Input ARGV/ENV STDIN
  59. 59. Input OptionParser ARGV/ENV STDIN
  60. 60. Input OptionParser ARGV/ENV STDIN
  61. 61. Input Libraries OptionParser ARGV/ENV STDIN
  62. 62. options = {} OptionParser.new do |opts| opts.banner = "Usage: example.rb [options]" opts.on("-v", "--[no-]verbose", "Run verbosely") do |v| options[:verbose] = v end end.parse!
  63. 63. options = {} OptionParser.new do |opts| opts.banner = "Usage: example.rb [options]" opts.on("-v", "--[no-]verbose", "Run verbosely") do |v| options[:verbose] = v end end.parse! ~ > ./opt.rb --help Usage: example.rb [options]    -v, --[no-]verbose               Run verbosely
  64. 64. options = {} OptionParser.new do |opts| opts.banner = "Usage: example.rb [options]" opts.on("-v", "--[no-]verbose", "Run verbosely") do |v| options[:verbose] = v end end.parse! ~ > ./opt.rb --help Usage: example.rb [options]    -v, --[no-]verbose               Run verbosely
  65. 65. options = {} OptionParser.new do |opts| opts.banner = "Usage: example.rb [options]" opts.on("-v", "--[no-]verbose", "Run verbosely") do |v| options[:verbose] = v end end.parse! ~ > ./opt.rb --help Usage: example.rb [options]    -v, --[no-]verbose               Run verbosely
  66. 66. options = {} OptionParser.new do |opts| opts.banner = "Usage: example.rb [options]" opts.on("-v", "--[no-]verbose", "Run verbosely") do |v| options[:verbose] = v end end.parse! ~ > ./opt.rb --help Usage: example.rb [options]    -v, --[no-]verbose               Run verbosely
  67. 67. The Mixlib Suite
  68. 68. class MyCLIApp include Mixlib::CLI option :config_file, :short => "-c CONFIG", :description => "Configuration file", :required => true end ~ > ./mycliapp.rb --help Usage: ./mix.rb (options) -c, --config CONFIG Configuration file (required) -h, --help Show this message
  69. 69. class MyCLIApp include Mixlib::CLI option :config_file, :short => "-c CONFIG", :description => "Configuration file", :required => true end ~ > ./mycliapp.rb --help Usage: ./mix.rb (options) -c, --config CONFIG Configuration file (required) -h, --help Show this message
  70. 70. class MyCLIApp include Mixlib::CLI option :config_file, :short => "-c CONFIG", :description => "Configuration file", :required => true end ~ > ./mycliapp.rb --help Usage: ./mix.rb (options) -c, --config CONFIG Configuration file (required) -h, --help Show this message
  71. 71. config.rb server_url “http://server.remote” username “elvis” password “hotdog”
  72. 72. config.rb server_url “http://server.remote” username “elvis” password “hotdog” class MyConfig extend(Mixlib::Config) server_url 'http://server.local' username 'king' password 'burger' end MyConfig.from_file(‘config.rb’) irb> MyConfig.server_url # ‘http://server.remote’
  73. 73. config.rb server_url “http://server.remote” username “elvis” password “hotdog” class MyConfig extend(Mixlib::Config) server_url 'http://server.local' username 'king' password 'burger' end MyConfig.from_file(‘config.rb’) irb> MyConfig.server_url # ‘http://server.remote’
  74. 74. Thor
  75. 75. class Test < Thor desc " FILE", "an example task" method_option :delete, :aliases => "-d", :desc => "Delete the file" def example(file) end end
  76. 76. class Test < Thor desc " FILE", "an example task" method_option :delete, :aliases => "-d", :desc => "Delete the file" def example(file) end end ~ > myapp help test:example Usage: thor test:example FILE Options: -d, [--delete=DELETE] # Delete the file after parsing it an example task
  77. 77. class Test < Thor desc " FILE", "an example task" method_option :delete, :aliases => "-d", :desc => "Delete the file" def example(file) end end ~ > myapp help test:example Usage: thor test:example FILE Options: -d, [--delete=DELETE] # Delete the file after parsing it an example task
  78. 78. class Test < Thor desc " FILE", "an example task" method_option :delete, :aliases => "-d", :desc => "Delete the file" def example(file) end end ~ > myapp help test:example Usage: thor test:example FILE Options: -d, [--delete=DELETE] # Delete the file after parsing it an example task
  79. 79. class Test < Thor desc " FILE", "an example task" method_option :delete, :aliases => "-d", :desc => "Delete the file" def example(file) end end ~ > myapp help test:example Usage: thor test:example FILE Options: -d, [--delete=DELETE] # Delete the file after parsing it an example task
  80. 80. class Test < Thor desc " FILE", "an example task" method_option :delete, :aliases => "-d", :desc => "Delete the file" def example(file) end end ~ > myapp help test:example Usage: thor test:example FILE Options: -d, [--delete=DELETE] # Delete the file after parsing it an example task
  81. 81. class Test < Thor desc " FILE", "an example task" method_option :delete, :aliases => "-d", :desc => "Delete the file" def example(file) end end ~ > myapp help test:example Usage: thor test:example FILE Options: -d, [--delete=DELETE] # Delete the file after parsing it an example task
  82. 82. class Test < Thor desc " FILE", "an example task" method_option :delete, :aliases => "-d", :desc => "Delete the file" def example(file) end end ~ > myapp help test:example Usage: thor test:example FILE Options: -d, [--delete=DELETE] # Delete the file after parsing it an example task
  83. 83. Testing
  84. 84. Input Execution Output
  85. 85. Input Execution Output
  86. 86. Input Execution Output Mostly third party libraries
  87. 87. Input Execution Output Test::Unit, rspec etc.
  88. 88. Input Execution Output System
  89. 89. System File System Network Process
  90. 90. Isolated Environments Container User Application File Memory Processes System
  91. 91. Streams & Signals
  92. 92. STDOUT STDIN App STDERR
  93. 93. STDOUT STDIN App STDERR
  94. 94. STDOUT STDIN App STDERR STDOUT & STDERR STDIN App
  95. 95. Mixlib::Shellout > ls = Mixlib::ShellOut.new("ls") > ls.run_command > ls.stdout “init.elnREADME.mdn”
  96. 96. Mixlib::Shellout > ls = Mixlib::ShellOut.new("ls") > ls.run_command > ls.stdout “init.elnREADME.mdn”
  97. 97. Plugin Architecture
  98. 98. Hooks
  99. 99. Logging
  100. 100. GNU CLI standards ‘--version’ and ‘--help’ Input/Output Files -- -O for Output Files
  101. 101. “CLI apps could be the first consumers ” of your services.
  102. 102. “CLI apps could be the first consumers ” of your services.
  103. 103. “CLI apps could be the first consumers ” of your services. Testing Development Cheap No rich UI, functional Fast tests
  104. 104. Ideas CLI app for a REST API: Github SCM Plugins for Knife Transform Rake scripts into first class CLI apps Shishir Nikhil @shishirdas @hyfather
  105. 105. Questions Comments Suggestions Shishir Nikhil @shishirdas @hyfather

Editor's Notes

  • \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 &amp; 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 &gt;&gt; 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&amp;#x2019;t assume anything.\n\nE. g. -- package managers assume latest version\n
  • \n
  • \n
  • \n
  • git add\n&gt; did you mean `git add .&amp;#x2019; ?\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 -&gt; Option Parsing\n\nOutput - status code, stdout, stderr\n
  • Input - command line -&gt; Option Parsing\n\nOutput - status code, stdout, stderr\n
  • Input - command line -&gt; Option Parsing\n\nOutput - status code, stdout, stderr\n
  • Input - command line -&gt; Option Parsing\n\nOutput - status code, stdout, stderr\n
  • Input - command line -&gt; Option Parsing\n\nOutput - status code, stdout, stderr\n
  • Input - command line -&gt; Option Parsing\n\nOutput - status code, stdout, stderr\n
  • Input - command line -&gt; Option Parsing\n\nOutput - status code, stdout, stderr\n
  • Input - command line -&gt; Option Parsing\n\nOutput - status code, stdout, stderr\n
  • Input - command line -&gt; Option Parsing\n\nOutput - status code, stdout, stderr\n
  • Input - command line -&gt; 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&amp;#xA0; mocha works out well.&amp;#xA0;\n&amp;#xA0; For filesystem, MockFS lets you mock the entire file system.&amp;#xA0;\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&apos;t change but the command changes the test breaks.\n&amp;#xA0;- 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&amp;#xA0;\n&amp;#xA0; - powerful machines.\n&amp;#xA0; - strong virtualizations (inbuilt)\nvagrant, lxc, openvz\n
  • 2 distinct sections -- your and subprocesses\n\nbackticks and system ruby calls not versatile. Doesn&amp;#x2019;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&amp;#x2019;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

×