diff --git a/lib/optparse.rb b/lib/optparse.rb index 4f631fbec6..e36b52f7ec 100644 --- a/lib/optparse.rb +++ b/lib/optparse.rb @@ -195,6 +195,11 @@ # options = OptparseExample.parse(ARGV) # pp options # +# === Shell Completion +# +# For modern shells (e.g. bash, zsh, etc.), you can use shell +# completion for command line options. +# # === Further documentation # # The above examples should be enough to learn how to use this class. If you @@ -218,12 +223,15 @@ class OptionParser # and resolved against a list of acceptable values. # module Completion - def complete(key, icase = false, pat = nil) - pat ||= Regexp.new('\A' + Regexp.quote(key).gsub(/\w+\b/, '\&\w*'), - icase) + def self.regexp(key, icase) + Regexp.new('\A' + Regexp.quote(key).gsub(/\w+\b/, '\&\w*'), icase) + end + + def self.candidate(key, icase = false, pat = nil, &block) + pat ||= Completion.regexp(key, icase) canon, sw, cn = nil candidates = [] - each do |k, *v| + block.call do |k, *v| (if Regexp === k kn = nil k === key @@ -234,7 +242,16 @@ class OptionParser v << k if v.empty? candidates << [k, v, kn] end - candidates = candidates.sort_by {|k, v, kn| kn.size} + candidates + end + + def candidate(key, icase = false, pat = nil) + Completion.candidate(key, icase, pat, &method(:each)) + end + + public + def complete(key, icase = false, pat = nil) + candidates = candidate(key, icase, pat, &method(:each)).sort_by {|k, v, kn| kn.size} if candidates.size == 1 canon, sw, * = candidates[0] elsif candidates.size > 1 @@ -717,9 +734,17 @@ class OptionParser # --help # Shows option summary. # + # --help=complete=WORD + # Shows candidates for command line completion. + # Officious['help'] = proc do |parser| - Switch::NoArgument.new do - puts parser.help + Switch::OptionalArgument.new do |arg| + case arg + when /\Acomplete=(.*)/ + puts parser.candidate($1) + else + puts parser.help + end exit end end @@ -1461,6 +1486,35 @@ class OptionParser end private :complete + def candidate(word) + list = [] + case word + when /\A--/ + word, arg = word.split(/=/, 2) + argpat = Completion.regexp(arg, false) if arg and !arg.empty? + long = true + when /\A-(!-)/ + short = true + when /\A-/ + long = short = true + end + pat = Completion.regexp(word, true) + visit(:each_option) do |opt| + opts = [*(opt.long if long), *(opt.short if short)] + opts = Completion.candidate(word, true, pat, &opts.method(:each)).map(&:first) if pat + if /\A=/ =~ opt.arg + opts = opts.map {|sw| sw + "="} + if arg and CompletingHash === opt.pattern + if opts = opt.pattern.candidate(arg, false, argpat) + opts.map!(&:last) + end + end + end + list.concat(opts) + end + list + end + # # Loads options from file names as +filename+. Does nothing when the file # is not present. Returns whether successfully loaded. @@ -1818,6 +1872,6 @@ ARGV.extend(OptionParser::Arguable) if $0 == __FILE__ Version = OptionParser::Version ARGV.options {|q| - q.parse!.empty? or puts "what's #{ARGV.join(' ')}?" + q.parse!.empty? or print "what's #{ARGV.join(' ')}?\n" } or abort(ARGV.options.to_s) end diff --git a/misc/rb_optparse.bash b/misc/rb_optparse.bash new file mode 100644 index 0000000000..5022442c94 --- /dev/null +++ b/misc/rb_optparse.bash @@ -0,0 +1,20 @@ +#! /bin/bash +# Completion for bash: +# +# (1) install this file, +# +# (2) load the script, and +# . ~/.profile.d/rb_optparse.bash +# +# (3) define completions in your .bashrc, +# rb_optparse command_using_optparse_1 +# rb_optparse command_using_optparse_2 + +_rb_optparse() { + COMPREPLY=($("${COMP_WORDS[0]}" --help=complete="${COMP_WORDS[COMP_CWORD]}")) + return 0 +} + +rb_optparse () { + [ $# = 0 ] || complete -o default -F _rb_optparse "$@" +} diff --git a/test/optparse/test_bash_completion.rb b/test/optparse/test_bash_completion.rb new file mode 100644 index 0000000000..baeb6d9882 --- /dev/null +++ b/test/optparse/test_bash_completion.rb @@ -0,0 +1,42 @@ +require 'test/unit' +require 'optparse' + +class TestOptionParser < Test::Unit::TestCase +end +class TestOptionParser::BashCompletion < Test::Unit::TestCase + def setup + @opt = OptionParser.new + @opt.define("-z", "zzz") {} + @opt.define("--foo") {} + @opt.define("--bar=BAR") {} + @opt.define("--for=TYPE", [:hello, :help, :zot]) {} + end + + def test_empty + assert_equal([], @opt.candidate("")) + end + + def test_one_hyphen + assert_equal(%w[-z --foo --bar= --for=], @opt.candidate("-")) + end + + def test_two_hyphen + assert_equal(%w[--foo --bar= --for=], @opt.candidate("--")) + end + + def test_long_f + assert_equal(%w[--foo --for=], @opt.candidate("--f")) + end + + def test_long_for_option + assert_equal(%w[--for=], @opt.candidate("--for")) + end + + def test_long_for_option_args + assert_equal(%w[hello help zot], @opt.candidate("--for=")) + end + + def test_long_for_option_complete + assert_equal(%w[hello help], @opt.candidate("--for=h")) + end +end diff --git a/test/optparse/test_getopts.rb b/test/optparse/test_getopts.rb index 1ba194ace1..ae22f68184 100644 --- a/test/optparse/test_getopts.rb +++ b/test/optparse/test_getopts.rb @@ -1,7 +1,9 @@ require 'test/unit' require 'optparse' -class TestOptionParserGetopts < Test::Unit::TestCase +class TestOptionParser < Test::Unit::TestCase +end +class TestOptionParser::Getopts < Test::Unit::TestCase def setup @opt = OptionParser.new end