Các biến nên được trích dẫn khi thực hiện?


18

Nguyên tắc chung trong kịch bản shell là các biến phải luôn được trích dẫn trừ khi không có lý do thuyết phục nào. Để biết thêm chi tiết mà bạn có thể muốn biết, hãy xem Câu hỏi và trả lời tuyệt vời này: Hàm ý bảo mật của việc quên trích dẫn một biến trong các vỏ bash / POSIX .

Tuy nhiên, hãy xem xét một chức năng như sau:

run_this(){
    $@
}

Có nên $@trích dẫn ở đó hay không? Tôi đã chơi với nó một chút và không thể tìm thấy bất kỳ trường hợp nào mà việc thiếu dấu ngoặc kép gây ra vấn đề. Mặt khác, việc sử dụng dấu ngoặc kép sẽ khiến nó bị hỏng khi truyền lệnh chứa khoảng trắng dưới dạng biến được trích dẫn:

#!/usr/bin/sh
set -x
run_this(){
    $@
}
run_that(){
    "$@"
}
comm="ls -l"
run_this "$comm"
run_that "$comm"

Chạy đoạn script trên trả về:

$ a.sh
+ comm='ls -l'
+ run_this 'ls -l'
+ ls -l
total 8
-rw-r--r-- 1 terdon users  0 Dec 22 12:58 da
-rw-r--r-- 1 terdon users 45 Dec 22 13:33 file
-rw-r--r-- 1 terdon users 43 Dec 22 12:38 file~
+ run_that 'ls -l'
+ 'ls -l'
/home/terdon/scripts/a.sh: line 7: ls -l: command not found

Tôi có thể giải quyết vấn đề đó nếu tôi sử dụng run_that $commthay vì run_that "$comm", nhưng vì run_thischức năng (không trích dẫn) hoạt động với cả hai, có vẻ như đặt cược an toàn hơn.

Vì vậy, trong trường hợp cụ thể của việc sử dụng $@trong một chức năng có công việc là thực thi $@như một lệnh, nên $@được trích dẫn? Vui lòng giải thích lý do tại sao nên / không nên trích dẫn và đưa ra một ví dụ về dữ liệu có thể phá vỡ nó.


6
run_thatHành vi của nó chắc chắn là những gì tôi mong đợi (nếu có một khoảng trống trong đường dẫn đến lệnh thì sao?). Nếu bạn muốn có hành vi khác, chắc chắn bạn sẽ bỏ qua nó tại trang web cuộc gọi nơi bạn biết dữ liệu là gì? Tôi hy vọng sẽ gọi chức năng này là run_that ls -l, hoạt động tương tự trong cả hai phiên bản. Có một trường hợp khiến bạn mong đợi khác nhau?
Michael Homer

@MichaelHomer Tôi đoán bản chỉnh sửa của tôi ở đây đã nhắc điều này: unix.stackexchange.com/a/250985/70524
muru

@MichaelHomer vì một số lý do (có lẽ vì tôi vẫn chưa có tách cà phê thứ hai) Tôi đã không xem xét khoảng trắng trong đối số hoặc đường dẫn của lệnh, nhưng chỉ trong chính lệnh (tùy chọn). Như thường thấy, điều này có vẻ rất rõ ràng khi nhìn lại.
terdon

Có một lý do tại sao shell vẫn hỗ trợ các hàm thay vì chỉ nhồi các lệnh vào một mảng và thực thi nó ${mycmd[@]}.
chepner

Câu trả lời:


20

Vấn đề nằm ở chỗ lệnh được truyền cho hàm:

$ run_this ls -l Untitled\ Document.pdf 
ls: cannot access Untitled: No such file or directory
ls: cannot access Document.pdf: No such file or directory
$ run_that ls -l Untitled\ Document.pdf 
-rw------- 1 muru muru 33879 Dec 20 11:09 Untitled Document.pdf

"$@"nên được sử dụng trong trường hợp chung khi run_thishàm của bạn được đặt trước một lệnh được viết thông thường. run_thisdẫn đến trích dẫn địa ngục:

$ run_this 'ls -l Untitled\ Document.pdf'
ls: cannot access Untitled\: No such file or directory
ls: cannot access Document.pdf: No such file or directory
$ run_this 'ls -l "Untitled\ Document.pdf"'
ls: cannot access "Untitled\: No such file or directory
ls: cannot access Document.pdf": No such file or directory
$ run_this 'ls -l Untitled Document.pdf'
ls: cannot access Untitled: No such file or directory
ls: cannot access Document.pdf: No such file or directory
$ run_this 'ls -l' 'Untitled Document.pdf'
ls: cannot access Untitled: No such file or directory
ls: cannot access Document.pdf: No such file or directory

