iOSエンジニアが開発効率のために最低限知るべきシェルスクリプト入門

iOSエンジニアが開発効率のために最低限知るべきシェルスクリプト入門

Table of Contents

bashやzsh,ターミナルは弄っていますか?黒い画面は古臭いと思ってませんか?

iOSエンジニアと言えどXcode上でSwiftをコーディングだけが全てではありません。
CocoaPodsCarthagefastlane などエコシステムはシェルコマンド操作が必須です。

またAPIとの通信にシェルスクリプトを使うことで、効率よく情報を集めたり事前準備できたりします。
シェルスクリプトはSwiftと同じただの道具です。使い方を覚えて視野を広げましょう。

この記事で基礎知識を得よう

この記事では難しいイディオムテクニックが書けるなどではなく、基本を理解してシェルスクリプトを読めるようになるための土台作りだとイメージしてください。
この土台を踏み台に少しずつシェルスクリプトに触れていくことで、より生産性の高いスキルを身に付けれるかと思います。

bashやzsh, fishと色々あるが基本は同じ

シェルスクリプトは色々ありますが、Swiftと違ってどれも活躍場所がターミナル上に閉じています。
そして基本部分は同じなので慣れてきたら自分好みのシェルを選ぶと流れがいいです。
ターミナル上の作業を効率よく進められれば結果的にiOS開発効率の向上に繋がります。

シェルスクリプトとは?

広義と狭義の意味を持っているのですが、ここではUnixシェルで使われるスクリプト言語です。
つまりシェルのスクリプトです。

ではシェルとは?

シェルとはOSとユーザーが対話するためのインターフェイスを提供するソフトウェアです。
ユーザーがOSに対して命令するための窓口とも言えます。
bashやzshなどもシェルでありコマンド言語でもあります。

コマンドとは?

シェルが提供している対話手段みたいなものです。
echo などがそれにあたります。

つまりシェルスクリプトとはechoなどのシェルコマンドにより構成されたスクリプト言語です。

よく使うコマンド

  • echo: 画面に文字列などを出力する
  • shift: 引数をN個ずらす
  • source: ファイル内のシェルを読み込んで実行する
  • awk: 空白で区切られたテキストを処理する
  • sed: 文字列置換
  • grep: テキスト検索
  • ag: grep高速版
  • pbcopy: クリップボードにコピー
  • pbpaste: クリップボードからペースト
  • mv: ファイル移動

echo: 画面に文字列などを出力する

$ echo "hoge"
hoge

単純に出力するしたり

$ var='asdf';echo $var
asdf

変数を展開したり

$ echo "Failed tests" > ~/Downloads/test.txt

出力先をリダイレクトしたりも出来ます。

shift: 引数をN個ずらす

shift [N] 引数が1,2,3,4,5の場合、

echo $1 → 1
echo $2 → 2
shift
echo $1 → 2
echo $2 → 3
shift 2
echo $1 → 4
echo $2 → 5

case/inと組み合わせることで引数を一つずつハンドリングする処理が書くこともできます。

source: ファイル内のシェルを読み込んで実行する

$ source ~/.bash_profile

というは、実は .bash_profile というシェルスクリプトを実行してるだけです。

awk: 空白で区切られたテキストを処理する

パイプなどで受け取った出力結果を一部分だけ抜き取る場合などに利用できます。

$ echo 'a b c d e f' | awk '{ print $2 }'
b

これは空白区切りして2つ目のテキストを出力します。

awkは高機能なためこれだけではありません。 詳しくは他解説サイトをご覧ください。

sed: 文字列置換

パイプなどで受け取った出力結果にたいし文字列置換したい場合に利用できます。

$ echo "apple orange" | sed s/apple/Apple/
Apple orange

sedは高機能なためこれだけではありません。 詳しくは他解説サイトをご覧ください。

grep: テキスト検索

パイプで受け取った出力結果に対して文字列を検索します。

