bash再実装課題の振り返り
42Tokyoでbashの再実装をする課題に取り組んだので、振り返り記事を書きます。
課題の概要
c言語でbashもどきを作ります。実行ファイルの名前はminishellです。
デモ
コード
ざっくり以下の実装をする必要があります。
- パイプライン、クォート、リダイレクト、ヒアドキュメント
- 環境変数
- ビルトイン関数 echo , cd , pwd, export , unset , env , exit
- シグナルハンドル Ctrl-C Ctrl-D Ctrl-\
- 終了ステータス
課題の進め方
同じく42 Tokyoの学生であるrakiyamaさんとペアを組み課題に取り組みました。
最初に「The Architecture of Open Source Applications」のbashの章で内部構造についての説明があるのでこれを読んでから実装方針をペアと話し合いました。 これによると、bashのコンポーネントは大まかに分けると以下の4つになっているようです。
- Input(入力)
- Lexical Analysis and Parsing(構文解析)
- Expansion(変数展開)
- Command Execution(コマンド実行)
自分は構文解析と変数展開を主に担当したので、後で少しだけその部分について書こうと思います。コマンド実行部分に関してはrakiyamaさんのお話を参考にして見てください。プログラムの流れについても図を用いて説明してくれています。
課題を進める中で、実装に迷った場合はbashのマニュアルやソースコードを読み、ペアと話し合って実装するか判断しました。「実装に迷った場合」というのは
- その機能について課題の文章に明確な規定がなく、実装するべきか迷う場合
- bashが直感的でない挙動をしていて、それに合わせる必要があるのか
という2つのパターンがありました。
例えば、2>file
のような指定したファイルディスクリプターでリダイレクトする機能について、課題文には明確な規定がなかったので実装するべきか迷いました。
そこで、リダイレクトについてのマニュアルを読みに行ったところ以下のように書かれていました。
出力をリダイレクトすると、 word の展開した結果の名前を持つファイルがオープンされ、 ファイル・ディスクリプター n で書き込めるようになります。 n が指定されていなければ、書き込みは標準出力 (ファイル・ディスクリプター 1) に行われます。 出力のリダイレクトは、一般的には以下の形式です:
[n]>word
これを読んだ結果、>file
のような書き方は1>file
を省略した書き方にすぎず、一般的に形式である[n]>word
も実装する必要があると判断しました。
実装に取り掛かってからはGitHubとNotionを活用しながら進めました。
Notion
共有のワークスペースを作成し、それぞれのToDoリストや参考記事などをまとめました。
GitHub
issueを立てる→コード書く→プルリク→マージを繰り返して開発しました。
終盤ではGitHub Actionsも使いました。プルリク時点でテストを走らせて、レビュワーに見せることができるのがよかったです。
データ構造
構文解析では木構造にするのが一般的のようですが、今回の課題では木構造にする必要がないと判断して線形リストを作成しました。
パイプ区切りでexecdataというリストを作成し、そのメンバーとして
- リダイレクトに関するコマンドのリスト(iolilst)
- それ以外のコマンドのリスト(cmdlist)
- 環境変数(envlist)
を持たせました。
下の図は、
echo hoge > file1 | cat file2
というコマンドを実行しようとした時に作成されるリストの例です。
構文解析
入力されたコマンドを分割してトークン化した後、上記のようなデータ構造を作成します。
処理が複雑になり、バグが頻発するので早い段階でテストを回しながら開発を進めました。 「この入力に対応したら別の入力がバグるようになった」みたいなことがよくあります。
syntax errorもここで判断しています。
環境変数の展開
シングルクォートの外で$
を見つけたらその後ろに続く単語をkeyとして変数展開を行います。
細かい仕様がいくつかあるのですが、一つだけ紹介しておきます。
展開後文字列の中にスペースがあると、文字列が分割されます。
bash-5.1 export VAR="hoge fuga" bash-5.1 ls $VAR ls: fuga: No such file or directory ls: hoge: No such file or directory bash-5.1 ls "$VAR" ls: hoge fuga: No such file or directory
また、リダイレクト先が分割された場合、エラーとなります。
bash-5.1 echo hogehoge > $VAR bash: $VAR: ambiguous redirect
ヒアドキュメントの入力中だと仕様が異なるので注意が必要です。
役に立ったコマンド等
この課題に取り組む際はbashで様々なコマンドを試すことになるのですが、その際に役に立ったコマンドや設定を紹介します。
プロンプトに終了ステータスを表示させる。
毎回echo $?
をしなくて済みます。
~/.bash_profile
に以下を記入
reset='\[\e[0m\]' green='\[\e[32m\]' red='\[\e[0;31m\]' PS1="\$(" PS1+="status="\$?"; " PS1+="if [ \$status -ne 0 ]; then echo \"$red[\$status] $rst\"; fi" PS1+=")" PS1+="${green}\s-\v${reset} " export PS1
参考:https://blog.sgry.jp/entry/2019/11/03/234538
minishellとbashで同時に入力する
そこまで使用頻度は高くなかったのですが、bashとminishellの挙動を横で見比べることができます。
iTermの機能を使う場合
Cmd+d
で画面分割- 片方の画面でminishell起動、もう片方の画面でbash起動
Shift+Cmd+i
を押す- Warningが出てくるのでOKを押す
- 終了するときは
Shift+Opt+Cmd+i
tmuxを使う場合
tmux
でtmuxセッション開始- Prefix(デフォルトだと
Ctrl+b
)→%
で画面分割 - minishellとbashを起動
- Prefix→
:set-window-option synchronize-panes on
- 終了するときはPrefix →
:set-window-option synchronize-panes off
反省点
テスト
膨大な量の入力パターンをテストする必要があるため、シェルスクリプトでテストを作成しました。 中身は以下のようになっていました。
#!/bin/bash # "echo hoge"というコマンドの挙動を確かめたい場合 echo "echo hoge" | ./minishell > minishell.txt echo "echo hoge" | bash > bash.txt diff minishell.txt bash.txt
コマンドを一つずつ実行させているので、cd
した後のpwd
やexport
が動作しているかの確認など、複数コマンドを必要とするテストの実行方法が分からず手動でのテストも多く行いました。
しかし、提出直前に他の学生にその方法を教えていただけました。
#!/bin/bash echo -e "cd ..\n pwd" | ./minishell > minishell.txt echo -e "cd ..\n pwd" | bash > bash.txt diff minishell.txt bash.txt
echo -e
はバックスラッシュでエスケープした文字を解釈するので、cd ..
の入力後pwd
を入力された場合と同じ挙動になります。(bashだと-e
が必要ですがzshだとデフォルトで解釈するそうです)
これを最初からできていれば相当な手間を省くことができたと思うので、残念です。
git
今回はチーム課題ということで、gitらしいgitの使い方を学ぶことができました。 大きなトラブルはなかったのですが、運用方法については反省点が残りました。
コミットの粒度に関して、特に序盤では手探り状態でコードを書いていたこともあり、1回のコミット・プルリクが大きくなりすぎてしまってお互いのコードの確認が大変でした。 ブランチの運用についても適当でした。
これからの個人開発でもgitの使い方は意識して取り組みたいと思います。
感想
minishellはこれまで自分が取り組んだことのある課題と比較して重たい課題で、ほぼフルコミットで1.5ヶ月ほどかかりました。
コード量が多く時間もかかるので、保守性を維持しながら開発を進める方法を考えさせられました。 また、最初から全てのケースを想定してコードを書くことは難しく、後から機能を追加することも多かったので、なるべく拡張性を持たせながら実装することも重要だと思いました。
ちゃんとした共同開発は初めてで、gitの使い方をはじめ難しい部分もあったのですが楽しく課題を進めることができました。
rakiyamaさんありがとうございました。