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
@@ -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