フューチャー技術ブログ

Bashのシェル展開

Photo by Fotis Fotopoulos on Unsplash

はじめに

こんにちは、TIGの岸本卓也です。 シェルスクリプト連載 の7日目です。

シェルスクリプトで前提とするシェルは、大抵のコンピューターにインストールされていることが多いbashを選択することが多いと思います。当記事ではそのbashを対象に、意外と色々あるシェルの展開処理の概要をまとめました。シェルスクリプトに限らず普段のコマンド操作でも展開処理が便利なシーンは多々あると思いますので、皆さんの時間の節約に役立てば幸いです。

以降では、処理される順番に展開処理を紹介します。また、説明している動作と具体例はGNU bash version 5.1.4で確認しました。

なお、Windowsでは初期状態ではbashがありませんが、インストールする方法はいくつかあります。Gitを使うためにインストールするGit for WindowsのGit Bashは名前の通りシェルにbashが使われているので、Windowsユーザーの方はお試しください。

ブレース展開 (Brace Expansion)

ブレース ({}) の間にパターンを記述すると、文字列に展開されます。パターンの記述方法は2種類あります。

  • カンマ区切りで文字列を列挙すると、列挙した文字列が列挙した順に当てはめられた文字列に展開されます。

    $ echo a{d,c,b}e
    ade ace abe

    # ファイル名の変更に使う例
    $ ls
    test.txt

    $ mv test.txt{,.orig}

    $ ls
    test.txt.orig

    # ブレース展開はネスト可能
    $ echo test_{a_{1,2},b_{3,4}}
    test_a_1 test_a_2 test_b_3 test_b_4

    # クォートで囲むと展開されない
    $ echo "a{d,c,b}e"
    a{d,c,b}e
  • 2重ドット区切りで数字またはアルファベットを指定すると、指定した数字またはアルファベットの間の連続値に展開されます。

    $ echo test_{1..5}
    test_1 test_2 test_3 test_4 test_5

    # 2重ドット区切りで3個目の数字を指定すると、展開される連続値の増減量を変更できる。
    $ echo test_{1..5..2}
    test_1 test_3 test_5

    # アルファベットの連続値や逆順での展開も可能
    $ echo test_{e..a}
    test_e test_d test_c test_b test_a

    $ echo test_{e..a..2}
    test_e test_c test_a

    # 数字の連続値なら、複数桁やゼロパディングも可能
    $ echo test_{01..15..2}
    test_01 test_03 test_05 test_07 test_09 test_11 test_13 test_15

チルダ展開 (Tilde Expansion)

単語がチルダ (~) から始まる場合、次のスラッシュ (/) までの文字列はチルダプレフィックスと呼ばれ、ディレクトリに展開されます。ここでは便利なチルダプレフィックスを紹介します。

チルダのみを指定すると、現在のユーザーのホームディレクトリに展開されます。

$ echo ~
/home/kishimoto

$ echo ~/foo
/home/kishimoto/foo

# クォートで囲むと展開されない
$ echo "~"
~

チルダに続けてログイン名を指定すると、そのログイン名のホームディレクトリに展開されます。

$ echo ~future/foo
/home/future/foo

~- というチルダプレフィックスは、変数 OLDPWD (1個前のカレントディレクトリが保持されている) が保持する値に展開されます。

$ cd /tmp/

$ cd test/

$ echo ~-
/tmp

その他のチルダプレフィックスはマニュアルの Tilde Expansion を参照してください。

パラメーター展開 (Shell Parameter Expansion)

ドル記号に続けて ${parameter}, $parameter のように変数名を記述すると、変数 parameter が保持する値に展開されます。

$ foo="hoge hoge"

$ echo ${foo}
hoge hoge

