5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Ruby] コマンドライン用フレームワーク「Benry-CmdApp」の紹介

Last updated at Posted at 2023-12-25

2023年 Ruby アドベントカレンダー 8 日目の記事です。もともと別の方(dzsさん)がエントリーしていましたが、キャンセルされたようなので、代打を務めます。記事公開が最終日になったのは、キャンセルに気づいたのが 2、3 日前だったからです。ご理解をお願いします。)

サブコマンドを受け取るタイプのコマンドラインアプリケーション用に、「Benry-CmdApp」というフレームワークを Ruby で開発したので紹介します。

概要

Git や Docker や Gem のコマンドは、サブコマンド(またはアクション)を受け取ります。またサブコマンドごとに異なるオプションを受けつけます。

$ git commit -m "message"          # 「commit」がサブコマンド

$ docker run -it "ubuntu:latest"   # 「run」がサブコマンド

$ gem install -N rails             # 「install」がサブコマンド

Rake でも似たようなことはできますが、Rake には次のような欠点があります。

  • サブコマンド(Rake では「タスク」)ごとのオプションを定義できない。
  • サブコマンドに任意個のコマンドライン引数を指定できない。指定するとそれは別のタスク名として扱われる。
  • サブコマンドのヘルプ機能が貧弱で、かつカスタマイズができない。

Rake(や Make)はもともと「タスクとその依存関係を記述する」というのが目的であり、「サブコマンドごとにオプションや引数を定義する」という用途には向いてない、つまり Git や Docker のようなコマンドラインアプリケーションには向いていないのです。

そこで、「Benry-CmdApp」というフレームワークを作りました。これを使うと、サブコマンド(またはアクション)ごとにオプションや引数が定義できます。基本となるアイデアは、たとえばコマンドラインで <command> hello --lang=en Alice を実行すると、Ruby の hello("Alice", lang: "en") が呼び出されるというものです。

インストール

$ gem install -N benry-cmdapp

基本的な使い方

アクションを定義

Benry-CmdApp では、サブコマンドは「アクション」と呼びます。アクションは、Action クラスのサブクラスの中でメソッドとして定義します。このメソッドを「アクションメソッド」といいます。これは xUnit において TestCase クラスのサブクラスにテストメソッドを定義するのと同様です。

ただし「メソッド名が test_ で始まるとテストメソッド」というルールとは違い、Python の関数アノテーションに似た書き方を使っています。

File: ex01.rb

require 'benry/cmdapp'

class SampleAction < Benry::CmdApp::Action      # !!!

  ## アクション(=サブコマンド)を定義
  @action.("print greeting message")            # !!!
  def hello()
    puts "Hello, world!"
  end

end

## コマンドラインアプリケーションを実行
version = "1.0.0"
status_code = Benry::CmdApp.main("sample app", version)  # !!!
exit(status_code)      # status_code -- 0: OK, 1: Error

ここで6行目に @action.() という書き方をしています。これは @action.call() と同じです。

  • Rubyでは call() というメソッドは特別扱いされており、obj.call(args) という呼び出しはメソッド名を省略してobj.(args) という呼び出し方ができます。
  • また Ruby のインスタンス変数には @ という接頭辞をつけます。

この 2 つの言語仕様を利用して、Python の関数デコレータのような書き方を実現しています。ただし関数デコレータとは違うので、@action() のように「.」をなくした呼び出し方はできません。

アクションを実行するには、コマンドラインでアクション名を指定します。すると対応するアクションメソッドが実行されます。

実行例:

## アクションを実行する
$ ruby ex01.rb hello       # アクション名「hello」を指定する
Hello, world!

Rake との違いを今一度説明すると、Rake はコマンドラインアプリケーションなので専用の rake コマンドを使いますが、Benry-CmdApp はフレームワークなので専用のコマンドは存在せず、ruby コマンドで起動するか、または chmod +x ex01.rb のようにしてスクリプトに実行属性をつけてから実行します。

さて、上の実行例では「hello」というアクション名を指定しましたが、アクション名を省略した場合は利用可能なアクション名の一覧が表示されます。Rake のようにデフォルトタスクを実行するのとは違うことに注意してください。

## アクションの一覧を表示
$ ruby ex01.rb                # または「ruby ex01.rb -l」
Actions:
  hello              : print greeting message
  help               : print help message (of action if specified)

上の実行例を見ると、「help」というアクションも定義されていることがわかります。これはフレームワークが自動的に定義するアクションであり、実行するとヘルプを表示します。詳しくは後ほど説明します。

アクション名

Benry-CmdApp では、アクションメソッド名がそのままアクション名になります。ただしいくつか改変ルールがあります。

  • アクションメソッド名の末尾の「_」は取り除かれます。たとえば def print_() というアクションメソッドがあれば、アクション名は「print」になりなす。これは Ruby の予約後や、Kernel モジュールのメソッドと同じ名前のアクションメソッドを定義するときに便利です。
  • アクションメソッド名に含まれる「__」(2 つの連続した「_」)は、「:」に変換されます。たとえば def foo__bar__baz() は「foo:bar:baz」というアクション名になります。
  • アクションメソッド名に含まれる「_」は、「-」に変換されます。たとえば def foo_bar_baz() は「foo-bar-baz」というアクション名になります。

なおアクション名に関係するのはアクションメソッド名だけであり、クラス名は関係しません。

ヘルプメッセージ

先ほど説明したように、Benry-CmdApp では自動的に「help」アクションが定義されます。ヘルプメッセージを表示するにはこの「help」アクションを実行するか、または「-h」や「--help」オプションを指定します。

## ヘルプを表示
$ ruby ex01.rb help           # または「-h」や「--help」オプションを指定
ex01.rb (1.0.0) --- sample app

Usage:
  $ ex01.rb [<options>] <action> [<arguments>...]

Options:
  -h, --help         : print help message (of action if specified)
  -V, --version      : print version
  -l, --list         : list actions and aliases
  -a, --all          : list hidden actions/options, too

Actions:
  hello              : print greeting message
  help               : print help message (of action if specified)

デフォルトではこのようなヘルプメッセージが表示されます。また Qiita の記法(Markdown)では表現できませんが、実際の画面では色がつきます。Benry-CmdApp はフレームワークなので、ヘルプメッセージを大幅にカスタマイズする方法が提供されます。詳しくはドキュメントを参照してください。

「help」アクション(または「-h」や「--help」オプション)に引数としてアクション名を指定すると、そのアクションのヘルプが表示されます。これはコマンドラインアプリケーションのヘルプとは違うことに注意してください。

## 「hello」アクションのヘルプを表示する
$ ruby ex01.rb -h hello        # または ruby ex01.rb help hello
ex01.rb hello --- print greeting message

Usage:
  $ ex01.rb hello

これも実際には色つきで表示され、またカスタマイズができます。

なおアクション名のあとに「-h」または「--help」を指定しても、アクションのヘルプメッセージが表示されます。

