Tại sao tập lệnh shell của tôi bị nghẹt trên khoảng trắng hoặc các ký tự đặc biệt khác?


284

Hoặc, một hướng dẫn giới thiệu để xử lý tên tệp mạnh mẽ và chuỗi khác chuyển qua các tập lệnh shell.

Tôi đã viết một kịch bản shell hoạt động tốt hầu hết thời gian. Nhưng nó bóp nghẹt một số đầu vào (ví dụ trên một số tên tệp).

Tôi đã gặp một vấn đề như sau:

  • Tôi có một tên tệp chứa một khoảng trắng hello worldvà nó được coi là hai tệp riêng biệt helloworld.
  • Tôi có một dòng đầu vào với hai khoảng trắng liên tiếp và chúng co lại thành một trong đầu vào.
  • Khoảng trắng hàng đầu và dấu vết biến mất khỏi dòng đầu vào.
  • Đôi khi, khi đầu vào chứa một trong các ký tự \[*?, chúng được thay thế bằng một số văn bản thực sự là tên của các tệp.
  • Có một dấu nháy đơn '(hoặc một trích dẫn kép ") trong đầu vào và mọi thứ trở nên kỳ lạ sau thời điểm đó.
  • Có một dấu gạch chéo ngược trong đầu vào (hoặc: Tôi đang sử dụng Cygwin và một số tên tệp của tôi có \dấu phân cách kiểu Windows ).

Điều gì đang xảy ra và làm thế nào để tôi sửa chữa nó?


16
shellcheckgiúp bạn cải thiện chất lượng chương trình của bạn.
aurelien

3
Bên cạnh các kỹ thuật bảo vệ được mô tả trong các câu trả lời, và mặc dù có thể rõ ràng đối với hầu hết người đọc, tôi nghĩ có thể nhận xét rằng khi các tệp được xử lý bằng các công cụ dòng lệnh, thì nên tránh các ký tự ưa thích trong tên ở nơi đầu tiên, nếu có thể.
bli


1
@bli Không, điều đó làm cho chỉ có lỗi mất nhiều thời gian hơn để bật lên. Hôm nay nó đang che giấu lỗi. Và bây giờ, bạn không biết tất cả tên tệp sau này được sử dụng với mã của bạn.
Volker Siegel

Trước hết, nếu các tham số của bạn chứa khoảng trắng thì chúng cần được trích dẫn đi vào (trên dòng lệnh). Tuy nhiên, bạn có thể lấy toàn bộ dòng lệnh và tự phân tích nó. Hai không gian không chuyển sang một không gian; bất kỳ dung lượng nào cũng cho biết tập lệnh của bạn là biến tiếp theo, vì vậy nếu bạn làm một cái gì đó như "echo $ 1 $ 2" thì đó là tập lệnh của bạn đặt một khoảng trắng ở giữa. Đồng thời sử dụng "find (-exec)" để lặp lại các tệp có khoảng trắng thay vì vòng lặp for; bạn có thể đối phó với các không gian dễ dàng hơn.
Patrick Taylor

Câu trả lời:


352

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 $footrí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 readdựng sẵn, sử dụngwhile IFS= read -r line; do …
    Plain readxử 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?

$fookhô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ử IFSkhô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 myfileschứa chuỗi 5 ký tự *.txtvà khi bạn viết $myfilesrằ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 setvà 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 fthà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 evalnộ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 codelà chuỗi /path/to/executable --option --message="hello world" -- /path/to/file1. Nội dung thông báo evalcho 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 evallà 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 readgì thế?

Không có -r, readcho 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

readchia 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ó -rdấ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 firstthành từ đầu tiên của từ đầu tiên, secondthành từ thứ hai và thirdtừ 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 IFSthà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 xargsvậy?

Định dạng đầu vào của các xargschuỗ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 -L1hoặc xargs -lgầ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_commandcầ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 shmộ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ên trang web này, hoặc hoặc . (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 .


6
@ John1024 Đây chỉ là một tính năng của GNU, vì vậy tôi sẽ gắn bó với không có công cụ tiêu chuẩn nào.
Gilles

2
Bạn cũng cần trích dẫn xung quanh $(( ... ))(cũng $[...]trong một số shell) ngoại trừ trong zsh(ngay cả trong mô phỏng sh) và mksh.
Stéphane Chazelas

3
Lưu ý rằng đó xargs -0không phải là POSIX. Ngoại trừ FreeBSD xargs, bạn thường muốn xargs -r0thay vì xargs -0.
Stéphane Chazelas

2
@ John1024, không, ls --quoting-style=shell-alwayskhông tương thích với xargs. Hãy thửtouch $'a\nb'; ls --quoting-style=shell-always | xargs
Stéphane Chazelas

3
Một tính năng hay khác (chỉ dành cho GNU) là xargs -d "\n"để bạn có thể chạy, ví dụ như locate PATTERN1 |xargs -d "\n" grep PATTERN2để tìm kiếm các tên tệp khớp với PATTERN1 với nội dung khớp với PATTERN2 . Không có GNU, bạn có thể làm điều đó, ví dụ nhưlocate PATTERN1 |perl -pne 's/\n/\0/' |xargs -0 grep PATTERN1
Adam Katz

26

Trong khi câu trả lời của Gilles là tuyệt vời, tôi đưa ra vấn đề ở điểm chính của anh ấy

Luôn sử dụng dấu ngoặc kép xung quanh thay thế biến và thay thế lệnh: "$ foo", "$ (foo)"

Khi bạn đang bắt đầu với một vỏ giống như Bash mà tách từ, tất nhiên lời khuyên an toàn là luôn sử dụng dấu ngoặc kép. Tuy nhiên, việc tách từ không phải lúc nào cũng được thực hiện

§ Chia tách từ

Các lệnh này có thể được chạy mà không có lỗi

foo=$bar
bar=$(a command)
logfile=$logdir/foo-$(date +%Y%m%d)
PATH=/usr/local/bin:$PATH ./myscript
case $foo in bar) echo bar ;; baz) echo baz ;; esac

