gli 2.11.0 → 2.20.0

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 (92) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +28 -0
  3. data/.gitignore +3 -3
  4. data/.tool-versions +1 -0
  5. data/Gemfile +0 -2
  6. data/README.rdoc +29 -19
  7. data/Rakefile +15 -37
  8. data/bin/ci +29 -0
  9. data/bin/gli +24 -54
  10. data/bin/rake +29 -0
  11. data/bin/setup +5 -0
  12. data/exe/gli +68 -0
  13. data/gli.gemspec +20 -24
  14. data/gli.rdoc +9 -9
  15. data/lib/gli/app.rb +31 -8
  16. data/lib/gli/app_support.rb +15 -3
  17. data/lib/gli/command.rb +24 -2
  18. data/lib/gli/command_finder.rb +42 -25
  19. data/lib/gli/command_support.rb +7 -6
  20. data/lib/gli/commands/doc.rb +9 -3
  21. data/lib/gli/commands/help.rb +2 -1
  22. data/lib/gli/commands/help_modules/arg_name_formatter.rb +2 -2
  23. data/lib/gli/commands/help_modules/command_help_format.rb +19 -1
  24. data/lib/gli/commands/help_modules/full_synopsis_formatter.rb +3 -2
  25. data/lib/gli/commands/help_modules/global_help_format.rb +1 -1
  26. data/lib/gli/commands/help_modules/options_formatter.rb +4 -6
  27. data/lib/gli/commands/initconfig.rb +3 -6
  28. data/lib/gli/commands/rdoc_document_listener.rb +2 -1
  29. data/lib/gli/commands/scaffold.rb +71 -142
  30. data/lib/gli/dsl.rb +2 -1
  31. data/lib/gli/flag.rb +23 -2
  32. data/lib/gli/gli_option_parser.rb +66 -15
  33. data/lib/gli/option_parser_factory.rb +9 -2
  34. data/lib/gli/options.rb +2 -2
  35. data/lib/gli/switch.rb +4 -0
  36. data/lib/gli/terminal.rb +6 -2
  37. data/lib/gli/version.rb +1 -1
  38. data/lib/gli.rb +1 -0
  39. data/object-model.dot +29 -0
  40. data/object-model.png +0 -0
  41. data/test/apps/todo/Gemfile +1 -1
  42. data/test/apps/todo/bin/todo +12 -6
  43. data/test/apps/todo/lib/todo/commands/create.rb +42 -41
  44. data/test/apps/todo/lib/todo/commands/list.rb +48 -36
  45. data/test/apps/todo/lib/todo/commands/ls.rb +25 -24
  46. data/test/apps/todo/lib/todo/commands/make.rb +42 -39
  47. data/test/apps/todo/todo.gemspec +1 -2
  48. data/test/apps/todo_legacy/todo.gemspec +1 -2
  49. data/test/apps/todo_plugins/commands/third.rb +2 -0
  50. data/test/integration/gli_cli_test.rb +69 -0
  51. data/test/integration/gli_powered_app_test.rb +52 -0
  52. data/test/integration/scaffold_test.rb +30 -0
  53. data/test/integration/test_helper.rb +52 -0
  54. data/test/unit/command_finder_test.rb +54 -0
  55. data/test/{tc_command.rb → unit/command_test.rb} +20 -7
  56. data/test/unit/compound_command_test.rb +17 -0
  57. data/test/{tc_doc.rb → unit/doc_test.rb} +38 -51
  58. data/test/{tc_flag.rb → unit/flag_test.rb} +19 -25
  59. data/test/{tc_gli.rb → unit/gli_test.rb} +78 -50
  60. data/test/{tc_help.rb → unit/help_test.rb} +54 -113
  61. data/test/{tc_options.rb → unit/options_test.rb} +4 -4
  62. data/test/unit/subcommand_parsing_test.rb +263 -0
  63. data/test/unit/subcommands_test.rb +245 -0
  64. data/test/{config.yaml → unit/support/gli_test_config.yml} +1 -0
  65. data/test/unit/switch_test.rb +49 -0
  66. data/test/{tc_terminal.rb → unit/terminal_test.rb} +28 -3
  67. data/test/unit/test_helper.rb +13 -0
  68. data/test/unit/verbatim_wrapper_test.rb +24 -0
  69. metadata +85 -141
  70. data/.ruby-gemset +0 -1
  71. data/.ruby-version +0 -1
  72. data/.travis.yml +0 -12
  73. data/ObjectModel.graffle +0 -1191
  74. data/bin/report_on_rake_results +0 -10
  75. data/bin/test_all_rubies.sh +0 -6
  76. data/features/gli_executable.feature +0 -90
  77. data/features/gli_init.feature +0 -232
  78. data/features/step_definitions/gli_executable_steps.rb +0 -18
  79. data/features/step_definitions/gli_init_steps.rb +0 -11
  80. data/features/step_definitions/todo_steps.rb +0 -100
  81. data/features/support/env.rb +0 -55
  82. data/features/todo.feature +0 -546
  83. data/features/todo_legacy.feature +0 -128
  84. data/test/option_test_helper.rb +0 -13
  85. data/test/tc_compound_command.rb +0 -22
  86. data/test/tc_subcommand_parsing.rb +0 -104
  87. data/test/tc_subcommands.rb +0 -259
  88. data/test/tc_switch.rb +0 -55
  89. data/test/tc_verbatim_wrapper.rb +0 -36
  90. data/test/test_helper.rb +0 -20
  91. /data/test/{init_simplecov.rb → unit/init_simplecov.rb} +0 -0
  92. /data/test/{fake_std_out.rb → unit/support/fake_std_out.rb} +0 -0