## 「hello」アクションのヘルプを表示する
$ ruby ex01.rb hello -h        # または ruby ex01.rb hello --help
ex01.rb hello --- print greeting message

Usage:
  $ ex01.rb hello

つまりこれは、アクションを定義すると自動的に「-h, --help」オプションも追加されるということです。そのため、アクションに独自の「-h」オプションや「--help」オプションを追加するのは避けましょう。

アクションの引数

アクション(=サブコマンド)は引数を受け取れます。コマンドラインにおいてアクションに引数を指定すると、それらがアクションメソッドの引数として渡されます。

File: ex02.rb

require 'benry/cmdapp'

class SampleAction < Benry::CmdApp::Action

  ## 引数をとるアクション
  @action.("print greeting message")
  def hello(name="world")             # !!!
    puts "Hello, #{name}!"
  end

end

## コマンドラインアプリケーションを実行
status_code = Benry::CmdApp.main("sample app", "1.0.0")
exit(status_code)      # status_code -- 0: OK, 1: Error

実行例:

## 「hello」アクションに「Alice」という引数を指定する
$ ruby ex02.rb hello Alice
Hello, Alice!

## 「hello」アクションに何も引数を指定しなかったとき
$ ruby ex02.rb hello
Hello, world!

Benry-CmdApp では、コマンドラインでアクション名を指定すると、それに対応した Ruby 側のアクションメソッドが呼び出されます。そのため、コマンドラインでの引数がそのままアクションメソッドの引数になるのが自然かつ直感的な仕様です。上の例では引数が 1 つだけでしたが、2 つ以上の引数も定義できるし、(上の例のように)引数にデフォルト値を指定すれば省略可能な引数となります。

これが Rake だと、rake hello name=Alicerake hello[Alice] のような不自然な指定をしなければいけません。なぜなら Rake では rake hello Alice のように指定すると、「hello」タスクと「Alice」タスクを指定したことになるからです。これでは git コマンドや docker コマンドのようなコマンドラインアプリケーションは作れないでしょう。

ヘルプメッセージにおける引数名

アクションメソッドの引数名は、ヘルプメッセージにも表示されます。このとき、次のルールに則って表示を変えます。

  • 引数 foo が通常の引数なら、<foo> と表示される。
  • デフォルト引数なら、[<foo>] と表示される。
  • 可変長引数(つまり *foo)なら、[<foo>...] と表示される。
  • 可変長引数が *foo_ でその前の引数が foo なら、2 つを合わせて <foo>... と表示される。

また引数名に含まれる「_」によって、次のようなルールで表示されます。

  • aaa_bba_cccaaa-bbb-ccc と表示される。
  • aaa_or_bbb_or_cccaaa|bbb|ccc と表示される。
  • aaa__htmlaaa.html と表示される。

File: ex03.rb

require 'benry/cmdapp'

class SampleAction < Benry::CmdApp::Action

  ## 通常の引数、デフォルト引数、可変長引数
  @action.("sample action #1")
  def sample1(foo, bar=nil, *baz)   # !!!
    nil
  end

  ## それぞれ `aa-bb-cc`, `aa|bb|cc`, `aaa.html` と表示される
  @action.("sample action #2")
  def sample2(aa_bb_cc, aa_or_bb_or_cc, aaa__html)   # !!!
    nil
  end

end

## コマンドラインアプリケーションを実行
status_code = Benry::CmdApp.main("sample app")
exit(status_code)      # status_code -- 0: OK, 1: Error

実行例:

## 通常の引数は `<foo>`, デフォルト引数は `[<bar>]`、可変長引数は `[<baz>...]` と表示される
$ ruby ex03.rb -h sample1
ex03.rb sample1 --- sample action #1

Usage:
  $ ex03.rb sample1 <foo> [<bar> [<baz>...]]

## 引数名に含まれる「_」によって表示が変わる
$ ruby ex03.rb -h sample2
ex03.rb sample2 --- sample action #2

Usage:
  $ ex03.rb sample2 <aa-bb-cc> <aa|bb|cc> <aaa.html>

アクションのオプション

アクション(=サブコマンド)には「-v」や「-f file」のようなオプションを定義できます。コマンドラインで指定したオプション値は、Ruby のアクションメソッドにキーワード引数として渡されます。

そもそもコマンドラインのオプションというものは、指定してもしなくてもいいという性質を持っています。なのでこれをキーワード引数として渡すというのは、とても直感的な仕様です。

File: ex04.rb

require 'benry/cmdapp'

class SampleAction < Benry::CmdApp::Action

  ## オプション「-l <lang>」を定義
  @action.("print greeting message")
  @option.(:lang, "-l <en|fr|it>", "language")   # !!!
  def hello(name="world", lang: "en")            # !!!
    case lang
    when "en" ; puts "Hello, #{name}!"
    when "fr" ; puts "Bonjour, #{name}!"
    when "it" ; puts "Chao, #{name}!"
    else      ; raise option_error("#{lang}: Unknown language.")
    end
  end

end

## コマンドラインアプリケーションを実行
status_code = Benry::CmdApp.main("sample app", "1.0.0")
exit(status_code)

@option.(:lang, "-l <en|fr|it>", "language") の部分は次のように指定します。

  • 第 1 引数には、オプションの名前(=オプション名)をシンボルで指定します。このオプション名とキーワード引数名とは一致させてください。
  • 第 2 引数には、オプション定義を指定します。この例ではショートオプションだけを指定していますが、ロングオプションも指定できます(詳しくは後述)。またアクションのヘルプメッセージではそのまま表示されます。
  • 第 3 引数には、オプションの摘要(=簡単な説明)を指定します。アクションのヘルプメッセージでそのまま表示されます。

実行例:

## オプション「-l <lang>」を指定する
$ ruby ex04.rb hello -l fr Alice           # 「-l fr」を指定
Bonjour, Alice!

## オプションは引数のあとに指定してもよい
$ ruby ex04.rb hello Alice -l it           # 「-l it」を指定
Chao, Alice!

## オプションは省略できる
$ ruby ex04.rb hello Alice
Hello, Alice!

ヘルプメッセージ:

$ ruby ex04.rb -h hello
ex04.rb hello --- print greeting message

Usage:
  $ ex04.rb hello [<options>] [<name>]

Options:
  -l <en|fr|it>      : language

オプション定義

@option.() の第 2 引数では、オプション定義を次のように指定します。

  • 引数なし
    • ショート: "-v"
    • ロング: "--verbose"
    • ショート&ロング: "-v, --verbose"
  • 引数あり(必須)
    • ショート: "-f <file>"
    • ロング: "--file=<file>"
    • ショート&ロング: "-f, --file=<file>"
  • 引数あり(省略可能)
    • ショート: "-i[<width>]"
    • ロング: "--indent[=<width>]"
    • ショート&ロング: "-i, --indent[=<width>]"

オプションの引数は何を指定してもいいです。-f <file> でもいいし、-f FILE でも -f <file|dir> でもいいです。好きな形式を使ってもらっていいのですが、こだわりがなければ -f <file> という形式がいいでしょう。

