hammer_cli 0.0.9 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+