convoy 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (109) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.irbrc +3 -0
  4. data/.rspec +3 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +8 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE +22 -0
  9. data/README.md +705 -0
  10. data/Rakefile +1 -0
  11. data/convoy.gemspec +24 -0
  12. data/examples/.my_apprc +24 -0
  13. data/examples/basic +10 -0
  14. data/examples/basic_config_file +16 -0
  15. data/examples/basic_conflicts +17 -0
  16. data/examples/basic_depends_on +25 -0
  17. data/examples/basic_flags +15 -0
  18. data/examples/basic_options +14 -0
  19. data/examples/basic_options_multi +15 -0
  20. data/examples/basic_require_arguments +17 -0
  21. data/examples/basic_texts +21 -0
  22. data/examples/basic_validations +21 -0
  23. data/examples/basic_with_everything +30 -0
  24. data/examples/commands/example_command.rb +13 -0
  25. data/examples/suite_complex +65 -0
  26. data/examples/suite_simple +19 -0
  27. data/examples/suite_with_sub_commands +94 -0
  28. data/lib/convoy.rb +83 -0
  29. data/lib/convoy/action_command/base.rb +85 -0
  30. data/lib/convoy/action_command/escort_utility_command.rb +53 -0
  31. data/lib/convoy/app.rb +127 -0
  32. data/lib/convoy/arguments.rb +20 -0
  33. data/lib/convoy/auto_options.rb +71 -0
  34. data/lib/convoy/error/error.rb +33 -0
  35. data/lib/convoy/formatter/command.rb +87 -0
  36. data/lib/convoy/formatter/commands.rb +37 -0
  37. data/lib/convoy/formatter/cursor_position.rb +29 -0
  38. data/lib/convoy/formatter/default_help_formatter.rb +117 -0
  39. data/lib/convoy/formatter/global_command.rb +17 -0
  40. data/lib/convoy/formatter/option.rb +152 -0
  41. data/lib/convoy/formatter/options.rb +28 -0
  42. data/lib/convoy/formatter/shell_command_executor.rb +49 -0
  43. data/lib/convoy/formatter/stream_output_formatter.rb +88 -0
  44. data/lib/convoy/formatter/string_grid.rb +108 -0
  45. data/lib/convoy/formatter/string_splitter.rb +50 -0
  46. data/lib/convoy/formatter/terminal.rb +30 -0
  47. data/lib/convoy/global_pre_parser.rb +43 -0
  48. data/lib/convoy/logger.rb +75 -0
  49. data/lib/convoy/option_dependency_validator.rb +82 -0
  50. data/lib/convoy/option_parser.rb +155 -0
  51. data/lib/convoy/setup/configuration/generator.rb +75 -0
  52. data/lib/convoy/setup/configuration/instance.rb +34 -0
  53. data/lib/convoy/setup/configuration/loader.rb +43 -0
  54. data/lib/convoy/setup/configuration/locator/base.rb +19 -0
  55. data/lib/convoy/setup/configuration/locator/chaining.rb +29 -0
  56. data/lib/convoy/setup/configuration/locator/descending_to_home.rb +23 -0
  57. data/lib/convoy/setup/configuration/locator/executing_script_directory.rb +15 -0
  58. data/lib/convoy/setup/configuration/locator/specified_directory.rb +21 -0
  59. data/lib/convoy/setup/configuration/merge_tool.rb +38 -0
  60. data/lib/convoy/setup/configuration/reader.rb +36 -0
  61. data/lib/convoy/setup/configuration/writer.rb +46 -0
  62. data/lib/convoy/setup/dsl/action.rb +17 -0
  63. data/lib/convoy/setup/dsl/command.rb +67 -0
  64. data/lib/convoy/setup/dsl/config_file.rb +13 -0
  65. data/lib/convoy/setup/dsl/global.rb +29 -0
  66. data/lib/convoy/setup/dsl/options.rb +81 -0
  67. data/lib/convoy/setup_accessor.rb +206 -0
  68. data/lib/convoy/trollop.rb +861 -0
  69. data/lib/convoy/utils.rb +21 -0
  70. data/lib/convoy/validator.rb +45 -0
  71. data/spec/integration/basic_config_file_spec.rb +126 -0
  72. data/spec/integration/basic_conflicts_spec.rb +47 -0
  73. data/spec/integration/basic_depends_on_spec.rb +275 -0
  74. data/spec/integration/basic_options_spec.rb +41 -0
  75. data/spec/integration/basic_options_with_multi_spec.rb +30 -0
  76. data/spec/integration/basic_spec.rb +38 -0
  77. data/spec/integration/basic_validations_spec.rb +77 -0
  78. data/spec/integration/basic_with_arguments_spec.rb +35 -0
  79. data/spec/integration/basic_with_text_fields_spec.rb +21 -0
  80. data/spec/integration/suite_simple_spec.rb +45 -0
  81. data/spec/integration/suite_sub_command_spec.rb +51 -0
  82. data/spec/lib/convoy/action_command/base_spec.rb +200 -0
  83. data/spec/lib/convoy/formatter/command_spec.rb +238 -0
  84. data/spec/lib/convoy/formatter/global_command_spec.rb +50 -0
  85. data/spec/lib/convoy/formatter/option_spec.rb +300 -0
  86. data/spec/lib/convoy/formatter/shell_command_executor_spec.rb +59 -0
  87. data/spec/lib/convoy/formatter/stream_output_formatter_spec.rb +214 -0
  88. data/spec/lib/convoy/formatter/string_grid_spec.rb +59 -0
  89. data/spec/lib/convoy/formatter/string_splitter_spec.rb +50 -0
  90. data/spec/lib/convoy/formatter/terminal_spec.rb +19 -0
  91. data/spec/lib/convoy/setup/configuration/generator_spec.rb +101 -0
  92. data/spec/lib/convoy/setup/configuration/loader_spec.rb +79 -0
  93. data/spec/lib/convoy/setup/configuration/locator/chaining_spec.rb +81 -0
  94. data/spec/lib/convoy/setup/configuration/locator/descending_to_home_spec.rb +57 -0
  95. data/spec/lib/convoy/setup/configuration/locator/executing_script_directory_spec.rb +29 -0
  96. data/spec/lib/convoy/setup/configuration/locator/specified_directory_spec.rb +33 -0
  97. data/spec/lib/convoy/setup/configuration/merge_tool_spec.rb +41 -0
  98. data/spec/lib/convoy/setup/configuration/reader_spec.rb +41 -0
  99. data/spec/lib/convoy/setup/configuration/writer_spec.rb +75 -0
  100. data/spec/lib/convoy/setup_accessor_spec.rb +226 -0
  101. data/spec/lib/convoy/utils_spec.rb +30 -0
  102. data/spec/spec_helper.rb +29 -0
  103. data/spec/support/integration_helpers.rb +2 -0
  104. data/spec/support/matchers/execute_action_for_command_matcher.rb +21 -0
  105. data/spec/support/matchers/execute_action_with_arguments_matcher.rb +25 -0
  106. data/spec/support/matchers/execute_action_with_options_matcher.rb +29 -0
  107. data/spec/support/matchers/exit_with_code_matcher.rb +29 -0
  108. data/spec/support/shared_contexts/integration_setup.rb +34 -0
  109. metadata +292 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 62e64e31cb32b012e592465bee8d1447f7a996b4