$ cat Podfile.lock | grep -i firebase

このようにPodfile.lockに対してちょっとした検索などにも使えます。

パイプだけでなく指定フォルダのファイル内検索も可能ですが、結構重いです。なので自分は次のagを使っています。

ag: grep高速版

The Silver Searcherと呼ばれてます。
grepよりかなり高速です。

$ ag .storyboard

とするだけで実行ディレクトリから子ディレクトリ含め全部のファイルに対し、ファイル内検索を実行します。
アプリ上で指定文言が使われているか検索する場合、Xcode使わずこれを使うと早いです。
Xcodeでは検索に引っかからない場合もありますし。

pbcopy: クリップボードにコピー

出力結果をクリップボードにコピーします。
結果をslackなどに貼りたいなど別の場所に貼りたい場合に便利です。

$ tail -n 2 Gemfile.lock | pbcopy

↓がコピーされます。

BUNDLED WITH
1.17.2

pbpaste: クリップボードからペースト

pbcopy とは逆の貼り付けです。

$ echo "hoge" | pbcopy
$ pbpaste
hoge

mv: ファイルやフォルダを移動

ファイルを別ディレクトリに移動します。

fileAをdirectoryに移動

$ mv fileA directory/

ワイルドカードも使うことができます。

file-Aとfile-Bをdirectoryに移動

$ mv file-* directory/

フォルダの移動もできます。

directoryBをdirectoryに移動

$ mv directoryB/ directory/

入出力リダイレクト

入力をコマンドに渡したり、コマンドの結果を別ストリームに渡したりすることをいいます。

  • > : 上書き or ファイル作成
  • >> : 追加出力 or ファイル作成
  • 2> : 標準エラー出力をファイルにリダイレクト(上書き or 作成)
  • &> : 標準出力と標準エラー出力をファイルにリダイレクト
  • &>> : 標準出力と標準エラー出力をファイルに追記書き込み
  • >&2 : 標準出力を標準エラー出力にリダイレクト

ファイル作成(上書き)

結果を指定パスに出力します。

$ echo "test" > ~/test.txt
$ cat ~/test.txt
test

出力先が存在する場合は上書きします。

$ cat ~/test.txt
test
$ echo "test2" > ~/test.txt
$ cat ~/test.txt
test2

パイプライン

コマンドの結果を次のコマンドに渡す処理のことをいいます。
コマンドとコマンドをつなぐシェルでとても重要な機能の一つです。

使い方は簡単

command a | command bのように | を挟むだけで、 command aの結果が command b に引数として渡されます。

例えばファイル一覧からテキストを絞り込む場合は

$ ls -l | grep ".txt"

のようにls -lの出力結果をgrepに渡してテキストファイルの行を絞り込みします。

コマンドの連結

連結することで、コマンドを立て続けに実行して一度の命令で複数のコマンドを実行することができる。

  • ;(セミコロン)
  • &(アンパサンド)
  • &&(ダブルアンパサンド)
  • ||

;(セミコロン)で連結

command a ; command b command aを実行、コマンドが終わったらcommand bを実行する。

echo 'a' ; echo 'b'
a
b

&(アンパサンド)で連結

command a & command b command aを実行後すぐに command bを実行する。

sleep 1 ; echo 'a'

だと1秒後にaが出力される。

sleep 1 & echo 'a'

だと待たずにaが出力される。

&&(ダブルアンパサンド)で連結

command a && command b command aが成功したら、command bを実行する。 command aが失敗したら、command bは実行しない。

$ git checkout develop && git checkout pull --prune && git branch -d feature/hoge && git checkout -b feature/fuga

というコマンド連結だと、

  1. developブランチに移動
  2. pull –prune 実行
  3. feature/hogeブランチを削除
  4. feature/fugaブランチをチェックアウト or 作成

になりますが、どれか失敗すると以降のコマンドは実行されません。

