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

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.message
  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