File: ex05.rb

require 'benry/cmdapp'

class SampleAction < Benry::CmdApp::Action

  ## ショートオプション
  @action.("test for short options")
  @option.(:verbose, "-v", "verbose mode")
  @option.(:file   , "-f <file>", "file name")
  @option.(:indent , "-i[<width>]", "indent width")
  def short(verbose: false, file: nil, indent: 0)
    puts "verbose=#{verbose.inspect}, file=#{file.inspect}, indent=#{indent.inspect}"
  end

  ## ロングオプション
  @action.("test for long options")
  @option.(:verbose, "--verbose", "verbose mode")
  @option.(:file   , "--file=<file>", "file name")
  @option.(:indent , "--indent[=<width>]", "indent width")
  def long(verbose: false, file: nil, indent: 0)
    puts "verbose=#{verbose.inspect}, file=#{file.inspect}, indent=#{indent.inspect}"
  end

  ## ショート&ロングオプション
  @action.("test for short & long options")
  @option.(:verbose, "-v, --verbose", "verbose mode")
  @option.(:file   , "-f, --file=<file>", "file name")
  @option.(:indent , "-i, --indent[=<width>]", "indent width")
  def both(verbose: false, file: nil, indent: 0)
    puts "verbose=#{verbose.inspect}, file=#{file.inspect}, indent=#{indent.inspect}"
  end

end

## コマンドラインアプリケーションを実行
status_code = Benry::CmdApp.main("sample app", "1.0.0")
exit(status_code)

実行例:

## ショートオプション
$ ruby ex05.rb short -v -f foo.txt -i4
verbose=true, file="foo.txt", indent="4"

## ロングオプション
$ ruby ex05.rb long --verbose --file=foo.txt --indent=4
verbose=true, file="foo.txt", indent="4"

## ショート&ロングオプション
$ ruby ex05.rb both -v -f foo.txt -i4
verbose=true, file="foo.txt", indent="4"
$ ruby ex05.rb both --verbose --file=foo.txt --indent=4
verbose=true, file="foo.txt", indent="4"

この実行例で分かるように、引数なしのオプションをコマンドラインで指定した場合は、対応するキーワード引数に true が入ります。引数ありのオプションなら、文字列が入ります。整数など別のデータ型へ変更する方法は後ほど説明します。

オプション定義の文字列は、そのままヘルプメッセージの中に含まれて表示されます。

実行例:

$ ruby ex05.rb -h short
ex05.rb short --- test for short options

Usage:
  $ ex05.rb short [<options>]

Options:
  -v                 : verbose mode
  -f <file>          : file name
  -i[<width>]        : indent width

$ ruby ex05.rb -h long
ex05.rb long --- test for long options

Usage:
  $ ex05.rb long [<options>]

Options:
  --verbose          : verbose mode
  --file=<file>      : file name
  --indent[=<width>] : indent width

$ ruby ex05.rb -h both
ex05.rb both --- test for short & long options

Usage:
  $ ex05.rb both [<options>]

Options:
  -v, --verbose      : verbose mode
  -f, --file=<file>  : file name
  -i, --indent[=<width>] : indent width

オプション引数のバリデーション

オプション引数の値はバリデーションできます。そのためには @option.() に次のようなキーワード引数を指定します。

  • type: Integertype: TrueClass のようにデータ型のクラスを指定します。
  • rexp: /\A\d+\z/ のように正規表現を指定します。
  • enum: ["A", "B", "C"]enum: [10, 20, 30] のように取りうる値を指定します。
  • range: 1..5 のように値の範囲を指定します。range: (0..) だと「0 以上」になります。

File: ex06.rb

require 'benry/cmdapp'

class SampleAction < Benry::CmdApp::Action

  ## バリデーションのサンプル
  @action.("validation sample")
  @option.(:repeat , "-r <num>"  , "repeat",
                     type: Integer, range: 1..5)   # !!!
  @option.(:lang   , "-l <lang>" , "language",
                     rexp: /\A[a-z]{2}\z/, enum: ["en","fr","it"])  # !!!
  def hello(name="world", repeat: 1, lang: "en")
    repeat.times do
      case lang
      when "en" ; puts "Hello, #{name}!"
      when "fr" ; puts "Bonjour, #{name}!"
      when "it" ; puts "Chao, #{name}!"
      else      ; raise option_error("#{lang}: Unknown language.")
      end
    end
  end

end

exit Benry::CmdApp.main("sample app")

実行例:

$ ruby ex06.rb hello -r abc
[ERROR] -r abc: Integer expected.

$ ruby ex06.rb hello -r 100
[ERROR] -r 100: Too large (max: 5)

$ ruby ex06.rb hello -r 0
[ERROR] -r 0: Positive value (>= 1) expected.

$ ruby ex06.rb hello -l english
[ERROR] -l english: Pattern unmatched.

$ ruby ex06.rb hello -l ja
[ERROR] -l ja: Expected one of en/fr/it.

@option.(..., type: Integer, rexp: /\A\d+\z/, range: 1..5, enum: [1, 3, 5])」のキーワード引数名を省略して「@option.(..., Integer, /\A\d+\z/, (1..5), [1, 3, 5])」のように書くこともできますが、お勧めはしません。

@option.() にブロック引数を与えることで、オプション値を任意のコードでバリデーションできます。またブロック引数の戻り値が新しいオプション値になるので、値を変換する用途にも使えます。

File: ex07.rb

require 'benry/cmdapp'

class SampleAction < Benry::CmdApp::Action

  MESSAGES = {
    en: "Hello",
    fr: "Bonjour",
    it: "Chao",
  }

  ## 任意のコードでバリデーション
  @action.("validation sample")
  @option.(:lang, "-l <lang>", "language") {|val|   # !!!
    sym = val.intern                                # !!!
    MESSAGES.key?(sym)  or                          # !!!
      raise "Unknown language."                     # !!!
    sym                                             # !!!
  }                                                 # !!!
  def hello(name="world", lang: :en)
    msg = MESSAGES[lang]
    puts "#{msg}, #{name}!"
  end

end

exit Benry::CmdApp.main("sample app")

実行例:

$ ruby ex07.rb hello -l ja
[ERROR] -l ja: Unknown language.

もちろん、アクションメソッドの中でもバリデーションできます。このほうが分かりやすいという人も多いでしょう。

File: ex08.rb

require 'benry/cmdapp'

class SampleAction < Benry::CmdApp::Action

  MESSAGES = {
    en: "Hello",
    fr: "Bonjour",
    it: "Chao",
  }

  ## 任意のコードでバリデーション
  @action.("validation sample")
  @option.(:lang, "-l <lang>", "language")
  def hello(name="world", lang: "en")
    ## アクション引数とオプション値のエラーでは raise option_error() を使い、
    ## その他のエラーでは raise action_error() を使う。
    sym = lang.intern                                  # !!!
    msg = MESSAGES[sym]  or                            # !!!
      raise option_error("#{lang}: Unknown language.") # !!!
    puts "#{msg}, #{name}!"
  end

