gli 1.6.0 → 2.0.0.rc3

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 (78) hide show
  1. data/.gitignore +11 -0
  2. data/.rvmrc +1 -0
  3. data/.travis.yml +10 -0
  4. data/Gemfile +8 -0
  5. data/LICENSE.txt +201 -0
  6. data/ObjectModel.graffle +1191 -0
  7. data/README.rdoc +60 -10
  8. data/Rakefile +145 -0
  9. data/bin/gli +12 -30
  10. data/bin/report_on_rake_results +10 -0
  11. data/bin/test_all_rubies.sh +6 -0
  12. data/features/gli_executable.feature +84 -0
  13. data/features/gli_init.feature +219 -0
  14. data/features/step_definitions/gli_executable_steps.rb +12 -0
  15. data/features/step_definitions/gli_init_steps.rb +11 -0
  16. data/features/step_definitions/todo_steps.rb +69 -0
  17. data/features/support/env.rb +49 -0
  18. data/features/todo.feature +182 -0
  19. data/gli.cheat +95 -0
  20. data/gli.gemspec +34 -0
  21. data/lib/gli.rb +11 -571
  22. data/lib/gli/app.rb +184 -0
  23. data/lib/gli/app_support.rb +226 -0
  24. data/lib/gli/command.rb +107 -95
  25. data/lib/gli/command_line_option.rb +34 -0
  26. data/lib/gli/command_line_token.rb +13 -9
  27. data/lib/gli/command_support.rb +200 -0
  28. data/lib/gli/commands/compound_command.rb +42 -0
  29. data/lib/gli/commands/help.rb +63 -0
  30. data/lib/gli/commands/help_modules/command_help_format.rb +134 -0
  31. data/lib/gli/commands/help_modules/global_help_format.rb +61 -0
  32. data/lib/gli/commands/help_modules/list_formatter.rb +22 -0
  33. data/lib/gli/commands/help_modules/options_formatter.rb +50 -0
  34. data/lib/gli/commands/help_modules/text_wrapper.rb +53 -0
  35. data/lib/gli/commands/initconfig.rb +67 -0
  36. data/lib/{support → gli/commands}/scaffold.rb +150 -34
  37. data/lib/gli/dsl.rb +194 -0
  38. data/lib/gli/exceptions.rb +13 -4
  39. data/lib/gli/flag.rb +30 -41
  40. data/lib/gli/gli_option_parser.rb +98 -0
  41. data/lib/gli/option_parser_factory.rb +44 -0
  42. data/lib/gli/options.rb +2 -1
  43. data/lib/gli/switch.rb +19 -51
  44. data/lib/gli/terminal.rb +30 -20
  45. data/lib/gli/version.rb +5 -0
  46. data/test/apps/README.md +2 -0
  47. data/test/apps/todo/Gemfile +2 -0
  48. data/test/apps/todo/README.rdoc +6 -0
  49. data/test/apps/todo/Rakefile +23 -0
  50. data/test/apps/todo/bin/todo +52 -0
  51. data/test/apps/todo/lib/todo/commands/create.rb +22 -0
  52. data/test/apps/todo/lib/todo/commands/list.rb +53 -0
  53. data/test/apps/todo/lib/todo/commands/ls.rb +47 -0
  54. data/test/apps/todo/lib/todo/version.rb +3 -0
  55. data/test/apps/todo/test/tc_nothing.rb +14 -0
  56. data/test/apps/todo/todo.gemspec +23 -0
  57. data/test/apps/todo/todo.rdoc +5 -0
  58. data/test/config.yaml +10 -0
  59. data/test/fake_std_out.rb +30 -0
  60. data/test/gli.reek +122 -0
  61. data/test/init_simplecov.rb +8 -0
  62. data/test/option_test_helper.rb +13 -0
  63. data/test/roodi.yaml +18 -0
  64. data/test/tc_command.rb +260 -0
  65. data/test/tc_compount_command.rb +22 -0
  66. data/test/tc_flag.rb +56 -0
  67. data/test/tc_gli.rb +611 -0
  68. data/test/tc_help.rb +223 -0
  69. data/test/tc_options.rb +31 -0
  70. data/test/tc_subcommands.rb +162 -0
  71. data/test/tc_switch.rb +57 -0
  72. data/test/tc_terminal.rb +97 -0
  73. data/test/test_helper.rb +13 -0
  74. metadata +318 -49
  75. data/lib/gli_version.rb +0 -3
  76. data/lib/support/help.rb +0 -179
  77. data/lib/support/initconfig.rb +0 -34
  78. data/lib/support/rdoc.rb +0 -119
