LoginSignup
711
587

More than 1 year has passed since last update.

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

Last updated at Posted at 2017-11-17

はじめに

個人的なシェル(スクリプト)あるあるなんですが、変数操作に悩んでいると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でググってもmanの内容は大体すぐ見つかります。
細かな記載のズレで分かりづらさを生まぬよう、以降は 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.

というような感じになっています。
したがってこれをシェル芸人の所作で grep して tr なりすると...

変数展開一覧
$ 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

set -e なんかでエラーとともにスクリプトが止まるようにしておくと、想定外な状態で動かないように組むことができます。
削除スクリプトで削除対象を指定する変数が空だったので rm -rf / で動いてしまった 」みたいな話は稀によく聞きますが、そうした悲しみを生まないためにもこの使い方は覚えておいてください。

${parameter:+word}: 定義時の代用(代入あり)

変数が入っていない時に入れてくれる優しいやつがいる一方で、 値が入っているときにだけ割り込んでくる やつもいます。
:+ とすると変数定義時に指定の値が代用されるようになります。

定義時の代用(代入あり)
# 値が入っていると指定の値で代用される
$ hoge='hoge'
$ echo ${hoge:+"other-hoge-value"}
other-hoge-value

# 代入される
echo ${hoge}
other-hoge-value

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

いつ使うんでしょうね。

${parameter+word}: 定義時の代用(代入なし)

:-と同様こちらも+だけで使うと、変数定義時にword値が代用されるだけになります。

定義時の代用(代入なし)
# 値が入っていると指定の値で代用される
$ hoge='hoge'
$ echo ${hoge+"other-hoge-value"}
other-hoge-value

# もとの値はそのまま
echo ${hoge}
hoge

$ unset hoge

# 値が入っていなければ何も表示されない
$ echo ${hoge+foo}

${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

$ echo -n ${hoge} | 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

よくわからない。

さいごに

奥深い変数展開の世界を味わってしまいました。
cut, tr, sed, wc, ...など気軽にパイプ処理できるのがシェルコマンドのいいところですが、そうしたものに頼らずとも、かなり色々なことができるとわかりました。
皆さんもぜひ魅惑のBashワールドの虜になってみてください。

ただし、こういったマニアックな表現は 基本的にスクリプトの可読性を損なう ということを忘れないでください。
この記事を読んだ皆さんは ${parameter**なんとかかんとか**} という表現を見て、「ははーん、何かの変数展開を利用しているんだな」と直感し、そういう頭で調べることができますが、多くの人はそこまで読み取れないはずです。
一方で、「何か特殊なことが起きている」と思ってもこういったショートな記法は圧倒的にググラビリティに劣るので、コメントを忘れると、その何気ない表現1つで同僚の1日を葬り去ることにもなりかねない恐ろしい表現であるとも言えます。

慣れれば麻薬のように気分がいい変数展開ですが、用法・用量を守り、シェル芸人の指導の下で正しくお使いください。

学びの成果を感じるワンライナー

それでは、今日学んだことをふんだんに使ってワンライナーをひとつ。言うまでもありませんが、黙ってこんなものを使ったら狂人扱いされます。

頭文字別定義済みシェル変数一覧
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)

うーん、奥深い。

711
587
11

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
711
587