ShellScript
Bash
シェル芸
シェルスクリプト

【シェル芸人への道】Bashの変数展開と真摯に向き合う

More than 1 year has passed since last update.


はじめに

個人的なシェル(スクリプト)あるあるなんですが、Bashの 変数展開 って思った以上に色んなことができるってことに気がつきます。

「なんかいい感じの書き方ないかなー」

「cut, tr, sed, awk, ...、まぁできるのは間違いないんだけど」
「うーん」ググリ―

(10分後)

「えっ、変数展開...?(怪訝)」ポチポチ
「...」カタカタ

「いけるやん!!!」

ってことが結構あります(当社比)。

毎回調べるのも感動があっていいと思うんですが、もはや衝動を押さえきれないので 全部調べたい と思います。

Bashのバージョンは 4.3.48(1) でした。


バージョン

$ bash --version

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.



どうやって?

あまり馴染みがない人もいるかもしれませんが、みんな大好き man を見ます。

bash って身近すぎてmanで見るイメージがないかもしれませんが、見れます。

$ man bash

BASH(1) General Commands Manual BASH(1)

NAME
bash - GNU Bourne-Again SHell

SYNOPSIS
bash [options] [command_string | file]

COPYRIGHT
Bash is Copyright (C) 1989-2013 by the Free Software Foundation, Inc.
(略)

そう、Bashの使い方のすべてはここにあるのです。(でも本当は書いてないこともあります)

ちなみにWebでググっても大体すぐ見つかります。

以降は bash(1) - Linux manual page のページをベースに話します。


どこなの?

変数展開の話は章立てでいくと、

Basic Shell Features -> Shell Expansions -> Shell Parameter Expansion

にあります。どうやらBasicな話らしい。


変数展開

さしあたって、展開方法の一覧を出してみましょう。

実際に man bash を見てみると、


manの抜粋

       ${parameter:-word}

Use Default Values. If parameter is unset or null, the expansion of word is substituted.
Otherwise, the value of parameter is substituted.
${parameter:=word}
Assign Default Values. If parameter is unset or null, the expansion of word is assigned to
parameter. The value of parameter is then substituted. Positional parameters and special
parameters may not be assigned to in this way.

というような感じになっています。

したがって...


変数展開一覧

$ man bash | grep "^\s*\${.*}$" | tr -d " "