Tôi không khuyến khích người dùng chấp nhận hành vi này, nhưng nếu ai đó hiểu chắc chắn khi xảy ra sự chia tách từ thì họ có thể tự quyết định khi nào nên sử dụng dấu ngoặc kép.


19
Như tôi đã đề cập trong câu trả lời của mình, hãy xem unix.stackexchange.com/questions/68694/ để biết chi tiết. Đừng để ý câu hỏi - Tại sao kịch bản shell của tôi bị sặc? Vấn đề phổ biến nhất (từ nhiều năm kinh nghiệm trên trang web này và các nơi khác) là thiếu dấu ngoặc kép. Luôn luôn sử dụng dấu ngoặc kép. Dễ dàng ghi nhớ hơn so với sử dụng dấu ngoặc kép, ngoại trừ những trường hợp không cần thiết.
Gilles

14
Các quy tắc rất khó hiểu cho người mới bắt đầu. Ví dụ, foo=$barlà OK, nhưng export foo=$barhoặc env foo=$varkhông (ít nhất là trong một số shell). Một lời khuyên cho người mới bắt đầu: luôn trích dẫn các biến của bạn trừ khi bạn biết bạn đang làm gì và có lý do chính đáng để không .
Stéphane Chazelas

5
@StevenPenny Có thực sự đúng hơn không? Có trường hợp hợp lý mà trích dẫn sẽ phá vỡ kịch bản? Trong các tình huống trong đó một nửa trường hợp phải sử dụng dấu ngoặc kép và trong nửa trích dẫn khác có thể được sử dụng tùy ý - thì một khuyến nghị "luôn luôn sử dụng dấu ngoặc kép, chỉ trong trường hợp" là điều nên được suy nghĩ, vì nó đúng, đơn giản và ít rủi ro. Dạy những danh sách ngoại lệ như vậy cho người mới bắt đầu được biết là không hiệu quả (thiếu ngữ cảnh, họ sẽ không nhớ chúng) và phản tác dụng, vì họ sẽ nhầm lẫn các trích dẫn cần thiết / không cần thiết, phá vỡ kịch bản của họ và giải thích chúng để tìm hiểu thêm.
Peteris

6
0,02 đô la của tôi sẽ là đề nghị trích dẫn mọi thứ là lời khuyên tốt. Trích dẫn sai một cái gì đó không cần nó là vô hại, sai lầm khi trích dẫn một cái gì đó không cần nó là có hại. Vì vậy, đối với phần lớn các tác giả kịch bản shell, những người sẽ không bao giờ hiểu được sự phức tạp của việc phân tách từ chính xác xảy ra, trích dẫn mọi thứ an toàn hơn nhiều so với cố gắng chỉ trích dẫn khi cần thiết.
trời ơi

5
@Peteris và godlygeek: "Có trường hợp hợp lý nào mà trích dẫn sẽ phá vỡ kịch bản không?" Nó phụ thuộc vào định nghĩa của bạn về "hợp lý". Nếu một tập lệnh thiết lập criteria="-type f", sau đó find . $criteriahoạt động nhưng find . "$criteria"không.
G-Man