@@ -26,8 +26,10 @@ module GLI
26
26
  @pre_block = false
27
27
  @post_block = false
28
28
  @default_command = :help
29
+ @autocomplete = false
29
30
  @around_block = nil
30
31
  @subcommand_option_handling_strategy = :legacy
32
+ @argument_handling_strategy = :loose
31
33
  clear_nexts
32
34
  end
33
35
 
@@ -67,8 +69,10 @@ module GLI
67
69
  flags,
68
70
  switches,
69
71
  accepts,
70
- @default_command,
71
- self.subcommand_option_handling_strategy)
72
+ :default_command => @default_command,
73
+ :autocomplete => autocomplete,
74
+ :subcommand_option_handling_strategy => subcommand_option_handling_strategy,
75
+ :argument_handling_strategy => argument_handling_strategy)
72
76
 
73
77
  parsing_result = gli_option_parser.parse_options(args)
74
78
  parsing_result.convert_to_openstruct! if @use_openstruct
@@ -197,14 +201,22 @@ module GLI
197
201
 
198
202
  def override_default(tokens,config)
199
203
  tokens.each do |name,token|
200
- token.default_value=config[name] if config[name]
204
+ token.default_value=config[name] unless config[name].nil?
201
205
  end
202
206
  end
203
207
 
208
+ def argument_handling_strategy
209
+ @argument_handling_strategy || :loose
210
+ end
211
+
204
212
  def subcommand_option_handling_strategy
205
213
  @subcommand_option_handling_strategy || :legacy
206
214
  end
207
215
 
216
+ def autocomplete
217
+ @autocomplete.nil? ? true : @autocomplete
218
+ end
219
+
208
220
  private
209
221
 
210
222
  def handle_exception(ex,command)
data/lib/gli/command.rb CHANGED
@@ -30,8 +30,16 @@ module GLI
30
30
  include DSL
31
31
  include CommandSupport
32
32
 
33
- # Key in an options hash to find the parent's parsed options
34
- PARENT = Object.new
33
+ class ParentKey
34
+ def to_sym
35
+ "__parent__".to_sym
36
+ end
37
+ end
38
+
39
+ # Key in an options hash to find the parent's parsed options. Note that if you are
40
+ # using openstruct, e.g. via `use_openstruct true` in your app setup, you will need
41
+ # to use the method `__parent__` to access parent parsed options.
42
+ PARENT = ParentKey.new
35
43
 
36
44
  # Create a new command.
37
45
  #
@@ -45,6 +53,8 @@ module GLI
45
53
  # +skips_post+:: if true, this command advertises that it doesn't want the post block called after it
46
54
  # +skips_around+:: if true, this command advertises that it doesn't want the around block called
47
55
  # +hide_commands_without_desc+:: if true and there isn't a description the command is not going to be shown in the help
56
+ # +examples+:: An array of Hashes, where each hash must have the key +:example+ mapping to a string, and may optionally have the key +:desc+
57
+ # that documents that example.
48
58
  def initialize(options)
49
59
  super(options[:names],options[:description],options[:long_desc])
50
60
  @arguments_description = options[:arguments_name] || ''
@@ -57,9 +67,21 @@ module GLI
57
67
  @commands_declaration_order = []
58
68
  @flags_declaration_order = []
59
69
  @switches_declaration_order = []