end

exit Benry::CmdApp.main("sample app")

実行例:

$ ruby ex08.rb hello -l ko
[ERROR] ko: Unknown language.

Benry-CmdApp ではオプション値をバリデーションする機能はありますが、アクションの引数をバリデーションする機能はありません。アクションメソッドの中で必要に応じて raise option_error() を実行してください。

トグルオプション

引数なしのオプションは、Boolean 型と見なされます。たとえばコマンドラインで「--verbose」を指定すると、キーワード引数「verbose:」には true が入ります(そのため、キーワード引数のデフォルト値は false にする必要があります)。

こうではなく、「--verbose=on」や「--verbose=off」のような指定方法をするには、引数つきの Boolean 型オプションにします。このようなオプションは「トグルオプション」といいます。

File: ex09.rb

require 'benry/cmdapp'

class SampleAction < Benry::CmdApp::Action

  ## トグルオプション: --color=on, --color=off
  @action.("toggle option example")
  @option.(:color, "--color=<on|off>", "color mode", type: TrueClass)  # !!!
  def hello(name="world", color: false)
    if color
      puts "Hello, \e[32m#{name}\e[0m!"
    else
      puts "Hello, #{name}!"
    end
  end

end

exit Benry::CmdApp.main("sample app")

実行例:

## トグルオプションをオン
$ ruby ex09.rb hello --color=on
Hello, world!                       # カラーで表示

## トグルオプションをオフ
$ ruby ex09.rb hello --color=off
Hello, world!                       # モノクロで表示

## トグルオプションを指定せずに実行
$ ruby ex09.rb hello
Hello, world!                       # モノクロで表示

トグルオプションでは、引数として「on/off」だけでなく「true/false」や「yes/no」も受けつけます。プログラマなら「true/false」に慣れていますが、そうではない人には「on/off」や「yes/no」のほうがずっと分かりやすいようです。

実行例:

## トグルオプションをオン
$ ruby ex09.rb hello --color=on
Hello, world!                       # カラーで表示
$ ruby ex09.rb hello --color=true
Hello, world!                       # カラーで表示
$ ruby ex09.rb hello --color=yes
Hello, world!                       # カラーで表示

## トグルオプションをオフ
$ ruby ex09.rb hello --color=off
Hello, world!                       # モノクロで表示
$ ruby ex09.rb hello --color=false
Hello, world!                       # モノクロで表示
$ ruby ex09.rb hello --color=no
Hello, world!                       # モノクロで表示

オプション引数を省略して「--color」と指定したら「--color=on」と同じ意味になるようにするには、オプション引数を非必須にします。そのためには @option.() の第 2 引数を "--color=<on|off>" から "--color[=<on|off>]" にします。

またトグルオプションの意味を逆にしたい、つまりデフォルトではオンで、コマンドラインでオプションが指定されたときにオフにしたい場合は次のようにします。

  • @option.()value: false を指定する。
  • キーワード引数のデフォルト値を true に変更する。

サンプルコードは省略します。

なお他のコマンドでは、オプション「--xxxx」がオン、「--no-xxxx」がオフという仕様であることが多いです。Benry-CmdApp ではこの仕様はサポートしていません(強い理由は無し)。

マルチオプション

Ruby の「-I」オプションや GCC の「-I」オプションは、複数回指定できます。このような複数回指定できるオプションを定義するには、@option.()multiple: true を指定します。

File: ex10.rb

require 'benry/cmdapp'

class SampleAction < Benry::CmdApp::Action

  ## 複数回指定できるオプションのサンプル
  @action.("multiple option example")
  @option.(:dirs, "-I <dirpath>", "directory path", multiple: true)  # !!!
  def test1(dirs: [])
    p paths                #=> ex: paths=["/usr", "/usr/local", "/opt"]
  end

end

exit Benry::CmdApp.main("sample app")

実行結果:

$ ruby ex10.rb test1 -I /usr -I /usr/local -I /opt
["/usr", "/usr/local", "/opt"]

より高度な機能

オプションセット

複数のアクションで同じオプションを使いたいなら、オプションセットを使います。オプションセットは、単数または複数のオプションをグループ化して扱う機能です。

File: ex21.rb

require 'benry/cmdapp'

class SampleAction < Benry::CmdApp::Action

  ## オプションセットを定義
  optset1 = optionset do
    @option.(:quiet, "-q, --quiet", "quiet mode")
    @option.(:debug, "-D, --debug", "debug mode")
  end

  ## オプションセットを使う
  @action.("sample #1")
  @optionset.(optset1)
  def sample1(quiet: false, debug: false)
    puts "quiet=#{quiet.inspect}, debug=#{debug.inspect}"
  end

  ## 別のオプションとともにオプションセットを使う
  @action.("sample #2")
  @option.(:verbose, "-v, --verbose", "verbose mode")
  @optionset.(optset1)
  @option.(:trace, "    --trace", "trace mode")
  def sample2(verbose: false, quiet: false, debug: false, trace: false)
    puts "verbose=#{verbose}, quiet=#{quiet}, debug=#{debug}, trace=#{trace}"
  end

end

exit Benry::CmdApp.main("sample app")

実行例:

## オプション「-q, --quiet」と「-D, --debug」が複数のアクションで共通していることを確認
$ ruby ex21.rb -h sample1
ex21.rb sample1 --- sample #1

Usage:
  $ ex21.rb sample1 [<options>]

Options:
  -q, --quiet        : quiet mode
  -D, --debug        : debug mode

$ ruby ex21.rb -h sample2
ex21.rb sample2 --- sample #2

Usage:
  $ ex21.rb sample2 [<options>]

Options:
  -v, --verbose      : verbose mode
  -q, --quiet        : quiet mode
  -D, --debug        : debug mode
      --trace        : trace mode

これとは別に、既存のアクションのオプション定義をすべてコピーしてしまう機能もあります。詳しくはドキュメントを参照してください。

カテゴリ

Benry-CmdApp では、複数のアクションをカテゴリに分類できます。カテゴリは、Rake でいえば namespace に相当する機能です。

File: ex22.rb

require 'benry/cmdapp'

class GitAction < Benry::CmdApp::Action

  ## カテゴリ「branch:」
  category "branch:" do                   # !!!

    @action.("create a new branch")
    def create(new_branch)
      puts "git branch #{new_branch}"
    end

    @action.("delete an existing branch")
    def delete(branch)
      puts "git branch -d #{branch}"
    end

  end

  ## カテゴリ「tag:」
  category "tag:" do                      # !!!

    @action.("create a new tag")
    def create(tag)
      puts "git tag #{tag}"
    end

  end

end

exit Benry::CmdApp.main("Git wrapper")

カテゴリ名を指定するときは、末尾に「:」をつけてください。現在の仕様では必須です(将来変更の可能性あり)。

実行例:

## カテゴリ名「branch:」と「tag:」がアクション名の前についている
$ ruby ex22.rb -l
Actions:
  branch:create      : create a new branch
  branch:delete      : delete an existing branch
  help               : print help message (of action if specified)
  tag:create         : create a new tag

