ShellScript
Bash
Linux
Ubuntu
WSL

shell scriptの基本を学ぶ 意外と知らない関数の定義

shell scriptの中で, 見落とされている機能の一つが関数だと思います.

通常のプログラミング言語のように, 関数を定義することができます.

むしろ, 普通のプログラミング言語よりも簡便に定義し, 利用できます.

今日は, bashの関数について学んだことを紹介しようと思います.

環境

この記事では, 以下の環境を使っています.

OSのバージョン

Windows Subsystem for Linux, 通称WSLで, Microsoft storeで, インストールしたubuntuを乗せて, 動かしている.

仮想環境ではないので, ゲストホストとかは, ないですが, 便宜上ホストゲストを使いました.

ホスト側:
PC: surface Pro4
OS: Microsoft Windows 10 Pro

ゲスト側
OS: Ubuntu 16.04.4 LTS

bashのバージョン

GNU bash, version 4.3.48(1)-release (x86_64-pc-linux-gnu)
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

関数の定義

関数の定義の仕方は, 簡単で,

func()
{

}

のように書くだけです.

funcには, 自分の好きな名前をつけて, 空の()をつけてください.

()の中には, どんな引数を取るかなどは, 書きません.

そして, {}の間に, 好きな処理を書きます.

そして, bashの場合は, returnexitを使って, 関数にある値を返すことができます.

逆に, 何も返さないような, ただの手続きだけ書いてある関数も定義できます.

それでは, 早速, 具体例を考えいきましょう.

ユーザーを追加する関数add_a_userを定義し, それを呼び出すscriptです.

func
#!/bin/bash
add_a_user() 
{
    if [ -z "$1" -o -z "$2" ]; then
        echo "usage: add_user user password comments"
    else 
        USER=$1
        PASSWORD=$2
        shift; shift;
        COMMENTS=$@;
        echo "adding user $USER"
        echo useradd -c $COMMENTS $USER
        echo passwd $USER $PASSWORD
        echo "added user $USER ($COMMENTS) with pass $PASSWORD"
    fi
}

# to be executed from here
echo "start of script"
add_a_user Konankun ran This is my home
echo "end of script"

bashは, いままで, 前から一文ずつ, 実行していく形でした.

関数の場合は, 定義した部分では, 読み込まれるだけで実行されません.

呼び出されたときにはじめて, 実行されます.

さて, 関数add_a_userの定義を見てみましょう.

おなじみの特殊変数の$1$2$@などが出ています.

よくセットで使われるshiftも出ています.

関数で引数を取る場合は, 呼び出す際に,

add_a_user 引数1 引数2・・・のようにします.

()などはいらず, スペース区切りで, 引数を連ねます.

そして, その引数の順番ごとに, $1, $2, $3, ...に入っていきます.

また$@は, 引数すべて配列として入っています.

まずifで引数に値が入っているか, 判定しています.

$1$2に値が入っていなければ, usageを表示するようにしています.

ここらへんは, shell scriptの基本を学ぶ 縁の下の力持ち testコマンドとifの使い方で, 解説しています.

次に,

USER=$1
PASSWORD=$2

USERPASSWORDにそれぞれ1個目の引数と2個目の引数を代入しています.

これは, 変数名をつけることによって, 意味が明確になり, 可読性(readability)があがります.

また, あとで, shiftするために, 変数を退避させるといった2つの意味があります.

次のshiftは, どんな意味があるかというと,

perlのshiftと全く一緒です.

$@には, すべての引数が配列として入っています.

その配列の一番目を, 取り除きます.

2回, shiftされているので, $@には, 3個目からの引数が入っています.

そして, それをすべてCOMMENTSに代入しています.

次に, userを加えるコマンドと, パスワードを設定するコマンドが実行されます.

echo useradd -c $COMMENTS $USER
echo passwd $USER $PASSWORD

ここで, echoが先頭についています.

これは, デバッグする際に約立つテクニックで, 実際に, 実行されるコマンドを, そのまま標準出力に出してくれます(なので, 実際にコマンドは実行されません).

useraddは,root権限でしか実行できませんし, 実際に実行されてしまうと, いらないユーザーが追加されてしまうので, echoをつけて, 実行されるであろうコマンドを出力から確認するだけです.

shell scriptのデバッグ方法なので覚えておくと役に立つかもしれません.

関数の定義が終わって, そのしたに,

echo "start of script"
add_a_user Konankun ran This is my home
echo "end of script"

があります.

実際に実行されるのは, この3行だけです.

add_a_user Konankun ran This is my homeが関数の呼び出しになります.

結果は, 以下のようになります.

start of script
adding user Konankun 
useradd -c This is my home Konankun 
passwd Konankun ran
added user Konankun (This is my home) with pass ran
end of script

