Đặt dấu nháy trong shell để giữ nguyên tên tệp có khoảng trắng
Theo dõi cách Bash tạo đối số, xử lý tên tệp có khoảng trắng, ký tự đại diện và giá trị rỗng mà không làm mất ranh giới giữa chúng.

Dấu nháy trong shell giữ ranh giới giữa các đối số. Trong Bash, "$file" truyền đúng một đối số chứa giá trị của biến file; còn $file không có dấu nháy có thể bị tách thành nhiều từ rồi được khai triển thành tên tệp. Vì vậy, một lệnh chạy đúng với report.txt vẫn có thể hỏng khi gặp quarterly report.txt.
Bài này dành cho bạn đang viết một script bảo trì nhỏ. Các ví dụ dùng tệp tạm và in rõ từng đối số để quan sát hành vi, không xóa dữ liệu thật hay tải dữ liệu lên dịch vụ nào. Các quy tắc được trình bày là của Bash. Nhiều shell có điểm tương tự, nhưng cú pháp mảng và một số chi tiết khai triển có thể khác.
Xem lệnh thực sự nhận được gì
Phần lớn chương trình không nhận nguyên một chuỗi lệnh rồi tự diễn giải lại như shell. Bash chuẩn bị một danh sách đối số, thường gọi là vector đối số. Hàm nhỏ sau đánh số những đối số được truyền vào hàm:
show_args() {
local i=0
for arg in "$@"; do
printf 'argv[%d]=<%s>\n' "$i" "$arg"
i=$((i + 1))
done
}
file='quarterly report.txt'
show_args "$file"
show_args $file
Lần gọi có dấu nháy truyền một đối số. Với giá trị IFS thông thường, lần gọi không có dấu nháy truyền riêng quarterly và report.txt. Chương trình nhận chúng không thể khôi phục chắc chắn tên tệp ban đầu: nó không biết khoảng trắng từng nằm trong một biến hay từng ngăn cách hai đối số mà người gọi chủ ý truyền vào.
Dấu nháy nằm bên trong giá trị biến không tự trở thành cú pháp shell trong quá trình khai triển thông thường. Gán file='"quarterly report.txt"' chỉ lưu thêm hai ký tự dấu nháy. Khai triển biến đó mà không đặt dấu nháy ở chỗ gọi vẫn không giữ được ranh giới mong muốn. Cần đặt dấu nháy ngay trong câu lệnh sử dụng giá trị.
Nhánh trên giữ nguyên một đối số. Nhánh dưới minh họa bước tách từ sau khi khai triển không có dấu nháy. Các khung trong hình biểu diễn ranh giới đối số, không phải ký tự được thêm vào tên tệp.
Ký tự đại diện tạo ra một kiểu lỗi khác
Khoảng trắng chỉ là một phần của vấn đề. Biến không được đặt trong dấu nháy mà chứa *.log có thể được Bash khai triển thành những đường dẫn khớp trong thư mục hiện tại. Hành vi lúc này phụ thuộc vào các tệp đang tồn tại ở thời điểm script chạy.
work=$(mktemp -d)
(
cd "$work" || exit 1
touch -- 'one.log' 'two.log'
pattern='*.log'
show_args "$pattern"
show_args $pattern
)
Lần gọi đầu truyền nguyên mẫu tìm kiếm. Lần thứ hai để Bash thực hiện phép khớp tên tệp, thường gọi là glob. Cả hai đều có thể đúng trong một ngữ cảnh cụ thể. Công cụ tìm kiếm có thể cần nhận nguyên mẫu, còn vòng lặp shell có thể chủ ý duyệt các tệp khớp mẫu. Điều cần xác định là thành phần nào chịu trách nhiệm khớp.
Chẳng hạn, find "$work" -name '*.log' truyền mẫu cho find. Nếu bỏ dấu nháy đơn, shell có thể khai triển mẫu trước khi find khởi động. Danh sách đối số khi đó thay đổi; thông báo lỗi nhận được có thể khiến bạn tưởng nguyên nhân nằm ở một tùy chọn khác.
Duyệt đường dẫn mà không biến chúng thành văn bản trung gian
Một vòng lặp dễ hỏng là for file in $(ls ...). Phép thay thế lệnh tạo ra văn bản, bỏ các ký tự xuống dòng ở cuối, rồi kết quả không có dấu nháy tiếp tục bị tách từ và khai triển. Nếu tên tệp tự chứa ký tự xuống dòng, cách biểu diễn mỗi tên trên một dòng đã không còn rõ ràng, kể cả khi đã xử lý khoảng trắng.
Với một thư mục, có thể dùng glob có chủ đích và đặt dấu nháy quanh biến khi sử dụng:
(
shopt -s nullglob
for file in "$work"/*.log; do
printf 'Would inspect: %q\n' "$file"
done
)
Phần tiền tố thư mục có dấu nháy và phần *.log không có dấu nháy làm hai việc khác nhau. Tiền tố được giữ nguyên, còn mẫu chọn các mục phù hợp. Mỗi kết quả khớp trở thành một giá trị riêng của biến vòng lặp. Tùy chọn nullglob khiến mẫu không khớp biến mất, nên thư mục rỗng không tạo ra một tên tệp giả chứa dấu sao. Khối subshell giới hạn thay đổi tùy chọn trong chính khối đó.
Khi cần duyệt đệ quy, find ... -print0 kết hợp với bộ đọc hiểu dấu phân cách null có thể giữ nguyên mọi byte hợp lệ trong tên tệp, ngoại trừ byte null vốn không được phép xuất hiện trong tên tệp Linux. Nếu thao tác có thể viết trực tiếp bằng find -exec, cách đó cũng tránh việc phân tích lại danh sách đã in. Chọn dạng đơn giản phù hợp với thao tác cần làm.
Tùy chọn của chương trình là một ranh giới riêng
Dấu nháy không ngăn tên tệp như -n bị chương trình nhận hiểu là một tùy chọn. Với lệnh có hỗ trợ, -- đánh dấu điểm kết thúc phần tùy chọn:
printf '%s\n' "$file"
touch -- "$work/-n"
Chuỗi định dạng cố định của printf cũng là một ranh giới. Nếu dùng văn bản không đáng tin cậy làm chuỗi định dạng, các dấu phần trăm trong văn bản sẽ mang ý nghĩa đặc biệt. Giữ nguyên định dạng và truyền dữ liệu qua đối số riêng sẽ tránh nhầm lẫn đó.
Khi một hàm chuyển tiếp các đối số, dùng "$@" để giữ từng phần tử. "$*" nối chúng thành một đối số, dùng ký tự đầu tiên của IFS làm dấu phân cách. Khác biệt này quan trọng với hàm bọc quanh trình biên dịch hoặc công cụ triển khai: hàm bọc nên giữ yêu cầu của người gọi, trừ khi việc nối chuỗi là một phần được mô tả rõ trong giao diện của nó.
Mảng phù hợp khi script xây dựng lệnh từ nhiều tùy chọn. Mỗi phần tử mảng đã là một đối số; "${args[@]}" giữ nguyên các phần tử đó. Nối tất cả thành chuỗi rồi gọi eval lại yêu cầu shell diễn giải dữ liệu thành mã thêm một lần. Script bảo trì thông thường hiếm khi cần bước phân tích bổ sung này.
Tài liệu Bash mô tả riêng dấu nháy, các bước khai triển và mảng vì chúng tác động đến những giai đoạn khác nhau. Khi rà soát script, theo đường đi của một giá trị từ lúc đọc vào đến lệnh cuối cùng. Kiểm tra tách từ, glob và phân tích tùy chọn ở đúng ranh giới đó. Một tên có khoảng trắng, dấu sao hoặc dấu gạch ngang đầu tên giúp kiểm tra tốt hơn một tệp nữa tên report.txt.
Chủ động giữ giá trị rỗng và thêm đối số tùy chọn
Biến rỗng làm lộ thêm một phần của giao ước. Khai triển biến rỗng trong dấu nháy truyền một đối số rỗng; khai triển không có dấu nháy thường biến mất hoàn toàn. Ứng dụng có thể phân biệt giữa không cung cấp giá trị cấu hình và cung cấp một giá trị rỗng. Hai cách viết vì thế không thay thế cho nhau. Khi chẩn đoán, hãy xem cả số lượng đối số lẫn những ký tự được in ra.
Hàm dưới dùng phép chuyển đổi %q của Bash để làm rõ khoảng trắng và ký tự xuống dòng. Đây là cách biểu diễn phục vụ chẩn đoán trong Bash, không phải định dạng trao đổi dùng chung cho mọi chương trình. Mảng chứa bốn phần tử, trong đó có một phần tử rỗng và một tên tệp chứa xuống dòng.
show_escaped() {
printf 'count=%d\n' "$#"
for argument in "$@"; do
printf '<%q>\n' "$argument"
done
}
inputs=('quarterly report.txt' '' '*.log' $'two\nlines.txt')
show_escaped "${inputs[@]}"
empty=''
show_escaped "$empty"
show_escaped $empty
Ba lần gọi lần lượt báo bốn, một và không đối số. Các phần tử mảng không bị glob vì phép khai triển được bao bằng dấu nháy kép. Ký tự xuống dòng vẫn thuộc một phần tử. Nếu in nó bằng định dạng chuỗi thông thường và thấy hai dòng trên màn hình, điều đó không có nghĩa lệnh đã nhận hai đối số.
Bảng giả định cách tách trường thông thường và đúng hai tệp log khớp ở hàng cuối. Hàng giá trị rỗng đặc biệt hữu ích khi kiểm tra hàm bọc: giữ một đối số rỗng có thể là đúng, nhưng thêm một đối số rỗng cho tùy chọn đáng ra phải vắng mặt lại có thể sai.
Hãy biểu diễn đối số tùy chọn bằng phần tử mảng chỉ được thêm khi cần. Ví dụ, bắt đầu với args=(inspect), chỉ thêm cờ in chi tiết khi người gọi yêu cầu, sau đó thêm -- và tên tệp nếu chương trình đích hỗ trợ dấu phân cách đó. Cách này tách ngữ pháp tùy chọn của chương trình khỏi quy tắc khai triển của shell. Không cần biến toàn bộ lệnh thành chuỗi chỉ để nó trông giống một dòng lệnh gõ tay.
Theo dõi cả bộ phân tích tiếp theo
Dấu nháy ngăn một số phép biến đổi của shell. Nó không bảo đảm công cụ nhận sẽ coi mọi đối số là tên tệp nguyên văn. Tùy chọn tìm kiếm bằng biểu thức chính quy vẫn diễn giải cú pháp mẫu; giao diện shell từ xa có thể chuyển văn bản sang một shell khác; chương trình có thể đọc và phân tích tệp tham số được chỉ định trong đối số. Mỗi bộ phân tích bổ sung cần một ranh giới dữ liệu được mô tả riêng.
Giải thích SC2086 của ShellCheck liên hệ trực tiếp khai triển không có dấu nháy với tách từ và glob, đồng thời nêu các ngoại lệ có chủ đích. Bài học về công cụ shell của Missing Semester cung cấp bối cảnh học rộng hơn. Các nguồn này hỗ trợ cách rà soát; tài liệu Bash xác định hành vi dùng trong ví dụ. Việc đọc ghi chú khóa học không đồng nghĩa đã làm mọi bài tập hoặc xem video bài giảng.
Để tự kiểm tra, truyền mảng bốn phần tử qua một hàm bọc dùng "$*", rồi đổi sang "$@". Dự đoán số đối số trước khi chạy. Giải thích vì sao phiên bản thứ hai giữ được yêu cầu ban đầu và vì sao phiên bản thứ nhất không thể khôi phục chắc chắn các ranh giới sau khi nối. Nắm được lý do này hữu ích hơn việc chỉ nhớ thêm dấu nháy mỗi khi script gặp lỗi.