bash再実装課題の振り返り

42Tokyoでbashの再実装をする課題に取り組んだので、振り返り記事を書きます。

課題の概要

c言語bashもどきを作ります。実行ファイルの名前はminishellです。

デモ

https://user-images.githubusercontent.com/76856052/135187498-f8a922a5-38c3-41a5-b368-81309fc1da9a.gif

コード

github.com

ざっくり以下の実装をする必要があります。

  • パイプライン、クォート、リダイレクト、ヒアドキュメント
  • 環境変数
  • ビルトイン関数 echo , cd , pwd, export , unset , env , exit
  • シグナルハンドル Ctrl-C Ctrl-D Ctrl-\
  • 終了ステータス

課題の進め方

同じく42 Tokyoの学生であるrakiyamaさんとペアを組み課題に取り組みました。

最初に「The Architecture of Open Source Applications」のbashの章で内部構造についての説明があるのでこれを読んでから実装方針をペアと話し合いました。 これによると、bashコンポーネントは大まかに分けると以下の4つになっているようです。

  1. Input(入力)
  2. Lexical Analysis and Parsing(構文解析)
  3. Expansion(変数展開)
  4. Command Execution(コマンド実行)

自分は構文解析と変数展開を主に担当したので、後で少しだけその部分について書こうと思います。コマンド実行部分に関してはrakiyamaさんのお話を参考にして見てください。プログラムの流れについても図を用いて説明してくれています。

rakiyama0229.hatenablog.jp

課題を進める中で、実装に迷った場合は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

というコマンドを実行しようとした時に作成されるリストの例です。

f:id:rio_1:20211006090903p:plain

構文解析

入力されたコマンドを分割してトークン化した後、上記のようなデータ構造を作成します。

処理が複雑になり、バグが頻発するので早い段階でテストを回しながら開発を進めました。 「この入力に対応したら別の入力がバグるようになった」みたいなことがよくあります。

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で様々なコマンドを試すことになるのですが、その際に役に立ったコマンドや設定を紹介します。

プロンプトに終了ステータスを表示させる。

f:id:rio_1:20211005182617j:plain

毎回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の機能を使う場合

  1. Cmd+dで画面分割
  2. 片方の画面でminishell起動、もう片方の画面でbash起動
  3. Shift+Cmd+iを押す
  4. Warningが出てくるのでOKを押す
  5. 終了するときはShift+Opt+Cmd+i

tmuxを使う場合

  1. tmuxでtmuxセッション開始
  2. Prefix(デフォルトだとCtrl+b)→%で画面分割
  3. minishellとbashを起動
  4. Prefix→:set-window-option synchronize-panes on
  5. 終了するときは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した後のpwdexportが動作しているかの確認など、複数コマンドを必要とするテストの実行方法が分からず手動でのテストも多く行いました。 しかし、提出直前に他の学生にその方法を教えていただけました。

#!/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さんありがとうございました。