シェルの再実装しましたというお話。
何を作ったか
bashの機能(制限あり)を備えたプログラムを作りました。
言語 : C
実行ファイル名 : minishell
できること
- パイプライン、クォート、リダイレクト、ヒアドキュメント、環境変数
minishell$ cat << EOF | more minishell$ echo "current path is $PWD" > file minishell$ echo '$USER (not expanded)' >> file minishell$ 0<file cat 1>file minishell$ ./put_something_to_fd42 42>file
- ビルトイン関数もいくつか実装
echo , cd , pwd , export , unset , env , exit - シグナルハンドル
ctrl-C ctrl-D ctrl-\ - 終了ステータス
echo $?
で最後のコマンドの終了ステータスを参照できます。
開発の流れ
42tokyoの学生rsudoさんと以下のツールを使って開発しました。
初めは以下の資料を見る、bashを触る、とりあえずコードを書いてみるを繰り返しました。
Bash Reference Manual
オープンソースアプリケーションのアーキテクチャ
ある程度把握できたところで、データ構造を決定しコードを書いていきました。
プログラムの流れ
構文解析などはrsudoさんの記事が参考になるかと思います。テストをする上でのTipsも載っています。
細かな実装の話
ビルトイン以外のコマンド(catなど)実行に使うexecve関数・fork関数や、
入出力処理(レダイレクションやパイプラインの処理)で使うdup系関数・pipe関数の使い方はパイプを実装してみたというお話 - rakiyama0229のブログで書いています。
親プロセスで処理するか、子プロセスで処理するか
- ビルトイン以外のコマンドはサブシェル(子プロセス)で処理
cat file
execve()を使用して実行->成功すると実行プロセスが終了するので子プロセスで処理します。 - コマンド行にパイプが存在するなら、パイプ区切りでそれぞれ子プロセスで処理
echo aaa | cat -e | cat -n
shellが行っているようにマルチプロセスで実行するためです。
コード例:
while(コマンドの数だけ) { pipe(pipefd); pid = fork(); if (pid == 0) /* 前のpipefd-(入力)-> コマンド実行 -(出力)-> 新しいpipefd */ } while(プロセスの数だけ) wait(NULl);
- 単一のビルトインコマンドはminishellのプロセス(親プロセス)で処理
export ENV=val
cd ..
echo rakiyama > file
などなど
exportやcdなど結果を親プロセスに反映したいためです。
ヒアドキュメント
コマンド行にヒアドキュメントがあれば、(リダイレクトやコマンド実行処理に入る前に)全てのヒアドキュメントに対する入力内容を受け取ります。
cat << EOF | cat << EOF | cat << EOF
ヒアドキュメントの内容はPIPE(pipe()で生成するPIPE)に書き込み、その後のリダイレクト処理ではPIPEのFD(ファイルディスクリプタ)を参照するようにします。
子プロセスで入力・書き込み処理をした理由は、
ヒアドキュメントを受け取っている最中のシグナルハンドラは親プロセスと違ったのでシグナルハンドラを変えた別のプロセスで処理するようにしたかったからです。
リダイレクション
dup2()を使い、FDが指定されていれば(3>file
)そのFDで複製、指定がなければ(>file
)デフォルト値(0か1)で複製します。
out_fd = open("file name", O_WRONLY | O_CREAT | O_TRUNC, 0666); dup2(out_fd, FD);
- 親でリダイレクト処理する際の注意点
親でリダイレクト処理をする=標準入出力FDを書き換えると、その後入出力先が変わってしまうので
次の入力を受け取るまでに標準入出力FDをデフォルト値に戻す必要があります。
dup()でFD=0~2をバックアップしておき、一連の処理が終了したら、バックアップFD(標準入出力を指してる)を使って0~2を元の標準入出力に戻します。
一連の処理中に指定されたFD(3>file
の3)とバックアップFDが被ることが判明すれば、再度バックアップFDを取り直す必要があります。 - その他注意点
テスト
minishellの挙動を見る際に、以下の二つのスクリプトを別ターミナルで回していました。
- リーク検出(nopさんのコードを参考にしました)
psコマンドでminishellプロセスを見つけ出して、1秒毎にリークを監視します。
リークが検出されれば何かしら表示されるはずです。
#!/bin/bash echo ------------ps-------------- ps | tee ps_current_process.txt | sed '/grep/d' | GREP_COLOR='0;35' egrep --color=auto '.*minishel.*|$' echo ------------ps-------------- pid=`cat ps_current_process.txt | grep minishell | awk '{print $1}'` echo -e "\033[35mPID->${pid}\033[m" rm ps_current_process.txt while [ 1 ] do leaks $pid 1>/dev/null if [ $? -ne 0 ]; then leaks $pid fi sleep 1 done
#!/bin/bash echo ------------ps-------------- ps | tee ps_current_process.txt | sed '/grep/d' | GREP_COLOR='0;35' egrep --color=auto '.*minishel.*|$' echo ------------ps-------------- pid=`cat ps_current_process.txt | grep minishell | awk '{print $1}'` echo -e "\033[35mPID->${pid}\033[m" sleep 5 rm ps_current_process.txt while [ 1 ] do lsof -p $pid sleep 1 done
まとめ
学べたこと
反省点
チーム開発では、初期段階からお互いが何をしていてどこまで進んでいるのかを把握することが完成への近道ということ・またその意見交換の難しさを体感して学べました。
特に開発初期段階はとにかく手を動かしてみないとわからないことが多かったので、ついつい意見交換より個人的プログラミングに集中してしまって、作業が一旦落ち着いてから意見交換を挟むと膨大な量を説明することになり、相手を疲れさせてしまいました。
どれだけ個人の作業が途中段階でも、一定の期間でお互いを把握することの方が大切だと思いました。
初めはこのプロジェクトが終わる気がしませんでしたが、チームで話し合うと少しずつ前に進み面白かったです。
最後まで一緒に取り組めたrsudoさんには感謝です。
その他参考文献
チーム開発におけるプルリクの作法 - Qiita
各関数のman