Code Reading:Redox(Rust)版coreutilsのcatコマンド その1(全2回)
前書き
Rustを学習するための一環として、Redox(OS)版coreutilsのcatコマンドをCode Readingします。Redoxプロジェクトや環境構築方法に関しては、以下の記事にまとめてあります。
環境構築:Redox向けcoreutils(Rust)のCode Reading準備およびReading対象コマンド一覧
catコマンドは、記事を2つに分けて説明します。今回(その1)は、catコマンドのオプションパース処理までを説明し、その2でcatコマンドの主要な処理(ファイル内容の表示)を説明します。
catコマンド 挙動のおさらい
ソースコードの確認に入る前に、catコマンドの挙動を確認します。catコマンドは、由来である”concatenate(〜を連結する)”の通り、ファイルの中身を連結します。連結操作よりも、ファイルの中身を表示するために用いる事が多いと思います。
以下に、実行例を示します。Redox版coreutils(Rust版coreutils)を用いていますが、GNU版/BSD版と挙動は同じです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
(注釈):Redox版coreutils(Rust版のcoreutils)のcatコマンド、 テスト用ファイルをワーキングディレクトリ以下に用意してあります。 $ ls cat fileA.txt fileB.txt fileC.txt (注釈):file[A-C].txtの中身をcatコマンドで表示。内容は、大文字のアルファベット3文字。 $ cat fileA.txt AAA $ cat fileB.txt BBB $ cat fileC.txt CCC (注釈):file[A-C].txtの中身をcatコマンドで連結して表示。 $ cat fileA.txt fileB.txt fileC.txt AAA BBB CCC |
catコマンドのコード全文(Reading対象)
catコマンドは約270Stepと短いですが、本記事では全文を示しません。全文を確認する場合、Redox版coreutilsの公式リポジトリを参照して下さい。ただし、Reading結果を示す際は、部分的なコードを示します。
一行目:#![deny(warnings)]について
以下に示すcat.rsを示します。最初の部分は、”#![deny(warnings)]”、crateの読込(extern部分)、モジュールの読込(use部分)で開始しています。crate・モジュールの読み込みは一般的な文法のため、一行目のみ説明します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#![deny(warnings)] extern crate arg_parser; extern crate extra; use std::cell::Cell; // Provide mutable fields in immutable structs use std::env; use std::error::Error; use std::fs; use std::io::{self, BufReader, Read, Stderr, StdoutLock, Write}; use std::process::exit; use extra::option::OptionalExt; use arg_parser::ArgParser; |
“#![deny(warnings)]”によって、コンパイラがWarningを出した場合、強制的にエラー(ビルド失敗)扱いにします。安定したバイナリを提供するために有効な設定に思えますが、現段階では推奨されていません。その理由は、2019年現在のRustが安定した仕様ではなく(仕様変更が多く)、下方互換性に関するWarningが出やすいためです(参考情報:その1、その2)。
2019年現在のRustは、構文と標準ライブラリAPIのみが安定しており、バージョンが上がると非推奨API(obsolete API)が増えます。非推奨APIは、ビルド時にWarningとして指摘されます。つまり、#![deny(warnings)]を記述している場合、バージョンアップ後にビルドが壊れる可能性があります。
なお、#![deny(warnings)]を使用せずに、Warning発生時にビルドエラーを発生させる方法は、
- CI時(正確にはテスト時のみ)に有効化する方法
- ビルド時オプションによる方法
の2通りがあります(正確には、他の方法もあります)。順に、方法を説明します。
1. CI時(正確にはテスト時のみ)に有効化する方法
Cargo.tomlに[features]以下の内容を記載し、crate(lib.rsやmain.rs)のトップにcfg_attr(条件付き属性)を記載します。
1 2 |
[features] strict = [] |
1 |
#![cfg_attr(feature = "strict", deny(warnings))] |
cfg_attr(条件付き属性)は、第一引数(今回はfeature=”strict”)が有効になっている場合、”#[deny(warnings)]”と同じ扱いになります。第一引数が無効の場合、この処理は何もしません。
第一引数を有効化するために、テスト時のコンパイルは、以下のようにcargoを実行します。この記載は、strict機能を有効化しています。
1 |
$ cargo build --features "strict" |
2.ビルド時オプションによる方法
変数RUSTFLAGS経由で、cargoが呼び出すコンパイラに-D(deny)オプションを付与する事によって、Warning時にビルドエラーを発生させます。具体的には、以下の形式でビルドします。
1 |
$ RUSTFLAGS="-D warnings" cargo build |
main関数における標準出力・標準エラーの設定
以下に示すmain関数内で、標準出力へのハンドラ(変数stdout)、標準エラーのハンドラ(変数stderr)を取得します。それぞれ、バッファに関する仕様が異なります。
- 標準出力ハンドラは、mutex(排他制御)によってアクセス同期される共有グローバルバッファへの参照
- 標準エラーハンドラは、バッファリングされない
1 2 3 4 5 6 |
fn main() { let stdout = io::stdout(); let mut stdout = stdout.lock(); let mut stderr = io::stderr(); exit(Program::initialize(&mut stdout, &mut stderr).and_execute(&mut stdout, &mut stderr)); } |
“stdout.lock()“は、明示的にアクセスを同期させます。ハンドラを標準ストリームにmutexでロックし、書き込み可能な状態にします。一度だけロックを取得した方が、複数回ロックを取得するよりも高速で動作します。このロックは、ハンドラがスコープ範囲外となった場合(今回であればmain関数を抜けた場合)、解除されます。
同様に、”stderr.lock()”も存在しますが、使われていません。エラーメッセージは量が少ないため、明示的な同期が不要という判断でしょうか?
main関数における構造体Programの初期化
main関数の残りの部分は、構造体Programの初期化(initialize())を実行後、メインとなる処理(and_execute())を実行するだけです。and_execute()の実行結果(終了ステータス)を引数として、現在のプロセスをexit()で終了させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
struct Program { exit_status: Cell, number: bool, number_nonblank: bool, show_ends: bool, show_tabs: bool, show_nonprinting: bool, squeeze_blank: bool, paths: Vec, } fn main() { /* 説明済みのため、省略 */ exit(Program::initialize(&mut stdout, &mut stderr).and_execute(&mut stdout, &mut stderr)); } |
構造体Programが持つメンバの意味合い(用途)は、下表の通りです。
メンバ名 | 用途 |
exit_status | catコマンドの終了ステータス |
number | 表示対象ファイルの行番号を表示するためのフラグ |
number_nonblank | 表示対象ファイルの行番号(空白行を除く)を表示するためのフラグ |
show_ends | 行末を表示するためのフラグ |
show_tabs | Tabを表示するためのフラグ |
show_nonprinting | 非表示文字を表示するためのフラグ |
squeeze_blank | 連続した空行を一行に変更するためのフラグ |
paths | 表示もしくは連結対象のファイルPATH |
初期化処理(initialize())の実装は、以下の通りです。initialize()は、実装の上に書かれているコメント通り、引数・フラグの初期化をしています。上から順番に説明します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
/// Initialize the program's arguments and flags. fn initialize(stdout: &mut StdoutLock, stderr: &mut Stderr) -> Program { let mut parser = ArgParser::new(10). add_flag(&["A", "show-all"]). //vET add_flag(&["b", "number-nonblank"]). add_flag(&["e"]). //vE add_flag(&["E", "show-ends"]). add_flag(&["n", "number"]). add_flag(&["s", "squeeze-blank"]). add_flag(&["t"]). //vT add_flag(&["T", "show-tabs"]). add_flag(&["v", "show-nonprinting"]). add_flag(&["h", "help"]); parser.parse(env::args()); let mut cat = Program { exit_status: Cell::new(0i32), number: false, number_nonblank: false, show_ends: false, show_tabs: false, show_nonprinting: false, squeeze_blank: false, paths: Vec::with_capacity(parser.args.len()), }; if parser.found("help") { stdout.write(MAN_PAGE.as_bytes()).try(stderr); stdout.flush().try(stderr); exit(0); } if parser.found("show-all") { cat.show_nonprinting = true; cat.show_ends = true; cat.show_tabs = true; } if parser.found("number") { cat.number = true; cat.number_nonblank = false; } if parser.found("number-nonblank") { cat.number_nonblank = true; cat.number = false; } if parser.found("show-ends") || parser.found(&'e') { cat.show_ends = true; } if parser.found("squeeze-blank") { cat.squeeze_blank = true; } if parser.found("show-tabs") || parser.found(&'t') { cat.show_tabs = true; } if parser.found("show-nonprinting") || parser.found(&'e') || parser.found(&'t') { cat.show_nonprinting = true; } if !parser.args.is_empty() { cat.paths = parser.args; } cat } |
まず、引数の初期化部分(以下に示したコード)です。引数をパースするcrateは、Redoxプロジェクトが独自に作成したものです。この処理では、以下の内容を順番に実行しています。
- オプションが10個として、ArgParserのコンストラクタ(new())を実行
- add_flag()でオプションを追加(第一引数=shotオプション、第二引数=longオプション)
- 標準API経由(env::args())で、catコマンドの実行時オプションをパース
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/// Initialize the program's arguments and flags. fn initialize(stdout: &mut StdoutLock, stderr: &mut Stderr) -> Program { let mut parser = ArgParser::new(10). add_flag(&["A", "show-all"]). //vET add_flag(&["b", "number-nonblank"]). add_flag(&["e"]). //vE add_flag(&["E", "show-ends"]). add_flag(&["n", "number"]). add_flag(&["s", "squeeze-blank"]). add_flag(&["t"]). //vT add_flag(&["T", "show-tabs"]). add_flag(&["v", "show-nonprinting"]). add_flag(&["h", "help"]); parser.parse(env::args()); /* 省略 */ } |
Shortオプション | Longオプション | 機能 |
A | show-all | 全ての非表示文字(行末、Tab、CR)を表示 |
b | number-nonblank | 空白行を除いて、行番号を付与 |
e | なし | Tabを除いて、全ての非表示文字(行末、CR)を表示 |
E | show-ends | 行の最後に”$”を表示 |
n | number | 行番号を付与 |
s | squeeze-blank | 連続した空行を一行に変更 |
t | なし | TABを”^I”で表示 |
T | show-tabs | TABを”^I”で表示 |
v | show-nonprinting | 非表示文字をキャレット記法などで表示 |
h | help | ヘルプを表示 |
初期化処理の残り部分は、catコマンドのオプションパース結果をもとに、各フラグを有効化し、表示・連結対象のファイルパスを保持するだけです。helpの表示のみ(以下の実装のみ)、説明します。
1 2 3 4 5 6 7 8 9 |
fn initialize(stdout: &mut StdoutLock, stderr: &mut Stderr) -> Program { /* 省略 */ if parser.found("help") { stdout.write(MAN_PAGE.as_bytes()).try(stderr); stdout.flush().try(stderr); exit(0); } /* 省略 */ } |
オプションパーサで”help”(もしくは”h”)を見つけた場合、helpの表示処理に移ります。正常系の処理は、以下のように、コードを読んだ通りです。
- 標準出力に変数MAN_PAGE(後述)をバイト列として出力
- ストリームバッファのフラッシュ(標準出力への書き出し)
- 正常終了
異常系の処理では、try()によるエラーハンドリングをしています。このtry()は、昔のバージョンで?演算子を用いてResult型(エラーかどうかを示す列挙型)を返していた処理と同等です。エラーハンドリングに用いる型を柔軟にするための処理のようですが、公式ドキュメントに詳細な説明がなかったため、割愛します。
変数MAN_PAGEは、以下のように、help文をそのままハードコーディングされています。
1 2 3 4 5 6 7 8 9 10 |
const MAN_PAGE: &'static str = /* @MANSTART{cat} */ r#"NAME cat - concatenate files and print on the standard output SYNOPSIS cat [-h | --help] [-A | --show-all] [-b | --number-nonblank] [-e] [-E | --show-ends] [-n | --number] [-s | --squeeze-blank] [-t] [-T] FILES... /* 長いため、省略 */ "#; /* @MANEND */ |
変数MAN_PAGEは、&’static str型なので、文字リテラル(バイト列)としてバイナリ中に埋め込まれます。また、文字リテラルは、r#”文字列”#という形式で表記されています。この形式は、Raw(生)の文字リテラルを定義するために使います。この形式を用いる事によって、エスケープ文字(“\”)なしで、文字リテラル内の特殊文字(エスケープシーケンス)を通常の文字として扱えます(参考)。
Reading結果 その2へのリンク
Code Reading:Redox(Rust)版coreutilsのcatコマンド その2(全2回)
ロシア人と国際結婚した地方エンジニア。
小学〜大学院、就職の全てが新潟。
大学の専攻は福祉工学だったのに、エンジニアとして就職。新卒入社した会社ではOS開発や半導体露光装置ソフトを開発。現在はサーバーサイドエンジニアとして修行中。HR/HM(メタル)とロシア妻が好き。サイトに関するお問い合わせやTwitterフォローは、お気軽にどうぞ。