rcmdnk's blog
Last update

20180129_shellscript_200_200

シェルスクリプトで関数を定義する時、functionと付けても付けなくても 定義出来たりしますが、 その辺のしっかりとした定義について。

function foo or foo ()

Bashなどのシェルスクリプトで関数を定義するには

function foo {
  ...
}

の様にfunctionを前に付けて定義するか

foo () {
  ...
}

の様にfunctionを使わずに後ろに()を付けて定義するのが通常です。

今まで作ってるスクリプト(Bash)では、grepとかで探しやすい、ということもあって 前者の方をよく使っていました。

この辺の違いについて Stack Exchangeの回答の中に分かりやすくまとまってるものがありました。

bash - difference between “function foo() {}” and “foo() {}” - Unix & Linux Stack Exchange

6つの関数の形についてPOSIXサポートなものなのか、もしくはどのシェルにサポートされてるものなのか などが書かれています。

foo () any-command

この形はBorne Shellで使われてたものですがBashではサポートされてません。 Zshではサポートされていて

$ foo () echo hello
$ foo
hello

と出来ます。この際、()の両側のスペースはあってもなくても定義できます。

Bashでは

$ foo() echo hello
bash: syntax error near unexpected token `echo'`

と定義しようとするとエラーが出ます。

普段Bashを使うので知らなかったですが Zshの様に使えるシェルもある様です。 が、1コマンドなので関数にする必要がある場面が限られそう。

foo () any-compound-command

any-compound-commandとは{ ...; }()、またfor文やif文でコマンドリストを囲った形を 指します1

最初に書いた例の後者がこれにあたります({ ...; })。

この形はPOSIXでサポートされた形でシェルスクリプトを書くならこれを使っておけばOKなもの。

