1901
2277

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

株式会社NucoAdvent Calendar 2023

Day 10

【永久保存版】シェルスクリプト完全攻略ガイド

Last updated at Posted at 2023-12-09

この記事はNuco Advent Calendar 202310日目の記事です。

目次

シェルスクリプトの世界へようこそ!
1. シェルスクリプトの作成と実行を体験しよう
2. シェルスクリプトで変数を使ってみよう
3. シェルスクリプトで文字列を扱おう
4. シェルスクリプトで引数を扱おう
5. 特殊パラメータについて知ろう
6. シェルスクリプトで配列を作ってみよう
7. シェルスクリプトで条件分岐をしてみよう
8. シェルスクリプトでループ処理を書いてみよう
9. 関数を使って処理をまとめてみよう
10. リダイレクト・ヒアドキュメント・パイプを使いこなそう
11. オプションを使ってデバッグしやすくしよう
12. 便利なパラメータ展開を使ってみよう
13. ブレース展開を使ってみよう
14. パス名展開とチルダ展開を使ってみよう
15. コマンド置換を使ってみよう
16. シェルスクリプトで計算をしてみよう
17. 可読性を上げるためにコーディング規約を守ろう
18. 最後に便利なシェルスクリプトで便利なコマンドを紹介!

弊社Nucoでは、他にも様々なお役立ち記事を公開しています。よかったら、Organizationのページも覗いてみてください。
また、Nucoでは一緒に働く仲間も募集しています!興味をお持ちいただける方は、こちらまで。

シェルスクリプトの世界へようこそ!

シェルスクリプトって何?

シェルスクリプトは普段 Bash などのシェルで実行しているコマンドを並べて、まとめて実行できるようにしたものです。ファイルのバックアップ、ログファイルの解析、システムのセットアップなど、ターミナルで行っていた一連の作業をまとめてシェルスクリプトに記述することで、シェルスクリプトを実行するだけで自動的に一連の作業を終わらせることができるようになります。また、シェルスクリプトはファイルに保存することになるため、再利用するのも簡単になります。

シェルスクリプトで使うコマンドの多くは UNIX コマンドになると思います。そのため、シェルスクリプトを書く際は UNIX コマンドについてある程度の知識があることが前提となってきます。以下の記事などを参考に UNIX コマンドについても学んでおくことをオススメします。

シェルスクリプトはいつ使うの?

シェルスクリプトでは変数や条件分岐、ループ処理などの他のプログラミング言語にもあるような機能が使えるので、少し複雑な処理も書くことができます。しかし、言語仕様や実行速度の関係で大規模なシステムの開発には向いていません。シェルスクリプトは以下のような場合に使うのがいいでしょう。

  • 定期的なシステムメンテナンス
    ファイルのバックアップ、ログの分割・圧縮・削除、システムの状態チェックなど、定期的に行う作業の自動化
  • 簡単なデータ処理
    ファイルの検索、テキストの加工、簡単な集計など、UNIXコマンドを組み合わせて行うデータ処理
  • システムのセットアップやデプロイ
    ソフトウェアのインストール、設定ファイルの編集、サービスの起動・停止など、システムの初期設定やアップデートの自動化
  • テストスクリプトの作成
    システムやアプリケーションの動作を確認するための簡単なテストスクリプト

他のプログラミング言語と比べたときのメリット、デメリットも紹介します。

メリット

  • シェルと同じ言語で書ける
    普段シェル上で作業をしている場合、シェル上での手作業を何度か繰り返して面倒になったら、その流れをシェルスクリプトにそのまま書けばいいだけです。また、シェルスクリプト用にシェルの構文を学ぶことで、普段のシェル上での作業にも活かすことができます。
  • 他のプログラミング言語に比べて短いコードで処理できる
    シェルのコマンドや構文を利用すると他のプログラミング言語に比べて非常に短いコードで目的を達成できることが多いです。ファイルへの出力は > (リダイレクト)でできますし、コマンドの実行結果を別のコマンドに渡すのも、変数を介さずに | (パイプ)で繋ぐだけで可能です。

デメリット

  • 処理速度が遅い
    シェルスクリプトは外部コマンドの起動が遅いので、外部コマンドを頻繁に起動すると、起動のコストが大きくパフォーマンスが低下します。
  • 大規模なアプリケーションの開発には向かない
    シェルスクリプトでは構造体や階層構造を持ったデータ構造を扱えません。また、上記の処理速度の問題もあるため、大規模なアプリケーションの開発には不向きです。シェルスクリプトの規模が大きくなってきたら、他のプログラミング言語への乗り換えを検討すべきです。

シェルスクリプトを使う際は、上記を念頭に入れておきましょう。

ここまではシェルスクリプトの特徴について解説しました。ここからは実際にシェルスクリプトをどのように書けばいいのかを解説していきます!

1. シェルスクリプトの作成と実行を体験しよう

(1-1) シェルスクリプトファイルを作成しよう

ターミナルから test.sh という名のシェルスクリプトを作成してみましょう。

terminal
$ touch test.sh

シェルスクリプトの拡張子について
基本的に .sh をつけましょう。

詳細

シェルスクリプトの拡張子は通常 .sh ですが、必須ではありません。 test というファイル名でもシェルスクリプトとして実行可能です。ただし、.sh 拡張子をつけることでシェルスクリプトだと理解しやすくなるため、基本的には拡張子をつけることをオススメします。

(1-2) シェルスクリプトファイルを編集しよう

作成した test.sh をお好みのエディタで下記のように編集してみましょう。

test.sh
#!/bin/bash

echo 'Hello world!'

shebang
スクリプト行頭の #! で始まる行を shebang (シェバン/シェバング/シバン)と言います。

詳細