data/lib/gli/dsl.rb ADDED
@@ -0,0 +1,194 @@
1
+ module GLI
2
+ # The primary DSL for GLI. This represents the methods shared between your top-level app and
3
+ # the commands. See GLI::Command for additional methods that apply only to command objects.
4
+ module DSL
5
+ # Describe the next switch, flag, or command. This should be a
6
+ # short, one-line description
7
+ #
8
+ # +description+:: A String of the short descripiton of the switch, flag, or command following
9
+ def desc(description); @next_desc = description; end
10
+ alias :d :desc
11
+
12
+ # Provide a longer, more detailed description. This
13
+ # will be reformatted and wrapped to fit in the terminal's columns
14
+ #
15
+ # +long_desc+:: A String that is s longer description of the switch, flag, or command following.
16
+ def long_desc(long_desc); @next_long_desc = long_desc; end
17
+
18
+ # Describe the argument name of the next flag. It's important to keep
19
+ # this VERY short and, ideally, without any spaces (see Example).
20
+ #
21
+ # +name+:: A String that *briefly* describes the argument given to the following command or flag.
22
+ #
23
+ # Example:
24
+ # desc 'Set the filename'
25
+ # arg_name 'file_name'
26
+ # flag [:f,:filename]
27
+ #
28
+ # Produces:
29
+ # -f, --filename=file_name Set the filename
30
+ def arg_name(name); @next_arg_name = name; end
31
+
32
+ # set the default value of the next flag
33
+ #
34
+ # +val+:: A String reprensenting the default value to be used for the following flag if the user doesn't specify one
35
+ # and, when using a config file, the config also doesn't specify one
36
+ def default_value(val); @next_default_value = val; end
37
+
38
+ # Create a flag, which is a switch that takes an argument
39
+ #
40
+ # +names+:: a String or Symbol, or an Array of String or Symbol that represent all the different names
41
+ # and aliases for this flag. The last element can be a hash of options:
42
+ # +:desc+:: the description, instead of using #desc
43
+ # +:long_desc+:: the long_description, instead of using #long_desc
44
+ # +:default_value+:: the default value, instead of using #default_value
45
+ # +:arg_name+:: the arg name, instead of using #arg_name
46
+ # +:must_match+:: A regexp that the flag's value must match
47
+ # +:type+:: A Class (or object you passed to GLI::App#accept) to trigger type coversion
48
+ #
49
+ # Example:
50
+ #
51
+ # desc 'Set the filename'
52
+ # flag [:f,:filename,'file-name']
53
+ #
54
+ # flag :ipaddress, :desc => "IP Address", :must_match => /\d+\.\d+\.\d+\.\d+/
55
+ #
56
+ # flag :names, :desc => "list of names", :type => Array
57
+ #
58
+ # Produces:
59
+ #
60
+ # -f, --filename, --file-name=arg Set the filename
61
+ def flag(*names)
62
+ options = extract_options(names)
63
+ names = [names].flatten
64
+
65
+ verify_unused(names)
66
+ flag = Flag.new(names,options)
67
+ flags[flag.name] = flag
68
+
69
+ clear_nexts
70
+ flag
71
+ end
72
+ alias :f :flag
73
+
74
+ # Create a switch, which is a command line flag that takes no arguments (thus, it _switches_ something on)
75
+ #
76
+ # +names+:: a String or Symbol, or an Array of String or Symbol that represent all the different names
77
+ # and aliases for this switch. The last element can be a hash of options:
78
+ # +:desc+:: the description, instead of using #desc
79
+ # +:long_desc+:: the long_description, instead of using #long_desc
80
+ # +:negatable+:: if true, this switch will get a negatable form (e.g. <tt>--[no-]switch</tt>, false it will not. Default is true
81
+ def switch(*names)
82
+ options = extract_options(names)
83
+ names = [names].flatten
84
+
85
+ verify_unused(names)
86
+ switch = Switch.new(names,options)
87
+ switches[switch.name] = switch
88
+
89
+ clear_nexts
90
+ switch
91
+ end
92
+ alias :s :switch
93
+
94
+ def clear_nexts # :nodoc:
95
+ @next_desc = nil
96
+ @next_arg_name = nil
97
+ @next_default_value = nil
98
+ @next_long_desc = nil
99
+ end
100
+
101
+ # Define a new command. This can be done in a few ways, but the most common method is
102
+ # to pass a symbol (or Array of symbols) representing the command name (or names) and a block.
103
+ # The block will be given an instance of the Command that was created.
104
+ # You then may call methods on this object to define aspects of that Command.
105
+ #
106
+ # Alternatively, you can call this with a one element Hash, where the key is the symbol representing the name
107
+ # of the command, and the value being an Array of symbols representing the commands to call in order, as a
108
+ # chained or compound command. Note that these commands must exist already, and that only those command-specific
109
+ # options defined in *this* command will be parsed and passed to the chained commands. This might not be what you expect
110
+ #
111
+ # +names+:: a String or Symbol, or an Array of String or Symbol that represent all the different names and aliases
112
+ # for this command *or* a Hash, as described above.
113
+ #
114
+ # ==Examples
115
+ #
116
+ # # Make a command named list
117
+ # command :list do |c|
118
+ # c.action do |global_options,options,args|
119
+ # # your command code
120
+ # end
121
+ # end
122
+ #
123
+ # # Make a command named list, callable by ls as well
124
+ # command [:list,:ls] do |c|
125
+ # c.action do |global_options,options,args|
126
+ # # your command code
127
+ # end
128
+ # end
129
+ #
130
+ # # Make a command named all, that calls list and list_contexts
131
+ # command :all => [ :list, :list_contexts ]
132
+ #
133
+ # # Make a command named all, aliased as :a:, that calls list and list_contexts
134
+ # command [:all,:a] => [ :list, :list_contexts ]
135
+ #
136
+ def command(*names)
137
+ command_options = {
138
+ :description => @next_desc,
139
+ :arguments_name => @next_arg_name,
140
+ :long_desc => @next_long_desc,
141
+ :skips_pre => @skips_pre,
142
+ :skips_post => @skips_post,
143
+ }
144
+ if names.first.kind_of? Hash
145
+ command = GLI::Commands::CompoundCommand.new(self,
146
+ names.first,
147
+ command_options)
148
+ command.parent = self
149
+ commands[command.name] = command
150
+ else
151
+ command = Command.new(command_options.merge(:names => [names].flatten))
152
+ command.parent = self
153
+ commands[command.name] = command
154
+ yield command
155
+ end
156
+ clear_nexts
157
+ end
158
+ alias :c :command
159
+
160
+ private
161
+ # Checks that the names passed in have not been used in another flag or option
162
+ def verify_unused(names) # :nodoc:
163
+ names.each do |name|
164
+ verify_unused_in_option(name,flags,"flag")
165
+ verify_unused_in_option(name,switches,"switch")
166
+ end
167
+ end
168
+
169
+ def verify_unused_in_option(name,option_like,type) # :nodoc:
170
+ return if name.to_s == 'help'
171
+ raise ArgumentError.new("#{name} has already been specified as a #{type} #{context_description}") if option_like[name]
172
+ option_like.each do |one_option_name,one_option|
173
+ if one_option.aliases
174
+ if one_option.aliases.include? name
175
+ raise ArgumentError.new("#{name} has already been specified as an alias of #{type} #{one_option_name} #{context_description}")
176
+ end
177
+ end
178
+ end
179
+ end
180
+
181
+ # Extract the options hash out of the argument to flag/switch and
182
+ # set the values if using classic style
183
+ def extract_options(names)
184
+ options = {}
185
+ options = names.pop if names.last.kind_of? Hash
186
+ options = { :desc => @next_desc,
187
+ :long_desc => @next_long_desc,
188
+ :default_value => @next_default_value,
189
+ :arg_name => @next_arg_name}.merge(options)
190
+ end
191
+
192
+
193
+ end
194
+ end
@@ -1,7 +1,14 @@
1
1
  module GLI
