Tên biến động trong Bash


159

Tôi bối rối về một kịch bản bash.

Tôi có đoạn mã sau:

function grep_search() {
    magic_way_to_define_magic_variable_$1=`ls | tail -1`
    echo $magic_variable_$1
}

Tôi muốn có thể tạo một tên biến chứa đối số đầu tiên của lệnh và mang giá trị ví dụ như dòng cuối cùng của ls.

Vì vậy, để minh họa những gì tôi muốn:

$ ls | tail -1
stack-overflow.txt

$ grep_search() open_box
stack-overflow.txt

Vì vậy, tôi nên xác định / khai báo $magic_way_to_define_magic_variable_$1như thế nào và tôi nên gọi nó như thế nào trong tập lệnh?

Tôi đã cố gắng eval, ${...}, \$${...}, nhưng tôi vẫn còn nhầm lẫn.


3
Đừng. Sử dụng một mảng kết hợp để ánh xạ tên lệnh vào dữ liệu.
chepner

3
VAR = A; VAL = 333; đọc "$ VAR" <<< "$ VAL"; tiếng vang "A = $ A"
Grigory K

Câu trả lời:


150

Sử dụng một mảng kết hợp, với tên lệnh làm khóa.

# Requires bash 4, though
declare -A magic_variable=()

function grep_search() {
    magic_variable[$1]=$( ls | tail -1 )
    echo ${magic_variable[$1]}
}

Nếu bạn không thể sử dụng mảng kết hợp (ví dụ: bạn phải hỗ trợ bash3), bạn có thể sử dụng declaređể tạo tên biến động:

declare "magic_variable_$1=$(ls | tail -1)"

và sử dụng mở rộng tham số gián tiếp để truy cập giá trị.

var="magic_variable_$1"
echo "${!var}"

Xem BashFAQ: Indirection - Đánh giá các biến gián tiếp / tham chiếu .


5
@DeaDEnD -akhai báo một mảng được lập chỉ mục, không phải là một mảng kết hợp. Trừ khi đối số grep_searchlà một số, nó sẽ được coi là một tham số có giá trị số (mặc định là 0 nếu tham số không được đặt).
chepner

1
Hừm. Tôi đang sử dụng bash 4.2.45(2)và tuyên bố không liệt kê nó như là một tùy chọn declare: usage: declare [-afFirtx] [-p] [name[=value] ...]. Nó dường như đang hoạt động chính xác.
13 lúc 15:30

declare -htrong 4.2,45 (2) cho tôi thấy declare: usage: declare [-aAfFgilrtux] [-p] [name[=value] ...]. Bạn có thể kiểm tra kỹ xem bạn có thực sự đang chạy 4.x chứ không phải 3.2.
chepner

5
Tại sao không chỉ declare $varname="foo"?
Ben Davis

1
${!varname}đơn giản hơn nhiều và tương thích rộng rãi
Brad Hein

227

Tôi đã tìm kiếm cách tốt hơn để làm điều đó gần đây. Mảng liên kết nghe có vẻ quá mức đối với tôi. Hãy nhìn những gì tôi tìm thấy:

suffix=bzz
declare prefix_$suffix=mystr

...và sau đó...

varname=prefix_$suffix
echo ${!varname}

Nếu muốn khai báo toàn cục bên trong hàm, có thể sử dụng "khai báo -g" trong bash> = 4.2. Trong bash trước đó, có thể sử dụng "chỉ đọc" thay vì "khai báo", miễn là bạn không muốn thay đổi giá trị sau này. Có thể ổn cho cấu hình hoặc những gì có bạn.
Sam Watkins

7
tốt nhất để sử dụng định dạng biến được đóng gói: prefix_${middle}_postfix(nghĩa là định dạng của bạn sẽ không hoạt động varname=$prefix_suffix)
msciwoj

1
Tôi đã bị mắc kẹt với bash 3 và không thể sử dụng các mảng kết hợp; như vậy đây là một sự cứu rỗi $ {! ...} không dễ để google trên đó. Tôi giả sử nó chỉ mở rộng một tên var.
Neil McGill

