Cách gán giá trị chứa không gian cho các biến trong bash bằng eval


19

Tôi muốn tự động gán giá trị cho các biến bằng cách sử dụng eval. Ví dụ giả sau đây hoạt động:

var_name="fruit"
var_value="orange"
eval $(echo $var_name=$var_value)
echo $fruit
orange

Tuy nhiên, khi giá trị biến chứa khoảng trắng, evalsẽ trả về lỗi, ngay cả khi $var_valueđược đặt giữa dấu ngoặc kép:

var_name="fruit"
var_value="blue orange"
eval $(echo $var_name="$var_value")
bash: orange : command not found

Bất kỳ cách nào để phá vỡ điều này?

Câu trả lời:


13

Đừng dùng eval, hãy dùng declare

$ declare "$var_name=$var_value"
$ echo "fruit: >$fruit<"
fruit: >blue orange<

11

Đừng sử dụng evalcho việc này; sử dụng declare.

var_name="fruit"
var_value="blue orange"
declare "$var_name=$var_value"

Lưu ý rằng chia tách từ không phải là một vấn đề, bởi vì mọi thứ theo sau =được coi là giá trị của declare, không chỉ là từ đầu tiên.

Trong bash4.3, các tham chiếu có tên làm cho điều này đơn giản hơn một chút.

$ declare -n var_name=fruit
$ var_name="blue orange"
$ echo $fruit
blue orange

Bạn có thể thực hiện evalcông việc, nhưng bạn vẫn không nên :) Sử dụng evallà một thói quen xấu cần có.

$ eval "$(printf "%q=%q" "$var_name" "$var_value")"

2
Sử dụng eval cách đó là sai. Bạn đang mở rộng $var_valuetrước khi chuyển nó sang evalđiều đó có nghĩa là nó sẽ được hiểu là mã shell! (thử ví dụ với var_value="';:(){ :|:&};:'")
Stéphane Chazelas

1
Điểm tốt; có một số chuỗi bạn không thể gán an toàn bằng cách sử dụng eval(đó là một lý do tôi nói bạn không nên sử dụng eval).
chepner

@chepner - Tôi không tin đó là sự thật. có lẽ nó là, nhưng ít nhất không phải cái này thay thế tham số cho phép mở rộng có điều kiện và vì vậy bạn chỉ có thể mở rộng các giá trị an toàn trong hầu hết các trường hợp. Tuy nhiên, vấn đề chính của bạn $var_valuelà một trong những đảo ngược trích dẫn - giả sử giá trị an toàn cho $var_name (có thể là một giả định nguy hiểm, thực sự) , sau đó bạn nên đặt dấu ngoặc kép bên phải trong dấu ngoặc đơn - không phải ngược lại.
mikeerv

Tôi nghĩ rằng tôi đã sửa lỗi eval, sử dụng printfvà định dạng bashcụ thể của nó %q. Đây vẫn không phải là một khuyến nghị để sử dụng eval, nhưng tôi nghĩ rằng nó an toàn hơn so với trước đây. Thực tế rằng bạn cần phải nỗ lực rất nhiều để làm cho nó hoạt động là bằng chứng cho thấy bạn nên sử dụng declarehoặc đặt tên tài liệu tham khảo thay thế.
chepner

Vâng, thực sự, theo tôi, các tài liệu tham khảo có tên là vấn đề. Cách tốt nhất để sử dụng nó - theo kinh nghiệm của tôi - giống như ... set -- a bunch of args; eval "process2 $(process1 "$@")"nơi process1chỉ in những con số được trích dẫn như thế nào "${1}" "${8}" "${138}". Điều đó thật đơn giản - và dễ dàng như '"${'$((i=$i+1))'}" 'trong hầu hết các trường hợp. tài liệu tham khảo được lập chỉ mục làm cho nó an toàn, mạnh mẽ và nhanh chóng . Tuy nhiên - tôi đã nâng cao.
mikeerv

4

Một cách tốt để làm việc evallà thay thế nó echođể thử nghiệm. echoevalhoạt động tương tự (nếu chúng ta dành riêng phần \xmở rộng được thực hiện bởi một số echotriển khai như bashtrong một số điều kiện).