カテゴリは入れ子にできます。またブロックを使わない書き方もできます。

File: ex23.rb

require 'benry/cmdapp'

class GitBranchAction < Benry::CmdApp::Action
  ## カテゴリ「branch:」
  category "branch:"                    # !!!

  @action.("create a new branch")
  def create(new_branch)
    puts "git branch #{new_branch}"
  end

  @action.("delete an existing branch")
  def delete(branch)
    puts "git branch -d #{branch}"
  end

end

class GitTagAction < Benry::CmdApp::Action
  ## カテゴリ「tag:」
  category "tag:"                       # !!!

  @action.("create a new tag")
  def create(tag)
    puts "git tag #{tag}"
  end

end

exit Benry::CmdApp.main("Git wrapper")

カテゴリと同名のアクションを定義できます。たとえば category "branch:", action: "list" とすると、カテゴリと同名のアクション「branch」が自動的に定義され、その対応するメソッドとして「list()」が使われます。

File: ex24.rb

require 'benry/cmdapp'

class GitAction < Benry::CmdApp::Action

  ## カテゴリ「branch:」
  category "branch:", action: "list" do                   # !!!

    @action.("list branches")
    def list()
      puts "git branch -l"
    end

    @action.("create a new branch")
    def create(new_branch)
      puts "git branch #{new_branch}"
    end

    @action.("delete an existing branch")
    def delete(branch)
      puts "git branch -d #{branch}"
    end

  end

end

exit Benry::CmdApp.main("Git wrapper")

実行例:

## カテゴリと同名のアクションが定義されている
## (アクション「branch:list」は定義されないことに注意)
$ ruby ex24.rb -l
Actions:
  branch             : list branches              ← !!!
  branch:create      : create a new branch
  branch:delete      : delete an existing branch
  help               : print help message (of action if specified)

カテゴリに属するアクションの一覧

コマンドラインにおいてカテゴリ名と「:」とを指定すると、そのカテゴリに属するアクションの一覧が表示されます。またカテゴリ名と同名のアクションも一覧に含まれて表示されます。

実行例:

## 「:」つきでカテゴリ名を指定すると、それに属するアクションの一覧が表示される
$ ruby ex24.rb branch:          # !!!
Actions:
  branch             : list branches
  branch:create      : create a new branch
  branch:delete      : delete an existing branch

ここで単に「ruby ex24.rb」として実行すると、すべてのアクションが一覧表示されます。もしアクションの数が多いと、あるカテゴリのコマンドをその中から探すのは少し面倒です。そのような場合はカテゴリ名を指定すれば、他のカテゴリのアクションは表示されないので探しやすくなります。

カテゴリ名の一覧

先ほどの方法において、カテゴリ名を省略して「:」だけを指定すると、トップレベルのカテゴリが一覧表示されます。「トップレベルのカテゴリ」とは、たとえば「foo:bar:baz:」という入れ子のカテゴリがあれば、「foo:」カテゴリのことです。

この機能は、アクションの数が多い場合にとりあえずどんなカテゴリがあるのかを把握するのに便利です。

実行例:

## トップレベルのカテゴリだけ表示する
$ ruby ex23.rb :               # !!!
Categories: (depth=1)
  branch: (2)
  tag: (1)

カテゴリ名の横の「(2)」や「(1)」は、そのカテゴリに属するアクションの数を表します。この例だと、「branch:」カテゴリには 2 つのアクションがあり、「tag:」カテゴリには 1 つのアクションがあることが分かります。

「:」ではなく「::」を指定すると、トップレベルとその直下のカテゴリが表示されます。「:::」だと、トップレベルから 2 つ下のカテゴリまでが表示されます。

また「:」を指定するかわりに、「-L categories」を指定しても同じ実行結果が得られます。「-L」は隠しオプションなのでヘルプメッセージには出てきませんが、Benry-CmdApp のデフォルト設定では使用可能になっています。

実行例:

## トップレベルのカテゴリだけ表示する
$ ruby ex23.rb -L categories         # !!!
Categories: (depth=0)
  branch: (2)
  tag: (1)

別名(エイリアス)

カテゴリを使うと、アクション名全体がどうしても長くなってしまいます。このようなときに別名(エイリアス)機能を使うと、アクションに短い別名をつけられます。

File: ex25.rb

require 'benry/cmdapp'

class GitAction < Benry::CmdApp::Action

  category "branch:" do

    @action.("list branches")
    def list()
      puts "git branch -l"
    end

    @action.("create a new branch")
    @option.(:switch, "-w, --switch", "switch to new branch")
    def create(new_branch, switch: false)
      puts "git branch #{new_branch}"
      puts "git checkout #{new_branch}" if switch
    end

  end

  ## 「branch:list」に「branches」という別名をつける
  define_alias "branches", "branch:list"              # !!!

  ## 「branch:create --switch」に「fork」という別名をつける
  define_alias "fork", ["branch:create", "--switch"]  # !!!

end

exit Benry::CmdApp.main("Git wrapper")

実行例:

## 別名を使ってアクションを実行する
$ ruby ex25.rb branches           # !!!
git branch -l

$ ruby ex25.rb fork bugfix1       # !!!
git branch bugfix1
git checkout bugfix1

前のセクションでは、「category "branch:", action: "list"」とすることでカテゴリと同名のアクションを定義しましたが、この方法だと「brach:list」アクションが定義されません。「branch:list」は定義したいし「branch」だけで「branch:list」を実行するようにしたいなら、別名を使います。

  ## 「branch」アクションは定義されるが「branch:list」アクションは定義されない
  category "branch:", action: "list" do     # !!!
    @action.("list branches")
    def list(); ....; end
  end

  ## 「branch」は「branch:list」への別名となる(「branch:list」も残る)
  category "branch:" do
    @action.("list branches")
    def list(); ....; end
  end
  define_alias "branch", "branch:list"      # !!!

アクション名の一覧を表示すると、別名も表示されます。

実行例:

## アクション名の一覧を表示すると別名も表示される
$ ruby ex25.rb -l
Actions:
  branch:create      : create a new branch
                       (alias: fork (with '--switch'))
  branch:list        : list branches
                       (alias: branches)
  help               : print help message (of action if specified)

Aliases:
  fork               : alias for 'branch:create --switch'
  branches           : alias for 'branch:list'

アクション名だけを一覧表示したい場合は「-L actions」、別名だけを一覧表示したい場合は「-L aliases」を使ってください。

オプションエラーとアクションエラー

アクションメソッドの中で例外を発生させるなら、次の 2 つを使ってください。この 2 つは Benry-CmdApp が特別な扱いをし、不要なスタックトレースを表示しないでくれます。

  • raise option_error("error message") --- オプションや引数にエラーがあった場合。スタックトレースが表示されず、エラーメッセージだけが表示されます。
  • raise action_error("error message") --- アクションの実行にエラーがあった場合。フレームワークのスタックトレースは表示されず、ユーザスクリプトのスタックトレースだけが表示されます。