10
@NeilMcGill: Xem "man bash" gnu.org/software/bash/manual/html_node/ tựa : Hình thức mở rộng tham số cơ bản là $ {tham số}. <...> Nếu ký tự đầu tiên của tham số là dấu chấm than (!), Một mức độ cảm ứng biến được đưa ra. Bash sử dụng giá trị của biến được hình thành từ phần còn lại của tham số làm tên của biến; biến này sau đó được mở rộng và giá trị đó được sử dụng trong phần còn lại của sự thay thế, thay vì giá trị của chính tham số.
Yorik.sar

1
@syntaxerror: bạn có thể gán các giá trị bao nhiêu tùy ý bằng lệnh "khai báo" ở trên.
Yorik.sar

48

Ngoài các mảng kết hợp, có một số cách để đạt được các biến động trong Bash. Lưu ý rằng tất cả các kỹ thuật này có rủi ro, được thảo luận ở phần cuối của câu trả lời này.

Trong các ví dụ sau tôi sẽ giả sử rằng i=37và bạn muốn đặt bí danh cho biến có tên var_37giá trị ban đầu là lolilol.

Phương pháp 1. Sử dụng biến con trỏ của người dùng

Bạn có thể chỉ cần lưu trữ tên của biến trong một biến không xác định, không giống như một con trỏ C. Bash sau đó có một cú pháp để đọc biến bí danh: ${!name}mở rộng đến giá trị của biến có tên là giá trị của biến name. Bạn có thể nghĩ về nó như một bản mở rộng hai giai đoạn: ${!name}mở rộng tới $var_37, mở rộng ra lolilol.

name="var_$i"
echo "$name"         # outputs “var_37”
echo "${!name}"      # outputs “lolilol”
echo "${!name%lol}"  # outputs “loli”
# etc.

Thật không may, không có cú pháp đối tác để sửa đổi biến bí danh. Thay vào đó, bạn có thể đạt được sự phân công với một trong những thủ thuật sau.

1a. Chỉ định vớieval

evallà xấu xa, nhưng cũng là cách đơn giản và dễ mang theo nhất để đạt được mục tiêu của chúng tôi. Bạn phải cẩn thận thoát khỏi phía bên phải của bài tập, vì nó sẽ được đánh giá hai lần . Một cách dễ dàng và có hệ thống để làm điều này là đánh giá phía bên tay phải trước (hoặc sử dụng printf %q).