定義した関数を呼び出すには, 今のように, 同一のscript内に書くか,

関数定義したファイルを別で保存して, 外部化する.

そして, 呼び出す側で, sourceコマンドを用いて, その定義を反映させる.

といった2種類の方法があります.

後者のほうをもうちょっと説明しましょう.

関数の外部化

さきほどのadd_a_user関数を外部化しましょう.

libというファイル名で, それを保存しましょう.

lib
add_a_user() 
{
    if [ -z "$1" -o -z "$2" ]; then
        echo "usage: add_user user password comments"
    else 
        USER=$1
        PASSWORD=$2
        shift; shift;
        COMMENTS=$@;
        echo "adding user $USER"
        echo useradd -c $COMMENTS $USER
        echo passwd $USER $PASSWORD
        echo "added user $USER ($COMMENTS) with pass $PASSWORD"
    fi
}

同じディレクトリに, 呼び出すためのファイルを作ります.

func2
#!/bin/bash

# import lib
source ./lib
echo "start of script"
add_a_user Konankun ran This is my home
echo "end of script"

出力結果を見てみましょう.

おなじになっているはずです.

このように, 外部化しても関数を呼び出すことができます.

変数のスコープ

shell scriptには, スコープがありません.

関数内で定義しようが, グローバル変数となります.

逆に, 関数外で定義しようが, 関数内で呼び出し, 書き換えることができます.

func3
some_func()
{
echo "inside some_func: " "$x"
x=2
}

x=1
echo "outside some_func: " "$x"
some_func
echo "after changin x, outside some_func " "$x"

実行すると,

outside some_func:  1
inside some_func:  1
after changin x, outside some_func  2

関数内外かかわらず, 読み書きできています.

しかし, $1などの引数が代入される特殊変数だけ特別です.

そのshell scriptの引数と, 関数の引数は, 別です.

shell scriptの引数を関数内で, 呼び出したい場合は, 関数を呼び出すときに, $1などの特殊変数を渡す必要があります.

言葉ではわかりにくいので, 例を見てみましょう.

func4
#!/bin/bash
some_func()
{
echo $1
}

echo $1
some_func

上のshell scriptを, 実行してみましょう.

$ ./func3 first_arg
first_arg

第一引数は, first_argなので, $1には, first_argが代入され, echo $1で出力されます.

関数some_funcが呼び出されて, echo $1が実行されます.

しかし, 関数内の$1は, 関数が呼び出されたときの第一引数を意味するので, 引数を取っていないので, 何も表示されません.

もし, shell scriptの引数を, 関数の引数として, 与えたかったら, 以下のように変更すればいいでしょう.

func5
#!/bin/bash
some_func()
{
echo $1
}
ARGS=$@
echo $1
some_func $ARGS

そして, 最後にかなりconfusingな例を一つ.

パイプで関数を呼び出した場合です.

この場合は, 関数は, sub shellで関数を呼び出すので, 関数内で書き換えた場合は, 影響を受けません(関数を呼び出す側で, 定義した変数は参照できる).

func6
#!/bin/bash
some_func()
{
echo "before changin x inside func: $x"
x=2
echo "after changin x inside func: $x"
}

x=1
echo "outside func before calling some_func: $x"
some_func | tee log.txt
echo "outside func after calling some_func: $x"

結果は,

outside func before calling some_func: 1
before changin x inside func: 1
after changin x inside func: 2
outside func after calling some_func: 1

関数の結果がパイプで, 次のコマンドに渡される場合は, sub shellで実行されます.

よって, 関数内には, 関数外で, 定義されたxは受け継がれますが, xが2に書き換えられても,

親のshellでは, 受け継がれないです.

次に, プログラミング言語と同様に再帰的に関数を呼び出すことができます.

再帰的呼び出し

他のプログラミング言語と同じように, 再帰的に呼び出すことができます.

再帰呼び出しでお馴染みの階乗を求める関数を見てみましょう.

factorial
#!/bin/bash
factorial()
{
if [ "$1" -gt "1" ]; then
    i=`expr $1 - 1`
    j=`factorial $i`
    k=`expr $1 \* $j`
    echo $k
else
    echo 1
fi
}

一つだけ, 掛け算は\*で, \をつけないとsyntax errorになるので気をつけてください.

あとは, 2や3をいれたときどうなるか, 確認するといい練習にコードリーディングの練習になると思います.

戻り値

returnを使って, 関数の戻り値を指定できます.

exitの場合は, プログラムを終了させますが, returnの場合は, 終了しません.

詳しくは, Shell Scripting Tutorial
Exit Codes
を読むといいでしょう.

参考文献

Shell Scripting Tutorial
Functions