Tôi không chắc làm thế nào tôi nên chuyển một tên tệp có dấu cách run_this.


1
Đó thực sự là chỉnh sửa của bạn đã nhắc nhở điều này. Vì một số lý do, tôi đã không kiểm tra tên tệp có dấu cách. Tôi hoàn toàn không biết tại sao không, nhưng có bạn đi. Tất nhiên, bạn hoàn toàn đúng, tôi không thấy cách nào để làm điều này một cách chính xác với run_thismột trong hai.
terdon

@terdon trích dẫn trở thành một thói quen mà tôi cho rằng bạn đã $@vô tình bỏ qua. Tôi nên để lại một ví dụ. : D
muru

2
Không, đó thực sự là một thói quen mà tôi đã thử nghiệm nó (sai) và kết luận rằng "huh, có lẽ cái này không cần trích dẫn". Một thủ tục thường được gọi là một bộ não.
terdon

1
Bạn không thể truyền tên tệp có dấu cách vào run_this. Về cơ bản, đây là cùng một vấn đề mà bạn gặp phải khi nhồi các lệnh phức tạp vào các chuỗi như được thảo luận trong Bash FAQ 050 .
Etan Reisner

9

Đó cũng là:

interpret_this_shell_code() {
  eval "$1"
}

Hoặc là:

interpret_the_shell_code_resulting_from_the_concatenation_of_those_strings_with_spaces() {
  eval "$@"
}

hoặc là:

execute_this_simple_command_with_these_arguments() {
  "$@"
}

Nhưng:

execute_the_simple_command_with_the_arguments_resulting_from_split+glob_applied_to_these_strings() {
  $@
}

Không có nhiều ý nghĩa.

Nếu bạn muốn thực thi ls -llệnh (không phải lslệnh với ls-llàm đối số), bạn sẽ làm:

interpret_this_shell_code '"ls -l"'
execute_this_simple_command_with_these_arguments 'ls -l'

Nhưng nếu (nhiều khả năng), đó là lslệnh với ls-llàm đối số, bạn sẽ chạy:

interpret_this_shell_code 'ls -l'
execute_this_simple_command_with_these_arguments ls -l

Bây giờ, nếu đó không chỉ là một lệnh đơn giản mà bạn muốn thực thi, nếu bạn muốn thực hiện các phép gán biến đổi, chuyển hướng, đường ống ..., interpret_this_shell_codesẽ chỉ thực hiện:

interpret_this_shell_code 'ls -l 2> /dev/null'

mặc dù tất nhiên bạn luôn có thể làm:

execute_this_simple_command_with_these_arguments eval '
  ls -l 2> /dev/null'

5

Nhìn vào nó từ phối cảnh bash / ksh / zsh, $*$@là một trường hợp đặc biệt của việc mở rộng mảng chung. Mở rộng mảng không giống như mở rộng biến thông thường:

$ a=("a b c" "d e" f)
$ printf ' -> %s\n' "${a[*]}"
 -> a b c d e f
$ printf ' -> %s\n' "${a[@]}"
-> a b c
-> d e
-> f
$ printf ' -> %s\n' ${a[*]}
 -> a
 -> b
 -> c
 -> d
 -> e
 -> f
$ printf ' -> %s\n' ${a[@]}
 -> a
 -> b
 -> c
 -> d
 -> e
 -> f

Với $*/ ${a[*]}mở rộng, bạn có được mảng được nối với giá trị đầu tiên của IFSWapwhich là không gian theo mặc định thành một chuỗi khổng lồ. Nếu bạn không trích dẫn nó, nó sẽ bị tách ra như một chuỗi bình thường.

Với $@/ ${a[@]}mở rộng, hành vi phụ thuộc vào việc $@/ ${a[@]}mở rộng có được trích dẫn hay không:

  1. nếu nó được trích dẫn ( "$@"hoặc "${a[@]}"), bạn nhận được tương đương "$1" "$2" "$3" #... hoặc"${a[1]}" "${a[2]}" "${a[3]}" # ...
  2. nếu nó không được trích dẫn ( $@hoặc ${a[@]}), bạn nhận được tương đương $1 $2 $3 #... hoặc${a[1]} ${a[2]} ${a[3]} # ...

Đối với các lệnh gói, bạn chắc chắn muốn mở rộng @ trích dẫn (1.).


