BASH in mảng kết hợp


16

Có cách nào để in toàn bộ một mảng ([key] = value) mà không lặp qua tất cả các phần tử không?

Giả sử tôi đã tạo một mảng với một số phần tử:

declare -A array
array=([a1]=1 [a2]=2 ... [b1]=bbb ... [f500]=abcdef)

Tôi có thể in lại toàn bộ mảng với

for i in "${!array[@]}"
do
echo "${i}=${array[$i]}"
done

Tuy nhiên, có vẻ như bash đã biết cách lấy tất cả các phần tử mảng trong một lần "đi" - cả khóa ${!array[@]}và giá trị ${array[@]}.

Có cách nào để làm bash in thông tin này mà không cần vòng lặp không?

Chỉnh sửa:
typeset -p arraylàm điều đó!
Tuy nhiên tôi không thể loại bỏ cả tiền tố và hậu tố trong một thay thế duy nhất:

a="$(typeset -p array)"
b="${a##*(}"
c="${b%% )*}"

Có cách nào sạch hơn để chỉ nhận / in phần key = value của đầu ra không?

Câu trả lời:


14

Tôi nghĩ bạn đang hỏi hai điều khác nhau ở đó.

Có cách nào để làm bash in thông tin này mà không cần vòng lặp không?

Có, nhưng chúng không tốt như chỉ sử dụng vòng lặp.

Có cách nào sạch hơn để chỉ nhận / in phần key = value của đầu ra không?

Vâng, forvòng lặp. Nó có những ưu điểm mà nó không yêu cầu các chương trình bên ngoài, đơn giản và giúp dễ dàng kiểm soát định dạng đầu ra chính xác mà không gây bất ngờ.


Bất kỳ giải pháp nào cố gắng xử lý đầu ra của declare -p( typeset -p) đều phải xử lý a) khả năng chính các biến chứa dấu ngoặc đơn hoặc dấu ngoặc, b) trích dẫn declare -pphải thêm vào để làm đầu ra hợp lệ cho đầu ra của nó.

Ví dụ: bản mở rộng của bạn b="${a##*(}"ăn một số giá trị, nếu bất kỳ khóa / giá trị nào chứa dấu ngoặc đơn mở. Điều này là do bạn đã sử dụng ##, loại bỏ tiền tố dài nhất . Tương tự cho c="${b%% )*}". Mặc dù tất nhiên bạn có thể khớp với bản soạn sẵn được in declarechính xác hơn, bạn vẫn sẽ gặp khó khăn nếu bạn không muốn tất cả các trích dẫn.

Điều này trông không đẹp lắm trừ khi bạn cần nó.

$ declare -A array=([abc]="'foobar'" [def]='"foo bar"')
$ declare -p array
declare -A array='([def]="\"foo bar\"" [abc]="'\''foobar'\''" )'

Với forvòng lặp, việc chọn định dạng đầu ra như bạn muốn sẽ dễ dàng hơn:

# without quoting
$ for x in "${!array[@]}"; do printf "[%s]=%s\n" "$x" "${array[$x]}" ; done
[def]="foo bar"
[abc]='foobar'

# with quoting
$ for x in "${!array[@]}"; do printf "[%q]=%q\n" "$x" "${array[$x]}" ; done
[def]=\"foo\ bar\"
[abc]=\'foobar\'

Từ đó, thật đơn giản để thay đổi định dạng đầu ra (loại bỏ các dấu ngoặc quanh khóa, đặt tất cả các cặp khóa / giá trị trên một dòng ...). Nếu bạn cần trích dẫn cho một cái gì đó không phải là chính nó, bạn vẫn sẽ cần phải tự làm điều đó, nhưng ít nhất bạn có dữ liệu thô để làm việc. (Nếu bạn có dòng mới trong các khóa hoặc giá trị, có lẽ bạn sẽ cần một số trích dẫn.)

Với một Bash hiện tại (4.4, tôi nghĩ), bạn cũng có thể sử dụng printf "[%s]=%s" "${x@Q}" "${array[$x]@Q}"thay vì printf "%q=%q". Nó tạo ra một định dạng trích dẫn có phần đẹp hơn, nhưng tất nhiên là phải nhớ thêm một chút công việc để viết. (Và nó trích dẫn trường hợp góc @là khóa mảng, %qkhông trích dẫn.)

