gli 1.6.0 → 2.0.0.rc3

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