2
+ # Mixed into all exceptions that GLI handles; you can use this to catch
3
+ # anything that came from GLI intentionally. You can also mix this into non-GLI
4
+ # exceptions to get GLI's exit behavior.
5
+ module StandardException
6
+ def exit_code; 1; end
7
+ end
2
8
  # Indicates that the command line invocation was bad
3
- class BadCommandLine < Exception
4
- def exit_code; -1; end
9
+ class BadCommandLine < StandardError
10
+ include StandardException
11
+ def exit_code; 64; end
5
12
  end
6
13
 
7
14
  # Indicates the bad command line was an unknown command
@@ -14,6 +21,7 @@ module GLI
14
21
 
15
22
  # Indicates the bad command line was an unknown command argument
16
23
  class UnknownCommandArgument < BadCommandLine
24
+ # The command for which the argument was unknown
17
25
  attr_reader :command
18
26
  # +message+:: the error message to show the user
19
27
  # +command+:: the command we were using to parse command-specific options
@@ -24,14 +32,15 @@ module GLI
24
32
  end
25
33
 
26
34
  # Raise this if you want to use an exit status that isn't the default
27
- # provided by GLI. Note that GLI#exit_now! might be a bit more to your liking.
35
+ # provided by GLI. Note that GLI::App#exit_now! might be a bit more to your liking.
28
36
  #