4
+ data.tar.gz: 0787f79d59ffa58a26d8a6f51cfb5aa84e67146f
5
+ SHA512:
6
+ metadata.gz: 8ee90b28c6f9775eb877047900a510cc74da752b7ad014f617e7bb8057eb5cf94a2f8ce0e16a2cf0c599b09d4c846eecf918197c0749a6f7751f30e9b089edac
7
+ data.tar.gz: a58865ac6fe810286734579e6b179cb1c3132f34856c8cf678f947a905508bd7389529eabc6f551687f5f894db76b18b71c89d6b0e59ea15d357f5656794f4b5
data/.gitignore ADDED
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ tags
19
+ .idea/
20
+ .DS_Store
data/.irbrc ADDED
@@ -0,0 +1,3 @@
1
+ $:.unshift File.expand_path(File.join(File.dirname(__FILE__), 'lib'))
2
+
3
+ require './lib/convoy'
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format doc
3
+ --require spec_helper
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.0.0-p0
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - "1.9.2"
4
+ - "1.9.3"
5
+ - "2.0.0"
6
+ - jruby-19mode # JRuby in 1.9 mode
7
+ #- rbx-19mode
8
+ script: bundle exec rspec spec
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in convoy.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 - Albert Rannetsperger
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,705 @@
1
+ # Convoy
2
+
3
+ Writing even complex command-line apps should be quick, easy and fun. Convoy takes the excellent [Trollop](http://trollop.rubyforge.org/) option parser and adds a whole bunch of awesome features to produce a library you will always want to turn to when a 'quick script' is in order.
4
+
5
+ ## Why Write Another CLI Tool
6
+
7
+ A lot of the existing CLI making libraries delegate to OptionParser for actually parsing the option string, while OptionParser is nice it doesn't allow things like specifying the same option multiple times (e.g. like CURL -H parameter) which I like and use quite often. Trollop handles this case nicely, so a Trollop-based CLI tool is superior.
8
+
9
+ Also a lot of the other CLI libraries in an attempt to be extra terse and DRY make their syntax a little obtuse. Convoy tries to create a DSL that strikes a balance between being terse and being easy to understand, remember and read.
10
+
11
+ I find that I end up with a similar structure for the CLI apps that I write and I want to capture that as a bit of a convention/pattern. An app doesn't stop at the option parsing, how do you actually structure the code that executes the work?
12
+
13
+ In general, some libraries give you great option parsing, but no infinitely nesting sub-command, others have sub-commands, but the option parsing is inferior. I want everything from my CLI library, without having to dig through feature lists, trying to figure out what will give me the biggest bang for my buck.
14
+
15
+ ## Features
16
+
17
+ * Long and short form options (e.g. `-g` and `--global`)
18
+ * Command support (e.g. `my_app do` - where `do` is a command)
19
+ * Infinitely nesting sub-command support (e.g. `my_app do something fun` - where `do`, `something` and `fun` are commands)
20
+ * Options on a per command and sub-command level (e.g. `my_app -g do -g something -g fun` - where `-g` can mean different things for each command)
21
+ * Nicely formatted help text (your app get `-h` and `--help` options automatically)
22
+ * Version support (your app can get the `-v` and `--version` based on specification)
23
+ * Multi options (e.g. `my_app -x foo -x bar -x baz`)
24
+ * You can mark options as conflicting with each other
25
+ * You can mark options as depending on each other
26
+ * Declarative validation support for each option, with multiple validation rules per option
27
+ * Specify script arguments as required or optional (when arguments are required `my_app -g foo` will execute, but `my_app -g` will prompt the user to enter arguments)
28
+ * Specify summary and description for your script as well as per command or sub-command
29
+ * A pattern for executing command actions (e.g. a base class to inherit from, with many helper methods)
30
+ * Automatic option to up the verbosity level of output, for debugging purposes
31
+ * Sensible and robust error handling, that distinguishes between library issues and implementation errors
32
+ * Config file support (e.g. `.my_apprc`), JSON format, created in user home directory by default
33
+ * Set up config files to be auto-creatable
34
+ * Built in config file discovery by walking up the directory tree
35
+ * Automatic option to supply a specific config file for current run
36
+ * Config file values can override default values
37
+ * Command line options can override config file values
38
+ * A space for user specific config values in the config file, which is available at runtime
39
+ * Automatic command with options to update and create config files in a default location or anywhere else (e.g. `my_app convoy --update-config`)
40
+ * Tool to bootstrap an Convoy script (basic, with commands and with sub-commands) (NOT YET IMPLEMENTED)
41
+ * Envrionment aware configuration (NOT YET IMPLEMENTED)
42
+ * Project specific scripts support (NOT YET IMPLEMENTED)
43
+ * Lots of usage examples (COMING SOON)
44
+ * Lots of documentation (COMING SOON)
45
+
46
+ ## Installation
47
+
48
+ Add this line to your application's Gemfile:
49
+
50
+ gem 'convoy'
51
+
52
+ And then execute:
53
+
54
+ $ bundle
55
+
56
+ Or install it yourself as:
57
+
58
+ $ gem install convoy
59
+
60
+ ## Usage
61
+
62
+ Let's say you want to do a basic app. As long as `convoy` is installed as a gem, you might do something like this:
63
+
64
+ ```ruby
65
+ #!/usr/bin/env ruby
66
+
67
+ require 'convoy'
68
+ require 'my_app'
69
+
70
+ Convoy::App.create do |app|
71
+ app.action do |options, arguments|
72
+ MyApp::ExampleCommand.new(options, arguments).execute
73
+ end
74
+ end
75
+ ```
76
+
77
+ If your script is called `app.rb` you is executable, you can then call it like this:
78
+
79
+ ```
80
+ ./app.rb
81
+ ./app.rb --help
82
+ ```
83
+ And it will run whatever code you have in the execute method of your `ExampleCommand`. The command should inherit from `::Convoy::ActionCommand::Base` which will give it access to things like `options`, `arguments`, `command_options`, `config` etc.
84
+
85
+ Of course the above configuration didn't let us specify any options (except for `--help`), so lets add some.
86
+
87
+ ```ruby
88
+ #!/usr/bin/env ruby
89
+
90
+ require 'convoy'
91
+ require 'my_app'
92
+
93
+ Convoy::App.create do |app|
94
+ app.options do |opts|
95
+ opts.opt :option1, "Option1", :short => '-o', :long => '--option1', :type => :string, :default => "option 1"
96
+ end
97
+
98
+ app.action do |options, arguments|
99
+ MyApp::ExampleCommand.new(options, arguments).execute
100
+ end
101
+ end
102
+ ```
103
+ We can now do:
104
+
105
+ ```
106
+ ./app.rb -o blah
107
+ ./app.rb --option1=blah
108
+ ```
109
+
110
+ Now when your command is being executed and passed the option through on the command-line, the `command_options` hash will contain the value keyed on the canonical name of your option `:option1`.
111
+
112
+ ### Multi Options
113
+
114
+ Creating an option that can be specified multiple times is dead simple:
115
+
116
+ ```ruby
117
+ #!/usr/bin/env ruby
118
+
119
+ require 'convoy'
120
+ require 'my_app'
121
+
122
+ Convoy::App.create do |app|
123
+ app.options do |opts|
124
+ opts.opt :option1, "Option1", :short => '-o', :long => '--option1', :type => :string, :default => "option 1"
125
+ opts.opt :option2, "Option2", :short => :none, :long => '--option2', :type => :string, :multi => true
126
+ end
127
+
128
+ app.action do |options, arguments|
129
+ MyApp::ExampleCommand.new(options, arguments).execute
130
+ end
131
+ end
132
+ ```
133
+
134
+ We can now do:
135
+
136
+ ```
137
+ ./app.rb --option2=hello --option2=world
138
+ ```
139
+
140
+ Notice that the short form for the multi option was disabled so we had to use the long form, we could, of course create a short form for this option as well.
141
+
142
+ The `command_options` hash in our `ExampleCommand` will have an array of values for multi options.
143
+
144
+ ### Flags
145
+
146
+ Flags are easy, just decalre an option as a `:boolean` and you can use it as a flag and will automatically gain a negation for it as well. For example:
147
+
148
+ ```ruby
149
+ #!/usr/bin/env ruby
150
+
151
+ require 'convoy'
152
+ require 'my_app'
153
+
154
+ Convoy::App.create do |app|
155
+ app.options do |opts|
156
+ opts.opt :option1, "Option1", :short => '-o', :long => '--option1', :type => :boolean, :default => true
157
+ end
158
+
159
+ app.action do |options, arguments|
160
+ MyApp::ExampleCommand.new(options, arguments).execute
161
+ end
162
+ end
163
+ ```
164
+
165
+ We can now do:
166
+
167
+ ```
168
+ ./app.rb -h
169
+ ./app.rb -o
170
+ ./app.rb --option1
171
+ ./app.rb --no-option1
172
+ ```
173
+
174
+ If you use the negation, the value of `:option1` in the command hash will be `false`, otherwise it will be `true`. Since we made `true` the default value, even if you don't specify the option at all, `:option1` will be `true`. If you don't set the default for a flag to `true`, then it will have no negation, since not using the flag will be equivalent to setting to `false`.
175
+
176
+ ### Required Arguments
177
+
178
+ When you define an Convoy app, by default it will not require you to supply any arguments to it. However you can specify that arguments are required for this app.
179
+
180
+ ```ruby
181
+ #!/usr/bin/env ruby
182
+
183
+ require 'convoy'
184
+ require 'my_app'
185
+
186
+ Convoy::App.create do |app|
187
+ app.requires_arguments
188
+
189
+ app.options do |opts|
190
+ opts.opt :option1, "Option1", :short => '-o', :long => '--option1', :type => :string, :default => "option 1"
191
+ opts.opt :option2, "Option2", :short => :none, :long => '--option2', :type => :string, :multi => true
192
+ end
193
+
194
+ app.action do |options, arguments|
195
+ MyApp::ExampleCommand.new(options, arguments).execute
196
+ end
197
+ end
198
+ ```
199
+
200
+ We can now do:
201
+
202
+ ```
203
+ ./app.rb -o foo --option2=bar argument1
204
+ ```
205
+
206
+ This will execute fine, but if we do:
207
+
208
+ ```
209
+ ./app.rb -o foo --option2=bar
210
+ ```
211
+
212
+ The app will prompt the user to enter an argument. Pressing `Enter` will allow to enter more arguments. Like with many command-line apps you will need to press `Ctrl-D` to jump out of the prompt and allow the app to execute.
213
+
214
+ There is one extra thing to note. If you're writing a command suite (see below for explanation). The `requires_arguments` setting will be inherited by all the sub-commands. You can however easily override the setting for any sub-command and this overridden value will be inherited by all the sub-commands of that command.
215
+
216
+ ```ruby
217
+ ...
218
+ app.command :start do |command|
219
+ command.requires_arguments false
220
+
221
+ command.action do |options, arguments|
222
+ MyApp::ExampleCommand.new(options, arguments).execute
223
+ end
224
+ end
225
+ ...
226
+ ```
227
+
228
+ This way different commands in your app can pick and choose whether or not they want to require arguments or make them optional.
229
+
230
+
231
+ ### Better Help Text
232
+
233
+ You automatically get some nicely formatted help text for your app. But to make the help a bit nicer, make sure you specify a summary and description.
234
+
235
+ ```ruby
236
+ #!/usr/bin/env ruby
237
+
238
+ require 'convoy'
239
+ require 'my_app'
240
+
241
+ Convoy::App.create do |app|
242
+ app.version "0.1.1"
243
+ app.summary "Summary 1"
244
+ app.description "Description 1"
245
+
246
+ app.requires_arguments
247
+
248
+ app.options do |opts|
249
+ opts.opt :option1, "Option1", :short => '-o', :long => '--option1', :type => :string, :default => "option 1"
250
+ opts.opt :option2, "Option2", :short => :none, :long => '--option2', :type => :string, :multi => true
251
+ end
252
+
253
+ app.action do |options, arguments|
254
+ MyApp::ExampleCommand.new(options, arguments).execute
255
+ end
256
+ end
257
+ ```
258
+
259
+ Your help text will then look like this:
260
+
261
+ ```
262
+ NAME
263
+ app.rb - Summary 1
264
+
265
+ Description 1
266
+
267
+ USAGE
268
+ app.rb [options] [arguments...]
269
+
270
+ VERSION
271
+ 0.1.1
272
+
273
+ OPTIONS
274
+ --option1, -o <s> - Option1 (default: option 1)
275
+ --option2 <s> - Option2
276
+ --verbosity <s> - Verbosity level of output for current execution (e.g. INFO, DEBUG) (default: WARN)
277
+ --error-output-format <s> - The format to use when outputting errors (e.g. basic, advanced) (default: basic)
278
+ --version, -v - Print version and exit
279
+ --help, -h - Show this message
280
+ ```
281
+
282
+ As you can see we've also specified the version, which gives us the `--version` flag.
283
+
284
+ ### Option Dependencies
285
+
286
+ You can set up some options to be dependent on the presence or absence of other options. The app will not execute successfully unless all the dependencies are met. You can make your dependencies quite complex, but it is best to keep it pretty simple.
287
+
288
+ ```ruby
289
+ #!/usr/bin/env ruby
290
+
291
+ require 'convoy'
292
+ require 'my_app'
293
+
294
+ Convoy::App.create do |app|
295
+ app.options do |opts|
296
+ opts.opt :flag1, "Flag 1", :short => '-f', :long => '--flag1', :type => :boolean
297
+ opts.opt :flag2, "Flag 2", :short => :none, :long => '--flag2', :type => :boolean, :default => true
298
+ opts.opt :option1, "Option1", :short => '-o', :long => '--option1', :type => :string
299
+ opts.opt :option2, "Option2", :short => :none, :long => '--option2', :type => :string, :multi => true
300
+ opts.opt :option3, "Option3", :short => :none, :long => '--option3', :type => :string
301
+ opts.opt :option4, "Option4", :short => :none, :long => '--option4', :type => :string
302
+
303
+ opts.dependency :option1, :on => :flag1
304
+ opts.dependency :option2, :on => [:flag1, :option1]
305
+ opts.dependency :option3, :on => {:option1 => 'foo'}
306
+ #opts.dependency :option4, :on => [{:flag1 => false}, :option1] #This will get you into big trouble as it can never be fulfilled
307
+ opts.dependency :option4, :on => [{:flag2 => false}, :option1]
308
+ end
309
+
310
+ app.action do |options, arguments|
311
+ MyApp::ExampleCommand.new(options, arguments).execute
312
+ end
313
+ end
314
+ ```
315
+
316
+ In this case if you're using `:option1` you need to have `:flag1` as well e.g.:
317
+
318
+ ```
319
+ ./app.rb -o foo -f
320
+ ```
321
+
322
+ `:option2` requires both `:option1` and `:flag1` (of course since `:option1` already has a dependency on `:flag1` it is bit redundant, but demostrates the syntax).
323
+
324
+ `:option3` is dependant on `:option1` having a particular value, so we need to have:
325
+
326
+ ```
327
+ ./app.rb -o foo -f --option3=bar
328
+ ```
329
+
330
+ in order for the app to execute successfully, when we've specified `:option3` on the command-line.
331
+
332
+ Note how with `:option4` we can mix the syntax. However also note the commented out dependency, this is how you can get into trouble if you make your dependencies too complex. `:option4` in the comment requires `:option1` and the absence of `:flag1`, but `:option1` requires `:flag1` to be present. Needless to say with that specification, using `:option4` on the command-line will never allow the app to execute successfully.
333
+
334
+
335
+ ### Option Conflicts
336
+
337
+ You can specify 2 or more option as conflicting, which means the app will not execute successfully if more than one of those options are provided on the command-line together.
338
+
339
+ ```ruby
340
+ #!/usr/bin/env ruby
341
+
342
+ require 'convoy'
343
+ require 'my_app'
344
+
345
+ Convoy::App.create do |app|
346
+ app.options do |opts|
347
+ opts.opt :flag1, "Flag 1", :short => '-f', :long => '--flag1', :type => :boolean
348
+ opts.opt :flag2, "Flag 2", :short => :none, :long => '--flag2', :type => :boolean
349
+
350
+ opts.conflict :flag1, :flag2, :flag3
351
+ end
352
+
353
+ app.action do |options, arguments|
354
+ MyApp::ExampleCommand.new(options, arguments).execute
355
+ end
356
+ end
357
+ ```
358
+
359
+ This will succeed:
360
+
361
+ ```
362
+ ./app.rb --flag1
363
+ ```
364
+
365
+ This will fail:
366
+
367
+ ```
368
+ ./app.rb --flag1 --flag2
369
+ ```
370
+
371
+ Simple!
372
+
373
+
374
+ ### Validations
375
+
376
+ Validations are pretty easy, they can be defined inside the options block. You must provide an option symbol and an error message for when validation fails. The actual validation is a block, when the block evaluates to true, it means validation is successful, othewise validation fails. That's all there is to it.
377
+
378
+ ```ruby
379
+ #!/usr/bin/env ruby
380
+
381
+ require 'convoy'
382
+ require 'my_app'
383
+
384
+ Convoy::App.create do |app|
385
+ app.options do |opts|
386
+ opts.opt :option1, "Option 1", :short => '-o', :long => '--option1', :type => :string
387
+ opts.opt :int1, "Int 1", :short => '-i', :long => '--int1', :type => :int
388
+ opts.opt :option2, "Option 2", :short => :none, :long => '--option2', :type => :string
389
+
390
+ opts.validate(:option1, "must be either 'foo' or 'bar'") { |option| ["foo", "bar"].include?(option) }
391
+ opts.validate(:int1, "must be between 10 and 20 exclusive") { |option| option > 10 && option < 20 }
392
+ opts.validate(:option2, "must be two words") {|option| option =~ /\w\s\w/}
393
+ opts.validate(:option2, "must be at least 20 characters long") {|option| option.length >= 20}
394
+ end
395
+
396
+ app.action do |options, arguments|
397
+ MyApp::ExampleCommand.new(options, arguments).execute
398
+ end
399
+ end
400
+ ```
401
+
402
+ In this case running:
403
+
404
+ ```
405
+ ./app.rb -o baz
406
+ ```
407
+
408
+ will produce:
409
+
410
+ ```
411
+ option1 must be either 'foo' or 'bar'
412
+ ```
413
+
414
+ But if you run:
415
+
416
+ ```
417
+ ./app.rb -o bar
418
+ ```
419
+
420
+ Everything will work fine.
421
+
422
+ As you can see you may define multiple validation rules for the same option without any issues, all will need to pass for the app to execute successfully.
423
+
424
+
425
+ ### Configuration File
426
+
427
+ Sometime you have a whole bunch of command line options you need to pass in to your app, but the value of these options hardly ever changes. It's a bit of a drag to have supply these on the command-line every time. This is why we have config file support. You may indicate that your app has a config file like so:
428
+
429
+ ```ruby
430
+ #!/usr/bin/env ruby
431
+
432
+ require 'convoy'
433
+ require 'my_app'
434
+
435
+ Convoy::App.create do |app|
436
+ app.config_file ".my_apprc", :autocreate => true
437
+
438
+ app.options do |opts|
439
+ opts.opt :option1, "Option1", :short => '-o', :long => '--option1', :type => :string, :default => "option 1"
440
+ end
441
+
442
+ app.action do |options, arguments|
443
+ MyApp::ExampleCommand.new(options, arguments).execute
444
+ end
445
+ end
446
+ ```
447
+
448
+ Essentially we give the config file a name and optionally set the config file to be autocreatable. When the config file is autocreatable, the first time you run the command-line app, a config file with the name your specified will be created in the default location (which is your home directory). The config file format is JSON, and it is structured in a specific way to support infinitely nesting sub commands. The config file for an app like the one above, might look like this:
449
+
450
+ ```
451
+ {
452
+ "global": {
453
+ "options": {
454
+ "option1": "option 1",
455
+ "config": null,
456
+ "verbosity": "WARN",
457
+ "error_output_format": "basic"
458
+ },
459
+ "commands": {
460
+ "convoy": {
461
+ "options": {
462
+ "create_config": null,
463
+ "create_default_config": null,
464
+ "update_config": null,
465
+ "update_default_config": null
466
+ },
467
+ "commands": {
468
+ }
469
+ }
470
+ }
471
+ },
472
+ "user": {
473
+ }
474
+ }
475
+ ```
476
+
477
+ All the options you can supply to your app will be present in the config file as well as all the commands and all their options etc (you also get a bunch of options and a single command that get automatically added by convoy for your convenience, these will be explained below). You can go and modify the values for all your options in the config file. The config file values, take precedence over the default values that you specified for the options (if any), but if you supply an option value on the command-line, this will still take precedence over the option value you define in the config file. This way you can provide sensible defaults and still override when necessary.
478
+
479
+ Whenever you define an app to have a config file, convoy will add some utility options and commands for you to make working with config files easier. Firstly, you will get a global `config` option. This can be used to make your app take a specific config file instead of the default one, this config file will only be valid for the single execution of your command-line app e.g.:
480
+
481
+ ```
482
+ ./app.rb --config='/var/opt/.blahrc' -o yadda
483
+ ```
484
+
485
+ You also get a utility command for working with config files - the `convoy` command. This command has 4 options:
486
+
487
+ * `--create-config` - this can be used to create a config file in a specified location
488
+ * `--create-default-config` - this can be used to create a config file in the default location (home directory) if one doesn't already exit
489
+ * `--update-config` - this can be used to update a specific config file (mainly useful during development), if you've added new options/commands using this option will allow your config file to reflect this (it will not blow away any of the values you have set for your options)
490
+ * `--update-default-config` - same as above but will just work with the default config file if it is present (if not, it will create a default config file)
491
+
492
+ These utility options can be very useful as the nested format of the config file can become confusing if you have to update it by hand with new options and commands while you're developing your apps.
493
+
494
+ It is also worth knowing that Convoy is quite clever when it comes to working out which config file to use for a particular run of the command-line app. It will not just use the config file in your home directory, but will instead attempt to find a config file closer in the directory hierarchy to the location of the command-line script. The strategy is as follows:
495
+
496
+ * look for a config file in the directory where the command-line script itself lives, if found use that one
497
+ * otherwise look for a config file in the current working directory and use that one
498
+ * if still not found, go down one level of the directory tree from the current working directory and look for a config file there
499
+ * keep going down the directory tree until the home directory is reached, if no config file was found, then no config file exists
500
+ * in which case either create the config file in home directory (if autocreatable) or continue without config file
501
+
502
+ As you can see the config file support is quite extensive and can take your command-line apps to the next level in terms of useability and utility.
503
+
504
+
505
+ ### Command Suites
506
+
507
+ You're not just limited to options for your command-line apps. Convoy allows you to turn your command-line apps into command-line suites, with command support. Let's say you want a command-line app to control a process of some sort. Your process is `app.rb`. What you want to be able to do is the following:
508
+
509
+ ```
510
+ ./app.rb start
511
+ ./app.rb stop
512
+ ./app.rb restart
513
+ ```
514
+
515
+ You also want to be able to provide options both to the main process itself as well as to the various commands, e.g.:
516
+
517
+ ```
518
+ ./app.rb -e production start --reload
519
+ ```
520
+
521
+ Convoy makes this easy:
522
+
523
+ ```ruby
524
+ #!/usr/bin/env ruby
525
+
526
+ require 'convoy'
527
+ require 'my_app'
528
+
529
+ Convoy::App.create do |app|
530
+ app.config_file ".my_apprc", :autocreate => true
531
+
532
+ app.options do |opts|
533
+ opts.opt :environment, "Environment", :short => '-e', :long => '--environment', :type => :string, :default => "development"
534
+ end
535
+
536
+ app.command :start do |command|
537
+ command.summary "Start process"
538
+ command.description "Start process"
539
+
540
+ command.options do |opts|
541
+ opts.opt :reload, "Reload", :short => '-r', :long => '--reload', :type => :flag
542
+ end
543
+
544
+ command.action do |options, arguments|
545
+ MyApp::ExampleCommand.new(options, arguments).execute
546
+ end
547
+ end
548
+
549
+ app.command :stop do |command|
550
+ command.summary "Stop process"
551
+ command.description "Stop process"
552
+
553
+ command.action do |options, arguments|
554
+ MyApp::ExampleCommand.new(options, arguments).execute
555
+ end
556
+ end
557
+
558
+ app.command :restart do |command|
559
+ command.summary "Restart process"
560
+ command.description "Restart process"
561
+
562
+ command.action do |options, arguments|
563
+ MyApp::ExampleCommand.new(options, arguments).execute
564
+ end
565
+ end
566
+ end
567
+ ```
568
+
569
+ Of course you would probably use a different `ActionCommand` class to implement each of your defined commands (more on `ActionCommands` below).
570
+
571
+
572
+ ### Command Suites Sub-Commands
573
+
574
+ Of course if one level of commands is good it is only logical that multiple levels of commands is even better. Luckily Convoy supports infinitely nesting sub-commands so you can create command-line suites which are truly extensive. Let's say you're writing a command-line utility for your framework, you've named your framework 'Rails' (cause surely that's not taken :P) and your utility is `rails`. You want your utility to be used for generating migrations as well as controllers, something like:
575
+
576
+ ```
577
+ rails generate migration
578
+ rails g controller
579
+ ```
580
+
581
+ Of course you want to be able to supply a set of options to each of the sub-commands when necessary:
582
+
583
+ ```
584
+ rails -e development generate migration --sequel
585
+ ```
586
+
587
+ As we've come to expect, this is very easy:
588
+
589
+ ```ruby
590
+ #!/usr/bin/env ruby
591
+
592
+ require 'convoy'
593
+ require 'my_app'
594
+
595
+ Convoy::App.create do |app|
596
+ app.config_file ".my_apprc", :autocreate => false
597
+
598
+ app.options do |opts|
599
+ opts.opt :environment, "Environment", :short => '-e', :long => '--environment', :type => :string, :default => "development"
600
+ end
601
+
602
+ app.command :generate, :aliases => [:g] do |command|
603
+ command.command :migration do |command|
604
+ app.options do |opts|
605
+ opts.opt :sequel, "Sequel", :short => '-s', :long => '--sequel', :type => :flag
606
+ end
607
+
608
+ command.action do |options, arguments|
609
+ MyApp::ExampleCommand.new(options, arguments).execute
610
+ end
611
+ end
612
+
613
+ command.command :controller do |command|
614
+ command.action do |options, arguments|
615
+ MyApp::ExampleCommand.new(options, arguments).execute
616
+ end
617
+ end
618
+ end
619
+ end
620
+ ```
621
+
622
+ Of course, you can flesh it out by providing a summary and description for the commands, specifying if arguments are required, conflicts, dependencies etc (this will be reflected in the help text). Just about everything you can do at the global level for apps without sub-commands, you can do at the command level (the only things you currently can't specify at the command level are 'version' and 'config_file', these are global only).
623
+
624
+
625
+ ### Implementing the Actions
626
+
627
+ So far we've seen a lot of examples of how to add various command-line UI features, but what about implementing the actual functionality.
628
+
629
+ If your functionality is a one liner, you can, of course, just dump it into the action block, but we're trying to keep a clean separation between our UI logic and the actual functionality we are trying to implement. We do this by writing `ActionCommand` classes.
630
+
631
+ In all the previous examples you've seen the following:
632
+
633
+ ```ruby
634
+ ...
635
+
636
+ app.action do |options, arguments|
637
+ MyApp::ExampleCommand.new(options, arguments).execute
638
+ end
639
+
640
+ ...
641
+ ```
642
+
643
+ The `ExampleCommand` is an `ActionCommand` and is implemented in a separate class. In the above examples we would `require 'my_app'` with the assumption that it requires the file where `ExampleCommand` is imlpemented. You could of course just require the file with the implementation directly.
644
+
645
+ A command might look like this:
646
+
647
+ ```ruby
648
+ module Convoy
649
+ class ExampleCommand < ::Convoy::ActionCommand::Base
650
+ def execute
651
+ Convoy::Logger.output.puts "Command: #{command_name}"
652
+ Convoy::Logger.output.puts "Options: #{options}"
653
+ Convoy::Logger.output.puts "Command options: #{command_options}"
654
+ Convoy::Logger.output.puts "Arguments: #{arguments}"
655
+ if config
656
+ Convoy::Logger.output.puts "User config: #{config}"
657
+ end
658
+ end
659
+ end
660
+ end
661
+ ```
662
+
663
+ As you can see all you need to do is inherit from `::Convoy::ActionCommand::Base` and implement the execute method. Inheriting from `Base` gives you access to a bunch of useful methods:
664
+
665
+ * `command_name` - the name of the command that is being executed (or :global if it is the main action)
666
+ * `command_options` - the options for this command
667
+ * `parent_options` - the options for the parent command (if any)
668
+ * `global_options` - the global app options
669
+ * `config` - a hash of the user defined portion of the configuration file
670
+ * `options` - the raw options hash
671
+ * `arguments` - a list of arguments passed to the app
672
+
673
+ These should give you hand in implementing the functionality you need, but most importantly it allows us to keep our actual command-line UI definition clean and separate the useful logic into what is/are essentially pure ruby classes.
674
+
675
+ ## Examples
676
+
677
+ There is an examples directory where you can have a play with a whole bunch of little 'Convoy' apps, from a very basic one with no options to more complex nested command suites with all the trimmings. Most of them call the `ExampleCommand` which lives in `examples/commands`. Have a read/play to learn, or just copy/paste bits straight into your apps.
678
+
679
+ ## More In-Depth
680
+
681
+ TODO
682
+
683
+ ## Command-Line Tools Built With Convoy
684
+
685
+ If you've used Convoy to build a command-line tool that you have made publicly available, feel free to add a link to it here.
686
+
687
+ * https://github.com/skorks/ppjson - pretty print your JSON on the command-line (works with JSON strings as well as files containing JSON, can uglify as well as pretty print)
688
+
689
+
690
+ ## Alternatives
691
+
692
+ * https://github.com/davetron5000/gli
693
+ * https://github.com/visionmedia/commander
694
+ * https://github.com/ddfreyne/cri
695
+ * https://github.com/ahoward/main
696
+ * https://github.com/wycats/thor
697
+ * https://github.com/injekt/slop
698
+
699
+ ## Contributing
700
+
701
+ 1. Fork it
702
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
703
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
704
+ 4. Push to the branch (`git push origin my-new-feature`)
705
+ 5. Create new Pull Request