Nếu vòng lặp for có vẻ quá mệt mỏi để viết, hãy lưu nó vào một hàm ở đâu đó (không trích dẫn ở đây):

printarr() { declare -n __p="$1"; for k in "${!__p[@]}"; do printf "%s=%s\n" "$k" "${__p[$k]}" ; done ;  }  

Và sau đó chỉ cần sử dụng:

$ declare -A a=([a]=123 [b]="foo bar" [c]="(blah)")
$ printarr a
a=123
b=foo bar
c=(blah)

Cũng hoạt động với các mảng được lập chỉ mục:

$ b=(abba acdc)
$ printarr b
0=abba
1=acdc

Lưu ý rằng đầu ra của printf ...%q...biến thể của bạn không phù hợp để cung cấp lại cho shell nếu mảng có @khóa là% q không trích dẫn nó và a=([@]=value)là một lỗi cú pháp bash.
Stéphane Chazelas

@ StéphaneChazelas, rõ ràng. "${x@Q}"trích dẫn đó, vì nó trích dẫn tất cả các chuỗi (và trông đẹp hơn). đã thêm một lưu ý về việc sử dụng đó.
ilkkachu

Vâng, được sao chép từ mksh. Một toán tử khác có hình dạng khác không thể kết hợp với hầu hết các hình khác. Một lần nữa, hãy xem zshvới các cờ mở rộng biến đổi của nó (một lần nữa trước bash's hàng thập kỷ và bạn có thể chọn kiểu trích dẫn: $ {(q) var}, $ {(qq) var} ...) để có thiết kế tốt hơn. bash có cùng một vấn đề với mksh ở chỗ nó không trích dẫn chuỗi trống (không phải là vấn đề ở đây vì dù sao bash không hỗ trợ các khóa trống). Ngoài ra, khi sử dụng trích dẫn phong cách khác hơn là dấu nháy đơn ( ${var@Q}khu nghỉ mát để $'...'cho một số giá trị) điều quan trọng là các mã được reinput trong miền địa phương cùng.
Stéphane Chazelas

@ StéphaneChazelas, tôi nghĩ bạn có nghĩa là một giá trị chưa đặt, không phải là một chuỗi rỗng? ( x=; echo "${x@Q}"không cho '', unset x; echo "${x@Q}"không cho gì.) Bash @Qdường như thích $'\n'một dòng mới theo nghĩa đen, điều này thực sự có thể tốt trong một số tình huống (nhưng tôi không thể nói những gì người khác thích). Tất nhiên có một sự lựa chọn sẽ không có xấu.
ilkkachu

Ồ vâng xin lỗi, tôi đã không nhận ra điều đó. Đó là một sự khác biệt từ mksh. Các $'...'cú pháp là một vấn đề tiềm ẩn trong những thứ như LC_ALL=zh_HK.big5hkscs bash -c 'a=$'\''\n\u3b1'\''; printf "%s\n" "${a@Q}"'mà kết quả đầu ra $'\n<0xa3><0x5c>'0x5cmột mình là dấu chéo ngược, do đó bạn sẽ có một vấn đề nếu trích đoạn mà đã được giải thích trong một miền địa phương khác nhau.
Stéphane Chazelas

9
declare -p array
declare -A array='([a2]="2" [a1]="1" [zz]="Hello World" [b1]="bbb" [f50]="abcd" )'

2 ngã ba

Có lẽ điều này:

printf "%s\n" "${!array[@]}"
a2
a1
f50
zz
b1

printf "%s\n" "${array[@]}"
2
1
abcd
Hello World
bbb

printf "%s\n" "${!array[@]}" "${array[@]}" | pr -2t
a2                              2
a1                              1
f50                             abcd
zz                              Hello World
b1                              bbb

3 dĩa

hoặc này:

paste -d= <(printf "%s\n" "${!array[@]}") <(printf "%s\n" "${array[@]}")
a2=2
a1=1
f50=abcd
zz=Hello World
b1=bbb

Không có ngã ba

được so sánh với

for i in "${!array[@]}";do printf "%s=%s\n" "$i" "${array[$i]}";done
a2=2
a1=1
f50=abcd
zz=Hello World
b1=bbb

Thời gian thực hiện so sánh

Vì cú pháp cuối cùng không sử dụng fork, chúng có thể nhanh hơn:

time printf "%s\n" "${!array[@]}" "${array[@]}" | pr -2t | wc
      5      11      76
real    0m0.005s
user    0m0.000s
sys     0m0.000s

time paste -d= <(printf "%s\n" "${!array[@]}") <(printf "%s\n" "${array[@]}") | wc
      5       6      41
real    0m0.008s
user    0m0.000s
sys     0m0.000s

time for i in "${!array[@]}";do printf "%s=%s\n" "$i" "${array[$i]}";done | wc
      5       6      41
real    0m0.002s
user    0m0.000s
sys     0m0.001s

Nhưng sự khẳng định này không đúng nếu mảng trở nên lớn; nếu giảm dĩa là hiệu quả cho quy trình nhỏ, sử dụng các công cụ chuyên dụng sẽ hiệu quả hơn cho quy trình lớn hơn.

for i in {a..z}{a..z}{a..z};do array[$i]=$RANDOM;done


time printf "%s\n" "${!array[@]}" "${array[@]}" | pr -2t | wc
  17581   35163  292941
real    0m0.150s
user    0m0.124s
sys     0m0.036s

time paste -d= <(printf "%s\n" "${!array[@]}") <(printf "%s\n" "${array[@]}") | wc
  17581   17582  169875
real    0m0.140s
user    0m0.000s
sys     0m0.004s

time for i in "${!array[@]}";do printf "%s=%s\n" "$i" "${array[$i]}";done | wc
  17581   17582  169875
real    0m0.312s
user    0m0.268s
sys     0m0.076s

Ghi chú

Vì cả hai giải pháp ( rẽ nhánh ) đều sử dụng căn chỉnh , không có giải pháp nào trong số chúng sẽ hoạt động nếu có bất kỳ biến nào chứa dòng mới . Trong trường hợp này, cách duy nhất là một forvòng lặp.


Trong khi nhìn thông minh, cả hai cách đều kém hiệu quả hơn a for. Đó là một sự xấu hổ, thực sự.
Satō Katsura

@SatoKatsura Tôi đồng ý, nhưng nếu chậm hơn, việc sử dụng cú pháp prngắn hơn ... Tôi không chắc về prcú pháp chậm hơn, ngay cả với các mảng lớn!
F. Hauri

2
@MiniMax Bởi vì nó không tạo ra kết quả chính xác (cùng các yếu tố, thứ tự sai). Bạn cần phải nén các mảng ${!array[@]}${array[@]}trước tiên để nó hoạt động.
Satō Katsura

1
Đó là đoạn cuối cùng với pastecòn hơn forvòng lặp trong câu hỏi bằng văn bản trên cùng một dòng for i in "${!array[@]}"; do echo "$i=${array[$i]}" ; done, nhưng đòi hỏi hai subshells và một chương trình bên ngoài. Làm thế nào là gọn gàng hơn? Giải pháp prcũng phá vỡ nếu có nhiều yếu tố, vì nó cố gắng phân trang đầu ra. Bạn cần sử dụng một cái gì đó giống như | pr -2t -l"${#array[@]}"bắt đầu khó nhớ so với vòng lặp đơn giản, và một lần nữa, dài hơn nó.
ilkkachu

1
Trong bash, cmd1 | cmd2có nghĩa là 2 nhánh, ngay cả khi cmd1 hoặc cmd2 hoặc cả hai đều được dựng sẵn.
Stéphane Chazelas

2

Nếu bạn đang tìm kiếm một shell với sự hỗ trợ mảng kết hợp tốt hơn, hãy thử zsh.

Trong zsh(nơi các mảng kết hợp đã được thêm vào năm 1998, so với năm 1993 cho ksh93 và 2009 cho bash) $varhoặc ${(v)var}mở rộng đến các giá trị (không trống) của hàm băm, ${(k)var}cho các khóa (không trống) (theo cùng thứ tự), và ${(kv)var}cho cả khóa và giá trị.

Để duy trì các giá trị trống, như đối với mảng, bạn cần trích dẫn và sử dụng @cờ.

Vì vậy, để in các khóa và giá trị, đó chỉ là vấn đề

printf '%s => %s\n' "${(@kv)var}"

Mặc dù để tính đến một hàm băm trống có thể, bạn nên làm:

(($#var)) &&  printf '%s => %s\n' "${(@kv)var}"

Cũng lưu ý rằng zsh sử dụng cú pháp định nghĩa mảng hữu ích và hợp lý hơn nhiều so với ksh93(được sao chép bởi bash):

typeset -A var
var=(k1 v1 k2 v2 '' empty '*' star)

Điều này làm cho việc sao chép hoặc hợp nhất các mảng kết hợp dễ dàng hơn nhiều:

var2=("${(@kv)var1}")
var3+=("${(@kv)var2}")
var4=("${@kv)var4}" "${(@kv)var5}")

(bạn không thể dễ dàng sao chép hàm băm mà không có vòng lặp bashvà lưu ý rằng bashhiện tại không hỗ trợ khóa trống hoặc khóa / giá trị với byte NUL).

Xem thêm zshcác tính năng nén mảng mà bạn thường cần để làm việc với các mảng kết hợp:

keys=($(<keys.txt)) values=($(<values.txt))
hash=(${keys:^values})

1

Vì kiểu sắp xếp những gì bạn muốn tại sao không chỉ chỉnh sửa đầu ra của nó?

typeset -p array | sed s/^.*\(// | tr -d ")\'\""  | tr "[" "\n" | sed s/]=/' = '/

cho

a2 = 2  
a1 = 1  
b1 = bbb 

Ở đâu

array='([a2]="2" [a1]="1" [b1]="bbb" )'

Verbose nhưng khá dễ dàng để xem cách định dạng hoạt động: chỉ cần thực hiện đường ống với nhiều lệnh sedtr hơn . Sửa đổi chúng cho phù hợp với thị hiếu in ấn đẹp.


Loại đường ống đó chắc chắn sẽ thất bại trong thời điểm một số khóa hoặc giá trị của mảng chứa bất kỳ ký tự nào bạn thay thế, như dấu ngoặc đơn, dấu ngoặc hoặc dấu ngoặc kép. Và một đường dẫn của seds và tr'thậm chí không đơn giản hơn nhiều so với một forvòng lặp với printf.
ilkkachu

Ngoài ra, bạn có biết rằng trdịch từng ký tự, nó không khớp với chuỗi? tr "]=" " ="thay đổi "]" thành một khoảng trắng và =thành một =, bất kể vị trí. Vì vậy, bạn có thể chỉ cần kết hợp cả ba trthành một.
ilkkachu

Rất đúng về một số các ký tự không chữ và số đang gồng mình lên. Tuy nhiên, bất cứ điều gì phải giải quyết chúng đều có thứ tự phức tạp hơn và ít đọc hơn, trừ khi có lý do thực sự tốt để đưa chúng vào nguồn cấp dữ liệu của bạn và điều đó được nêu trong câu hỏi tôi cho rằng chúng đã được lọc ra trước khi chúng tôi đến đây. Nên luôn luôn có cảnh báo rõ ràng của bạn tho. Tôi thấy các đường ống này đơn giản hơn, ví dụ và mục đích gỡ lỗi, hơn là một bản in printf hoạt động hoàn hảo hoặc thổi vào mặt bạn. Tại đây bạn thực hiện một thay đổi đơn giản cho mỗi phần tử, kiểm tra nó, sau đó thêm 1 thay đổi.
Nadreck

Lỗi của tôi! Có _tr_s và _sed_s của tôi hoàn toàn lẫn lộn! Đã sửa trong bản chỉnh sửa mới nhất.
Nadreck

1

Thêm một lựa chọn nữa là liệt kê tất cả các biến và grep cho biến bạn muốn.

set | grep -e '^aa='

Tôi sử dụng điều này để gỡ lỗi. Tôi nghi ngờ rằng nó rất hiệu quả vì nó liệt kê tất cả các biến.

Nếu bạn đang làm điều này thường xuyên, bạn có thể làm cho nó một chức năng như thế này:

aap() { set | grep -e "^$1="; }

Thật không may khi chúng tôi kiểm tra hiệu suất bằng thời gian:

$ time aap aa aa=([0]="abc") . real 0m0.014s user 0m0.003s sys 0m0.006s

Do đó, nếu bạn thường xuyên làm điều này, bạn sẽ muốn phiên bản KHÔNG GIỚI HẠN của @ F.Hauri vì nó nhanh hơn rất nhiều.

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.