29
37
  # Example:
30
38
  #
31
39
  # raise CustomExit.new("Not connected to DB",-5) unless connected?
32
40
  # raise CustomExit.new("Bad SQL",-6) unless valid_sql?(args[0])
33
41
  #
34
- class CustomExit < Exception
42
+ class CustomExit < StandardError
43
+ include StandardException
35
44
  attr_reader :exit_code #:nodoc:
36
45
  # Create a custom exit exception
37
46
  #
data/lib/gli/flag.rb CHANGED
@@ -1,52 +1,41 @@
1
- require 'gli/command_line_token.rb'
2
- require 'gli/switch.rb'
1
+ require 'gli/command_line_option.rb'
3
2
 
4
3
  module GLI
5
4
  # Defines a flag, which is to say a switch that takes an argument
6
- class Flag < Switch # :nodoc:
5
+ class Flag < CommandLineOption # :nodoc:
7
6
 
8
- attr_accessor :default_value
7
+ # Regexp that is used to see if the flag's argument matches
8
+ attr_reader :must_match
9
9
 
10
- def initialize(names,description,argument_name=nil,default=nil,long_desc=nil)
11
- super(names,description,long_desc)
12
- @argument_name = argument_name || "arg"
13
- @default_value = default
14
- end
10
+ # Type to which we want to cast the values
11
+ attr_reader :type
15
12
 
16
- def get_value!(args)
17
- args.each_index() do |index|
18
- arg = args[index]
19
- present,matched,value = find_me(arg)
20
- if present
21
- args.delete_at index
22
- if !value || value == ''
23
- if args[index]
24
- value = args[index]
25
- args.delete_at index
26
- return value
27
- else
28
- raise BadCommandLine.new("#{matched} requires an argument")
29
- end
30
- else
31
- return value
32
- end
33
- end
34
- end
35
- return @default_value
13
+ # Name of the argument that user configured
14
+ attr_reader :argument_name
15
+
16
+ # Creates a new option
17
+ #
18
+ # names - Array of symbols or strings representing the names of this switch
19
+ # options - hash of options:
20
+ # :desc - the short description
21
+ # :long_desc - the long description
22
+ # :default_value - the default value of this option
23
+ # :arg_name - the name of the flag's argument, default is "arg"
24
+ # :must_match - a regexp that the flag's value must match
25
+ # :type - a class to convert the value to
26
+ def initialize(names,options)
27
+ super(names,options)
28
+ @argument_name = options[:arg_name] || "arg"
29
+ @default_value = options[:default_value]
30
+ @must_match = options[:must_match]
31
+ @type = options[:type]
36
32
  end
37
33
 
38
- def find_me(arg)
39
- if @names[arg]
40
- return [true,arg,nil] if arg.length == 2
41
- # This means we matched the long-form, but there's no argument
42
- raise BadCommandLine.new("#{arg} requires an argument via #{arg}=argument")
43
- end
44
- @names.keys.each() do |name|
45
- match_string = "^#{name}=(.*)$"
46
- match_data = arg.match(match_string)
47
- return [true,name,$1] if match_data;
48
- end
49
- [false,nil,nil]
34
+ def arguments_for_option_parser
35
+ args = all_forms_a.map { |name| "#{name} VAL" }
36
+ args << @must_match if @must_match
37
+ args << @type if @type
38
+ args
50
39
  end
51
40
 
52
41
  # Returns a string of all possible forms