||で連結

command a || command b && の逆。command aに失敗したらcommand bを実行する。

外部シェルスクリプトの作成

ターミナル上でシェルを操作するだけだとたくさんのコマンドや複雑な条件処理をしたいときに1行で書くこととなり不便です。 別のPCでも実行したいとなると保存されていないので、またそのPCで最初から打ち直すことになります。
1つファイルにシェルコマンドを集約するのがシェルスクリプトとなります。

シェルスクリプト雛形を用意

  1. ファイル作成
  2. シェバング記載
  3. 適当なコマンド入れる
  4. 実行権限の付与
  5. 実行してみる

ファイル作成

$ touch script

これでscriptというファイルが作成されます。

シェバング記載

scriptファイルをエディタで開いて1行目に次のコードを入れます。

#!/bin/bash

適当なコマンド入れる

scriptファイルを実行時に正しく実行できたか判断できるように簡単なコマンドを入れておきます。

echo 'I am script.'

実行権限の付与

scriptファイルに実行権限を付与します。これをしないと作成者以外のユーザーがこれを実行できないためです。

$ chmod 711 script

権限エラーの場合は頭にsudoつけてください。

実行してみる

ではこれを実行してみます。

$ ./script

コメント

# これはコメント

Shebang(シェバン)でOSにスクリプト情報を伝える

シェルスクリプトの先頭にコメントで

#!/bin/bash

と入っているのは、おまじないではなく、シェルスクリプトを実行したときOSがどの言語で実行するべきスクリプトなのかを判断するための情報となります。

例えばhoge.shというファイル名であれば拡張子から実行方法を割り出します。でもhogeというファイル名では分かりません。
OSはファイルの先頭に記載された情報を元に実行する方法を切り替えています。

実際に動かしてみる

次のファイルを用意します hoge

#!/bin/bash

echo 'I am bash'

次に実行権限を与えます。(必要に応じsudoつける)

$ chmod 711 hoge

このファイルを実行方法の指定なしで実行します。

$ cd <hogeがあるディレクトリまで移動>
$ ./hoge
I am bash

問題なくechoが出力されました。

では次にシェバングを#!/bin/bashから#!/usr/bin/rubyに変えます。

