寒月記

住みにくいところをどれほどか寛容て

ShellScript Tips --関数--

 ShellScript の基本についての記事を, 備忘を兼ねて最近まとめています。

今回の記事では, 関数 について書きます。
なお, これまで明記してませんでしたが, 基本的には bash を前提 として書いています。

ShellScript の関数

 ShellScript にも関数はあります。
ただし, 一般的なプログラミング言語の関数とは勝手が異なるので, その注意点を中心にメモしておきます。

基本: 関数定義と実行

 まず関数を定義します。
def などは不要で, 関数名(){コマンド} の形で書いていきます (正確に言うと {} である必要はなく, sh や bash の仕様で定義されている複合コマンドであれば何でもよいです, おまけで少し説明します)。
このあたりは他の言語と似ています。

#!/bin/bash

func_countDown(){
    for i in {5..1}
    do
        echo "${i}"
    done
}

ちなみに {5..1} は bash の ブレース展開*1 という機能を使っています*2{1..5} だと 1 から 5, {a..z} だと a から z などの連続した値を順に展開してくれる便利機能です。
ここでは脱線するので, 興味のある方は Man Bash(1) の Brace Expansion などをご確認ください。

さて, この関数を実行するには, 初めに source コマンドや . コマンド*3を使って 現在実行中のシェルで関数定義を読み込む 必要があります。
./ファイル名bash ファイル名 のように実行すると, 現在のシェルと異なるプロセスで実行されるため, 現在のシェルには読み込んだ変数や関数が反映されないので, 実行できません。

実行時は, 他の多くの言語とは異なり () は不要で, 関数名 (と必要であれば引数) だけ指定すればよい です。
失敗例から見てみましょう。

[kangetsu@ubuntu16 shell_tips Sat Apr 04 17:07:37]
$ bash func1.sh
[kangetsu@ubuntu16 shell_tips Sat Apr 04 17:07:59]
$ func_countDown
func_countDown: command not found
[kangetsu@ubuntu16 shell_tips Sat Apr 04 17:08:06]

現在のシェルに関数を読み込めていないので, command not found になっています。
次は成功例です。

[kangetsu@ubuntu16 shell_tips Sat Apr 04 17:08:06]
$ source func1.sh
[kangetsu@ubuntu16 shell_tips Sat Apr 04 17:08:50]
$ func_countDown
5
4
3
2
1
[kangetsu@ubuntu16 shell_tips Sat Apr 04 17:08:52]

source で現在のシェルで関数定義を実行したことで, ちゃんと定義を読み込めており実行できています。

引数

 上のサンプルは非常にシンプルなパターンでした。
今度は, 引数を受け取る関数を見てみます。

条件分岐について の記事のサンプルで実はすでに紹介していますが, ShellScript の関数で引数を利用したいときは, 特殊変数 $#$n を使います。

  • $#: 引数の数を格納する変数
  • $n: n番目の引数の値を格納する変数
#!/bin/bash

