OptionParser

OptionParser

OptionParser는 명령줄 옵션을 분석하는 클래스예요. GetoptLong보다 훨씬 발전했으면서도 쓰기 쉽고, 더 Ruby스러운 해법이에요.

출처: Ruby 4.0 API

본문

특징

  • 인자 지정과 그것을 처리하는 코드가 같은 곳에 쓰여요.
  • 옵션 요약(summary)을 자동으로 출력할 수 있어요. 문자열을 별도로 유지 관리할 필요가 없죠.
  • 선택적 인자와 필수 인자를 아주 우아하게 지정할 수 있어요.
  • 인자를 지정된 클래스로 자동 변환할 수 있어요.
  • 인자를 특정 집합으로 제한할 수 있어요.

이 모든 특징은 아래 예제에 나와 있어요. 전체 문서는 make_switch를 참고하세요.

최소 예제

require 'optparse'

options = {}
OptionParser.new do |parser|
  parser.banner = "Usage: example.rb [options]"

  parser.on("-v", "--[no-]verbose", "Run verbosely") do |v|
    options[:verbose] = v
  end
end.parse!

p options
p ARGV

도움말 생성하기

OptionParser는 여러분이 작성한 명령의 도움말을 자동으로 생성할 수 있어요.

require 'optparse'

Options = Struct.new(:name)

class Parser
  def self.parse(options)
    args = Options.new("world")

    opt_parser = OptionParser.new do |parser|
      parser.banner = "Usage: example.rb [options]"

      parser.on("-nNAME", "--name=NAME", "Name to say hello to") do |n|
        args.name = n
      end

      parser.on("-h", "--help", "Prints this help") do
        puts parser
        exit
      end
    end

    opt_parser.parse!(options)
    return args
  end
end
options = Parser.parse %w[--help]

#=>
   # Usage: example.rb [options]
   #     -n, --name=NAME                  Name to say hello to
   #     -h, --help                       Prints this help

필수 인자(Required Arguments)

인자가 필요한 옵션의 경우, 옵션 지정 문자열에 옵션 이름을 대문자로 넣을 수 있어요. 옵션을 필수 인자 없이 쓰면 예외가 발생해요.

require 'optparse'

options = {}
OptionParser.new do |parser|
  parser.on("-r", "--require LIBRARY",
            "Require the LIBRARY before executing your script") do |lib|
    puts "You required #{lib}!"
  end
end.parse!

실행 결과:

$ ruby optparse-test.rb -r
optparse-test.rb:9:in '<main>': missing argument: -r (OptionParser::MissingArgument)
$ ruby optparse-test.rb -r my-library
You required my-library!

타입 강제 변환(Type Coercion)

OptionParser는 명령줄 인자를 객체로 강제 변환(coerce)해 주는 기능을 지원해요. 기본으로 쓸 수 있는 강제 변환 종류는 다음과 같아요.

  • DateDate.parse가 받아들이는 것 (optparse/date require 필요)
  • DateTimeDateTime.parse가 받아들이는 것 (optparse/date require 필요)
  • TimeTime.httpdateTime.parse가 받아들이는 것 (optparse/time require 필요)
  • URIURI.parse가 받아들이는 것 (optparse/uri require 필요)
  • ShellwordsShellwords.shellwords가 받아들이는 것 (optparse/shellwords require 필요)
  • String – 비어 있지 않은 어떤 문자열이든
  • Integer – 어떤 정수든. 8진수도 변환해요. (예: 124, -3, 040)
  • Float – 어떤 부동소수든. (예: 10, 3.14, -100E+13)
  • Numeric – 어떤 정수·부동소수·유리수든 (1, 3.4, 1/3)
  • DecimalIntegerInteger와 같지만 8진수 형식은 없어요.
  • OctalIntegerInteger와 같지만 10진수 형식은 없어요.
  • DecimalNumeric – 10진 정수 또는 부동소수.
  • TrueClass – '+ yes true - no false'를 받고 기본값은 true
  • FalseClassTrueClass와 같지만 기본값은 false
  • Array – ','로 구분된 문자열 (예: 1,2,3)
  • Regexp – 정규표현식. 옵션도 포함돼요.

