gli 0.1.6
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.
- data/README.rdoc +135 -0
- data/bin/gli +61 -0
- data/lib/gli.rb +265 -0
- data/lib/gli/command.rb +71 -0
- data/lib/gli/command_line_token.rb +47 -0
- data/lib/gli/flag.rb +66 -0
- data/lib/gli/switch.rb +56 -0
- data/lib/support/help.rb +112 -0
- data/lib/support/scaffold.rb +127 -0
- metadata +66 -0
data/README.rdoc
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
This is a command line parser for a git-like command line client.
|
2
|
+
|
3
|
+
= Use
|
4
|
+
|
5
|
+
The simplest way to get started is to create a scaffold project
|
6
|
+
|
7
|
+
gli init my_proj command_name other_command_name
|
8
|
+
|
9
|
+
This will create a (very) basic scaffold project in <tt>./my_proj</tt>, with a bare-bones
|
10
|
+
main file in <tt>./my_proj/bin/my_proj</tt>. This file demonstrates most of what you need
|
11
|
+
to describe your command line interface
|
12
|
+
|
13
|
+
== More Detail
|
14
|
+
|
15
|
+
This sets you up to use the DSL that GLI defines:
|
16
|
+
|
17
|
+
#!/usr/bin/ruby
|
18
|
+
$: << File.expand_path(File.dirname(__FILE__) + '/../lib')
|
19
|
+
|
20
|
+
require 'gli'
|
21
|
+
|
22
|
+
include GLI
|
23
|
+
|
24
|
+
This describes a command line switch "-n" that is global to all commands and specified before
|
25
|
+
the command name on the command line.
|
26
|
+
|
27
|
+
desc 'Dry run; don\'t change the disk'
|
28
|
+
switch :n
|
29
|
+
|
30
|
+
|
31
|
+
This describes a command line flag that is global and has a default value of '<tt>.</tt>'. It also
|
32
|
+
specifies a short description of its argument. This is used to print command line help. Note that we
|
33
|
+
have specified two different aliases for this flag. <tt>-r</tt> (because it is listed first) is the default
|
34
|
+
one and <tt>--root</tt> (note two-dash syntax) is also supported. This means that <tt>-r some_dir</tt> and <tt>--root=some_dir</tt> mean
|
35
|
+
the same thing to the application.
|
36
|
+
|
37
|
+
desc 'Root dir in which to create project'
|
38
|
+
default_value '.'
|
39
|
+
arg_name 'root_dir'
|
40
|
+
flag [:r,:root]
|
41
|
+
|
42
|
+
Here we specify a command. Inside the block we can use the same sorts of things as we did above to define flags
|
43
|
+
and switches specific to the command. These must come after the command name. Also note that we use <tt>arg_name</tt>
|
44
|
+
here to describe the arguments this command accepts.
|
45
|
+
|
46
|
+
desc 'Create a new GLI-based project'
|
47
|
+
arg_name 'project_name [command[ command]*]'
|
48
|
+
command [:init,:scaffold] do |c|
|
49
|
+
|
50
|
+
c.desc 'Create an ext dir'
|
51
|
+
c.switch [:e,:ext]
|
52
|
+
|
53
|
+
c.desc 'Overwrite/ignore existing files and directories'
|
54
|
+
c.switch [:force]
|
55
|
+
|
56
|
+
Here we specify the actual actions to take when the command is executed. We define a block that
|
57
|
+
will be given the global options (as a Hash), the command-specific options (as a hash) and the command
|
58
|
+
line arguments
|
59
|
+
|
60
|
+
c.action do |global_options,options,args|
|
61
|
+
if args.length < 1
|
62
|
+
raise(MissingArgumentException,'You must specify the name of your project')
|
63
|
+
end
|
64
|
+
Scaffold.create_scaffold(g[:r],!o[:notest],o[:e],args[0],args[1..-1],o[:force],g[:n])
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
You can also specify some global code to run before, after and on errors:
|
69
|
+
|
70
|
+
pre do |global_options,command,options,args|
|
71
|
+
puts "After parsing, but before #{command.name} is run"
|
72
|
+
return true
|
73
|
+
# return false if we want to skip command execution for some reason
|
74
|
+
end
|
75
|
+
|
76
|
+
post do |global_options,command,options,args|
|
77
|
+
puts "After successful execution of #{command.name}"
|
78
|
+
end
|
79
|
+
|
80
|
+
on_error do |global_options,command,options,args|
|
81
|
+
puts "We go an error"
|
82
|
+
return true # does the standard error handling code
|
83
|
+
# return false # this would skip standard error handling code
|
84
|
+
end
|
85
|
+
|
86
|
+
Now, we run the program using the arguments the user provided on the command line
|
87
|
+
|
88
|
+
run(ARGV)
|
89
|
+
|
90
|
+
What this gives you:
|
91
|
+
|
92
|
+
* A reasonably useful help system. <tt>your_program help</tt> will list all the global options and commands (along with command aliases) and <tt>your_program help command_name</tt> will list help for that given command.
|
93
|
+
* Error handling when flags do not receive arguments or unknown flags or switches are given
|
94
|
+
* Error handling when an unknown command is specified
|
95
|
+
* Default values for flags if they are not specified by the user (switches all default to false)
|
96
|
+
|
97
|
+
What this doesn't give you:
|
98
|
+
|
99
|
+
* A way to indicate required flags
|
100
|
+
* A way to indicate a require argument or required number of arguments
|
101
|
+
* A way to do default switches to 'true' and therefore accept things like <tt>--no-force</tt>
|
102
|
+
|
103
|
+
= Interface Generated
|
104
|
+
|
105
|
+
*executable* <i>global options and flags</i> *command* <i>command specific options and flags</i> `arguments`
|
106
|
+
|
107
|
+
[switch] a command line control string that takes no argument. The <tt>-l</tt> in <tt>ls -l</tt>
|
108
|
+
[flag] a switch that takes an argument. The <tt>-d' '</tt> in <tt>cut -d' ' file</tt>
|
109
|
+
[command] the command to execute. The <tt>rebase</tt> in <tt>git rebase</tt>
|
110
|
+
[arguments] Anything that's not a switch, flag, or command. The <tt>main.c</tt> in <tt>git add main.c</tt>
|
111
|
+
|
112
|
+
== Switches
|
113
|
+
|
114
|
+
Switches can be specified one at a time in either a long or short format:
|
115
|
+
|
116
|
+
git add -i
|
117
|
+
git add --interactive
|
118
|
+
|
119
|
+
Switches can also be combined in their short form:
|
120
|
+
|
121
|
+
ls -l -a
|
122
|
+
ls -la
|
123
|
+
|
124
|
+
== Flags
|
125
|
+
|
126
|
+
Flags can be specified in long or short form, and with or without an equals:
|
127
|
+
|
128
|
+
git merge -s resolve
|
129
|
+
git merge --strategy=resolve
|
130
|
+
|
131
|
+
== Stop Switch
|
132
|
+
|
133
|
+
A <tt>--</tt> at any time stops processing and sends the rest of the argument to the command as arguments, even if
|
134
|
+
they start with a "--"
|
135
|
+
|
data/bin/gli
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
$: << File.expand_path(File.dirname(__FILE__) + '/../lib')
|
3
|
+
|
4
|
+
require 'gli'
|
5
|
+
require 'support/scaffold'
|
6
|
+
|
7
|
+
include GLI
|
8
|
+
desc 'Be verbose'
|
9
|
+
switch :v
|
10
|
+
|
11
|
+
desc 'Print version'
|
12
|
+
switch :version
|
13
|
+
|
14
|
+
desc 'Dry run; don\'t change the disk'
|
15
|
+
switch :n
|
16
|
+
|
17
|
+
desc 'Root dir of project'
|
18
|
+
default_value '.'
|
19
|
+
flag [:r,:root]
|
20
|
+
|
21
|
+
desc 'Create a new GLI-based project'
|
22
|
+
arg_name 'project_name [command[ command]*]'
|
23
|
+
command [:init,:scaffold] do |c|
|
24
|
+
|
25
|
+
c.desc 'Create an ext dir'
|
26
|
+
c.switch [:e,:ext]
|
27
|
+
|
28
|
+
c.desc 'Do not create a test dir'
|
29
|
+
c.switch [:notest]
|
30
|
+
|
31
|
+
c.desc 'Overwrite/ignore existing files and directories'
|
32
|
+
c.switch [:force]
|
33
|
+
|
34
|
+
c.action do |g,o,args|
|
35
|
+
if args.length < 1
|
36
|
+
raise(MissingArgumentException,'You must specify the name of your project')
|
37
|
+
end
|
38
|
+
Scaffold.create_scaffold(g[:r],!o[:notest],o[:e],args[0],args[1..-1],o[:force],g[:n])
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
pre do |global,command,options,args|
|
43
|
+
if (!command || command.name == :help) && global[:version]
|
44
|
+
puts "#{$0} v#{GLI::VERSION}"
|
45
|
+
false
|
46
|
+
else
|
47
|
+
puts "Executing #{command.name}" if global[:v]
|
48
|
+
true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
post do |global,command,options,args|
|
53
|
+
puts "Executed #{command.name}" if global[:v]
|
54
|
+
end
|
55
|
+
|
56
|
+
#on_error do |global,command,options,args|
|
57
|
+
# puts "Got an error" if global[:v]
|
58
|
+
# true
|
59
|
+
#end
|
60
|
+
|
61
|
+
run(ARGV)
|
data/lib/gli.rb
ADDED
@@ -0,0 +1,265 @@
|
|
1
|
+
require 'gli/command_line_token.rb'
|
2
|
+
require 'gli/command.rb'
|
3
|
+
require 'gli/switch.rb'
|
4
|
+
require 'gli/flag.rb'
|
5
|
+
require 'support/help.rb'
|
6
|
+
|
7
|
+
# A means to define and parse a command line interface that works as
|
8
|
+
# Git's does, in that you specify global options, a command name, command
|
9
|
+
# specific options, and then command arguments.
|
10
|
+
module GLI
|
11
|
+
extend self
|
12
|
+
|
13
|
+
VERSION = '0.1.5'
|
14
|
+
|
15
|
+
@@program_name = $0.split(/\//)[-1]
|
16
|
+
@@post_block = nil
|
17
|
+
@@pre_block = nil
|
18
|
+
@@error_block = nil
|
19
|
+
|
20
|
+
# Reset the GLI module internal data structures; mostly for testing
|
21
|
+
def reset
|
22
|
+
switches.clear
|
23
|
+
flags.clear
|
24
|
+
commands.clear
|
25
|
+
clear_nexts
|
26
|
+
end
|
27
|
+
|
28
|
+
# describe the next switch, flag, or command
|
29
|
+
def desc(description); @@next_desc = description; end
|
30
|
+
# describe the argument name of the next flag
|
31
|
+
def arg_name(name); @@next_arg_name = name; end
|
32
|
+
# set the default value of the next flag
|
33
|
+
def default_value(val); @@next_default_value = val; end
|
34
|
+
|
35
|
+
# Create a flag, which is a switch that takes an argument
|
36
|
+
def flag(names)
|
37
|
+
flag = Flag.new(names,@@next_desc,@@next_arg_name,@@next_default_value)
|
38
|
+
flags[flag.name] = flag
|
39
|
+
clear_nexts
|
40
|
+
end
|
41
|
+
|
42
|
+
# Create a switch
|
43
|
+
def switch(names)
|
44
|
+
switch = Switch.new(names,@@next_desc)
|
45
|
+
switches[switch.name] = switch
|
46
|
+
clear_nexts
|
47
|
+
end
|
48
|
+
|
49
|
+
# Define a command.
|
50
|
+
def command(names)
|
51
|
+
command = Command.new(names,@@next_desc,@@next_arg_name)
|
52
|
+
commands[command.name] = command
|
53
|
+
yield command
|
54
|
+
clear_nexts
|
55
|
+
end
|
56
|
+
|
57
|
+
# Define a block to run after command line arguments are parsed
|
58
|
+
# but before any command is run. If this block raises an exception
|
59
|
+
# the command specified will not be executed.
|
60
|
+
# The block will receive the global-options,command,options, and arguments
|
61
|
+
# If this block evaluates to true, the program will proceed; otherwise
|
62
|
+
# the program will end immediately
|
63
|
+
def pre(&a_proc)
|
64
|
+
@@pre_block = a_proc
|
65
|
+
end
|
66
|
+
|
67
|
+
# Define a block to run after command hase been executed, only
|
68
|
+
# if there was not an error.
|
69
|
+
# The block will receive the global-options,command,options, and arguments
|
70
|
+
def post(&a_proc)
|
71
|
+
@@post_block = a_proc
|
72
|
+
end
|
73
|
+
|
74
|
+
# Define a block to run if an error occurs.
|
75
|
+
# The block will receive the exception that was caught.
|
76
|
+
# It should return false to avoid the built-in error handling
|
77
|
+
def on_error(&a_proc)
|
78
|
+
@@error_block = a_proc
|
79
|
+
end
|
80
|
+
|
81
|
+
# Runs whatever command is needed based on the arguments.
|
82
|
+
def run(args)
|
83
|
+
commands[:help] = DefaultHelpCommand.new if !commands[:help]
|
84
|
+
begin
|
85
|
+
global_options,command,options,arguments = parse_options(args)
|
86
|
+
proceed = true
|
87
|
+
proceed = @@pre_block.call(global_options,command,options,arguments) if @@pre_block
|
88
|
+
if proceed
|
89
|
+
command = commands[:help] if !command
|
90
|
+
command.execute(global_options,options,arguments)
|
91
|
+
@@post_block.call(global_options,command,options,arguments) if @@post_block
|
92
|
+
end
|
93
|
+
rescue UnknownCommandException, UnknownArgumentException, MissingArgumentException => ex
|
94
|
+
regular_error_handling = true
|
95
|
+
regular_error_handling = @@error_block.call(ex) if @@error_block
|
96
|
+
|
97
|
+
if regular_error_handling
|
98
|
+
puts "error: #{ex}"
|
99
|
+
puts
|
100
|
+
help = commands[:help]
|
101
|
+
help.execute({},{},[])
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def program_name(override=nil)
|
107
|
+
if override
|
108
|
+
@@program_name = override
|
109
|
+
end
|
110
|
+
@@program_name
|
111
|
+
end
|
112
|
+
|
113
|
+
# Returns an array of four values:
|
114
|
+
# * global options (as a Hash)
|
115
|
+
# * Command
|
116
|
+
# * command options (as a Hash)
|
117
|
+
# * arguments (as an Array)
|
118
|
+
def parse_options(args)
|
119
|
+
global_options,command,options,arguments = parse_options_helper(args.clone,Hash.new,nil,Hash.new,Array.new)
|
120
|
+
flags.each { |name,flag| global_options[name] = flag.default_value if !global_options[name] }
|
121
|
+
command.flags.each { |name,flag| options[name] = flag.default_value if !options[name] }
|
122
|
+
return [global_options,command,options,arguments]
|
123
|
+
end
|
124
|
+
|
125
|
+
# Finds the index of the first non-flag
|
126
|
+
# argument or -1 if there wasn't one.
|
127
|
+
def find_non_flag_index(args)
|
128
|
+
args.each_index do |i|
|
129
|
+
return i if args[i] =~ /^[^\-]/;
|
130
|
+
return i-1 if args[i] =~ /^\-\-$/;
|
131
|
+
end
|
132
|
+
-1;
|
133
|
+
end
|
134
|
+
|
135
|
+
alias :d :desc
|
136
|
+
alias :f :flag
|
137
|
+
alias :s :switch
|
138
|
+
alias :c :command
|
139
|
+
|
140
|
+
def clear_nexts
|
141
|
+
@@next_desc = nil
|
142
|
+
@@next_arg_name = nil
|
143
|
+
@@next_default_value = nil
|
144
|
+
end
|
145
|
+
|
146
|
+
clear_nexts
|
147
|
+
|
148
|
+
def flags; @@flags ||= {}; end
|
149
|
+
def switches; @@switches ||= {}; end
|
150
|
+
def commands; @@commands ||= {}; end
|
151
|
+
|
152
|
+
# Recursive helper for parsing command line options
|
153
|
+
# [args] the arguments that have yet to be processed
|
154
|
+
# [global_options] the global options hash
|
155
|
+
# [command] the Command that has been identified (or nil if not identified yet)
|
156
|
+
# [command_options] options for Command
|
157
|
+
# [arguments] the arguments for Command
|
158
|
+
#
|
159
|
+
# This works by finding the first non-switch/flag argument, and taking that sublist and trying to pick out
|
160
|
+
# flags and switches. After this is done, one of the following is true:
|
161
|
+
# * the sublist is empty - in this case, go again, as there might be more flags to parse
|
162
|
+
# * the sublist has a flag left in it - unknown flag; we bail
|
163
|
+
# * the sublist has a non-flag left in it - this is the command (or the start of the arguments list)
|
164
|
+
#
|
165
|
+
# This sort does the same thing in two phases; in the first phase, the command hasn't been identified, so
|
166
|
+
# we are looking for global switches and flags, ending when we get the command.
|
167
|
+
#
|
168
|
+
# Once the command has been found, we start looking for command-specific flags and switches.
|
169
|
+
# When those have been found, we know the rest of the argument list is arguments for the command
|
170
|
+
def parse_options_helper(args,global_options,command,command_options,arguments)
|
171
|
+
non_flag_i = find_non_flag_index(args)
|
172
|
+
all_flags = false
|
173
|
+
if non_flag_i == 0
|
174
|
+
# no flags
|
175
|
+
if !command
|
176
|
+
command_name = args.shift
|
177
|
+
command = find_command(command_name)
|
178
|
+
raise(UnknownCommandException,"Unknown command '#{command_name}'") if !command
|
179
|
+
return parse_options_helper(args,global_options,command,command_options,arguments)
|
180
|
+
else
|
181
|
+
return global_options,command,command_options,arguments | args
|
182
|
+
end
|
183
|
+
elsif non_flag_i == -1
|
184
|
+
all_flags = true
|
185
|
+
end
|
186
|
+
|
187
|
+
try_me = args[0..non_flag_i]
|
188
|
+
rest = args[(non_flag_i+1)..args.length]
|
189
|
+
if all_flags
|
190
|
+
try_me = args
|
191
|
+
rest = []
|
192
|
+
end
|
193
|
+
|
194
|
+
# Suck up whatever options we can
|
195
|
+
switch_hash = switches
|
196
|
+
flag_hash = flags
|
197
|
+
options = global_options
|
198
|
+
if command
|
199
|
+
switch_hash = command.switches
|
200
|
+
flag_hash = command.flags
|
201
|
+
options = command_options
|
202
|
+
end
|
203
|
+
|
204
|
+
switch_hash.each do |name,switch|
|
205
|
+
value = switch.get_value!(try_me)
|
206
|
+
options[name] = value if !options[name]
|
207
|
+
end
|
208
|
+
|
209
|
+
flag_hash.each do |name,flag|
|
210
|
+
value = flag.get_value!(try_me)
|
211
|
+
options[name] = value if !options[name]
|
212
|
+
end
|
213
|
+
|
214
|
+
if try_me.empty?
|
215
|
+
return [global_options,command,command_options,arguments] if rest.empty?
|
216
|
+
# If we have no more options we've parsed them all
|
217
|
+
# and rest may have more
|
218
|
+
return parse_options_helper(rest,global_options,command,command_options,arguments)
|
219
|
+
else
|
220
|
+
if command
|
221
|
+
check = rest
|
222
|
+
check = rest | try_me if all_flags
|
223
|
+
check.each() do |arg|
|
224
|
+
if arg =~ /^\-\-$/
|
225
|
+
try_me.delete arg
|
226
|
+
break
|
227
|
+
end
|
228
|
+
raise(UnknownArgumentException,"Unknown argument #{arg}") if arg =~ /^\-/
|
229
|
+
end
|
230
|
+
return [global_options,command,command_options,try_me | rest]
|
231
|
+
else
|
232
|
+
# Now we have our command name
|
233
|
+
command_name = try_me.shift
|
234
|
+
raise(UnknownArgumentException,"Unknown argument #{command_name}") if command_name =~ /^\-/
|
235
|
+
|
236
|
+
command = find_command(command_name)
|
237
|
+
raise(UnknownCommandException,"Unknown command '#{command_name}'") if !command
|
238
|
+
|
239
|
+
return parse_options_helper(rest,global_options,command,command_options,arguments)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
end
|
244
|
+
|
245
|
+
def find_command(name)
|
246
|
+
sym = name.to_sym
|
247
|
+
return commands[name.to_sym] if commands[sym]
|
248
|
+
commands.keys.each do |command_name|
|
249
|
+
command = commands[command_name]
|
250
|
+
return command if (command.aliases && command.aliases.include?(sym))
|
251
|
+
end
|
252
|
+
nil
|
253
|
+
end
|
254
|
+
|
255
|
+
# Raise this if you get an argument you were not expecting
|
256
|
+
class UnknownArgumentException < Exception
|
257
|
+
end
|
258
|
+
|
259
|
+
class UnknownCommandException < Exception
|
260
|
+
end
|
261
|
+
|
262
|
+
# Raise this if your command doesn't get the number of arguments you were expecting
|
263
|
+
class MissingArgumentException < Exception
|
264
|
+
end
|
265
|
+
end
|
data/lib/gli/command.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'gli/command_line_token.rb'
|
2
|
+
|
3
|
+
module GLI
|
4
|
+
# A command to be run, in context of global flags and switches
|
5
|
+
class Command < CommandLineToken
|
6
|
+
|
7
|
+
# Create a new command
|
8
|
+
#
|
9
|
+
# [names] the name or names of this command (symbol or Array of symbols)
|
10
|
+
# [description] description of this command
|
11
|
+
# [arguments_name] description of the arguments, or nil if this command doesn't take arguments
|
12
|
+
#
|
13
|
+
def initialize(names,description,arguments_name=nil)
|
14
|
+
super(names,description)
|
15
|
+
@arguments_description = arguments_name || ''
|
16
|
+
clear_nexts
|
17
|
+
end
|
18
|
+
|
19
|
+
def names
|
20
|
+
all_forms
|
21
|
+
end
|
22
|
+
|
23
|
+
def usage
|
24
|
+
usage = name.to_s
|
25
|
+
usage += ' [options]' if !flags.empty? || !switches.empty?
|
26
|
+
usage += ' ' + @arguments_description if @arguments_description
|
27
|
+
usage
|
28
|
+
end
|
29
|
+
|
30
|
+
def flags; @flags ||= {}; end
|
31
|
+
def switches; @switches ||= {}; end
|
32
|
+
|
33
|
+
# describe the next switch or flag
|
34
|
+
def desc(description); @next_desc = description; end
|
35
|
+
# describe the argument name of the next flag
|
36
|
+
def arg_name(name); @next_arg_name = name; end
|
37
|
+
# set the default value of the next flag
|
38
|
+
def default_value(val); @next_default_value = val; end
|
39
|
+
|
40
|
+
def flag(names)
|
41
|
+
flag = Flag.new(names,@next_desc,@next_arg_name,@next_default_value)
|
42
|
+
flags[flag.name] = flag
|
43
|
+
clear_nexts
|
44
|
+
end
|
45
|
+
|
46
|
+
# Create a switch
|
47
|
+
def switch(names)
|
48
|
+
switch = Switch.new(names,@next_desc)
|
49
|
+
switches[switch.name] = switch
|
50
|
+
clear_nexts
|
51
|
+
end
|
52
|
+
|
53
|
+
def action(&block)
|
54
|
+
@action = block
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.name_as_string(name)
|
58
|
+
name.to_s
|
59
|
+
end
|
60
|
+
|
61
|
+
def clear_nexts
|
62
|
+
@next_desc = nil
|
63
|
+
@next_arg_name = nil
|
64
|
+
@next_default_value = nil
|
65
|
+
end
|
66
|
+
|
67
|
+
def execute(global_options,options,arguments)
|
68
|
+
@action.call(global_options,options,arguments)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module GLI
|
2
|
+
# Logical element of a command line, mostly so that subclasses can have similar
|
3
|
+
# initialization and interface
|
4
|
+
class CommandLineToken
|
5
|
+
attr_reader :name
|
6
|
+
attr_reader :aliases
|
7
|
+
attr_reader :description
|
8
|
+
|
9
|
+
def initialize(names,description)
|
10
|
+
@description = description
|
11
|
+
@name,@aliases,@names = parse_names(names)
|
12
|
+
end
|
13
|
+
|
14
|
+
def usage
|
15
|
+
all_forms
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
# Returns a string of all possible forms
|
20
|
+
# of this flag. Mostly intended for printing
|
21
|
+
# to the user.
|
22
|
+
def all_forms(joiner=', ')
|
23
|
+
forms = all_forms_a
|
24
|
+
forms.join(joiner)
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
# Handles dealing with the "names" param, parsing
|
29
|
+
# it into the primary name and aliases list
|
30
|
+
def parse_names(names)
|
31
|
+
names_hash = Hash.new
|
32
|
+
names = names.is_a?(Array) ? names : [names]
|
33
|
+
names.each { |n| names_hash[self.class.name_as_string(n)] = true }
|
34
|
+
name = names.shift
|
35
|
+
aliases = names.length > 0 ? names : nil
|
36
|
+
[name,aliases,names_hash]
|
37
|
+
end
|
38
|
+
|
39
|
+
def all_forms_a
|
40
|
+
forms = [self.class.name_as_string(name)]
|
41
|
+
if aliases
|
42
|
+
forms |= aliases.collect { |a| self.class.name_as_string(a) }.sort { |x,y| y.length <=> x.length }
|
43
|
+
end
|
44
|
+
forms
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/gli/flag.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'gli/command_line_token.rb'
|
2
|
+
|
3
|
+
module GLI
|
4
|
+
# Defines a flag, which is to say a switch that takes an argument
|
5
|
+
class Flag < Switch
|
6
|
+
|
7
|
+
attr_reader :default_value
|
8
|
+
|
9
|
+
def initialize(names,description,argument_name=nil,default=nil)
|
10
|
+
super(names,description)
|
11
|
+
@argument_name = argument_name || "arg"
|
12
|
+
@default_value = default
|
13
|
+
end
|
14
|
+
|
15
|
+
def get_value!(args)
|
16
|
+
args.each_index() do |index|
|
17
|
+
arg = args[index]
|
18
|
+
present,matched,value = find_me(arg)
|
19
|
+
if present
|
20
|
+
args.delete_at index
|
21
|
+
if !value || value == ''
|
22
|
+
if args[index]
|
23
|
+
value = args[index]
|
24
|
+
args.delete_at index
|
25
|
+
return value
|
26
|
+
else
|
27
|
+
raise(MissingArgumentException,"#{matched} requires an argument")
|
28
|
+
end
|
29
|
+
else
|
30
|
+
return value
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
return @default_value
|
35
|
+
end
|
36
|
+
|
37
|
+
def find_me(arg)
|
38
|
+
if @names[arg]
|
39
|
+
return [true,arg,nil] if arg.length == 2
|
40
|
+
# This means we matched the long-form, but there's no argument
|
41
|
+
raise(MissingArgumentException,"#{arg} requires an argument via #{arg}=argument")
|
42
|
+
end
|
43
|
+
@names.keys.each() do |name|
|
44
|
+
match_string = "^#{name}=(.*)$"
|
45
|
+
match_data = arg.match(match_string)
|
46
|
+
return [true,name,$1] if match_data;
|
47
|
+
end
|
48
|
+
[false,nil,nil]
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns a string of all possible forms
|
52
|
+
# of this flag. Mostly intended for printing
|
53
|
+
# to the user.
|
54
|
+
def all_forms(joiner=', ')
|
55
|
+
forms = all_forms_a
|
56
|
+
string = forms.join(joiner)
|
57
|
+
if forms[-1] =~ /^\-\-/
|
58
|
+
string += '='
|
59
|
+
else
|
60
|
+
string += ' '
|
61
|
+
end
|
62
|
+
string += @argument_name
|
63
|
+
return string
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/gli/switch.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'gli/command_line_token.rb'
|
2
|
+
|
3
|
+
module GLI
|
4
|
+
# Defines a command line switch
|
5
|
+
class Switch < CommandLineToken
|
6
|
+
|
7
|
+
def initialize(names,description)
|
8
|
+
super(names,description)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Given the argument list, scans it looking for this switch
|
12
|
+
# returning true if it's in the argumennt list (and removing it from the argument list)
|
13
|
+
def get_value!(args)
|
14
|
+
idx = -1
|
15
|
+
args.each_index do |i|
|
16
|
+
result = find_me(args[i])
|
17
|
+
if result[0]
|
18
|
+
if result[1]
|
19
|
+
args[i] = result[1]
|
20
|
+
else
|
21
|
+
args.delete_at i
|
22
|
+
end
|
23
|
+
return result[0]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
false
|
27
|
+
end
|
28
|
+
|
29
|
+
# Finds the switch in the given arg, returning the arg to keep.
|
30
|
+
# Returns an array of size 2:
|
31
|
+
# [0] true or false if the arg was found
|
32
|
+
# [1] the remaining arg to keep in the command line or nil to remove it
|
33
|
+
def find_me(arg)
|
34
|
+
if @names[arg]
|
35
|
+
return [true,nil]
|
36
|
+
end
|
37
|
+
@names.keys.each() do |name|
|
38
|
+
if name =~ /^-(\w)$/
|
39
|
+
match_string = "^\\-(\\w*)#{$1}(\\w*)$"
|
40
|
+
match_data = arg.match(match_string)
|
41
|
+
if match_data
|
42
|
+
# Note that if [1] and [2] were both empty
|
43
|
+
# we'd have returned above
|
44
|
+
return [true, "-" + match_data[1] + match_data[2]]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
[false]
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.name_as_string(name)
|
52
|
+
string = name.to_s
|
53
|
+
string.length == 1 ? "-#{string}" : "--#{string}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/support/help.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'gli'
|
2
|
+
require 'gli/command'
|
3
|
+
|
4
|
+
module GLI
|
5
|
+
class DefaultHelpCommand < Command
|
6
|
+
def initialize
|
7
|
+
super(:help,'Shows list of commands or help for one command','[command]')
|
8
|
+
end
|
9
|
+
|
10
|
+
def execute(global_options,options,arguments)
|
11
|
+
if arguments.empty?
|
12
|
+
list_global_flags
|
13
|
+
list_commands
|
14
|
+
else
|
15
|
+
list_one_command_help(arguments[0])
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def list_global_flags
|
22
|
+
usage = "usage: #{GLI.program_name} command"
|
23
|
+
all_options = GLI.switches.merge(GLI.flags)
|
24
|
+
if !all_options.empty?
|
25
|
+
usage += ' [options]'
|
26
|
+
end
|
27
|
+
puts usage
|
28
|
+
puts
|
29
|
+
puts 'Options:' if !all_options.empty?
|
30
|
+
output_command_tokens_for_help(all_options)
|
31
|
+
puts if !all_options.empty?
|
32
|
+
end
|
33
|
+
|
34
|
+
def list_commands
|
35
|
+
puts 'Commands:'
|
36
|
+
output_command_tokens_for_help(GLI.commands,:names)
|
37
|
+
end
|
38
|
+
|
39
|
+
def list_one_command_help(command_name)
|
40
|
+
command = GLI.find_command(command_name)
|
41
|
+
if command
|
42
|
+
puts command.usage
|
43
|
+
description = wrap(command.description,4)
|
44
|
+
puts " #{description}"
|
45
|
+
all_options = command.switches.merge(command.flags)
|
46
|
+
if !all_options.empty?
|
47
|
+
puts
|
48
|
+
puts "Options:"
|
49
|
+
output_command_tokens_for_help(all_options)
|
50
|
+
end
|
51
|
+
else
|
52
|
+
puts "No such command #{command_name}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def output_command_tokens_for_help(tokens,usage_name=:usage)
|
57
|
+
max = 0
|
58
|
+
tokens.values.each do |token|
|
59
|
+
len = token.send(usage_name).length
|
60
|
+
if len > max
|
61
|
+
max = len
|
62
|
+
end
|
63
|
+
end
|
64
|
+
names = tokens.keys.sort { |x,y| x.to_s <=> y.to_s }
|
65
|
+
names.each do |name|
|
66
|
+
token = tokens[name]
|
67
|
+
description = token.description || ''
|
68
|
+
if token.kind_of? Flag
|
69
|
+
description += " (default: #{token.default_value})" if token.default_value
|
70
|
+
end
|
71
|
+
description = wrap(description,max+7)
|
72
|
+
printf " %-#{max}s - %s\n",token.send(usage_name),description
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# Wraps the line at the given column length, using the given line padding.
|
80
|
+
# Assumes that the first line doesn't need the padding, as its filled
|
81
|
+
# up with other stuff
|
82
|
+
def wrap(line,pad_length=0,line_length=80)
|
83
|
+
line_padding = sprintf("%#{pad_length}s",'')
|
84
|
+
words = line.split(/\s+/)
|
85
|
+
return line if !words || words.empty?
|
86
|
+
wrapped = ''
|
87
|
+
while wrapped.length + line_padding.length < line_length
|
88
|
+
wrapped += ' ' if wrapped.length > 0
|
89
|
+
word = words.shift
|
90
|
+
if (wrapped.length + line_padding.length + word.length > line_length)
|
91
|
+
words.unshift word
|
92
|
+
break;
|
93
|
+
end
|
94
|
+
wrapped += word
|
95
|
+
return wrapped if words.empty?
|
96
|
+
end
|
97
|
+
wrapped += "\n"
|
98
|
+
this_line = line_padding
|
99
|
+
words.each do |word|
|
100
|
+
if this_line.length + word.length > line_length
|
101
|
+
wrapped += this_line
|
102
|
+
wrapped += "\n"
|
103
|
+
this_line = line_padding + word
|
104
|
+
else
|
105
|
+
this_line += ' ' if this_line.length > line_padding.length
|
106
|
+
this_line += word
|
107
|
+
end
|
108
|
+
end
|
109
|
+
wrapped.chomp!
|
110
|
+
wrapped + "\n" + this_line
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'gli'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module GLI
|
5
|
+
class Scaffold
|
6
|
+
|
7
|
+
def self.create_scaffold(root_dir,create_test_dir,create_ext_dir,project_name,commands,force=false,dry_run=false)
|
8
|
+
dirs = [File.join(root_dir,project_name,'lib')]
|
9
|
+
dirs << File.join(root_dir,project_name,'bin')
|
10
|
+
dirs << File.join(root_dir,project_name,'test') if create_test_dir
|
11
|
+
dirs << File.join(root_dir,project_name,'ext') if create_ext_dir
|
12
|
+
|
13
|
+
if mkdirs(dirs,force,dry_run)
|
14
|
+
mk_binfile(root_dir,create_ext_dir,force,dry_run,project_name,commands)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.mk_binfile(root_dir,create_ext_dir,force,dry_run,project_name,commands)
|
19
|
+
bin_file = File.join(root_dir,project_name,'bin',project_name)
|
20
|
+
if !File.exist?(bin_file) || force
|
21
|
+
if !dry_run
|
22
|
+
File.open(bin_file,'w') do |file|
|
23
|
+
file.chmod(0755)
|
24
|
+
file.puts '#!/usr/bin/ruby'
|
25
|
+
file.puts '$: << File.expand_path(File.dirname(__FILE__) + \'/../lib\')'
|
26
|
+
file.puts '$: << File.expand_path(File.dirname(__FILE__) + \'/../ext\')' if create_ext_dir
|
27
|
+
file.puts <<EOS
|
28
|
+
require 'rubygems'
|
29
|
+
require 'gli'
|
30
|
+
|
31
|
+
include GLI
|
32
|
+
|
33
|
+
desc 'Describe some switch here'
|
34
|
+
switch [:s,:switch]
|
35
|
+
|
36
|
+
desc 'Describe some flag here'
|
37
|
+
default_value 'the default'
|
38
|
+
arg_name 'The name of the argument'
|
39
|
+
flag [:f,:flagname]
|
40
|
+
EOS
|
41
|
+
first = true
|
42
|
+
commands.each do |command|
|
43
|
+
if first
|
44
|
+
file.puts <<EOS
|
45
|
+
|
46
|
+
desc 'Describe #{command} here'
|
47
|
+
arg_name 'Describe arguments to #{command} here'
|
48
|
+
command :#{command} do |c|
|
49
|
+
c.desc 'Describe a switch to #{command}'
|
50
|
+
c.switch :s
|
51
|
+
|
52
|
+
c.desc 'Describe a flag to #{command}'
|
53
|
+
c.default_value 'default'
|
54
|
+
c.flag :s
|
55
|
+
c.action do |global_options,options,args|
|
56
|
+
# Your command logic here
|
57
|
+
end
|
58
|
+
end
|
59
|
+
EOS
|
60
|
+
else
|
61
|
+
file.puts <<EOS
|
62
|
+
command :#{command} do |c|
|
63
|
+
c.action do |global_options,options,args|
|
64
|
+
end
|
65
|
+
end
|
66
|
+
EOS
|
67
|
+
end
|
68
|
+
first = false
|
69
|
+
end
|
70
|
+
file.puts <<EOS
|
71
|
+
pre do |global,command,options,args|
|
72
|
+
# Pre logic here
|
73
|
+
# Return true to proceed; false to abourt and not call the
|
74
|
+
# chosen command
|
75
|
+
true
|
76
|
+
end
|
77
|
+
|
78
|
+
post do |global,command,options,args|
|
79
|
+
# Post logic here
|
80
|
+
end
|
81
|
+
|
82
|
+
on_error do |exception|
|
83
|
+
# Error logic here
|
84
|
+
# return false to skip default error handling
|
85
|
+
true
|
86
|
+
end
|
87
|
+
|
88
|
+
GLI.run(ARGV)
|
89
|
+
EOS
|
90
|
+
puts "Created #{bin_file}"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
else
|
94
|
+
puts bin_file + " exists; use --force to override"
|
95
|
+
return false
|
96
|
+
end
|
97
|
+
true
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.mkdirs(dirs,force,dry_run)
|
101
|
+
exists = false
|
102
|
+
if !force
|
103
|
+
dirs.each do |dir|
|
104
|
+
if File.exist? dir
|
105
|
+
puts "#{dir} exists; use --force to override"
|
106
|
+
exists = true
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
if !exists
|
111
|
+
dirs.each do |dir|
|
112
|
+
puts "Creating dir #{dir}..."
|
113
|
+
if dry_run
|
114
|
+
$stderr.puts "dry-run; #{dir} not created"
|
115
|
+
else
|
116
|
+
FileUtils.mkdir_p dir
|
117
|
+
end
|
118
|
+
end
|
119
|
+
else
|
120
|
+
puts "Exiting..."
|
121
|
+
return false
|
122
|
+
end
|
123
|
+
true
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gli
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.6
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- David Copeland
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-04-19 00:00:00 -04:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: davidcopeland@naildrivin5.com
|
18
|
+
executables:
|
19
|
+
- gli
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.rdoc
|
24
|
+
files:
|
25
|
+
- lib/gli/command.rb
|
26
|
+
- lib/gli/command_line_token.rb
|
27
|
+
- lib/gli/flag.rb
|
28
|
+
- lib/gli/switch.rb
|
29
|
+
- lib/gli.rb
|
30
|
+
- lib/support/help.rb
|
31
|
+
- lib/support/scaffold.rb
|
32
|
+
- bin/gli
|
33
|
+
- README.rdoc
|
34
|
+
has_rdoc: true
|
35
|
+
homepage: http://davetron5000.github.com/gli
|
36
|
+
post_install_message:
|
37
|
+
rdoc_options:
|
38
|
+
- --title
|
39
|
+
- Git Like Interface
|
40
|
+
- --main
|
41
|
+
- README.rdoc
|
42
|
+
- -ri
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
- lib
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: "0"
|
51
|
+
version:
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: "0"
|
57
|
+
version:
|
58
|
+
requirements: []
|
59
|
+
|
60
|
+
rubyforge_project: gli
|
61
|
+
rubygems_version: 1.3.1
|
62
|
+
signing_key:
|
63
|
+
specification_version: 2
|
64
|
+
summary: A Git Like Interface for building command line apps
|
65
|
+
test_files: []
|
66
|
+
|