func_countDown(){

    if [ $# -gt 1 ]
    then
        echo "too much arguments!"
        return 1
    else
        if [ $# -eq 1 ]
        then
            STR="$1"
        fi
    fi

    for i in {5..1}
    do
        echo "${i}"
    done

    if [ -n "${STR}" ]
    then
        echo "${STR}"
    fi
}

. で読み込んで, 引数の数を変えて実行してみます。
ちゃんと引数の数や内容に応じて処理が行われていることが分かります。

[kangetsu@ubuntu16 shell_tips Sat Apr 04 17:10:03]
$ . func2.sh
[kangetsu@ubuntu16 shell_tips Sat Apr 04 17:32:25]
$ func_countDown
5
4
3
2
1
[kangetsu@ubuntu16 shell_tips Sat Apr 04 17:32:35]
$ func_countDown hoge fuga
too much arguments!
[kangetsu@ubuntu16 shell_tips Sat Apr 04 17:32:46]
$ func_countDown hoge
5
4
3
2
1
hoge
[kangetsu@ubuntu16 shell_tips Sat Apr 04 17:32:49]

変数のスコープ:local によるローカル変数宣言

 ところで, 上のサンプルでは関数 func_countDown 内で 変数 STR を宣言しています。
他のプログラミング言語であれば, 大抵の場合これはローカル変数となり, スコープ外になれば参照できなくなります。
shell ではどうなるか, 先ほど関数を実行したものと同じシェルで STR を参照してみます。

[kangetsu@ubuntu16 shell_tips Sat Apr 04 17:48:50]
$ echo $STR
hoge
[kangetsu@ubuntu16 shell_tips Sat Apr 04 18:06:26]

参照できてしまいました。
このように, shell では基本的に変数はすべてグローバル変数 になります。
スコープを関数内に限定したい場合, bash のビルトインコマンド local を使って変数を宣言する必要があります。

#!/bin/bash

func_countDown(){

    if [ $# -gt 1 ]
    then
        echo "too much arguments!"
        return 1
    else
        if [ $# -eq 1 ]
        then
            local STR="$1"  # ここで local を使ってローカル変数として宣言
        fi
    fi

    for i in {5..1}
    do
        echo "${i}"
    done

    if [ -n "${STR}" ]
    then
        echo "${STR}"
    fi
}

先ほどの実行結果の影響を受けないよう新しい bash プロセスを立ち上げてから実行して確認してみます。

[kangetsu@ubuntu16 shell_tips Sat Apr 04 18:47:44]
$ bash
[kangetsu@ubuntu16 shell_tips Sat Apr 04 18:47:50]
$ echo $STR

[kangetsu@ubuntu16 shell_tips Sat Apr 04 18:47:53]
$ . func4.sh
[kangetsu@ubuntu16 shell_tips Sat Apr 04 18:47:59]
$ func_countDown fuga
5
4
3
2
1
fuga
[kangetsu@ubuntu16 shell_tips Sat Apr 04 18:48:10]
$ echo $STR

[kangetsu@ubuntu16 shell_tips Sat Apr 04 18:48:14]

このように, local を使うことでローカル変数を宣言 できます。

戻り値

 最後は戻り値についてです。
関数で処理をした結果を呼び出し元で受け取ってあれこれしたいことは多々あります。
これまでのサンプルでも何度か出ているように, bash には return というビルトインコマンドがあります。
他の言語から連想して, return に返したい値を渡せばよいのでは? と通常思うと思います。

では, 関数の最後で "SUCCESS!" という文字列を返すことを試してみます。

#!/bin/bash

func_countDown(){

    for i in {5..1}
    do
        echo "${i}"
    done

    return "SUCCESS!"
}
[kangetsu@ubuntu16 shell_tips Sat Apr 04 20:18:32]
$ . func5.sh
[kangetsu@ubuntu16 shell_tips Sat Apr 04 20:18:42]
$ func_countDown
5
4
3
2
1
-bash: return: SUCCESS!: numeric argument required
[kangetsu@ubuntu16 shell_tips Sat Apr 04 20:18:49]

エラーとなりました。
エラーメッセージにあるように, bash の return は数値のみを返せます。
Man bash(1) に仕様があるので見てみます。

return [n]
        Causes a function to stop executing and return the value specified by n to its caller.  If n is omitted, the return status is that  of
        the  last  command  executed  in  the  function body.  If return is used outside a function, but during execution of a script by the .
        (source) command, it causes the shell to stop executing that script and return either n or the exit status of the  last  command  exe‐
        cuted  within  the  script as the exit status of the script.  If n is supplied, the return value is its least significant 8 bits.  The
        return status is non-zero if return is supplied a non-numeric argument, or is used outside a function and not during  execution  of  a
        script by . or source.  Any command associated with the RETURN trap is executed before execution resumes after the function or script.
  • 関数の実行を止める (多くの他の言語と同様)
  • n を渡していれば n を, 省略していれば直前のコマンドの exit code を返す

というところが基本で必要な点かと思います。
このように, bash の return は exit code を返す目的で使われ, 数値以外を返すことはできません。

では, 他の言語のように数値以外を受け取りたい場合はどうすればよいか。
実は, 例えば関数の実行結果を変数に格納などすると, その関数の標準出力が格納されます

例を見るのが分かりやすいと思うので, 試してみます。

[kangetsu@ubuntu16 shell_tips Sat Apr 04 20:59:59]
$ RET=$(func_countDown)
-bash: return: SUCCESS!: numeric argument required
[kangetsu@ubuntu16 shell_tips Sat Apr 04 21:00:06]
$ echo $RET
5 4 3 2 1
[kangetsu@ubuntu16 shell_tips Sat Apr 04 21:00:09]

$() でコマンド (今回は関数) を実行し, その結果を変数 RET に代入しています。
中身は, func_countDown を実行した際の標準出力と同じ, 5 4 3 2 1 となりました。
return の誤用を警告するエラーメッセージが格納されていないのは, これが標準出力でなく標準エラー出力だからです。

つまり, 関数実行時に任意の文字列などを受け取りたい場合, return に渡すのではなく, 標準出力に渡してやればよいことになります。
先ほどのスクリプトを改修して, SUCCESS! という文字列を受け取れるようにしてみましょう。

#!/bin/bash

func_countDown(){

    for i in {5..1}
    do
        echo "${i}"
    done

    echo "SUCCESS!"
}
[kangetsu@ubuntu16 shell_tips Sat Apr 04 21:05:25]
$ . func6.sh
[kangetsu@ubuntu16 shell_tips Sat Apr 04 21:05:28]
$ RET=$(func_countDown)
[kangetsu@ubuntu16 shell_tips Sat Apr 04 21:05:29]
$ echo $RET
5 4 3 2 1 SUCCESS!
[kangetsu@ubuntu16 shell_tips Sat Apr 04 21:05:32]

確かに, echo で文字列を標準出力に渡すことで, 変数 RET に格納できました。
しかし, 不要な 5 4 3 2 1 まで一緒になっています。 これは, 5 4 3 2 1 も標準出力に出力されているためです。

これを回避し, "SUCCESS!" という文字列だけ受け取るには, 受け取りたくないもの以外は標準出力以外の場所に渡す必要があります。
最も分かりやすいのは次の例でしょう。

#!/bin/bash

func_countDown(){

    for i in {5..1}
    do
        echo "${i}" > /dev/null
    done

    echo "SUCCESS!"
}
[kangetsu@ubuntu16 shell_tips Sat Apr 04 21:05:32]
$ . func7.sh
[kangetsu@ubuntu16 shell_tips Sat Apr 04 21:08:17]
$ RET=$(func_countDown)
[kangetsu@ubuntu16 shell_tips Sat Apr 04 21:08:21]
$ echo $RET
SUCCESS!
[kangetsu@ubuntu16 shell_tips Sat Apr 04 21:08:24]

5 4 3 2 1 を出力する部分で, 標準出力でなく /dev/null に捨てることでほしいものだけに絞ることができました。
ただし, このままでは for ループの結果がどうなったのかどこにも出力されないので, 実際にこうした処理をする場合はちゃんと影響を考慮して, /dev/null でないところに渡すなど, 工夫が必要かと思います。

まとめ

 今回のまとめです。
ShellScript の関数は他の言語と似ている部分もありますが, 同じように扱おうとすると違いに悩まされます。
以下の点を把握しておけば, 最低限 ShellScript の関数を利用することはできるかと思います

ShellScript における関数基本
  1. 他のファイルに定義した関数の呼び出し時は, source か . コマンドでそのファイルを読み込むと呼び出せるようになる
  2. 呼び出し時には関数名と引数だけでよく, () は不要
  3. bash の関数内の変数は全てグローバル変数
  4. ローカル変数を使いたいときは変数宣言時に local を使う
  5. bash 関数の戻り値は関数内の標準出力になる
  6. return は exit code を返すためのものなので注意

おまけ:bash における関数定義の仕様

Man bash(1) には, bash における関数定義が書いてあります。
「基本: 関数定義と実行」の部分で少し触れましたが, これを読むと, 関数定義時の () の後は必ずしも {} でなく, compound-command であればよいということが書かれています。

Shell Function Definitions
    A  shell  function is an object that is called like a simple command and executes a compound command with a new set of positional parameters.
    Shell functions are declared as follows:

    name () compound-command [redirection]
    function name [()] compound-command [redirection]
            This defines a function named name.  The reserved word function is optional.  If the function reserved word is supplied, the parenthe‐
            ses  are  optional.  The body of the function is the compound command compound-command (see Compound Commands above).  That command is
            usually a list of commands between { and }, but may be any command listed under Compound Commands above.  compound-command is executed
            whenever  name is specified as the name of a simple command.  When in posix mode, name may not be the name of one of the POSIX special
            builtins.  Any redirections (see REDIRECTION below) specified when a function is defined are performed when the function is  executed.
            The  exit  status  of  a  function  definition  is zero unless a syntax error occurs or a readonly function with the same name already
            exists.  When executed, the exit status of a function is the exit status of the last command executed in  the  body.   (See  FUNCTIONS
            below.)

つまり, 次のような記述でも問題ありません。

#!/bin/bash

func_countDown2() (echo "hoge")
[kangetsu@ubuntu16 shell_tips Sat Apr 04 21:18:29]
$ . func3.sh
[kangetsu@ubuntu16 shell_tips Sat Apr 04 21:18:31]
$ func_countDown2
hoge
[kangetsu@ubuntu16 shell_tips Sat Apr 04 21:18:32]

とは言え積極的に他の記述法を使うこともないとは思うので, おまけでした。

新しいLinuxの教科書

新しいLinuxの教科書

新しいシェルプログラミングの教科書

新しいシェルプログラミングの教科書

*1:brace は英語で大括弧の意

*2:つまり brace 展開は sh では使えません

*3:source と . は同じコマンドです。編集したばかりの .bashrc などをすぐに反映するのに使われます