パラメーター展開は単に変数の値に展開するだけではなく、多様な処理をして展開させることができます。ここではよく使いそうな展開処理を紹介します。

  • ${!parameter} : 変数の間接展開

    $ foo=HOME

    $ echo ${!foo}
    /home/kishimoto
  • ${parameter:-word} : 変数が未定義またはnullなら、 word に展開する。

    $ unset foo

    $ echo ${foo:-hoge}
    hoge

    # `:-` の代わりにコロンなしの `-` を使うと、変数が未定義の場合のみ `word` に展開する。
    $ foo=

    $ echo ${foo-hoge}


    $ echo ${foo:-hoge}
    hoge
  • ${parameter:=word} : 変数が未定義またはnullなら、変数に word を設定してから展開する。

    $ unset foo

    $ echo ${foo:=hoge}
    hoge

    $ declare -p foo
    declare -- foo="hoge"

    # 変数が未定義の場合のみに限定したいなら、 `:=` の代わりにコロンなしの `=` を使う。
    $ foo=

    $ echo ${foo=hoge}


    $ declare -p foo
    declare -- foo=""
  • ${parameter:?word} : 変数が未定義またはnullなら、標準エラー出力に word を出力する。非インタラクティブモードのシェルなら終了する。

    $ MSG_EMPTY_PARAMETER="Please specify the parameter."

    $ unset foo

    $ echo ${foo:?${MSG_EMPTY_PARAMETER}}
    bash: foo: Please specify the parameter.

    # `word` を指定しない場合、標準のエラーメッセージが出力される。
    $ echo ${foo:?}
    bash: foo: parameter null or not set

    # 変数が未定義の場合のみに限定したいなら、 `:?` の代わりにコロンなしの `?` を使う。
    $ foo=

    $ echo ${foo?}

  • ${parameter:+word} : 変数が未定義またはnullなら何も展開しない。値が設定されているなら word に展開する。

    $ foo=hoge

    $ bar=()

    # `foo` に値が設定されているため、 `--foo "hoge"` に展開される。
    $ bar+=(${foo:+--foo "${foo}"})

    $ declare -p bar
    declare -a bar=([0]="--foo" [1]="hoge")

    $ unset foo

    # `foo` は未定義のため、何も展開されない。
    $ bar+=(${foo:+--foo "${foo}"})

    $ declare -p bar
    declare -a bar=([0]="--foo" [1]="hoge")

    $ foo=

    # 変数が未定義の場合のみに限定したいなら、 `:+` の代わりにコロンなしの `+` を使う。
    # `foo` は定義されているがnullのため、 `--foo ""` に展開される。
    $ bar+=(${foo+--foo "${foo}"})

    $ declare -p bar
    declare -a bar=([0]="--foo" [1]="hoge" [2]="--foo" [3]="")
  • ${parameter:offset:length} : 変数 parameter が保持する値の offset 文字目から長さ length 文字の部分文字列に展開する。

    $ foo=01234

    $ echo ${foo:1:2}
    12

    # `length` を省略すると、末尾までの部分文字列に展開される。
    $ echo ${foo:1}
    1234

    # 負値の `offset` を指定すると、末尾からの `offset` として処理される。 `:-` による展開と区別するため、 `:` の後にスペースが必要な点に注意。
    $ echo ${foo: -3}
    234

    # 負値の `length` を指定すると、長さではなく末尾からのオフセット指定として処理される。
    $ echo ${foo: -3:-2}
    2

    # 配列に対して適用し、部分配列に展開可能。 `offset`, `length` はだいたい上記と同様に処理される。
    $ foo_array=(hoge fuga piyo)

    $ echo ${foo_array[@]:1}
    fuga piyo
  • ${parameter#word} : 変数 parameter が保持する値の、先頭から word のパターンに一致する部分が削除された文字列に展開する。

    $ foo=/hoge/fuga/piyo

    # `#` なら最短一致で削除
    $ echo ${foo#*/}
    hoge/fuga/piyo

    # `##` なら最長一致で削除
    $ echo ${foo##*/}
    piyo
  • ${parameter%word} : 変数 parameter が保持する値の、末尾から word のパターンに一致する部分が削除された文字列に展開する。

    $ foo=abc1234

    # `%%` なら最長一致で削除。パターン `*([0-9])` は正規表現の `[0-9]*` と同じ。
    $ echo ${foo%%*([0-9])}
    abc
  • ${parameter/pattern/string} : 変数 parameter が保持する値の pattern のパターンに一致する部分が string に置換された文字列に展開する。パターンマッチの詳細はマニュアルの Pattern Matching を参照。

    $ foo=hoge1234

    # パターンに一致する部分が最長一致で置換される。パターン `+([a-z])` は正規表現の `[a-z]+` と同じ。
    $ echo ${foo/+([a-z])/fuga}
    fuga1234

    # 配列に対して適用し、配列の各要素が置換された配列に展開可能
    $ foo_array=(hoge1 hoge2 hoge3)

    $ echo ${foo_array[@]/+([a-z])/fuga}
    fuga1 fuga2 fuga3
  • ${parameter@operator} : 変数 parameter が保持する値を、 operator で指定した変換処理によって変換した文字列に展開する。

    $ foo="aBc123dEf xYz"

    # 小文字アルファベットをすべて大文字に変換
    $ echo ${foo@U}
    ABC123DEF XYZ

    # 先頭の小文字アルファベットを大文字に変換。 `x` は小文字のままである点に注意。
    $ echo ${foo@u}
    ABc123dEf xYz

    # 大文字アルファベットをすべて小文字に変換
    $ echo ${foo@L}
    abc123def xyz

    # 配列にも適用可能
    $ foo_array=(hOge Fuga piYo)

    $ echo ${foo_array[@]@U}
    HOGE FUGA PIYO

    $ echo ${foo_array[@]@u}
    HOge Fuga PiYo

    $ echo ${foo_array[@]@L}
    hoge fuga piyo

:-- のようなコロンの有無による違いは、 Parameter Expansion の表に分かりやすくまとめられています。

ここで紹介したのはパラメーター展開の一部ですが、 【シェル芸人への道】Bashの変数展開と真摯に向き合う という記事にはマニュアル記載のないパラメーター展開も含めてほぼ全量が分かりやすく解説されています。

算術式展開 (Arithmetic Expansion)

ドル記号に続く2重の丸括弧の間に算術式を記述する ($(( expression ))) と、算術式展開されます。

算術式では変数はドル記号 $ が無くても参照でき、算術演算や比較、論理演算などを記述できます。

$ foo=2

# 四則演算
$ echo $(( foo ** 2 + 1))
5

# 比較
$ echo $(( foo >= 2 ))
1

$ echo $(( foo > 2 ))
0

$ echo $(( foo != 1 ))
1

# 前置インクリメント
$ echo $(( ++foo ))
3

$ echo ${foo}
3

# 後置デクリメント
$ echo $(( foo-- ))
3

$ echo ${foo}
2

$ unset foo

# パラメーター展開やコマンド置換も可能
$ echo $(( ${foo:-2} ** 2 ))
4

コマンド置換 (Command Substitution)

ドル記号に続く1重の丸括弧の間にコマンドを記述する ($(command)) と、その部分がコマンドの実行結果で置き換えられます。

$ touch "test_$(date +'%Y-%m-%d').log"

$ ls
some-list.txt test_2021-04-05.log

# `git grep` で検索対象のブランチ指定に使う例
$ git grep 'Future' $(git for-each-ref --format="%(refname)" refs/remotes/origin) -- '*.vue' ':^*.log'

# `$(cat some-list.txt)` と同じ処理だが、より早い書き方
$ echo $(< some-list.txt)
foo bar baz

$ cat some-list.txt
foo
bar
baz

なお、バッククォートの間にコマンドを書く `command` という形式でもコマンド置換できますが、ネストしたコマンド置換が書きやすく、バックスラッシュの扱いに特別気を付ける必要もない $(command) の方が使いやすいです。

ファイル名展開 (Filename Expansion)

文字列に *, ?, [ の文字が含まれているとパターンとみなされ、パターンにマッチするファイル名のリストに展開されます。

$ ls
test1.txt test11.txt test2.txt test3.txt

$ foo=test*.txt

$ declare -p foo
declare -- foo="test*.txt"

$ echo ${foo}
test1.txt test11.txt test2.txt test3.txt

$ bar=test[1-2].txt

$ declare -p bar
declare -- bar="test[1-2].txt"

$ echo ${foo} ${bar}
test1.txt test11.txt test2.txt test3.txt test1.txt test2.txt

# クォートで囲むと展開されない
$ echo "${foo}" "${bar}"
test*.txt test[1-2].txt

プロセス置換 (Process Substitution)

プロセス置換はプロセスへの入出力をファイルで参照できるようにします。

<(process-list) 形式の場合、 process-list の実行結果を別のコマンドの入力として使うことができます。パイプと似たような機能ですが、パイプが1個しか使えずパイプ先が固定であるのに対して、プロセス置換は複数のプロセス置換が使え、ファイルを指定する箇所で使うことができます。

例えば、ある grep の結果を別の grep の検索パターンとして使う場合、プロセス置換を使わないなら中間ファイルが必要になります。

# 検索パターンを生成するgrep。検索結果は中間ファイルに出力する。
$ grep -oE '^[0-9]+' config.txt > patterns.txt

# 中間ファイルに出力された検索パターンを使ってgrepする。
$ grep -rf patterns.txt some-directory/

2個めの greppatterns.txt 部分にプロセス置換を使って1個めの grep 処理を記述すると、中間ファイルを無くせます。

grep -rf <(grep -oE '^[0-9]+' config.txt) some-directory/

プロセス置換は複数使えるため、diffを取るファイルに前処理をかけてから差分を取るといったことも簡潔に実行できます。

diff -u <(iconv -f SJIS hoge.txt) <(iconv -f SJIS fuga.txt)

同様に >(process-list) 形式の場合、別のコマンドのファイルへの出力を process-list の入力として使うことができます。次の例では標準エラー出力のリダイレクト先 2> にプロセス置換を使用しています。

# 標準エラー出力のみ、標準エラー (画面) に出力しつつログファイルにも出力する例
$ perl -le 'print "info message."; print STDERR "error message."' 2> >(tee error.log >&2)
info message.
error message.

$ cat error.log
error message.

なお、プロセス置換を最後に紹介しましたが、プロセス置換は最後に展開されるというわけではありません。

さいごに

当記事ではbashのシェル展開を紹介しました。この中で私が好きなのはプロセス置換です。中間ファイルは作成してから削除までの管理が地味に面倒なため、その中間ファイルを無くせるメリットが大きいと感じています。また、パラメーター展開の種類の豊富さには驚きました。コマンドを繋げてやっている処理をパラメーター展開に集約できるケースも多いのではないでしょうか。

当記事で シェルスクリプト連載 は一区切りとなります。シェルスクリプトというテーマの中で様々なジャンルの記事が公開されました。未読の記事があればぜひ過去記事も参照してください。

参考リンク