プログラミングの環境構築を自動化するシェルスクリプトを作ったので解説してみる
スポンサーリンク

どうも、ウェブ系ウシジマくんです。

先日、次のようなツイートをしたところ、多くの方に反応をいただくことができました。

作ろうと思ったキッカケは、毎回新しい案件に参画するたびに貸与される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に例えるなら、putsprintに近いですね。

今回のシェルスクリプトでは、

-> % 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_profilebashrcのような設定ファイルを再読込みするコマンド。

例えば、次のようにrbenvのPATHを追記した処理を記述しただけだと、書き込まれることはあっても、読み込まれることはありません。

echo 'eval "$(rbenv init -)"' >> ~/.zshrc

読み込ませるためには再度ターミナルを再起動するか、ログインする必要があります。

しかし、追加するたびに毎回再起動するのは面倒ですよね。

そこで、次のようにsourceコマンドで対象のファイルを指定することで、ファイル内に記述したPATHや環境変数などの設定値を再起動しなくても反映させることができるのです。

source ~/.zshrc

まとめ

結構大掛かりな解説になりました。。。

しかし、これでより一層Linuxコマンドやシェルに対する抵抗はなくなったので、これからもシェルスクリプトを書いていこうと思います。

スポンサーリンク
おすすめの記事