`set -u`を使用して空の配列拡張をバッシュ


103

私はを持つbashスクリプトを書いていてset -u、空の配列の展開に問題があります。bashは、展開中に空の配列を未設定の変数として扱うようです。

$ set -u
$ arr=()
$ echo "foo: '${arr[@]}'"
bash: arr[@]: unbound variable

declare -a arrどちらも役に立ちません。)

これに対する一般的な解決策は、${arr[@]-}代わりに使用することです。したがって、(「未定義」)空の配列の代わりに空の文字列に置き換えます。ただし、これは適切な解決策ではありません。単一の空の文字列が含まれている配列と空の配列を区別できないためです。(@ -expansionはbashで特別であり、に展開"${arr[@]}"される"${arr[0]}" "${arr[1]}" …ため、コマンドラインを構築するための完璧なツールになります。)

$ countArgs() { echo $#; }
$ countArgs a b c
3
$ countArgs
0
$ countArgs ""
1
$ brr=("")
$ countArgs "${brr[@]}"
1
$ countArgs "${arr[@]-}"
1
$ countArgs "${arr[@]}"
bash: arr[@]: unbound variable
$ set +u
$ countArgs "${arr[@]}"
0

それで、その中の配列の長さをチェックするif(以下のコードサンプルを参照)か-u、その短い部分の設定をオフにする以外に、その問題を回避する方法はありますか?

if [ "${#arr[@]}" = 0 ]; then
   veryLongCommandLine
else
   veryLongCommandLine "${arr[@]}"
fi

更新:bugs池上さんの説明でタグを削除しました。

回答:


17

唯一の安全なイディオムです${arr[@]+"${arr[@]}"}

これはすでに池上氏の回答で推奨されていますが、このスレッドには多くの誤った情報と当て推量があります。${arr[@]-}またはなどの他のパタ​​ーンは、Bashのすべてのメジャーバージョンで安全${arr[@]:0}ではありません

以下の表が示すように、すべての現代風のBashバージョンで信頼できる唯一の拡張は${arr[@]+"${arr[@]}"}(列+")です。注目すべきことに、Bash 4.2では他のいくつかの拡張が失敗し${arr[@]:0}ます。これには(残念ながら)短いイディオムが含まれますが、これは誤った結果を生成するだけでなく、実際には失敗します。4.4より前のバージョン、特に4.2をサポートする必要がある場合、これが唯一の有効なイディオムです。

バージョン間のさまざまなイディオムのスクリーンショット

残念ながら+、一見すると同じように見える他の拡張は、実際には異なる動作をします。-expansionは単一の空の要素()を持つ配列を「null」として扱い、したがって(一貫して)同じ結果に展開しないため、:+展開は安全ではありません。:('')

入れ子になった配列("${arr[@]+${arr[@]}}")の代わりに完全な展開を引用すると、これは4.2でも同様に安全とは言えません。

このデータを生成したコードと、bistのいくつかの追加バージョンの結果をこの要点で確認できます。


1
テスト中とは思わない"${arr[@]}"。何か不足していますか?私が見ることができるものから、それは少なくともで動作し5.xます。
x-yuri

1
@ x-yuriはい、Bash 4.4で状況が修正されました。スクリプトが4.4以降でのみ実行されることがわかっている場合は、このパターンを使用する必要はありませんが、多くのシステムは以前のバージョンのままです。
dimo414

もちろんです。見栄えが良い(フォーマットなど)にもかかわらず、余分なスペースはbashの大きな悪であり、多くの問題を引き起こしています
agg3l

81

ドキュメントによると、

添え字に値が割り当てられている場合、配列変数は設定されていると見なされます。null文字列は有効な値です。

下付き文字には値が割り当てられていないため、配列は設定されません。

ただし、ここではエラーが適切であるとドキュメントで示されていますが、4.4以降ではこれは当てはまりません。

$ bash --version | head -n 1
GNU bash, version 4.4.19(1)-release (x86_64-pc-linux-gnu)

$ set -u

$ arr=()

$ echo "foo: '${arr[@]}'"
foo: ''

古いバージョンで必要なことを達成するためにインラインで使用できる条件があります。の${arr[@]+"${arr[@]}"}代わりに使用してください"${arr[@]}"

$ function args { perl -E'say 0+@ARGV; say "$_: $ARGV[$_]" for 0..$#ARGV' -- "$@" ; }

$ set -u

$ arr=()

$ args "${arr[@]}"
-bash: arr[@]: unbound variable

$ args ${arr[@]+"${arr[@]}"}
0

$ arr=("")

$ args ${arr[@]+"${arr[@]}"}
1
0: 

$ arr=(a b c)

$ args ${arr[@]+"${arr[@]}"}
3
0: a
1: b
2: c

bash 4.2.25および4.3.11でテスト済み。


4
誰もがこれがどのようにそしてなぜ機能するのか説明できますか?私が[@]+実際に何をしているのか、なぜ2番目${arr[@]}がアンバインドされたエラーを引き起こさないのか混乱しています。
Martin von Wittich、2016

2
${parameter+word}が設定されていないword場合にのみ展開されparameterます。
池上

2
${arr+"${arr[@]}"}より短く、同様に動作するようです。
Cederberg氏による2017

3
@Per Cerderberg、機能しません。unset arrarr[1]=aargs ${arr+"${arr[@]}"}VSargs ${arr[@]+"${arr[@]}"}
池上

1
正確には、ケースに+膨張が発生していない(すなわち、空の配列)の膨張はで置換されて何も空の配列は、に展開まさにあります。:+単一要素の('')配列も未設定として扱い、同様に何も展開せずに値を失うため、安全ではありません。
dimo414

23

@ikegamiの受け入れられた答えは微妙に間違っています!正しい呪文は${arr[@]+"${arr[@]}"}

$ countArgs () { echo "$#"; }
$ arr=('')
$ countArgs "${arr[@]:+${arr[@]}}"
0   # WRONG
$ countArgs ${arr[@]+"${arr[@]}"}
1   # RIGHT
$ arr=()
$ countArgs ${arr[@]+"${arr[@]}"}
0   # Let's make sure it still works for the other case...

もはや違いはありません。bash-4.4.23arr=('') && countArgs "${arr[@]:+${arr[@]}}"生成し1ます。しかし、${arr[@]+"${arr[@]}"}フォームでは、コロンを追加/追加しないことにより、空/空でない値を区別できます。
X-ゆり

arr=('') && countArgs ${arr[@]:+"${arr[@]}"}-> 0arr=('') && countArgs ${arr[@]+"${arr[@]}"}-> 1
x-yuri

1
これはずっと前に私の答えで修正されました。(実際、私は以前にその効果に対するこの回答についてコメントを残したと思いますか?!)
ikegami

16

最近リリースされた(2016/09/16)bash 4.4(Debianストレッチなどで利用可能)で配列の処理が変更されたことが判明しました。

$ bash --version | head -n1
bash --version | head -n1
GNU bash, version 4.4.0(1)-release (x86_64-pc-linux-gnu)

空の配列の拡張で警告が出なくなりました

$ set -u
$ arr=()
$ echo "${arr[@]}"

$ # everything is fine

確認できますbash-4.4.12 "${arr[@]}"
x-yuri 2018年

14

これは、arr [@]を複製したくない場合や空の文字列を使用する場合に適している別のオプションです。

echo "foo: '${arr[@]:-}'"

テストする:

set -u
arr=()
echo a "${arr[@]:-}" b # note two spaces between a and b
for f in a "${arr[@]:-}" b; do echo $f; done # note blank line between a and b
arr=(1 2)
echo a "${arr[@]:-}" b
for f in a "${arr[@]:-}" b; do echo $f; done

10
これは、変数を補間するだけで機能しますが、配列をaで使用したいfor場合、配列がundefined / defined-as-emptyの場合、ループ本体が必要になる場合があるため、単一の空の文字列になります。配列が定義されていない場合は実行されません。
Ash Berlin-Taylor

@AshBerlinのおかげで、読者が気づくようにforループを私の回答に追加しました
Jayen

このアプローチに-1は、それは単に間違っています。これは、空の配列を、同じではない単一の空の文字列に置き換えます。受け入れられた回答で提案されているパターンは${arr[@]+"${arr[@]}"}、空の配列の状態を正しく保持します。
dimo414

この拡張が失敗する状況を示す私の回答も参照してください。
dimo414

間違いではありません。空の文字列を与えると明示的に言っており、空の文字列を確認できる例が2つあります。
Jayen

7

@ikegamiの答えは正しいですが、構文は${arr[@]+"${arr[@]}"}恐ろしいと思います。長い配列変数名を使用すると、通常よりもスパゲッティっぽく見えるようになります。

代わりにこれを試してください:

$ set -u

$ count() { echo $# ; } ; count x y z
3

$ count() { echo $# ; } ; arr=() ; count "${arr[@]}"
-bash: abc[@]: unbound variable

$ count() { echo $# ; } ; arr=() ; count "${arr[@]:0}"
0

$ count() { echo $# ; } ; arr=(x y z) ; count "${arr[@]:0}"
3

Bash配列スライスオペレーターは非常に寛容であるように見えます。

では、なぜBashが配列のエッジケースの処理をそれほど難しくしたのでしょうか。 はぁ。 バージョンが配列スライス演算子のそのような乱用を許可することを保証することはできませんが、それは私にとってはうまくいきません。

警告:私はGNU bash, version 3.2.25(1)-release (x86_64-redhat-linux-gnu) あなたの走行距離を使用しています。


9
ikegamiには元々この機能がありましたが、理論的には(これが機能する理由はありません)、実際には(OPのバージョンのbashはそれを受け入れませんでした)信頼性が低いため、削除しました。

@hvd:更新ありがとうございます。読者:上記のコードが機能しないバージョンのbashを見つけたら、コメントを追加してください。
kevinarpe 2015

HVPすでにやった、と私はあまりにもあなたを教えてあげましょう:"${arr[@]:0}"与えます-bash: arr[@]: unbound variable
池上

バージョン間で機能するはずの1つは、デフォルトの配列値をに設定し、どこでもarr=("_dummy_")展開を使用する${arr[@]:1}ことです。これは、センチネル値を参照して、他の回答で言及されています。
init_js 2018年

1
@init_js:あなたの編集は悲しいことに拒否されました。別の回答として追加することをお勧めします。(参照:stackoverflow.com/review/suggested-edits/19027379
kevinarpe 2018年

6

「興味深い」矛盾は確かにあります。

さらに、

$ set -u
$ echo $#
0
$ echo "$1"
bash: $1: unbound variable   # makes sense (I didn't set any)
$ echo "$@" | cat -e
$                            # blank line, no error

現在の動作は@ikegamiが説明している意味でのバグではない可能性があることには同意しますが、IMOはバグが(「セット」の)定義自体にあるか、一貫性のない形で適用されているということになります。マニュアルページの前の段落は言う

... ${name[@]}nameの各要素を個別の単語に展開します。配列メンバーがない場合${name[@]}は、何も展開しません。

これは、での位置パラメータの拡張についての説明と完全に一致しています"$@"。配列と位置パラメータの動作に他の不整合がないわけではありませんが、私には、この詳細が2つの間で不整合であるべきであるというヒントはありません。

続けて、

$ arr=()
$ echo "${arr[@]}"
bash: arr[@]: unbound variable   # as we've observed.  BUT...
$ echo "${#arr[@]}"
0                                # no error
$ echo "${!arr[@]}" | cat -e
$                                # no error

そうarr[]ではありませんので、我々はその要素(0)の数、またはそのキーの(空)のリストを得ることができないことを結合していませんか?私にはこれらは賢明で便利です-唯一の外れ値は${arr[@]}(および${arr[*]})展開であるようです。


2

私は上の補完しています池上さん@(受け入れ)とkevinarpeの@(も良い)の回答。

あなた"${arr[@]:+${arr[@]}}"は問題を回避するために行うことができます。右側(つまりの後:+)は、左側が定義されていない/ nullの場合に使用される式を提供します。

構文は難解です。式の右側はパラメータ展開されるため、一貫した引用を行うことに特に注意を払う必要があることに注意してください。

: example copy arr into arr_copy
arr=( "1 2" "3" )
arr_copy=( "${arr[@]:+${arr[@]}}" ) # good. same quoting. 
                                    # preserves spaces

arr_copy=( ${arr[@]:+"${arr[@]}"} ) # bad. quoting only on RHS.
                                    # copy will have ["1","2","3"],
                                    # instead of ["1 2", "3"]

@kevinarpeの言及のように、難解度の低い構文は、配列スライス表記${arr[@]:0}(Bashバージョンの場合>= 4.4)を使用することです。これは、インデックス0から始まるすべてのパラメーターに展開されます。また、それほど多くの繰り返しを必要としません。この拡張はに関係なく機能するset -uため、いつでも使用できます。マニュアルページには次のように書かれています(パラメータ展開の下):

  • ${parameter:offset}

  • ${parameter:offset:length}

    ... parameterが@orを添え字とするインデックス付き配列名の場合*、結果はで始まる配列の長さメンバーです${parameter[offset]}。負のオフセットは、指定された配列の最大インデックスよりも大きいオフセットを基準にして取得されます。長さが0未満の数値に評価される場合、これは拡張エラーです。

これは、@ kevinarpeが提供する例であり、出力を証拠に配置するための代替フォーマットを使用しています。

set -u
function count() { echo $# ; };
(
    count x y z
)
: prints "3"

(
    arr=()
    count "${arr[@]}"
)
: prints "-bash: arr[@]: unbound variable"

(
    arr=()
    count "${arr[@]:0}"
)
: prints "0"

(
    arr=(x y z)
    count "${arr[@]:0}"
)
: prints "3"

この動作は、Bashのバージョンによって異なります。また、長さ演算子${#arr[@]}は、「バインドされていない変数エラー」を発生させることなく0、に関係なく常に空の配列に対して評価されることに気づいたかもしれませんset -u


残念ながら、:0イディオムはBash 4.2で失敗するため、これは安全な方法ではありません。私の答えをください。
dimo414

1

これは、このようなことを行うためのいくつかの方法です。1つはセンチネルを使用し、もう1つは条件付きアペンドを使用します。

#!/bin/bash
set -o nounset -o errexit -o pipefail
countArgs () { echo "$#"; }

arrA=( sentinel )
arrB=( sentinel "{1..5}" "./*" "with spaces" )
arrC=( sentinel '$PWD' )
cmnd=( countArgs "${arrA[@]:1}" "${arrB[@]:1}" "${arrC[@]:1}" )
echo "${cmnd[@]}"
"${cmnd[@]}"

arrA=( )
arrB=( "{1..5}" "./*"  "with spaces" )
arrC=( '$PWD' )
cmnd=( countArgs )
# Checks expansion of indices.
[[ ! ${!arrA[@]} ]] || cmnd+=( "${arrA[@]}" )
[[ ! ${!arrB[@]} ]] || cmnd+=( "${arrB[@]}" )
[[ ! ${!arrC[@]} ]] || cmnd+=( "${arrC[@]}" )
echo "${cmnd[@]}"
"${cmnd[@]}"

0

興味深い矛盾; これにより、「セットと見なされない」ものを定義することができますが、以下の出力に表示されます。declare -p

arr=()
set -o nounset
echo ${arr[@]}
 =>  -bash: arr[@]: unbound variable
declare -p arr
 =>  declare -a arr='()'

更新:他の人が述べたように、この回答が投稿された後にリリースされた4.4で修正されました。


これは配列の構文が間違っているだけです。必要ですecho ${arr[@]}(ただし、Bash 4.4より前のバージョンでは引き続きエラーが表示されます)。
dimo414

@ dimo414に感謝します。次回は、反対投票ではなく編集を提案します。ところで、echo $arr[@]自分で試した場合は、エラーメッセージが異なることがわかります。
MarcH

-2

最もシンプルで互換性のある方法は次のようです。

$ set -u
$ arr=()
$ echo "foo: '${arr[@]-}'"

1
OP自体は、これが機能しないことを示しました。何もないのではなく、空の文字列に展開されます。
池上

そう、文字列の補間は問題ありませんが、ループはできません。
クレイグリンガー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.