22

Theo tôi biết, chỉ có hai trường hợp cần phải trích dẫn mở rộng hai lần, và những trường hợp đó liên quan đến hai tham số shell đặc biệt "$@""$*"- được chỉ định để mở rộng khác nhau khi được đặt trong dấu ngoặc kép. Trong tất cả các trường hợp khác (không bao gồm, có lẽ, triển khai mảng dành riêng cho hệ vỏ) , hành vi của một bản mở rộng là một thứ có thể định cấu hình - có các tùy chọn cho điều đó.

Tất nhiên, điều này không có nghĩa là nên tránh trích dẫn kép - ngược lại, đây có lẽ là phương pháp thuận tiện và mạnh mẽ nhất để phân định một bản mở rộng mà shell phải cung cấp. Nhưng, tôi nghĩ, vì các lựa chọn thay thế đã được giải thích một cách thành thạo, đây là một nơi tuyệt vời để thảo luận về những gì xảy ra khi vỏ mở rộng một giá trị.

Shell, trong trái tim và linh hồn của nó (đối với những người có như vậy) , là một trình thông dịch lệnh - nó là một trình phân tích cú pháp, giống như một tương tác lớn , sed. Nếu câu lệnh shell của bạn bị nghẹt trong khoảng trắng hoặc tương tự thì rất có thể là do bạn chưa hiểu đầy đủ về quy trình giải thích của shell - đặc biệt là cách thức và lý do tại sao nó chuyển một câu lệnh đầu vào thành một lệnh có thể thao tác. Công việc của shell là:

  1. chấp nhận đầu vào

  2. giải thích và phân chia chính xác thành các từ đầu vào tokenized

    • từ đầu vào là các mục cú pháp shell như $wordhoặcecho $words 3 4* 5

    • các từ luôn được phân chia trên khoảng trắng - đó chỉ là cú pháp - nhưng chỉ các ký tự khoảng trắng theo nghĩa đen được phân phát cho trình bao trong tệp đầu vào của nó

  3. mở rộng những thứ đó nếu cần thiết thành nhiều lĩnh vực

    • các trường kết quả từ việc mở rộng từ - chúng tạo thành lệnh thực thi cuối cùng

    • ngoại trừ "$@", $IFS chia tách trườngmở rộng tên đường dẫn, một từ đầu vào phải luôn luôn đánh giá thành một trường duy nhất .

  4. và sau đó để thực hiện lệnh kết quả

    • trong hầu hết các trường hợp, điều này liên quan đến việc chuyển qua kết quả giải thích của nó dưới hình thức này hay hình thức khác

Mọi người thường nói vỏ là một chất keo , và, nếu điều này là đúng, thì cái mà nó đang dán là danh sách các đối số - hoặc các trường - cho một quá trình này hay quá trình khác khi chúng execlà chúng. Hầu hết các shell không xử lý NULtốt byte - nếu hoàn toàn - và điều này là do chúng đã tách trên nó. Shell phải exec có rất nhiều và nó phải thực hiện điều này với một NULloạt các đối số được phân tách mà nó trao cho kernel hệ thống tại execthời điểm đó. Nếu bạn xen kẽ dấu phân cách của shell với dữ liệu được phân tách của nó thì shell có thể sẽ làm hỏng nó. Cấu trúc dữ liệu nội bộ của nó - giống như hầu hết các chương trình - dựa vào dấu phân cách đó. zsh, đáng chú ý, không làm hỏng việc này.

Và đó là nơi $IFSxuất hiện. $IFSLuôn luôn có mặt - và tương tự có thể giải quyết - tham số shell xác định cách shell sẽ phân chia mở rộng shell từ từ này sang trường khác - cụ thể về giá trị mà các trường đó nên phân định. $IFSchia tách các mở rộng shell trên các dấu phân cách khác NUL- hoặc, nói cách khác, shell thay thế các byte dẫn đến sự mở rộng khớp với các giá trị trong $IFSvới NULcác mảng dữ liệu bên trong của nó. Khi bạn nhìn vào nó như thế bạn có thể bắt đầu thấy rằng mọi mở rộng vỏ phân tách trường là một $IFSmảng dữ liệu được phân định giới hạn.

