10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Rustのclapはどう動いているのか

Last updated at Posted at 2019-11-30

clap

clapクレートはRustでCLIを作るときに大変重宝していると思います。しかし、clapがどうやって実装されているのかを追ってみたことがある人は少ないと思います。そこでGithubのclapリポジトリを覗いて実際にどいう挙動をして、CLI引数の解析までしているのかを調べてみました。

そもそもclapはどう使うのか

そもそもclapを使ったことがない人が居ると思うので簡単に説明しておきます。clapでCLIアプリを作るには次のようにBuilderパターン的にアプリ本体を構築します。clapのアプリのCLI引数は一つ目のように本体と同様にBuilderパターン的に対応できますが、二つ目のように特定の書式に従って書かれた文字列から自動生成することができます。

let app = App::new("myprog")
          .arg(
              Arg::with_name("debug")
                  .short('d')
                  .help("turns on debugging mode")
                  .take_calue(false)
            )
           .arg(
              Arg::from(
                  "-c --config=[CONFIG] 'Optionally sets a config file to use'"
              )
           );

そしてその後、アプリを起動させてCLI引数のパースをします。

let result = app.get_matches();

その結果をもとにclap利用者が処理を実装していきます。

 if result.is_present("debug") {
    println!("in debug!!");
}

実はほかにもCargo.tomlを使った初期化方法や用意したyamlファイルから読み込んでの初期化方法があるのですが、今回は対象にしません。

いざ内部へ

使い方もわかったところで実際のコードを見ながらどのように実装されているのか見ていくことにします。

App構造体の定義

先ほどの使い方の例からもわかるようにエントリーポイントはApp構造体です。ですので、まずはその定義を見てみます。
というわけでまずsrc/lib.rsを見てみると最後の方に

src/lib.rs
pub use crate::build::{App, AppSettings, Arg, ArgGroup, ArgSettings, Propagation};
// ...
mod build;

という記述があるのでsrc/buildモジュールを見てみます。そこでも同様な記述があり、そこから本体がsrc/build/appモジュールにあることがわかります。次がその定義です。

src/build/app/mod.rs
#[derive(Default, Debug, Clone)]
pub struct App<'b> {
    #[doc(hidden)]
    pub id: Id,
    #[doc(hidden)]
    pub name: String,
    #[doc(hidden)]
    pub bin_name: Option<String>,
    #[doc(hidden)]
    pub author: Option<&'b str>,
    #[doc(hidden)]
    pub version: Option<&'b str>,
    #[doc(hidden)]
    pub long_version: Option<&'b str>,
    #[doc(hidden)]
    pub about: Option<&'b str>,
    #[doc(hidden)]
    pub long_about: Option<&'b str>,
    #[doc(hidden)]
    pub more_help: Option<&'b str>,
    #[doc(hidden)]
    pub pre_help: Option<&'b str>,
    #[doc(hidden)]
    pub aliases: Option<Vec<(&'b str, bool)>>, // (name, visible)
    #[doc(hidden)]
    pub usage_str: Option<&'b str>,
    #[doc(hidden)]
    pub usage: Option<String>,
    #[doc(hidden)]
    pub help_str: Option<&'b str>,
    #[doc(hidden)]
    pub disp_ord: usize,
    #[doc(hidden)]
    pub term_w: Option<usize>,
    #[doc(hidden)]
    pub max_w: Option<usize>,
    #[doc(hidden)]
    pub template: Option<&'b str>,
    #[doc(hidden)]
    pub settings: AppFlags,
    #[doc(hidden)]
    pub g_settings: AppFlags,
    #[doc(hidden)]
    pub args: MKeyMap<'b>,
    #[doc(hidden)]
    pub subcommands: Vec<App<'b>>,
    #[doc(hidden)]
    pub groups: Vec<ArgGroup<'b>>,
    #[doc(hidden)]
    pub help_headings: Vec<Option<&'b str>>,
}

設定できる項目の数だけフィールドが並んでいます。そしてこの定義の下の実装処理の最初の部分に初期化関数がありました。

src/build/app/mod.rs
    pub fn new<S: Into<String>>(n: S) -> Self {
        let name = n.into();
        let id = name.key();
        App {
            id,
            name,
            ..Default::default()
        }
    }

これらを見るに本体の構築に関しては典型的なBuilderパターンで実装されているようです。

Arg構造体の定義