どのプログラムでスクリプトを実行すべきかを指定するためのものです。 shebang はシェルスクリプト限定のものではなく、スクリプト言語すべてに対して使えます(ex. #!/usr/bin/env python3)。スクリプトファイルに後述の実行権限をつけて直接実行すると、シェルがこの shebang を見て、該当するプログラムでスクリプトファイルを実行します。これにより、シェルスクリプトのファイル名に .sh 拡張子がついていなくても、実行時にシェルスクリプトとして実行されます。

#!/bin/bash#!/bin/sh の使い分け
基本的に #!/bin/bash を使うのが良さそうです。

詳細
  • シェルスクリプトのサンプルでは #!/bin/bash#!/bin/sh の2つがよく現れますが、結論、基本的に #!/bin/bash を使うのが良さそうです。bash の方が sh より機能が多く便利なためです。bash では使えるコマンドや記法が sh では使えないこともあります。
  • 移植性を気にする場合は #!/bin/sh を使いましょう。#!/bin/shではPOSIX(UNIX 系 OS 間でアプリケーションの移植性を高めるために定義された標準規格)準拠の動きをするプログラムでシェルスクリプトを実行します。(Red Hat 系の Linux では bash のPOSIXモードが実行され、Ubuntu 系の Linux では dash というプログラムが実行されます。)

以下では #!/bin/bash の場合のシェルスクリプトについて説明していきます。

(1-3) シェルスクリプトに実行権限をつけよう

ターミナルで test.sh に実行権限をつけてみましょう。

terminal
$ chmod +x test.sh

(1-4) シェルスクリプトを実行してみよう

ターミナルから test.sh を実行してみましょう。

terminal
$ ./test.sh
Hello world!

シェルスクリプトの実行方法について
実行権限をつけたファイル名を直接指定して実行しましょう。

詳細

上で説明した「実行権限をつけたファイル名を指定して実行する」方法(./test.sh)の他に、 sh test.sh, bash test.sh, source test.sh, . ./test.sh といった実行方法もあります。しかし、ターミナルからシェルスクリプトを実行する際は、「実行権限をつけたファイル名を指定して実行する」方法を推奨します。例えば、 shebang に #!/bin/bash と書かれたシェルスクリプトファイルを sh test.sh として実行した場合、 shebang が無視されて sh で実行されることになり、bash 用に書いていたスクリプトが動かないといったことが起こり得ます。

(1-5) シェルスクリプトにコメントをつけてみよう

test.sh を編集してコメントをつけてみましょう。シェルスクリプトのコメントは # でつけることができます。コメントアウトされたコマンドは実行されません。

test.sh
#!/bin/bash

# ここはコメントです。
# echo 'この行は実行されません。'

echo 'Hello world!' # 行の途中からでもコメントをつけることができます。
terminal
$ ./test.sh
Hello world!

(1-6) シェルスクリプトで複数のコマンドを実行してみよう

  • 改行で区切る場合
    シェルスクリプトでは基本的に1つの行に1つのコマンドを書いていきますが、この時改行が1つのコマンドを区切る役割を果たしています。そのため、コマンドの途中で改行を入れることはできません。ただし、行末に \ を書くことで、改行をまたいで1つのコマンドとして実行することも可能です。
test.sh
#!/bin/bash

echo 'Hello world!' 
pwd                 # 改行で区切られているので上のechoとは別のコマンドとして実行される
echo \              # 改行をまたいで1つのコマンドとして実行可能
        'I' \       # 複数行にわたって繋げることができる
        'like' \    # コマンド名やオプションが長くなって来た時に使おう
        'shell' \
        'script'
echo                # 引数なしの echo コマンドとして実行される
'End world!'        # 'End world!' というコマンドを実行しようとするが存在しないのでエラーになる
terminal
$ ./test.sh
Hello world!                                                  
/Users/user1/work/shellscript
I like shell script

./test.sh: line 11: End world!: command not found        
  • ; で区切る場合
    1つの行に複数のコマンドを書くことも可能です。コマンド同士を ; で区切ると別々のコマンドとして逐次実行してくれます。
test.sh
#!/bin/bash

echo 'Hello world!'; pwd; echo 'End world!';
terminal
$ ./test.sh

Hello world!                                                  
/Users/user1/work/shellscript
End world!

2. シェルスクリプトで変数を使ってみよう

(2-1) 変数への代入

変数へ値を代入するときは 変数名=値 の形式で書きます。可読性目的で = の前後にスペースを入れてはいけません。言語的にエラーになってしまいます。

test.sh
#!/bin/bash

var='value'   # = の周りにスペースを入れてはいけません
echo $var     # 後述しますが $ は変数を参照する時につける記号です

var='change'  # 定義済みの変数の値を変更することも可能です
echo $var
terminal
$ ./test.sh
value
change

変数への代入時に = の前後にスペースを入れてはいけない理由
= の前の文字列が変数ではなくコマンドとして扱われてしまうためです。

詳細

実際に = の前後にスペースを入れて実行してみるとどうなるでしょうか? 

test.sh
#!/bin/bash

var = 'value'
echo $var
terminal
$ ./test.sh
./test.sh: line 3: var: command not found

このエラーは「var というコマンドに = と 'value' という引数を与えた」と解釈されるために起こります。
コマンドとして解釈されないために、 = の前後にスペースを入れてはいけないのです。

(2-2) 変数の参照

変数を参照するときは $変数名 のように変数名の前に $ をつけます。また、 ${変数名} のように {} で囲んでも参照できます。

test.sh
#!/bin/bash

var1='value' 
echo $var1   # $ で変数を参照

var2=$var1   # 代入先の変数には $ をつけません
echo ${var2} # {} をつけても参照できます
terminal
$ ./test.sh
value
value
change

変数名に使える文字
半角英数字とアンダーバー(_)が使えます。ただし、数字で始まる変数名は使えません。また、英語の大文字と小文字は区別されます。

test.sh
#!/bin/bash

# 1var='error' (数字で始まる変数名は使用不可です)

_var1='_var1'
echo $_var1

Var_1='Var_1' # OK
echo $Var_1


# 大文字小文字は区別されます
var='lower'
echo $var
VAR='UPPER'
echo $VAR
Capital='Capital'
echo $Capital
terminal
$ ./test.sh
_var1
Var_1
lower
UPPER
Capital

(2-3) readonly による変数の上書き防止

readonly をつけて変数を定義することで上書きができなくなります。定数の定義に使うといいでしょう。

test.sh
#!/bin/bash

readonly VAR='hoge'
VAR='fuga'
terminal
$ ./test.sh
./test.sh: line 4: VAR: readonly variable

(2-4) unset による変数の削除

unset コマンドを使うことで変数を未定義の状態に戻すことができます。

test.sh
#!/bin/bash

var='set'
echo $var
unset var # unset するときは $ をつけない
echo $var # 未定義なので何も表示されない
terminal
$ ./test.sh
set

3. シェルスクリプトで文字列を扱おう

(3-1) シェルスクリプトにおける文字列の扱い

シェルスクリプトでは文字列の前後をクォーテーション(''"")で囲まなくても文字列として扱われます。ただし、スペースやタブを含む文字列を1つの文字列として扱いたい場合はクォーテーションで囲む必要があります。

test.sh
#!/bin/bash

var=value       # クォーテーションをつけなくても文字列の扱いになる
echo $var
var='hoge fuga' # スペースを含む場合はクォーテーションをつける必要がある
echo $var
var=hoge fuga   # クォーテーションをつけないと予期せぬエラーに
echo $var
terminal
$ ./test.sh
value
hoge fuga
./test.sh: line 7: fuga: command not found

(3-2) 空文字と未定義

変数には空文字を代入することもできます。空文字が代入された変数を参照した時と、未定義の変数を参照した時は同じ結果に見えますが、状態としては全く別の扱いになります。

test.sh
#!/bin/bash

empty=''
echo \$empty: $empty # 後述のエスケープを使用
empty= # これも空文字を意味する
echo \$empty: $empty
echo \$undefined: $undefined
terminal
$ ./test.sh
$empty:
$empty:
$undefined:

(3-3) 特殊文字とエスケープ

シェルスクリプトでは以下の文字は特殊文字として扱われます。

* ? [ ' " ` \ $ ; & ( ) | ~ < > # % = スペース タブ 改行

これらの文字を単に文字列として使用したいときは \ を前に置いてエスケープして使いましょう。

test.sh
#!/bin/bash

var=value
echo $var
echo \$var      # $ をエスケープすることでパラメータ展開させない
echo \$var $var # 並べて出力
echo \\         # \ 自身も \ でエスケープできる

var=hoge\ fuga  # スペースをエスケープすることで連続した文字列として処理できる(見づらいので非推奨)
echo $var
terminal
$ ./test.sh
value
$var
$var value
\
hoge fuga

(3-4) 文字列をシングルクォーテーションで囲む

シェルスクリプトでは文字列をダブルクォーテーションで囲んだ時と、シングルクォーテーションで囲んだ時とで、内部の文字列の扱いに明確な差があります。シングルクォーテーションで囲んだ文字列では、シングルクォーテーション以外の特殊文字がエスケープされます。結果として、シングルクォーテーション内では式が展開されません。

test.sh
#!/bin/bash

var1=value
var2='${var1}'
echo $var2
terminal
$ ./test.sh
${var1}

(3-5) 文字列をダブルクォーテーションで囲む

一方、ダブルクォーテーションで囲んだ文字列では $, `, / 以外の特殊文字がエスケープされます。このため、ダブルクォーテーションの中では以下が機能します。

  • $ による式展開
  • ` によるコマンド置換(後述)
  • / によるエスケープ
test.sh
#!/bin/bash

var1=value
var2="${var1}"
echo $var2
terminal
$ ./test.sh
value

(3-6) 文字列の連結

文字列を連結するには演算子は必要なく、繋げて記述すれば連結できます。文字列の間にスペースは不要です。

test.sh
#!/bin/bash

var1='hoge''fuga'
echo '$var1': $var1

var2=hoge
var3=$var1$var2
echo '$var3': $var3

# 以下は $var3 と同じ結果になるでしょうか?
echo "\$var1hoge": $var1hoge
echo "\$var1'hoge'": $var1'hoge'
echo "\${var1}hoge": ${var1}hoge
echo "\${var1}'hoge'": ${var1}'hoge'
terminal
$ ./test.sh
(見やすいように整形しています)
$var1:         hogefuga
$var3:         hogefugahoge
$var1hoge:                  # var1hoge という変数として扱われ、未定義なので何も表示されません
$var1'hoge':   hogefugahoge
${var1}hoge:   hogefugahoge 
${var1}'hoge': hogefugahoge

4. シェルスクリプトで引数を扱おう

(4-1) シェルスクリプトへの引数の渡し方

シェルスクリプト実行時に引数を渡すには、以下のように、スクリプトファイル名の後ろに複数の文字列をスペース区切りで列挙します。

terminal
$ ./test.sh arg1 arg2 arg3

(4-2) 位置パラメータ

  • 実行時に渡された引数は、位置パラメータという特殊パラメータで保持されます。位置パラメータは $1 ~ $9 で参照することができます(${1} ~ ${9} でも可能)。
  • 10番目以降の位置パラメータは ${10} のように {} をつける必要があります。
test.sh
#!/bin/bash

echo '$1': $1       # {} なくても OK
echo '${2}': ${2}   # {} があっても OK
echo '"$3"': "$3"   # もちろんダブルクォーテーションの中でも展開される
echo '${10}': ${10} # 10番目以降は {} が必要
echo '${11}': ${11} # 引数として指定されていない番号の位置パラメータには値が設定されない
echo '$10':$10      # $1 の部分が先に展開され、文字列の連結として処理される
terminal
$ ./test.sh arg1 2 3 4 5 6 7 8 9 10
(見やすいように整形しています)
$1:    arg1
${2}:  2
"$3":  3
${10}: 10
${11}: 
$10:   arg10

5. 特殊パラメータについて知ろう

位置パラメータも特殊パラメータの一種ですが、他にも様々な意味の特殊パラメータが存在します。

(5-1) $0

位置パラメータは $1 から始まっていましたが、では $0 は何を表すでしょうか?
確認してみましょう。

test.sh
#!/bin/bash

echo $0
echo $1
terminal
$ ./test.sh arg1
./test.sh
arg1

$0 は実行されたシェルスクリプトの名前を保持する特殊パラメータになります。

(5-2) $?

$? は直前に実行したコマンドの終了ステータスを保持します。

Linux におけるコマンドの終了ステータスについて

  • Linux におけるコマンドの終了ステータスの範囲は符号なしの8ビットの範囲である 0 ~ 255 に限られます。
  • 終了ステータス 0 のみが正常終了を表し、他の値は異常終了を表します。

シェルスクリプトの終了ステータスについて

  • シェルスクリプトでは exit 0exit 1 のように exit コマンドで終了ステータスを指定することができます。
  • シェルスクリプト内で exit コマンドの省略は可能ですが、その場合、シェルスクリプト内で最後に実行されたコマンドの終了ステータスがそのシェルスクリプトの終了ステータスとなります。

以下の入力された引数に応じて終了ステータスを返すシェルスクリプトで、 $? の値がどうなるか確認してみましょう。

test.sh
#!/bin/bash

echo '入力値' $1 # 第1引数を出力
exit $1 # exit コマンドで終了ステータスを指定
terminal
$ ./test.sh 0; echo '終了ステータス' $?
'入力値' 0
'終了ステータス' 0

$ ./test.sh 1; echo '終了ステータス' $?
'入力値' 1
'終了ステータス' 1

$ ./test.sh 255; echo '終了ステータス' $?
'入力値' 255
'終了ステータス' 255

$ ./test.sh 256; echo '終了ステータス' $?
'入力値' 256
'終了ステータス' 0

$ ./test.sh 257; echo '終了ステータス' $?
'入力値' 257
'終了ステータス' 1

$ ./test.sh -1; echo '終了ステータス' $?
'入力値' -1
'終了ステータス' 255

$ ./test.sh -2; echo '終了ステータス' $?
'入力値' -2
'終了ステータス' 254

(5-3) $#

$# はスクリプトに与えられた引数の数を保持します。

位置パラメータと $#shift コマンド
shift コマンドを実行すると、位置パラメータの内容が左に1つ移動します($2 の値が $1 に、 $3 の値が $2 に、 ... といった具合です)。また、同時に $# の値も 1 減少します。shift を繰り返し、 $# の値が 0 になった後にさらに shift を実行するとエラーになります。

test.sh
#!/bin/bash

echo -----------
echo 引数の数 $#
echo '$1'の値 $1
echo '$2'の値 $2
shift
echo $?
echo -----------
echo 引数の数 $#
echo '$1'の値 $1
echo '$2'の値 $2
shift
echo $?
echo -----------
echo 引数の数 $#
echo '$1'の値 $1
echo '$2'の値 $2
shift
echo $?
echo -----------
terminal
$ ./test.sh arg1 arg2
-----------
引数の数 2
$1の値 arg1
$2の値 arg2
0
-----------
引数の数 1
$1の値 arg2  # 元々 $1 に入っていた値は消えてしまうので注意
$2の値       # $2 の値が $1 に移動して、$2 は未定義の状態になる
0
-----------
引数の数 0
$1の値
$2の値
1           # $# の値が 0 になった後に shift したので失敗
-----------

(5-4) "$*", "$@"

"$*", "$@" は位置パラメータを集合的に扱うための特殊パラメータです。"$*" は全ての位置パラメータを結合して、1つの引数として処理するときに使用します。"$@" は全ての位置パラメータを個別のまま処理するときに使用します。
まだ紹介していない関数を使った例になりますが、"$*", "$@" の挙動を確認してみましょう。

test.sh
#!/bin/bash

function echo_args() {
  echo '引数の数' $#
  echo '第1引数' $1
  echo '第2引数' $2
  echo '第3引数' $3
  echo --------------------
}

echo '"$@"'の場合
echo_args "$@" # echo_args "$1" "$2" "$3" ... と同じ
echo '"$*"'の場合
echo_args "$*" # echo_args "$1 $2 $3 ..." と同じ
terminal
$ ./test.sh arg1 arg2 arg3
"$@"の場合
引数の数 3
第1引数 arg1
第2引数 arg2
第3引数 arg3
--------------------
"$*"の場合
引数の数 1
第1引数 arg1 arg2 arg3
第2引数
第3引数

$ ./test.sh "arg1 arg2" "arg3"
"$@"の場合
引数の数 2
第1引数 arg1 arg2
第2引数 arg3
第3引数
--------------------
"$*"の場合
引数の数 1
第1引数 arg1 arg2 arg3
第2引数
第3引数
--------------------

ダブルクォーテーションをつけていない $*$@ は想定と異なる動きをする危険があるので、詳しい挙動を知らない場合は使わない方が無難です。使用する際は "$*", "$@" を使いましょう。

$*$@ の場合の結果
test.sh
#!/bin/bash

function echo_args() {
  echo '引数の数' $#
  echo '第1引数' $1
  echo '第2引数' $2
  echo '第3引数' $3
  echo --------------------
}

echo '$@'の場合
echo_args $@ # 非推奨
echo '$*'の場合
echo_args $* # 非推奨
terminal
$ ./test.sh arg1 arg2 arg3
$@の場合
引数の数 3
第1引数 arg1
第2引数 arg2
第3引数 arg3
--------------------
$*の場合
引数の数 3
第1引数 arg1
第2引数 arg2
第3引数 arg3
--------------------

$ ./test.sh "arg1 arg2" "arg3"
$@の場合
引数の数 3
第1引数 arg1
第2引数 arg2
第3引数 arg3
--------------------
$*の場合
引数の数 3
第1引数 arg1
第2引数 arg2
第3引数 arg3
--------------------

(5-5) $$, $!

$$ は実行されたシェルスクリプトのプロセスIDを保持します。
$! は最後に実行したバックグラウンドプロセスのプロセスIDを保持します。

test.sh
#!/bin/bash

echo $$ # このスクリプトが実行されているプロセスIDを返す

(sleep 10; echo 'end') & # &(アンパサンド)によりバックグランドで実行され、10秒経つ前に次の行に進む
echo $! # 上のコマンドが実行されているバックグランドプロセスのプロセスIDを返す
wait $! # プロセスの終了を待つ。$! は最後に実行されたバックグランドプロセスのプロセスIDなので値は変わっていない。
terminal
$ ./test.sh
19664
19665
end

6. シェルスクリプトで配列を作ってみよう

シェルスクリプトでも他の言語同様に配列が使えます。

(6-1) 配列の複合代入

配列へ値を代入するときは、変数への代入文の右辺で () の中に要素を並べ、 変数名=(要素1 要素2 要素3 ...) のように書きます。普通の変数への代入と同じく、 = の前後にスペースを書いてはいけません。逆に、配列の要素間にはスペースが必要です。

(6-2) 配列の要素の参照

配列の要素を参照するときは ${変数名[インデックス]} のように書きます。例えば ${array[1]} のようになります。変数の参照は {} があってもなくても良かったのですが、配列の要素の参照の場合は {} が必要です。

test.sh
#!/bin/bash

# 複合代入
array=(item1 item2 'item3 item4' item5)

# 参照
echo '${array[0]}': ${array[0]}
echo '${array[1]}': ${array[1]}
echo '${array[2]}': ${array[2]}
echo '${array[3]}': ${array[3]}
terminal
$ ./test.sh
${array[0]}: item1
${array[1]}: item2
${array[2]}: item3 item4
${array[3]}: item5

(6-3) 配列の要素数の取得

配列の要素数は ${#変数名[@]} という構文で取得できます。未定義の要素はカウントされません。

test.sh
#!/bin/bash

array=(item1 item2 'item3 item4' item5)
echo ${#array[@]}
terminal
$ ./test.sh
4

(6-4) 配列へのインデックスを利用した代入

シェルスクリプトでは配列の要素は連続している必要はなく、途中に空きのある配列も作成できます。途中に空きのある配列を作成する場合は、複合代入の際に要素の部分で [インデック]=値 と書きます。また、既存の配列の要素を変更するには、 変数名[インデックス]=値 と書きます。未定義の配列に対して 変数名[インデックス]=値 を使うと、複合代入をしなくても空きのある配列が作成できます。

test.sh
#!/bin/bash

# 複合代入の際にインデックスを指定して値を入れることで、空きのある配列を作成可能
array=(item0 [2]=item2 [4]=item4)
echo ${#array[@]} # 3つしか値が入っていないので要素数は3となる
echo ${array[0]}
echo ${array[1]}
echo ${array[2]}
echo ${array[3]}
echo ${array[4]}
echo -----------------

# 既存の配列の要素を書き換える
array[1]=item1    # 未定義だったインデックスに値を代入する
array[2]=         # 既存のインデックスを空文字にする
echo ${#array[@]} # [1] に値が入ったので要素数は4となる
echo ${array[0]}
echo ${array[1]}
echo ${array[2]}
echo ${array[3]}
echo ${array[4]}
echo -----------------

# 未定義の配列にいきなりインデックス指定で要素を代入できる
new_array[3]=item3
echo ${#new_array[@]}    
echo ${new_array[0]}
echo ${new_array[1]}
echo ${new_array[2]}
echo ${new_array[3]}
echo ${new_array[4]}
echo -----------------
terminal
$ ./test.sh
3
item0

item2

item4
-----------------
4
item0
item1


item4
-----------------
1



item3

-----------------

(6-5) 配列の全ての要素の参照

引数と同じく @, * で全ての要素を参照可能ですが、想定通りの動作を保証するためにダブルクォーテーションで囲った "${変数名[@]}", "${変数名[*]}" を使うのが良いでしょう。

test.sh
#!/bin/bash

array=(item1 item2 'item3 item4' item5)

function echo_array_items() {
  echo $1
  echo $2
  echo $3
  echo $4
  echo $5
  echo -----------------------
}

echo Use '"${array[@]}"'
echo_array_items "${array[@]}" # echo_array_items "${array[0]}" "${array[1]}" ... と同じ

echo Use '"${array[*]}"'
echo_array_items "${array[*]}" # echo_array_items "${array[0]} ${array[1]} ..." と同じ

echo Use '${array[@]}'
echo_array_items ${array[@]}

echo Use '${array[*]}'
echo_array_items ${array[*]}
terminal
$ ./test.sh
Use "${array[@]}"
item1
item2
item3 item4
item5

-----------------------
Use "${array[*]}"
item1 item2 item3 item4 item5




-----------------------
Use ${array[@]}
item1
item2
item3
item4
item5
-----------------------
Use ${array[*]}
item1
item2
item3
item4
item5
-----------------------

(6-6) 配列への要素の追加

"${変数名[@]}" と組み合わせることで、配列に要素を追加できます。

test.sh
#!/bin/bash
array=(item1 item2 item3)
echo "${array[@]}"

# 先頭に追加
array2=(item_a item_b "${array[@]}")
echo "${array2[@]}"

# 末尾に追加
array3=("${array[@]}" item_c item_d)
echo "${array3[@]}"

# 自身の末尾に追加
array+=(item_e item_f) # array=("${array[@]}" item_e item_f) と同じ
echo "${array[@]}"
terminal
$ ./test.sh
item1 item2 item3
item_a item_b item1 item2 item3
item1 item2 item3 item_c item_d
item1 item2 item3 item_e item_f

(6-7) 値の存在するインデックスの取得

${!変数名[@]} で配列の中で値の存在するインデックスを取得できます。

test.sh
#!/bin/bash

array=(item0 [2]=item2 [4]=item4)
echo ${!array[@]}
terminal
$ ./test.sh
0 2 4

シェルスクリプトにおける連想配列について
シェルスクリプトでも他の言語で言うハッシュやマップのような機能である連想配列が使用可能です。ただし、 bash のバージョンが4未満の場合は連想配列を使用できません。

詳細

使用感はほぼ配列と同じなので、使い方は簡単にまとめておきます。

  • 宣言
    連想配列は他の変数や配列のように暗黙的な宣言ができません。代わりに declare -A で明示的に宣言する必要があります。先に declare -A 変数名 で変数を宣言しておいて、後から複合代入することも可能ですし、 declare -A 変数名=([キー1]=値1 [キー2]=値2 ...) のように宣言と代入を同時に行うことも可能です。
  • 参照
    参照の仕方は配列と似ていて、${変数名[キー名]} です。 {} は省略不可です。
  • 代入
    代入も配列と同様 変数名[キー名]=値 です。
  • 全ての値の参照
    全ての値の参照は配列と全く同じで "${array[@]}" です。ただし、代入順は維持されないことに注意してください。
  • キーの一覧の取得
    連想配列には配列のように空の要素というものはないため、 ${!変数名[@]} ではキーの一覧が取得できます。
test.sh
#!/bin/bash
# 宣言
declare -A price=([water]=100 [bread]=160 [vegetable]=200)

# 代入
price[beef]=360

# 参照
echo ${price[water]}
# キーの一覧
echo ${!price[@]}
# 全ての値の参照(キー順になっていることに注意)
echo "${price[@]}" 
# 要素数
echo ${#price[@]}
terminal
$ ./test.sh
100
160 360 100 200
bread beef water vegetable
4

7. シェルスクリプトで条件分岐をしてみよう

(7-1) if による条件分岐の書き方

if 文の一番単純な書き方は以下のようになります。 if の逆をとった文字である fi により if 文の終わる場所を明示する必要があります。普段あまり使わない文字列なので間違えないように気をつけましょう。

if 条件; then
    条件が真の場合の処理
fi

以下も可能です。

  • 条件が真のときに複数の処理を行う
  • elif 句により条件と処理を追加
  • else 句により条件が全て偽であった時の処理を追加
  • if 文自体を入れ子にする
if 条件1; then
  条件1が真の場合の処理1
  条件1が真の場合の処理2
elif 条件2; then
  条件2が真の場合の処理
elif 条件3; then
  条件3が真の場合の処理
  if 条件4; then
    条件3, 4が真の場合の処理
  fi
else
  上記全ての条件が偽である場合の処理
fi

if による条件分岐の書き方の詳細
if 条件 はコマンドとして扱われるので、if 条件 の後ろにはコマンドの終わりを表す記号が必要になります。(1-6) で解説したように、コマンドは ; または改行で区切ることができます。上記の例のように ; を付けて同じ行に then を続けて書くこともできますし、以下のように改行で区切って then を別の行に分けることもできます。

if 条件
then
    条件が真の場合の処理
fi

ただし、 ; を付けて同じ行に then を続けて書く書き方の方が一般的なので、そちらの書き方で書くことをオススメします。

(7-2) if 文の条件にはコマンドを使用する

一般的なプログラミング言語では if 文の条件として真偽値を返す式を書きますが、シェルスクリプトでは if 文の条件としてコマンドを書き、そのコマンドの終了ステータスによって条件分岐を制御します。終了ステータスが 0 であれば真、0 以外であれば偽として判定します。これは(5-2)で解説したように、Linuxのコマンドは終了ステータスとして、成功の場合は 0 を、失敗の場合は 0 以外を返すようになっているためです。

test.sh
#!/bin/bash

if grep -n test test.txt; then
  echo $?
  echo success
else
  echo $?
  echo fail
fi
terminal
# test.txt に test という文字列が含まれている場合
$ ./test.sh
1:test is writen # grep による出力
0                # grep コマンドの終了ステータス
success          

# test.txt に test という文字列が含まれていない場合
$ ./test.sh
1                # grep は対象ファイルに検索文字列が含まれていないと終了ステータスとしては失敗となる
fail 

(7-3) [ コマンドで if 文の条件を書いてみよう

if 文の条件として [ コマンドがよく使用されます。[ は単なる記号ではなく、コマンドとして機能し、別名の test コマンドと同じ動作をします。[test コマンドは引数に演算子を渡すと、文字列や数値の比較をしたり、ファイルの存在を判定してくれたりします。そして、判定の結果が真であれば 0、偽であれば 1 を終了ステータスとして返します。見た目のわかりやすさから、 test コマンドより [ コマンドの方がよく使用されます。

test.sh
#!/bin/bash

test "$1" = "test"
echo 'test コマンドの終了ステータス': $?

[ "$1" = "test" ]
echo '[ コマンドの終了ステータス':$?

if [ "$1" = "test" ]; then
  echo success
else
  echo fail
fi
terminal
$ ./test.sh test
test コマンドの終了ステータス: 0
[ コマンドの終了ステータス: 0
success

$ ./test.sh xxx
test コマンドの終了ステータス: 1
[ コマンドの終了ステータス: 1
fail

[ コマンドの書き方の注意点

  • [ をコマンドとして認識させるために、if との間、第1引数との間にそれぞれスペースを空ける。
  • 演算子と他の引数との間にもスペースを空ける。
  • 最後の引数として ] を渡す。
  • ] を単体の引数として認識させるために、 ] の前にもスペースを空ける。

(7-4) [ コマンドの演算子の紹介

[, test コマンドには条件判定のために様々な演算子が用意されています。以下で使用用途に分けて紹介していきます。

文字列の比較

演算子 判定結果が真になる条件 頻出度
str (演算子なし) str が空文字列でも未定義でもない ★☆☆
-n str str が空文字列でない(未定義の場合も真) ★★★
-z str str が空文字列である ★★★
str1 = str2 str1str2 が等しい ★★★
str1 == str2 str1str2 が等しい (= と同じ) ★☆☆
str1 != str2 str1str2 が等しくない ★★★
str1 < str2 str1str2 よりも辞書順で前にある ★★☆
str1 > str2 str1str2 よりも辞書順で後にある ★★☆

<, > は bash にとって別の意味(リダイレクト)の記号なので、 [ コマンドに引数として渡す際は文字列として渡すために '<', ">", \> のようにクォーティング(エスケープ)する必要があります。

整数の比較

演算子 意味 判定結果が真になる条件 頻出度
int1 -eq int2 equal int1int2 が等しい ★★★
int1 -ne int2 not equal int1int2 が等しくない ★★★
int1 -lt int2 less than int1int2 より小さい ★★★
int1 -le int2 less than or equal int1int2 以下 ★★★
int1 -gt int2 greater than int1int2 より大きい ★★★
int1 -ge int2 greater than or equal int1int2 以上 ★★★

ファイルに関する評価

ファイルに関する評価には多くの演算子が用意されていますが、ここではよく使われる演算子に絞って紹介します。

演算子 判定結果が真になる条件 頻出度
-d file file が存在し、ディレクトリである ★★★
-e file file が存在する(-a と同じ) ★★★
-f file file が存在し、通常のファイルである ★★★
-r file file が存在し、読み取り権限が与えられている ★★☆
-w file file が存在し、書き込み権限が与えられている ★★☆
-x file file が存在し、実行権限が与えられている ★★☆
その他のファイルに関する評価のための演算子
演算子 判定結果が真になる条件
-b file file が存在し、ブロック特殊ファイル1である
-c file file が存在し、キャラクター特殊ファイル1である
-g file file が存在し、SGID(特殊なアクセス権)2が設定されている
-h file file が存在し、シンボリックリンクである3(-L と同じ)
-k file file が存在し、スティッキービット4が設定されている
-p file file が存在し、名前付きパイプである
-s file file が存在し、ファイルサイズが 0 より大きい
-u file file が存在し、SUID(特殊なアクセス権)2が設定されている
-G file file が存在し、ファイルのグループが現在のユーザーのグループと一致する
-L file file が存在し、シンボリックリンクである3(-k と同じ)
-O file file が存在し、ファイルの所有者が現在のユーザーと一致する
-S file file が存在し、ソケットである
file1 -ef file2 file1file2 が同じ物理的ファイルを指している
file1 -nt file2 file1 の変更時刻が file2 より新しい
file1 -ot file2 file1 の変更時刻が file2 より古い
-t file descriptor 指定された番号のファイルディスクリプタが端末に接続されている

演算子の結合

以上の演算子は次の演算子によって条件を組み合わせることが可能です。

演算子 意味
expression1 -a expression2 expression1expression2 の両方の条件が真の場合に真(AND)
expression1 -o expression2 expression1expression2 のどちらかの条件が真の場合に真(OR)
(expression) 条件式をグループ化する際に用いる

() は bash にとって別の意味の記号なので、 [ コマンドに引数として渡す際は、文字列として渡すために [ \( "$str" = "test" \) ]のようにクォーティング(エスケープ)する必要があります。

演算子の結合は以下のように書きます。

test.sh
#!/bin/bash

if [ \( "$1" = "a"  -o "$2" = "b" \) -a -f test.txt ]; then
  echo '第1引数がaまたは第2引数がbで、かつtest.txtが存在しています!'
fi
terminal
$ touch test.txt
$ ./test.sh a c
第1引数がaまたは第2引数がbで、かつtest.txtが存在しています!

(7-5) より安全な [[ ]] を使って条件分岐してみよう

[, test コマンドと似た構文として [[ ]] があります。[[ ]] の中では、基本的には [ ] と同じ演算子が使えますが、以下のようにいくつかの違いがあります。[[ ]] の方がシェルスクリプト特有の制限が少なく、シンブルに記述できて便利なため、こちらを使うことをオススメします。

&&, || で条件式をよりシンプルに書ける
[[ ]] の中ではAND演算子、OR演算子として -a, -o の代わりに &&, || を使用できます。

test.sh
#!/bin/bash

if [[ ( "$1" = "a"  || "$2" = "b" ) && -f test.txt ]]; then
  echo '第1引数がaまたは第2引数がbで、かつtest.txtが存在しています!'
fi
terminal
$ tuoch test.txt
$ ./test.sh a c
第1引数がaまたは第2引数がbで、かつtest.txtが存在しています!

&&, || について
元々 &&, || は複数のコマンドをAND演算、 OR演算で連結するための演算子ですが、[[ ]] では専用の機構でこれらの記号を特別扱いしています。

詳細
  • &&
    コマンド1 && コマンド2 のように書くと、まずコマンド1を実行して、コマンド1が成功したら(コマンド1の終了ステータスが0だったら)コマンド2を続けて実行します。以下の例では mkdir test が権限の問題などで失敗した場合には、後ろの cd test コマンドは実行されません。
$ mkdir test && cd test
  • ||
    コマンド1 || コマンド2 のように書くと、まずコマンド1を実行して、コマンド1が失敗したら(コマンド1の終了ステータスが0以外だったら)コマンド2を続けて実行します。以下の例では cat test.txt がファイルが存在せずに失敗した場合のみ、後ろの touch test.txt コマンドでファイルを新規作成します。
$ cat test.txt || touch test.txt

単語分割されない

[ コマンドにおける単語分割

単語分割とは、後述の展開がコマンド内で行われた後、展開された文字列をスペース、タブ、改行によって複数の単語に区切る機能ことです。
[ コマンドでは単語分割が行われるため、スペースを含む文字列を値としてもつ変数や、空文字の変数を [ で比較するとエラーになります。

test.sh
#!/bin/bash

var=$1
if [ $var = 'hoge fuga' ]; then
  echo success
else
  echo fail
fi
terminal
$ ./test.sh 'hoge fuga'
./test.sh: line 4: [: too many arguments
fail

$ ./test.sh
./test.sh: line 4: [: =: unary operator expected
fail

[ ] の中でも "" で変数をクォートしていれば、その内側では単語分割は行われません。変数の間にスペースが含まれるかどうかは事前にわからないことが多いので、 [ ] の中では変数を常にクォートしておくことをオススメします。

test.sh
#!/bin/bash

var=$1
if [ "$var" = 'hoge fuga' ]; then # $var を "" でクォート
  echo success
else
  echo fail
fi
terminal
$ ./test.sh 'hoge fuga'
success

$ ./test.sh
fail

[[ ]] の内側では、変数を "" でクォートしているかどうかに関わらず、変数の値を1つの文字列とみなします。そのため、クォートしていない変数の値がスペースを含んでいたり、空文字であったりしてもエラーになりません。

test.sh
#!/bin/bash

var=$1
if [[ $var = 'hoge fuga' ]]; then # [[ ]] を使用
  echo success
else
  echo fail
fi
terminal
$ ./test.sh 'hoge fuga'
success

$ ./test.sh
fail

パターンマッチができる

[[ ]] 内で =, ==, != の右辺で * を用いると、パターン文字列とみなされます。

test.sh
#!/bin/bash

var=hoge-fuga
if [[ $var == hoge-* ]]; then # hoge-の後ろに任意の文字列というパターンに一致したら真
  echo success
else
  echo fail
fi
terminal
$ ./test.sh
success
* によるパターンマッチの詳細

[[ ]] 内の =, ==, != の右辺で * をそのままの文字として扱いたい場合は、右辺を "" でクォートします。

test.sh
#!/bin/bash

var=hoge-fuga
if [[ $var == "hoge-*" ]]; then # hoge-* という文字列と一致したら真
  echo success
else
  echo fail
fi
terminal
$ ./test.sh
fail

また、右辺に指定するパターンは文字列ではなく、変数で指定することも可能です。

test.sh
#!/bin/bash

var=hoge-fuga
pattern='hoge-*'
if [[ $var == $pattern ]]; then # hoge-の後ろに任意の文字列というパターンに一致したら真
  echo success
else
  echo fail
fi
terminal
$ ./test.sh
success

(7-7) case による条件分岐の書き方

case は1つの文字列に対して複数のパターンを上から順番に照合していき、最初にマッチしたパターンに応じて処理を実行するための構文です。if 文と同様に、 case の逆をとった esac という文字列で case 文の終わりを明示する必要があります。また、各パターンに対しての処理の後に ;; をつけるのを忘れがちなので注意しましょう。

case 文字列 in
  パターン1)
    処理1
    ;;
  パターン2)
    処理2
    ;;
  *)
    処理3
    ;;
esac

case 文には以下の特徴があります。

  • パターンにワイルドカード(*)を使用できる
  • 最後に *) というパターンを書くことで、それよりに前に書いた全てのパターンにマッチしなかったときの処理を書くことができる
  • | を使うことで、複数のパターンに対して同一の処理をさせることができる
test.sh
#!/bin/bash

file="$1"

case "$file" in
  *.csv)
    echo this is csv
    ;;
  special-* | important-*)
    echo this is special file
    ;;
  *)
    echo "Invalid file: $file"
    ;;
esac
terminal
$ ./test.sh abc.csv
this is csv

$ ./test.sh importand-paper.pdf
this is special file

$ ./test.sh other.txt
Invalid fiel: other.txt

8. シェルスクリプトでループ処理を書いてみよう

(8-1) for によるループ処理の書き方

for を使うことで、単語リストに対してループ処理を行うことができます。

for 変数 in 単語リスト; do
  繰り返す処理
done

単語リストに(5-4)で解説した "$@" を用いることで、シェルスクリプトに渡された全引数に対して繰り返し処理を行えます。

test.sh
#!/bin/bash

for arg in "$@"; do # for の後に続く変数名には $ をつけない
    echo $arg       # 参照するときは $ をつける
done
terminal
$ ./test.sh 'aaa bbb' ccc ddd
aaa bbb
ccc
ddd

また、(6-5)で解説した "${array[@]}" を用いることで、配列の全要素に対して繰り返し処理を行えます。

test.sh
#!/bin/bash

array=(aaa 'bbb ccc' ddd)
for element in "${array[@]}"; do
    echo $element
done
terminal
$ ./test.sh
aaa
bbb ccc
ddd

(8-2) while, until によるループ処理の書き方

while を使うことで、指定した条件が真である限り処理を繰り返すことができます。

while 条件; do
  繰り返す処理
done

条件部分には if 文と同様に、 [ コマンドや [[ ]] 構文も使えます。条件部分のコマンドを実行した結果終了ステータスが0であれば真とみなされ、処理が続行されます。条件部分の終了ステータスが0以外だと、偽とみなされ、その時点でループ処理は止まります。

test.sh
#!/bin/bash

while [[ $# -gt 0 ]]; do
  echo $1
  shift
done
terminal
$ ./test.sh aaa bbb ccc
aaa
bbb
ccc

untilwhile の逆で、指定した条件が偽である限り処理を繰り返します。

test.sh
#!/bin/bash

until [[ $# -eq 0 ]]; do
  echo $1
  shift
done
terminal
$ ./test.sh aaa bbb ccc
aaa
bbb
ccc

9. 関数を使って処理をまとめてみよう

(9-1) 関数の定義

シェルスクリプトが複雑になってきたら、ある程度のまとまりで処理を分けることで見通しをよくすることを考えましょう。その際に使えるのが関数です。関数は以下のように定義します。

function 関数名() 
{
  処理
}

1行目の function 関数名() の部分は () を抜いて function 関数名 と書くこともできます。また、function を抜いた 関数名() だけでも関数を定義できます。しかし、関数の定義とわかりやすいように上で紹介した function 関数名() を使うのをオススメします。

(9-2) 関数内のみで有効なローカル変数の定義

bash の変数は特に指定しない限りシェルスクリプト全体で有効なグローバル変数となります。これは変数を関数内で定義した場合も同様です。一般に、グローバル変数は有効範囲が広すぎて意図しない動作を招きやすいことから、関数内ではローカル変数を使うことが推奨されます。bash でローカル変数を定義するには変数名の前に local を付けます。

function test_func() {
  local var1
  var1=aaa

  local var2=bbb # 定義と同時に代入することも可能
}

ローカル変数の特徴

  • 関数外で同じ変数名のグローバル変数が定義されていても、関数内ではローカル変数の方の値が有効。ローカル変数の値を変更しても、関数外の同名のグローバル変数の値は変わらない。
  • 関数から関数を呼び出した場合は、呼び出し元関数のローカル変数は呼び出し先関数でも有効。ただし、呼び出し先関数で呼び出し元関数と同じ名前のローカル変数を使用している場合はそちらが優先される。
動作例
test.sh
#!/bin/bash

function echo_value1() 
{
  local var1=local1
  echo 'echo_value1:$var1:1:' $var1

  echo 'echo_value1:$var2:1:' $var2

}

function echo_value2() 
{
  local var1=local2
  echo 'echo_value2:$var1:1:' $var1
  
  local var2=local2
  echo 'echo_value2:$var2:1:' $var2

  echo_value1

  echo 'echo_value2:$var1:2:' $var1
  echo 'echo_value2:$var2:2:' $var2
}

var1=global
echo 'global:$var1:1:' $var1
echo 'global:$var2:1:' $var2

echo_value2

echo 'global:$var1:2:' $var1
echo 'global:$var2:2:' $var2
terminal
$ ./test.sh
global:$var1:1: global
global:$var2:1:
echo_value2:$var1:1: local2
echo_value2:$var2:1: local2
echo_value1:$var1:1: local1
echo_value1:$var2:1: local2
echo_value2:$var1:2: local2
echo_value2:$var2:2: local2
global:$var1:2: global
global:$var2:2:

(9-3) 関数で引数を扱う

コマンドやシェルスクリプトと同じように関数も引数を渡せます。引数として渡した値を関数内で参照するには、シェルスクリプトと同様に位置パラメータを使用します。また、位置パラメータの他に $#"$@" も使用できます。

test.sh
#!/bin/bash

function echo_self_args()
{
  echo $1
  echo "$2"
  echo ${3}
  echo "${4}"
  echo $#
  echo "$@"
}

echo_self_args arg1 arg2 'arg3 arg4' arg5
terminal
$ ./test.sh
arg1
arg2
arg3 arg4
arg5
4
arg1 arg2 arg3 arg4 arg5

(9-4) 関数名を取得する

(5-1)で解説した通り、実行しているシェルスクリプトの名前は $0 という特殊パラメータで取得できましたが、これは関数内かどうかに関わらず、常に実行しているシェルスクリプトの名前に展開されます。では、実行している関数の名前はどのように取得すればいいでしょうか?

実行している関数の名前はFUNCNAME 変数によって取得できます。 FUNCNAME は関数が呼び出されるたびに先頭にその関数名を追加していく配列です。そのため、 ${FUNCNAME[0]} は常に実行中の関数の名前になります。
ログ等でどの関数で処理を実行したかを表示しておきたい場合に便利です。

test.sh
#!/bin/bash

function echo_value1() 
{
  local var1=local1
  echo ${FUNCNAME[0]} ':$var1:1:' $var1

  echo ${FUNCNAME[0]} ':$var2:1:' $var1

}

function echo_value2() 
{
  local var1=local2
  echo ${FUNCNAME[0]} ':$var1:1:' $var1
  
  local var2=local2
  echo ${FUNCNAME[0]} ':$var2:1:' $var1

  echo_value1

  echo ${FUNCNAME[0]} ':$var1:2:' $var1
  echo ${FUNCNAME[0]} ':$var2:2:' $var1
}

var1=global
echo 'global :$var1:1:' $var1
echo 'global :$var2:1:' $var2

echo_value2

echo 'global :$var1:2:' $var1
echo 'global :$var2:2:' $var2
terminal
$ ./test.sh
global :$var1:1: global
global :$var2:1:
echo_value2 :$var1:1: local2
echo_value2 :$var2:1: local2
echo_value1 :$var1:1: local1
echo_value1 :$var2:1: local2
echo_value2 :$var1:2: local2
echo_value2 :$var2:2: local2
global :$var1:2: global
global :$var2:2:

(9-5) 関数の終了ステータス

(5-2)で解説した通り、シェルスクリプトの終了ステータスは exit コマンドで指定できましたが、関数の終了ステータスはどのように指定すればいいでしょうか?

関数の終了ステータスを明示的に指定したい場合は return コマンドを使用します。return の引数を省略すると、return の直前で実行した関数の終了ステータスを返します。また、return 自体を省略したときは関数内で最後に実行したコマンドの終了ステータスが関数の終了ステータスとなります。

test.sh
#!/bin/bash

function return_test()
{
  if [[ -z $1 ]]; then
    echo 'arg1 is empty.'
    return 1
  fi

  echo $1
}

return_test test
echo '終了ステータス': $?
echo ---
return_test
echo '終了ステータス': $?
terminal
$ ./test.sh
test
終了ステータス: 0
---
arg1 is empty.
終了ステータス: 1

10. リダイレクト・ヒアドキュメント・パイプを使いこなそう

標準入出力について
Linux では通常、標準入出力は以下に割り当てられています。

名前 割当先
標準入力(stdin) キーボード
標準出力(stdout) ターミナル画面
標準エラー出力(sterr) ターミナル画面
詳細

Linux のコマンドは標準入出力を使用して動作しています。具体的には標準入力ファイルから入力を受け取り、コマンドの実行結果を標準出力ファイルに出力しています。また、エラーが起きた際はエラーメッセージを標準エラー出力という標準出力とは別のファイルに出力しています。この、標準入力、標準出力、標準エラー出力の3つを合わせて標準入出力と言います。
ここでファイルと言っているのは仮想的なもので、Linux カーネルではディスク、キーボード、ターミナル画面などのハードウェアを抽象化して、ファイルとして統一的に扱えるようにしています。(これらハードウェアを仮想化したファイルは /dev 配下に存在しています。)

通常では標準出力も標準エラー出力も同じターミナル画面に割り当てられているため、シェルスクリプトで複数のコマンドを実行している途中でエラーが起きた際に、成功したコマンドの出力も、失敗したコマンドの出力も同じ画面上に出力されることになっています。

(10-1) 出力のリダイレクト機能を使ってみよう

コマンド実行時に標準入出力の入力元、出力先を置き換えるシェルの機能をリダイレクトと言います。標準出力の出力先をターミナル画面から置き換えるには > という記号を用います。

以下の例では、本来ターミナル画面に標準出力される Hello という文字列の出力先を hello.txt に変更しています。その結果、ターミナルから test.sh を実行しても何も表示されません。また、 cat コマンドで hello.txt の中身を確認すると、確かに echo コマンドの出力がされていることがわかります。

test.sh
echo 'Hello' > hello.txt
terminal
$ ./test.sh
# 何も出力されない
$ cat hello.txt
Hello

標準エラー出力のリダイレクト
標準エラー出力のリダイレクトは 2> で行います。

ファイルディスクリプタと出力リダイレクトの記法について

> は実は 1> の省略形になります。この 1 はファイルディスクリプタと呼ばれる番号です。ファイルディスクリプタはプロセスから開かれた全てのファイルに割り当てられており、標準入出力には次の番号が割り当てられています。

名前 ファイルディスクリプタの番号
標準入力 0
標準出力 1
標準エラー出力 2

リダイレクトの際、 n> ファイル という書き方でファイルディスクリプタn番の出力先を指定したファイルに置き換えています。n を省略すると 1 とみなされるので、 > ファイル1> ファイル と同義であり、標準出力の出力先が指定したファイルに置換されるのです。

1 の部分を 2に変えて 2> とすることで標準エラー出力のリダイレクトになります。

n> ファイル の記法についてですが、n と > の間にスペースを入れてn >とすることはできません。また、> と ファイル の間にスペースを入れないことも可能ですが、見やすさのためにスペースを入れることをオススメします。

>> による追記

  • > はリダイレクト先のファイルが存在する場合は元々のファイルの内容を上書きします。上書きではなく追記を行いたい場合は >> を使用します。
  • >>> もリダイレクト先のファイルが存在しない場合はファイルを新規に作成し、そこに出力します。
  • >>> と同様に記号の左側でファイルディスクリプタの番号を指定できます。

/dev/null
/dev/null は特殊なファイルで、読み込んでも何もデータを返さず、書き込んでも結果的にどこにも書き込まれることなくデータが消えていきます。そのため、出力のリダイレクトと組み合わせて、不要なエラー出力を破棄したり、サイズ0のファイルを作成したりするのに使われます。

./myscript.sh 2> /dev/null # 不要なエラー出力の破棄
cat /dev/null > empty.txt  # サイズ0のテキストファイルを作成

(10-2) 標準出力と標準エラー出力を同時にリダイレクトしてみよう

リダイレクトは1行で複数指定することが可能です。標準出力と標準エラー出力を別々のファイルにリダイレクトしたい場合は以下のように指定します。

terminal
$ ls /bin /error > bin.txt 2> error.txt

逆に、標準出力と標準エラー出力を同じファイルにリダイレクトしたい場合は以下のように &> で指定します。(上書きの場合は &>>

terminal
$ ls /bin /error &> result.txt
&> についての詳細

以下の2つは同じ処理を表しています。

$ ls /bin /error &> result.txt
$ ls /bin /error > result.txt 2>&1

n>&m はファイルディスクリプタn番をファイルディスクリプタm番のコピーにするという意味です。上の例では、まず、 > result.txt で標準出力の出力先が result.txt に変わり、続いて 2>&1 で標準エラー出力が標準出力のコピーになります。標準出力の出力先はすでに result.txt に置換されているため、標準エラー出力の出力先も result.txt になります。その結果、標準出力、標準エラー出力の両方が result.txt にリダイレクトされることになります。

逆に、 1>&2 とすると標準出力が標準エラー出力と同じ場所にされるようになります。

test.sh
#!/bin/bash

echo success   # こちらは標準出力に出力される
echo fail 1>&2 # こちらは標準エラー出力に出力される
terminal
$ ./test.sh
$ ./test.sh > result.txt
fail

n>&mn は省略すると 1 とみなされるので、 1>&2>&2 は同義です。

(10-3) 入力のリダイレクト機能を使ってみよう

通常の標準入力の動作イメージ

まずリダイレクトしない通常の標準入力がどのように動作するのかを cat コマンドを例に確認しましょう。cat コマンドを引数なしで実行すると、標準入力からの入力を受け付ける状態になり、対話的な操作が開始されます。試しに a と打って Enter を押すと、cat コマンドは標準入力から受け取った a という文字をそのまま標準出力に出力します。

terminal
$ cat
=> a
a
=> bb
bb
=> ccc
ccc
=> (Ctrl + D) # 入力の終了を表す

標準入力のリダイレクトは < で可能です。<0< の省略形で、入力のリダイレクト先として 0 の他にもファイルディスクリプタの番号を指定できます。

test.sh
#!/bin/bash

tr hoge fuga < hoge.txt
terminal
$ cat hoge.txt
hoge hoge
$ ./test.sh
fuga fuga

(10-4) ヒアドキュメントを使ってみよう

リダイレクトでは標準入力をファイルに置き換えていましたが、ファイルではなく直接シェルスクリプト内に入力文字列を書きたい場合はヒアドキュメントを使います。ヒアドキュメントの構文は次のようになります。

コマンド << EOF
ヒアドキュメントとして入力される内容
EOF

後述のコマンド置換との併用により、複数行の文字列をファイルを介さずに変数に代入することも可能です。

test.sh
#!/bin/bash

var1=value
text=$(cat << EOF
    arg1: $1       # 位置パラメータや変数なども展開される
    var1: $var1    # コメントも出力されてしまう
EOF
)
echo "$text"       # "" でクォートしないと出力時に改行されない
terminal
$ ./test.sh test
    arg1: test     # 位置パラメータや変数なども展開される
    var1: value    # コメントも出力されてしまう

EOF について

  • 書き方の注意点
    • 2つある EOF の部分は終了文字列と言い、 EOF 以外でもどんな文字列でも良いのですが、一般的には EOF, EOD, EOS, END などが使われます。ただし、ヒアドキュメントの内容の中で出てこない文字列を選ぶ必要がある点には注意してください。
    • 最後の EOF は1つの行に単独で書く必要があります。
    • 最後の EOF の前後にはスペースやタブなどの空白文字を入れることはできません。
  • << 'EOF'
    • ヒアドキュメントの中では後述する展開が行われます。ヒアドキュメント全体で展開をしないようにしたい場合は、 << の右に書く終了文字列を '' でクォートします。
  • <<-EOF
    • << の代わりに <<- と書くとヒアドキュメント内の行頭タブが無視されます。ヒアドキュメントをわかりやすいように書くためのインデントを無視できますが、スペースによるインデントは無視されないので注意が必要です。

ヒアストリング <<<
ヒアストリングはヒアドキュメントを1行にした書き方で、終了文字列は不要です。 <<< の右に書いた文字列では、ヒアドキュメントと同様。展開が行われます。

test.sh
#!/bin/bash

var1=value
text=$(cat <<< "var1: $var1") # <<< の右は1つの文字列でないといけない
echo $text
terminal
$ ./test.sh
var1: value

(10-5) パイプラインを使ってみよう

コマンドの出力はリダイレクトでファイルに出力する代わりに、別のコマンドの入力にすることも可能です。このような機能をパイプラインと呼び、 | という記号で書くことができます。パイプラインは前のコマンドの標準出力を後続のコマンドの標準入力に渡すものであり、後続のコマンドの引数として前のコマンドの出力をつかうことはできません。

$ ls src | grep '.sh' | head -n 10 # src ディレクトリ内の .sh ファイルを上から 10個表示

標準エラー出力も合わせて次のコマンドに渡す場合は |& または 2>&1 を使います。

$ ls src bin |& grep '.sh' | head -n 10
$ ls src bin 2>&1 | grep '.sh' | head -n 10

プロセス置換
コマンドの実行結果を別のコマンドの入力として使用するための機能としてプロセス置換があります。パイプラインでは1つのコマンドの結果しか渡すことができませんが、プロセス置換を使うことによって2つ以上の引数を指定する diff などのコマンドに、別のコマンドの実行結果を渡すことができます。

test.sh
#!/bin/bash

diff <(ls dir-a) <(ls dir-b)

これを知らないと同じことをするために中間ファイルとして一時ファイルを作成(+削除)する手間が発生してしまいます。

test.sh
#!/bin/bash

ls dir-a > tmp-a.txt
ls dir-b > tmp-b.txt
diff tmp-a.txt tmp-b.txt
rm tmp-a.txt tmp-b.txt

(10-6) グループコマンドを使ってみよう

{} で複数のコマンドを囲むことで、複数のコマンドの出力をまとめて1つのファイルにリダイレクトすることができます。以下は全て同義です。

test.sh
#!/bin/bash
# 通常の方法
echo first message > output.txt
echo second message >> output.txt
echo third message >> output.txt

# グループコマンドの使用
{
  echo first message
  echo second message
  echo third message
} > output.txt

# グループコマンドを1行で書く場合はコマンド同士を ; で区切る。最後のコマンドにも必要。
{ echo first message; echo second message; echo third message; } > output.txt

{} の代わりに () で囲むことでも同様のことが可能です。ただし、() で囲んだ時は中身のコマンドはサブシェルで実行されます。現在のシェルから子プロセスとして別のシェルが起動され、その中で () 内のコマンドが実行されます。これを利用することで、 () 内でカレントディレクトリを変更しても親のプロセスに影響を与えずに処理を続けたりすることができます。また、サブシェル外で定義された変数はサブシェル内でも使えますが、サブシェル内でその変数の値を変更しても、サブシェル外に影響は与えません。

test.sh
#!/bin/bash

cd /bin
pwd
var1=value1
echo $var1

(
  cd /home/myname
  pwd
  echo $var1
  var1=value2
  echo $var1
)

pwd
echo $var1
terminal
$ ./test.sh
/bin
value1
/home/myname
value1
value2
/bin
value1

11. オプションを使ってデバッグしやすくしよう

(11-1) -e オプションを使ってみよう

普通シェルスクリプトを実行した時は、途中のコマンドでエラーが起きても止まらず、最後まで処理を続けてしまいます。-e オプションを使うことで、シェルスクリプト内で何らかのエラーが発生した時点で、シェルスクリプトを終了してくれます。使い方は簡単で、シェルスクリプト内で set -e と書くと、その場所からオプションが有効になります。

test.sh
#!/bin/bash
set -e

ls /error         # ここでエラーが起きて、その時点で止まる
mkdir /error/dir1 # この処理は実行されない

(11-2) -u オプションを使ってみよう

-u オプションを使うことで未定義の変数を参照しようとした際にシェルスクリプトをエラー終了にしてくれます。使い方は -e オプションと同じで、シェルスクリプト内で set -u と書くと、その場所からオプションが有効になります。

test.sh
#!/bin/bash
set -u

# ここら辺で work_dir を定義していたと勘違い

rm -rf $work_dir/ # -u オプションがなければここで / 以下を全部消してしまうところだった
terminal
$ ./test.sh 
./test.sh: line 6: work_dir: unbound variable

(11-3) -x オプションを使ってみよう

-x オプションをつけることで、実行したコマンドをすべて標準エラー出力に出力してくれます。標準エラー出力なので、標準出力とリダイレクト先を分けておけば、シェルスクリプトの出力はターミナルに表示し、-x による実行コマンドのログはファイルに出力することもできます。また、-x による出力は展開後の文字列で行われます。

test.sh
#!/bin/bash
set -x

var1=value
echo $var1

var1=value2
echo $var1
terminal
$ ./test.sh 2> log.txt # -x の出力先は標準エラー出力なので、リダイレクトしておく
value
value2

$ cat log.txt
+ var1=value
+ echo value
+ var1=value2
+ echo value2

(11-4) -C オプションを使ってみよう

> で出力のリダイレクト先に既存のファイルを指定すると、元々の中身を上書きしてしまいますが、-C オプションを使うと上書きしようとした際にエラーにしてくれます。-C オプションをつけていても >> による追記は可能です。また、-C オプションを有効にしている際に、それでも上書きをしたい場合は、リダイレクトの記号を >| とすることで上書きが可能になります。

test.sh
#!/bin/bash
set -C

touch test1.txt 
echo "aaa" >| test1.txt # 上書きだけど >| なので OK
echo "bbb" >> test1.txt # 追記なので OK

touch test2.txt
echo "ccc" > test2.txt  # 上書きのため、ここでエラー
terminal
$ ./test.sh
./test.sh: line 9: test2.txt: cannot overwrite existing file
$ cat test1.txt
aaa
bbb

(11-5) -o pipefail オプションを使ってみよう

-e オプションでコマンドでのエラー発生時にシェルスクリプトが終了すると解説したのですが、パイプラインの途中でエラーが起きた時はキャッチしてくれません。これはパイプライン全体の終了ステータスが通常パイプラインの末尾のコマンドの終了ステータスになっているため、末尾のコマンドで成功すると途中のコマンドで失敗していてもエラー判定にならないからです。

test.sh
#!/bin/bash
false | true
echo $?
terminal
$ ./test.sh
0

-o pipefail オプションをつけるとパイプラインの途中でエラーが起きた時に、パイラプイン全体の終了ステータスをエラーの起きたコマンドの終了ステータスと同じにしてくれます。

test.sh
#!/bin/bash
set -o pipefail

false | true
echo $?
terminal
$ ./test.sh
1

これと -e を組み合わせることで、パイプラインの途中でエラーが起きた時点でシェルスクリプトを終了できるようになります。

test.sh
#!/bin/bash
set -e
set -o pipefail

false | true # ここでエラーが出てシェルスクリプトが終了する
echo $?      # よって、これは出力されない
terminal
$ ./test.sh

(11-6) 複数のオプションを同時に使ってみよう

複数のオプションを有効にする方法は以下の通りです。

# 1個ずつ set していく
set -e
set -u
set -x
set -o pipefail

# 1行で全て set する
set -e -u -x -o pipefail

# オプション同士を繋げて set する
set -euxo pipefail

shebang でオプションを指定する方法(非推奨)
ここまでオプションは set で有効化していましたが、実は以下のように shebang でも有効化できます。

test.sh
#!/bin/bash -u

echo $undefined
defined='defined'
echo $defined
terminal
$ ./test.sh
./test.sh: line 3: undefined: unbound variable

ただし、この指定方法のときに bash test.sh のような起動方法をすると shebang が無視されてオプションが効きません。オプションは set で指定することをオススメします。

terminal
$ bash test.sh

defined

一時的にオプションの効果を無効化する方法
オプションの前の文字を - ではなく + にすると、そのオプションを無効化することができます。

test.sh
#!/bin/bash
set -x
echo 'hoge'
set +x
echo 'fuga'
set -x
echo 'hogefuga'
terminal
$ ./test.sh
+ echo hoge
hoge
+ set +x
fuga
+ echo hogefuga
hogefuga

12. 便利なパラメータ展開を使ってみよう

(12-1) 展開とは

シェルは特殊文字を他の文字列に置換する「展開」という機能を持っています。この展開を使用することで、面倒な文字列操作を簡略化したり、コマンドや計算実行結果を変数で保持したりすることができます。bash では以下の展開機能を持っています。

  • パラメータ展開
  • パス名展開
  • ブレース展開
  • チルダ展開
  • コマンド置換
  • 算術式展開・評価
  • プロセス置換

この章ではパラメータ展開について解説します。

(12-2) パラメータ展開とは

変数の参照で変数名の前に $ をつけて $変数名 とすると、変数の値を参照できると解説しましたが、これも展開の一種です。$変数名 の部分が変数の値に置換されているのです。このような展開をパラメータ展開と呼びます。
${変数名} のように {} を付けてもパラメータ展開が働きますが、この {} の中で := などの特殊な記号を用いることで、変数の値を操作することもできます。このようなパラメータ展開を利用することで、複雑な文字列処理を短く簡単に書けたり、条件分岐を省略したりすることができます。

(12-3) ${parameter:-default}: デフォルト値への置換

参照した変数に値が入っていない(未定義またはた空文字)場合、 :- の後ろの文字列がデフォルト値として返されます。

動作例
test.sh
#!/bin/bash
echo ----------------------------------
echo first
var="value"
echo '$var': $var
echo '${var:-default}': ${var:-default} # 値が入っているときは置換されない

echo ----------------------------------
echo unset
unset var
echo '$var': $var
echo '$var:-default': $var:-default     # {} をつけないと適用されない
echo '${var:-default}': ${var:-default} # 値が入っていないので置換される
echo '$var': $var                       # 適用後も元の値は変わらない

echo ----------------------------------
echo empty
var=
echo '$var': $var
echo '${var:-default}': ${var:-default} # 空文字でも置換される
echo '$var': $var                       # 適用後も元の値は変わらない
terminal
$ ./test.sh
----------------------------------
first
$var: value
${var:-default}: value
----------------------------------
unset
$var:
$var:-default: :-default
${var:-default}: default
$var:
----------------------------------
empty
$var:
${var:-default}: default
$var:

こんなときに便利!
シェルスクリプトの引数が指定されていない時のデフォルト値の指定

test.sh
#!/bin/bash

file=${1:-default.csv} # 位置パラメータに対しても使用可能
echo $file
terminal
$ ./test.sh
default.csv

(12-4) ${parameter-default}: 未定義時のみデフォルト値への置換

: を抜いて - だけにすると、参照した変数が未定義の場合のみ- の後ろの文字列がデフォルト値として返されます。

動作例
test.sh
#!/bin/bash
echo ----------------------------------
echo first
var="value"
echo '$var': $var
echo '${var-default}': ${var-default}   # 値が入っているときは置換されない

echo ----------------------------------
echo unset
unset var
echo '$var': $var
echo '${var-default}': ${var-default}   # 値が入っていないので置換される
echo '$var': $var                       # 適用後も元の値は変わらない

echo ----------------------------------
echo empty
var=
echo '$var': $var
echo '${var-default}': ${var-default}   # 空文字では置換されない
echo '$var': $var                       # 空文字のまま
terminal
$ ./test.sh
----------------------------------
first
$var: value
${var-default}: value
----------------------------------
unset
$var:
${var-default}: default
$var:
----------------------------------
empty
$var:
${var-default}: 
$var:

(12-5) ${parameter:=default}: デフォルト値の代入

参照した変数に値が入っていない(未定義またはた空文字)場合、 := の後ろの文字列がデフォルト値として返され、さらに、変数にデフォルト値が代入されます

動作例
test.sh
#!/bin/bash
echo ----------------------------------
echo first
var="value"
echo '$var': $var
echo '${var:=default}': ${var:=default} # 値が入っているときは代入されない

echo ----------------------------------
echo unset
unset var
echo '$var': $var
echo '${var:=default}': ${var:=default} # 値が入っていないので代入される
echo '$var': $var                       # 適用後は元の値が変わっている

echo ----------------------------------
echo empty
var=""
echo '$var': $var
echo '${var:=default}': ${var:=default} # 空文字でも代入される
echo '$var': $var                       # 適用後は元の値が変わっている
terminal
$ ./test.sh
----------------------------------
first
$var: value
${var:=default}: value
----------------------------------
unset
$var:
${var:=default}: default
$var: default
----------------------------------
empty
$var:
${var:=default}: default
$var: default

こんなときに便利!

値が入っていない変数にのみ初期値を与えたい場合に以下のような条件分岐を発生させずに済みます

if [ -z ${parameter} ]; then
  parameter="default"
fi
# => ${parameter:=default} で代用可能

(12-6) ${parameter=default}: 未定義時のみデフォルト値への置換

: を抜いて = だけにすると、参照した変数が未定義の場合のみ= の後ろの文字列がデフォルト値として返され、さらに、変数にデフォルト値が代入されます。(動作例は省略)

(12-7) ${parameter:+default}: デフォルト値の使用

:-, := があれば :+ もあります。ただ、:-, := とは逆に、参照した変数に値が入っている(未定義またはた空文字でない)場合に、 :+ の後ろの文字列がデフォルト値として返され、さらに、変数にデフォルト値が代入されます。正直使い道はよくわかりません。

動作例
test.sh
#!/bin/bash
echo ----------------------------------
echo first
var="value"
echo '$var': $var
echo '${var:+default}': ${var:+default} # 値が入っているので代入される

echo ----------------------------------
echo unset
unset var
echo '$var': $var
echo '${var:+default}': ${var:+default} # 値が入っていないので代入されない
echo '$var': $var                       # 値が入っていないまま

echo ----------------------------------
echo empty
var=""
echo '$var': $var
echo '${var:+default}': ${var:+default} # 値が入っていないので代入されない
echo '$var': $var                       # 値が入っていないまま
terminal
$ ./test.sh
----------------------------------
first
$var: value
${var:+default}: default
----------------------------------
unset
$var:
${var:+default}: 
$var: 
----------------------------------
empty
$var:
${var:+default}: 
$var: 

(12-8) ${parameter+default}: 定義時にデフォルト値の使用

参照した変数が未定義でない場合に、 + の後ろの文字列がデフォルト値として返され、さらに、変数にデフォルト値が代入されます。こちらは値が空文字の場合でもデフォルト値が代入されます。(動作例は省略)

(12-9) ${parameter:?[message]}: 値の検査とエラー

参照した変数が未定義または空文字の場合に、message を表示し、 非対話実行されているシェルをエラー終了にします。message は省略することもでき、省略するとデフォルトのエラーメッセージが表示されます。

test.sh
#!/bin/bash

var="value"
echo ${var:?value is undefined}

var=""
echo ${var:?value is undefined} # 空文字なのでここでエラー終了する

unset var
echo ${var:?value is undefined}
terminal
$ ./test.sh
value
./test.sh: line 7: var: value is undefined

(12-10) ${parameter?[message]}: 値の検査とエラー

: を抜いて ? だけにすると、参照した変数が未定義の場合のみエラー終了にします。

test.sh
#!/bin/bash

var2="value2"
echo ${var2?}

var2=""
echo ${var2?} # 空文字なのでここではエラーにならない

unset var2
echo ${var2?} # 未定義なのでここでエラー終了する。messageが未指定の場合はデフォルトのメッセージが出力される。
terminal
$ ./test.sh
value2

./test.sh: line 10: var2: parameter null or not set

(12-11) ${#parameter}: 文字列長の取得

パラメータ展開だけで文字列長を取得できてしまいます。日本語でもOKです。

test.sh
#!/bin/bash

var="hoge"
echo ${var}の長さは${#var}です

var="墾田永年私財法"
echo ${var}の長さは${#var}です
terminal
$ ./test.sh
hogeの長さは4です
墾田永年私財法の長さは7です

(12-12) ${parameter:offset}, ${parameter:offset:length}: 部分抽出

${parameter:offset} では offset で指定した位置以降の文字列を抽出します。さらに :length もつけることで、 offset で指定した位置から length 文字分までを抽出してくれます。

test.sh
#!/bin/bash

var="墾田永年私財法"
echo ${var:4}
echo ${var:0:2}
terminal
$ ./test.sh
私財法
墾田

(12-13) ${parameter#word}: 前方一致除去(最短一致)

word で指定した文字列に最短で一致する特定の部分までを除去して抽出してくれます。

test.sh
#!/bin/bash

var="kon-den-ei-nen-shi-zai-hou"
echo ${var#kon-} 
echo ${var#kan-} # 前方一致しないので何も消えない
echo ${var#*-} # *(ワイルドカード)も使用可能
echo ${var#*den-}
terminal
$ ./test.sh
den-ei-nen-shi-zai-hou
kon-den-ei-nen-shi-zai-hou
den-ei-nen-shi-zai-hou
ei-nen-shi-zai-hou

(12-14) ${parameter##word}: 前方一致除去(最長一致)

# を2つにすると最長一致になります。

test.sh
#!/bin/bash

var="kon-den-ei-nen-shi-zai-hou"
echo ${var##kon-} 
echo ${var##*-} # 最短一致と結果が異なる
terminal
$ ./test.sh
den-ei-nen-shi-zai-hou
hou

こんなときに便利!
拡張子に使えます。

test.sh
#!/bin/bash

file="kon.den/ei.nen/shi-zai-hou.csv"
echo ${file##*.}
terminal
$ ./test.sh
csv

(12-15) ${parameter%word}: 後方一致除去(最短一致)

#% に変えると後方一致になります。

test.sh
#!/bin/bash

var="kon-den-ei-nen-shi-zai-hou"
echo ${var%-hou} 
echo ${var%-hau} # 後方一致しないので何も消えない
echo ${var%-*} # *(ワイルドカード)も使用可能
echo ${var%-zai*} 
terminal
$ ./test.sh
kon-den-ei-nen-shi-zai
kon-den-ei-nen-shi-zai-hou
kon-den-ei-nen-shi-zai
kon-den-ei-nen-shi

(12-16)${parameter%%word}: 後方一致除去(最長一致)

% を2つにすると最長一致になります。

test.sh
#!/bin/bash

var="kon-den-ei-nen-shi-zai-hou"
echo ${var%%-hou} 
echo ${var%%-*} # 最短一致と結果が異なる
terminal
$ ./test.sh
kon-den-ei-nen-shi-zai
kon

(12-17) ${parameter/word1/word2}, ${parameter//word1/word2}: 文字列置換

sed 等を使わずにパラメータ展開だけで文字列の置換もできます。${parameter/word1/word2} は最初に一致したもののみを置換し、${parameter//word1/word2} は一致したもの全てを置換します。#, % で置換パターンの制限もできます。

test.sh
#!/bin/bash

var="hogehoge"
echo ${var/hoge/fuga} # 最初だけ置換
echo ${var/geho/fuga} # 一致した部分を置換
echo ${var/#hoge/fuga} # 前方一致した部分を置換
echo ${var/#geho/fuga} # 前方一致しないから変化しない
echo ${var/%hoge/fuga} # 後方一致した部分を置換
echo ${var//hoge/fuga} # 全て置換
terminal
$ ./test.sh
fugahoge
hofugage
fugahoge
hogehoge
hogefuga
fugafuga

13. ブレース展開を使ってみよう

(13-1) {str1,str2,str3,・・・} を使ったブレース展開

{} の中にカンマ区切りで文字列を並べると、それぞれの要素が前後の文字列と結合されて展開されます。

$ echo hoge-{aaa,bbb,ccc}-fuga
hoge-aaa-fuga hoge-bbb-fuga hoge-ccc-fuga

これを使うことで、複数のファイルを同時に touch で作成したり、 cp で同じディレクトリに別名でファイルをコピーするのが簡単になります。

$ ls test/
hoge
$ cp test/{hoge,fuga}
$ ls test/
fuga hoge
$ cp test/hoge{,.txt}
$ ls test/
fuga hoge hoge.txt

(13-2) {start..end} を使ったブレース展開

{} の中で start と end の文字列の間にドットを2つ並べると、 start から end までの連続した文字や数字に展開されます。

$ ls
file0
$ touch file{1..5}
$ ls
file0 file1 file2 file3 file4 file5
$ touch file{d..g}
$ ls
file0 file1 file2 file3 file4 file5 filed filee filef fileg

この構文は for 文の単語リストにも使うことができます。

test.sh
#!/bin/bash

for i in {1..5}; do
  処理
done

14. パス名展開とチルダ展開を使ってみよう

(14-1) *, ? を使ったパス名展開

パス名展開では、*? などの記号をパス名やファイルの文字列に置き換えます。
* は任意の文字列にマッチして展開されます。

terminal
$ ls -a                               # .txt も表示するために -a オプション
. .. .txt 0.txt 123.txt hoge.txt fuga.txt
$ ls *.txt                            # パス名展開では隠しファイルの . にはマッチしない
0.txt 123.txt hoge.txt fuga.txt
$ ls .*                               # . にマッチさせるには明示的に . を書く必要がある
. .. .txt
$ ls *.sh                             # パス名展開した結果1個もマッチしない場合はそのままの文字列で実行される
ls: *.sh: No such file or directory

パス名展開ではディレクトリをまたいだ展開はできません

terminal
$ ls test/texts/*.txt
0.txt 123.txt hoge.txt fuga.txt
$ ls test/*.txt       
ls: test/*.txt: No such file or directory

? は任意の1文字に展開されます。

terminal
$ ls -a                               
. .. .txt 0.txt 123.txt hoge.txt fuga.txt
$ ls ?.txt                            
0.txt
$ ls ????.txt                         # ? を連続で使用することも可能
hoge.txt fuga.txt

(14-2) [ ], [! ], [^ ] を使ったパス展開

[ ] の中に1つ以上の文字を書くと、その中のいずれか1文字にマッチします。

$ ls
aaa.txt aab.txt aac.txt aad.txt aae.txt aaf.txt
$ ls aa[abcd].txt # ls aaa.txt aab.txt aac.txt aad.txt に展開される
aaa.txt aab.txt aac.txt aad.txt

[! ], [^ ] を用いると、 [ ] の中に含まれていない1文字にマッチします。

$ ls
aaa.txt aab.txt aac.txt aad.txt aae.txt aaf.txt
$ ls aa[!abcd].txt 
aae.txt aaf.txt

パス名展開の記号は複数組み合わせて使うことも可能です。

$ ls -a                               
. .. .txt 0.txt 123.txt hoge.txt fuga.txt
$ ls [0-9]*.txt
0.txt 123.txt

(14-3) ~ を使ったチルダ展開

チルダ展開では ~ がユーザーのホームディレクトリに展開されます。

$ (cd ~/bin; pwd) # (cd /home/user1/bin; pwd) に展開される。サブシェルなのでディレクトリは移動しない。
/home/user1/bin

15. コマンド置換を使ってみよう

(15-1) $( ) を使ったコマンド置換

コマンド置換ではコマンド実行による出力をそのまま文字列に展開します。コマンド置換は $(コマンド) のように書きます。$( ) の中に書いたコマンドが実行され、標準出力に出力された文字列が展開されます。

test.sh
#!/bin/bash

touch $(date +%Y-%m-%d).txt
terminal
$ ls

$ ./test.sh
$ ls
2023-12-01.txt

コマンド置換は `コマンド` でも可能ですが、こちらは入れ子にするのが難しく、見た目上もわかりづらいため、基本的には $(コマンド) を使用するのをオススメします。

test.sh
#!/bin/bash

var=$(echo $(cat <<< $(pwd)))
echo "${var}"

var2=`echo \`cat <<< \\\`pwd\\\`\``
echo "${var2}"
terminal
$ ./test.sh
/home/myname/work
/home/myname/work

16. シェルスクリプトで計算をしてみよう

(16-1) $(( )), (( )) を使った算術式の計算と評価

シェルスクリプトでの計算には $(( 算術式 )), 数値比較等の評価には (( 算術式 )) を使いましょう。計算だけしたいのに (( 算術式 )) を使うと予期せぬエラーになり得ます。また、 letexpr などの算術計算のためのコマンドもありますが、基本的に使用しません。

test.sh
#!/bin/bash

x=$((100+200))
echo 終了ステータス: $?
echo x: $x

# (( )) は評価した結果が真(0 以外)であれば終了ステータスが成功(0)、偽(0)であれば失敗(1)となる
((x > 1000))
echo 終了ステータス: $? # 偽なので 1
echo x: $x

((x < 1000))
echo 終了ステータス: $? # 真なので 0
echo x: $x

x=0
((x++)) # インクリメント前の値 0 として評価され、偽となるので、終了ステータスは 1
echo 終了ステータス: $?
echo x: $x

x=0
x=$((x + 1))
echo 終了ステータス: $? # 計算は成功するので終了ステータスは 0
echo x: $x
terminal
$ ./test.sh
終了ステータス: 0
x: 300
終了ステータス: 1
x: 300
終了ステータス: 0
x: 300
終了ステータス: 1
x: 1
終了ステータス: 0
x: 1

17. 可読性を上げるためにコーディング規約を守ろう

シェルスクリプトは自由度が高く、簡単に書き始めることができますが、自由度が高い分チーム内で品質を合わせづらいです。可読性の低い書き方になってしまうと、バグの原因になってしまうこともあるため、一定の規約を設けて読みやすいシェルスクリプトを書くための仕組みを作るのがいいでしょう。細かい規約はチームごとに変える必要はあると思いますが、大まかな指針を以下で解説します。

以下の指針は基本的に Google のスタイルガイドを参考にしています。

(17-1) ファイルの冒頭にはこれを書こう

shebang を書こう

特に sh を使うわけではないなら 1 行目に #!/bin/bash を明示しておきましょう。

test.sh
#!/bin/bash

オプションを書こう

オプションは shebang で定義することも可能ですが、スクリプトファイルを直接指定する以外の方法で起動される場合もあるので 2行目に set で定義しておきましょう。とりあえず -euxCo pipefail を指定しておけばいいと思います。オプションが不要になったら都度 + で無効化しましょう。

test.sh
#!/bin/bash
set -euxCo pipefail

set +x
echo "ここはデバッグ不要"
set -x

処理内容が作業ディレクトリに依存しないようにディレクトリを移動しよう

シェルスクリプトはスクリプトを起動したディレクトリをカレントディレクトリとして動作します。そのため、シェルスクリプトファイルからの相対パスで別のファイルを参照している場合などは、作業ディレクトリからシェルスクリプトファイルを起動すると想定通りに動かないことがあります。

NG例
test.sh
#!/bin/bash
set -euxCo pipefail
cat hello.txt
terminal
$ ls
test.sh hello.txt
$ cat hello.txt
hello
$ ./test.sh
+ cat hello.txt
hello
$ cd ..
$ ./work/test.sh
+ cat hello.txt
cat: hello.txt: No such file or directory

これを回避するために、次のようにシェルスクリプトの冒頭でスクリプトがあるディレクトリに移動しておくといいです。

test.sh
#!/bin/bash
set -euxCo pipefail
cd "$(dirname "$0")"
terminal
$ ls
test.sh hello.txt
$ cat hello.txt
hello
$ ./test.sh
+ cat hello.txt
hello
$ cd ..
$ ./work/test.sh
++ dirname ./work/test.sh
+ cd ./work
+ cat hello.txt
hello

ただし、この方法を用いる場合は、シェルスクリプト内の全てのパスをスクリプトの場所からの相対パスで書くか、絶対パスで書く必要があります。作業ディレクトリに依存したシェルスクリプトは書かないようにしましょう。

usage 関数で処理内容および使い方をスクリプト内に記載しよう

何をするスクリプトなのか、どのような引数を指定すればいいのかなどを usage 関数にまとめておきましょう。

test.sh
#!/bin/bash
set -euxCo pipefail
cd "$(dirname "$0")"

function usage() {
cat <<EOF >&2 # ヒアドキュメントを使う際はリダイレクトをここに書きます
Description:
  Description of this script.
  
Usage:
  $0 [OPTIONS] <FILE>

Options:
  --version, -v   print "$(basename "$0")" version
  --help, -h      print this
EOF 
exit 1
}

getopts コマンドでオプション解析を行おう

getopts という組み込みコマンドと while, case 文を組み合わせることで、オプションの解析が簡単になります。先ほどの usage 関数に合うようにオプション解析を実装しましょう。

getopts コマンドの使い方

getopts コマンドは第1引数で渡されたオプション候補とシェルスクリプト実行時に渡された引数を順に照らし合わせていき、マッチ結果を第2引数で渡された変数に格納します。第2引数で渡す変数名は何でも構いません。

第1引数のオプション候補については以下のような特徴があります。

  • オプション候補とできるのは引数で - の後に続く英字1文字に限ります(大文字小文字どちらも可能)。--help のようなロングオプションを候補とすることはできません。
  • 先頭の : はどの候補にもマッチしない引数が渡された時にエラーメッセージを表示しないようにするための記号です。これを使う場合は case 文の最後に *) でどの候補にもマッチしなかった場合の処理を独自に設定しておく必要があります。
  • オプションの英字の後に : をつけると、そのオプションは引数を取ることを意味します。例えば a: とした場合は -a というオプションは引数を取り、シェルスクリプト実行時に -a aaa のように指定することになります。オプションの引数は OPTARG という特殊変数に格納されます。これは他の変数名で代替することはできません。
test.sh
#!/bin/bash
set -euxCo pipefail
cd "$(dirname "$0")"

function usage() {
(省略)
}

while getopts :a:bc:hH OPT; do
  case $OPT in
    a)
      echo "[-a] が指定されました(引数: ${OPTARG})"
      ;;
    b)
      echo "[-b] が指定されました"
      ;;
    c)
      echo "[-c] が指定されました(引数: ${OPTARG})"
      ;;
    h | H)
      usage
      ;;
    *)
      echo "不正なオプションが指定されました: ${OPT}"
      ;;
  esac
done

(5-3)で紹介した $#, shift, 位置パラメータを組み合わせることでもオプション解析はできますが、 getopts コマンドを使う方が簡単に解析できると思います。ただし、ロングオプションを使う場合は $#, shift, 位置パラメータを組み合わせたオプション解析を行うのがいいでしょう。

getopts を使えばシェルスクリプトでオプション解析を実装するのが少しは簡単になるとは言え、豊富なオプションを受け付けるのは多くの場合労力がかかります。オプション解析を実装する代わりに、環境変数でオプション値を受け渡したが方がシンプルで可読性も高くなる場合があります。

test.sh
#!/bin/bash
set -euxCo pipefail
cd "$(dirname "$0")"

option_a=${OPTION_A}
option_b=${OPTION_B}
terminal
$ OPTION_A="hoge" ./test.sh

(17-2) スペース、改行を使って見やすくしよう

インデントは半角スペース2つにしよう

シェルスクリプトではコマンド同士をパイプで繋いだりする関係で1行が長くなりがちです。なるべく1行に収めるためにインデントは半角スペース2つにしておくといいでしょう。

パイプの前後には半角スペースを1つずつ入れよう

パイプ (|) の前後には半角スペースがあった方が見やすいです。

command1 | command2 # 見やすい
command1| command2  # 見にくい
command1 |command2  # 見にくい
command1|commnad2   # 見にくい

リダイレクトの前には半角スペースを1つ入れ、後ろにはスペースを入れないようにしよう

> 前の数字との間に半角スペースを入れるのはそもそも文法的な間違いです。> には見やすいようにスペースを入れましょう。

$ echo aaa 1> sample.txt  # 見やすい
$ cat sample.txt
aaa
$ echo aaa 1 > sample.txt # 文法的な間違い
$ cat sample.txt 
aaa 1
$ echo aaa 1>sample.txt   # 見づらい
$ cat sample.txt
aaa

1行で表示しきれない場合は \ で改行しよう

1行が長くなってきた場合は、適宜 \ で改行しましょう。

test.sh
#!/bin/bash

echo 'この行は非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に'\
'長いです'
terminal
$ ./test.sh
この行は非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に長いです

(17-3) 変数、展開の書き方を守ろう

複数単語の場合は _ で結合しよう

文法的な決まりではないですが、変数名はいわゆるスネークケースという形式で書きましょう。

sample_variable="sample variable"

変数宣言の右辺が文字列の場合は "" で囲おう

後の変更で予期せぬエラーを生まないためにも先に文字列は "" で囲んでおきましょう。

sample_variable="sample variable" # OK
sample_variable=sample variable   # NG

定数は大文字、定数以外は小文字で書こう

定数は、定数だということがわかりやすいように大文字で書きましょう。スネークケースなのは通常の変数名と同じです。

CONST_VARIABLE="const variable"

定数は readonly で宣言しよう

定数は予期せず値を変更されないように readonly 付きで宣言して、読み込み専用にしておきましょう。

readonly CONST_VARIABLE="const variable"

ファイル名、ディレクトリ名を格納する変数は定数にしよう

ファイル名、ディレクトリ名は一度定義してしまえばスクリプト内で変えることは滅多にないはずなので、定数にしておきましょう。定数にするということは(上記の規約的に言えば)変数名を大文字のスネークケースにして、readonly 付きで宣言するということです。

readonly FILE_NAME="/home/myname/work/sample.txt"

パラメータ展開をする際は ${} で囲おう

定義済みの変数をパラメータ展開で参照する場合は $変数名 ではなく、 ${変数名} を使うようにしましょう。${変数名} の方がパラメータ展開を使っていることがわかりやすく、また変数名と文字列が連結することによる不意なエラーを防ぐことができます。

var="hoge"
echo "varの値は${var}です。" 
echo "${var}fuga"

ただし、$0, $1, $@, $? 等の特殊パラメータはそのままでもわかりやすいので {} をつけなくてもいいでしょう。

未定義または空文字になる可能性がある場合はデフォルト値で埋めるかエラーメッセージを表示する

オプションを環境変数で指定するようにしている場合や、コマンド置換を使って変数にコマンドの結果を格納しようとしている場合は、変数の値が空文字になる場合があります。そのような場合に備えてパラメータ展開時にデフォルト値を埋めるか、エラーメッセージを指定しましょう。

cat_result="$(cat ./sample.txt)"
echo ${cat_result:-default text}

open_api_key=${OPEN_API_KEY:?Specify OPEN_API_KEY} 

コマンド置換は $() で囲おう

コマンド置換は $( )` ` で可能ですが、 $( ) の方が可読性が高く、ネストもしやすいので $( ) の方を使いましょう。

cat_result="$(cat ./sample.txt)"
cat_result=`cat ./sample.txt`

パラメータ展開している箇所は "" で囲おう

パラメータ展開の結果がスペースや改行を含んでいる場合に備えて "" で囲んでおくと安全です。

test.sh
#!/bin/bash

cat_result=$(cat sample.txt)
echo ${cat_result}   # "" で囲まないと結果をそのまま扱えない場合がある
echo "${cat_result}"
terminal
$ cat sample.txt
hoge
fuga
$ ./test.sh
hoge fuga
hoge
fuga

(17-4) 関数の書き方を守ろう

function を書こう

関数定義の際に function() を省略する記法もありますが、可読性のためにはどちらもつけておくのが良いでしょう。

function some_func() {
  echo "do something"
  return 0
}

ローカル変数の先頭には _ を付けよう

関数内でしか使わない変数は積極的にローカル変数とし、ローカル変数とする場合は参照時にローカル変数だとわかりやすいように変数名の先頭に _ をつけましょう。

function some_func() {
  local _print_str="do something"
  echo "${_print_str}"
  return 0
}

関数名は _ で区切ろう

変数名と同じく小文字のスネークケースで書きましょう。

function some_func() {
  echo "do something"
  return 0
}

必ず return しよう

終了ステータスは正常終了時は 0、異常終了時は 0 以外になるようにしましょう。

function some_func() {
  if [[ "$1" == "test" ]]; then
    echo "success"
    return 0
  else
    echo "fail" >&2
    return 1
  fi
}

(17-5) 制御文をきれいに書こう

if と then は同一行に書き、; の後ろは半角スペースを1つつけよう

if 文は以下のように書きましょう。

if 条件; then
  処理
fi

; を使わずに then の前で改行する書き方もありますが上記のように書くのが多いように思います。どちらで書いても構わないのですが、少なくともチーム内では書き方を統一しましょう。

if 条件
then
  処理
fi

if 文を省略せずに書こう

次のように if 文を使わなくても条件分岐を書くことができますが、わかりづらいので if 文を書くようにしましょう。

[[ "$var" -eq 0 ]] && echo "true" || echo "false"

if や while 文の条件は [ ] ではなく [[ ]] を使おう

[[ ]] では単語分割されないため安全に変数扱うことができ、パターンマッチなど機能も豊富なので、[ ] ではなく [[ ]] を使うようにしましょう。

if [[ $var === "test" ]]; then
  echo "This is sample."
fi

for, while と do は同一行に書き、; の後ろは半角スペースを1つつけよう

if 文は以下のように書きましょう。

for 変数 in 単語リスト; do
  処理
done

while 条件; do
  処理
done

; を使わずに do の前で改行する書き方もありますが、 if と合わせてこちらを推奨しています。どちらで書いても構わないのですが、少なくともチーム内では書き方を統一しましょう。

for 変数 in 単語リスト
do
  処理
done

while 条件
do 
  処理
done

(17-6) リンター、フォーマッターを使おう

VSCode にシェルスクリプト用のリンター、フォーマッターの拡張機能が存在するので使っておくと良いでしょう。

ShellCheck (リンター)

shell-format (フォーマッター)

18. 最後に便利なシェルスクリプトで便利なコマンドを紹介!

(18-1) source コマンドで他のシェルスクリプトから関数や変数を読み込もう

(1-4)で紹介したようにシェルスクリプトの実行方法には以下の3つがあります。

  1. 実行権限をつけたファイル名を指定して実行する
  2. bashsh コマンドの引数と指定して実行する
  3. source または . コマンドの引数として指定して実行する

このうち 1. と 2. の方法はシェルスクリプトの実行を命令した現在のプロセス(カレントシェル)から新しく子プロセスが作られ、その子プロセス上でシェルスクリプトが実行されます。シェルスクリプトが終了すると、子プロセス自体も終了し、呼び出し側のカレントシェルに子プロセスの状態は引き継がれません。

逆に、3. の source または . コマンドを使用する方法では、シェルスクリプトがカレントシェルで実行されます。そのため、シェルスクリプト実行後の状態(変数や関数の定義など)がカレントシェルでも引き継がれます。

ターミナルから source または . コマンドでシェルスクリプトを実行することはほぼありませんが、シェルスクリプト内で別のシェルスクリプトに書かれた設定や関数を使いたい場合は、source または . コマンドでシェルスクリプトを実行します。

another.sh
#!/bin/bash
function another_func() {
  echo "another func"
}
test.sh
#!/bin/bash
set -euCo pipefail
cd "$(dirname "$0")"

source ./another.sh

another_func
terminal
$ ./test.sh
another func

(18-2) trap コマンドでシェルスクリプトが終了・中断した時の動作を実装しよう

trap コマンドを使うことでシェルスクリプトが終了したり、中断した場合に必ず動く処理を定義しておくことができます。これを使うことで、中断時に作成途中のファイルを削除したり、終了時に終了時に退避していたファイルを元に戻したりすることができます。

trap コマンドは第1引数に実行する処理を文字列で渡します。また、第2引数以降でどのシグナルに対して処理を実行するのかを選択します。シグナルにはシグナル名とシグナル番号が割り振られており、どちらで指定しても構いません。例えば、 Ctrl-C を押してプログラムを中断する際には (SIG)INT というシグナルが送られます。この (SIG)INT に対するシグナル番号は 2 となっています。また、シグナル番号 0 はシェルスクリプト終了時に、プロセスが自分自身に対して創出する EXIT シグナルを表します。

test.sh
#!/bin/bash

data=
SAVE_TMP_FILE="~/tmpdata.txt"

function save_tmp_data() {
  echo "$data" > "$SAVE_TMP_FILE"
  echo "save data to $SAVE_TMP_FILE"
}

trap "
  echo 'receive INT!'
  save_tmp_data         # 関数も実行できる
  exit 1                # exit しておかないと INT を受け取っても処理を続行してしまうので注意
" INT                   # INT は 2 でも可

for i in {1..100}; do
  echo "$i"
  data=$((data + i))
done

echo $data
terminal
$ ./test.sh
1
2
3
^Creceive INT!
save data to ~/tmpdata.txt
$ cat ~/tmpdata.txt
6

(18-3) mktemp コマンドで一時的に必要なファイル/ディレクトリを作ろう

mktemp コマンドを使うと適当な名前のファイルが作成されます。 -d オプションをつけるとファイルではなくディレクトリが作成されます。これと上で紹介した trap コマンドを組み合わせることで、シェルスクリプトの処理中だけ存在するファイル、ディレクトリが使えます。

test.sh
#!/bin/bash

temp_file="$(mktemp)"
temp_dir="$(mktemp -d)"

trap "
  echo delete $temp_file
  rm -f $temp_file
  
  echo delete $temp_dir
  rm -rf $temp_dir
" 0 # 実行終了時に必ず実行される(INTを受け取った場合でも)

echo "aaaa" > $temp_file

data="$(cat $temp_file)"
echo $data

terminal
$ ./test.sh
aaaa
delete /var/folders/c0/b120wc4j3fn8nc055bjzcyt40000gn/T/tmp.QhkRN9Bg
delete /var/folders/c0/b120wc4j3fn8nc055bjzcyt40000gn/T/tmp.AjB0It3r

参考

本記事の執筆にあたって、以下の書籍を参考にさせていただきました。非常にわかりやすい書籍なのでシェルスクリプトに興味を持ったらぜひ読んでみることをオススメします!

最後に

弊社Nucoでは、他にも様々なお役立ち記事を公開しています。よかったら、Organizationのページも覗いてみてください。
また、Nucoでは一緒に働く仲間も募集しています!興味をお持ちいただける方は、こちらまで。

  1. ブロック特殊ファイルやキャラクタ特殊ファイルについては以下などを参照してください。
    https://qiita.com/angel_p_57/items/1faafa275525469788b4 2

  2. SGID, SUID については以下などを参照してください。
    https://eng-entrance.com/linux-permission-sgid 2

  3. スティッキービットについては以下などを参照してください。
    https://eng-entrance.com/linux-permission-stickybit

1901
2277
12

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1901
2277

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?