$ ./hoge
./hoge:2:in \`<main>\': undefined method \`echo\' for main:Object (NoMethodError)

rubyとして実行され、echoというメソッドがないことで失敗しました。

反対にシェバングを#!/bin/bashにしてコードをrubyに変更します。

#!/bin/bash
p 'I am ruby'

これを実行します。

$ ./hoge
./hoge: line 3: p: command not found

bashとして実行され、pというコマンドがないことで失敗しました。

このようにシェバングはOSに実行方法を伝える役割です。

exit 1 の意味

コマンドは実行の成功・失敗を表す終了ステータスという数値があり、その数値は特殊変数$?に格納される。
終了ステータスの数値は一般的に 成功0 失敗10以外で表されている。

$ echo 'complex command'
$ echo $?
complex command
0

変数

  • 変数の宣言
  • 変数の参照
  • 変数のエクスポート
  • 特殊変数

変数の宣言

変数宣言は次のように書きます。

VARIABLE=1

注意 =の前後にスペースを空けるとエラーになります。

変数の参照

変数にアクセスして中の値を使うには変数名の前に$をつけて$変数名と書いて使います。

echo $VARIABLE

${}でより厳密な参照

$varでも変数への参照はできるが、本来の正しい参照は${var}が正しい。
この方法じゃないと変数名によっては間違った解釈で異なる変数参照をしようとする。 また後述する文字列演算子を使うときに必要となる。

文字列演算子

変数の参照時にちょっとしたマクロ的な使い方ができます。

演算子 内容
${var:-value} varがありNULLではないなら、varを返し、それ以外は valueを返す
${var:=value} varがありNULLではないなら、varを返し、それ以外は varにvalueを入れて返す. 例外あり
${var:?msg} varがありNULLではないなら、varを返し、それ以外は msgを出力し処理を中止する
${var:+value} varがありNULLではないなら、varを返し、それ以外は NULLを返す
${var#pattern} varを先頭検索して pattern が一致したら、最短一致した部分を削除する
${var##pattern} varを先頭検索して pattern が一致したら、最長一致した部分を削除する
${var%pattern} varを末尾検索して pattern が一致したら、最短一致した部分を削除する
${var%%pattern} varを末尾検索して pattern が一致したら、最長一致した部分を削除する
${#var} varの長さを返す

変数のエクスポート

変数を定義したシェルから起動したシェルや実行したコマンドから変数を参照できるようになります。 これを環境変数と呼びます。

export var2=cdn

特殊変数

シェルスクリプトには予約された変数が存在しており、それらは決められた機能を持っています。

変数名 説明
$0 実行してるスクリプト名
$$ 実行してるスクリプトのプロセスID
$# スクリプト実行時に渡された引数の数
$1 ~ $9 引数それぞれの変数
$* 渡された引数全部を1つの変数として扱う
$@ 渡された引数を個別に扱う
$? 最後に実行したコマンドの実行結果
$! 最後に実行したバックグラウンドのプロセスID

引数

コマンドラインに渡された値が格納された変数です。

例えばtest.shの中身が下記コードだとして、 test.sh

echo $1

次のようなコマンドを実行すると、

$ sh test.sh hoge fuga nuga
hoge

とターミナルに出力されます。

$1

直前のコマンドの終了ステータスを保持

$?

条件分岐

コマンドの結果によって次の処理を変えたい場合は、if文やswitch/case文を使います。

if/else文

if 条件式 ; then
  # some process
elif 条件式 ; then
  # some process
else
  # some process
fi

閉じスコープが if の反対になってるのが特徴です。
また ; then;はSwiftと同じ役割です。開始ブレースが頭についたほうが読みやすいようにシェルスクリプトでも;をつかって表現しています。

これは;使わずに書くと

if 条件式
then
  # some process
elif 条件式
then
  # some process
else
  # some process
fi

となり、さっきまであったレイアウトの法則性がなくなり、視線の運び方に乱れがおきて負荷がかかります。 つまりスラスラと読みにくいです。

例えば

if 1 > 0 {
  print("True")
}

を書く場合は

if [ 1 -gt 0 ] ; then
  echo True
fi

と書きます。

ifの評価式にtestや[]を使う理由

if文はlsやgrepなどコマンドの終了ステータスを評価しています。 そして、終了ステータスは0なら真となりそれ以外なら偽となります。 そのため if $age > 5 ; then みたいには書けません。

上記みたいな式を書きたい場合は、testコマンドを使います。 testコマンドは受け取った引数を評価し、真なら0、偽なら1の終了ステータスを返してくれます。 これをifの評価式に挟むことで、我々が本来そうていしているif文が使えるようになります。

test 数値1 -eq 数値2

$ test 1 -eq 1 ; echo $?

ちなみにこれのシュガーシンタックスが[]となります。 上のコードは次のように書けます。

$ [ 1 -eq 1 ] ; echo $?

しかし残念ながら、testコマンドは ==,<,>,<=,>= は使えない。代わりに文字列で指定するしかない。

引数 意味 いわゆる
-lt Less than <
-gt Greater than >
-le Less than or equal <=
-ge Greater than or equal >=
-eq Equal ==
-ne Not equal !=

ちなみに and と or は and はexpression1 -a expression2 or はexpression1 -o expression2 となる。

否定は ! となる [ ! $hoge ]

一般的にif test hogeと書かずにif [[hoge]]のように書かれる。 [[]]はtestの略式コマンドとなる

また文字列の比較であれば、 =!= が使える

testコマンドでファイルやディレクトリの存在有無が分かる

ディレクトリは -d オプションを指定することで、その次に続くパスにディレクトリがあるか確認できる

if [ -d '/your/directory/path' ]; then
  echo "ある"
else
  echo "ない"
fi

ファイルは -f or -e オプションを指定することで、確認できる。

またファイルが読取可能, 書込可能, 実行可能の確認もできて、それぞれ -r -w -xとなる。

またファイルが空サイズかどうかの確認は -sで確認できる。

testコマンドで2つのファイルの新旧を比較できる

[ $file1 -nt $file2 ]$file1 が新しいと真となる [ $file1 -ot $file2 ]$file1 が古いと真となる

それぞれ -nt → newer than -ot → older than となる。

testコマンドで文字列の長さチェックができる

[ -z $hoge] は文字列の長さが0なら真 [ -n $hoge] は文字列の長さが1以上なら真

bash,zshなどは[]より[[]]を使おう

[]の説明をしましたが、[[]]が使える場合はそちらを使ったほうがいいです。 []は変数展開でワード分割やパス名展開されて想定外の判定結果となるためです。

通常であれば↓のような結果ですが、

$ var='hogehoge'
$ [ $var == hogehoge ]; echo $?
0

このような場合だとエラーになります。

$ var='hoge hoge'
$ [ $var == hogehoge ]; echo $?
too many arguments
2

詳しくはこちらの記事が参考になります。 test と [ と [[ コマンドの違い - 拡張 POSIX シェルスクリプト Advent Calendar 2013 - ダメ出し Blog

switch/case文

casein
  ケース1 ) 処理 ;;
  ケース2 ) 処理 ;;
esac

;; セミコロン二つはコマンドの終わりを知らせます。

評価値が文字列の場合は正規表現が使える

branch=
case "$1" in
  -b | --branch)
    echo $2
    branch=$2
    ;;
esac

正規表現マッチング

変数varに挨拶が含まれてるか調べる

var='hello, world'
if [[ $var =~ ^.*hello.*$ ]] ; then
  echo '挨拶'
fi

=~を使うことで正規表現判定となります。

ループ

for文

for var in one two three; do
  echo $var
done

while文

while 評価式; do
  break
done
while read var; do
  echo $var
done < ~/Downloads/hoge.txt

関数

function my_func() {
  echo hello_world
}

引数あり関数

function hello() {
  echo "hello, $1"
}
hello "hoge"

特殊引数

ダブル・ダッシュ

ハイフン二つ(–)のことをダブル・ダッシュ(Double Dash)と呼ばれており、これはコマンドフラグの終わりを示す記号となる。
オプションのスキャン処理を明示的に終了させるときに使う。

展開

$(コマンド) でコマンド結果を出力します。
同様の機能にバッククォートで囲む コマンド もあります。
ちなみにバッククォートは入れ子にできません。

正常終了と異常終了

exit 終了ステータス

正常: exit 0 以上: exit 1など0以外

echo で色

16進数

echo -e "\033[1;31m This is red text \033[0m"

応用

bash限定: local の意味

シェルスクリプトの変数はデフォルトはグローバル変数です。
グローバル変数は、関数内で変数が使われていても、外でその変数にアクセスできるなど不具合の巣窟です。

function hoge() {
  var="test"
  echo $var
}
hoge
echo $var # "test" と出力される

関数で使った変数は関数から出たら消えてほしい場合には local を使います。

function hoge() {
  local var="test"
  echo $var
}
hoge
echo $var # 空出力

forなど内部で変数が使われるような場合は、事前に使う変数をlocal変数宣言しておきます。

local var
for var in `echo test1 test2 test3` ; do
  echo $var
done
echo $var # 空出力

まとめ

一度に全部を覚える必要はないですが、シェルやシェルスクリプトを使いこなすことで、iOS開発効率やAPIを効率良く検証したりいいことばかりです。

このエントリーをはてなブックマークに追加