use new "argumentparser.rb"

modelled after "argparse" in Python
This commit is contained in:
Johan Holmberg 2015-08-06 22:33:48 +02:00
parent b9c716a80e
commit 13b284ef8c
2 changed files with 366 additions and 72 deletions

View File

@ -29,6 +29,7 @@ top_dir = File.dirname(File.dirname(__FILE__))
lib_dir = File.join(File.expand_path(top_dir), "lib")
$:.unshift(lib_dir) if File.directory?(File.join(lib_dir, "cmdtest"))
require "cmdtest/argumentparser"
require "cmdtest/baselogger"
require "cmdtest/consolelogger"
require "cmdtest/junitlogger"
@ -220,7 +221,7 @@ module Cmdtest
def get_test_methods(runner)
@testcase_class.public_instance_methods(false).sort.select do |method|
in_list = runner.opts.tests.empty? || runner.opts.tests.include?(method)
in_list = runner.opts.test.empty? || runner.opts.test.include?(method)
method =~ /^test_/ && in_list
end
end
@ -394,71 +395,48 @@ module Cmdtest
class Main
attr_reader :tests, :quiet, :verbose, :fast, :ruby_s, :incremental
attr_reader :patterns, :parallel
def initialize
@tests = []
@quiet = false
@verbose = false
@fast = false
@parallel = 1
@xml = nil
@set_exit_code = true
@ruby_s = Util.windows?
@incremental = false
@patterns = []
_update_cmdtest_level
end
def _parse_options
pr = @argument_parser = ArgumentParser.new("cmdtest")
pr.add("", "--version", "show version")
pr.add("-q", "--quiet", "be more quiet")
pr.add("-v", "--verbose", "be more verbose")
pr.add("", "--fast", "run fast without waiting for unique mtime:s")
pr.add("-j", "--parallel", "build in parallel", type: Integer, default: 1, metavar: "N")
pr.add("", "--test", "only run named test", type: [String])
pr.add("", "--xml", "write summary on JUnit format", type: String, metavar: "FILE")
pr.add("", "--no-exit-code", "exit with 0 status even after errors")
pr.add("-i", "--incremental", "incremental mode")
pr.addpos("arg", "testfile or pattern", nargs: 0..999)
return pr.parse_args(ARGV, patterns: [], ruby_s: Util.windows?)
end
def run
opts = _parse_options
files = []
while ! ARGV.empty?
opt = ARGV.shift
for arg in opts.args
case
when opt =~ /^--test=(.*)/
@tests << $1
when opt =~ /^--quiet$/
@quiet = true
when opt =~ /^--verbose$/
@verbose = true
when opt =~ /^--fast$/
@fast = true
when opt =~ /^--xml=(.+)$/
@xml = File.expand_path($1)
when opt =~ /^--no-exit-code$/
@set_exit_code = false
when opt =~ /^--ruby_s$/
@ruby_s = ! @ruby_s
when opt =~ /^-r$/
@incremental = true
when opt =~ /^-i$/
@incremental = true
when opt =~ /^-j(\d+)$/
@parallel = Integer($1)
when opt =~ /^--help$/ || opt =~ /^-h$/
puts
_show_options
puts
exit 0
when File.file?(opt)
files << opt
when File.directory?(opt)
files << opt
when opt =~ /^\/(.+)\/$/
@patterns << $1
when File.file?(arg)
files << arg
when File.directory?(arg)
files << arg
when arg =~ /^\/(.+)\/$/
opts.patterns << $1
else
puts "ERROR: unknown argument: #{opt}"
puts "ERROR: unknown argument: #{arg}"
puts
_show_options
@argument_parser.print_usage()
puts
exit 1
end
end
begin
@patterns.map! {|pattern| Regexp.new(pattern) }
opts.patterns.map! {|pattern| Regexp.new(pattern) }
rescue RegexpError => e
puts "ERROR: syntax error in regexp?"
puts "DETAILS: " + e.message
@ -466,20 +444,20 @@ module Cmdtest
end
clog = LogClient.new
Util.opts = self
Util.opts = opts
error_logger = ErrorLogger.new(self)
error_logger = ErrorLogger.new(opts)
clog.add_listener(error_logger)
logger = ConsoleLogger.new(self)
logger = ConsoleLogger.new(opts)
clog.add_listener(logger)
if @xml
clog.add_listener(JunitLogger.new(self, @xml))
if opts.xml
clog.add_listener(JunitLogger.new(opts, File.expand_path(opts.xml)))
end
@project_dir = ProjectDir.new(files)
@runner = Runner.new(@project_dir, @incremental, self)
@runner = Runner.new(@project_dir, opts.incremental, opts)
$cmdtest_got_ctrl_c = 0
trap("INT") do
@ -492,7 +470,7 @@ module Cmdtest
end
@runner.run(clog)
if ! @quiet
if ! opts.quiet
puts
puts "%s %d test classes, %d test methods, %d commands, %d errors, %d fatals." % [
error_logger.n_failures == 0 && error_logger.n_errors == 0 ? "###" : "---",
@ -506,7 +484,7 @@ module Cmdtest
end
ok = error_logger.everything_ok?
error_exit = @set_exit_code && ! ok
error_exit = ! opts.no_exit_code && ! ok
exit( error_exit ? 1 : 0 )
end
@ -517,19 +495,6 @@ module Cmdtest
ENV["CMDTEST_LEVEL"] = $cmdtest_level.to_s
end
def _show_options
puts "Usage: cmdtest [options] [files/directories]"
puts
puts " --help show this help"
puts " --quiet be more quiet"
puts " --verbose be more verbose"
puts " --fast run fast without waiting for unique mtime:s"
puts " --test=NAME only run named test"
puts " --xml=FILE write summary on JUnit format"
puts " --no-exit-code exit with 0 status even after errors"
puts " -i incremental mode"
end
end
end

View File

@ -0,0 +1,329 @@
#----------------------------------------------------------------------
# argumentparser.rb
#----------------------------------------------------------------------
# Copyright 2015 Johan Holmberg.
#----------------------------------------------------------------------
# This file is part of "cmdtest".
#
# "cmdtest" is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# "cmdtest" is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with "cmdtest". If not, see <http://www.gnu.org/licenses/>.
#----------------------------------------------------------------------
module Cmdtest
class Option
attr_reader :sname, :name, :help
def initialize(sname, name, help, args)
@sname = sname
@name = name
@help = help
@extra = args
end
def init(opts)
if _with_arg
if @extra[:default]
opts[_opt_name] = @extra[:default]
elsif Array === @extra[:type]
opts[_opt_name] = []
else
opts[_opt_name] = nil
end
else
opts[_opt_name] = false
end
end
def _set(value, opts)
if _with_arg
if @extra[:type] == String
opts[_opt_name] = value
elsif @extra[:type] == Integer
opts[_opt_name] = Integer(value)
elsif @extra[:type] == [String]
opts[_opt_name] << value
elsif @extra[:type] == [Integer]
opts[_opt_name] << Integer(value)
else
raise RuntimeError, "unknown type"
end
else
opts[_opt_name] = true
end
end
def usage_text
res = @sname.length == 0 ? name : sname
res += _arg_extra()
return "[" + res + "]"
end
def _arg_extra
return _with_arg ? " " + _arg_name() : ""
end
def _with_arg
return @extra[:type]
end
def _opt_name
return @name.sub(/^-+/, "").gsub("-", "_")
end
def _arg_name
return @extra[:metavar] if @extra[:metavar]
return _opt_name.upcase
end
def names
extra = _with_arg() ? " " + _arg_name() : ""
if @sname.length == 0
return name + extra
else
return sname + extra + ", " + name + extra
end
end
end
#----------------------------------------------------------------------
class Argument
attr_reader :name, :help
def initialize(name, help, args)
@name = name
@help = help
@extra = args
end
def usage_text
n = @extra[:nargs] || 1
if Range === n && n.begin == 0
return "[" + @name + " [" + @name + " ...]]"
else
raise RuntimeError, "unexpected"
end
end
end
#----------------------------------------------------------------------
class ArgumentParser
def initialize(program)
@program = program
@options = []
@argv = nil
@optind = nil
@help = false
@args = []
add("-h", "--help", "show this help message and exit")
end
def add(sname, name, help, args = {})
option = Option.new(sname, name, help, args)
@options << option
end
def addpos(name, help, args = {})
@args << Argument.new(name, help, args)
end
def print_usage_synopsis(f)
leading = "usage: " + @program
f.print(leading)
off = leading.size
for option in @options
str = option.usage_text()
if off + 1 + str.size > 79
f.puts
f.print(" " * leading.size)
off = leading.length
end
f.print(" ", str)
off += 1 + str.size
end
f.puts
if @args.size > 0
f.print(" " * leading.size)
f.print(" ")
f.print(@args.map {|arg| arg.usage_text() }.join(" "), "\n")
end
end
def print_usage()
print_usage_synopsis(STDOUT)
if @args.size > 0
puts
puts("positional arguments:")
for arg in @args
print(" ", arg.name, " ", arg.help, "\n")
end
puts
end
puts("optional arguments:")
puts(" -h, --help show this help message and exit")
for option in @options
str = " " + option.names()
wanted = 22
str = str.ljust(wanted)
if str.size > wanted
puts(str)
print(" " * wanted, " ", option.help, "\n")
else
print(str, " ", option.help, "\n")
end
end
end
def parse_args(argv, extras = {})
@argv = argv
@opts = {}
# initialize options
for option in @options
option.init(@opts)
end
@optind = 0
while _more_args() && _arg() =~ /^-./
if _arg() == "-h"
print_usage()
exit(0)
end
if _arg() == "--"
@optind += 1
break
end
# -f
if _arg() =~ /^(-\w)$/
option = _find_option($1)
if option._with_arg()
if ! _more_args(1)
print_usage_synopsis(STDERR);
STDERR.print(@program, ": error: argument ",
option.show(), ": expected one argument")
exit(2)
end
option._set(_arg(1), @opts)
@optind += 2
else
option._set(true, @opts)
@optind += 1
end
# -f...
elsif _arg() =~ /^(-\w)(.+)/
option = _find_option($1)
if option._with_arg()
option._set($2, @opts)
@optind += 1
else
option._set(true, @opts)
_setarg("-" + $2)
end
# --foo
elsif _arg() =~ /^(--\w[-_\w_]*)$/
option = _find_option($1)
if option._with_arg()
option._set(_arg(1), @opts)
@optind += 2
else
option._set(true, @opts)
@optind += 1
end
# --foo=...
elsif _arg() =~ /^(--\w[-_\w]*)=(.*)$/
option = _find_option($1)
if option._with_arg()
option._set($2, @opts)
@optind += 1
else
print_usage_synopsis(STDERR)
STDERR.print(@program, ": error: argument: ",
option.show(), ": ignored explicit argument '",
$2, "'");
exit(2)
end
else
STDERR.print("INTERNAL ERROR: arg = ", _arg())
raise RuntimeError("unexpected else ...");
end
end
fields = @opts.keys.sort + ["args"]
all_fields = fields + extras.keys.sort
res_class = Struct.new("GivenOptions", *all_fields)
values = fields.map {|k| @opts[k] }
res = res_class.new(*values)
res.args = []
for k,v in extras
res[k] = v
end
if @args.size > 0
res.args.concat(@argv[@optind..999]) # TODO
elsif @optind < @argv.size
raise RuntimeError("too many options")
end
return res
end
def _find_option(name)
for option in @options
return option if option.name == name
return option if option.sname == name
end
print_usage_synopsis(STDERR)
STDERR.print(@program, ": error: unrecognized arguments: ", name, "\n")
exit(2)
end
def _arg(i=0)
if @optind + i < @argv.size
return @argv[@optind + i]
else
raise RuntimeError("index out of range");
end
end
def _setarg(value)
if @optind < @argv.size
@argv[@optind] = value
else
raise RuntimeError("index out of range");
end
end
def _more_args(i=0)
return @optind + i < @argv.size
end
end
end