Bash mở rộng mảng trống với `set -u`


103

Tôi đang viết một tập lệnh bash có set -uvà tôi gặp sự cố với việc mở rộng mảng trống: bash dường như coi một mảng trống là một biến chưa được đặt trong quá trình mở rộng:

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

( declare -a arrcũng không giúp được gì.)

Một giải pháp phổ biến cho điều này là sử dụng ${arr[@]-}thay thế, do đó thay thế một chuỗi trống thay vì mảng trống ("không xác định"). Tuy nhiên, đây không phải là một giải pháp tốt, vì bây giờ bạn không thể phân biệt giữa một mảng có một chuỗi rỗng trong đó và một mảng trống. (@ -expansion là đặc biệt trong bash, nó mở rộng "${arr[@]}"thành "${arr[0]}" "${arr[1]}" …, làm cho nó trở thành một công cụ hoàn hảo để xây dựng các dòng lệnh.)

$ 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

Vì vậy, có cách nào để giải quyết vấn đề đó, ngoài việc kiểm tra độ dài của một mảng trong một if(xem mẫu mã bên dưới), hoặc tắt -ucài đặt cho đoạn ngắn đó?

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

Cập nhật: Đã xóa bugsthẻ do ikegami giải thích.

Câu trả lời:


17

Thành ngữ an toàn duy nhất${arr[@]+"${arr[@]}"}

Đây đã là đề xuất trong câu trả lời của ikegami , nhưng có rất nhiều thông tin sai lệch và phỏng đoán trong chủ đề này. Các mẫu khác, chẳng hạn như ${arr[@]-}hoặc ${arr[@]:0}, không an toàn trên tất cả các phiên bản chính của Bash.

