# coreutilsのecho.cを読んでみた
今回, coreutilsのソースコードとして, GitHubで公開されているGNUのecho.c (opens new window)を参照しました.
今後用いる引数として, 第1引数をecho
, それ以降に続くi番目の引数を第(i+1)引数とします.例えば, echo -e "hoge\nfuga\npiyo" "spam"
の場合, "spam"
は第4引数でたり, コマンド引数の数は4です.
# 文字が表示されるまでの過程
main関数内で文字が表示されるまでの過程は, 変数宣言等を行う前処理部, 引数をパースする引数処理部, 出力を行う文字列出力部の3部に分けられます.
echoの想定入力は, echo [-neE] [args...]
です. これらの内, [-neE]
を処理するのが引数処理部, [args...]
を処理するのが文字出力部です.
それぞれ順番に説明します.
# 前処理部
前処理部 (opens new window)は, 変数宣言処理および前処理関数呼び出し処理の2つの処理によって構成されています.
# 変数宣言処理
変数宣言処理では, main関数内で必要とされている変数を宣言します.宣言された変数は以下の通りです.
bool display_return
(opens new window)- 末尾の改行を出力するか否かを保持する変数
- 初期値は
true
- 藤原によると, 互換性維持のためにlib/stdbool.in.h (opens new window)で実装されているとのこと[1]
bool posixly_correct
(opens new window)- 環境変数
POSIXLY_CORRECT
の値を保持する変数 - POSIX specification (opens new window)によると, 環境変数が設定されていない場合はNULLポインタが返る仕様なのでfalse
- 環境変数
bool allow_options
(opens new window)コマンドライン引数に含まれるオプションを許可するか否かを保持する変数
DEFAULT_ECHO_TO_XPG
はデフォルトでエスケープシーケンスの解釈をするかを保持する変数- echo.c#L30-L33 (opens new window)で, この変数が定義されていない場合はfalseで初期化される
STREQ()
(opens new window)はgnulibで実装されているstrcmp()用のマクロ- Recommended C Style and Coding Standards (opens new window)によると, strcmp()は正常系の帰り値が0なのでバグの原因になりやすいため, このマクロを紹介している[4]
bool do_v9
(opens new window)\n
のようなエスケープシーケンスを解釈するか否かを保持する変数- 先述した
DEFAULT_ECHO_TO_XPG
の値で初期化される
# 前処理関数呼び出し処理
前処理関数呼び出し処理では, main関数内の処理で必要とされている関数を実行します.実行する関数は以下の通りです.
initialize_main(&argc, &argv)
(opens new window)coreutils/src/system.h
(opens new window)でマクロが書かれている- What does initialize_main (&argc, &argv) do? (opens new window)によると, 以前Linuxと競合していたOpenVMSというOSのためのマクロ
- OpenVMSではリダイレクト(
>
)やワイルドカード(*
)を認識しないため, これらを認識するように初期化するための関数と考えられる
- OpenVMSではリダイレクト(
set_program_name(argv[0])
(opens new window)- argv[0]から実行されたコマンド名のみを抽出して、
program_name
というグローバル変数に格納[7]
- argv[0]から実行されたコマンド名のみを抽出して、
setlocale(LC_ALL, "")
(opens new window)- setlocale in locale.h (opens new window)
- ロケール (locale) 情報の設定を行う
- この場合, 全てのカテゴリで処理系依存の設定としている
bindtextdomain(PACKAGE, LOCALEDIR)
(opens new window)- ドメイン名"PACKAGE"のカタログディレクトリを"LOCALEDIR"に設定[9]
- 具体的には, 変数PACKAGEはMakefile中に"coreutils"と定義され、変数LOCALEDIRはlib/configmake.h中に"/usr/share/locale"と定義されている
textdomain(PACKAGE)
(opens new window)- ドメイン名を"PACKAGE=coreutils"に設定[9]
atexit(close_stdout)
(opens new window)- atexit(3)システムコール (opens new window)[8]
- プロセスが正常終了した時に呼び出される関数を登録
# 引数処理部
引数処理部 (opens new window)では略語の受け入れを避けるために, parse_long_optionsを使用するのではなく, 直接オプションをパースします.
この部分は, (allow_options
がtrue
) かつ コマンド引数が2つの場合の処理, 見ているコマンドライン引数の更新 および allow_options
がtrue
の場合の処理がの3つの処理によって構成されています.
# (allow_options
がtrue
) かつ コマンド引数が2つの場合の処理
第2引数が--help
ならばusage(EXIT_SUCCESS)
を実行します.
usage()
(opens new window)はfputs()を用いてコマンドのusageを表示します.この関数では, assert()
(opens new window)によって, それ以降のstatusがEXIT_SUCCESS
であることを保証しています.
第2引数が--version
ならば, version_etc(stdout, PROGRAM_NAME, PACKAGE_NAME, Version, AUTHORS, (char *)NULL)
を実行します.
version_etc()
はgnulib (opens new window)で実装されています.
これらの処理は単にecho --help
やecho --version
を実行するだけでは実行されません.こちらの記事 (opens new window)によると, builtinを無効化しないとこれらは実行されないとされています[6].
確認用の実行ログは次の通りです.
$ echo --help
--help
$ enable -n echo
$ echo --help
Usage: echo [SHORT-OPTION]... [STRING]...
or: echo LONG-OPTION
Echo the STRING(s) to standard output.
-n do not output the trailing newline
-e enable interpretation of backslash escapes
-E disable interpretation of backslash escapes (default)
--help display this help and exit
--version output version information and exit
If -e is in effect, the following sequences are recognized:
\\ backslash
\a alert (BEL)
\b backspace
\c produce no further output
\e escape
\f form feed
\n new line
\r carriage return
\t horizontal tab
\v vertical tab
\0NNN byte with octal value NNN (1 to 3 digits)
\xHH byte with hexadecimal value HH (1 to 2 digits)
NOTE: your shell may have its own version of echo, which usually supersedes
the version described here. Please refer to your shell's documentation
for details about the options it supports.
GNU coreutils online help: <https://www.gnu.org/software/coreutils/>
Full documentation at: <https://www.gnu.org/software/coreutils/echo>
or available locally via: info '(coreutils) echo invocation'
# 見ているコマンドライン引数の更新
その後, echo.c#L144-L145 (opens new window)で見ているコマンドライン引数を1つ次に進めます.具体的には, 残りのコマンドライン引数の数を保持しているargc
を1減らして, コマンドライン引数のデータが格納されている先頭アドレスのポインタが格納されている先頭アドレスが格納されているポインタであるargv
を1つ次に進めます.
# allow_options
がtrue
の場合の処理
残りのコマンドライン引数の数であるargc
が正 かつ 見ているコマンドライン引数の最初の文字が-
である場合, 次の処理を繰り返し行います.1ループ処理が終了すると, echo.c#L187-#L188 (opens new window)で, 先述した見ているコマンドライン引数の更新を随時行います.
echo.c#L150 (opens new window)でオプションと考えられる文字列の1文字目を取得します.-e
の場合, e
を取得します.
その後, オプションを処理している場合は指定された全てのオプションが有効かどうかを確認します.具体的には, echo.c#L153-L167 (opens new window)でe
, E
, n
以外の文字が来た場合は, ラベルjust_echo
にジャンプし, それ以外はジャンプしません.このラベルは後述しますが, 一言でいうと文字列出力の直前に置かれているラベルです.例えば, echo -eEEEeEEeeeEeenEEee "hoge"
はの-
以降は有効ですが, echo -abcdefghijdklmn "hoge"
の-
以降は無効と判断され, ラベルjust_echo
にジャンプします.
そして, echo.c#L169-185 (opens new window)で, オプションが有効と判断された場合は, e
ならばdo_v9
をtrue
に, E
ならばdo_v9
をfalse
に, n
ならばdisplay_return
をfalse
にします.
確認用の実行ログは以下の通りです.
$ echo -eEEEeEEeeeEeenEEee "hoge"
hoge$ echo -abcdefghijdklmn "hoge"
-abcdefghijdklmn hoge
$
# 文字列出力部
文字列出力部 (opens new window)は, 前処理部および引数処理部で設定された情報を用いてechoコマンドにおける出力部分を行います.
この部分は, (backslashを解釈する場合または環境変数POSIXLY_CORRECTが存在している場合)の処理 および それ以外の場合処理, 末尾の改行処理の3つによって構成されています.
# (backslashを解釈する場合または環境変数POSIXLY_CORRECTが存在している場合)の処理
残りのコマンドライン引数が正の場合, 次の処理を繰り返し行います.
今見ているコマンドライン引数の先頭アドレスを保持します.そして, 出力用のバッファのためのunsigned char
型のc
を宣言します.その後, 先頭アドレスから格納されているデータの1文字目がバックスラッシュ(\\
)である場合, その次のデータに応じて次の通りオプションを解釈します.これらの意味は, 先述したusage()
に記載されています.
オプションの引数 | c に格納される値 | 意味 |
---|---|---|
a | \a | alert(BEL) |
b | \b | backspace |
c | - | 強制終了(それ以上表示しない) |
e | \x1B | escape |
f | \f | form feedを作る |
n | \n | 改行(new line) |
r | \r | 改行(carriage return) |
t | \t | horizontal tab |
v | \v | vertical tab |
x | \xnn | interpreting in hexadecimal(詳細は後述) |
0-9 | \0nnn | the character whose ASCII code is NNN(octal) |
\\ | - | そのまま\\ を出力 (opens new window) |
ここで, \x
および\0-9
の処理を深堀りします.
\x
の処理では, まずecho.c#L218-L219 (opens new window)で続く文字列がisxdigit()
(opens new window)で16進数であることを確認し, 異なる場合はバックスラッシュ(\\
)を単体とみなします.その後, 16進数のchar型を10進数に変換するhextobin() (opens new window)を介して16進数の文字が1桁ならばそのままc
に格納し, 2桁ならば16倍する処理を挟んでc
に格納します.ここで, c
はunsigned char
型ですが, サイズは1バイトであるため2桁までの16進数なら値を保持できます.
\0-9
の処理では, 8進数の範囲を超えていないか確認する処理を挟みつつ, \x
の時と同様にc
に値を格納します.
最後に, echo.c#L250 (opens new window)のputchar()
でc
に格納したデータを出力します.
1ループ処理が終了すると, echo.c#L252-#L253 (opens new window)で, 先述した見ているコマンドライン引数の更新を随時行います.同時に, putchar()
を用いたスペース()の出力も行います.
# (backslashを解釈する場合または環境変数POSIXLY_CORRECTが存在している場合) 以外の場合の処理
残りのコマンドライン引数が正の場合, fputs()
を用いて, 見ているコマンドライン引数をそのまま出力します.
1ループ処理が終了すると, echo.c#L263-#L264 (opens new window)で, 先述した見ているコマンドライン引数の更新を随時行います.同時に, putchar()
を用いたスペース()の出力も行います.
# 末尾の改行処理
先述したdisplay_return
がtrue
ならばputchar('\n')
を実行します.
それが終了後, EXIT_SUCCESS
を返します.
# References
- [0] Ryo Ezoe (2013)「様々なUNIX環境のecho.cの比較」, [online] https://cpplover.blogspot.com/2013/04/unixechoc.html (2020-03-01参照).
- [1] 藤原克則 (2013) 「lsを読まずにプログラマを名乗るな!」, 秀和システム.
- [2] coreutils 「coreutils / coreutils」, [online] https://github.com/coreutils/coreutils (2020-03-01参照).
- [3] gnulib 「coreutils / gnulib」, [online] https://github.com/coreutils/gnulib (2020-03-01参照).
- [4] Spencer, Keppel, Brader他 (1990)「Recommended C Style and Coding Standards」, [online] https://www2.cs.arizona.edu/~mccann/cstyle.html (2020-03-01参照).
- [5] 「What does initialize_main (&argc, &argv) do?」, [online] https://stackoverflow.com/questions/19276701/what-does-initialize-main-argc-argv-do (2020-03-01参照).
- [6] YumaInaura (2018) 「Bash — echo --help でヘルプが表示されない理由から考える シェルの builtin コマンドなどなど」, [online] https://qiita.com/YumaInaura/items/9dc68da0fac5a2703b25 (2020-03-01参照).