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
@@ -0,0 +1,34 @@
1
+ require 'gli/command_line_token.rb'
2
+
3
+ module GLI
4
+ # An option, not a command or argument, on the command line
5
+ class CommandLineOption < CommandLineToken #:nodoc:
6
+
7
+ attr_accessor :default_value
8
+ # Command to which this option "belongs", nil if it's a global option
9
+ attr_accessor :associated_command
10
+
11
+ # Creates a new option
12
+ #
13
+ # names - Array of symbols or strings representing the names of this switch
14
+ # options - hash of options:
15
+ # :desc - the short description
16
+ # :long_desc - the long description
17
+ # :default_value - the default value of this option
18
+ def initialize(names,options = {})
19
+ super(names,options[:desc],options[:long_desc])
20
+ @default_value = options[:default_value]
21
+ end
22
+
23
+ def self.name_as_string(name,negatable=true)
24
+ string = name.to_s
25
+ if string.length == 1
26
+ "-#{string}"
27
+ elsif negatable
28
+ "--[no-]#{string}"
29
+ else
30
+ "--#{string}"
31
+ end
32
+ end
33
+ end
34
+ end
@@ -2,12 +2,12 @@ module GLI
2
2
  # Abstract base class for a logical element of a command line, mostly so that subclasses can have similar
3
3
  # initialization and interface
4
4
  class CommandLineToken
5
- attr_reader :name #:ndoc:
6
- attr_reader :aliases #:ndoc:
7
- attr_reader :description #:ndoc:
8
- attr_reader :long_description #:ndoc:
5
+ attr_reader :name #:nodoc:
6
+ attr_reader :aliases #:nodoc:
7
+ attr_reader :description #:nodoc:
8
+ attr_reader :long_description #:nodoc:
9
9
 
10
- def initialize(names,description,long_description=nil) #:ndoc:
10
+ def initialize(names,description,long_description=nil) #:nodoc:
11
11
  @description = description
12
12
  @long_description = long_description
13
13
  @name,@aliases,@names = parse_names(names)
@@ -17,7 +17,7 @@ module GLI
17
17
  all_forms
18
18
  end
19
19
 
20
- # Sort based on name
20
+ # Sort based on primary name
21
21
  def <=>(other)
22
22
  self.name.to_s <=> other.name.to_s
23
23
  end
@@ -37,7 +37,7 @@ module GLI
37
37
  def parse_names(names)
38
38
  # Allow strings; convert to symbols
39
39
  names = [names].flatten.map { |name| name.to_sym }
40
- names_hash = Hash.new
40
+ names_hash = {}
41
41
  names.each do |name|
42
42
  raise ArgumentError.new("#{name} has spaces; they are not allowed") if name.to_s =~ /\s/
43
43
  names_hash[self.class.name_as_string(name)] = true
@@ -47,10 +47,14 @@ module GLI
47
47
  [name,aliases,names_hash]
48
48
  end
49
49
 
50
+ def negatable?
51
+ false;
52
+ end
53
+
50
54
  def all_forms_a
51
- forms = [self.class.name_as_string(name)]
55
+ forms = [self.class.name_as_string(name,negatable?)]
52
56
  if aliases
53
- forms |= aliases.collect { |one_alias| self.class.name_as_string(one_alias) }.sort { |one,two| two.length <=> one.length }
57
+ forms |= aliases.map { |one_alias| self.class.name_as_string(one_alias,negatable?) }.sort { |one,two| one.length <=> two.length }
54
58
  end
55
59
  forms
56
60
  end