Cả hai lệnh tham gia đối số của chúng với một khoảng trắng ở giữa. Sự khác biệt là echo hiển thị kết quả trong khi eval đánh giá / diễn giải dưới dạng mã vỏ kết quả.

Vì vậy, để xem mã shell nào

eval $(echo $var_name=$var_value)

sẽ đánh giá, bạn có thể chạy:

$ echo $(echo $var_name=$var_value)
fruit=blue orange

Đó không phải là những gì bạn muốn, những gì bạn muốn là:

fruit=$var_value

Ngoài ra, sử dụng $(echo ...)ở đây không có ý nghĩa.

Để xuất ra ở trên, bạn sẽ chạy:

$ echo "$var_name=\$var_value"
fruit=$var_value

Vì vậy, để giải thích nó, đó chỉ đơn giản là:

eval "$var_name=\$var_value"

Lưu ý rằng nó cũng có thể được sử dụng để đặt các thành phần mảng riêng lẻ:

var_name='myarray[23]'
var_value='something'
eval "$var_name=\$var_value"

Như những người khác đã nói, nếu bạn không quan tâm mã của mình là bashcụ thể, bạn có thể sử dụng declarenhư:

declare "$var_name=$var_value"

Tuy nhiên lưu ý rằng nó có một số tác dụng phụ.

Nó giới hạn phạm vi của biến đối với chức năng nơi nó chạy. Vì vậy, bạn không thể sử dụng nó trong ví dụ như:

setvar() {
  var_name=$1 var_value=$2
  declare "$var_name=$var_value"
}
setvar foo bar

Bởi vì điều đó sẽ khai báo một foobiến cục bộ để setvarnhư vậy sẽ vô dụng.

bash-4.2đã thêm một -gtùy chọn declaređể khai báo một biến toàn cục, nhưng đó không phải là điều chúng ta muốn vì chúng ta setvarsẽ đặt một biến toàn cục trái ngược với biến của người gọi nếu người gọi là một hàm, như trong:

setvar() {
  var_name=$1 var_value=$2
  declare -g "$var_name=$var_value"
}
foo() {
  local myvar
  setvar myvar 'some value'
  echo "1: $myvar"
}
foo
echo "2: $myvar"

mà sẽ xuất ra:

1:
2: some value

Ngoài ra, lưu ý rằng trong khi declaređược gọi declare(thực tế đã bashmượn khái niệm từ typesetnội dung của vỏ Korn ), nếu biến đã được đặt, declaresẽ không khai báo một biến mới và cách thực hiện chuyển nhượng phụ thuộc vào loại biến.

Ví dụ:

varname=foo
varvalue='([PATH=1000]=something)'
declare "$varname=$varvalue"

sẽ tạo ra một kết quả khác (và có khả năng có tác dụng phụ khó chịu) nếu varnametrước đây được khai báo là mảng vô hướng , mảng hoặc mảng kết hợp .


2
Có gì sai với bash cụ thể? OP đặt thẻ bash cho câu hỏi, vì vậy anh ấy đang sử dụng bash. Cung cấp thay thế là tốt, nhưng tôi nghĩ việc nói với ai đó không sử dụng tính năng của vỏ vì nó không di động là ngớ ngẩn.
Patrick