70
+ @examples = options[:examples] || []
60
71
  clear_nexts
61
72
  end
62
73
 
74
+ # Specify an example invocation.
75
+ #
76
+ # example_invocation:: test of a complete command-line invocation you want to show
77
+ # options:: refine the example:
78
+ # +:desc+:: A description of the example to be shown with it (optional)
79
+ def example(example_invocation,options = {})
80
+ @examples << {
81
+ example: example_invocation
82
+ }.merge(options)
83
+ end
84
+
63
85
  # Set the default command if this command has subcommands and the user doesn't
64
86
  # provide a subcommand when invoking THIS command. When nil, this will show an error and the help
65
87
  # for this command; when set, the command with this name will be executed.
@@ -1,40 +1,57 @@
1
1
  module GLI
2
2
  class CommandFinder
3
- # Initialize a finder on the given list of commands, using default_command as the default if none found
4
- def initialize(commands,default_command)
5
- @default_command = default_command
6
- @names_to_commands = {}
7
- commands.each do |command_name,command|
8
- @names_to_commands[command_name.to_s] = command
9
- Array(command.aliases).each do |command_alias|
10
- @names_to_commands[command_alias.to_s] = command
11
- end
12
- end
3
+ attr_accessor :options
4
+
5
+ DEFAULT_OPTIONS = {
6
+ :default_command => nil,
7
+ :autocomplete => true
8
+ }
9
+
10
+ def initialize(commands, options = {})
11
+ self.options = DEFAULT_OPTIONS.merge(options)
12
+ self.commands_with_aliases = expand_with_aliases(commands)
13
13
  end
14
14
 
15
- # Finds the command with the given name, allowing for partial matches. Returns the command named by
16
- # the default command if no command with +name+ matched
17
15
  def find_command(name)
18
- name ||= @default_command
19
-
20
- raise UnknownCommand.new("No command name given nor default available") if String(name).strip == ''
16
+ name = String(name || options[:default_command]).strip
17
+ raise UnknownCommand.new("No command name given nor default available") if name == ''
21
18
 
22
- command_found = @names_to_commands.fetch(name.to_s) do |command_to_match|
23
- find_command_by_partial_name(@names_to_commands, command_to_match)
24
- end
25
- if Array(command_found).empty?
26
- raise UnknownCommand.new("Unknown command '#{name}'")
27
- elsif command_found.kind_of? Array
28
- raise AmbiguousCommand.new("Ambiguous command '#{name}'. It matches #{command_found.sort.join(',')}")
19
+ command_found = commands_with_aliases.fetch(name) do |command_to_match|
20
+ if options[:autocomplete]
21
+ found_match = find_command_by_partial_name(commands_with_aliases, command_to_match)
22
+ if found_match.kind_of? GLI::Command
23
+ if ENV["GLI_DEBUG"] == 'true'
24
+ $stderr.puts "Using '#{name}' as it's is short for #{found_match.name}."
25
+ $stderr.puts "Set autocomplete false for any command you don't want matched like this"
26
+ end
27
+ elsif found_match.kind_of?(Array) && !found_match.empty?
28
+ raise AmbiguousCommand.new("Ambiguous command '#{name}'. It matches #{found_match.sort.join(',')}")
29
+ end
30
+ found_match
31
+ end
29
32
  end
33
+
34
+ raise UnknownCommand.new("Unknown command '#{name}'") if Array(command_found).empty?
30
35
  command_found
31
36
  end
32
37
 
33
38
  private
39
+ attr_accessor :commands_with_aliases
40
+
41
+ def expand_with_aliases(commands)
42
+ expanded = {}
43
+ commands.each do |command_name, command|
44
+ expanded[command_name.to_s] = command
45
+ Array(command.aliases).each do |command_alias|
46
+ expanded[command_alias.to_s] = command
47
+ end
48
+ end
49
+ expanded
50
+ end
34
51
 