Thêm thông tin tốt về mảng bash (và giống như bash): https://lukeshu.com/blog/bash-arrays.html


1
Chỉ cần nhận ra tôi đang đề cập đến một liên kết bắt đầu với Luke, trong khi đeo mặt nạ Vader. Các lực lượng mạnh mẽ với bài này.
PSkocik

4

Vì khi bạn không trích dẫn hai lần $@, bạn đã để lại tất cả các vấn đề toàn cầu trong liên kết bạn đưa ra cho chức năng của mình.

Làm thế nào bạn có thể chạy một lệnh có tên *? Bạn không thể làm điều đó với run_this:

$ ls
1 2
$ run_this '*'
dash: 2: 1: not found
$ run_that '*'
dash: 3: *: not found

Và bạn thấy, ngay cả khi xảy ra lỗi, run_thatđã cho bạn một thông điệp có ý nghĩa hơn.

Cách duy nhất để mở rộng $@thành các từ riêng lẻ là trích dẫn kép. Nếu bạn muốn chạy nó dưới dạng một lệnh, bạn nên truyền lệnh và tham số dưới dạng các từ riêng biệt. Đó là điều bạn đã làm ở phía người gọi, không phải bên trong chức năng của bạn.

$ cmd=ls
$ param1=-l
$ run_that "$cmd" "$param1"
total 0
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 1
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 2

là một lựa chọn tốt hơn. Hoặc nếu mảng hỗ trợ shell của bạn:

$ cmd=(ls -l)
$ run_that "${cmd[@]}"
total 0
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 1
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 2

Ngay cả khi shell hoàn toàn không hỗ trợ mảng, bạn vẫn có thể chơi với nó bằng cách sử dụng"$@" .


3

Thực hiện các biến trong bashlà một kỹ thuật dễ bị lỗi. Đơn giản là không thể viết một run_thishàm xử lý chính xác tất cả các trường hợp cạnh, như:

  • đường ống dẫn (ví dụ ls | grep filename)
  • chuyển hướng đầu vào / đầu ra (ví dụ ls > /dev/null)
  • báo cáo vỏ như if whilevv

Nếu tất cả những gì bạn muốn làm là tránh lặp lại mã, tốt hơn hết bạn nên sử dụng các hàm. Ví dụ: thay vì:

run_this(){
    "$@"
}
command="ls -l"
...
run_this "$command"

Bạn nên viết

command() {
    ls -l
}
...
command

Nếu các lệnh chỉ khả dụng khi chạy, bạn nên sử dụng eval, được thiết kế đặc biệt để xử lý tất cả các yêu cầu sẽ gây ra run_thislỗi:

command="ls -l | grep filename > /dev/null"
...
eval "$command"

Lưu ý rằng evalđược biết về các vấn đề an ninh, nhưng nếu bạn vượt qua biến từ các nguồn không tin cậy để run_this, bạn sẽ phải đối mặt với thực thi mã tùy ý chỉ là tốt.


1

Sự lựa chọn là của bạn. Nếu bạn không trích dẫn $@bất kỳ giá trị nào của nó trải qua mở rộng và giải thích bổ sung. Nếu bạn trích dẫn nó, tất cả các đối số được thông qua, hàm sẽ được sao chép trong nguyên văn mở rộng của nó. Bạn sẽ không bao giờ có thể xử lý một cách đáng tin cậy các mã thông báo cú pháp shell như thế nào &>|, v.v.

  1. Chính xác các từ được sử dụng trong việc thực hiện một lệnh đơn giản với "$@".

...hoặc là...

  1. Một phiên bản mở rộng và diễn giải thêm các đối số của bạn mà sau đó chỉ được áp dụng cùng nhau như một lệnh đơn giản với $@.

Không có cách nào là sai nếu nó là cố ý và nếu tác động của những gì bạn chọn được hiểu rõ. Cả hai cách đều có lợi thế so với cách khác, mặc dù lợi thế của cách thứ hai hiếm khi đặc biệt hữu ích. Vẫn...

(run_this(){ $@; }; IFS=@ run_this 'ls@-dl@/tmp')

drwxrwxrwt 22 root root 660 Dec 28 19:58 /tmp

... nó không vô dụng , hiếm khi có khả năng được sử dụng nhiều . Và trong một bashvỏ, vì bashmặc định không gắn một định nghĩa biến với môi trường của nó ngay cả khi định nghĩa đã nói được đặt trước dòng lệnh của một nội dung đặc biệt hoặc cho một hàm, giá trị toàn cục $IFSkhông bị ảnh hưởng và khai báo của nó là cục bộ Chỉ để run_this()gọi.