bạn nên kiểm tra thủ công rằng phía bên trái là tên biến hợp lệ hoặc tên có chỉ mục (nếu đó là evil_code #gì?). Ngược lại, tất cả các phương pháp khác dưới đây thực thi nó tự động.

# check that name is a valid variable name:
# note: this code does not support variable_name[index]
shopt -s globasciiranges
[[ "$name" == [a-zA-Z_]*([a-zA-Z_0-9]) ]] || exit

value='babibab'
eval "$name"='$value'  # carefully escape the right-hand side!
echo "$var_37"  # outputs “babibab”

Nhược điểm:

  • không kiểm tra tính hợp lệ của tên biến.
  • eval là xấu xa
  • eval là xấu xa
  • eval là xấu xa

1b. Chỉ định vớiread

Nội dung readcho phép bạn gán giá trị cho một biến mà bạn đặt tên, một thực tế có thể được khai thác cùng với chuỗi ở đây:

IFS= read -r -d '' "$name" <<< 'babibab'
echo "$var_37"  # outputs “babibab\n”

Phần IFSvà tùy chọn -rđảm bảo rằng giá trị được gán nguyên trạng, trong khi tùy chọn -d ''cho phép gán giá trị nhiều dòng. Do tùy chọn cuối cùng này, lệnh trả về với mã thoát khác không.

Lưu ý rằng, vì chúng tôi đang sử dụng chuỗi ở đây, một ký tự dòng mới được thêm vào giá trị.

Nhược điểm:

  • hơi mơ hồ;
  • trả về với mã thoát khác không;
  • nối thêm một dòng mới vào giá trị

1c. Chỉ định vớiprintf

Kể từ Bash 3.1 (phát hành năm 2005), printfnội dung cũng có thể gán kết quả của nó cho một biến có tên được đặt. Ngược lại với các giải pháp trước đó, nó chỉ hoạt động, không cần nỗ lực thêm để thoát khỏi mọi thứ, để ngăn chặn sự chia tách và như vậy.

printf -v "$name" '%s' 'babibab'
echo "$var_37"  # outputs “babibab”

Nhược điểm:

  • Ít di động (nhưng, tốt).

Phương pháp 2. Sử dụng biến tham chiếu của người Viking

Kể từ Bash 4.3 (phát hành năm 2014), declarenội dung dựng sẵn có một tùy chọn -nđể tạo một biến là tham chiếu tên của tên Cameron đến một biến khác, giống như các tham chiếu C ++. Giống như trong Phương pháp 1, tham chiếu lưu tên của biến bí danh, nhưng mỗi lần tham chiếu được truy cập (để đọc hoặc gán), Bash sẽ tự động giải quyết vấn đề.

Ngoài ra, Bash có một cú pháp đặc biệt và rất khó hiểu để có được giá trị của chính tham chiếu, hãy tự đánh giá : ${!ref}.

declare -n ref="var_$i"
echo "${!ref}"  # outputs “var_37”
echo "$ref"     # outputs “lolilol”
ref='babibab'
echo "$var_37"  # outputs “babibab”

Điều này không tránh được những cạm bẫy được giải thích dưới đây, nhưng ít nhất nó làm cho cú pháp đơn giản.

Nhược điểm:

  • Không xách tay.

Rủi ro

Tất cả các kỹ thuật răng cưa này có một số rủi ro. Cái đầu tiên là thực thi mã tùy ý mỗi khi bạn giải quyết được chỉ định (để đọc hoặc để gán) . Thật vậy, thay vì một tên biến vô hướng, như var_37, bạn cũng có thể đặt bí danh cho một mảng con, như arr[42]. Nhưng Bash đánh giá nội dung của dấu ngoặc vuông mỗi khi cần, do đó, việc khử răng cưa arr[$(do_evil)]sẽ có tác dụng không mong muốn. Do đó, chỉ sử dụng các kỹ thuật này khi bạn kiểm soát nguồn gốc của bí danh .

function guillemots() {
  declare -n var="$1"
  var="«${var}»"
}

arr=( aaa bbb ccc )
guillemots 'arr[1]'  # modifies the second cell of the array, as expected
guillemots 'arr[$(date>>date.out)1]'  # writes twice into date.out
            # (once when expanding var, once when assigning to it)

Rủi ro thứ hai là tạo ra một bí danh tuần hoàn. Vì các biến Bash được xác định bằng tên của chúng chứ không phải theo phạm vi của chúng, bạn có thể vô tình tạo một bí danh cho chính nó (trong khi nghĩ rằng nó sẽ bí danh một biến từ một phạm vi kèm theo). Điều này có thể xảy ra đặc biệt khi sử dụng tên biến phổ biến (như var). Do đó, chỉ sử dụng các kỹ thuật này khi bạn kiểm soát tên của biến bí danh .

function guillemots() {
  # var is intended to be local to the function,
  # aliasing a variable which comes from outside
  declare -n var="$1"
  var="«${var}»"
}

var='lolilol'
guillemots var  # Bash warnings: “var: circular name reference”
echo "$var"     # outputs anything!

Nguồn:


1
Đây là câu trả lời tốt nhất, đặc biệt vì ${!varname}kỹ thuật này yêu cầu var trung gian varname.
RichVel

Thật khó hiểu khi câu trả lời này chưa được nâng cao hơn
Marcos

18

Ví dụ dưới đây trả về giá trị của $ name_of_var

var=name_of_var
echo $(eval echo "\$$var")

4
Lồng hai echos với một thay thế lệnh (mà bỏ dấu ngoặc kép) là không cần thiết. Thêm vào đó, tùy chọn -nnên được đưa ra echo. Và, như mọi khi, evallà không an toàn. Nhưng tất cả những điều này là không cần thiết vì Bash có cú pháp an toàn hơn, rõ ràng hơn và ngắn hơn cho chính mục đích này : ${!var}.
Maëlan

4

Điều này sẽ làm việc:

function grep_search() {
    declare magic_variable_$1="$(ls | tail -1)"
    echo "$(tmpvar=magic_variable_$1 && echo ${!tmpvar})"
}
grep_search var  # calling grep_search with argument "var"

4

Điều này cũng sẽ làm việc

my_country_code="green"
x="country"

eval z='$'my_"$x"_code
echo $z                 ## o/p: green

Trong trường hợp của bạn

eval final_val='$'magic_way_to_define_magic_variable_"$1"
echo $final_val

3

Theo BashFAQ / 006 , bạn có thể sử dụng readvới đây cú pháp chuỗi để gán biến gián tiếp:

function grep_search() {
  read "$1" <<<$(ls | tail -1);
}

Sử dụng:

$ grep_search open_box
$ echo $open_box
stack-overflow.txt

3

Sử dụng declare

Không cần sử dụng tiền tố như trên các câu trả lời khác, không phải là mảng. Sử dụng chỉ declare, dấu ngoặc képmở rộng tham số .

Tôi thường sử dụng thủ thuật sau đây để phân tích các danh sách one to nđối số liên quan đến các đối số được định dạng là key=value otherkey=othervalue etc=etc, như:

# brace expansion just to exemplify
for variable in {one=foo,two=bar,ninja=tip}
do
  declare "${variable%=*}=${variable#*=}"
done
echo $one $two $ninja 
# foo bar tip

Nhưng mở rộng danh sách argv như

for v in "$@"; do declare "${v%=*}=${v#*=}"; done

Mẹo thêm

# parse argv's leading key=value parameters
for v in "$@"; do
  case "$v" in ?*=?*) declare "${v%=*}=${v#*=}";; *) break;; esac
