hammer_cli 0.0.9 → 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/doc/creating_apipie_commands.md +296 -0
  4. data/doc/creating_commands.md +547 -0
  5. data/doc/developer_docs.md +5 -926
  6. data/doc/development_tips.md +30 -0
  7. data/doc/writing_a_plugin.md +90 -0
  8. data/lib/hammer_cli/abstract.rb +31 -11
  9. data/lib/hammer_cli/apipie/resource.rb +14 -6
  10. data/lib/hammer_cli/apipie/write_command.rb +14 -5
  11. data/lib/hammer_cli/exception_handler.rb +7 -4
  12. data/lib/hammer_cli/options/normalizers.rb +27 -0
  13. data/lib/hammer_cli/output/adapter/abstract.rb +8 -8
  14. data/lib/hammer_cli/output/adapter/csv.rb +37 -4
  15. data/lib/hammer_cli/output/adapter/silent.rb +2 -2
  16. data/lib/hammer_cli/output/dsl.rb +3 -1
  17. data/lib/hammer_cli/output/output.rb +24 -19
  18. data/lib/hammer_cli/utils.rb +18 -0
  19. data/lib/hammer_cli/version.rb +1 -1
  20. data/lib/hammer_cli.rb +1 -0
  21. data/test/unit/abstract_test.rb +296 -0
  22. data/test/unit/apipie/command_test.rb +270 -0
  23. data/test/unit/apipie/fake_api.rb +101 -0
  24. data/test/unit/apipie/read_command_test.rb +34 -0
  25. data/test/unit/apipie/write_command_test.rb +38 -0
  26. data/test/unit/exception_handler_test.rb +45 -0
  27. data/test/unit/main_test.rb +47 -0
  28. data/test/unit/options/normalizers_test.rb +148 -0
  29. data/test/unit/options/option_definition_test.rb +43 -0
  30. data/test/unit/output/adapter/abstract_test.rb +96 -0
  31. data/test/unit/output/adapter/base_test.rb +27 -0
  32. data/test/unit/output/adapter/csv_test.rb +75 -0
  33. data/test/unit/output/adapter/table_test.rb +58 -0
  34. data/test/unit/output/definition_test.rb +27 -0
  35. data/test/unit/output/dsl_test.rb +119 -0
  36. data/test/unit/output/fields_test.rb +97 -0
  37. data/test/unit/output/formatters_test.rb +83 -0
  38. data/test/unit/output/output_test.rb +104 -0
  39. data/test/unit/settings_test.rb +106 -0
  40. data/test/unit/test_helper.rb +20 -0
  41. data/test/unit/utils_test.rb +35 -0
  42. data/test/unit/validator_test.rb +142 -0
  43. metadata +112 -35
  44. data/LICENSE +0 -5
  45. data/hammer_cli_complete +0 -13
