ShellScript
Bash
Zsh

シェルスクリプト(sh/bash/zsh)で変数から変数へ代入する方法について

More than 5 years have passed since last update.

以下 sh/bash/zsh について同様ですが、例では bash とします。

変数から変数への代入

シェルスクリプトで変数から変数へ代入する場合、ダブルコーテーション(")で括る必要はありません。

# クォートあり
hoge="$fuga"

# クォートなし
hoge=$fuga

上記はどちらも同じ結果になります。

Sample
src=$'abc  def\nghi'
echo "=== src ==="
echo "$src"

# 代入
dest=$src
echo "=== dest ==="
echo "$dest"
Output
=== src ===
abc  def
ghi
=== dest ===
abc  def
ghi

コマンド置換の結果を代入

コマンド置換でも同様です。

バッククォート(`) の場合

Sample
src=$'abc  def\nghi'
echo "=== src ==="
echo "$src"

# 代入
dest=`echo "$src"`
echo "=== dest ==="
echo "$dest"
Output
=== src ===
abc  def
ghi
=== dest ===
abc  def
ghi

$() の場合

Sample
src=$'abc  def\nghi'
echo "=== src ==="
echo "$src"

# 代入
dest=$(echo "$src")
echo "=== dest ==="
echo "$dest"
Output
=== src ===
abc  def
ghi
=== dest ===
abc  def
ghi

どちらもクォートは不要です。

仮にクォートしたとしても結果は同じですが、スマートに書く方法を知っておくのも良いのではないでしょうか。

続いて、zsh 特有の問題についても触れておきます。
(zsh でシェルスクリプトを書くかもしれない方は要注意です)

zsh で代入時に2回以上変数展開を行う場合

zsh では以下のように変数展開を一度に複数回行うことができます。
(bash では複数回の変数展開は行えません。2回に分けて行う必要があります)

Sample
## ここ重要
setopt SH_WORD_SPLIT

## パスっぽいもの
src='/aaa  aa/bbb  bb/ccc  cc/ddd  dd'
echo "=== src ==="
echo "$src"

## 1つ上のディレクトリを取得 (クォート不要)
dest1=${src%/*}
echo "=== dest1 ==="
echo "$dest1"

## 2つ上のディレクトリを取得 (クォートあり)
dest2_quote="${${src%/*}%/*}"
echo "=== dest2_quote (クォートあり) ==="
echo "$dest2_quote"

## 2つ上のディレクトリを取得 (クォートなし)
dest2_no_quote=${${src%/*}%/*}
echo "=== dest2_no_quote (クォートなし) ==="
echo "$dest2_no_quote"
Output
=== src ===
/aaa  aa/bbb  bb/ccc  cc/ddd  dd
=== dest1 ===
/aaa  aa/bbb  bb/ccc  cc
=== dest2_quote (クォートあり) ===
/aaa  aa/bbb  bb
=== dest2_no_quote (クォートなし) ===
 aa bb cc

例にある「2つ上のディレクトリを取得」を行ったときのように、2回以上変数展開を行う場合はダブルコーテーションでクォートする必要があります。

ただし、setopt SH_WORD_SPLIT を設定していなかった場合には単語分割が発生しないので関係ありません。

SH_WORD_SPLIT はデフォルトではオフなので、独立したシェルスクリプトでは SH_WORD_SPLIT を自分で制御できるので問題になることは少ないですが、インタラクティブシェルに source して使うようなシェルスクリプトの場合は、ユーザが setopt SH_WORD_SPLIT を設定している可能性があるため、常に単語分割を意識する必要があります。
(補完関数を書く場合も同様ですね)

zsh で変数宣言(local/typeset)と同時に代入する場合

この場合も setopt SH_WORD_SPLIT を設定している場合のみ問題となります。
(bashではクォートがなくても大丈夫みたいです)

Sample
func() {
    setopt SH_WORD_SPLIT

    src=$'abc  def\nghi'
    echo "=== src ==="
    echo "$src"

    ## クォートあり
    local dest_quote="$src"
    echo "=== dest_quote (クォートあり) ==="
    echo "$dest_quote"

    ## クォートなし
    local dest_no_quote=$src
    echo "=== dest_no_quote (クォートなし) ==="
    echo "$dest_no_quote"
}
func
Output
=== src ===
abc  def
ghi
=== dest_quote (クォートあり) ===
abc  def
ghi
=== dest_no_quote (クォートなし) ===
abc

このように、クォートしないと期待した動作になりません。

また、コマンド置換の場合は、setopt SH_WORD_SPLIT設定していない 場合でも正常に代入できません。

Sample
func() {
    ## デフォルトなのでなくても同じ
    unsetopt SH_WORD_SPLIT

    src=$'abc  def\nghi'
    echo "=== src ==="
    echo "$src"

    ## クォートあり
    local dest_quote="$(echo "$src")"
    echo "=== dest_quote (クォートあり) ==="
    echo "$dest_quote"

    ## クォートなし
    local dest_no_quote=$(echo "$src")
    echo "=== dest_no_quote (クォートなし) ==="
    echo "$dest_no_quote"
}
func
Output
=== src ===
abc  def
ghi
=== dest_quote (クォートあり) ===
abc  def
ghi
=== dest_no_quote (クォートなし) ===
abc

まとめ

  • 変数から変数へ代入にはクォート不要
  • コマンド置換(`...`, $(...))の結果を変数へ代入する場合もクォート不要
  • zsh で source するタイプのスクリプトを書く場合は SH_WORD_SPLIT が設定されている可能性を考えること
  • zsh で複数回変数展開する場合はクォートが必要 (SH_WORD_SPLIT あり)
  • zsh で変数宣言と同時に代入する場合はクォートが必要 (SH_WORD_SPLIT あり)
  • zsh で変数宣言と同時にコマンド置換する場合はクォートが必要 (SH_WORD_SPLIT 関係なし)

zsh は便利だけどシェルスクリプトを書く場合はハマりポイントも多いので注意しよう。

zsh に限らず、単語分割の問題はスペースや改行が含まれない場合には問題にならないので非常に気付きづらいです。
シェルスクリプトを書く時は常に単語分割を意識するようにしましょう。
あと、ちゃんとテストしましょう。