@@ -0,0 +1,98 @@
1
+ module GLI
2
+ # Parses the command-line options using an actual +OptionParser+
3
+ class GLIOptionParser
4
+ def initialize(commands,flags,switches,accepts)
5
+ @commands = commands
6
+ @flags = flags
7
+ @switches = switches
8
+ @accepts = accepts
9
+ end
10
+
11
+ # Given the command-line argument array, returns and array of size 4:
12
+ #
13
+ # 0:: global options
14
+ # 1:: command, as a Command
15
+ # 2:: command-specific options
16
+ # 3:: unparsed arguments
17
+ def parse_options(args) # :nodoc:
18
+ args_clone = args.clone
19
+ global_options = {}
20
+ command = nil
21
+ command_options = {}
22
+ remaining_args = nil
23
+
24
+ global_options,command_name,args = parse_global_options(OptionParserFactory.new(@flags,@switches,@accepts), args)
25
+ @flags.each do |name,flag|
26
+ global_options[name] = flag.default_value unless global_options[name]
27
+ end
28
+
29
+ command_name ||= @default_command || :help
30
+ command = find_command(command_name)
31
+ if Array(command).empty?
32
+ raise UnknownCommand.new("Unknown command '#{command_name}'")
33
+ elsif command.kind_of? Array
34
+ raise UnknownCommand.new("Ambiguous command '#{command_name}'. It matches #{command.sort.join(',')}")
35
+ end
36
+
37
+ command_options,args = parse_command_options(OptionParserFactory.new(command.flags,command.switches,@accepts),
38
+ command,
39
+ args)
40
+
41
+ command.flags.each do |name,flag|
42
+ command_options[name] = flag.default_value unless command_options[name]
43
+ end
44
+ command.switches.each do |name,switch|
45
+ command_options[name] = switch.default_value unless command_options[name]
46
+ end
47
+
48
+ [global_options,command,command_options,args]
49
+ end
50
+
51
+ private
52
+
53
+ def parse_command_options(option_parser_factory,command,args)
54
+ option_parser,command_options = option_parser_factory.option_parser
55
+ option_parser.parse!(args)
56
+ [command_options,args]
57
+ rescue OptionParser::InvalidOption => ex
58
+ raise UnknownCommandArgument.new("Unknown option #{ex.args.join(' ')}",command)
59
+ rescue OptionParser::InvalidArgument => ex
60
+ raise UnknownCommandArgument.new("#{ex.reason}: #{ex.args.join(' ')}",command)
61
+ end
62
+
63
+ def parse_global_options(option_parser_factory,args,&error_handler)
64
+ if error_handler.nil?
65
+ error_handler = lambda { |message|
66
+ raise UnknownGlobalArgument.new(message)
67
+ }
68
+ end
69
+ option_parser,global_options = option_parser_factory.option_parser
70
+ command = nil
71
+ option_parser.order!(args) do |non_option|
72
+ command = non_option
73
+ break
74
+ end
75
+ [global_options,command,args]
76
+ rescue OptionParser::InvalidOption => ex
77
+ error_handler.call("Unknown option #{ex.args.join(' ')}")
78
+ rescue OptionParser::InvalidArgument => ex
79
+ error_handler.call("#{ex.reason}: #{ex.args.join(' ')}")
80
+ end
81
+
82
+ def find_command(name) # :nodoc:
83
+ names_to_commands = {}
84
+ @commands.each do |command_name,command|
85
+ names_to_commands[command_name.to_s] = command
86
+ Array(command.aliases).each do |command_alias|
87
+ names_to_commands[command_alias.to_s] = command
88
+ end
89
+ end
90
+ name = name.to_s
91
+ return names_to_commands[name] if names_to_commands[name]
92
+ # Now try to match on partial names
93
+ partial_matches = names_to_commands.keys.select { |command_name| command_name =~ /^#{name}/ }
94
+ return names_to_commands[partial_matches[0]] if partial_matches.size == 1
95
+ partial_matches
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,44 @@
1
+ module GLI
2
+ # Factory for creating an OptionParser based on app configuration and DSL calls
3
+ class OptionParserFactory
4
+ # Create an OptionParserFactory for the given
5
+ # flags, switches, and accepts
6
+ def initialize(flags,switches,accepts)
7
+ @flags = flags
8
+ @switches = switches
9
+ @accepts = accepts
10
+ end
11
+
12
+ # Return an option parser to parse the given flags, switches and accepts
13
+ def option_parser
14
+ options = {}
15
+ option_parser = OptionParser.new do |opts|
16
+ self.class.setup_accepts(opts,@accepts)
17
+ self.class.setup_options(opts,@switches,options)
18
+ self.class.setup_options(opts,@flags,options)
19
+ end
20
+ [option_parser,options]
21
+ end
22
+
23
+ private
24
+
25
+ def self.setup_accepts(opts,accepts)
26
+ accepts.each do |object,block|
27
+ opts.accept(object) do |arg_as_string|
28
+ block.call(arg_as_string)
29
+ end
30
+ end
31
+ end
32
+
33
+ def self.setup_options(opts,tokens,options)
34
+ tokens.each do |ignore,token|
35
+ opts.on(*token.arguments_for_option_parser) do |arg|
36
+ [token.name,token.aliases].flatten.compact.each do |name|
37
+ options[name] = arg
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ end
44
+ end