자체 강제 변환을 추가할 수도 있는데, 아래에서 다뤄요.

내장 변환 사용하기

내장 Time 변환을 예로 들어 볼게요. 다른 내장 변환도 동일하게 동작해요. OptionParser는 인자를 Time으로 파싱하려 시도해요. 성공하면 그 시간을 핸들러 블록에 넘기고, 실패하면 예외를 발생시켜요.

require 'optparse'
require 'optparse/time'
OptionParser.new do |parser|
  parser.on("-t", "--time [TIME]", Time, "Begin execution at given time") do |time|
    p time
  end
end.parse!

실행 결과:

$ ruby optparse-test.rb  -t nonsense
... invalid argument: -t nonsense (OptionParser::InvalidArgument)
$ ruby optparse-test.rb  -t 10-11-12
2010-11-12 00:00:00 -0500
$ ruby optparse-test.rb  -t 9:30
2014-08-13 09:30:00 -0400

커스텀 변환 만들기

OptionParseraccept 메서드로 변환기를 만들 수 있어요. 클래스가 지정될 때마다 어떤 변환 블록을 호출할지 지정하는 거예요. 아래 예제는 on 핸들러가 받기 전에 User 객체를 가져오는 데 사용해요.

require 'optparse'

User = Struct.new(:id, :name)

def find_user id
  not_found = ->{ raise "No User Found for id #{id}" }
  [ User.new(1, "Sam"),
    User.new(2, "Gandalf") ].find(not_found) do |u|
    u.id == id
  end
end

op = OptionParser.new
op.accept(User) do |user_id|
  find_user user_id.to_i
end

op.on("--user ID", User) do |user|
  puts user
end

op.parse!

실행 결과:

$ ruby optparse-test.rb --user 1
#<struct User id=1, name="Sam">
$ ruby optparse-test.rb --user 2
#<struct User id=2, name="Gandalf">
$ ruby optparse-test.rb --user 3
optparse-test.rb:15:in 'block in find_user': No User Found for id 3 (RuntimeError)

옵션을 해시로 저장하기

order, parse 등의 메서드의 into 옵션은 명령줄 옵션을 해시에 저장해요.

require 'optparse'

options = {}
OptionParser.new do |parser|
  parser.on('-a')
  parser.on('-b NUM', Integer)
  parser.on('-v', '--verbose')
end.parse!(into: options)

p options

실행 결과:

$ ruby optparse-test.rb -a
{:a=>true}
$ ruby optparse-test.rb -a -v
{:a=>true, :verbose=>true}
$ ruby optparse-test.rb -a -b 100
{:a=>true, :b=>100}

완전한 예제

다음 예제는 완전한 Ruby 프로그램이에요. 직접 실행하며 다양한 옵션의 효과를 볼 수 있어요. optparse의 특징을 배우는 가장 좋은 방법일 거예요.

require 'optparse'
require 'optparse/time'
require 'ostruct'
require 'pp'

