forked from fatso83/dotfiles
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy patheasyoptions.rb
executable file
·277 lines (249 loc) · 10.2 KB
/
easyoptions.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
#!/usr/bin/env ruby
# Encoding: UTF-8
##
## EasyOptions 2015.2.28
## Copyright (c) 2013, 2014 Renato Silva
## BSD licensed
##
## This script is supposed to parse command line arguments in a way that,
## even though its implementation is not trivial, it should be easy and
## smooth to use. For using this script, simply document your target script
## using double-hash comments, like this:
##
## ## Program Name v1.0
## ## Copyright (C) Someone
## ##
## ## This program does something. Usage:
## ## @#script.name [option]
## ##
## ## Options:
## ## -h, --help All client scripts have this by default,
## ## it shows this double-hash documentation.
## ##
## ## -o, --option This option will get stored as true value
## ## under EasyOptions.options[:option]. Long
## ## version is mandatory, and can be specified
## ### before or after short version.
## ##
## ## --some-boolean This will get stored as true value under
## ## EasyOptions.options[:some_boolean].
## ##
## ## --some-value=VALUE This is going to store the VALUE specified
## ## under EasyOptions.options[:some_value].
## ## The equal sign is optional and can be
## ## replaced with blank space when running the
## ## target script. If VALUE is composed of
## ## digits, it will be converted into an
## ## integer, otherwise it will get stored as a
## ## string. Short version is not available in
## ## this format.
##
## The above comments work both as source code documentation and as help
## text, as well as define the options supported by your script. There is no
## duplication of the options specification. The string @#script.name will be
## replaced with the actual script name.
##
## After writing your documentation, you simply require this script. Then all
## command line options will get parsed into the EasyOptions.options hash, as
## described above. You can then check their values for reacting to them. All
## regular arguments will get stored into the EasyOptions.arguments array.
##
## In fact, this script is an example of itself. You are seeing this help
## message either because you are reading the source code, or you have called
## the script in command line with the --help option.
##
## Note: the options and arguments are also available as global variables in
## current version, but their use is discouraged and is supposed to be
## eventually removed.
##
## This script can be used from Bash scripts as well. If the $from environment
## variable is set, that will be assumed as the source Bash script from which to
## parse the documentation and the provided options. Then, instead of parsing
## the options into Ruby variables, evaluable assignment statements will be
## generated for the corresponding Bash environment variables. For example:
##
## eval "$(from="$0" @script.name "$@" || echo exit 1)"
##
## If the script containing this command is documented as in the example above,
## and it is executed from command line with the -o and --some-value=10 options,
## and one regular argument abc, then the evaluable output would look like this:
##
## option="yes"
## some_value="10"
## unset arguments
## arguments+=("abc")
## arguments
##
module EasyOptions
class Option
def initialize(long_version, short_version, boolean = true)
fail ArgumentError.new('Long version is mandatory') if !long_version || long_version.length < 2
@short = short_version.to_sym if short_version
@long = long_version.to_s.gsub('-', '_').to_sym
@boolean = boolean
end
def to_s
"--#{long_dashed}"
end
def in?(string)
string =~ /^--#{long_dashed}$/ || (@short && string =~ /^-#{@short}$/)
end
def in_with_value?(string)
string =~ /^--#{long_dashed}=.*$/
end
def long_dashed
@long.to_s.gsub('_', '-')
end
attr_accessor :short
attr_accessor :long
attr_accessor :boolean
end
class Parser
def initialize
@known_options = [Option.new(:help, :h)]
@documentation = parse_doc
@arguments = []
@options = {}
end
def parse_doc
begin
doc = File.readlines($PROGRAM_NAME)
rescue Errno::ENOENT
exit false
end
doc = doc.find_all do |line|
line =~ /^##[^#]*/
end
doc.map do |line|
line.strip!
line.sub!(/^## ?/, '')
line.gsub!(/@script.name/, File.basename($PROGRAM_NAME))
line.gsub(/@#/, '@')
end
end
def parse
# Parse known options from documentation
@documentation.map do |line|
line = line.strip
case line
when /^-h, --help.*/ then next
when /^--help, -h.*/ then next
when /^-.*, --.*/ then line = line.split(/(^-|,\s--|\s)/); @known_options << Option.new(line[4], line[2])
when /^--.*, -.*/ then line = line.split(/(--|,\s-|\s)/); @known_options << Option.new(line[2], line[4])
when /^--.*=.*/ then line = line.split(/(--|=|\s)/); @known_options << Option.new(line[2], nil, false)
when /^--.* .*/ then line = line.split(/(--|\s)/); @known_options << Option.new(line[2], nil)
end
end
# Format arguments input
raw_arguments = ARGV.map do |argument|
if argument =~ /^-[^-].*$/i
argument.split('')[1..-1].map { |char| "-#{char}" }
else
argument
end
end.flatten
# Parse the provided options
raw_arguments.each_with_index do |argument, index|
unknown_option = true
@known_options.each do |known_option|
# Boolean option
if known_option.in?(argument) && known_option.boolean
@options[known_option.long] = true
unknown_option = false
break
# Option with value in next parameter
elsif known_option.in?(argument) && !known_option.boolean
value = raw_arguments[index + 1]
Parser.finish("you must specify a value for #{known_option}") if !value || value.start_with?('-')
value = value.to_i if value =~ /^[0-9]+$/
@options[known_option.long] = value
unknown_option = false
break
# Option with value after equal sign
elsif known_option.in_with_value?(argument) && !known_option.boolean
value = argument.split('=')[1]
value = value.to_i if value =~ /^[0-9]+$/
@options[known_option.long] = value
unknown_option = false
break
# Long option with unnecessary value
elsif known_option.in_with_value?(argument) && known_option.boolean
value = argument.split('=')[1]
Parser.finish("#{known_option} does not accept a value (you specified \"#{value}\")")
end
end
# Unrecognized option
Parser.finish("unrecognized option \"#{argument}\"") if unknown_option && argument.start_with?('-')
end
# Help option
if @options[:help]
if BashOutput
print "printf '"
puts @documentation
puts "'"
puts 'exit'
else
puts @documentation
end
exit(-1)
end
# Regular arguments
next_is_value = false
raw_arguments.each do |argument|
if argument.start_with?('-')
known_option = @known_options.find { |option| option.in?(argument) }
next_is_value = (known_option && !known_option.boolean)
else
arguments << argument unless next_is_value
next_is_value = false
end
end
# Bash support
return unless BashOutput
@options.keys.each do |name|
puts "#{name}=\"#{@options[name].to_s.sub('true', 'yes')}\""
end
puts 'unset arguments'
arguments.each do |argument|
puts "arguments+=(\"#{argument}\")"
end
end
def self.finish(error)
warn "Error: #{error}."
warn 'See --help for usage and options.'
puts 'exit 1' if BashOutput
exit false
end
def self.check_bash_output
$0 = ENV['from'] || $PROGRAM_NAME
$PROGRAM_NAME == ENV['from']
end
BashOutput = check_bash_output
attr_accessor :documentation
attr_accessor :arguments
attr_accessor :options
end
class << self
@@parser = Parser.new
@@parser.parse
def options
@@parser.options
end
def arguments
@@parser.arguments
end
def documentation
@@parser.documentation
end
def all
[options, arguments, documentation]
end
def finish(error)
Parser.finish(error)
end
end
# This is supposed to be eventually removed
$documentation = @@parser.documentation
$arguments = @@parser.arguments
$options = @@parser.options
end