@@ -0,0 +1,547 @@
1
+ Create your first command
2
+ -------------------------
3
+
4
+ We will create a simple command called `hello` that will print a sentence "Hello World!" to stdout.
5
+
6
+ ### Declare the command
7
+
8
+ ```
9
+ touch ./lib/hammer_cli_hello/hello_world.rb
10
+ ```
11
+
12
+ ```ruby
13
+ # ./lib/hammer_cli_hello/hello_world.rb
14
+ require 'hammer_cli'
15
+
16
+ # it's a good practise to nest commands into modules
17
+ module HammerCLIHello
18
+
19
+ # hammer commands must be descendants of AbstractCommand
20
+ class HelloCommand < HammerCLI::AbstractCommand
21
+
22
+ # execute is the heart of the command
23
+ def execute
24
+ # we use print_message instead of simple puts
25
+ # the reason will be described later in the part called Output
26
+ print_message "Hello World!"
27
+ end
28
+ end
29
+
30
+ # now plug your command into the hammer's main command
31
+ HammerCLI::MainCommand.subcommand
32
+ 'hello', # command's name
33
+ "Say Hello World!", # description
34
+ HammerCLIHello::HelloCommand # the class
35
+ end
36
+ ```
37
+
38
+ The last bit is to require the file with your command in `hammer_cli_hello.rb`.
39
+ Hammer actually loads this file and this is how the commands from plugins get loaded
40
+ into hammer.
41
+ ```ruby
42
+ # ./lib/hammer_cli_hello.rb
43
+ require 'hammer_cli_hello/hello_world'
44
+ ```
45
+
46
+ Rebuild and reinstall your plugin and see the results of `hammer -h`
47
+ ```
48
+ gem build ./hammer_cli_hello.gemspec && gem install hammer_cli_hello-0.0.1.gem
49
+ ```
50
+
51
+
52
+ ```
53
+ $ hammer -h
54
+ Usage:
55
+ hammer [OPTIONS] SUBCOMMAND [ARG] ...
56
+
57
+ Parameters:
58
+ SUBCOMMAND subcommand
59
+ [ARG] ... subcommand arguments
60
+
61
+ Subcommands:
62
+ shell Interactive Shell
63
+ hello Say Hello World!
64
+
65
+ Options:
66
+ -v, --verbose be verbose
67
+ -c, --config CFG_FILE path to custom config file
68
+ -u, --username USERNAME username to access the remote system
69
+ -p, --password PASSWORD password to access the remote system
70
+ --version show version
71
+ --show-ids Show ids of associated resources
72
+ --csv Output as CSV (same as --adapter=csv)
73
+ --output ADAPTER Set output format. One of [csv, table, base, silent]
74
+ --csv-separator SEPARATOR Character to separate the values
75
+ -P, --ask-pass Ask for password
76
+ --autocomplete LINE Get list of possible endings
77
+ -h, --help print help
78
+ ```
79
+
80
+ Now try running the command.
81
+
82
+ ```
83
+ $ hammer hello
84
+ Hello World!
85
+ Error: exit code must be integer
86
+ ```
87
+
88
+ What's wrong here? Hammer requires integer exit codes as return values from the method `execute`.
89
+ It's usually just `HammerCLI::EX_OK`. Add it as the very last line of `execute`, rebuild and the
90
+ command should run fine.
91
+
92
+ See [exit_codes.rb](https://github.com/theforeman/hammer-cli/blob/master/lib/hammer_cli/exit_codes.rb)
93
+ for the full list of available exit codes.
94
+
95
+
96
+ ### Declaring options
97
+ Our new command has only one option so far. It's `-h` which is built in for every command by default.
98
+ Option declaration is the same as in clamp so please read it's
99
+ [documentation](https://github.com/mdub/clamp/#declaring-options)
100
+ on that topic.
101
+
102
+ Example option usage could go like this:
103
+ ```ruby
104
+ class HelloCommand < HammerCLI::AbstractCommand
105
+
106
+ option '--name', "NAME", "Name of the person you want to greet"
107
+
108
+ def execute
109
+ print_message "Hello %s!" % (name || "World")
110
+ HammerCLI::EX_OK
111
+ end
112
+ end
113
+ ```
114
+
115
+ ```
116
+ $ hammer hello -h
117
+ Usage:
118
+ hammer hello [OPTIONS]
119
+
120
+ Options:
121
+ --name NAME Name of the person you want to greet
122
+ -h, --help print help
123
+ ```
124
+
125
+ ```
126
+ $ hammer hello --name 'Foreman'
127
+ Hello Foreman!
128
+ ```
129
+
130
+
131
+ ### Option validation
132
+ Hammer provides extended functionality for validating options.
133
+
134
+ #### DSL
135
+ First of all there is a dsl for validating combinations of options:
136
+ ```ruby
137
+ validate_options do
138
+ all(:name, :surname).required # requires all the options
139
+ option(:age).required # requires a single option,
140
+ # equivalent of :required => true in option declaration
141
+ any(:email, :phone).required # requires at least one of the options
142
+
143
+ # Tt is possible to create more complicated constructs.
144
+ # This example requires either the full address or nothing
145
+ if any(:street, :city, :zip).exist?
146
+ all(:street, :city, :zip).required
147
+ end
148
+
149
+ # Here you can reject all address related option when --no-address is passed
150
+ if option(:no_address).exist?
151
+ all(:street, :city, :zip).rejected
152
+ end
153
+ end
154
+
155
+ ```
156
+
157
+ #### Option normalizers
158
+ Another option-related feature is a set of normalizers for specific option types. They validate and preprocess
159
+ option values. Each normalizer has a description of the format it accepts. This description is printed
160
+ in commands' help.
161
+
162
+ ##### _List_
163
+
164
+ Parses comma separated strings to a list of values.
165
+
166
+ ```ruby
167
+ option "--users", "USER_NAMES", "List of user names",
168
+ :format => HammerCLI::Options::Normalizers::List.new
169
+ ```
170
+ `--users='J.R.,Gary,Bobby'` -> `['J.R.', 'Gary', 'Bobby']`
171
+
172
+ ##### _File_
173
+
174
+ Loads contents of a file and returns it as a value of the option.
175
+
176
+ ```ruby
177
+ option "--poem", "PATH_TO_POEM", "File containing the text of your poem",
178
+ :format => HammerCLI::Options::Normalizers::File.new
179
+ ```
180
+ `--poem=~/verlaine/les_poetes_maudits.txt` -> content of the file
181
+
182
+ ##### _Bool_
183
+
184
+ Case insensitive true/false values. Translates _yes,y,true,t,1_ to `true` and _no,n,false,f,0_ to `false`.
185
+
186
+ ```ruby
187
+ option "--start", "START", "Start the action",
188
+ :format => HammerCLI::Options::Normalizers::Bool.new
189
+ ```
190
+ `--start=yes` -> `true`
191
+
192
+ ##### _KeyValueList_
193
+
194
+ Parses a comma separated list of key=value pairs. Can be used for naming attributes with vague structure.
195
+
196
+ ```ruby
197
+ option "--attributes", "ATTRIBUTES", "Values of various attributes",
198
+ :format => HammerCLI::Options::Normalizers::KeyValueList.new
199
+ ```
200
+ `--attributes="material=unoptanium,thickness=3"` -> `{'material' => 'unoptanium', 'thickness' => '3'}`
201
+
202
+ ### Adding subcommands
203
+ Commands in the cli can be structured into a tree of parent commands (nodes) and subcommands (leaves).
204
+ Neither the number of subcommands nor the nesting is limited. Please note that no parent command
205
+ can perform any action and therefore it's useless to define `execute` method for them. This limit
206
+ comes from Clamp's implementation of the command hierarchy.
207
+
208
+ We've already used command nesting for plugging the `HelloCommand` command into the main command.
209
+ But let's create a new command `say` and show how to connect it with others to be more demonstrative.
210
+
211
+ ```ruby
212
+ module HammerCLIHello
213
+
214
+ # a new parent command 'say'
215
+ class SayCommand < HammerCLI::AbstractCommand
216
+
217
+ # subcommand 'hello' remains the same
218
+ class HelloCommand < HammerCLI::AbstractCommand
219
+
220
+ option '--name', "NAME", "Name of the person you want to greet"
221
+
222
+ def execute
223
+ print_message "Hello %s!" % (name || "World")
224
+ HammerCLI::EX_OK
225
+ end
226
+ end
227
+
228
+ # plug the original command into 'say'
229
+ subcommand 'hello', "Say Hello World!", HammerCLIHello::SayCommand::HelloCommand
230
+ end
231
+
232
+ # plug the 'say' command into the main command
233
+ HammerCLI::MainCommand.subcommand 'say', "Say something", HammerCLIHello::SayCommand
234
+ end
235
+ ```
236
+
237
+ The result will be:
238
+ ```
239
+ $ hammer say hello
240
+ Hello World!
241
+ ```
242
+
243
+ This is very typical usage of subcommands. When you create more of them it may feel a bit
244
+ duplicit to always define the subcommand structure at the end of the class definition.
245
+ Hammer provides utility methods for subcommand autoloading. This is handy especially
246
+ when you have growing number of subcommands. See how it works in the following example:
247
+
248
+ ```ruby
249
+ module HammerCLIHello
250
+
251
+ class SayCommand < HammerCLI::AbstractCommand
252
+
253
+ class HelloCommand < HammerCLI::AbstractCommand
254
+ command_name 'hello' # name and description moves to the command's class
255
+ desc 'Say Hello World!'
256
+ # ...
257
+ end
258
+
259
+ class HiCommand < HammerCLI::AbstractCommand
260
+ command_name 'hi'
261
+ desc 'Say Hi World!'
262
+ # ...
263
+ end
264
+
265
+ class ByeCommand < HammerCLI::AbstractCommand
266
+ command_name 'bye'
267
+ desc 'Say Bye World!'
268
+ # ...
269
+ end
270
+
271
+ autoload_subcommands
272
+ end
273
+
274
+ HammerCLI::MainCommand.subcommand 'say', "Say something", HammerCLIHello::SayCommand
275
+ end
276
+ ```
277
+
278
+ ```
279
+ $ hammer say
280
+ Usage:
281
+ hammer say [OPTIONS] SUBCOMMAND [ARG] ...
282
+
283
+ Parameters:
284
+ SUBCOMMAND subcommand
285
+ [ARG] ... subcommand arguments
286
+
287
+ Subcommands:
288
+ hi Say Hi World!
289
+ hello Say Hello World!
290
+ bye Say Bye World!
291
+
292
+ Options:
293
+ -h, --help print help
294
+ ```
295
+
296
+
297
+ ### Conflicting subcommands
298
+ It can happen that two different plugins define subcommands with the same name by accident.
299
+ In such situations `subcommand` will throw an exception. If this is intentional and you
300
+ want to redefine the existing command, use `subcommand!`.
301
+ This method does not throw exceptions, replaces the original subcommand, and leaves
302
+ a message in a log for debugging purposes.
303
+
304
+
305
+ ### Removing subcommands
306
+ If your plugin needs to disable existing subcommand, you can use `remove_subcommand` for this.
307
+
308
+ ```ruby
309
+ HammerCLI::MainCommand.remove_subcommand 'say'
310
+ ```
311
+
312
+ Call to this action is automatically logged.
313
+
314
+
315
+ ### Printing some output
316
+ We've mentioned above that it's not recommended practice to print output
317
+ directly with `puts` in Hammer. The reason is we separate definition
318
+ of the output from its interpretation. Hammer uses so called _output adapters_
319
+ that can modify the output format.
320
+
321
+ Hammer comes with four basic output adapters:
322
+ * __base__ - simple output, structured records
323
+ * __table__ - records printed in tables, ideal for printing lists of records
324
+ * __csv__ - comma separated output, ideal for scripting and grepping
325
+ * __silent__ - no output, used for testing
326
+
327
+ The detailed documentation on creating adapters is coming soon.
328
+
329
+ #### Printing messages
330
+ Very simple, just call
331
+ ```ruby
332
+ print_message(msg)
333
+ ```
334
+
335
+ #### Printing hash records
336
+ Typical usage of a cli is interaction with some api. In many cases it's listing
337
+ some records returned by the api.
338
+
339
+ Hammer comes with support for selecting and formatting of hash record fields.
340
+ You first create so called _output definition_ that you apply on your data. The result
341
+ is a collection of fields each having its type. The collection is then passed to some
342
+ _output adapter_ which handles the actuall formatting and printing.
343
+
344
+ Hammer provides a DSL for defining the output. Next rather complex example will
345
+ explain how to use it in action.
346
+
347
+ Imagine there's an API of some service that returns list of users:
348
+ ```ruby
349
+ [{
350
+ :id => 1,
351
+ :email => 'tom@email.com',
352
+ :phone => '123456111',
353
+ :first_name => 'Tom',
354
+ :last_name => 'Sawyer',
355
+ :roles => ['Admin', 'Editor'],
356
+ :timestamps => {
357
+ :created => '2012-12-18T15:24:42Z',
358
+ :updated => '2012-12-18T15:24:42Z'
359
+ }
360
+ },{
361
+ :id => 2,
362
+ :email => 'huckleberry@email.com',
363
+ :phone => '123456222',
364
+ :first_name => 'Huckleberry',
365
+ :last_name => 'Finn',
366
+ :roles => ['Admin'],
367
+ :timestamps => {
368
+ :created => '2012-12-18T15:25:00Z',
369
+ :updated => '2012-12-20T14:00:15Z'
370
+ }
371
+ }]
372
+ ```
373
+
374
+ We can create an output definition that selects and formats some of the fields:
375
+ ```ruby
376
+ class Command < HammerCLI::AbstractCommand
377
+
378
+ output do
379
+ # Simple field with a label. The first parameter is key in the printed hash.
380
+ field :id, 'ID'
381
+
382
+ # Fields can have types. The type determines how the field is printed.
383
+ # All available types are listed below.
384
+ # Here we want the roles to act as list.
385
+ field :roles, 'System Roles', Fields::List
386
+
387
+ # Label is used for grouping fields.
388
+ label 'Contacts' do
389
+ field :email, 'Email'
390
+ field :phone, 'Phone No.'
391
+ end
392
+
393
+ # From is used for accessing nested fields.
394
+ from :timestamps do
395
+ # See how date gets formatted in the output
396
+ field :created, 'Created At', Fields::Date
397
+ end
398
+ end
399
+
400
+ def execute
401
+ records = retrieve_data
402
+ print_records( # <- printing utility of AbstractCommand
403
+ output_definition, # <- method for accessing fields defined in the block 'output'
404
+ records # <- the data to print
405
+ )
406
+ return HammerCLI::EX_OK
407
+ end
408
+
409
+ end
410
+ ```
411
+
412
+ Using the base adapter the output will look like:
413
+ ```
414
+ ID: 1
415
+ System Roles: Admin, Editor
416
+ Name: Tom Sawyer
417
+ Contacts:
418
+ Email: tom@email.com
419
+ Phone No.: 123456111
420
+ Created At: 2012/12/18 15:24:42
421
+
422
+ ID: 2
423
+ System Roles: Admin
424
+ Name: Huckleberry Finn
425
+ Contacts:
426
+ Email: huckleberry@email.com
427
+ Phone No.: 123456222
428
+ Created At: 2012/12/18 15:25:00
429
+ ```
430
+
431
+ You can optionally use output definition from another command as a base and extend it with
432
+ additional fields. This is helpful when there are two commands, one listing brief data and
433
+ another one showing details. Typically it's list and show.
434
+ ```ruby
435
+ class ShowCommand < HammerCLI::AbstractCommand
436
+
437
+ output ListCommand.output_definition do
438
+ # additional fields
439
+ end
440
+
441
+ # ...
442
+ end
443
+ ```
444
+
445
+
446
+ All Hammer field types are:
447
+ * __Date__
448
+ * __Id__ - Used to mark ID values, current print adapters have support for turning id printing on/off.
449
+ See hammer's parameter `--show-ids`.
450
+ * __List__
451
+ * __KeyValue__ - Formats hashes containing `:name` and `:value`
452
+ * __Collection__ - Enables to render subcollections. Takes a block with another output definition.
453
+
454
+ The default adapter for every command is Base adapter. It is possible to override
455
+ the default one by redefining command's method `adapter`.
456
+
457
+ ```ruby
458
+ def adapter
459
+ # return :base, :table, :csv or name of your own adapter here
460
+ :table
461
+ end
462
+ ```
463
+
464
+
465
+ Other useful command features
466
+ -----------------------------
467
+
468
+ #### Logging
469
+ Hammer provides integrated [logger](https://github.com/TwP/logging)
470
+ with broad setting options (use hammer's config file):
471
+
472
+ ```yaml
473
+ :log_dir: '<path>' # - directory where the logs are stored.
474
+ # The default is /var/log/foreman/ and the log file is named hammer.log
475
+ :log_level: '<level>' # - logging level. One of debug, info, warning, error, fatal
476
+ :log_owner: '<owner>' # - logfile owner
477
+ :log_group: '<group>' # - logfile group
478
+ :log_size: 1048576 # - size in bytes, when exceeded the log rotates. Default is 1MB
479
+ :watch_plain: false # - turn on/off syntax highlighting of data being logged in debug mode
480
+ ```
481
+
482
+ Example usage in commands:
483
+ ```ruby
484
+ # Get a logger instance
485
+ logger('Logger name')
486
+
487
+ # It uses a command class name as the logger's name by default
488
+ logger
489
+
490
+ # Log a message at corresponding log level
491
+ logger.debug("...")
492
+ logger.error("...")
493
+ logger.info("...")
494
+ logger.fatal("...")
495
+ logger.warn("...")
496
+
497
+ # Writes an awesome print dump of a value to the log
498
+ logger.watch('Some label', value)
499
+ ```
500
+
501
+ #### Exception handling
502
+ Exception handling in Hammer is centralized by
503
+ [ExceptionHandler](https://github.com/theforeman/hammer-cli/blob/master/lib/hammer_cli/exception_handler.rb).
504
+ Each plugin, module or even a command can have a separate exception handler. The exception handler class
505
+ is looked up in the module structure from a command to the top level.
506
+
507
+ Define method `self.exception_handler_class` in your plugin's module to use a custom exception handler:
508
+ ```ruby
509
+ # ./lib/hammer_cli_hello.rb
510
+
511
+ module HammerCLIHello
512
+
513
+ def self.exception_handler_class
514
+ HammerCLIHello::CustomExceptionHandler
515
+ end
516
+ end
517
+
518
+ require 'hammer_cli_hello/hello_world'
519
+ ```
520
+
521
+ Centralized exception handling implies that you should raise exceptions on error states in your command
522
+ rather than handle it and return error codes. This approach guarrantees that error messages are logged and
523
+ printed consistently and correct exit codes are returned.
524
+
525
+
526
+ #### Configuration
527
+ Values form config files are accesible via class `HammerCLI::Settings`.
528
+ It's method `get` returns either the value or nil when it's not found.
529
+
530
+ Config values belonging to a specific plugin must be nested under
531
+ the plugin's name in config files.
532
+
533
+ ```yaml
534
+ #cli_config.yml
535
+ :log_dir: /var/log/hammer/
536
+ :hello_world:
537
+ :name: John
538
+ ```
539
+
540
+ ```ruby
541
+ HammerCLI::Settings.get(:log_dir) # get a value
542
+ HammerCLI::Settings.get(:hello_world, :name) # get a nested value
543
+ ```
544
+
545
+ There's more ways where to place your config file for hammer.
546
+ Read more in [the settings howto](https://github.com/theforeman/hammer-cli#configuration).
547
+