@Patrick, thấy cười không? Phải nói rằng, sử dụng cú pháp di động có nghĩa là ít nỗ lực hơn khi bạn cần chuyển mã của mình sang một hệ thống khác, nơi bashkhông có sẵn (hoặc khi bạn nhận ra rằng bạn cần một trình bao tốt hơn / nhanh hơn). Các evalcông trình cú pháp trong tất cả Bourne giống như vỏ và là POSIX vì vậy tất cả các hệ thống sẽ có một shnơi mà các công trình. (mà cũng có nghĩa là câu trả lời của tôi áp dụng cho tất cả vỏ, và sớm hay muộn, như xảy ra thường xuyên ở đây, bạn sẽ thấy một cụ thể câu hỏi không liên bash đóng cửa như một bản sao của thế này.
Stéphane Chazelas

Nhưng nếu $var_namechứa token thì sao? ... như thế ;nào?
mikeerv

@mikeerv, thì đó không phải là một tên biến. Nếu bạn không thể tin tưởng nội dung của nó, sau đó bạn cần phải khử trùng nó với cả hai evaldeclare(nghĩ PATH, TMOUT, PS4, SECONDS...).
Stéphane Chazelas

nhưng trên lần đầu tiên, nó luôn luôn là một mở rộng biến và không bao giờ là một tên biến cho đến lần thứ hai. trong câu trả lời của tôi, tôi vệ sinh nó bằng một mở rộng tham số, nhưng nếu bạn ngụ ý thực hiện việc khử trùng trong một nhánh con trên đường chuyền đầu tiên, điều đó cũng có thể được thực hiện một cách hợp lý export. Tôi không theo bit cha mẹ về cuối mặc dù.
mikeerv

1

Nếu bạn làm:

eval "$name=\$val"

... và $namechứa một ;- hoặc bất kỳ mã thông báo nào khác, shell có thể hiểu là phân định một lệnh đơn giản - trước cú pháp shell thích hợp, sẽ được thực thi.

name='echo hi;varname' val='be careful with eval'
eval "$name=\$val" && echo "$varname"

ĐẦU RA

hi
be careful with eval

Tuy nhiên, đôi khi có thể tách rời việc đánh giá và thực hiện các tuyên bố đó. Ví dụ, aliascó thể được sử dụng để đánh giá trước một lệnh. Trong ví dụ sau, định nghĩa biến được lưu vào một aliaschỉ có thể được khai báo thành công nếu $nmbiến mà nó đang đánh giá không chứa byte nào không khớp với chữ số ASCII hoặc _.

LC_OLD=$LC_ALL LC_ALL=C
alias "${nm##*[!_A-Z0-9a-z]*}=_$nm=\$val" &&
eval "${nm##[0-9]*}" && unalias "$nm"
LC_ALL=$LC_OLD

evalđược sử dụng ở đây để xử lý việc gọi cái mới aliastừ một tên biến. Nhưng nó chỉ được gọi hoàn toàn nếu aliasđịnh nghĩa trước đó thành công và trong khi tôi biết nhiều cách triển khai khác nhau sẽ chấp nhận rất nhiều loại giá trị khác nhau cho aliastên, tôi vẫn chưa chạy vào một định nghĩa sẽ chấp nhận một cách hoàn toàn trống rỗng .

Tuy nhiên, định nghĩa trong aliaslà dành cho _$nmvà điều này là để đảm bảo rằng không có giá trị môi trường quan trọng nào được ghi lại. Tôi không biết về bất kỳ giá trị môi trường đáng chú ý nào bắt đầu bằng một _và nó thường là đặt cược an toàn cho khai báo bán riêng.

Dù sao, nếu aliasđịnh nghĩa thành công, nó sẽ khai báo một giá trị aliasđược đặt tên $nm. Và evalsẽ chỉ gọi rằng aliasnếu cũng không bắt đầu bằng một số - người khác evalchỉ nhận được một đối số null. Vì vậy, nếu cả hai điều kiện được đáp ứng, hãy evalgọi bí danh và định nghĩa biến được lưu trong bí danh được thực hiện, sau đó cái mới aliassẽ nhanh chóng bị xóa khỏi bảng băm.


;không được phép trong tên biến. Nếu bạn không có quyền kiểm soát nội dung của $name, sau đó bạn cần phải khử trùng nó cho export/ declarelà tốt. Trong khi exportkhông thực thi mã, đặt một số biến như PATH, PS4và nhiều trong số những người ở info -f bash -n 'Bash Variables'có tác dụng phụ không kém phần nguy hiểm.
Stéphane Chazelas

@ StéphaneChazelas - tất nhiên là không được phép, nhưng, như trước đây, nó không phải là một tên biến trong evallần đầu tiên vượt qua - đó là một sự mở rộng biến. Như bạn đã nói ở nơi khác, trong bối cảnh đó, nó được cho phép rất nhiều. Tuy nhiên, đối số $ PATH là một đối số rất tốt - tôi đã thực hiện một chỉnh sửa nhỏ và sẽ thêm một số sau.
mikeerv

@ StéphaneChazelas - muộn còn hơn không ...?
mikeerv

Trên thực tế, zsh, pdksh, mksh, yashkhông phàn nàn về unset 'a;b'.
Stéphane Chazelas

Bạn cũng sẽ muốn unset -v -- ...như vậy.
Stéphane Chazelas
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.