35
- def find_command_by_partial_name(names_to_commands, command_to_match)
36
- partial_matches = names_to_commands.keys.select { |command_name| command_name =~ /^#{command_to_match}/ }
37
- return names_to_commands[partial_matches[0]] if partial_matches.size == 1
52
+ def find_command_by_partial_name(commands_with_aliases, command_to_match)
53
+ partial_matches = commands_with_aliases.keys.select { |command_name| command_name =~ /^#{command_to_match}/ }
54
+ return commands_with_aliases[partial_matches[0]] if partial_matches.size == 1
38
55
  partial_matches
39
56
  end
40
57
  end
@@ -49,6 +49,11 @@ module GLI
49
49
  all_forms
50
50
  end
51
51
 
52
+ # Returns the array of examples
53
+ def examples
54
+ @examples
55
+ end
56
+
52
57
  # Get an array of commands, ordered by when they were declared
53
58
  def commands_declaration_order # :nodoc:
54
59
  @commands_declaration_order
@@ -161,12 +166,8 @@ module GLI
161
166
 
162
167
  def generate_error_action(arguments)
163
168
  lambda { |global_options,options,arguments|
164
- if am_subcommand?
165
- if arguments.size > 0
166
- raise UnknownCommand,"Unknown command '#{arguments[0]}'"
167
- else
168
- raise BadCommandLine,"Command '#{name}' requires a subcommand"
169
- end
169
+ if am_subcommand? && arguments.size > 0
170
+ raise UnknownCommand,"Unknown command '#{arguments[0]}'"
170
171
  elsif have_subcommands?
171
172
  raise BadCommandLine,"Command '#{name}' requires a subcommand #{self.commands.keys.join(',')}"
172
173
  else
@@ -4,7 +4,7 @@ module GLI
4
4
  # about your app, so as to create documentation in whatever format you want
5
5
  class Doc < Command
6
6
  FORMATS = {
7
- 'rdoc' => GLI::Commands::RdocDocumentListener,
7
+ 'rdoc' => GLI::Commands::RdocDocumentListener
8
8
  }
9
9
  # Create the Doc generator based on the GLI app passed in
10
10
  def initialize(app)
@@ -139,7 +139,7 @@ module GLI
139
139
  private
140
140
 
141
141
  def format_class(format_name)
142
- FORMATS.fetch(format_name) {
142
+ FORMATS.fetch(format_name) {
143
143
  begin
144
144
  return format_name.split(/::/).reduce(Kernel) { |context,part| context.const_get(part) }
145
145
  rescue => ex
@@ -168,8 +168,14 @@ module GLI
168
168
  command.description,
169
169
  command.long_description,
170
170
  command.arguments_description]
171
- if document_listener.method(:command).arity == 6
171
+ if document_listener.method(:command).arity >= 6
172
172
  command_args << command.arguments_options
173
+ if document_listener.method(:command).arity >= 7
174
+ command_args << command.arguments
175
+ end
176
+ if document_listener.method(:command).arity >= 8
177
+ command_args << command.examples
178
+ end
173
179
  end
174
180
  document_listener.command(*command_args)
175
181
  end
@@ -59,7 +59,8 @@ module GLI
59
59
  super(:names => :help,
60
60
  :description => 'Shows a list of commands or help for one command',
61
61
  :arguments_name => 'command',
62
- :long_desc => 'Gets help for the application or its commands. Can also list the commands in a way helpful to creating a bash-style completion function')
62
+ :long_desc => 'Gets help for the application or its commands. Can also list the commands in a way helpful to creating a bash-style completion function',
63
+ :arguments => [Argument.new(:command_name, [:multiple, :optional])])
63
64
  @app = app
64
65
  @parent = app
65
66
  @sorter = SORTERS[@app.help_sort_type]
@@ -22,7 +22,7 @@ module GLI
22
22
  arg_desc = "[#{arg_desc}]"
23
23
  end
24
24
  if arg.multiple?
25
- arg_desc = "#{arg_desc}[, #{arg_desc}]*"
25
+ arg_desc = "#{arg_desc}..."
26
26
  end
27
27
  desc = desc + " " + arg_desc
28
28
  end
@@ -37,7 +37,7 @@ module GLI
37
37
  desc = "[#{desc}]"
38
38
  end
39
39
  if arguments_options.include? :multiple
40
- desc = "#{desc}[, #{desc}]*"
40
+ desc = "#{desc}..."
41
41
  end
42
42
  " " + desc
43
43
  end
@@ -18,6 +18,7 @@ module GLI
18
18
 
19
19
  options_description = OptionsFormatter.new(flags_and_switches(@command,@app),@sorter,@wrapper_class).format
20
20
  commands_description = format_subcommands(@command)
21
+ command_examples = format_examples(@command)
21
22
 
22
23
  synopses = @synopsis_formatter.synopses_for_command(@command)
23
24
  COMMAND_HELP.result(binding)
@@ -45,7 +46,14 @@ COMMAND OPTIONS
45
46
 
46
47
  COMMANDS
47
48
  <%= commands_description %>
48
- <% end %>),nil,'<>')
49
+ <% end %>
50
+ <% unless @command.examples.empty? %>
51
+
52
+ <%= @command.examples.size == 1 ? 'EXAMPLE' : 'EXAMPLES' %>
53
+
54
+
55
+ <%= command_examples %>
56
+ <% end %>))
49
57
 
