わずか25行のRubyコードで自作シェルを作ってみよう
LinuxやMacを使っているなら、ターミナルを開くたびに、あなたは無意識のうちにシェルというアプリケーションを利用しています。
シェルとは、システム上でコマンドを実行するためのインターフェースです。環境変数を管理しながら、コマンド履歴や自動補完といった便利な機能を提供してくれます。
「物事の仕組みを裏側から理解したい」というタイプの人にとって、この記事はうってつけの内容です。それでは、実際にシェルを作りながら、その内部構造を見ていきましょう。
シェルはどのように動作するのか?
独自のシェルアプリケーションを構築するために、まず「シェルとは本来何なのか」を考えてみます。
シェルの動作は非常にシンプルです。まずプロンプトが表示され、通常は現在のユーザー名やカレントディレクトリといった情報が添えられています。そこへコマンドを入力し、Enterキーを押すと、結果が画面に表示される——これだけです。
とても基本的な仕組みに聞こえますが、どこかで見覚えがあると思いませんか?
pryを思い浮かべたなら、その通りです!
実は、シェルとはオペレーティングシステムのためのREPL(Read-Eval-Print-Loop)なのです。この点さえ押さえれば、最初のバージョンのシェルをすぐに書くことができます。
prompt = "> " print prompt while (input = gets.chomp) break if input == "exit" system(input) print prompt end
これだけで、最小限ながらちゃんと動作するシェルが完成しました。さらに改善するために、多くのREPL系アプリケーションが採用しているライブラリを活用してみましょう。
そのライブラリの名はReadlineです。
Readlineライブラリを活用する
ReadlineはRuby標準ライブラリの一部なので、インストール作業は一切不要。requireするだけで即座に使い始められます。
Readlineを使う最大のメリットは、コマンド履歴を自動的に保持してくれる点です。加えて、プロンプトの表示なども引き受けてくれるため、コードがぐっとすっきりします。
こちらがReadlineを採用したシェルのバージョン2です。
require 'readline'
while input = Readline.readline("> ", true)
break if input == "exit"
system(input)
end
いかがでしょうか。プロンプト表示のためのputsが不要になり、Readlineの強力な機能を自由に使えるようになりました。例えば、キーボードショートカットで単語を削除したり(CTRL + W)、履歴を検索したり(CTRL + R)することも可能です!
次に、全コマンド履歴を表示する新しいコマンドを追加してみましょう。
require 'readline'
while input = Readline.readline("> ", true)
break if input == "exit"
puts Readline::HISTORY.to_a if input == "hist"
# 履歴から空行を取り除く
Readline::HISTORY.pop if input == ""
system(input)
end
豆知識:このコードをpry上で実行すると、pry自身のコマンド履歴が表示されてしまいます!理由は、pryもまた
Readlineを使用しており、Readline::HISTORYがプロセス全体で共有された状態だからです。
これでhistと入力するだけで、コマンド履歴を一覧表示できるようになりました 🙂
自動補完機能を追加する
普段お使いのシェルの自動補完機能のおかげで、私たちは膨大なタイピングの手間を省けています。実は、Readlineを使えばこの機能を驚くほど簡単に自作シェルへ組み込めます。
まずは、履歴にあるコマンドを自動補完するところから始めてみましょう。
サンプルコード:
comp = proc { |s| Readline::HISTORY.grep(/^#{Regexp.escape(s)}/) }
Readline.completion_append_character = " "
Readline.completion_proc = comp
## 残りのコードはここに記述 ##
このコードにより、<tab>キーを押すだけで、過去に入力したコマンドを自動補完できるようになります。さらに一歩進んで、今度はディレクトリ名の自動補完も追加してみましょう。
サンプルコード:
comp = proc do |s|
directory_list = Dir.glob("#{s}*")
if directory_list.size > 0
directory_list
else
Readline::HISTORY.grep(/^#{Regexp.escape(s)}/)
end
end
completion_procは、補完候補のリストを返す役割を担います。この例では、Dir.globを使って入力中の文字列がディレクトリ名の一部に該当するかどうかをチェックしているだけです。あとの処理はすべてReadlineが面倒を見てくれます!
systemメソッドの正体に迫る
ここまでで、履歴と自動補完を備えた本格的なシェルが完成しました。たった25行程度のコードでここまでできるのは、なかなか悪くない成果です 🙂
しかし、もう少し掘り下げてみたいポイントがあります。それは、コマンドが実際に実行される瞬間、裏側で何が起きているのかという疑問です。
この処理を担っているのがsystemメソッドです。C言語のレベルでは、このメソッドは単純にコマンドを/bin/sh(シェルアプリケーション)へ渡しています。では、/bin/shが行っていることをRubyで再現するとどうなるのでしょうか。
注意:以下のコードはLinux / Mac環境でのみ動作します 🙂
systemメソッドの実装はこうなります。
def system(command)
fork {
exec(command)
}
end
ここで起きていることを順に追うと、まずforkが現在のプロセスのコピーを新しく生成します。続いて、その子プロセスがexecメソッドによって、実行したいコマンドのプロセスへと丸ごと置き換えられるのです。これはLinuxプログラミングにおいて非常によく使われる定番パターンです。
もしforkせずに直接execを呼ぶとどうなるでしょうか?現在のプロセス自体が置き換えられてしまうため、実行したコマンド(lsやcdなど)が終了した時点で、あなたのRubyプログラムも一緒に終了してしまいます。
実際にその挙動を確認してみましょう。
def system(command)
exec(command)
end
system('ls')
# この行は決して実行されません!
puts "after system"
まとめ
この記事を通じて、シェルとはシステムと対話するためのREPL風インターフェース(irbやpryをイメージすると分かりやすい)であることを学びました。また、強力なReadlineライブラリを活用すれば、履歴や自動補完といった機能を備えたオリジナルシェルを短いコードで構築できることも確認できました(ただし、補完の具体的な挙動は自分で定義する必要があります)。
さらに、Linuxプログラミングのプロジェクトで頻出するfork + execパターンの仕組みについても理解を深めることができました。
この記事が気に入ったら、ぜひRuby仲間の皆さんにもシェアしてみてください。ブログの成長につながり、より多くの人が学べる機会になります 🙂
-
Test-Commit-Revert(TCR):Rubyでレガシーコードにテストを追加するための実践ワークフロー
これは誰にでも起こりうることです。ソフトウェアプロジェクトが成長していくと、包括的なテストスイートを持たないまま本番環境にコードがデプロイされてしまうことがあります。数ヶ月後に同じコードを見返すと、理解するのが難しくなっているかもしれません。さらに悪いことに、バグが見つかっても、どこから手をつければいいのか分からないという状況も起こり得ます。 テストのないコードを修正することは大きな挑戦です。変更によって何かを壊してしまうかもしれないと確信が持てず、すべてを手動で確認するのは、良くてもミスが起きやすく、通常は不可能です。 このようなコードへの対応は、開発者が日常的に行う最も一般的なタスクの
-
Rubyでの静的分析入門!parser gemでメソッド定義を抽出する方法
ソースコードを解析して、すべてのメソッドがどこで定義され、どんな引数を受け取るのかを把握したいと思ったことはありませんか? どうすれば実現できるのでしょうか? 最初に思いつくのは、正規表現(regexp)を書くことかもしれません。 しかし、もっと良い方法があるとしたらどうでしょう? 答えは「あります」! 静的解析(Static Analysis)とは、ソースコードそのものから情報を抽出するためのテクニックです。 これは、ソースコードをトークンへと変換する(パースする)ことで実現されます。 それでは早速見ていきましょう! parser gemを使う Rubyには標準ライブラリとしてRipper