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