これら以外の例外、たとえば ArgumentError や NameError が発生した場合は、通常のスタックトレースが表示されます。

他のアクションの呼び出し

あるアクションの中から他のアクションを呼び出すには、方法が 2 つあります。

  • run_once "<action-name>"」を使うと、他のアクションを 1 回だけ呼び出します。複数回呼び出しても実際に実行されるのは 1 回目だけであり、2 回目以降は無視されます。
  • run_action "<action-name>"」を使うと、他のアクションを何度でも呼び出します。実質的に、通常の関数呼び出しと同じです。

これらには引数(位置引数とキーワード引数)が渡せます。たとえば「`run_once "setup", "build", verbose: true」のようにします。

なお Rake のようなタスクの依存関係を記述する機能は、Benry-CmdApp にはありません。しかし「run_once」を使えば、実質的に Rake と同等のことができます。

後始末処理

アクションメソッドの中で「at_end { .... }」を使うと、後始末処理を登録できます。登録されたブロックはプロセスが終了する前に実行されます。これは特に、初期化処理の中で後始末処理を登録するのに便利です。

File: ex26.rb

require 'benry/cmdapp'
require 'benry/unixcommand'        # fileutils.rb の代替 (gem install benry-unixcommand)

class BuildAction < Benry::CmdApp::Action
  include Benry::UnixCommand

  @action.("setup", )
  def setup()
    mkdir :p, "build"
    at_end {                       # !!!
      rm :rf, "build"              # !!!
    }                              # !!!
  end

  @action.("build something")
  def build()
    run_once "setup"
    pushd "build" do
      puts "... do something ..."
    end
  end

end

exit Benry::CmdApp.main("sample app")

実行例:

## 「at_end {...}」のブロックが最後に実行される
$ mkdir -p build
$ pushd build
... do something ...
$ popd
$ rm -rf build              ← 後始末処理

隠しアクションと隠しオプション

@actions.()」や「@options.()」に「hidden: true」をつけると、そのアクションやオプションは隠しアクションや隠しオプションとなり、ヘルプメッセージに表示されなくなります。ただし「-a, --all」オプションをつけると表示されます。

なお隠しアクションでも実行できるし、隠しオプションもコマンドラインで指定できます。これらは単にヘルプメッセージに表示されないだけであり、それ以外は通常のアクションやオプションと変わりません。

サンプルコードは省略します。

重要度

@actions.()」や「@options.()」に「important: true」をつけると、そのアクションやオプションは重要なものと見なされ、ヘルプメッセージで強調表示(太字で表示)されます。逆に「important: false」をつけると重要ではないと見なされ、ヘルプメッセージではグレー表示されます。

サンプルコードは省略します。

カスタマイズ

アクションのヘルプメッセージをカスタマイズ

アクションのヘルプメッセージは、さまざまなカスタマイズが可能です。そのためには、「@action.()」にキーワード引数を指定します。

File: ex31.rb

require 'benry/cmdapp'

class SampleAction < Benry::CmdApp::Action

  @action.("generate sequence numbers",
           ## 追加の説明用テキスト
           detail: "Generates sequence numbers.",      # !!!
           ## Usage: を 2 行で表示する
           usage: [                                    # !!!
             "<begin> <end>   # sequence from <begin> to <end>",
             "<end>           # sequence from 1 to <end>",
           ],
           ## ヘルプメッセージの終わりに使用例やURLを表示する。
           postamble: {                                # !!!
             "Example:" => <<~END.gsub(/^/, "  "),
               $ ruby ex31.rb seq 1 5   # sequence numbers from 1 to 5
               $ ruby ex31.rb seq 5     # sequence numbers from 1 to 5
             END
             "Document:" => "  https://...",
           })
  def seq(begin_, end_=nil)
    rexp = /\A[-+]?\d+\z/
    begin_ =~ rexp  or raise option_error("#{begin_}: Integer expected.")
    if end_ != nil
      end_ =~ rexp  or raise option_error("#{end_}: Integer expected.")
      begin_ = begin_.to_i
      end_   = end_.to_i
    else
      end_   = begin_.to_i
      begin_ = 1
    end
    puts((begin_ .. end_).to_a)
  end

end

exit Benry::CmdApp.main("sample app", "1.0.0")

実行例:

## アクションのヘルプメッセージがカスタマイズされたことを確認する
$ ruby ex31.rb seq --help
ex31.rb seq --- generate sequence numbers

Generates sequence numbers.

Usage:
  $ ex31.rb seq <begin> <end>   # sequence from <begin> to <end>
  $ ex31.rb seq <end>           # sequence from 1 to <end>

Example:
  $ ruby ex31.rb seq 1 5   # sequence numbers from 1 to 5
  $ ruby ex31.rb seq 5     # sequence numbers from 1 to 5

Document:
  https://...

またオプションのヘルプメッセージもカスタマイズできます。そのためには「@option.()」にキーワード引数を指定します。

File: ex32.rb

require 'benry/cmdapp'

class SampleAction < Benry::CmdApp::Action

  @action.("print greeting message")
  @option.(:lang, "-l <lang>", "language",
             detail: <<~END.gsub(/^/, "  ")          # !!!
	       -l en: English
	       -l fr: French
	       -l it: Italian
	     END
	  )
  def hello(name="world", lang: "en")
    case lang
    when "en" ; puts "Hello, #{name}!"
    when "fr" ; puts "Bonjour, #{name}!"
    when "it" ; puts "Chao, #{name}!"
    else      ; raise option_error("#{lang}: Unknown language.")
    end
  end

end

exit Benry::CmdApp.main("sample app", "1.0.0")

実行例:

## オプションのヘルプメッセージがカスタマイズされたことを確認する
$ ruby ex32.rb hello -h
ex32.rb hello --- print greeting message

Usage:
  $ ex32.rb hello [<options>] [<name>]

Options:
  -l <lang>          : language
                         -l en: English
                         -l fr: French
                         -l it: Italian

Config オブジェクト

今までのサンプルコードでは、次のようなメインプログラムを書いていました。

status_code = Benry::CmdApp.main("sample app", "1.0.0")
exit(status_code)
## または
exit Benry::CmdApp.main("sample app", "1.0.0")

実はこれは、次のような 3 つのステップを実行しているだけです。

  • (1) Config オブジェクトを作成し、
  • (2) それを使って Application オブジェクトを作成し、
  • (3) その Application オブジェクトを実行する。
## (1) Config オブジェクトを作成し、
config = Benry::CmdApp::Config.new("sample app", "1.0.0")
## (2) それを使って Application オブジェクトを作成し、
app = Benry::CmdApp::Application.new(config)
## (3) その Application オブジェクトを実行する。
status_code = app.main()
exit(status_code)

ここで、Config オブジェクトを作成したあとに属性を設定することで、さまざまなカスタマイズができます。どんな属性があるかは、ドキュメントを参照してください。

config = Benry::CmdApp::Config.new("sample app", "1.0.0")

## Config オブジェクトを使ってカスタマイズ
config.app_command = "foobar"   # default: File.basename($0)
config.app_name    = "FooBar"   # default: same as config.app_command
config.help_postamble = {       # { "SectionTitle"=>"SectionDocument" }
  "Examples" => <<END,
  $ foobar --help | less
  $ foobar hello
  $ foobar hello -l en Alice
END
  "Dcoument" => "https://...",
}

app = Benry::CmdApp::Application.new(config)
status_code = app.main()
exit(status_code)

なお Config オブジェクトを使うかわりに、「Benry::CmdApp.main()」にキーワード引数を指定することでもカスタマイズできます。

exit Benry::CmdApp::Config.new("sample app", "1.0.0",
                               app_command: "foobar",   # !!!
                               app_name:    "FooBar")   # !!!

グローバルオプションをカスタマイズする

アクションに指定するオプションではなく、コマンドラインアプリケーションに指定するオプションを「グローバルオプション」といいます。たとえば「-h, --help」や「--version」はグローバルオプションです。

グローバルオプションのカスタマイズは、次のように行います。

  • (1) Config オブジェクトを作成する。
  • (2) グローバルオプション用の Schema オブジェクトを作成し、グローバルオプションを追加する。
  • (3) Application クラスを継承する。
  • (4) グローバルオプションを扱うメソッドを上書きする。
  • (5) Config オブジェクトと Schema オブジェクトを Application オブジェクトに渡す。

試しに、「-q, --quiet」というグローバルオプションを追加してみましょう。

File: ex33.rb

require 'benry/cmdapp'

$QUET_MODE = false

class SampleAction < Benry::CmdApp::Action

  @action.("print greeting message")
  def hello(name="world")
    puts "Hello, #{name}!" unless $QUIET_MODE
  end

end

## (1) Config オブジェクトを作成する。
config = Benry::CmdApp::Config.new("sample app", "1.0.0")

## (2) グローバルオプション用の Schema オブジェクトを作成し、グローバルオプションを追加する。
gschema = Benry::CmdApp::GlobalOptionSchema.new(config)
gschema.add(:quiet, "-q, --quiet", "enable quiet mode")

## (3) Application クラスを継承する。
class MyApplication < Benry::CmdApp::Application

  ## (4) グローバルオプションを扱うメソッドを上書きする。
  def toggle_global_options(global_opts)
    status_code = super
    return status_code if status_code != nil
    if global_opts[:quiet]
      $QUIET_MODE = true
    end
    return nil
  end

end

## (5) Config オブジェクトと Schema オブジェクトを Application オブジェクトに渡す。
app = MyApplication.new(config, gschema)

status_code = app.main()
exit(status_code)

実行例:

## ヘルプメッセージに「-q, --quiet」が追加されている
$ ruby ex33.rb -h
ex33.rb (1.0.0) --- sample app

Usage:
  $ ex33.rb [<options>] <action> [<arguments>...]

Options:
  -h, --help         : print help message (of action if specified)
  -V, --version      : print version
  -l, --list         : list actions and aliases
  -a, --all          : list hidden actions/options, too
  -q, --quiet        : enable quiet mode                   # !!!

Actions:
  hello              : print greeting message
  help               : print help message (of action if specified)

## グローバルオプション「-q, --quiet」が利用できる
$ ruby ex33.rb hello            # メッセージが出力される
Hello, world!
$ ruby ex33.rb -q hello         # quiet モードなので出力されない

上のサンプルコードでは、グローバルオプション用の Schema オブジェクトを生成するとき、Config オブジェクトを渡していました。ここで nil を渡す(つまり何も渡さない)ようにすると、グローバルオプションが何も設定されません。グローバルオプションをゼロから設定する場合はこのようにします。

## Config オブジェクトではなく nil を渡す
gschema = Benry::CmdApp::GlobalOptionSchema.new(nil)
## グローバルオプションをゼロから設定する
schema.add(:help   , "-h, --help"   , "print help message")
schema.add(:version, "-V, --version", "print version")
schema.add(:list   , "-l, --list"   , "list actions and aliases")
schema.add(:all    , "-a, --all"    , "list hidden actions/options, too")

このような方法でグローバルオプションをカスタマイズできますが、かなり面倒ですよね(もっといい方法を検討中)。そこで、よくあるグローバルオプションだけは簡単に設定できるようにしています。それを次に説明します。

よくあるグローバルオプションを追加

「--verbose」や「--debug」といったグローバルオプションはよく使われます。Benry-CmdApp ではこのようなよく使われるグローバルオプションを Config オプションの属性経由で追加できます。

config.option_verbose = true    # enable '-v, --verbose' global option
config.option_quiet   = true    # enable '-q, --quiet' global option
config.option_color   = true    # enable '--color[=<on|off>]' global option
config.option_debug   = true    # enable '-D, --debug' global option
config.option_dryrun  = true    # enable '-X, --dryrun' global option

これらの属性が設定されると、Benry-CmdApp は次のような挙動をします。

  • 「-v, --verbose」が指定されたら「$VERBOSE_MODE = true」を設定する。
  • 「-q, --quiet」が指定されたら「$QUIET_MODE = true」を設定する。
  • 「--color」や「--color=off」が指定されたら「$COLOR_MODE = true/false」を設定する。
  • 「-D, --debug」が指定されたら「$DEBUG_MODE = true」を設定する。
  • 「-X, --dryrun」が指定されたら「$DRYRUN_MODE = true」を設定する。

このように、よくあるグローバルオプションは Config オプションの設定で追加できます。またそれらの挙動は、対応するグローバル変数を設定するだけという簡単なものです。Verbose モードや Quiet モードのときにどんな動作をするかは、アクション次第です。

その他のカスタマイズ

この他、クラスを継承したりメソッドを上書きすることで、ヘルプメッセージをカスタマイズしたり、ログを仕込んだりできます。詳しくはドキュメントを参照してください。

その他のトピック

詳しいことはドキュメントを参照してください。

ロングオプションの引数指定

Benry-CmdApp では、ロングオプションは「=」を使った「--file=foo.txt」形式のみサポートしています。「=」を使わない「--file foo.txt」形式はサポートしないので注意してください。ユーザビリティの観点からこのような仕様になっています。

一般的なコマンドでは、「--foo=bar」と「--foo bar」の両方の形式がサポートされることが多いです。しかしこれだと、たとえば「hogehoge --foo bar」というコマンドがあったときに、「bar」は「--foo」の引数なのか、それとも「hogehoge」の引数なのかが、ぱっと見ではわからず、オプションの仕様を調べる必要があります。

また「--foo」オプションが引数をとらないと思っていたのに実は引数をとるものだったとき、「hogehoge --foo bar」は意図しないオプション引数を指定することになります。つまり「bar」を「hogehoge」の引数として指定したつもりが実は「--foo」の引数になっているのだから、これは意図しない動作を引き起こし、最悪のときはセキュリティの問題を起こす可能性があります。

Benry-CmdApp では「--foo=bar」形式しかサポートしていないので、たとえば「hogehoge --foo bar」というコマンドがあったときは、「bar」は「hogehoge」の引数であって「--foo」の引数ではないことが明白ですし、オプション引数を意図せず指定しまうことを防げます。

ロングオプションを使うのは、ショートオプションと比べて読みやすい・理解しやすいことが理由のはずなので、Benry-CmdApp ではまぎらわしい形式はサポートしないようになっています。

ヘルプ中のロングオプションの位置を揃える

Benry-CmdApp では、オプション定義の文字列(@option.() の第 2 引数)がそのままヘルプメッセージとして表示されます。このことを利用すると、次のようにヘルプメッセージにおけるロングオプションの表示位置を揃えることができます。

File: ex41.rb

require 'benry/cmdapp'

class SampleAction < Benry::CmdApp::Action

  ## ショートオプションとロングオプションが混在しているので、
  ## 半角空白を使ってロングオプションの表示位置を揃える。
  @action.("sample action")
  @option.(:verbose, "-v, --verbose", "verbose mode")
  @option.(:color  , "    --color"  , "color mode")   # !!!
  @option.(:debug  , "    --debug"  , "debug mode")   # !!!
  def sample(verbose: false, color: false, debug: false)
    puts "Hello world!"
  end

end

exit Benry::CmdApp.main("sample app")

実行例:

$ ruby ex41.rb --help sample
ex41.rb sample --- sample action

Usage:
  $ ex41.rb sample [<options>]

Options:
  -v, --verbose      : verbose mode
      --color        : color mode
      --debug        : debug mode

この実行例を見ると、ロングオプションの表示カラムが揃っています。@option.(:color, "--color", ...) だとこうはならず、@option.(:color, " --color", ...) のように半角空白を入れることで表示カラムを揃えています。

アクションのメタデータ化

アクションやオプション定義を YAML 形式のメタデータとして取り出すことができます。そのためにはコマンドラインで「-L metadata」を指定してください。

実行例:

$ ruby ex33.rb -L metadata
actions:
  - action:    hello
    desc:      "print greeting message"
    class:     SampleAction
    method:    hello
    hidden:    false
    paramstr:  "[<name>]"
    parameters:
      - param:   name
        type:    opt
    options:
  - action:    help
    desc:      "print help message (of action if specified)"
    class:     Benry::CmdApp::BuiltInAction
    method:    help
    hidden:    false
    paramstr:  "[<action>]"
    parameters:
      - param:   action
        type:    opt
      - param:   all
        type:    key
    options:
      - key:     all
        desc:    "show all options, including private ones"
        optdef:  "-a, --all"
        short:   a
        long:    all
        paramreq: none
        hidden:  false

aliases:

categories:

abbreviations:

この機能は実験的であり、将来は変更される可能性があります。また何らかのバグを含んでいる可能性も高いです。

Rake のような使い方をしたい

重ねて言いますが、Benry-CmdApp はコマンドラインアプリケーション用のフレームワークであって、Rake(や Make や Gulp)のようなタスクランナーではありません。けど、「欲しいのはコマンド引数やオプションを指定できる Rake であってフレームワークじゃない」という人もいるでしょう。

そのような人は、Benry-ActionRunner を使ってください。Rake との互換性はないけど、Rake のように使えてかつコマンド引数やオプションを指定できます。

コマンドオプション解析ライブラリ

Ruby にはコマンドラインオプションを解析するライブラリ「optparse.rb」が標準で付属しています。しかし Benry-CmdApp ではそれを使っておらず、「Benry-CmdOpt」という別のライブラリを使っています。

Benry-CmdOpt の使い方は次のようになります。

require 'benry/cmdopt'

## オプションを定義
cmdopt = Benry::CmdOpt.new
cmdopt.add(:help   , '-h, --help'   , "print help message")
cmdopt.add(:version, '    --version', "print version")

## コマンドオプションをパースする(エラーハンドラつき)
options = cmdopt.parse(ARGV) do |err|
  abort "ERROR: #{err.message}"
end
p options     # ex: {:help => true, :version => true}
p ARGV        # options are removed from ARGV

## ヘルプメッセージを表示する
if options[:help]
  puts "Usage: foobar [<options>] [<args>...]"
  puts ""
  puts "Options:"
  puts cmdopt.to_s()
  ## or: puts cmdopt.to_s(20)              # 幅を指定
  ## or: puts cmdopt.to_s("  %-20s : %s")  # 書式を指定
  ## or:
  #format = "  %-20s : %s"
  #cmdopt.each_option_and_desc {|opt, help| puts format % [opt, help] }
end

Benry-CmdApp における @option.() を使ったオプション定義は、Benry-CmdOpt の機能をそのまま使っています。なので @option.() の詳細については Benry-CmdOpt のドキュメントを読むといいでしょう。また Benry-CmdApp のフレームワークはいらないけどオプションの指定方法は気に入った、という人がいれば、Benry-CmdOpt を使ってみてください。

Ruby 標準の optparse.rb を使わないのは、それが多くの問題点を持っていて、かつ(ソースコードを読む限り)解決が難しいからです。どのような問題点があるかはドキュメントの「Why not optparse.rb?」というセクションをご覧ください。

実用的なアプリケーション事例

Benry-CmdApp を使った実用的なアプリケーションとして、「GitImproved」というツールを作りました。これは git コマンドのラッパースクリプトであり、git コマンドよりも直感的に使えるよう設計されています。

使用例を見てください。なお「gi」がコマンド名であり、「Git Improved」を表しています。

## ブランチを作成してかつ切り替える
## (「git checkout -b bugfix1」と同等)
$ gi fork bugfix1

## 新しいファイルを登録する(すでに登録済ならエラー)
## (「git add CHANGES.md」と同等)
$ gi track CHANGES.md

## 既存のファイルの変更を追加する(登録されてないファイルならエラー)
## (「git add README.md」と同等)
$ gi stage README.md

## これからコミットする内容を確認する
## (「git diff --cached」と同等)
$ gi staged

## コミットする(「cc」は「Create a Commit」を表す)
## (「git commit -m "..."」と同等)
$ gi cc "Fix a small bug"

## 現在のブランチを直前のブランチにマージする
## (「git checkout -; git merge --noff -」と同等)
$ gi join

Benry-CmdApp なら、このようなアクションをとるコマンドラインアプリケーションを作れます。ソースコードは Git リポジトリを参照してください。

なお Git-Improved は実験的であり未成熟なため、予告なく機能が変更される可能性があります。

おわりに

コマンドラインアプリケーション用フレームワークの Benry-CmdApp を紹介しました。より詳しいことはドキュメントを参照してください。FAQ なども書かれています。

コマンドラインアプリケーション用フレームワークの自作は、Web アプリケーション用のフレームワークを自作するよりも技術的な難易度は低いので、初・中級レベルの人にとってはちょうどよい難易度の課題になるでしょう。ぜひ自分なりのアイデアをもってチャレンジしてみてください。

5
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?