Như bảng bên dưới cho thấy, phần mở rộng duy nhất đáng tin cậy trên tất cả các phiên bản Bash hiện đại là ${arr[@]+"${arr[@]}"}(cột +"). Lưu ý, một số mở rộng khác không thành công trong Bash 4.2, bao gồm (không may) ${arr[@]:0}thành ngữ ngắn hơn , không chỉ tạo ra kết quả không chính xác mà còn thực sự thất bại. Nếu bạn cần hỗ trợ các phiên bản trước 4.4, và cụ thể là 4.2, đây là thành ngữ duy nhất hoạt động.

Ảnh chụp màn hình các thành ngữ khác nhau giữa các phiên bản

Thật không may, các bản +mở rộng khác , trong nháy mắt, trông giống nhau thực sự phát ra hành vi khác nhau. :+mở rộng không an toàn, bởi vì :-expansion xử lý một mảng có một phần tử trống ( ('')) là "null" và do đó không (nhất quán) mở rộng cho cùng một kết quả.

Trích dẫn mở rộng đầy đủ thay vì mảng ( "${arr[@]+${arr[@]}}") lồng nhau , mà tôi mong đợi là gần tương đương, tương tự không an toàn trong 4.2.

Bạn có thể xem mã đã tạo dữ liệu này cùng với kết quả cho một số phiên bản bash bổ sung trong ý chính này .


1
Tôi không thấy bạn thử nghiệm "${arr[@]}". Tui bỏ lỡ điều gì vậy? Từ những gì tôi có thể thấy nó hoạt động ít nhất trong 5.x.
x-yuri

1
@ x-yuri vâng, Bash 4.4 đã khắc phục tình hình; bạn không cần sử dụng mẫu này nếu bạn biết tập lệnh của mình sẽ chỉ chạy trên 4.4+, nhưng nhiều hệ thống vẫn chạy trên các phiên bản trước đó.
dimo414

Chắc chắn rồi. Mặc dù trông đẹp mắt (ví dụ như định dạng), khoảng trắng thừa là tệ nạn lớn của bash, gây ra rất nhiều rắc rối
agg3l

81

Theo tài liệu,

Một biến mảng được coi là được đặt nếu một chỉ số con đã được gán một giá trị. Chuỗi null là một giá trị hợp lệ.

Không có chỉ số con nào được gán giá trị, vì vậy mảng không được đặt.

Nhưng trong khi tài liệu cho thấy một lỗi là phù hợp ở đây, thì điều này không còn xảy ra kể từ 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: ''

Có một điều kiện bạn có thể sử dụng nội tuyến để đạt được những gì bạn muốn trong các phiên bản cũ hơn: Sử dụng ${arr[@]+"${arr[@]}"}thay vì "${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

Đã thử nghiệm với bash 4.2.25 và 4.3.11.


4
Bất cứ ai có thể giải thích làm thế nào và tại sao điều này hoạt động? Tôi bối rối về những gì [@]+thực sự làm và tại sao thứ hai ${arr[@]}sẽ không gây ra lỗi không liên kết.
Martin von Wittich

2
${parameter+word}chỉ mở rộng wordnếu parameterkhông được đặt.
ikegami

2
${arr+"${arr[@]}"}ngắn hơn và dường như hoạt động tốt.
Per Cederberg

3
@Per Cerderberg, Không hoạt động. unset arr, arr[1]=a, args ${arr+"${arr[@]}"}Vsargs ${arr[@]+"${arr[@]}"}
Ikegami

1
Nói một cách chính xác, trong trường hợp việc +mở rộng không xảy ra (cụ thể là một mảng trống) thì phần mở rộng được thay thế bằng không , đó chính xác là những gì một mảng trống mở rộng thành. :+là không an toàn vì nó cũng coi ('')mảng một phần tử là chưa được đặt và tương tự mở rộng thành không, làm mất giá trị.
dimo414

23

Câu trả lời được chấp nhận của @ ikegami là sai một cách tinh vi! Câu thần chú đúng là ${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...

Không còn tạo ra sự khác biệt. bash-4.4.23: arr=('') && countArgs "${arr[@]:+${arr[@]}}"sản xuất 1. Nhưng ${arr[@]+"${arr[@]}"}biểu mẫu cho phép phân biệt giữa giá trị trống / không rỗng bằng cách thêm / không thêm dấu hai chấm.
x-yuri

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

1
Điều này đã được sửa trong câu trả lời của tôi từ lâu. (Trên thực tế, tôi chắc chắn trước đây tôi đã để lại nhận xét về câu trả lời này cho hiệu ứng đó ?!)
ikegami 19/09/19

16

Hóa ra việc xử lý mảng đã được thay đổi trong bản phát hành gần đây (2016/09/16) bash 4.4 (ví dụ: có sẵn trong phiên bản Debian).

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

Giờ đây, việc mở rộng mảng trống không phát ra cảnh báo

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

$ # everything is fine

Tôi có thể xác nhận, với bash-4.4.12 "${arr[@]}"đủ.
x-yuri

14

đây có thể là một tùy chọn khác cho những người không muốn sao chép arr [@] và có thể có một chuỗi trống

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

để kiểm tra:

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
Điều này sẽ hoạt động nếu bạn chỉ nội suy biến, nhưng nếu bạn muốn sử dụng mảng trong một biến fornày sẽ kết thúc với một chuỗi trống duy nhất khi mảng là không xác định / được xác định là trống, nơi bạn có thể muốn phần thân của vòng lặp không chạy nếu mảng không được xác định.
Ash Berlin-Taylor

cảm ơn @AshBerlin, tôi đã thêm vòng lặp for vào câu trả lời của mình để người đọc biết
Jayen

-1 đối với cách tiếp cận này, nó chỉ đơn giản là không chính xác. Điều này thay thế một mảng trống bằng một chuỗi trống duy nhất, không giống nhau. Mẫu được đề xuất trong câu trả lời được chấp nhận ${arr[@]+"${arr[@]}"}, duy trì chính xác trạng thái mảng trống.
dimo414

Xem thêm câu trả lời của tôi cho thấy các tình huống mà sự mở rộng này bị hỏng.
dimo414

nó không sai. nó nói rõ ràng rằng nó sẽ đưa ra một chuỗi trống và thậm chí có hai ví dụ mà bạn có thể thấy chuỗi trống.
Jayen

7

Câu trả lời của @ ikegami là đúng, nhưng tôi coi cú pháp là ${arr[@]+"${arr[@]}"}khủng khiếp. Nếu bạn sử dụng các tên biến mảng dài, nó sẽ bắt đầu trông nhanh hơn bình thường.

Hãy thử cái này thay thế:

$ 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

Có vẻ như toán tử lát mảng Bash rất dễ bị lỗi.

Vậy tại sao Bash lại làm cho việc xử lý trường hợp cạnh của mảng trở nên khó khăn như vậy? Thở dài. Tôi không thể đảm bảo rằng phiên bản của bạn sẽ cho phép lạm dụng toán tử lát mảng như vậy, nhưng nó hoạt động tốt đối với tôi.

Lưu ý: Tôi đang sử dụng Số GNU bash, version 3.2.25(1)-release (x86_64-redhat-linux-gnu) dặm của bạn có thể thay đổi.


9
ikegami ban đầu có điều này, nhưng đã loại bỏ nó vì nó không đáng tin cậy, cả về lý thuyết (không có lý do gì khiến điều này hoạt động) và trong thực tế (phiên bản bash của OP không chấp nhận nó).

@hvd: Cảm ơn bạn đã cập nhật. Người đọc: Vui lòng thêm nhận xét nếu bạn tìm thấy các phiên bản của bash mà mã trên không hoạt động.
kevinarpe

hvp đã làm, và tôi cũng sẽ nói với bạn: "${arr[@]:0}"cho -bash: arr[@]: unbound variable.
ikegami

Một điều sẽ hoạt động trên các phiên bản là đặt giá trị mảng mặc định thành arr=("_dummy_")và sử dụng phần mở rộng ${arr[@]:1}ở mọi nơi. Điều này được đề cập trong các câu trả lời khác, đề cập đến các giá trị sentinel.
init_js

1
@init_js: Đáng tiếc, chỉnh sửa của bạn đã bị từ chối. Tôi đề nghị bạn thêm như một câu trả lời riêng biệt. (Tham khảo: stackoverflow.com/review/suggested-edits/19027379 )
kevinarpe

6

Sự mâu thuẫn "thú vị" thực sự.

Hơn nữa,

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

Mặc dù tôi đồng ý rằng hành vi hiện tại có thể không phải là lỗi theo nghĩa mà @ikegami giải thích, IMO chúng tôi có thể nói rằng lỗi nằm trong chính định nghĩa (của "set") và / hoặc thực tế là nó được áp dụng không nhất quán. Đoạn trước trong trang người đàn ông nói

... ${name[@]}mở rộng từng thành phần của tên thành một từ riêng biệt. Khi không có thành viên mảng, ${name[@]}mở rộng thành không.

điều này hoàn toàn phù hợp với những gì nó nói về việc mở rộng các tham số vị trí trong "$@". Không phải là không có sự mâu thuẫn nào khác trong các hành vi của mảng và các tham số vị trí ... nhưng đối với tôi không có gợi ý rằng chi tiết này nên không nhất quán giữa hai thứ.

Tiếp tục,

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

Vì vậy, arr[]không nên cởi ra rằng chúng ta không thể có được một số phần tử của nó (0), hoặc một (trống) danh sách các phím của nó? Đối với tôi, những điều này là hợp lý và hữu ích - ngoại lệ duy nhất dường như là ${arr[@]}(và ${arr[*]}) mở rộng.


2

Tôi bổ sung trên @ Ikegami của (chấp nhận) và @ kevinarpe của câu trả lời (cũng tốt).

Bạn có thể làm gì "${arr[@]:+${arr[@]}}"để giải quyết vấn đề. Phía bên phải (tức là, sau :+) cung cấp một biểu thức sẽ được sử dụng trong trường hợp phía bên trái không được xác định / null.

Cú pháp rất phức tạp. Lưu ý rằng phía bên phải của biểu thức sẽ trải qua quá trình mở rộng tham số, vì vậy cần chú ý thêm để có trích dẫn nhất quán.

: 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"]

Giống như @kevinarpe đã đề cập, một cú pháp ít phức tạp hơn là sử dụng ký hiệu lát mảng ${arr[@]:0}(trên các phiên bản Bash >= 4.4), mở rộng đến tất cả các tham số, bắt đầu từ chỉ số 0. Nó cũng không yêu cầu lặp lại nhiều. Phần mở rộng này hoạt động bất kể set -u, vì vậy bạn có thể sử dụng phần mềm này mọi lúc. Trang người đàn ông cho biết (trong Mở rộng tham số ):

  • ${parameter:offset}

  • ${parameter:offset:length}

    ... Nếu tham số là một tên mảng được lập chỉ mục được ký hiệu bằng @hoặc *, kết quả là các thành viên độ dài của mảng bắt đầu bằng ${parameter[offset]}. Phần bù âm được lấy liên quan đến một phần lớn hơn chỉ số tối đa của mảng được chỉ định. Đó là lỗi mở rộng nếu độ dài đánh giá là một số nhỏ hơn 0.

Đây là ví dụ do @kevinarpe cung cấp, với định dạng thay thế để đặt đầu ra làm bằng chứng:

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"

Hành vi này thay đổi theo các phiên bản của Bash. Bạn cũng có thể nhận thấy rằng toán tử độ dài ${#arr[@]}sẽ luôn đánh giá đối 0với các mảng trống, bất kể set -u, mà không gây ra 'lỗi biến không liên kết'.


Thật không may, :0thành ngữ không thành công trong Bash 4.2, vì vậy đây không phải là một cách tiếp cận an toàn. Hãy xem câu trả lời của tôi .
dimo414

1

Dưới đây là một số cách để làm điều gì đó như thế này, một cách sử dụng lính canh và một cách khác sử dụng phần bổ sung có điều kiện:

#!/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

Sự mâu thuẫn thú vị; điều này cho phép bạn xác định một cái gì đó "chưa được coi là thiết lập" chưa hiển thị trong đầu ra củadeclare -p

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

CẬP NHẬT: như những người khác đã đề cập, đã sửa trong 4.4 phát hành sau khi câu trả lời này được đăng.


Đó chỉ là cú pháp mảng không chính xác; bạn cần echo ${arr[@]}(nhưng trước Bash 4.4, bạn vẫn sẽ thấy lỗi).
dimo414

Cảm ơn @ dimo414, lần sau hãy đề xuất chỉnh sửa thay vì phản đối. BTW nếu bạn đã echo $arr[@]tự mình thử, bạn sẽ thấy rằng thông báo lỗi là khác nhau.
MarcH

-2

Cách đơn giản và tương thích nhất dường như là:

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

1
Chính OP đã cho thấy rằng điều này không hiệu quả. Nó mở rộng thành một chuỗi rỗng thay vì không có gì.
ikegami

Đúng, vậy là OK cho nội suy chuỗi nhưng không lặp lại.
Craig Ringer
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.