done
# consume argv's leading key=value parameters
while (( $# )); do
  case "$v" in ?*=?*) declare "${v%=*}=${v#*=}";; *) break;; esac
  shift
done

1
Đây trông giống như một giải pháp rất sạch sẽ. Không có yếm và bobs độc ác và bạn sử dụng các công cụ có liên quan đến các biến, không che khuất các chức năng dường như không liên quan hoặc thậm chí nguy hiểm như printfhoặceval
kaugeour

2

Wow, hầu hết các cú pháp là khủng khiếp! Đây là một giải pháp với một số cú pháp đơn giản hơn nếu bạn cần gián tiếp tham chiếu các mảng:

#!/bin/bash

foo_1=("fff" "ddd") ;
foo_2=("ggg" "ccc") ;

for i in 1 2 ;
do
    eval mine=( \${foo_$i[@]} ) ;
    echo ${mine[@]} ;
done ;

Đối với các trường hợp sử dụng đơn giản hơn, tôi khuyên bạn nên sử dụng cú pháp được mô tả trong Hướng dẫn Bash-Scripting nâng cao .


2
ABS là một người khét tiếng vì thể hiện các thực hành xấu trong các ví dụ của nó. Vui lòng xem xét dựa vào wiki bash-tin tặc hoặc wiki Wooledge - nơi có mục nhập trực tiếp về chủ đề BashFAQ # 6 - thay vào đó.
Charles Duffy

@CharlesDuffy: Cảm ơn, tôi sẽ xem các tài liệu tham khảo đó. Tôi khá chắc chắn rằng cú pháp ở đây tôi tự nghĩ ra hoặc bị hack từ một ví dụ khác ở đâu đó. Tài liệu tham khảo của tôi về ABS là dành cho những người tìm kiếm trường hợp sử dụng đơn giản hơn, ví dụ như không sử dụng tên biến động. Tôi nghĩ, trời ạ, thật khó hiểu làm sao nếu ai đó chỉ muốn tham khảo một mảng và họ tìm thấy câu trả lời này.
ingyhere

2
Điều này chỉ hoạt động nếu các mục trong foo_1foo_2không có khoảng trắng và ký hiệu đặc biệt. Ví dụ cho các mục có vấn đề: 'a b'sẽ tạo hai mục bên trong mine. ''sẽ không tạo ra một mục bên trong mine. '*'sẽ mở rộng đến nội dung của thư mục làm việc. Bạn có thể ngăn chặn những vấn đề này bằng cách trích dẫn:eval 'mine=( "${foo_'"$i"'[@]}" )'
Socowi