Điều quan trọng là phải hiểu rằng $IFSchỉ phân định các mở rộng chưa được phân định bằng cách khác - điều mà bạn có thể làm với "dấu ngoặc kép. Khi bạn trích dẫn một bản mở rộng, bạn phân định nó ở phần đầu và ít nhất là phần đuôi của giá trị của nó. Trong những trường hợp $IFSkhông áp dụng vì không có trường để tách. Trong thực tế, một mở rộng được trích dẫn kép thể hiện hành vi phân tách trường giống hệt với một mở rộng không trích dẫn khi IFS=được đặt thành một giá trị trống.

Trừ khi được trích dẫn, $IFSbản thân nó là một $IFSmở rộng vỏ được phân định. Nó mặc định là một giá trị được chỉ định của <space><tab><newline>- cả ba trong số đó thể hiện các thuộc tính đặc biệt khi được chứa trong đó $IFS. Trong khi bất kỳ giá trị nào khác $IFSđược chỉ định để đánh giá một trường duy nhất cho mỗi lần xuất hiện mở rộng , thì $IFS khoảng trắng - bất kỳ trong số ba giá trị đó - được chỉ định để tách thành một trường duy nhất trên mỗi chuỗi mở rộng và các chuỗi dẫn / theo dõi hoàn toàn bị tách biệt. Điều này có lẽ dễ hiểu nhất qua ví dụ.

slashes=///// spaces='     '
IFS=/; printf '<%s>' $slashes$spaces
<><><><><><     >
IFS=' '; printf '<%s>' $slashes$spaces
</////>
IFS=; printf '<%s>' $slashes$spaces
</////     >
unset IFS; printf '<%s>' "$slashes$spaces"
</////     >

Nhưng đó chỉ là $IFS- chỉ phân tách từ hoặc khoảng trắng như được hỏi, vậy các ký tự đặc biệt là gì?

