Luôn sử dụng dấu ngoặc kép xung quanh thay thế biến và thay thế lệnh : "$foo"
,"$(foo)"
Nếu bạn sử dụng không $foo
trích dẫn, tập lệnh của bạn sẽ bị nghẹt đầu vào hoặc tham số (hoặc đầu ra lệnh, với $(foo)
) có chứa khoảng trắng hoặc \[*?
.
Ở đó, bạn có thể ngừng đọc. Vâng, đây là một vài điều nữa:
read
- Để đọc dòng đầu vào bằng cách phù hợp với read
dựng sẵn, sử dụngwhile IFS= read -r line; do …
Plain read
xử lý backslashes và khoảng trắng đặc biệt.
xargs
- Tránhxargs
. Nếu bạn phải sử dụng xargs
, làm cho điều đó xargs -0
. Thay vì find … | xargs
, thíchfind … -exec …
.
xargs
đối xử với khoảng trắng và các ký tự \"'
đặc biệt.
Câu trả lời này áp dụng đối với vỏ Bourne / POSIX-style ( sh
, ash
, dash
, bash
, ksh
, mksh
, yash
...). Người dùng Zsh nên bỏ qua nó và đọc phần cuối của Khi nào cần trích dẫn kép? thay thế. Nếu bạn muốn toàn bộ nitty-gritty, hãy đọc hướng dẫn tiêu chuẩn hoặc vỏ của bạn.
Lưu ý rằng các giải thích bên dưới có chứa một vài phép tính gần đúng (các câu lệnh đúng trong hầu hết các điều kiện nhưng có thể bị ảnh hưởng bởi bối cảnh xung quanh hoặc bởi cấu hình).
Tại sao tôi cần phải viết "$foo"
? Điều gì xảy ra mà không có dấu ngoặc kép?
$foo
không có nghĩa là người Viking lấy giá trị của biến số foo
. Nó có nghĩa là một cái gì đó phức tạp hơn nhiều:
- Đầu tiên, lấy giá trị của biến.
- Tách trường: coi giá trị đó là danh sách các trường được phân tách bằng khoảng trắng và xây dựng danh sách kết quả. Ví dụ, nếu biến chứa
foo * bar
sau đó là kết quả của bước này là danh sách 3 yếu tố foo
, *
, bar
.
- Tạo tên tệp: coi mỗi trường là một hình cầu, tức là dưới dạng mẫu ký tự đại diện và thay thế nó bằng danh sách các tên tệp khớp với mẫu này. Nếu mẫu không khớp với bất kỳ tệp nào, nó sẽ không được sửa đổi. Trong ví dụ của chúng tôi, kết quả này trong danh sách chứa
foo
, theo sau là danh sách các tệp trong thư mục hiện tại và cuối cùng bar
. Nếu thư mục hiện thời trống rỗng, kết quả là foo
, *
, bar
.
Lưu ý rằng kết quả là một danh sách các chuỗi. Có hai bối cảnh trong cú pháp shell: bối cảnh danh sách và bối cảnh chuỗi. Việc tách trường và tạo tên tệp chỉ xảy ra trong ngữ cảnh danh sách, nhưng đó hầu hết là thời gian. Dấu ngoặc kép phân định một bối cảnh chuỗi: toàn bộ chuỗi trích dẫn kép là một chuỗi đơn, không được phân chia. (Ngoại lệ: "$@"
để mở rộng danh sách các tham số vị trí, ví dụ như "$@"
tương đương với "$1" "$2" "$3"
nếu có ba tham số vị trí. Xem Sự khác biệt giữa $ * và $ @? )
Điều tương tự xảy ra với thay thế lệnh bằng $(foo)
hoặc với `foo`
. Mặt khác, không sử dụng `foo`
: quy tắc trích dẫn của nó là lạ và không thể mang theo được, và tất cả các hỗ trợ vỏ hiện đại $(foo)
hoàn toàn tương đương ngoại trừ có quy tắc trích dẫn trực quan.
Đầu ra của sự thay thế số học cũng trải qua các lần mở rộng tương tự, nhưng điều đó thường không phải là mối quan tâm vì nó chỉ chứa các ký tự không thể mở rộng (giả sử IFS
không chứa chữ số hoặc -
).
Xem Khi nào cần trích dẫn kép? để biết thêm chi tiết về các trường hợp khi bạn có thể để lại dấu ngoặc kép.
Trừ khi bạn có ý nghĩa cho tất cả sự nghiêm khắc này xảy ra, chỉ cần nhớ luôn luôn sử dụng dấu ngoặc kép xung quanh các thay thế biến và lệnh. Hãy cẩn thận: bỏ qua các trích dẫn có thể dẫn đến không chỉ các lỗi mà còn các lỗ hổng bảo mật .
Làm cách nào để xử lý danh sách tên tệp?
Nếu bạn viết myfiles="file1 file2"
, với khoảng trắng để tách các tệp, điều này không thể hoạt động với tên tệp chứa khoảng trắng. Tên tệp Unix có thể chứa bất kỳ ký tự nào ngoài /
(luôn là dấu phân cách thư mục) và byte rỗng (mà bạn không thể sử dụng trong các tập lệnh shell với hầu hết các shell).
Cùng một vấn đề với myfiles=*.txt; … process $myfiles
. Khi bạn thực hiện việc này, biến myfiles
chứa chuỗi 5 ký tự *.txt
và khi bạn viết $myfiles
rằng ký tự đại diện được mở rộng. Ví dụ này sẽ thực sự hoạt động, cho đến khi bạn thay đổi tập lệnh của mình thành myfiles="$someprefix*.txt"; … process $myfiles
. Nếu someprefix
được đặt thành final report
, điều này sẽ không hoạt động.
Để xử lý một danh sách của bất kỳ loại nào (chẳng hạn như tên tệp), hãy đặt nó trong một mảng. Điều này đòi hỏi mksh, ksh93, yash hoặc bash (hoặc zsh, không có tất cả các vấn đề trích dẫn này); một vỏ POSIX đơn giản (như tro hoặc gạch ngang) không có biến mảng.
myfiles=("$someprefix"*.txt)
process "${myfiles[@]}"
Ksh88 có các biến mảng với cú pháp gán khác nhau set -A myfiles "someprefix"*.txt
(xem biến gán trong môi trường ksh khác nếu bạn cần tính di động của ksh88 / bash). Các shell kiểu Bourne / POSIX có một mảng duy nhất, mảng các tham số vị trí "$@"
mà bạn đặt set
và là cục bộ của hàm:
set -- "$someprefix"*.txt
process -- "$@"
Tên tập tin bắt đầu bằng -
gì?
Trên một lưu ý liên quan, hãy nhớ rằng tên tệp có thể bắt đầu bằng -
(dấu gạch ngang / dấu trừ), mà hầu hết các lệnh diễn giải là biểu thị một tùy chọn. Nếu bạn có một tên tệp bắt đầu bằng một phần biến, hãy chắc chắn vượt qua --
trước nó, như trong đoạn trích ở trên. Điều này chỉ ra lệnh rằng nó đã đạt đến cuối các tùy chọn, vì vậy bất cứ thứ gì sau đó là tên tệp ngay cả khi nó bắt đầu bằng -
.
Ngoài ra, bạn có thể đảm bảo rằng tên tệp của bạn bắt đầu bằng một ký tự khác -
. Tên tệp tuyệt đối bắt đầu bằng /
và bạn có thể thêm ./
vào đầu tên tương đối. Đoạn mã sau đây biến nội dung của biến f
thành một cách an toàn của Google khi tham chiếu đến cùng một tệp được đảm bảo không bắt đầu -
.
case "$f" in -*) "f=./$f";; esac
Trong một lưu ý cuối cùng về chủ đề này, hãy cẩn thận rằng một số lệnh diễn giải -
là đầu vào tiêu chuẩn hoặc đầu ra tiêu chuẩn, ngay cả sau đó --
. Nếu bạn cần tham khảo một tệp thực tế có tên -
hoặc nếu bạn đang gọi một chương trình như vậy và bạn không muốn nó đọc từ stdin hoặc ghi vào thiết bị xuất chuẩn, hãy đảm bảo viết lại -
như trên. Xem sự khác biệt giữa "du -sh *" và "du -sh ./*" là gì? để thảo luận thêm.
Làm thế nào để tôi lưu trữ một lệnh trong một biến?
Lệnh Command có thể có ba nghĩa: tên lệnh (tên dưới dạng thực thi, có hoặc không có đường dẫn đầy đủ hoặc tên hàm, hàm dựng sẵn hoặc bí danh), tên lệnh có đối số hoặc mã mảnh. Có nhiều cách khác nhau để lưu trữ chúng trong một biến.
Nếu bạn có một tên lệnh, chỉ cần lưu trữ nó và sử dụng biến có dấu ngoặc kép như bình thường.
command_path="$1"
…
"$command_path" --option --message="hello world"
Nếu bạn có một lệnh với các đối số, vấn đề cũng giống như với một danh sách các tên tệp ở trên: đây là danh sách các chuỗi, không phải là một chuỗi. Bạn không thể nhét các đối số vào một chuỗi có khoảng trắng ở giữa, bởi vì nếu bạn làm điều đó, bạn không thể biết sự khác biệt giữa các không gian là một phần của các đối số và khoảng trắng tách biệt các đối số. Nếu shell của bạn có mảng, bạn có thể sử dụng chúng.
cmd=(/path/to/executable --option --message="hello world" --)
cmd=("${cmd[@]}" "$file1" "$file2")
"${cmd[@]}"
Nếu bạn đang sử dụng shell không có mảng thì sao? Bạn vẫn có thể sử dụng các tham số vị trí, nếu bạn không sửa đổi chúng.
set -- /path/to/executable --option --message="hello world" --
set -- "$@" "$file1" "$file2"
"$@"
Điều gì nếu bạn cần lưu trữ một lệnh shell phức tạp, ví dụ như với chuyển hướng, đường ống, v.v.? Hoặc nếu bạn không muốn sửa đổi các tham số vị trí? Sau đó, bạn có thể xây dựng một chuỗi chứa lệnh và sử dụng eval
nội dung.
code='/path/to/executable --option --message="hello world" -- /path/to/file1 | grep "interesting stuff"'
eval "$code"
Lưu ý các trích dẫn lồng nhau trong định nghĩa code
: các trích dẫn đơn '…'
phân định một chuỗi bằng chữ, sao cho giá trị của biến code
là chuỗi /path/to/executable --option --message="hello world" -- /path/to/file1
. Nội dung thông báo eval
cho shell phân tích chuỗi được truyền dưới dạng đối số như thể nó xuất hiện trong tập lệnh, vì vậy tại thời điểm đó, dấu ngoặc kép và đường ống được phân tích cú pháp, v.v.
Sử dụng eval
là khó khăn. Hãy suy nghĩ cẩn thận về những gì được phân tích cú pháp khi. Cụ thể, bạn không thể chỉ nhét tên tệp vào mã: bạn cần trích dẫn nó, giống như bạn sẽ làm nếu nó nằm trong tệp mã nguồn. Không có cách nào trực tiếp để làm điều đó. Một cái gì đó như code="$code $filename"
vỡ nếu tên tập tin chứa bất kỳ vỏ ký tự đặc biệt (số lượng, $
, ;
, |
, <
, >
, vv). code="$code \"$filename\""
vẫn phá vỡ trên "$\`
. Thậm chí code="$code '$filename'"
phá vỡ nếu tên tập tin chứa a '
. Có hai giải pháp.
Thêm một lớp trích dẫn xung quanh tên tập tin. Cách dễ nhất để làm điều đó là thêm các trích dẫn đơn xung quanh nó và thay thế các trích dẫn đơn bằng '\''
.
quoted_filename=$(printf %s. "$filename" | sed "s/'/'\\\\''/g")
code="$code '${quoted_filename%.}'"
Giữ mở rộng biến trong mã, để nó tìm kiếm khi mã được ước tính, không phải khi đoạn mã được xây dựng. Điều này đơn giản hơn nhưng chỉ hoạt động nếu biến vẫn ở xung quanh với cùng một giá trị tại thời điểm mã được thực thi, chứ không phải ví dụ nếu mã được xây dựng trong một vòng lặp.
code="$code \"\$filename\""
Cuối cùng, bạn có thực sự cần một biến chứa mã? Cách tự nhiên nhất để đặt tên cho khối mã là xác định hàm.
Có chuyện read
gì thế?
Không có -r
, read
cho phép các dòng tiếp tục - đây là một dòng logic đầu vào duy nhất:
hello \
world
read
chia dòng đầu vào thành các trường được phân tách bằng các ký tự trong $IFS
(không có -r
dấu gạch chéo ngược cũng thoát khỏi các trường đó). Ví dụ: nếu đầu vào là một dòng chứa ba từ, sau đó read first second third
đặt first
thành từ đầu tiên của từ đầu tiên, second
thành từ thứ hai và third
từ thứ ba. Nếu có nhiều từ hơn, biến cuối cùng chứa mọi thứ còn lại sau khi đặt các từ trước đó. Khoảng trắng hàng đầu và dấu vết được cắt.
Đặt IFS
thành chuỗi trống sẽ tránh mọi sự cắt xén. Xem tại sao `while IFS = read` được sử dụng thường xuyên như vậy, thay vì` IFS =; trong khi đọc..`? cho một lời giải thích dài hơn.
Có chuyện gì với bạn xargs
vậy?
Định dạng đầu vào của các xargs
chuỗi được phân tách bằng khoảng trắng có thể tùy ý là một hoặc hai trích dẫn. Không có công cụ tiêu chuẩn đầu ra định dạng này.
Đầu vào xargs -L1
hoặc xargs -l
gần như là một danh sách các dòng, nhưng không hoàn toàn - nếu có một khoảng trắng ở cuối dòng, dòng sau đây là một dòng tiếp tục.
Bạn có thể sử dụng xargs -0
ở nơi áp dụng (và nếu có: GNU (Linux, Cygwin), BusyBox, BSD, OSX, nhưng không có trong POSIX). Điều đó an toàn, bởi vì byte rỗng không thể xuất hiện trong hầu hết dữ liệu, đặc biệt là tên tệp. Để tạo danh sách tên tệp được phân tách bằng null, hãy sử dụng find … -print0
(hoặc bạn có thể sử dụng find … -exec …
như được giải thích bên dưới).
Làm cách nào để xử lý tệp được tìm thấy bởi find
?
find … -exec some_command a_parameter another_parameter {} +
some_command
cần phải là một lệnh bên ngoài, nó không thể là hàm shell hoặc bí danh. Nếu bạn cần gọi shell để xử lý tệp, hãy gọi sh
một cách rõ ràng.
find … -exec sh -c '
for x do
… # process the file "$x"
done
' find-sh {} +
Tôi có một số câu hỏi khác
Duyệt thẻ trích dẫn trên trang web này, hoặc shell hoặc shell-script . (Nhấp vào “tìm hiểu thêm ...” để xem một số lời khuyên chung và một danh sách tay chọn các câu hỏi thường gặp). Nếu bạn đã tìm kiếm và bạn không thể tìm thấy câu trả lời, hỏi đi .
shellcheck
giúp bạn cải thiện chất lượng chương trình của bạn.