class OptparseExample
  Version = '1.0.0'

  CODES = %w[iso-2022-jp shift_jis euc-jp utf8 binary]
  CODE_ALIASES = { "jis" => "iso-2022-jp", "sjis" => "shift_jis" }

  class ScriptOptions
    attr_accessor :library, :inplace, :encoding, :transfer_type,
                  :verbose, :extension, :delay, :time, :record_separator,
                  :list

    def initialize
      self.library = []
      self.inplace = false
      self.encoding = "utf8"
      self.transfer_type = :auto
      self.verbose = false
    end

    def define_options(parser)
      parser.banner = "Usage: example.rb [options]"
      parser.separator ""
      parser.separator "Specific options:"

      # add additional options
      perform_inplace_option(parser)
      delay_execution_option(parser)
      execute_at_time_option(parser)
      specify_record_separator_option(parser)
      list_example_option(parser)
      specify_encoding_option(parser)
      optional_option_argument_with_keyword_completion_option(parser)
      boolean_verbose_option(parser)

      parser.separator ""
      parser.separator "Common options:"
      # No argument, shows at tail.  This will print an options summary.
      # Try it and see!
      parser.on_tail("-h", "--help", "Show this message") do
        puts parser
        exit
      end
      # Another typical switch to print the version.
      parser.on_tail("--version", "Show version") do
        puts Version
        exit
      end
    end

    def perform_inplace_option(parser)
      # Specifies an optional option argument
      parser.on("-i", "--inplace [EXTENSION]",
                "Edit ARGV files in place",
                "(make backup if EXTENSION supplied)") do |ext|
        self.inplace = true
        self.extension = ext || ''
        self.extension.sub!(/\A\.?(?=.)/, ".")  # Ensure extension begins with dot.
      end
    end

    def delay_execution_option(parser)
      # Cast 'delay' argument to a Float.
      parser.on("--delay N", Float, "Delay N seconds before executing") do |n|
        self.delay = n
      end
    end

    def execute_at_time_option(parser)
      # Cast 'time' argument to a Time object.
      parser.on("-t", "--time [TIME]", Time, "Begin execution at given time") do |time|
        self.time = time
      end
    end

    def specify_record_separator_option(parser)
      # Cast to octal integer.
      parser.on("-F", "--irs [OCTAL]", OptionParser::OctalInteger,
                "Specify record separator (default \\0)") do |rs|
        self.record_separator = rs
      end
    end

    def list_example_option(parser)
      # List of arguments.
      parser.on("--list x,y,z", Array, "Example 'list' of arguments") do |list|
        self.list = list
      end
    end

    def specify_encoding_option(parser)
      # Keyword completion.  We are specifying a specific set of arguments (CODES
      # and CODE_ALIASES - notice the latter is a Hash), and the user may provide
      # the shortest unambiguous text.
      code_list = (CODE_ALIASES.keys + CODES).join(', ')
      parser.on("--code CODE", CODES, CODE_ALIASES, "Select encoding",
                "(#{code_list})") do |encoding|
        self.encoding = encoding
      end
    end

    def optional_option_argument_with_keyword_completion_option(parser)
      # Optional '--type' option argument with keyword completion.
      parser.on("--type [TYPE]", [:text, :binary, :auto],
                "Select transfer type (text, binary, auto)") do |t|
        self.transfer_type = t
      end
    end

    def boolean_verbose_option(parser)
      # Boolean switch.
      parser.on("-v", "--[no-]verbose", "Run verbosely") do |v|
        self.verbose = v
      end
    end
  end

  #
  # Return a structure describing the options.
  #
  def parse(args)
    # The options specified on the command line will be collected in
    # *options*.

    @options = ScriptOptions.new
    @args = OptionParser.new do |parser|
      @options.define_options(parser)
      parser.parse!(args)
    end
    @options
  end

  attr_reader :parser, :options
end  # class OptparseExample

example = OptparseExample.new
options = example.parse(ARGV)
pp options # example.options
pp ARGV

셸 완성(Shell Completion)

bash, zsh 같은 현대적인 셸에서는 명령줄 옵션에 대해 셸 완성을 사용할 수 있어요.

추가 문서

위 예제들과 함께 제공되는 Tutorial이면 이 클래스를 쓰는 법을 배우기에 충분해요. 질문이 있다면 bugs.ruby-lang.org에 티켓을 올려 주세요.

Constants

DecimalInteger

10진 정수 형식. Integer로 변환돼요.

DecimalNumeric

10진 정수/부동소수 형식. 정수 형식은 Integer로, 부동소수 형식은 Float로 변환돼요.

OctalInteger

Ruby/C 스타일 8진수/16진수/2진수 정수 형식. Integer로 변환돼요.

VERSION

버전 문자열

Version

호환용 별칭

Attributes

요약 앞에 붙는 제목 배너(banner). 기본적으로 파싱될 문자열들. 오류 메시지와 기본 배너에 나타날 프로그램 이름(기본 $0). 알 수 없는 옵션에서 예외를 던질지 여부. 릴리스 코드. 옵션이 정확히 일치해야 하는지(축약된 긴 옵션을 짧은 옵션으로 제공하는 것을 허용하지 않음) 여부. 요약 앞에 붙는 제목 배너. 오류 메시지와 기본 배너에 나타날 프로그램 이름(기본 $0) — program_name. 요약 들여쓰기(String 또는 + String 메서드를 가져야 함) — summary_indent. 요약의 옵션 목록 부분 너비(Numeric이어야 함) — summary_width. 요약 들여쓰기 — summary_indent. 요약의 옵션 목록 부분 너비 — summary_width. 버전.

