- 技術 💻
bash変数展開の一覧|${var:-default}と文字列の置換・切り出し
bashの変数展開(パラメータ展開)とは、${変数名...} の形で、変数の値を取り出すと同時に「既定値を補う・一部を切り出す・削除する・置換する」といった加工を行う機能です。 sed や cut などの外部コマンドを呼ばずに、シェルだけで文字列を扱えます。たとえば ${name:-guest} は「name が未定義か空なら guest を使う」、${file%.*} は「拡張子を取り除く」という意味です。
この記事では、変数展開の全記法を 「記法・意味・入力・実行結果」の4列の早見表 にまとめました。結果はすべて bash 5.2.21 で実行して確認したものです。さらに、:- と - の違い(未定義と空文字の扱い)を総当たりで比べた表、sh(dash)で使えない記法を実際に試した表、set -u との組み合わせ、クォートを省くと起きる単語分割まで、手元で試せるコードつきで説明します。
bash変数展開の早見表
入力の列は、展開前に変数へ代入した値です。結果は < > を除いた部分が実際の出力です。
既定値・必須チェック
| 記法 | 意味 | 入力 | 実行結果 |
|---|---|---|---|
${v:-X} |
未定義または空なら X を使う(v は変えない) | v 未定義 | X |
${v-X} |
未定義のときだけ X を使う | v="" |
(空文字) |
${v:=X} |
未定義または空なら X を代入して使う | v 未定義 | X(v も X になる) |
${v:?msg} |
未定義または空なら msg を表示して終了 | v 未定義 | v: msg、終了コード 1 |
${v:+X} |
値があるときだけ X を使う | v=abc |
X |
${v+X} |
定義済みなら(空でも)X を使う | v="" |
X |
長さ・切り出し
| 記法 | 意味 | 入力 | 実行結果 |
|---|---|---|---|
${#s} |
文字数 | s="hello world" |
11 |
${s:2} |
2文字目(0始まり)以降 | s=0123456789 |
23456789 |
${s:2:3} |
位置2から3文字 | 同上 | 234 |
${s: -3} |
末尾3文字(コロンの後に空白が必要) | 同上 | 789 |
${s: -3:2} |
末尾3文字目から2文字 | 同上 | 78 |
${s:2:-2} |
位置2から、末尾2文字の手前まで | 同上 | 234567 |
前方・後方の削除
| 記法 | 意味 | 入力 | 実行結果 |
|---|---|---|---|
${p#*/} |
先頭から最短一致で削除 | p=/var/log/app/access.2026.log.gz |
var/log/app/access.2026.log.gz |
${p##*/} |
先頭から最長一致で削除(ファイル名) | 同上 | access.2026.log.gz |
${p%/*} |
末尾から最短一致で削除(ディレクトリ) | 同上 | /var/log/app |
${p%.*} |
末尾から最短一致で削除(最後の拡張子を除く) | 同上 | /var/log/app/access.2026.log |
${p%%.*} |
末尾から最長一致で削除 | 同上 | /var/log/app/access |
${p##*.} |
最後の拡張子だけ残す | 同上 | gz |
置換・大文字小文字
| 記法 | 意味 | 入力 | 実行結果 |
|---|---|---|---|
${t/foo/X} |
最初の1か所を置換 | t="foo bar foo baz" |
X bar foo baz |
${t//foo/X} |
すべて置換 | 同上 | X bar X baz |
${t/#foo/X} |
先頭に一致したときだけ置換 | 同上 | X bar foo baz |
${t/%baz/X} |
末尾に一致したときだけ置換 | 同上 | foo bar foo X |
${t//foo} |
すべて削除(置換後を省略) | 同上 | bar baz |
${w^} |
先頭1文字を大文字に | w="hello World" |
Hello World |
${w^^} |
すべて大文字に | 同上 | HELLO WORLD |
${w,} |
先頭1文字を小文字に | w="Hello World" |
hello World |
${w,,} |
すべて小文字に | w="hello World" |
hello world |
間接参照・配列・変換
| 記法 | 意味 | 入力 | 実行結果 |
|---|---|---|---|
${!key} |
key の値を変数名とみなして参照 | APP_PORT=8080; key=APP_PORT |
8080 |
${!APP_*} |
APP_ で始まる変数名の一覧 | APP_HOST、APP_PORT を定義 |
APP_HOST APP_PORT |
"${arr[@]}" |
配列の全要素(要素ごとに1単語) | arr=("a b" c d e) |
a b/c/d/e の4つ |
${#arr[@]} |
配列の要素数 | 同上 | 4 |
${!arr[@]} |
配列の添字の一覧 | 同上 | 0 1 2 3 |
"${arr[@]:1:2}" |
配列の一部(位置1から2要素) | 同上 | c/d |
${q@Q} |
シェルに再入力できる形にクォート | q="it's a \$test" |
'it'\''s a $test' |
${w@U} |
すべて大文字に | w="hello World" |
HELLO WORLD |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「bash変数展開の一覧|${var:-default}と文字列の置換・切り出し」
検証環境と確認用スクリプト
| 項目 | 内容 |
|---|---|
| bash | GNU bash 5.2.21 |
| sh | dash 0.5.12(Ubuntu の /bin/sh) |
| ロケール | C.UTF-8(日本語の文字数を数える箇所) |
早見表の結果は、次の小さな関数で式と結果を並べて出力し、確認しました。手元で試すときにも使えます。
#!/usr/bin/env bash
# e '式' で「式 => <結果>」を表示する
e() {
local r
r=$(eval "printf '%s' \"$1\"" 2>&1)
printf '%-22s => <%s>\n' "$1" "$r"
}
s="0123456789"
e '${s:2:3}'
e '${s: -3}'
${s:2:3} => <234>
${s: -3} => <789>
変数や配列そのものの基本は、既存記事「シェルスクリプトの書き方|基本構文の早見表と安全なテンプレート」で扱っています。この記事は ${ } の中に書ける加工の記法に絞って掘り下げます。
既定値を使う:${var:-default}とコロンの有無
未定義と空文字を総当たりで比べる
既定値系の4つの記法は、コロンを付けるかどうかで「空文字」の扱いが変わります。 未定義・空文字・値ありの3つの状態で、8通りすべてを実行しました。
unset u; empty=""; val="abc"
for v in u empty val; do
for op in ':-' '-' ':+' '+'; do e "\${$v${op}X}"; done
done
| 記法 | 未定義(unset v) |
空文字(v="") |
値あり(v=abc) |
|---|---|---|---|
${v:-X} |
X |
X |
abc |
${v-X} |
X |
(空文字) | abc |
${v:+X} |
(空文字) | (空文字) | X |
${v+X} |
(空文字) | X |
X |
コロンあり(:-)は「未定義または空」を対象にし、コロンなし(-)は「未定義」だけを対象にします。 「空文字も有効な値として受け取りたい」ときはコロンなし、「空なら既定値にしたい」ときはコロンあり、と選びます。迷ったらコロンありが多くの場面に合います。
| やりたいこと | 記法 | 例 |
|---|---|---|
| 環境変数が無ければ既定値 | ${v:-X} |
port="${PORT:-8080}" |
| 引数が省略されたら既定値 | ${1:-X} |
target="${1:-.}" |
| 既定値を変数にも入れておく | ${v:=X} |
: "${LOG_DIR:=/tmp/logs}" |
| 必須の変数が無ければ止める | ${v:?msg} |
: "${DB_HOST:?DB_HOST を設定してください}" |
| 値があるときだけオプションを足す | ${v:+X} |
cmd ${verbose:+--verbose} |
| 定義されているかを調べる | ${v+x} |
[ -n "${DEBUG+x}" ] |
:= は代入も行う
unset u; : "${u:=init}"; echo "u=<$u>"
empty=""; : "${empty=init}"; echo "empty=<$empty>"
: "${empty:=init}"; echo "empty=<$empty>"
u=<init>
empty=<>
empty=<init>
:- との違いは、展開のあと変数自体にも値が残ること です。行頭の : は「何もしないコマンド」で、展開だけを行わせるために使います。2行目のとおり、コロンなしの = は空文字を「定義済み」とみなすので代入しません。なお $1 などの位置パラメータには := で代入できないため、引数には :- を使います。
:? で必須の変数を確認する
( unset u; echo "${u:?u is required}"; echo "not reached" ); echo "exit=$?"
( unset u; echo "${u:?}" ); echo "exit=$?"
all.sh: line 13: u: u is required
exit=1
all.sh: line 14: u: parameter null or not set
exit=1
メッセージは標準エラー出力に出て、スクリプト(非対話シェル)はその場で終了します。メッセージを省くと parameter null or not set という既定の文言になります。スクリプトの冒頭に必須の環境変数を並べておくと、設定漏れにすぐ気づけます。
#!/usr/bin/env bash
set -u
: "${DB_HOST:?環境変数 DB_HOST を設定してください}"
echo "connect to $DB_HOST"
$ bash req.sh
req.sh: line 3: DB_HOST: 環境変数 DB_HOST を設定してください
$ echo $?
1
$ DB_HOST=db01 bash req.sh
connect to db01
文字列の長さと切り出し
長さ ${#var}
s="hello world"; ja="こんにちは"; arr=(a b c)
| 記法 | 実行結果 | 説明 |
|---|---|---|
${#s} |
11 |
文字数 |
${#ja} |
5 |
UTF-8 のロケール(C.UTF-8 や ja_JP.UTF-8)では文字数 |
${#ja}(LC_ALL=C) |
15 |
C ロケールではバイト数になる |
${#arr[@]} |
3 |
配列の要素数 |
${#arr[0]} |
1 |
要素0の文字数 |
日本語の文字数はロケールによって結果が変わります。 cron など、環境変数が最小限の状態で動くスクリプトでは LC_ALL を明示しておくと、手元と同じ結果になります。
切り出し ${var:位置:長さ}
位置は 0 から数えます。s="0123456789" で試した結果です。
| 記法 | 実行結果 | 説明 |
|---|---|---|
${s:2} |
23456789 |
位置2から最後まで |
${s:2:3} |
234 |
位置2から3文字 |
${s:0:1} |
0 |
先頭1文字 |
${s: -3} |
789 |
末尾から3文字 |
${s:(-3)} |
789 |
かっこで囲んでも同じ |
${s:-3} |
0123456789 |
既定値の :- と解釈される(s に値があるのでそのまま) |
${s: -3:2} |
78 |
末尾3文字目から2文字 |
${s:2:-2} |
234567 |
長さに負数:末尾から2文字手前まで |
${s: -5:-2} |
567 |
末尾5文字目から、末尾2文字の手前まで |
${s:20} |
(空文字) | 範囲外はエラーにならず空になる |
負の位置を使うときは、コロンの後に空白を入れる(${s: -3})か、かっこで囲みます(${s:(-3)})。 空白が無い ${s:-3} は「未定義なら 3」という既定値の記法になり、エラーも出ないので気づきにくい箇所です。
長さに負数を指定して、開始位置より手前になった場合は次のエラーになります。
$ echo "${s:2:-9}"
bash: -9: substring expression < 0
前方・後方の削除:パスと拡張子を取り出す
4つの記号の覚え方
| 記号 | 削除する側 | 一致のしかた |
|---|---|---|
# |
先頭(前方) | 最短一致 |
## |
先頭(前方) | 最長一致 |
% |
末尾(後方) | 最短一致 |
%% |
末尾(後方) | 最長一致 |
キーボードの並びで # は $ の左、% は $ の右にある ので、「左(先頭)を削るのが #、右(末尾)を削るのが %」と覚えられます。記号を2つ重ねると最長一致です。パターンには glob(*、?、[...])が使えます。正規表現ではない点に注意してください。
パスの分解レシピ
path="/var/log/app/access.2026.log.gz" を分解します。
| 取り出したいもの | 記法 | 実行結果 |
|---|---|---|
| ファイル名(basename 相当) | ${path##*/} |
access.2026.log.gz |
| ディレクトリ(dirname 相当) | ${path%/*} |
/var/log/app |
| 最後の拡張子 | ${path##*.} |
gz |
| 最後の拡張子を除いたパス | ${path%.*} |
/var/log/app/access.2026.log |
| 最初のドット以降をすべて除く | ${path%%.*} |
/var/log/app/access |
先頭の / を除く |
${path#*/} |
var/log/app/access.2026.log.gz |
| ファイル名の、最初のドットより前 | f="${path##*/}"; ${f%%.*} |
access |
| ファイル名の、最初のドットより後 | f="${path##*/}"; ${f#*.} |
2026.log.gz |
同じ値で basename "$path" は access.2026.log.gz、dirname "$path" は /var/log/app を返し、結果が一致することも確認しました。
覚えておきたい境界の動き
| 入力 | 記法 | 実行結果 | 説明 |
|---|---|---|---|
nopath="file.txt" |
${nopath%/*} |
file.txt |
/ が無いと何も削除されない(dirname は . を返す) |
nopath="file.txt" |
${nopath##*/} |
file.txt |
一致しなければ元の値のまま |
p2="/usr/local/" |
${p2%/} |
/usr/local |
末尾の / を1つだけ除く |
url="https://example.com/a/b?x=1" |
${url#*://} |
example.com/a/b?x=1 |
スキーム部分を除く |
| 同上 | ${url%%\?*} |
https://example.com/a/b |
? 以降を除く(? は glob 文字なので \? と書く) |
パターンに一致しないときは、エラーにならず元の値がそのまま返ります。 ${nopath%/*} のように「ディレクトリ部分が無いパス」を扱う可能性があるなら、dirname コマンドを使うか、/ を含むかを先に確かめる書き方が安全です。
拡張子を付け替える
for f in report.md notes.md; do
echo "$f -> ${f%.md}.html"
done
report.md -> report.html
notes.md -> notes.html
${f%.md} で拡張子を取り除き、その後ろに新しい拡張子を続けて書くだけです。ファイルを順に処理するループの書き方は、別記事「シェルスクリプトのfor・whileループ」でまとめています。
文字列を置換する
t="foo bar foo baz" で試した結果です。
| 記法 | 実行結果 | 説明 |
|---|---|---|
${t/foo/X} |
X bar foo baz |
最初に一致した1か所 |
${t//foo/X} |
X bar X baz |
一致したすべて |
${t/#foo/X} |
X bar foo baz |
先頭が一致したときだけ |
${t/%baz/X} |
foo bar foo X |
末尾が一致したときだけ |
${t/#bar/X} |
foo bar foo baz |
先頭が bar ではないので変化なし |
${t//foo} |
bar baz |
置換後を省略すると削除 |
${t// /_} |
foo_bar_foo_baz |
空白をすべて _ に |
${t//[aeiou]/*} |
f** b*r f** b*z |
文字クラスも使える |
${t/f*o/X} |
X baz |
* は最長一致(foo bar foo 全体に一致) |
スラッシュ1つは最初の1か所、2つはすべて、/# は先頭、/% は末尾 です。パターンは削除のときと同じく glob で、最長一致になります。最後の行のように * を使うと思ったより広く一致することがあるので、結果を確かめながら使うとよいでしょう。
スラッシュや特殊文字を置換する
| 入力 | 記法 | 実行結果 | 説明 |
|---|---|---|---|
p="a/b/c" |
${p//\//-} |
a-b-c |
パターン中の / は \/ と書く |
amp="name" |
${amp/name/[&]} |
[name] |
bash 5.2 では、置換後の & が一致した文字列を表す |
2つ目は bash 5.2 で加わった動作です(patsub_replacement オプションが既定で有効)。置換後の文字列に & そのものを入れたいときは \& と書きます。置換後の文字列を変数で渡す場合は、"${t//foo/"$rep"}" のように変数をクォートすると & が文字どおりに扱われます。
sedとの使い分け
| 状況 | 向いている方法 |
|---|---|
| 変数に入った短い文字列を1回置換する | 変数展開(プロセスを起動しないので速い) |
| ループの中で何度も置換する | 変数展開 |
正規表現(+、\d、グループ参照など)が必要 |
sed または [[ =~ ]] |
| ファイル全体を置換する | sed |
大文字・小文字を変換する
w="hello World" で試した結果です。
| 記法 | 実行結果 | 説明 |
|---|---|---|
${w^} |
Hello World |
先頭1文字を大文字 |
${w^^} |
HELLO WORLD |
すべて大文字 |
${w,}(w="Hello World") |
hello World |
先頭1文字を小文字 |
${w,,} |
hello world |
すべて小文字 |
${w^^[lo]} |
heLLO WOrLd |
パターンに一致する文字だけ大文字 |
${w@U} |
HELLO WORLD |
^^ と同じ結果 |
${w@u} |
Hello World |
^ と同じ結果 |
${w@L} |
hello world |
,, と同じ結果 |
入力の表記ゆれをそろえて比較するときに便利 です。たとえば、確認の入力を小文字にそろえてから判定できます。
answer="Yes"
case "${answer,,}" in
y|yes) echo "続行します" ;;
*) echo "中止します" ;;
esac
^^ と ,, は bash 4.0 以降の機能です。macOS に標準で入っている bash は 3.2 系のため、そちらでは tr '[:lower:]' '[:upper:]' を使います(macOS での動作は一般に知られた仕様で、この記事では未実行です)。
間接参照:変数名を変数で指定する
APP_HOST=localhost; APP_PORT=8080; key=APP_PORT
echo "${!key}" # 8080
echo "${!APP_*}" # APP_HOST APP_PORT
for name in "${!APP_@}"; do
echo "$name=${!name}"
done
8080
APP_HOST APP_PORT
APP_HOST=localhost
APP_PORT=8080
| 記法 | 意味 |
|---|---|
${!key} |
key の値(APP_PORT)を変数名として、その値を取り出す |
${!APP_*} |
APP_ で始まる変数名を、1つの文字列にまとめて返す |
"${!APP_@}" |
APP_ で始まる変数名を、1つずつ別の単語として返す(ループ向き) |
${!arr[@]} |
配列の添字(連想配列ならキー)の一覧 |
${!...} は、書き方によって「間接参照」「変数名の一覧」「配列の添字」の3つの意味を持ちます。 接頭辞をそろえた設定用の変数をまとめて表示する、といった用途に向いています。
間接参照する変数名が決まっているなら、名前参照(nameref)も使えます。
declare -n ref=APP_HOST
echo "nameref: $ref" # nameref: localhost
配列と位置パラメータの展開
配列
arr=("a b" c d e) を、要素ごとに < > で囲んで表示しました。
show() { printf '<%s>' "$@"; echo; }
show "${arr[@]}"
| 記法 | 実行結果 | 説明 |
|---|---|---|
"${arr[@]}" |
<a b><c><d><e> |
要素ごとに1単語(4個) |
"${arr[*]}" |
<a b c d e> |
全要素を IFS の先頭文字でつないだ1単語 |
${arr[@]}(クォートなし) |
<a><b><c><d><e> |
単語分割されて5個になる |
"${!arr[@]}" |
<0><1><2><3> |
添字の一覧 |
"${arr[@]:1:2}" |
<c><d> |
位置1から2要素 |
${arr[-1]} |
e |
最後の要素 |
$arr |
a b |
添字を省くと要素0だけ |
"${arr[@]/#/x-}" |
<x-a b><x-c><x-d><x-e> |
全要素の先頭に文字を足す |
"${f[@]%.*}"(f=(a.txt b.md)) |
<a><b> |
全要素から拡張子を除く |
"${arr[@]^^}" |
<A B><C><D><E> |
全要素を大文字に |
(IFS=,; echo "${arr[*]}") |
a b,c,d,e |
カンマ区切りで連結 |
削除・置換・大文字小文字の記法は、[@] を付けると配列の全要素に一度に適用されます。 ループを書かずに一括で加工できるので、ファイル名の一覧を扱うときに重宝します。
位置パラメータ(引数)
set -- one two three four で引数を4つ設定して試しました。
| 記法 | 実行結果 | 説明 |
|---|---|---|
${1} |
one |
1番目の引数 |
${#} |
4 |
引数の個数 |
"${@:2}" |
two three four |
2番目以降 |
"${@:2:2}" |
two three |
2番目から2個 |
"${@: -1}" |
four |
最後の引数 |
$10 と ${10}(引数11個のとき) |
a0 と j |
2桁以上は波かっこが必要 |
最後の行は、引数を a から k まで11個にして確認しました。$10 は「$1 の後ろに 0」と解釈される ので、10番目以降は ${10} と書きます。
${var@Q}などの変換演算子
${変数@演算子} の形で、値を別の表現に変換できます。
| 記法 | 入力 | 実行結果 | 説明 |
|---|---|---|---|
${q@Q} |
q="it's a \$test" |
'it'\''s a $test' |
シェルにそのまま渡せる形にクォート |
"${arr[@]@Q}" |
arr=("a b" c d e) |
'a b' 'c' 'd' 'e' |
配列の各要素をクォート |
${esc@E} |
esc='a\tb\n' |
a(タブ)b(改行) |
\t などのエスケープを解釈 |
${ps@P} |
ps='\h:\W' |
ホスト名:現在のディレクトリ名 | プロンプト文字列として展開 |
${q@A} |
同上 | q='it'\''s a $test' |
変数を再作成できる代入文 |
${num@A} |
declare -i num=5 |
declare -i num='5' |
属性つきの宣言文 |
${num@a} |
同上 | i |
変数の属性(i=整数、r=読み取り専用、x=エクスポート、a=配列、A=連想配列) |
${m[@]@K} |
declare -A m=([k1]=v1 [k2]=v2) |
k1 "v1" k2 "v2" |
連想配列のキーと値の組(順序は不定) |
${w@U} / ${w@u} / ${w@L} |
w="hello World" |
HELLO WORLD / Hello World / hello world |
大文字・先頭大文字・小文字 |
よく使うのは @Q です。 実行するコマンドをログに残すときや、ssh や eval に渡す文字列を組み立てるときに、空白や引用符を含む値を安全な形にできます。
args=("my report.txt" "it's")
echo "実行: cp ${args[*]@Q} /backup/"
実行: cp 'my report.txt' 'it'\''s' /backup/
@U、@u、@L、@K は bash 5.1 以降で使える演算子として知られています(この記事では 5.2.21 で動作を確認しました)。
sh(dash)で使えない記法を実際に試す
#!/bin/sh のスクリプトや、Docker の軽量イメージ、sh -c で実行されるコマンドでは、bash ではなく dash などの POSIX シェルが動きます。同じ式を bash 5.2.21 と dash 0.5.12 の両方で実行しました(v="hello World"、path="/a/b/c.tar.gz"、u は未定義)。
| 記法 | bash | dash | 分類 |
|---|---|---|---|
${u:-def} |
def |
def |
POSIX |
${u:=def} |
def |
def |
POSIX |
${u:+alt} |
(空文字) | (空文字) | POSIX |
${u:?need u} |
エラーで終了 | u: need u で終了 |
POSIX |
${#v} |
11 |
11 |
POSIX |
${path#*/} |
a/b/c.tar.gz |
a/b/c.tar.gz |
POSIX |
${path##*/} |
c.tar.gz |
c.tar.gz |
POSIX |
${path%.*} |
/a/b/c.tar |
/a/b/c.tar |
POSIX |
${path%%.*} |
/a/b/c |
/a/b/c |
POSIX |
${v:0:5} |
hello |
Bad substitution |
bash 拡張 |
${v: -3} |
rld |
Bad substitution |
bash 拡張 |
${v/World/X} |
hello X |
Bad substitution |
bash 拡張 |
${v//l/L} |
heLLo WorLd |
Bad substitution |
bash 拡張 |
${v^^} |
HELLO WORLD |
Bad substitution |
bash 拡張 |
${v,,} |
hello world |
Bad substitution |
bash 拡張 |
${!v} |
(間接参照) | Bad substitution |
bash 拡張 |
${v@Q} |
'hello World' |
Bad substitution |
bash 拡張 |
a=(1 2 3) |
配列 | Syntax error: "(" unexpected |
bash 拡張 |
既定値系(:- := :? :+)、長さ(${#v})、前方・後方の削除(# ## % %%)は sh でも使えます。切り出し・置換・大文字小文字・間接参照・@ 演算子・配列は bash の拡張 です。Bad substitution と表示されたら、スクリプトが bash ではなく sh で実行されていないかを確認してください。
sh で動かすときの代わりの書き方
次の書き方は dash で実行して結果を確認しました。
| やりたいこと | bash | sh でも動く書き方 | 結果 |
|---|---|---|---|
| 先頭5文字 | ${v:0:5} |
echo "$v" | cut -c1-5 |
hello |
| 先頭5文字(外部コマンドなし) | ${v:0:5} |
${v%"${v#?????}"} |
hello |
| 置換(1か所) | ${v/World/X} |
echo "$v" | sed "s/World/X/" |
hello X |
| 置換(すべて) | ${v//l/L} |
echo "$v" | sed "s/l/L/g" |
heLLo WorLd |
| 大文字に | ${v^^} |
echo "$v" | tr "[:lower:]" "[:upper:]" |
HELLO WORLD |
set -uと組み合わせる
set -u(set -o nounset)は、未定義の変数を参照するとエラーで止める設定です。変数名の打ち間違いに気づけるので、スクリプトの冒頭に書くことが多い設定です。既定値系の変数展開を使うと、set -u のもとでも「未定義かもしれない変数」を安全に参照できます。
#!/usr/bin/env bash
set -u
echo "1: ${NAME:-guest}"
echo "2: ${NAME-}"
echo "3: <${1:-}>"
if [ -n "${DEBUG+x}" ]; then echo "DEBUG is set"; else echo "4: DEBUG is unset"; fi
[[ -v NAME ]] || echo "5: -v で未定義を判定"
arr=()
echo "6: 空配列の要素数=${#arr[@]} 展開=<${arr[@]}>"
echo "7: $NAME"
echo "ここには来ない"
1: guest
2:
3: <>
4: DEBUG is unset
5: -v で未定義を判定
6: 空配列の要素数=0 展開=<>
setu.sh: line 10: NAME: unbound variable
終了コードは 1 でした。7行目のように既定値なしで参照したところで止まり、最後の echo は実行されていません。
| 場面 | 書き方 |
|---|---|
| 省略できる引数を受け取る | arg="${1:-}" |
| 環境変数が無ければ既定値 | "${NAME:-guest}" |
| 無ければ空文字として扱う | "${NAME-}" |
| 定義されているかどうかだけ知りたい | [ -n "${DEBUG+x}" ] または [[ -v DEBUG ]] |
| 必須なので、無ければ分かりやすい文言で止める | : "${DB_HOST:?DB_HOST を設定してください}" |
${DEBUG+x} は「定義されていれば x、未定義なら空」になるので、空文字が入っている場合も「定義済み」と判定できる のが -n "$DEBUG" との違いです。
クォートしないと起きる単語分割
変数展開の結果は、ダブルクォートで囲まないと「空白での分割(単語分割)」と「glob の展開」が行われます。 引数の個数を表示する関数で確かめました。カレントディレクトリには a.txt と b.txt があります。
#!/usr/bin/env bash
file="my report.txt"; pat="*.txt"; blank=""
count() { echo "引数 $# 個: $(printf '<%s>' "$@")"; }
count $file
count "$file"
count $pat
count "$pat"
count $blank
count "$blank"
引数 2 個: <my><report.txt>
引数 1 個: <my report.txt>
引数 2 個: <a.txt><b.txt>
引数 1 個: <*.txt>
引数 0 個: <>
引数 1 個: <>
| クォートなしで起きたこと | 影響の例 | 直した書き方 |
|---|---|---|
| 空白で2つに分かれた | rm $file が my と report.txt を対象にする |
"$file" |
*.txt がファイル名に展開された |
意図しないファイルが引数になる | "$pat" |
| 空文字が引数ごと消えた | [ $blank = "x" ] が構文エラーになる |
"$blank" |
| 連続した空白が1つになった | echo $v で a b が a b と表示される |
echo "$v" |
3つ目は、実際に次のエラーになります。
$ [ $blank = "x" ]
bash: [: =: unary operator expected
$ [ "$blank" = "x" ]; echo $?
1
既定値に空白が含まれる場合も同じ です。${ } を使っていても、外側のダブルクォートは省略できません。
dir="${TARGET:-/tmp/my demo}"
count ${dir} # 引数 2 個: </tmp/my><demo>
count "${dir}" # 引数 1 個: </tmp/my demo>
| クォートが必要な場所 | クォートが不要な場所 |
|---|---|
コマンドの引数(cp "$src" "$dst") |
代入の右辺(a=$b、a=${b:-x}) |
[ ] の中([ "$a" = "$b" ]) |
[[ ]] の左辺([[ $a == x ]]) |
for の一覧(for f in "${arr[@]}") |
(( )) の中((( n > 0 ))) |
値があるときだけ足す引数は、意図的に外す(${verbose:+--verbose}) |
case $v in |
迷ったらダブルクォートで囲む、が基本 です。条件式の書き分けは、既存記事「シェルスクリプトif条件式一覧」に早見表があります。
よくある質問
Q. ${var:-default} と ${var-default} はどちらを使えばよいですか?
A. 多くの場合は、コロンありの ${var:-default} が合います。未定義のときも空文字のときも既定値になるためです。「空文字を、利用者が明示的に指定した値として尊重したい」場合だけ、コロンなしの ${var-default} を選びます。
Q. ${var:-default} を使うと、変数 var の値も変わりますか?
A. 変わりません。その場で既定値が使われるだけです。変数自体にも既定値を入れたいときは ${var:=default} を使います。この記事の検証でも、: "${u:=init}" のあとに u が init になることを確認しました。
Q. Bad substitution と表示されます。原因は何ですか?
A. bash 拡張の記法(切り出し ${v:0:5}、置換 ${v/a/b}、${v^^} など)を、sh(dash など)で実行している場合に表示されます。スクリプトの1行目を #!/usr/bin/env bash にして、sh script.sh ではなく bash script.sh で実行してください。sh で動かす必要があるときは、この記事の代わりの書き方の表を参考にしてください。
Q. 変数展開の中で正規表現は使えますか?
A. 使えません。#、%、/ のパターンは glob(*、?、[...])です。正規表現が必要なら、[[ $s =~ 正規表現 ]] と BASH_REMATCH、または sed を使います。
まとめ
- 既定値系は
:-(使う)、:=(代入もする)、:?(無ければ止める)、:+(あるときだけ使う) の4つ。コロンなしは「未定義」だけを対象にする - 切り出しは
${v:位置:長さ}。末尾からは${v: -3}と空白を入れる(${v:-3}は既定値の記法になる) #は先頭、%は末尾を削除し、記号2つで最長一致。${path##*/}でファイル名、${path%/*}でディレクトリ、${path##*.}で拡張子- 置換は
/が1か所、//がすべて、/#が先頭、/%が末尾。パターンは glob で、正規表現ではない ${v^^}${v,,}で大文字小文字、${!key}で間接参照、${v@Q}で安全なクォート- 既定値系・長さ・
#%は sh でも動く。切り出し・置換・大文字小文字・配列は bash 拡張で、sh ではBad substitutionになる set -uのもとでは${1:-}や${VAR+x}で未定義に備え、展開結果は必ずダブルクォートで囲む
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「bash変数展開の一覧|${var:-default}と文字列の置換・切り出し」
株式会社RJCでは、bashの変数展開や文字列操作の書き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。