50
58
 
51
59
  def flags_and_switches(command,app)
@@ -76,6 +84,16 @@ COMMANDS
76
84
  formatter = ListFormatter.new(commands_array,@wrapper_class)
77
85
  StringIO.new.tap { |io| formatter.output(io) }.string
78
86
  end
87
+
88
+ def format_examples(command)
89
+ command.examples.map {|example|
90
+ string = ""
91
+ if example[:desc]
92
+ string << " # #{example[:desc]}\n"
93
+ end
94
+ string << " #{example.fetch(:example)}\n"
95
+ }.join("\n")
96
+ end
79
97
  end
80
98
  end
81
99
  end
@@ -27,10 +27,11 @@ module GLI
27
27
 
28
28
  def sub_options_doc(sub_options)
29
29
  sub_options_doc = sub_options.map { |_,option|
30
- option.names_and_aliases.map { |name|
30
+ doc = option.names_and_aliases.map { |name|
31
31
  CommandLineOption.name_as_string(name,false) + (option.kind_of?(Flag) ? " #{option.argument_name }" : '')
32
32
  }.join('|')
33
- }.map { |invocations| "[#{invocations}]" }.sort.join(' ').strip
33
+ option.required?? doc : "[#{doc}]"
34
+ }.sort.join(' ').strip
34
35
  end
35
36
 
36
37
  private
@@ -51,7 +51,7 @@ GLOBAL OPTIONS
51
51
 
52
52
  <% end %>
53
53
  COMMANDS
54
- <%= commands %>),nil,'<>')
54
+ <%= commands %>))
55
55
 
56
56
  def global_flags_and_switches
57
57
  @app.flags_declaration_order + @app.switches_declaration_order
@@ -24,12 +24,10 @@ module GLI
24
24
 
25
25
  def description_with_default(option)
26
26
  if option.kind_of? Flag
27
- required = if option.required?
28
- 'required, '
29
- else
30
- ''
31
- end
32
- String(option.description) + " (#{required}default: #{option.safe_default_value || 'none'})"
27
+ required = option.required? ? 'required, ' : ''
28
+ multiple = option.multiple? ? 'may be used more than once, ' : ''
29
+
30
+ String(option.description) + " (#{required}#{multiple}default: #{option.safe_default_value || 'none'})"
33
31
  else
34
32
  String(option.description) + (option.default_value ? " (default: enabled)" : "")
35
33
  end
@@ -35,12 +35,9 @@ module GLI
35
35
  private
36
36
 
37
37
  def create_config(global_options,options,arguments)
38
- config = Hash[global_options.map { |option_name,option_value|
39
- if option_value.kind_of?(String) && option_value.respond_to?(:force_encoding)
40
- [option_name,option_value.force_encoding("utf-8")]
41
- else
42
- [option_name,option_value]
43
- end
38
+ config = Hash[(@app_switches.keys + @app_flags.keys).map { |option_name|
39
+ option_value = global_options[option_name]
40
+ [option_name,option_value]
44
41
  }]
45
42
  config[COMMANDS_KEY] = {}
46
43
  @app_commands.each do |name,command|
@@ -5,6 +5,7 @@ module GLI
5
5
  class RdocDocumentListener
6
6
 
7
7
  def initialize(global_options,options,arguments,app)
8
+ @app = app
8
9
  @io = File.new("#{app.exe_name}.rdoc",'w')
9
10
  @nest = ''
10
11
  @arg_name_formatter = GLI::Commands::HelpModules::ArgNameFormatter.new
@@ -81,7 +82,7 @@ module GLI
81
82
 
82
83
  # Gives you a command in the current context and creates a new context of this command
83
84
  def command(name,aliases,desc,long_desc,arg_name,arg_options)
84
- @io.puts "#{@nest}=== Command: <tt>#{([name] + aliases).join('|')} #{@arg_name_formatter.format(arg_name,arg_options)}</tt>"
85
+ @io.puts "#{@nest}=== Command: <tt>#{([name] + aliases).join('|')} #{@arg_name_formatter.format(arg_name,arg_options,[])}</tt>"
85
86
  @io.puts String(desc).strip
86
87
  @io.puts
87
88
  @io.puts String(long_desc).strip