Public Class Methods

accept(*args, &blk)

accept 메서드를 참고하세요.

getopts(*args, symbolize_names: false)

getopts 메서드를 참고하세요.

inc(arg, default = nil)

arg에 따라 default를 증가시킨 값을 돌려줘요.

new(banner = nil, width = 32, indent = ' ' * 4) { |self| ... }

인스턴스를 초기화하고, 블록과 함께 호출되면 self를 yield 해요. 파라미터는 banner(배너 메시지), width(요약 너비), indent(요약 들여쓰기)예요.

reject(*args, &blk)

reject 메서드를 참고하세요.

show_version(*pkgs)

Version이 정의된 패키지에서 버전 문자열을 보여줘요. pkgs는 패키지 목록이에요.

terminate(arg = nil)

terminate 메서드를 참고하세요.

top()

전역 top 옵션 목록을 돌려줘요. 직접 쓰지 마세요.

with(*args, &block)

새 인스턴스를 초기화하고 인스턴스의 컨텍스트에서 선택적 블록을 평가해요. 인자 argsnew에 전달돼요. 이 메서드는 deprecated이고, 그 동작은 이전 new 메서드에 해당해요.

Public Instance Methods

abort(mesg = $!)

프로그램 이름과 함께 메시지를 보여준 후 중단해요. mesg는 메시지로, 기본 $!이에요. Kernel#abort를 참고하세요. 슈퍼클래스 메서드 Kernel#abort를 호출해요.

accept(*args, &blk)

지정된 클래스 t를 받아들이도록 지시해요. 인자 문자열은 그 클래스로 변환돼야 하는 블록에 전달돼요. t는 인자 클래스 지정자로, Class를 포함한 어떤 객체든 가능해요. pat는 인자의 패턴으로, tmatch에 응답하면 기본 t예요.

accept(t, pat, &block)

additional_message(typ, opt)

추가 정보를 돌려줘요.

banner()

요약 앞에 붙는 제목 배너.

base()

on_tail의 대상(subject).

candidate(word)

word에 대한 후보를 돌려줘요.

define(*params, &block)

주어진 파라미터 params로 옵션을 만들어요. 블록이 주어지면 만들어진 옵션의 핸들러예요. 명령줄 파싱 중 그 옵션을 만나면, 블록은 그 옵션에 주어진 인자(있다면)와 함께 호출돼요.

define_by_keywords(options, method, **params)

주어진 파라미터 params로 옵션을 만들어요. method의 키워드 파라미터에 대한 options로 설정되는 옵션들을 정의해요. 각 키워드의 파라미터는 params의 요소로 주어져요.

define_head(*params, &block)

define과 같지만, 만들어진 옵션이 요약의 머리(head)에 추가돼요.

define_tail(*params, &block)

define과 같지만, 만들어진 옵션이 요약의 꼬리(tail)에 추가돼요.

environment(env = File.basename($0, '.*'), **keywords)

환경 변수 env(또는 그 대문자)를 셸처럼 쪼개서 파싱해요. env는 기본적으로 프로그램의 basename이에요.

getopts(*args, symbolize_names: false, **keywords)

getopts.rb의 래퍼 메서드예요.

params = ARGV.getopts("ab:", "foo", "bar:", "zot:Z;zot option")
# params["a"] = true   # -a
# params["b"] = "1"    # -b1
# params["foo"] = "1"  # --foo
# params["bar"] = "x"  # --bar x
# params["zot"] = "z"  # --zot Z

symbolize_names(불리언) 옵션은 돌려받는 해시 키가 심볼이어야 하는지 지정해요. 기본값은 false(문자열 사용)예요.

params = ARGV.getopts("ab:", "foo", "bar:", "zot:Z;zot option", symbolize_names: true)
# params[:a] = true   # -a
# params[:b] = "1"    # -b1
# params[:foo] = "1"  # --foo
# params[:bar] = "x"  # --bar x
# params[:zot] = "z"  # --zot Z

help()

옵션 요약 문자열을 돌려줘요.

inc(*args)

self.inc를 참고하세요.

load(filename = nil, **keywords)