${parameter}
${parameter:-word}
${parameter:=word}
${parameter:?word}
${parameter:+word}
${parameter:offset}
${parameter:offset:length}
${!prefix*}
${!prefix@}
${!name[@]}
${!name[*]}
${#parameter}
${parameter#word}
${parameter##word}
${parameter%word}
${parameter%%word}
${parameter/pattern/string}
${parameter^pattern}
${parameter^^pattern}
${parameter,pattern}
${parameter,,pattern}

というのが一覧です。manを見る限りは 合計21個 ありますが、これだけではさっぱりですね。恐ろしいことに、manに書いていない変数展開もあります。

それでは順に見ていきましょう。


${parameter}: 参照

最も普通なやつです。単純な参照の場合は {} で囲む必要はありませんが、ぱっと見たときの可読性もよく、いろいろ繋げた際に 変数名を取り違える こともないです。

シェル芸人か何かの持病で、コマンド文字数を少なくしたいとかいうことでなければ囲むのをオススメします。


参照

# 代入

$ hoge="hoge-value"

# 確認
$ echo ${hoge}
hoge-value

$ echo ${HOGE}


おわかりのように、Bashでは変数名の大文字小文字を区別します。


${parameter:-word}: デフォルト値(代入なし)

変数名の後ろに :- をつけたパターンです。

この場合、参照した変数に値が入っていないときは後ろにくっつけた値が デフォルト値 として使われます。


デフォルト値(代入なし)

# 値が入っている場合は特に変わらない

$ echo ${hoge}
hoge-value
$ echo ${hoge:-"hoge-default"}
hoge-value

# 値をリセットしてみる
$ unset hoge

# 値が入っていないと、デフォルト値が出る
$ echo ${hoge}

$ echo ${hoge:-"hoge-default"}
hoge-default

# 元の変数は無事
$ echo ${hoge}



${parameter:=word}: デフォルト値(代入あり)

先ほどと似ていますが、変数名の後ろに := をつけたパターンです。

:- と同様に、デフォルト値が出る挙動は同じなのですが、こちらの場合 元の変数にもデフォルト値が代入 されます。

これを知らないと if [ -z ${parameter} ] ; then parameter=word ; fi みたいに長々と書くことになります。


デフォルト値(代入あり)

# 値が入っているとき

$ echo ${hoge}
hoge-value
$ echo ${hoge:="hoge-default"}
hoge-value

# 値をリセットする
$ unset hoge

# デフォルトの挙動をする
$ echo ${hoge}

$ echo ${hoge:="hoge-default"}
hoge-default

# デフォルト値が参照されたタイミングで、変数本体にも代入される
$ echo ${hoge}
hoge-default



${parameter-word}: 変数未定義時デフォルト値(代入なし)

さて、先ほどの2つと似ているのですが、 : を外してやると変数未定義の際にだけ反応する変数になります。

ここまで細かく挙動を変えることはあまりないかと思いますが、 unset で変数自体をリセットすることと、空文字を代入することで挙動が変わります。


変数未定義時デフォルト値(代入なし)

# 変数が定義されていて、空でないとき

$ hoge=hoge-value
$ echo ${hoge-"hoge-default"}
hoge-value

# 変数が定義されていて、空であるとき
$ hoge=""
$ echo ${hoge-"hoge-default"}

# 変数が定義されていないとき
$ unset hoge
$ echo ${hoge-"hoge-default"}
hoge-default

# しかし元の変数は無事
$ echo ${hoge}



${parameter=word}: 変数未定義時デフォルト値(代入あり)

同様に、代入あり版です。


変数未定義時デフォルト値(代入あり)

# 変数が定義されていて、空でないとき

$ hoge=hoge-value
$ echo ${hoge="hoge-default"}
hoge-value

# 変数が定義されていて、空であるとき
$ hoge=""
$ echo ${hoge="hoge-default"}

# 変数が定義されていないとき
$ unset hoge
$ echo ${hoge="hoge-default"}
hoge-default

# 元の変数にも代入される
$ echo ${hoge}
hoge-default


このように、変数が定義されていることと、変数が空であることはちょっと違う扱いを受けます。

変数が定義されているかどうかを判断したい場合、後述する ${!prefix*} あたりで確認できるのでそちらも参照してみてください。


${parameter:?word}: 未定義時のエラー出力

未定義のとき値を入れて素通りしてくれる心優しい輩がいる一方で、そうもいかないやつもいます。

:? とすると、参照した変数が未定義だったり 値が入っていない場合にエラー が出るようになります。


未定義時のエラー出力

# 指定メッセージでエラーが出る

$ unset hoge
$ echo ${hoge:?"hoge-undefined"}
-bash: hoge: hoge-undefined

# エラー扱いにもなる
$ echo ${?}
1

# メッセージ指定がないとこうなる
$ echo ${hoge:?}
-bash: hoge: parameter null or not set



${parameter:+word}: 定義時の代用

変数が入っていない時に入れてくれる優しいやつがいる一方で、 値が入っているときにだけ割り込んでくる やつもいます。

:+ とすると変数定義時に指定の値が代用されるようになります。


定義時の代用

# 値が入っていると指定の値で代用される

$ echo ${hoge}
hoge-value
$ echo ${hoge:+"other-hoge-value"}
other-hoge-value

# 逆に値が入っていないと何も出ない
$ unset hoge
$ echo ${hoge:+"other-hoge-value"}


いつ使うんでしょうね。


${parameter:offset}: 部分展開(文字数指定なし)

manの参照例に出ていますが、オフセットとは「基準からの相対的な距離」みたいな意味を表す言葉です。

: で区切って後ろに数字をくっつけると、 指定位置までの文字列を除いて抜き出す 参照になります。


部分展開(文字数指定なし)

# 10文字セット

$ hoge="hoge-value"

# 6文字目以降を参照することになる
$ echo ${hoge:5}
value


これを他のコマンドでやるなら echo ${hoge} | cut -c 6- みたいになると思います。


${parameter:offset:length}: 部分展開(文字数指定あり)

先ほどの部分展開から、さらに : で数字を続けると、指定位置までの分を除いた 指定文字数分を抜き出す 参照になります。


部分展開(文字数指定なし)

# 6文字目から全て

$ echo ${hoge:5}
value

# 6文字目から3文字分
$ echo ${hoge:5:3}
val

# 6文字目から-1文字分
$ echo ${hoge:5:-1}
valu


文字数指定部分には実は 負の値も使えます

その場合は、残った文字数から指定数を引いた分(負の数なので正確には足した分ですが...)の文字数を抜き出します。

上の例でいけば、 ${hoge:5} で展開される残り5文字(value)から -1 を引いた残り4文字が展開されてvaluが出てきます。


${!prefix*}: 変数名一覧

なんとも不思議な感じですが、定義されている変数名を探すことができます。


変数名一覧

# BASH_ で始まる変数名一覧

$ echo ${!BASH_*}
BASH_ALIASES BASH_ARGC BASH_ARGV BASH_CMDS BASH_COMMAND BASH_COMPLETION_COMPAT_DIR BASH_LINENO BASH_SOURCE BASH_SUBSHELL BASH_VERSINFO BASH_VERSION

# @ を使っても同じ動きになる
$ echo ${!BASH_@}
BASH_ALIASES BASH_ARGC BASH_ARGV BASH_CMDS BASH_COMMAND BASH_COMPLETION_COMPAT_DIR BASH_LINENO BASH_SOURCE BASH_SUBSHELL BASH_VERSINFO BASH_VERSION



${!name[*]}: 連想配列キー名一覧

連想配列のキー名もとれます。


連想配列のキー名一覧

# 連想配列を定義

$ declare -A hash
$ hash["hoge"]="hoge-value"
$ hash["fuga"]="fuga-value"

# キー名一覧が取れる
$ echo ${!hash[*]}
fuga hoge

# やはり @ でも同じ動き
$ echo ${!hash[@]}
fuga hoge


ちなみに ! を取れば、キー名ではなく中身が見えます。


連想配列のバリュー一覧

$ echo ${hash[*]}

fuga-value hoge-value

$ echo ${hash[@]}
fuga-value hoge-value



${#parameter}: 文字数カウント

変数の値が何文字か をカウントしたくなったことはありませんか?

そう、展開だけでできてしまうんです。恐ろしい。


文字数カウント

# 普通の変数

$ echo ${hoge}
hoge-value

# カウントできる
$ echo ${#hoge}
10

# 空白入りはどうか
$ echo ${fuga}
fuga space

# 空白も1文字カウント
$ echo ${#fuga}
10


他のコマンドでやろうとすると wc -c が該当するかと思いますが、改行コードもカウントする都合でこの用途では元々使われない印象があります。


他の文字数カウント

# 10文字になるはずが

$ echo ${#hoge}
10

# 末尾の改行コードを含めて11文字に
$ echo ${hoge} | wc -c
11

# 改行コードを除去してやると一応一致はする(面倒)
$ echo ${hoge} | tr -d "\n" | wc -c
10


やっぱりBashがナンバーワン!!!


${#name[*]}: 配列の要素数カウント

変数の値もそうですが、 配列の要素数 を数えたくなることもあると思います。 list.size() みたいな。

やはり変数展開でできます。恐ろしい。


配列の要素数カウント

# 2個入れる

$ list=("hoge-value" "fuga-value")
$ echo ${list[*]}
hoge-value fuga-value
$ echo ${#list[*]}
2

# 増やしてみる
$ list+=("piyo-value")
$ echo ${list[*]}
hoge-value fuga-value piyo-value
$ echo ${#list[*]}
3

# やはり @ でも同じ動き
$ echo ${#list[@]}
3


連想配列の場合でも同様に要素数を取得することが可能です。

${hash[*]} でバリューの一覧が出ますから、 ${#hash[*]} バリューの数をカウントしている感じになります。


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

${parameter:offset:length} のように、文字数指定で部分除去するのも便利ですが、「 特定の記号まで除去 」みたいなこともできたら便利ですね。

そう、展開だけでできてしまうんです。便利。


前方一致除去(最短一致)

$ echo ${hoge}

hoge-value

# パターンに一致する部分が除去される
$ echo ${hoge#hoge-}
value

# * で任意のパターンにマッチする
$ echo ${hoge#*-}
value


他の方法でやるなら sed でしょうか。条件の複雑さにもよりますが、この展開でなんとかなるパターンも多いかと思います。


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

最短一致があれば当然最長一致もあります。


前方一致除去(最長一致)

# ちょっと長くしてみる

$ echo ${hoge}
hoge-hoge-value

# 最短一致だとこうなる
$ echo ${hoge#*hoge-}
hoge-value

# 最長一致だとこうなる
$ echo ${hoge##*hoge-}
value



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

前方一致があれば後方一致もある。


後方一致除去(最短一致)

$ echo ${hoge}

hoge-value

# 後方が除去される
$ echo ${hoge%-value}
hoge



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

後方一致の最短一致があれば後方一致の最長一致もある。なんでもある。


後方一致除去(最長一致)

$ echo ${hoge}

hoge-value-value

# 後方が除去される
$ echo ${hoge%-value*}
hoge-value

$ echo ${hoge%%-value*}
hoge


ちなみにこのシリーズは配列に対して一括に効きます。


配列に対する一致

# 配列に値を入れる

$ list=("hoge-value" "fuga-value")
$ echo ${list[@]}
hoge-value fuga-value

# 後方一致
ubuntu@ubuntu16:~$ echo ${list[@]%-value}
hoge fuga

# 前方一致
ubuntu@ubuntu16:~$ echo ${list[@]#hoge-}
value fuga-value


連想配列の場合は、バリューには効くけどキーには効かないという絶妙な感じでした。


効くor効かない

# 連想配列のキーとバリュー

$ echo ${!hash[@]}
fuga hoge
$ echo ${hash[@]}
fuga-value hoge-value

# バリューに対して前方一致
$ echo ${hash[@]#ho}
fuga-value ge-value

# キーに対して前方一致
$ echo ${!hash[@]#ho}



${parameter/pattern/string}: 文字列置換

文字列置換 もできちゃいます。 tr とかやってる場合じゃない。


文字列置換

$ echo ${hoge}

hoge-value

# valueをfugaに置換する
$ echo ${hoge/value/fuga}
hoge-fuga

# 置換文字列を省くと空白置換扱い(削除)になる
$ echo ${hoge/value}
hoge-

# 配列とか連想配列のバリューにも効く
$ echo ${hash[@]/hoge/fuga}
fuga-value fuga-value
$ echo ${list[@]/hoge/fuga}
fuga-value fuga-value


あとは # とか % を使うと置換パターンの位置を制限できます。


置換パターンの制限

$ echo ${hoge}

hoge-value

# 先頭からのマッチ
$ echo ${hoge/#hoge/***}
***-value

# 先頭じゃないからマッチしない
$ echo ${hoge/#value/***}
hoge-value

# 末尾なのでマッチする
$ echo ${hoge/%value/***}
hoge-***



${parameter//pattern/string}: 文字列置換

さっきと似たような感じですが // とすると全ての該当箇所を置換します。

正規表現でいうところの g オプションですかね。


文字列置換

$ echo ${hoge}

hoge-hoge

# 最初にヒットした部分だけが置換される
$ echo ${hoge/hoge/fuga}
fuga-hoge

# ヒットする部分の全てが置換される
$ echo ${hoge//hoge/fuga}
fuga-fuga



${parameter^pattern}: 大文字化

小文字の大文字化だってできます。


大文字化

$ echo ${hoge}

hoge-value

$ echo ${hoge^}
Hoge-value


^^ にすると全部大文字になります。決して読み手を煽っているわけではありません^^


全大文字化

$ echo ${hoge^^}

HOGE-VALUE

# 他の方法だとこんな感じ
$ echo ${hoge} | tr [:lower:] [:upper:]
HOGE-VALUE



${parameter,pattern}: 小文字化

大文字にできるんなら当然小文字にもできます。


小文字化

$ echo ${HOGE}

HOGE-VALUE

# 先頭を小文字化
$ echo ${HOGE,}
hOGE-VALUE

# 全てを小文字化
$ echo ${HOGE,,}
hoge-value



${parameter~pattern}: 大小文字反転

さらに大小文字の反転までできます。なぜかマニュアルには記述が見当たらない...。

便利...?


大小文字反転

$ echo ${Hoge}

Hoge-Value

# 先頭を反転
$ echo ${Hoge~}
hoge-Value

# 全てを反転
$ echo ${Hoge~~}
hOGE-vALUE


よくわからない。


さいごに

奥深い変数展開の世界を味わってしまいました。

今日学んだことをふんだんに使ってワンライナーをひとつ。


頭文字別定義済みシェル変数一覧

for char in {A..Z} ; do vars=($(eval echo \$\{\!${char}*\})) ; echo "${char}: (${#vars[*]}) ${vars[*]}" ; done

A: (0)
B: (15) BASH BASHOPTS BASHPID BASH_ALIASES BASH_ARGC BASH_ARGV BASH_CMDS BASH_COMMAND BASH_COMPLETION_COMPAT_DIR BASH_LINENO BASH_REMATCH BASH_SOURCE BASH_SUBSHELL BASH_VERSINFO BASH_VERSION
C: (2) COLUMNS COMP_WORDBREAKS
D: (1) DIRSTACK
E: (1) EUID
F: (0)
G: (1) GROUPS
H: (8) HISTCMD HISTCONTROL HISTFILE HISTFILESIZE HISTSIZE HOME HOSTNAME HOSTTYPE
I: (1) IFS
J: (0)
K: (0)
L: (7) LANG LESSCLOSE LESSOPEN LINENO LINES LOGNAME LS_COLORS
M: (3) MACHTYPE MAIL MAILCHECK
N: (0)
O: (3) OPTERR OPTIND OSTYPE
P: (7) PATH PIPESTATUS PPID PS1 PS2 PS4 PWD
Q: (0)
R: (1) RANDOM
S: (7) SECONDS SHELL SHELLOPTS SHLVL SSH_CLIENT SSH_CONNECTION SSH_TTY
T: (1) TERM
U: (2) UID USER
V: (0)
W: (0)
X: (2) XDG_RUNTIME_DIR XDG_SESSION_ID
Y: (0)
Z: (0)

うーん、奥深い。