Module: Strop
- Defined in:
- lib/strop.rb,
lib/strop/version.rb
Overview
Command-line option parser that builds options from help text
Defined Under Namespace
Modules: Exports Classes: Arg, Opt, Optdecl, OptionError, Optlist, Result
Constant Summary collapse
- Sep =
Const for parsed ‘–` end of option markers. Seen as member of Result.
:sep- RX_SOARG =
short opt optional arg
/\[\S+?\]/- RX_SARG =
short opt required arg
/[^\s,]+/- RX_LOARG =
long opt optional arg: –foo or –foo [bar]
/\[=\S+?\]| #{RX_SOARG}/- RX_LARG =
long opt required arg: –foo=bar or –foo bar
/[ =]#{RX_SARG}/- RX_NO =
prefix for –[no-]flags
/\[no-?\]/- RX_SOPT =
full short opt
/-[^-\s,](?: (?:#{RX_SOARG}|#{RX_SARG}))?/- RX_LOPT =
full long opt
/--(?=[^-=,\s])#{RX_NO}?[^\s=,\[]+(?:#{RX_LOARG}|#{RX_LARG})?/- RX_OPT =
either opt
/#{RX_SOPT}|#{RX_LOPT}/- RX_OPTS =
list of opts, comma separated
/#{RX_OPT}(?:, {0,2}#{RX_OPT})*/- VERSION =
"0.4.0"
Class Method Summary collapse
- .name_from_symbol(name) ⇒ Object
-
.parse(optlist, argv = ARGV) ⇒ Object
Parse command line arguments array against option declarations.
-
.parse! ⇒ Object
Parse with error handling: print message and exit on OptionError.
-
.parse_help(help, pad: /(?: ){1,2}/) ⇒ Object
Extract option declarations from formatted help text.
-
.prefix(name) ⇒ Object
helper for printing back option names with the right prefix.
Class Method Details
.name_from_symbol(name) ⇒ Object
9 |
# File 'lib/strop.rb', line 9 def self.name_from_symbol(name) = Symbol === name ? name.to_s.gsub(?_, ?-) : name |
.parse(optlist, argv = ARGV) ⇒ Object
Parse command line arguments array against option declarations. Defaults to parsing ARGV Accepts help text, file object for help file, or Optlist
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 |
# File 'lib/strop.rb', line 128 def self.parse(optlist, argv=ARGV) #=> Result[...] Array === argv && argv.all?{ String === it } or raise "argv must be an array of strings (given #{argv.class})" optlist = case optlist when IO then parse_help(optlist.read) when String then parse_help(optlist) when Optlist then optlist else raise "optlist must be an Optlist or help text (given #{optlist.class})" end tokens = argv.dup res = Result.new ctx = :top name, token, opt = nil rx_value = /\A[^-]|\A-?\z/ # not an opt loop do case ctx in :end then return res.concat tokens.map{ Arg[it] } # opt parsing ended, rest is positional args in :value then ctx = :top; res << Arg[token] # interspersed positional arg amidst opts in :top token = tokens.shift or next ctx = :end # next token or done case token in "--" then ctx = :end; res << Sep # end of options in /\A--(.+)\z/m then token, ctx = $1, :long # long (--foo, --foo xxx), long with attached value (--foo=xxx) in /\A-(.+)\z/m then token, ctx = $1, :short # short or clump (-a, -abc) in ^rx_value then ctx = :value # value end in :long name, value = *token.split(?=, 2) opt = optlist[name] or raise OptionError, "Unknown option: --#{name}" case [opt.arg?, value] in true, String then ctx = :top; res << Opt[opt, name, value] # --foo=XXX in false, nil then ctx = :top; res << Opt[opt, name] # --foo in true, nil then ctx = :arg # --foo XXX in false, String then raise OptionError, "Option --#{name} takes no argument" end in :short name, token = token[0], token[1..].then{ it if it != "" } # -abc -> a, bc opt = optlist[name] or raise OptionError, "Unknown option: -#{name}" case [opt.arg?, token] in true, String then ctx = :top; res << Opt[opt, name, token] # -aXXX in false, nil then ctx = :top; res << Opt[opt, name] # end of -abc in true, nil then ctx = :arg # -a XXX in false, String then res << Opt[opt, name] # -abc -> took -a, will parse -bc end in :arg token = tokens[0]&.match(rx_value) && tokens.shift case [opt.arg, token] in :may, String then ctx = :top; res << Opt[opt, name, token] # --opt val in :must, String then ctx = :top; res << Opt[opt, name, token] # --req val in :may, nil then ctx = :top; res << Opt[opt, name] # --opt followed by --foo, --opt as last token in :must, nil then raise OptionError, "Expected argument for option -#{?- if name[1]}#{name}" # --req missing value end end end end |
.parse! ⇒ Object
Parse with error handling: print message and exit on OptionError
189 190 191 192 193 194 |
# File 'lib/strop.rb', line 189 def self.parse!(...) #=> Result[...] parse(...) rescue OptionError => e $stderr.puts e. exit 1 end |
.parse_help(help, pad: /(?: ){1,2}/) ⇒ Object
Extract option declarations from formatted help text
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 |
# File 'lib/strop.rb', line 208 def self.parse_help(help, pad: /(?: ){1,2}/) #=> Optlist[...] decls = help.scan(/^#{pad}(#{RX_OPTS})(.*)/).map do |line, rest| # get all optdecl lines # Ambiguous: --opt Desc with only one space before will interpret "Desc" as arg. ambiguous = rest =~ /^ \S/ && line =~ / (#{RX_SARG})$/ # desc preceeded by sringle space && last arg is " "+word. Capture arg name for error below ambiguous and $stderr.puts "#{$1.inspect} was interpreted as argument, In #{(line+rest).inspect}. Use at least two spaces before description to avoid this warning." pairs = line.scan(RX_OPT).map { it.split(/(?=\[=)|=| +/, 2) } # take options from each line, separate name from arg pairs.map! { |name, arg| [name.sub(/^--?/, ''), arg.nil? ? :shant : arg[0] == "[" ? :may : :must] } # remove opt markers -/--, transform arg str into requirement names, args = pairs.transpose # [[name, arg], ...] -> [names, args] arg, *rest = args.uniq.tap{ it.delete :shant if it.size > 1 } # delete excess :shant (from -f in -f,--foo=x, without arg on short opt) raise "Option #{names} has conflicting arg requirements: #{args}" if rest.any? # raise if still conflict, like -f X, --ff [X] names = (names.flat_map{ it.start_with?(RX_NO) ? [$', $&[1...-1] + $'] : it }).uniq # expand --[no]flag into --flag and --noflag (also --[no-]) [names, arg] # [names and noflags, resolved single arg] end.uniq # allow identical opts dupes = decls.flat_map(&:first).tally.reject{|k,v|v==1} # detect repeated names with diff specs raise "Options #{dupes.keys.inspect} seen more than once in distinct definitions" if dupes.any? decls.map{ |names, arg| Optdecl[*names, arg:] }.then{ Optlist[*it] } # Return an Optlist from decls end |
.prefix(name) ⇒ Object
helper for printing back option names with the right prefix
8 |
# File 'lib/strop.rb', line 8 def self.prefix(name) = (name[1] ? "--" : "-") + name # helper for printing back option names with the right prefix |