@Socowi Đó là một vấn đề chung với việc lặp qua bất kỳ mảng nào trong BASH. Điều này cũng có thể được giải quyết bằng cách tạm thời thay đổi IFS (và sau đó tất nhiên thay đổi lại). Thật tốt khi thấy trích dẫn được thực hiện.
ingyhere

@ingyhere tôi xin khác. Nó không phải là một vấn đề chung. Có một giải pháp tiêu chuẩn: Luôn trích dẫn các [@]cấu trúc. "${array[@]}"sẽ luôn mở rộng vào danh sách chính xác các mục mà không gặp sự cố như tách từ hoặc mở rộng *. Ngoài ra, vấn đề tách từ chỉ có thể được giải quyết IFSnếu bạn biết bất kỳ ký tự không null nào không bao giờ xuất hiện bên trong mảng. Hơn nữa điều trị theo nghĩa đen *không thể đạt được bằng cách thiết lập IFS. Hoặc bạn đặt IFS='*'và phân chia tại các ngôi sao hoặc bạn đặt IFS=somethingOther*mở rộng.
Socowi

1

Đối với các mảng được lập chỉ mục, bạn có thể tham chiếu chúng như vậy:

foo=(a b c)
bar=(d e f)

for arr_var in 'foo' 'bar'; do
    declare -a 'arr=("${'"$arr_var"'[@]}")'
    # do something with $arr
    echo "\$$arr_var contains:"
    for char in "${arr[@]}"; do
        echo "$char"
    done
done

Các mảng kết hợp có thể được tham chiếu tương tự nhưng cần -Abật công tắc declarethay vì -a.


1

Một phương pháp bổ sung không phụ thuộc vào phiên bản shell / bash nào bạn có bằng cách sử dụng envsubst. Ví dụ:

newvar=$(echo '$magic_variable_'"${dynamic_part}" | envsubst)

0

Tôi muốn có thể tạo một tên biến chứa đối số đầu tiên của lệnh

script.sh tập tin:

#!/usr/bin/env bash
function grep_search() {
  eval $1=$(ls | tail -1)
}

Kiểm tra:

$ source script.sh
$ grep_search open_box
$ echo $open_box
script.sh

Theo help eval:

Thực thi các đối số như một lệnh shell.


Bạn cũng có thể sử dụng Bash ${!var}mở rộng gián tiếp, như đã đề cập, tuy nhiên nó không hỗ trợ truy xuất các chỉ số mảng.


Để đọc thêm hoặc ví dụ, kiểm tra BashFAQ / 006 về cảm ứng .

Chúng tôi không biết bất kỳ thủ thuật nào có thể nhân đôi chức năng đó trong các vỏ POSIX hoặc Bourne mà không có eval, có thể khó thực hiện một cách an toàn. Vì vậy, hãy xem đây là một sử dụng tại hack rủi ro của riêng bạn .

Tuy nhiên, bạn nên xem xét lại việc sử dụng cảm ứng theo các ghi chú sau.

Thông thường, trong kịch bản bash, bạn sẽ không cần tham khảo gián tiếp. Nói chung, mọi người xem xét giải pháp này khi họ không hiểu hoặc không biết về Bash Arrays hoặc chưa xem xét đầy đủ các tính năng khác của Bash như các hàm.

Đặt tên biến hoặc bất kỳ cú pháp bash nào khác bên trong các tham số thường được thực hiện không chính xác và trong các tình huống không phù hợp để giải quyết các vấn đề có giải pháp tốt hơn. Nó vi phạm sự tách biệt giữa mã và dữ liệu, và như vậy sẽ đưa bạn vào một dốc trơn trượt đối với các lỗi và các vấn đề bảo mật. Cảm ứng có thể làm cho mã của bạn kém minh bạch và khó theo dõi hơn.


-3

đối với varname=$prefix_suffixđịnh dạng, chỉ cần sử dụng:

varname=${prefix}_suffix
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.