# 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関数内で必要とされている変数を宣言します.宣言された変数は以下の通りです.

# 前処理関数呼び出し処理

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

# 引数処理部

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

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

# (allow_optionstrue) かつ コマンド引数が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 --helpecho --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_optionstrueの場合の処理

残りのコマンドライン引数の数である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_v9trueに, Eならばdo_v9falseに, nならばdisplay_returnfalseにします.

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

$ 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に格納します.ここで, cunsigned 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_returntrueならば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参照).
Last Updated: 9ヶ月前