coreutilsのecho.cを読んだ話

Table of Contents

今回, coreutilsのソースコードとして, GitHubで公開されているGNUのecho.cを参照しました.

今後用いる引数として, 第1引数をecho, それ以降に続くi番目の引数を第(i+1)引数とします.例えば, echo -e "hoge\nfuga\npiyo" "spam"の場合, "spam"は第4引数でたり, コマンド引数の数は4です.

文字が表示されるまでの過程

main関数内で文字が表示されるまでの過程は, 変数宣言等を行う前処理部, 引数をパースする引数処理部, 出力を行う文字列出力部の3部に分けられます.

echoの想定入力は, echo [-neE] [args...] です. これらの内, [-neE]を処理するのが引数処理部, [args...]を処理するのが文字出力部です.

それぞれ順番に説明します.

前処理部

前処理部は, 変数宣言処理および前処理関数呼び出し処理の2つの処理によって構成されています.

変数宣言処理

変数宣言処理では, main関数内で必要とされている変数を宣言します.宣言された変数は以下の通りです.

  • bool display_return
    • 末尾の改行を出力するか否かを保持する変数
    • 初期値はtrue
      • 藤原によると, 互換性維持のためにlib/stdbool.in.hで実装されているとのこと[1]
  • bool posixly_correct
    • 環境変数POSIXLY_CORRECTの値を保持する変数
    • POSIX specificationによると, 環境変数が設定されていない場合はNULLポインタが返る仕様なのでfalse
  • bool allow_options
    • コマンドライン引数に含まれるオプションを許可するか否かを保持する変数

    • DEFAULT_ECHO_TO_XPGはデフォルトでエスケープシーケンスの解釈をするかを保持する変数

      • echo.c#L30-L33で, この変数が定義されていない場合はfalseで初期化される
    • STREQ()はgnulibで実装されているstrcmp()用のマクロ

  • bool do_v9
    • \nのようなエスケープシーケンスを解釈するか否かを保持する変数
    • 先述したDEFAULT_ECHO_TO_XPGの値で初期化される

前処理関数呼び出し処理

前処理関数呼び出し処理では, main関数内の処理で必要とされている関数を実行します.実行する関数は以下の通りです.

引数処理部

引数処理部では略語の受け入れを避けるために, parse_long_optionsを使用するのではなく, 直接オプションをパースします.

この部分は, (allow_optionstrue) かつ コマンド引数が2つの場合の処理, 見ているコマンドライン引数の更新 および allow_optionstrueの場合の処理がの3つの処理によって構成されています.

(allow_optionstrue) かつ コマンド引数が2つの場合の処理

第2引数が--helpならばusage(EXIT_SUCCESS)を実行します. usage()はfputs()を用いてコマンドのusageを表示します.この関数では, assert()によって, それ以降のstatusがEXIT_SUCCESSであることを保証しています.

第2引数が--versionならば, version_etc(stdout, PROGRAM_NAME, PACKAGE_NAME, Version, AUTHORS, (char *)NULL)を実行します. version_etc()gnulibで実装されています.

これらの処理は単にecho --helpecho --versionを実行するだけでは実行されません.こちらの記事によると, 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で見ているコマンドライン引数を1つ次に進めます.具体的には, 残りのコマンドライン引数の数を保持しているargcを1減らして, コマンドライン引数のデータが格納されている先頭アドレスのポインタが格納されている先頭アドレスが格納されているポインタであるargvを1つ次に進めます.

allow_optionstrueの場合の処理

残りのコマンドライン引数の数であるargcが正 かつ 見ているコマンドライン引数の最初の文字が-である場合, 次の処理を繰り返し行います.1ループ処理が終了すると, echo.c#L187-#L188で, 先述した見ているコマンドライン引数の更新を随時行います.

echo.c#L150でオプションと考えられる文字列の1文字目を取得します.-eの場合, eを取得します.

その後, オプションを処理している場合は指定された全てのオプションが有効かどうかを確認します.具体的には, echo.c#L153-L167e, E, n以外の文字が来た場合は, ラベルjust_echoにジャンプし, それ以外はジャンプしません.このラベルは後述しますが, 一言でいうと文字列出力の直前に置かれているラベルです.例えば, echo -eEEEeEEeeeEeenEEee "hoge"はの-以降は有効ですが, echo -abcdefghijdklmn "hoge"-以降は無効と判断され, ラベルjust_echoにジャンプします.

そして, echo.c#L169-185で, オプションが有効と判断された場合は, eならばdo_v9trueに, Eならばdo_v9falseに, nならばdisplay_returnfalseにします.

確認用の実行ログは以下の通りです.

$ echo -eEEEeEEeeeEeenEEee "hoge"
hoge$ echo -abcdefghijdklmn "hoge"
-abcdefghijdklmn hoge
$ 

文字列出力部

文字列出力部は, 前処理部および引数処理部で設定された情報を用いて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)
\\ - そのまま\\を出力

ここで, \xおよび\0-9の処理を深堀りします.

\xの処理では, まずecho.c#L218-L219で続く文字列がisxdigit()で16進数であることを確認し, 異なる場合はバックスラッシュ(\\)を単体とみなします.その後, 16進数のchar型を10進数に変換するhextobin()を介して16進数の文字が1桁ならばそのままcに格納し, 2桁ならば16倍する処理を挟んでcに格納します.ここで, cunsigned char型ですが, サイズは1バイトであるため2桁までの16進数なら値を保持できます.

\0-9の処理では, 8進数の範囲を超えていないか確認する処理を挟みつつ, \xの時と同様にcに値を格納します.

最後に, echo.c#L250putchar()cに格納したデータを出力します.

1ループ処理が終了すると, echo.c#L252-#L253で, 先述した見ているコマンドライン引数の更新を随時行います.同時に, putchar()を用いたスペース( )の出力も行います.

(backslashを解釈する場合または環境変数POSIXLY_CORRECTが存在している場合) 以外の場合の処理

残りのコマンドライン引数が正の場合, fputs()を用いて, 見ているコマンドライン引数をそのまま出力します.

1ループ処理が終了すると, echo.c#L263-#L264で, 先述した見ているコマンドライン引数の更新を随時行います.同時に, putchar()を用いたスペース( )の出力も行います.

末尾の改行処理

先述したdisplay_returntrueならばputchar('\n')を実行します. それが終了後, EXIT_SUCCESSを返します.

References

Posted on Mar 6, 2020