@@ -0,0 +1,200 @@
1
+ module GLI
2
+ # Things unrelated to the true public interface of Command that are needed for bookkeeping
3
+ # and help support. Generally, you shouldn't be calling these methods; they are technically public
4
+ # but are essentially part of GLI's internal implementation and subject to change
5
+ module CommandSupport
6
+ # The parent of this command, either the GLI app, or another command
7
+ attr_accessor :parent
8
+
9
+ def context_description
10
+ "in the command #{name}"
11
+ end
12
+
13
+ # Return true to avoid including this command in your help strings
14
+ def nodoc
15
+ false
16
+ end
17
+
18
+ # Return the arguments description
19
+ def arguments_description
20
+ @arguments_description
21
+ end
22
+
23
+ # If true, this command doesn't want the pre block run before it executes
24
+ def skips_pre
25
+ @skips_pre
26
+ end
27
+
28
+ # If true, this command doesn't want the post block run before it executes
29
+ def skips_post
30
+ @skips_post
31
+ end
32
+
33
+ # Return the Array of the command's names
34
+ def names
35
+ all_forms
36
+ end
37
+
38
+ def flag(*names)
39
+ new_flag = if parent.kind_of? Command
40
+ parent.flag(*names)
41
+ else
42
+ super(*names)
43
+ end
44
+ new_flag.associated_command = self
45
+ new_flag
46
+ end
47
+
48
+ def switch(*names)
49
+ new_switch = if parent.kind_of? Command
50
+ parent.switch(*names)
51
+ else
52
+ super(*names)
53
+ end
54
+ new_switch.associated_command = self
55
+ new_switch
56
+ end
57
+
58
+ def desc(d)
59
+ if parent.kind_of? Command
60
+ parent.desc(d)
61
+ else
62
+ super(d)
63
+ end
64
+ end
65
+
66
+ def long_desc(d)
67
+ if parent.kind_of? Command
68
+ parent.long_desc(d)
69
+ else
70
+ super(d)
71
+ end
72
+ end
73
+
74
+ def arg_name(d)
75
+ if parent.kind_of? Command
76
+ parent.arg_name(d)
77
+ else
78
+ super(d)
79
+ end
80
+ end
81
+
82
+ def default_value(d)
83
+ if parent.kind_of? Command
84
+ parent.default_value(d)
85
+ else
86
+ super(d)
87
+ end
88
+ end
89
+
90
+ # Get the usage string
91
+ # CR: This should probably not be here
92
+ def usage
93
+ usage = name.to_s
94
+ usage += ' [command options]' if !flags.empty? || !switches.empty?
95
+ usage += ' ' + @arguments_description if @arguments_description
96
+ usage
97
+ end
98
+
99
+ # Return the flags as a Hash
100
+ def flags
101
+ @flags ||= {}
102
+ end
103
+ # Return the switches as a Hash
104
+ def switches
105
+ @switches ||= {}
106
+ end
107
+
108
+ def commands # :nodoc:
109
+ @commands ||= {}
110
+ end
111
+
112
+ def default_description
113
+ @default_desc
114
+ end
115
+
116
+ # Executes the command
117
+ def execute(global_options,options,arguments)
118
+ subcommand,arguments = find_subcommand(arguments)
119
+ if subcommand
120
+ subcommand.execute(global_options,options,arguments)
121
+ else
122
+ get_action(arguments).call(global_options,options,arguments)
123
+ end
124
+ end
125
+
126
+ def topmost_ancestor
127
+ some_command = self
128
+ top = some_command
129
+ while some_command.kind_of? self.class
130
+ top = some_command
131
+ some_command = some_command.parent
132
+ end
133
+ top
134
+ end
135
+
136
+ def has_action?
137
+ !!@action
138
+ end
139
+
140
+ def get_default_command
141
+ @default_command
142
+ end
143
+
144
+ private
145
+
146
+ def get_action(arguments)
147
+ if @action
148
+ @action
149
+ else
150
+ generate_error_action(arguments)
151
+ end
152
+ end
153
+
154
+ def generate_error_action(arguments)
155
+ lambda { |global_options,options,arguments|
156
+ if am_subcommand?
157
+ if arguments.size > 0
158
+ raise UnknownCommand,"Unknown command '#{arguments[0]}'"
159
+ else
160
+ raise BadCommandLine,"Command '#{name}' requires a subcommand"
161
+ end
162
+ elsif have_subcommands?
163
+ raise BadCommandLine,"Command '#{name}' requires a subcommand"
164
+ else
165
+ raise "Command '#{name}' has no action block"
166
+ end
167
+ }
168
+ end
169
+
170
+ def am_subcommand?
171
+ parent.kind_of?(Command)
172
+ end
173
+
174
+ def have_subcommands?
175
+ !self.commands.empty?
176
+ end
177
+
178
+ def find_subcommand(arguments)
179
+ subcommand = find_explicit_subcommand(arguments)
180
+ if subcommand
181
+ [subcommand,arguments[1..-1]]
182
+ else
183
+ if !@default_command.nil?
184
+ [find_explicit_subcommand([@default_command.to_s]),arguments]
185
+ else
186
+ [false,arguments]
187
+ end
188
+ end
189
+ end
190
+
191
+ def find_explicit_subcommand(arguments)
192
+ arguments = Array(arguments)
193
+ return false if arguments.empty?
194
+ subcommand_name = arguments.first
195
+ self.commands.values.find { |command|
196
+ [command.name,Array(command.aliases)].flatten.map(&:to_s).any? { |name| name == subcommand_name }
197
+ }
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,42 @@
1
+ module GLI
2
+ module Commands
3
+ # A command that calls other commands in order
4
+ class CompoundCommand < Command
5
+ # base:: object that respondes to +commands+
6
+ # configuration:: Array of arrays: index 0 is the array of names of this command and index 1
7
+ # is the names of the compound commands.
8
+ def initialize(base,configuration,options={})
9
+ name = configuration.keys.first
10
+ super(options.merge(:names => [name]))
11
+
12
+ command_names = configuration[name]
13
+
14
+ check_for_unknown_commands!(base,command_names)
15
+
16
+ @commands = command_names.map { |name| self.class.find_command(base,name) }
17
+ end
18
+
19
+ def execute(global_options,options,arguments) #:nodoc:
20
+ @commands.each do |command|
21
+ command.execute(global_options,options,arguments)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def check_for_unknown_commands!(base,command_names)
28
+ known_commands = base.commands.keys.map(&:to_s)
29
+ unknown_commands = command_names.map(&:to_s) - known_commands
30
+
31
+ unless unknown_commands.empty?
32
+ raise "Unknown commands #{unknown_commands.join(',')}"
33
+ end
34
+ end
35
+
36
+ def self.find_command(base,name)
37
+ base.commands.values.find { |command| command.name == name }
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,63 @@
1
+ require 'erb'
2
+ require 'gli/command'
3
+ require 'gli/terminal'
4
+ require 'gli/commands/help_modules/list_formatter'
5
+ require 'gli/commands/help_modules/text_wrapper'
6
+ require 'gli/commands/help_modules/options_formatter'
7
+ require 'gli/commands/help_modules/global_help_format'
8
+ require 'gli/commands/help_modules/command_help_format'
9
+
10
+ module GLI
11
+ module Commands
12
+ # The help command used for the two-level interactive help system
13
+ class Help < Command
14
+ def initialize(app,output=$stdout,error=$stderr)
15
+ super(:names => :help,
16
+ :description => 'Shows a list of commands or help for one command',
17
+ :arguments_name => 'command',
18
+ :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',
19
+ :skips_pre => true,
20
+ :skips_post => true)
21
+ @app = app
22
+ action do |global_options,options,arguments|
23
+ show_help(global_options,options,arguments,output,error)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def show_help(global_options,options,arguments,out,error)
30
+ if arguments.empty?
31
+ out.puts HelpModules::GlobalHelpFormat.new(@app).format
32
+ else
33
+ name = arguments.shift
34
+ command = find_command(name,@app)
35
+ return if unknown_command(command,name,error)
36
+ while !arguments.empty?
37
+ name = arguments.shift
38
+ command = find_command(name,command)
39
+ return if unknown_command(command,name,error)
40
+ end
41
+ out.puts HelpModules::CommandHelpFormat.new(command,@app,File.basename($0).to_s).format
42
+ end
43
+ end
44
+
45
+ def unknown_command(command,name,error)
46
+ if command.nil?
47
+ error.puts "error: Unknown command '#{name}'. Use 'gli help' for a list of commands."
48
+ true
49
+ else
50
+ false
51
+ end
52
+ end
53
+
54
+ def find_command(command_name,base)
55
+ base.commands.values.select { |command|
56
+ if [command.name,Array(command.aliases)].flatten.map(&:to_s).any? { |_| _ == command_name }
57
+ command
58
+ end
59
+ }.first
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,134 @@
1
+ require 'erb'
2
+
3
+ module GLI
4
+ module Commands
5
+ module HelpModules
6
+ class CommandHelpFormat
7
+ def initialize(command,app,basic_invocation)
8
+ @basic_invocation = basic_invocation
9
+ @app = app
10
+ @command = command
11
+ end
12
+
13
+ def format
14
+ command_wrapper = TextWrapper.new(Terminal.instance.size[0],4 + @command.name.to_s.size + 3)
15
+ wrapper = TextWrapper.new(Terminal.instance.size[0],4)
16
+ flags_and_switches = Hash[@command.topmost_ancestor.flags.merge(@command.topmost_ancestor.switches).select { |_,option| option.associated_command == @command }]
17
+ options_description = OptionsFormatter.new(flags_and_switches).format
18
+ commands_description = format_subcommands(@command)
19
+
20
+ synopses = []
21
+ one_line_usage = basic_usage(flags_and_switches)
22
+ one_line_usage << @command.arguments_description
23
+ if @command.commands.empty?
24
+ synopses << one_line_usage
25
+ else
26
+ synopses = sorted_synopses(flags_and_switches)
27
+ if @command.has_action?
28
+ synopses.unshift(one_line_usage)
29
+ end
30
+
31
+ end
32
+
33
+ COMMAND_HELP.result(binding)
34
+ end
35
+
36
+ private
37
+ COMMAND_HELP = ERB.new(%q(NAME
38
+ <%= @command.name %> - <%= command_wrapper.wrap(@command.description) %>
39
+
40
+ SYNOPSIS
41
+ <% synopses.each do |s| %>
42
+ <%= s %>
43
+ <% end %>
44
+ <% unless @command.long_description.nil? %>
45
+
46
+ DESCRIPTION
47
+ <%= wrapper.wrap(@command.long_description) %>
48
+ <% end %>
49
+ <% if options_description.strip.length != 0 %>
50
+
51
+ COMMAND OPTIONS
52
+ <%= options_description %>
53
+ <% end %>
54
+ <% unless @command.commands.empty? %>
55
+
56
+ COMMANDS
57
+ <%= commands_description %>
58
+ <% end %>),nil,'<>')
59
+
60
+ def command_with_subcommand_usage(sub,flags_and_switches,is_default_command)
61
+ usage = basic_usage(flags_and_switches)
62
+ sub_options = @command.flags.merge(@command.switches).select { |_,o| o.associated_command == sub }
63
+ usage << sub_options.map { |option_name,option|
64
+ all_names = [option.name,Array(option.aliases)].flatten
65
+ all_names.map { |_|
66
+ CommandLineOption.name_as_string(_,false) + (option.kind_of?(Flag) ? " #{option.argument_name }" : '')
67
+ }.join('|')
68
+ }.map { |_| "[#{_}]" }.sort.join(' ')
69
+ usage << ' '
70
+ if is_default_command
71
+ usage << "[#{sub.name}]"
72
+ else
73
+ usage << sub.name.to_s
74
+ end
75
+ usage
76
+ end
77
+
78
+ def basic_usage(flags_and_switches)
79
+ usage = @basic_invocation.dup
80
+ usage << " [global options] #{path_to_command} "
81
+ usage << "[command options] " unless global_flags_and_switches.empty?
82
+ usage
83
+ end
84
+
85
+ def path_to_command
86
+ path = []
87
+ c = @command
88
+ while c.kind_of? Command
89
+ path.unshift(c.name)
90
+ c = c.parent
91
+ end
92
+ path.join(' ')
93
+ end
94
+
95
+ def global_flags_and_switches
96
+ @app.flags.merge(@app.switches)
97
+ end
98
+
99
+ def format_subcommands(command)
100
+ commands_array = command.commands.values.sort.map { |cmd|
101
+ if command.get_default_command == cmd.name
102
+ [cmd.names,cmd.description + " (default)"]
103
+ else
104
+ [cmd.names,cmd.description]
105
+ end
106
+ }
107
+ if command.has_action?
108
+ commands_array.unshift(["<default>",command.default_description])
109
+ end
110
+ formatter = ListFormatter.new(commands_array)
111
+ StringIO.new.tap { |io| formatter.output(io) }.string
112
+ end
113
+
114
+ def sorted_synopses(flags_and_switches)
115
+ synopses_command = {}
116
+ @command.commands.each do |name,sub|
117
+ default = @command.get_default_command == name
118
+ synopsis = command_with_subcommand_usage(sub,flags_and_switches,default)
119
+ synopses_command[synopsis] = sub
120
+ end
121
+ synopses = synopses_command.keys.sort { |one,two|
122
+ if synopses_command[one].name == @command.get_default_command
123
+ -1
124
+ elsif synopses_command[two].name == @command.get_default_command
125
+ 1
126
+ else
127
+ synopses_command[one] <=> synopses_command[two]
128
+ end
129
+ }
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end