Shell - theo mặc định - cũng sẽ mở rộng một số mã thông báo không được trích dẫn ( ?*[như được ghi chú ở nơi khác ở đây) thành nhiều trường khi chúng xuất hiện trong danh sách. Điều này được gọi là mở rộng tên đường dẫn , hoặc globalbing . Nó là một công cụ cực kỳ hữu ích và, vì nó xuất hiện sau khi phân tách trường theo thứ tự phân tích của shell, nó không bị ảnh hưởng bởi $ IFS - các trường được tạo bởi một phần mở rộng tên đường dẫn được phân định trên đầu / đuôi của tên tệp bất kể nội dung của chúng chứa bất kỳ ký tự nào hiện tại $IFS. Hành vi này được đặt thành bật theo mặc định - nhưng nó rất dễ dàng được cấu hình khác.

set -f

Điều đó chỉ dẫn cho vỏ không phải toàn cầu . Việc mở rộng tên đường dẫn sẽ không xảy ra ít nhất cho đến khi cài đặt đó bằng cách nào đó được hoàn tác - chẳng hạn như nếu lớp vỏ hiện tại được thay thế bằng một quy trình vỏ mới khác hoặc ....

set +f

... được cấp cho vỏ. Báo giá kép - như chúng cũng thực hiện để $IFS phân tách trường - khiến cài đặt toàn cầu này không cần thiết cho mỗi lần mở rộng. Vì thế:

echo "*" *

... nếu hiện tại mở rộng tên đường dẫn sẽ có khả năng tạo ra các kết quả rất khác nhau cho mỗi đối số - vì lần đầu tiên sẽ chỉ mở rộng thành giá trị theo nghĩa đen của nó (ký tự dấu hoa thị duy nhất, không phải là tất cả) và chỉ thứ hai là giống nhau nếu thư mục làm việc hiện tại không chứa tên tệp nào có thể khớp (và nó khớp với hầu hết tất cả chúng) . Tuy nhiên nếu bạn làm:

set -f; echo "*" *

... kết quả cho cả hai đối số là giống hệt nhau - *không mở rộng trong trường hợp đó.


Tôi thực sự đồng ý với @ StéphaneChazelas rằng nó (phần lớn) gây nhầm lẫn nhiều thứ hơn là giúp đỡ ... nhưng tôi thấy nó hữu ích, cá nhân, vì vậy tôi đã ủng hộ. Bây giờ tôi có một ý tưởng tốt hơn (và một số ví dụ) về cách IFSthực sự hoạt động. Những gì tôi không nhận được là lý do tại sao nó sẽ không bao giờ là một ý tưởng tốt để thiết lập IFSmột cái gì đó khác hơn so với mặc định.
tự đại diện

1
@Wildcard - đó là một dấu phân cách trường. nếu bạn có một giá trị trong một biến mà bạn muốn mở rộng ra nhiều trường bạn chia nó ra $IFS. cd /usr/bin; set -f; IFS=/; for path_component in $PWD; do echo $path_component; donein \nsau đó usr\nsau đó bin\n. Đầu tiên echolà trống vì /là một trường null. Các path_components có thể có dòng mới hoặc dấu cách hoặc bất cứ điều gì - sẽ không thành vấn đề vì các thành phần được phân tách /và không phải là giá trị mặc định. mọi người làm điều đó với awktất cả thời gian, dù sao đi nữa. vỏ của bạn cũng làm điều đó
mikeerv

3

Tôi đã có một dự án video lớn với các khoảng trắng trong tên tệp và khoảng trắng trong tên thư mục. Mặc dù find -type f -print0 | xargs -0hoạt động cho một số mục đích và trên các hệ vỏ khác nhau, tôi thấy rằng việc sử dụng IFS tùy chỉnh (dấu tách trường đầu vào) sẽ giúp bạn linh hoạt hơn nếu bạn đang sử dụng bash. Đoạn mã dưới đây sử dụng bash và đặt IFS thành một dòng mới; miễn là không có dòng mới trong tên tệp của bạn:

(IFS=$'\n'; for i in $(find -type f -print) ; do
    echo ">>>$i<<<"
done)

Lưu ý việc sử dụng parens để cô lập định nghĩa lại của IFS. Tôi đã đọc các bài viết khác về cách phục hồi IFS, nhưng điều này chỉ dễ dàng hơn.

Hơn nữa, đặt IFS thành dòng mới cho phép bạn đặt các biến shell trước và dễ dàng in chúng ra. Chẳng hạn, tôi có thể tăng dần một biến V bằng cách sử dụng các dòng mới làm dấu phân cách:

V=""
V="./Ralphie's Camcorder/STREAM/00123.MTS,04:58,05:52,-vf yadif"
V="$V"$'\n'"./Ralphie's Camcorder/STREAM/00111.MTS,00:00,59:59,-vf yadif"
V="$V"$'\n'"next item goes here..."

và tương ứng:

(IFS=$'\n'; for v in $V ; do
    echo ">>>$v<<<"
done)

Bây giờ tôi có thể "liệt kê" cài đặt của V bằng echo "$V"cách sử dụng dấu ngoặc kép để xuất dòng mới. (Tín dụng cho chủ đề này để $'\n'giải thích.)


3
Nhưng sau đó, bạn vẫn sẽ gặp vấn đề với tên tệp có chứa dòng mới hoặc ký tự toàn cầu. Xem thêm: Tại sao lặp đi lặp lại tìm kiếm thực tiễn xấu? . Nếu sử dụng zsh, bạn có thể sử dụng IFS=$'\0'và sử dụng -print0( zshkhông thực hiện toàn cầu khi mở rộng để các ký tự toàn cầu không phải là vấn đề ở đó).
Stéphane Chazelas

1
Điều này hoạt động với các tên tệp chứa khoảng trắng, nhưng nó không hoạt động đối với các tên tệp có khả năng thù địch hoặc các tên tệp vô nghĩa vụng trộm. Bạn có thể dễ dàng khắc phục sự cố tên tệp chứa ký tự đại diện bằng cách thêm set -f. Mặt khác, cách tiếp cận của bạn về cơ bản thất bại với tên tệp chứa dòng mới. Khi xử lý dữ liệu khác với tên tệp, nó cũng thất bại với các mục trống.
Gilles

Phải, lời cảnh báo của tôi là nó sẽ không hoạt động với các dòng mới trong tên tệp. Tuy nhiên, tôi tin rằng chúng ta phải vẽ đường chỉ vì sự điên rồ ;-)
Russ

Và tôi không chắc tại sao điều này lại nhận được một downvote. Đây là một phương pháp hoàn toàn hợp lý để lặp lại tên tập tin có dấu cách. Sử dụng -print0 yêu cầu xargs và có những điều khó khăn khi sử dụng chuỗi đó. Tôi xin lỗi ai đó không đồng ý với câu trả lời của tôi, nhưng đó không phải là lý do để hạ thấp nó.
Nga

0

Xem xét tất cả các hàm ý bảo mật được đề cập ở trên và giả sử bạn tin tưởng và có quyền kiểm soát các biến bạn mở rộng, có thể có nhiều đường dẫn với khoảng trắng sử dụng eval. Nhưng hãy cẩn thận!

$ FILES='"a b" c'
$ eval ls $FILES
ls: a b: No such file or directory
ls: c: No such file or directory
$ FILES='a\ b c'
$ eval ls $FILES
ls: a b: No such file or directory
ls: c: No such file or directory
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.