Tương tự:

(run_this(){ $@; }; set -f; run_this ls -l \*)

ls: cannot access *: No such file or directory

... Globing cũng có thể cấu hình. Trích dẫn phục vụ một mục đích - chúng không phải là không có gì. Không có chúng mở rộng vỏ trải qua giải thích thêm - giải thích cấu hình . Nó đã từng - với một số vỏ rất - $IFSđược áp dụng trên toàn cầu cho tất cả các đầu vào, và không chỉ mở rộng. Trong thực tế, các shell cho biết hành vi rất giống như run_this()ở chỗ chúng đã phá vỡ tất cả các từ đầu vào trên giá trị của $IFS. Và vì vậy, nếu những gì bạn đang tìm kiếm là hành vi vỏ rất cũ, thì bạn nên sử dụng run_this().

Tôi không tìm kiếm nó, và tôi khá khó khăn vào lúc này để đưa ra một ví dụ hữu ích cho nó. Tôi thường thích các lệnh mà shell của tôi chạy là các lệnh mà tôi gõ vào nó. Và vì vậy, được lựa chọn, tôi sẽ luôn luôn như vậy run_that(). Ngoại trừ việc...

(run_that(){ "$@"; }; IFS=l run_that 'ls' '-ld' '/tmp')

drwxrwxrwt 22 root root 660 Dec 28 19:58 /tmp

Chỉ cần bất cứ điều gì có thể được trích dẫn. Các lệnh sẽ chạy trích dẫn. Nó hoạt động vì vào thời điểm lệnh thực sự chạy, tất cả các từ đầu vào đã trải qua quá trình loại bỏ trích dẫn - đó là giai đoạn cuối cùng của quá trình giải thích đầu vào của shell. Vì vậy, sự khác biệt giữa 'ls'lschỉ có thể quan trọng trong khi shell đang diễn giải - và đó là lý do tại sao trích dẫn lsđảm bảo rằng bất kỳ bí danh nào được đặt tên lskhông được thay thế cho lstừ lệnh được trích dẫn của tôi . Ngoài ra, điều duy nhất mà trích dẫn ảnh hưởng là phân định các từ (đó là cách thức và lý do trích dẫn biến / đầu vào khoảng trắng) và cách giải thích các ký tự đại diện và các từ dành riêng.

Vì thế:

'for' f in ...
 do   :
 done

bash: for: command not found
bash:  do: unexpected token 'do'
bash:  do: unexpected token 'done'

Bạn sẽ không bao giờ có thể làm điều đó với một trong hai run_this()hoặc run_that().

Nhưng tên hàm, hoặc $PATHcác lệnh hoặc lệnh dựng sẽ thực thi tốt chỉ trích dẫn hoặc không trích dẫn, và đó chính xác là cách thức run_this()run_that()hoạt động ở vị trí đầu tiên. Bạn sẽ không thể làm bất cứ điều gì hữu ích với $<>|&(){}bất kỳ ai trong số đó. Ngắn gọn eval, là.

(run_that(){ "$@"; }; run_that eval printf '"%s\n"' '"$@"')

eval
printf
"%s\n"
"$@"

Nhưng không có nó, bạn bị giới hạn bởi các giới hạn của một lệnh đơn giản nhờ vào các trích dẫn bạn sử dụng (ngay cả khi bạn không$@ sử dụng vì các hành động như một trích dẫn ở đầu quá trình khi lệnh được phân tích cú pháp cho các siêu ký tự) . Ràng buộc tương tự cũng đúng với các bài tập và chuyển hướng dòng lệnh, được giới hạn trong dòng lệnh của hàm. Nhưng đó không phải là một vấn đề lớn:

(run_that(){ "$@";}; echo hey | run_that cat)

hey

Tôi có thể dễ dàng <chuyển hướng đầu vào hoặc >đầu ra ở đó như tôi đã mở đường ống.

Dù sao, trong một cách xoay vòng, không có cách nào đúng hay sai ở đây - mỗi cách đều có cách sử dụng. Chỉ là bạn nên viết nó khi bạn định sử dụng nó, và bạn nên biết bạn muốn làm gì. Bỏ dấu ngoặc kép có thể có một mục đích - nếu không thì sẽ không trích dẫn nào cả - nhưng nếu bạn bỏ qua chúng vì những lý do không liên quan đến mục đích của bạn, bạn chỉ đang viết mã xấu. Làm những gì bạn có ý nghĩa; Tôi cố gắng nào.

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.