どうも、ウェブ系ウシジマくんです。
先日、次のようなツイートをしたところ、多くの方に反応をいただくことができました。
毎回案件参画するたびに、いちいちライブラリやツールをダウンロードするのは面倒。
— ウェブ系ウシジマくん@リモート専門エンジニア (@web_ushizima) March 19, 2019
だから復習も兼ねて質問に答えるだけで自動でインストールしてくれるシェルスクリプト作った。https://t.co/KeOz2RL9f8
これさえあれば、開発に必要な環境構築が一瞬で終わる。
やはり自動化は正義。
作ろうと思ったキッカケは、毎回新しい案件に参画するたびに貸与されるMacに諸々をインストールするのが面倒だと思ったから。
処理自体は簡単な内容ではあるのですが、せっかく作ったのに各実行コマンドの意味を理解していないのは意味がない。
そこで今回は作成したシェルスクリプト一つ一つのコマンドについて解説していこうかなと思います。
まずはシェルスクリプトの内容を確認
GitHubに記載している内容ですが、改めてソースコードを記載しておきます。
#!/bin/sh
set -e
echo " ------------ Set Password ------------"
# パスワードを記憶
read -sp "このMacにログインした際のパスワードを入力してください: " __pass;
echo "\n ------------ END ------------"
echo " ------------ Homebrew ------------"
read -p "Homebrewをインストールしますか? (y/n)" Answer < /dev/tty
case ${Answer} in
y|Y)
echo "Start Install Homebrew..."
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
echo "Install Homebrew is Complete!" ;;
n|N)
echo "インストールをスキップしました" ;;
esac
echo " ------------ END ------------"
echo " ------------ zsh ------------"
read -p "ログインシェルをzshに変更しますか? (y/n)" Answer < /dev/tty
case ${Answer} in
y|Y)
echo "=================="
echo "現在のzshのPATH: $(which zsh)"
echo "=================="
echo "=================="
echo "現在のzshのバージョン: $(/bin/zsh --version)"
echo "=================="
echo "=================="
echo "Homebrewでインストールできるzshのバージョン: $(brew info zsh)"
echo "=================="
read -p "このままzshをインストールしていいですか? (y/n)" Answer < /dev/tty
case ${Answer} in
y|Y)
brew install zsh zsh-syntax-highlighting
echo ${__pass} | sudo -S -- sh -c 'echo '/usr/local/bin/zsh' >> /etc/shells'
chsh -s /usr/local/bin/zsh
;;
n|N)
echo "インストールをスキップしました" ;;
esac
echo "=================="
echo "現在のzshのPATH: $(which zsh)"
echo "=================="
echo "=================="
echo "現在のzshのバージョン: $(/usr/local/bin/zsh --version)"
echo "=================="
echo "=================="
echo "現在のログインシェル: $(echo $SHELL)"
echo "=================="
FILE="${HOME}/.bash_profile"
if [[ -e ${FILE} ]]; then
source ${FILE} >> ~/.zshrc
elif [[ ! -e ${FILE} ]]; then
touch ${FILE}
fi
source ~/.zshrc
echo "zshをインストールしました。一度プロセスを終了します。"
exec $SHELL -l #ログインシェルの再読み込み
;;
n|N)
echo "インストールをスキップしました" ;;
esac
echo " ------------ END ------------"
echo " ------------ ITerm2 ------------"
read -p "ITerm2をインストールしますか? (y/n)" Answer < /dev/tty
case ${Answer} in
y|Y) brew cask install iterm2 ;;
n|N)
echo "インストールをスキップしました" ;;
esac
echo " ------------ END ------------"
echo " ------------ Install Ruby ------------"
read -p "zshにrbenv/ruby-buildをインストールしますか? (y/n)" Answer < /dev/tty
case ${Answer} in
y|Y) brew update
brew install rbenv ruby-build
echo 'eval "$(rbenv init -)"' >> ~/.zshrc
source ~/.zshrc
echo "rbenvとruby-buildをインストールしました"
echo "=================="
echo "現在のrbenvのバージョン: $(rbenv --version)"
echo "=================="
echo "=================="
echo "現在のインストール済Rubyのバージョン \n $(rbenv versions)"
echo "=================="
read -p "最新版のrubyをglobalのバージョンとして設定しますか? (y/n)" Answer < /dev/tty
case ${Answer} in
y|Y)
ruby_latest=$(rbenv install -l | grep -v '[a-z]' | tail -1 | sed 's/ //g')
rbenv install ${ruby_latest}
rbenv global ${ruby_latest}
rbenv rehash
echo "===インストールされたRubyのバージョン=== \n $(ruby -v) \n======"
;;
n|N)
echo "インストールをスキップしました" ;;
esac;;
n|N)
echo "インストールをスキップしました" ;;
esac
echo " ------------ END ------------"
echo " ------------ MySQL ------------"
read -p "MySQL5.7をインストールしますか? (y/n)" Answer < /dev/tty
case ${Answer} in
y|Y) brew install mysql@5.7
brew link mysql@5.7 --force
echo "MySQL5.7のインストールが完了しました" ;;
n|N)
echo "インストールをスキップしました" ;;
esac
echo " ------------ END ------------"
echo " ------------ Nokogiri ------------"
read -p "Nokogiri関連のライブラリをインストールしますか? (y/n)" Answer < /dev/tty
case ${Answer} in
y|Y) brew install libxml2
echo 'export PATH="/usr/local/opt/libxml2/bin:$PATH"' >> ~/.zshrc
export PKG_CONFIG_PATH="/usr/local/opt/libxml2/lib/pkgconfig"
export LDFLAGS="-L/usr/local/opt/libxml2/lib"
export CPPFLAGS="-I/usr/local/opt/libxml2/include"
echo "インストールが完了しました" ;;
n|N)
echo "インストールをスキップしました" ;;
esac
echo " ------------ END ------------"
echo " ------------ Redis ------------"
read -p "Redisをインストールしますか? (y/n)" Answer < /dev/tty
case ${Answer} in
y|Y) brew install redis
echo "インストールが完了しました" ;;
n|N)
echo "インストールをスキップしました" ;;
esac
echo " ------------ END ------------"
echo " ------------ Yarn ------------"
read -p "yarnをインストールしますか? (y/n)" Answer < /dev/tty
case ${Answer} in
y|Y) brew install yarn
echo "インストールが完了しました" ;;
n|N)
echo "インストールをスキップしました" ;;
esac
echo " ------------ END ------------"
結構眺めのスクリプトですが、実行しているコマンド自体はそこまで多くありません。
スクリプト内で実行しているコマンド
- case
- chsh
- echo
- exec
- export
- /dev/tty
- read
- source
洗い出してみると、8個しかないですね。それでは次のセクションから解説していきます。
#!/bin/shとは
普段シェルスクリプトを書くときに「おまじない」として書いておくケースが多いですが、自分の理解を深めるために解説していきます。
#!/bin/sh
は一見するとコメントっぽいですが、実はれっきとしたコマンド。
2つのコマンドが組み合わさったものであり、
- #!
- /bin/sh
に分類することができます。
#!について
#!
はShebang(シバンまたはシェバン)と呼ばれるもので、これが書かれていると後に続くPATHに対してファイルに記載さいれている処理の内容を実行することになります。
より詳しい話を知りたい場合は、以下の記事が参考になるので、読んでみると面白いかもしれないですね。
/bin/shについて
/bin
はBinary Code(バイナリーコード)のことを指していて、このPATHに含まれているインタプリタに対して処理コマンドが実行できるになっています。
インタプリタとは、簡単に言えば処理の内容をコンピューター用の言語(機械語で)に変換する翻訳機みたいなものですね。
ターミナルでls /bin
と実行してみると、使っているコンピューター内で使用できるインタプリタの一覧が表示されます。
-> % ls /bin
[ csh ed launchctl mv rmdir tcsh
bash date expr link pax sh test
cat dd hostname ln ps sleep unlink
chmod df kill ls pwd stty wait4path
cp echo ksh mkdir rm sync zsh
つまり、#!/bin/sh
と記述することで、
「shというインタプリタを通じてファイルに書かれている処理を機械語に翻訳し、それをコンピューターに実行させますよ」
ということですね。
caseについて
case文はifと同じく処理を条件分岐できるコマンド。
他のプログラミング言語にも登場しますので、慣れている方はイメージが付きやすいのではないでしょうか。
今回のシェルスクリプトでは頻出しており、次のように使用しています。
case ${Answer} in
y|Y) brew install yarn
echo "インストールが完了しました" ;;
n|N)
echo "インストールをスキップしました" ;;
esac
caseとinのあいだに変数名を指定し、それが渡されたことをキッカケ(以下、トリガーと記述)にして、条件分岐がスタートします。
)
の部分は条件分岐させるパターンとなっていて、それがトリガーとなり、各処理を実行していくことになるんですね。
また、そのパターンの最後には、処理の終わりを示す、セミコロンを2つ(;;)つけることになっています。
全体的な条件分岐の記述が終わったら、一番最後にcase
と同じインデントにesac
と下記、まとまりを示せば完成です!
構文
case ${変数名} in
) <実行したい処理> ;;
esac
chsh
このコマンドは実行するログインシェルを変更するためのコマンド。
ログインシェルというのは、PCにログインした際に最初に読み込まれるシェルの種類(bashやzshなど)ですね。
今回のシェルスクリプトでは、
chsh -s /usr/local/bin/zsh
といった形で使用していますね。
chsh -s
のように-s
オプションを渡すことで、指定したPATHをログインシェルに変更することができます。
注意点としては、ちゃんと指定したPATHが通るように(読み込めるように)しておかないと、正しくシェルが実行できないことですかね。
ちなみに、chはchangeの略で、shはshellの略です。
echoとは
引数に指定した内容をターミナル上に表示するためのコマンド。
Rubyに例えるなら、puts
やprint
に近いですね。
今回のシェルスクリプトでは、
-> % echo " ------------ END ------------"
------------ END ------------
このような形で使っています。
なお、変数や環境変数を次のように渡すと、その中を表示することができます。
# $VARのように$をつけるのがポイント
-> % echo $LANG
ja_JP.UTF-8
exec
execコマンドは、引数に指定したシェルを用いて処理を実行するコマンド。
今回のシェルスクリプトでは、
exec $SHELL -l
このように指定しています。
なぜこの処理をしているのかというと、zshをインストールする前のshellはbashなどのデフォルトのシェルが使われています。
それは$SHELL
という変数に格納されており、先程解説したechoコマンドを使って出力してみると、
-> % echo $SHELL
/usr/local/bin/bash
このようなになっています。
zshがインストールされた直後というのは、まだログインシェル自体はbashのまま。
exec $SHELLの-l
と実行することで、それまでのプログラミング実行の流れを無視して$SHELL -l
を実行することになります。
それまでのプログラミング実行の流れを無視して
ここの部分ですが、基本的にプログラミングでは上から処理が実行されていき、それぞれには後から実行された過程(プロセス)を参照しやすくするためのIDが振られています。(これをPID(プロセスID)といいます)
本来であれば、シェルコマンドを実行すると、親のPIDからコマンド実行するためのIDを作り、関連づけをします。
execコマンドは関連付けをせずに、割り込みで引数に与えた処理を実行するので、実行されるといくらその後に処理を書こうが実行されずに終了してしまうということを覚えておくといいかもです。
なお、 -l
というのはzshのコマンドで、指定したシェルを使って再ログインすることを指しています。
exportについて
exportは環境変数を設定するコマンドです。
export PATH="/usr/local/opt/libxml2/bin:$PATH
このように書くことでPATHという環境変数とその中身をセットしていることになり、結果として/usr/local/opt/libxml2/bin
というPATHを通すことになります。
ちなみに、PATH:/hoge/$PATHと書くことでPATHを追加する意味になります。
/dev/tty
これはコマンドというより、どちらかというと標準入力ですね。
/dev/ttyと記述することで、現在使っているターミナルからの入力を意味します。
Answer < /dev/tty
このように<
という記号と組み合わせれば、入力された値を出力先(今回でいえばAnswerという変数)に渡せます。
<
はリダイレクト記号と呼ばれるもので、これを用いればファイルに対しての処理を簡単に記述することが可能なんですね。
read
readコマンドはユーザーがキーボードで入力した値を受け取り、それを変数にわたすことができます。
今回のシェルスクリプトでは、
read -p "yarnをインストールしますか? (y/n)" Answer < /dev/tty
のように記述していますね。
-p
オプションにより、ユーザーが入力するのを待つようになり、入力されたら変数にその受け取った入力を代入します。
前述した/dev/tty
やリダイレクト記号、case文
を組み合わせることで、より柔軟なスクリプトを作成することができるので、便利です。
構文
read -p "表示したい文字列"
source
sourceはbash_profile
やbashrc
のような設定ファイルを再読込みするコマンド。
例えば、次のようにrbenvのPATHを追記した処理を記述しただけだと、書き込まれることはあっても、読み込まれることはありません。
echo 'eval "$(rbenv init -)"' >> ~/.zshrc
読み込ませるためには再度ターミナルを再起動するか、ログインする必要があります。
しかし、追加するたびに毎回再起動するのは面倒ですよね。
そこで、次のようにsourceコマンドで対象のファイルを指定することで、ファイル内に記述したPATHや環境変数などの設定値を再起動しなくても反映させることができるのです。
source ~/.zshrc
まとめ
結構大掛かりな解説になりました。。。
しかし、これでより一層Linuxコマンドやシェルに対する抵抗はなくなったので、これからもシェルスクリプトを書いていこうと思います。