同様にしてArg構造体の定義を見てみます。App構造体と同様に追跡してみると次の定義が見つかります。

src/build/arg/mod.rs
#[derive(Default, Clone)]
pub struct Arg<'help> {
    #[doc(hidden)]
    pub id: Id,
    #[doc(hidden)]
    pub name: &'help str,
    #[doc(hidden)]
    pub help: Option<&'help str>,
    #[doc(hidden)]
    pub long_help: Option<&'help str>,
    #[doc(hidden)]
    pub blacklist: Option<Vec<Id>>,
    #[doc(hidden)]
    pub settings: ArgFlags,
    #[doc(hidden)]
    pub r_unless: Option<Vec<Id>>,
    #[doc(hidden)]
    pub overrides: Option<Vec<Id>>,
    #[doc(hidden)]
    pub groups: Option<Vec<Id>>,
    #[doc(hidden)]
    pub requires: Option<Vec<(Option<&'help str>, Id)>>,
    #[doc(hidden)]
    pub short: Option<char>,
    #[doc(hidden)]
    pub long: Option<&'help str>,
    #[doc(hidden)]
    pub aliases: Option<Vec<(&'help str, bool)>>, // (name, visible)
    #[doc(hidden)]
    pub disp_ord: usize,
    #[doc(hidden)]
    pub unified_ord: usize,
    #[doc(hidden)]
    pub possible_vals: Option<Vec<&'help str>>,
    #[doc(hidden)]
    pub val_names: Option<VecMap<&'help str>>,
    #[doc(hidden)]
    pub num_vals: Option<u64>,
    #[doc(hidden)]
    pub max_vals: Option<u64>,
    #[doc(hidden)]
    pub min_vals: Option<u64>,
    #[doc(hidden)]
    pub validator: Option<Validator>,
    #[doc(hidden)]
    pub validator_os: Option<ValidatorOs>,
    #[doc(hidden)]
    pub val_delim: Option<char>,
    #[doc(hidden)]
    pub default_vals: Option<Vec<&'help OsStr>>,
    #[doc(hidden)]
    pub default_vals_ifs: Option<VecMap<(Id, Option<&'help OsStr>, &'help OsStr)>>,
    #[doc(hidden)]
    pub env: Option<(&'help OsStr, Option<OsString>)>,
    #[doc(hidden)]
    pub terminator: Option<&'help str>,
    #[doc(hidden)]
    pub index: Option<u64>,
    #[doc(hidden)]
    pub r_ifs: Option<Vec<(Id, &'help str)>>,
    #[doc(hidden)]
    pub help_heading: Option<&'help str>,
    #[doc(hidden)]
    pub global: bool,
}

定義も似ていますね。同様に初期化関数も見つかります。

src/build/arg/mod.rs
    pub fn new<T: Key>(t: T) -> Self {
        Arg {
            id: t.key(),
            disp_ord: 999,
            unified_ord: 999,
            ..Default::default()
        }
    }

unified_ordフィールドはわかりませんでしたが、disp_ord はどうやらヘルプメッセージの表示などで表示されるときにアルファベット順か指定値グループのアルファベット順かを制御するのに使っているようです。

この二つの定義を見てもわかるようにclapではアイテムの管理にIdを用いています。そのIdの実態はclap独自のHasherによって生成されるHash値です。

パース処理

さて、定義を見たので実際の動作を見てみます。App構造体と同じファイルの半分少しすぎのあたりに次のようなget_matches関数の定義があります。

src/build/app/mod.rs
   /// [`env::args_os`]: https://doc.rust-lang.org/std/env/fn.args_os.html
    pub fn get_matches(self) -> ArgMatches { self.get_matches_from(&mut env::args_os()) }
//...
    pub fn get_matches_from<I, T>(mut self, itr: I) -> ArgMatches
    where
        I: IntoIterator<Item = T>,
        T: Into<OsString> + Clone,
    {
        self.try_get_matches_from_mut(itr).unwrap_or_else(|e| {
            // Otherwise, write to stderr and exit
            if e.use_stderr() {
                wlnerr!("{}", e.message);
                if self.settings.is_set(AppSettings::WaitOnError) {
                    wlnerr!("\nPress [ENTER] / [RETURN] to continue...");
                    let mut s = String::new();
                    let i = io::stdin();
                    i.lock().read_line(&mut s).unwrap();
                }
                drop(self);
                drop(e);
                process::exit(1);
            }

            drop(self);
            e.exit()
        })
    }
//...
    pub fn try_get_matches_from_mut<I, T>(&mut self, itr: I) -> ClapResult<ArgMatches>
    where
        I: IntoIterator<Item = T>,
        T: Into<OsString> + Clone,
    {
        let mut it = itr.into_iter();
        // Get the name of the program (argument 1 of env::args()) and determine the
        // actual file
        // that was used to execute the program. This is because a program called
        // ./target/release/my_prog -a
        // will have two arguments, './target/release/my_prog', '-a' but we don't want
        // to display
        // the full path when displaying help messages and such
        if !self.settings.is_set(AppSettings::NoBinaryName) {
            if let Some(name) = it.next() {
                let bn_os = name.into();
                let p = Path::new(&*bn_os);
                if let Some(f) = p.file_name() {
                    if let Some(s) = f.to_os_string().to_str() {
                        if self.bin_name.is_none() {
                            self.bin_name = Some(s.to_owned());
                        }
                    }
                }
            }
        }

        self._do_parse(&mut it.peekable())
    }
//...
    #[doc(hidden)]
    fn _do_parse<I, T>(&mut self, it: &mut Peekable<I>) -> ClapResult<ArgMatches>
    where
        I: Iterator<Item = T>,
        T: Into<OsString> + Clone,
    {
        debugln!("App::_do_parse;");
        let mut matcher = ArgMatcher::default();

        // If there are global arguments, or settings we need to propgate them down to subcommands
        // before parsing incase we run into a subcommand
        if !self.settings.is_set(AppSettings::Built) {
            self._build();
        }

        {
            let mut parser = Parser::new(self);

            // do the real parsing
            parser.get_matches_with(&mut matcher, it)?;
        }

        let global_arg_vec: Vec<Id> = self
            .args
            .args
            .iter()
            .filter(|a| a.global)
            .map(|ga| ga.id)
            .collect();

        matcher.propagate_globals(&global_arg_vec);

        Ok(matcher.into_inner())
    }

get_matches関数では、CLI引数の入力をenv::args_os関数でOSが理解した空白区切りとして分割し、イテラブルで取得できる形式で取得します。その後、順にみていき最初に名前があればチェックして読み飛ばし、残りのCLI引数の最初をパース処理をしてくれる_do_parse関数に渡します。_do_parse関数ではサブコマンドの構築やサブでも使う引数等の処理を行ってmatcherという結果の格納庫となる構造体にparserのパース処理の結果を書き込んでいきます。その書き込み処理を担うget_matches_with関数は350行程度のコードなっています。長すぎるのでここでの全コード掲載は省略しますが、引数と戻り値の宣言部分は次のようになっています。

src/build/app/mod.rs
pub fn get_matches_with<I, T>(
        &mut self,
        matcher: &mut ArgMatcher,
        it: &mut Peekable<I>,
    ) -> ClapResult<()>
    where
        I: Iterator<Item = T>,
        T: Into<OsString> + Clone,
    {...}

実態としてはCLI引数を表すitの部分がある限り読み込んでいきます。
最初は--で始まり文字が続かないか調べ、続かないなら残りを末尾の引数(つまり hoge.exe -f hass -- s1 s2 s3s1, s2, s3ら)として、読み込むように状態をセットして自身を再帰的に呼び出してパースします。そうでなければ次の条件を調べてパースしていきます。
二回目以降も同様に「上から条件を見て満たしていればパースして結果をmatcherに追加して、状態をセットして必要なら自身を再帰的に呼び出す。しかし、最後まで条件を満たすものに出会えなかったらエラーで終了する。」といったようなことを繰り返します。そうして最後にParseResult::ValuesDoneを呼び出せばパース処理は無事成功、といった形の処理となっています。
自身の状態に応じてふるまいを変えて処理する… つまり、これはStateパターンであることがわかります。
これで_do_parsermatcherをclap利用者が受け取ってCLI引数のパースが完了となります。

まとめ

こうして実装を見てみると、clapもほかのCLIビルダー同様にアプリ本体の土台にいろいろと設定をつけて、その設定とこれまでの解析結果をもとに解析をしていく形ですね。これなら設計からインタプリタを書いたことがある人ならclapもほかのCLIビルダーも実装を参考にして独自のCLIビルダーライブラリを書くことができると思います。

長い記事ですが最後まで読んでいただきありがとうございました。

10
4
0

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
10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?