filename 파일에서 옵션을 로드해요. 파일이 없으면 아무것도 하지 않아요. 성공적으로 로드됐는지 돌려줘요. filename은 기본적으로 XDG와 Haiku 표준 위치 아래의 ~/.options 디렉터리에서 프로그램 basename을 접미사 없이 쓴 다음, '.options' 접미사를 붙인 basename을 사용해요. 선택적 into 키워드 인자는 parse 메서드에서 받는 것과 똑같이 동작해요.

make_switch(params, block = nil)

주어진 파라미터 params로 옵션을 만들어요. 블록이 주어지면 만들어진 옵션의 핸들러예요.

new() { |self| ... }

List를 push 해요. 블록이 주어지면 self를 yield 하고 블록의 결과를 돌려주고, 아니면 self를 돌려줘요.

on(*params, &block)

주어진 파라미터 params로 옵션을 만들어요. 블록은 옵션의 핸들러예요.

on_head(*params, &block)

on과 같지만, 새 옵션이 요약의 머리에 추가돼요.

on_tail(*params, &block)

on과 같지만, 새 옵션이 요약의 꼬리에 추가돼요.

order(*argv, **keywords, &nonopt)

명령줄 인자 argv를 순서대로 파싱해요. 블록이 주어지면 각 비옵션 인자를 yield 해요. 선택적 into 키워드 인자가 주어지면 파싱된 옵션 값이 []= 메서드로 그곳에 저장돼요(Hash, OpenStruct, 또는 비슷한 객체일 수 있어요). 파싱되지 않고 남은 argv의 나머지를 돌려줘요.

order!(argv = default_argv, into: nil, **keywords, &nonopt)

order와 같지만 스위치를 파괴적으로 제거해요. 비옵션 인자는 argv에 남아요.

parse(*argv, **keywords)

POSIXLY_CORRECT 환경 변수가 설정돼 있으면 인자를 순서대로 파싱하고, 그 외에는 permutation 모드로 파싱해요. 선택적 into 키워드 인자가 주어지면 파싱된 옵션 값이 []= 메서드로 저장돼요.

parse!(argv = default_argv, **keywords)

parse와 같지만 스위치를 파괴적으로 제거해요. 비옵션 인자는 argv에 남아요.

permute(*argv, **keywords)

명령줄 인자를 permutation 모드로 파싱하고 비옵션 인자 목록을 돌려줘요. 선택적 into 키워드 인자가 주어지면 파싱된 옵션 값이 []= 메서드로 저장돼요.

permute!(argv = default_argv, **keywords)

permute와 같지만 스위치를 파괴적으로 제거해요. 비옵션 인자는 argv에 남아요.

program_name()

오류 메시지와 기본 배너에 나타날 프로그램 이름. 기본 $0.

reject(*args, &blk)

지정된 클래스 인자를 거부하도록 지시해요. type은 인자 클래스 지정자로, Class를 포함한 어떤 객체든 가능해요.

reject(type)

release()

릴리스 코드.

remove()

마지막 List를 제거해요.

separator(string)

요약에 구분자를 추가해요.

summarize(to = [], width = @summary_width, max = width - 1, indent = @summary_indent, &blk)

옵션 요약을 to에 넣고 to를 돌려줘요. 블록이 주어지면 각 줄을 yield 해요. to<< 메서드를 가져야 하는 출력 대상(기본 []), width는 왼쪽 너비(기본 @summary_width), max는 왼쪽에 허용되는 최대 길이(기본 width - 1), indent는 들여쓰기(기본 @summary_indent)예요.

terminate(arg = nil)

옵션 파싱을 종료해요. 선택적 인자 arg는 첫 비옵션 인자가 될 문자열로 다시 밀어 넣어져요.

to_a()

옵션 요약 목록을 돌려줘요.

to_s()

top()

on/on_head, accept/reject의 대상(subject).

ver()

program_name, version, release에서 버전 문자열을 돌려줘요.

version()

버전.

warn(mesg = $!)

프로그램 이름과 함께 경고 메시지를 보여줘요. mesg는 메시지로, 기본 $!이에요. Kernel#warn을 참고하세요. 슈퍼클래스 메서드 Kernel#warn을 호출해요.