{}で囲う場合、Bashではワンライナーで書こうとする時は最初の{の後にスペースが必要になります。 (Zshだと要らない。)

for文とかを使えるのは知りませんでした。Bashでも

$ foo () for i in 1 2 3;do echo $i;done
$ foo
1
2
3
$ foo () if [ 1 = 1 ];then echo hello;fi
$ foo
hello
$

みたいな事が出来ます。

function foo { …; }

最初に書いた例。 これはKorn shell (Ksh)で導入されたものでBashでも取り入れられていて、 これも多くのシェルでサポートされています。

ただしPOSIX準拠ではありません。

function foo () { …; }

前の2つの合わせ技的なもの。 なんとなくフルにこんな風に書いておく方が良い感じもしないでもないのですが、 この形は使うべきではないと言われています。

現在のBashやZshではこの形でもエラー無く使えますが supported by accidentだそうで 将来的に使えなくなる可能性もありfuncitonを使わずfoo ()の形を使った方が良い、とのこと2

ただ、可読性を考えた時に 一般的な言語の関数の形を考えるとこの形に近いものが多いので この形がきちんとサポートされているのであれば使うのもありかも。

function foo () other-compound-command

{}以外の()などのcompound commandに関してはやはり現状BashやZshでは使えますが、 前の形が使えるKshでも使えないので、まず使うべきではない、と。

  • function foo () simple command

この形はZshでのみ使えます。 最初の例と同じものですが、 やはり積極的に使うべきではないものです。

まとめ

関数の定義として、まずZshとかでは{}で囲わない形でも 定義できる、ということをはじめて知りました。

また、()for文なんかでもそのまま定義出来るということも知りませんでした。

この辺ワンライナーを極めて行く上では上手く使えるとより簡潔に書けたりするかもしれません。

通常のシェルスクリプトでは{}を使ってきちんと定義した方が見やすくもなるので そうした方が良いとは思いますが。

形としてはfoo ()funcitonを使わない形がPOSIX準拠で とりあえずこれを使っておけば良い、という感じです。

特に#!/bin/shを使う必要があったり多環境で使う可能性のあるスクリプトでは この形にすべきです。

Shellcheck等でもシェバンが#!/bin/shだったりシェバン無いスクリプトでは

SC2113: 'function' keyword is non-standard. Use 'foo()' instead of 'function foo'.

というエラーが出る様になってます。 (bash/zsh等がシェバンに指定されてる場合は出ません。)

funcitonを使うと関数を探しやすくなり可読性が上がる、ということはあるので BashやZshでのスクリプトであればこれらを使う事も可能です。

個人で使う様なスクリプトに関しては好みで良いとは思いますが、 それぞれどの様なシェル/環境で使えるかということをきちんと把握しておく必要はあります。

個人的には、シェルスクリプトでは()は この中に引数の定義とかを書くわけでも無いので完全に飾り物で、 そうであればfunctionの名前を付けたほうが関数を見つけやすくて良いかな、 とも思ってました。

ただ、() {とかの形も関数以外ではめったに使わないので検索とかでは それ程無理せず見る事が出来ると言えば出来ます。

過去のスクリプトを見ても時々によって使ってるものが違いますが、 .bashrcを見てみたらfunction foo () {...の形を使ってたので 整理も兼ねてfoo () {...の形にしておきました。

追記: 2018/01/30

1つfunctionを使わないと出来ない事がありました。

それはaliasで定義された文字列と同じ名前の関数を作るときです。

$ alias a="echo alias"
$ a () { echo func; }
bash: syntax error near unexpected token `('
$ function a { echo func; }
$ a
alias
$ \a
func
$

こんな感じでaというaliasを作った後にa ()を定義しようとすると aがコマンドと解釈されその後の()...の部分が引数的な扱いを受け エラーが出ている様です。

一方、functionを使うと既にaliasになっていても定義できます。

aliasと関数が両方定義されてる場合にはaliasが優先されるのでそのまま 実行すればaliasの値が実行されますが、 aliasを無視する様にバックスラッシュでエスケープしてあげると 関数が実行されます。

Bash(4.4.12)とZsh(5.4.2)両方で同じ動作をすることを確かめました。

通常両方同時に定義する、ということは無いと思いますが、 ちょっと手元で問題になったのは.bashrcの中で ある環境ではaliasを定義、他の環境では関数として定義 する様なコマンドについて。

これがaliasとして定義される環境で、一度.bashrcを読み込んでから 再度手読み込むとこの関数の所でエラーが出ます。

具体的には以下の様なもの。

.bashrc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# tac (use tail -r at BSD): Revert lines in the file/std input
# Note: There is "rev" command which
#       reversing the order of characters in every line.
# Set reverse command as tac for BSD
if ! type tac >& /dev/null;then
  if ! tail --version 2>/dev/null|grep -q GNU;then
    alias tac='tail -r'
  else
    # need "function" to avoid error on alias,
    # even if this path is not used.
    tac () {
      if [ ! -f "$1" ];then
        echo "usage: tac <file>"
        return 1
      fi
      sed -e '1!G;h;$!d' "$1"|while read -r line;do
        echo "$line"
      done
    }
  fi
fi

Macとかだとtacはaliasになり、2度目以降.bashrcを読み込んでも aliasも関数の部分も使われません。

ですが、スクリプトを読み込む時の構文チェック的なものがあり 通らないパスにも関わらずtac ()が評価され、aliasがあるのに 関数を定義しようとしてエラーが出ます。

ターミナル立ち上げ時には定義されてないので問題ありませんが 再度読もうとするとエラーが出てしまいます。

仕方ないのでこの部分に関してはfunction tac ()としてあります。 最後の括弧無しの方が良い形かもしれませんが、 .bashrc内の全ての関数をfunc ()の形にしたため、 検索でひっかけやすくするために括弧は残した形にしています。

一応この様な違いがある、ということでちょっと注意。

追記ここまで

Sponsored Links
Sponsored Links

« iPhone Xに入れたアプリ Bash/Zshで関数やaliasを無視してオリジナルのコマンドを使う方法 »

}