gli 2.11.0 → 2.20.0

Sign up to get free protection for your applications and to get access to all the features.
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