Vâng, chúng tôi thấy một số điều như:
while read line; do
echo $line | cut -c3
done
Hoặc tồi tệ hơn:
for line in `cat file`; do
foo=`echo $line | awk '{print $2}'`
echo whatever $foo
done
(đừng cười, tôi đã thấy nhiều trong số đó).
Nói chung từ người mới bắt đầu kịch bản shell. Đó là những bản dịch theo nghĩa đen ngây thơ về những gì bạn sẽ làm bằng các ngôn ngữ bắt buộc như C hoặc python, nhưng đó không phải là cách bạn làm mọi thứ bằng vỏ sò, và những ví dụ đó rất không hiệu quả, hoàn toàn không đáng tin cậy (có thể dẫn đến các vấn đề bảo mật) và nếu bạn từng quản lý để sửa hầu hết các lỗi, mã của bạn trở nên không thể đọc được.
Về mặt khái niệm
Trong C hoặc hầu hết các ngôn ngữ khác, các khối xây dựng chỉ là một cấp trên hướng dẫn máy tính. Bạn nói với bộ xử lý của bạn phải làm gì và sau đó phải làm gì. Bạn cầm bộ xử lý của mình bằng tay và quản lý vi mô: bạn mở tệp đó, bạn đọc nhiều byte đó, bạn làm điều này, bạn làm điều đó với nó.
Vỏ là một ngôn ngữ cấp cao hơn. Người ta có thể nói nó thậm chí không phải là một ngôn ngữ. Họ trước tất cả các thông dịch viên dòng lệnh. Công việc được thực hiện bởi những lệnh bạn chạy và shell chỉ nhằm mục đích sắp xếp chúng.
Một trong những điều tuyệt vời mà Unix giới thiệu là đường ống và các luồng stdin / stdout / stderr mặc định mà tất cả các lệnh xử lý theo mặc định.
Trong 45 năm, chúng tôi đã không tìm thấy API tốt hơn để khai thác sức mạnh của các lệnh và để chúng hợp tác với một nhiệm vụ. Đó có lẽ là lý do chính tại sao mọi người vẫn sử dụng đạn pháo ngày nay.
Bạn đã có một công cụ cắt và một công cụ chuyển ngữ, và bạn có thể chỉ cần làm:
cut -c4-5 < in | tr a b > out
Shell chỉ đang thực hiện hệ thống ống nước (mở các tệp, thiết lập các đường ống, gọi các lệnh) và khi tất cả đã sẵn sàng, nó chỉ chảy mà không cần vỏ làm gì cả. Các công cụ thực hiện công việc của chúng đồng thời, hiệu quả theo tốc độ của riêng chúng với đủ bộ đệm để không phải cái này chặn cái kia, nó chỉ đẹp và đơn giản.
Gọi một công cụ mặc dù có chi phí (và chúng tôi sẽ phát triển công cụ đó trên điểm hiệu suất). Những công cụ đó có thể được viết với hàng ngàn hướng dẫn trong C. Một quy trình phải được tạo ra, công cụ phải được tải, khởi tạo, sau đó dọn sạch, xử lý bị phá hủy và chờ đợi.
Gọi cut
cũng giống như mở ngăn kéo nhà bếp, lấy con dao, sử dụng nó, rửa sạch, lau khô, đặt lại vào ngăn kéo. Khi bạn làm:
while read line; do
echo $line | cut -c3
done < file
Giống như từng dòng của tập tin, lấy read
công cụ từ ngăn kéo nhà bếp (một thứ rất vụng về vì nó không được thiết kế cho điều đó ), đọc một dòng, rửa công cụ đọc của bạn, đặt lại vào ngăn kéo. Sau đó lên lịch một cuộc họp cho echo
và cut
công cụ, lấy chúng từ ngăn kéo, gọi chúng, rửa chúng, lau khô, đặt chúng trở lại trong ngăn kéo và như vậy.
Một số trong số các công cụ đó ( read
và echo
) được xây dựng trong hầu hết các shell, nhưng điều đó hầu như không tạo ra sự khác biệt ở đây echo
và cut
vẫn cần phải được chạy trong các quy trình riêng biệt.
Nó giống như cắt hành tây nhưng rửa dao và đặt lại vào ngăn kéo bếp giữa mỗi lát.
Ở đây cách rõ ràng là lấy cut
công cụ của bạn từ ngăn kéo, cắt toàn bộ hành tây của bạn và đặt lại vào ngăn kéo sau khi hoàn thành toàn bộ công việc.
IOW, trong shell, đặc biệt là để xử lý văn bản, bạn gọi càng ít tiện ích càng tốt và để chúng hợp tác với nhiệm vụ, không chạy hàng ngàn công cụ theo trình tự chờ từng cái bắt đầu, chạy, dọn sạch trước khi chạy cái tiếp theo.
Đọc thêm trong câu trả lời tốt của Bruce . Các công cụ nội bộ xử lý văn bản cấp thấp trong shell (ngoại trừ có thể zsh
) bị hạn chế, cồng kềnh và thường không phù hợp để xử lý văn bản chung.
Hiệu suất
Như đã nói trước đó, chạy một lệnh có chi phí. Một chi phí rất lớn nếu lệnh đó không được dựng sẵn, nhưng ngay cả khi chúng được dựng sẵn, chi phí vẫn rất lớn.
Và shell không được thiết kế để chạy như vậy, chúng không có ý định trở thành ngôn ngữ lập trình biểu diễn. Họ không phải, họ chỉ là thông dịch viên dòng lệnh. Vì vậy, ít tối ưu hóa đã được thực hiện trên mặt trận này.
Ngoài ra, shell chạy các lệnh trong các quy trình riêng biệt. Những khối xây dựng đó không chia sẻ một bộ nhớ hoặc trạng thái chung. Khi bạn làm một fgets()
hoặc fputs()
trong C, đó là một chức năng trong stdio. stdio giữ bộ đệm nội bộ cho đầu vào và đầu ra cho tất cả các chức năng của stdio, để tránh thực hiện các cuộc gọi hệ thống tốn kém quá thường xuyên.
Thậm chí tiện ích dựng sẵn trình bao tương ứng ( read
, echo
, printf
) có thể không làm điều đó. read
có nghĩa là để đọc một dòng. Nếu nó đọc qua ký tự dòng mới, điều đó có nghĩa là lệnh tiếp theo bạn chạy sẽ bỏ lỡ nó. Vì vậy, read
phải đọc một byte đầu vào cùng một lúc (một số triển khai có tối ưu hóa nếu đầu vào là một tệp thông thường trong đó chúng đọc các đoạn và tìm kiếm lại, nhưng điều đó chỉ hoạt động đối với các tệp thông thường và bash
ví dụ chỉ đọc các đoạn 128 byte. vẫn còn ít hơn nhiều so với các tiện ích văn bản sẽ làm).
Tương tự ở phía đầu ra, echo
không thể chỉ đệm đầu ra của nó, nó phải xuất ngay lập tức vì lệnh tiếp theo bạn chạy sẽ không chia sẻ bộ đệm đó.
Rõ ràng, chạy các lệnh một cách tuần tự có nghĩa là bạn phải chờ đợi chúng, đó là một điệu nhảy lịch trình nhỏ giúp kiểm soát từ vỏ và các công cụ và trở lại. Điều đó cũng có nghĩa (trái ngược với việc sử dụng các công cụ chạy dài trong một đường ống) rằng bạn không thể khai thác nhiều bộ xử lý cùng một lúc khi khả dụng.
Giữa while read
vòng lặp đó và tương đương (được cho là) cut -c3 < file
, trong thử nghiệm nhanh của tôi, có tỷ lệ thời gian CPU khoảng 40000 trong các thử nghiệm của tôi (một giây so với nửa ngày). Nhưng ngay cả khi bạn chỉ sử dụng nội dung shell:
while read line; do
echo ${line:2:1}
done
(ở đây với bash
), đó vẫn là khoảng 1: 600 (một giây so với 10 phút).
Độ tin cậy / mức độ dễ đọc
Rất khó để có được mã đúng. Các ví dụ tôi đưa ra được nhìn thấy quá thường xuyên trong tự nhiên, nhưng chúng có nhiều lỗi.
read
là một công cụ tiện dụng có thể làm nhiều việc khác nhau. Nó có thể đọc đầu vào từ người dùng, chia nó thành các từ để lưu trữ trong các biến khác nhau. read line
không không đọc một dòng đầu vào, hoặc có thể nó đọc một dòng trong một cách rất đặc biệt. Nó thực sự đọc các từ từ đầu vào những từ được phân tách bằng $IFS
và trong đó dấu gạch chéo ngược có thể được sử dụng để thoát khỏi dấu phân cách hoặc ký tự dòng mới.
Với giá trị mặc định là $IFS
, trên một đầu vào như:
foo\/bar \
baz
biz
read line
sẽ lưu trữ "foo/bar baz"
vào $line
, không " foo\/bar \"
như bạn mong đợi.
Để đọc một dòng, bạn thực sự cần:
IFS= read -r line
Điều đó không trực quan lắm, nhưng đó là như vậy, hãy nhớ rằng đạn pháo không được sử dụng như thế.
Tương tự cho echo
. echo
mở rộng trình tự. Bạn không thể sử dụng nó cho các nội dung tùy ý như nội dung của một tệp ngẫu nhiên. Bạn cần printf
ở đây để thay thế.
Và tất nhiên, có một cách quên điển hình là trích dẫn biến của bạn mà mọi người đều rơi vào. Vì vậy, nó nhiều hơn:
while IFS= read -r line; do
printf '%s\n' "$line" | cut -c3
done < file
Bây giờ, một vài cảnh báo nữa:
- ngoại trừ
zsh
, điều đó không hoạt động nếu đầu vào chứa các ký tự NUL trong khi ít nhất các tiện ích văn bản GNU sẽ không gặp vấn đề.
- nếu có dữ liệu sau dòng mới nhất, nó sẽ bị bỏ qua
- Trong vòng lặp, stdin được chuyển hướng, do đó bạn cần chú ý rằng các lệnh trong nó không được đọc từ stdin.
- đối với các lệnh trong các vòng lặp, chúng tôi không chú ý đến việc chúng có thành công hay không. Thông thường, các điều kiện lỗi (đĩa đầy, lỗi đọc ...) sẽ được xử lý kém, thường kém hơn so với tương đương chính xác .
Nếu chúng tôi muốn giải quyết một số vấn đề ở trên, điều đó sẽ trở thành:
while IFS= read -r line <&3; do
{
printf '%s\n' "$line" | cut -c3 || exit
} 3<&-
done 3< file
if [ -n "$line" ]; then
printf '%s' "$line" | cut -c3 || exit
fi
Điều đó ngày càng trở nên dễ đọc hơn.
Có một số vấn đề khác với việc truyền dữ liệu tới các lệnh thông qua các đối số hoặc truy xuất đầu ra của chúng trong các biến:
- giới hạn về kích thước của các đối số (một số triển khai tiện ích văn bản cũng có một giới hạn, mặc dù hiệu quả của những điều đó đạt được thường ít có vấn đề hơn)
- ký tự NUL (cũng là một vấn đề với các tiện ích văn bản).
- các đối số được dùng làm tùy chọn khi chúng bắt đầu bằng
-
(hoặc +
đôi khi)
- nhiều quirks khác nhau của các lệnh khác nhau thường được sử dụng trong các vòng lặp như
expr
, test
...
- các toán tử thao tác văn bản (giới hạn) của các shell khác nhau xử lý các ký tự nhiều byte theo các cách không nhất quán.
- ...
Cân nhắc về Bảo mật
Khi bạn bắt đầu làm việc với các biến shell và đối số cho các lệnh , bạn đang nhập vào trường mỏ.
Nếu bạn quên trích dẫn các biến của mình , hãy quên kết thúc đánh dấu tùy chọn , làm việc ở các vị trí có các ký tự nhiều byte (định mức ngày nay), bạn chắc chắn sẽ đưa ra các lỗi mà sớm muộn gì cũng sẽ trở thành lỗ hổng.
Khi bạn có thể muốn sử dụng các vòng lặp.
TBD
yes
ghi vào tệp nhanh như vậy?