có vẻ như Bash là một ngôn ngữ hoàn chỉnh
Khái niệm về tính hoàn chỉnh của Turing hoàn toàn tách biệt với nhiều khái niệm hữu ích khác trong một ngôn ngữ để lập trình nói chung : tính khả dụng, tính biểu cảm, tính dễ hiểu, tốc độ, v.v.
Nếu Turing-đầy đủ là tất cả chúng ta được yêu cầu, chúng tôi sẽ không có bất kỳ ngôn ngữ lập trình nào cả , thậm chí không lắp ráp ngôn ngữ . Các lập trình viên máy tính tất cả sẽ chỉ viết mã máy , vì CPU của chúng tôi cũng đã hoàn tất.
Tại sao Bash được sử dụng hầu như chỉ để viết các kịch bản tương đối đơn giản?
Các tập lệnh shell lớn, phức tạp - chẳng hạn như configure
tập lệnh đầu ra của GNU Autoconf - không điển hình vì nhiều lý do:
Cho đến gần đây, bạn không thể tin tưởng vào việc có vỏ tương thích POSIX ở mọi nơi .
Nhiều hệ thống, đặc biệt là các hệ thống cũ hơn, về mặt kỹ thuật có lớp vỏ tương thích POSIX ở đâu đó trên hệ thống, nhưng nó có thể không ở vị trí có thể dự đoán được như thế nào /bin/sh
. Nếu bạn đang viết một kịch bản shell và nó phải chạy trên nhiều hệ thống khác nhau, vậy thì bạn viết dòng shebang như thế nào? Một lựa chọn là đi trước và sử dụng /bin/sh
, nhưng chọn giới hạn bản thân với phương ngữ vỏ POSIX trước POSIX trong trường hợp nó được chạy trên một hệ thống như vậy.
Các vỏ Bourne trước POSIX thậm chí không có số học tích hợp; bạn phải gọi ra expr
hoặc bc
để hoàn thành nó
Ngay cả với vỏ POSIX, bạn đang bỏ lỡ các mảng kết hợp và các tính năng khác mà chúng tôi dự kiến sẽ tìm thấy trong các ngôn ngữ kịch bản Unix kể từ khi Perl lần đầu tiên trở nên phổ biến vào đầu những năm 1990 .
Thực tế lịch sử đó có nghĩa là có một truyền thống kéo dài hàng thập kỷ bỏ qua nhiều tính năng mạnh mẽ trong phiên dịch kịch bản vỏ gia đình Bourne hiện đại hoàn toàn bởi vì bạn không thể tin vào việc có chúng ở mọi nơi.
Thực tế điều này vẫn tiếp tục cho đến ngày nay, trên thực tế: Bash đã không nhận được các mảng kết hợp cho đến phiên bản 4 , nhưng bạn có thể ngạc nhiên khi có bao nhiêu hệ thống vẫn được sử dụng dựa trên Bash 3. Apple vẫn phát hành Bash 3 với macOS vào năm 2017 - rõ ràng là cho lý do cấp phép - và các máy chủ Unix / Linux thường chạy tất cả nhưng không được sản xuất trong một thời gian rất dài, do đó bạn có thể có một hệ thống cũ ổn định vẫn chạy Bash 3, chẳng hạn như hộp CentOS 5. Nếu bạn có các hệ thống như vậy trong môi trường của mình, bạn không thể sử dụng các mảng kết hợp trong các tập lệnh shell phải chạy trên chúng.
Nếu câu trả lời của bạn cho vấn đề đó là bạn chỉ viết các kịch bản shell cho các hệ thống "hiện đại", thì bạn phải đối phó với thực tế là điểm tham chiếu chung cuối cùng cho hầu hết các shell Unix là tiêu chuẩn shell POSIX , phần lớn không thay đổi vì nó là được giới thiệu vào năm 1989. Có nhiều loại vỏ khác nhau dựa trên tiêu chuẩn đó, nhưng tất cả chúng đều được chuyển hướng đến các mức độ khác nhau so với tiêu chuẩn đó. Chịu mảng kết hợp một lần nữa, bash
, zsh
, và ksh93
tất cả đều có tính năng đó, nhưng có nhiều sự không tương thích thực hiện. Do đó, lựa chọn của bạn là chỉ sử dụng Bash hoặc chỉ sử dụng Zsh hoặc chỉ sử dụng ksh93
.
Nếu câu trả lời của bạn cho vấn đề đó là "vậy thì cứ cài đặt Bash 4" hoặc ksh93
, hoặc bất cứ điều gì, vậy thì tại sao không "chỉ" cài đặt Perl hoặc Python hoặc Ruby thay thế? Điều đó là không thể chấp nhận được trong nhiều trường hợp; mặc định vấn đề
Không có ngôn ngữ kịch bản lệnh vỏ gia đình Bourne nào hỗ trợ các mô-đun .
Gần nhất bạn có thể đến với một hệ thống mô-đun trong tập lệnh shell là .
lệnh - hay còn gọi là source
các biến thể shell Bourne hiện đại hơn - không thành công ở nhiều cấp độ so với hệ thống mô-đun phù hợp, trong đó cơ bản nhất là không gian tên .
Bất kể ngôn ngữ lập trình, sự hiểu biết của con người bắt đầu gắn cờ khi bất kỳ tệp nào trong một chương trình tổng thể lớn hơn vượt quá vài nghìn dòng. Lý do chúng tôi cấu trúc các chương trình lớn thành nhiều tệp là để chúng tôi có thể trừu tượng hóa nội dung của chúng thành một hoặc hai câu. Tệp A là trình phân tích cú pháp dòng lệnh, tệp B là bơm I / O mạng, tệp C là shim giữa thư viện Z và phần còn lại của chương trình, v.v. Khi phương pháp duy nhất của bạn để lắp ráp nhiều tệp vào một chương trình là bao gồm văn bản , bạn đặt giới hạn về mức độ lớn mà các chương trình của bạn có thể phát triển hợp lý.
Để so sánh, nó sẽ giống như nếu ngôn ngữ lập trình C không có trình liên kết, chỉ có các #include
câu lệnh. Một phương ngữ C-lite như vậy sẽ không cần các từ khóa như extern
hoặc static
. Những tính năng tồn tại để cho phép mô-đun.
POSIX không định nghĩa một cách để phạm vi biến cho một hàm script shell duy nhất, ít hơn nhiều cho một tệp.
Điều này có hiệu quả làm cho tất cả các biến toàn cầu , một lần nữa làm tổn thương tính mô đun và khả năng kết hợp.
Có những giải pháp cho vấn đề này trong các vỏ hậu POSIX - chắc chắn là trong bash
, ksh93
và zsh
ít nhất - nhưng điều đó chỉ đưa bạn trở lại điểm 1 ở trên.
Bạn có thể thấy tác động của điều này trong các hướng dẫn kiểu trong cách viết macro GNU Autoconf, ở đó họ khuyên bạn nên đặt tiền tố tên biến với tên của macro, dẫn đến các tên biến rất dài hoàn toàn để giảm khả năng va chạm đến gần số không.
Thậm chí C là tốt hơn về điểm số này, bằng một dặm. Không chỉ hầu hết các chương trình C được viết chủ yếu bằng các biến cục bộ-hàm, C còn hỗ trợ phạm vi khối, cho phép nhiều khối trong một hàm duy nhất sử dụng lại tên biến mà không bị nhiễm chéo.
Ngôn ngữ lập trình Shell không có thư viện chuẩn.
Có thể lập luận rằng thư viện chuẩn của ngôn ngữ kịch bản lệnh shell là nội dung của PATH
, nhưng điều đó chỉ nói rằng để đạt được bất kỳ hậu quả nào, một kịch bản shell phải gọi ra toàn bộ chương trình, có thể là một ngôn ngữ được viết bằng ngôn ngữ mạnh hơn bắt đầu với.
Không có kho lưu trữ thư viện tiện ích shell nào được sử dụng rộng rãi như với CPAN của Perl . Nếu không có một thư viện lớn về mã tiện ích của bên thứ ba, một lập trình viên phải viết thêm mã bằng tay, vì vậy cô ta sẽ làm việc kém hiệu quả hơn.
Ngay cả khi bỏ qua thực tế là hầu hết các tập lệnh shell dựa vào các chương trình bên ngoài thường được viết bằng C để thực hiện bất kỳ điều gì hữu ích, vẫn có tổng phí của tất cả các pipe()
→ fork()
→ exec()
chuỗi cuộc gọi. Mô hình đó khá hiệu quả trên Unix, so với IPC và quá trình khởi chạy trên các HĐH khác, nhưng ở đây nó thay thế hiệu quả những gì bạn làm với một cuộc gọi chương trình con trong một ngôn ngữ kịch bản lệnh khác, vẫn hiệu quả hơn nhiều. Điều đó đặt một giới hạn nghiêm trọng về giới hạn trên của tốc độ thực thi tập lệnh shell.
Các kịch bản Shell có rất ít khả năng tích hợp để tăng hiệu suất của chúng thông qua thực thi song song.
Vỏ Bourne đã &
, wait
và đường ống dẫn cho điều này, nhưng đó là phần lớn chỉ hữu ích cho việc soạn nhiều chương trình, chứ không phải để đạt được CPU hay I / O song song. Bạn không có khả năng chốt các lõi hoặc bão hòa một mảng RAID chỉ với kịch bản shell và nếu bạn làm như vậy, bạn có thể có thể đạt được hiệu suất cao hơn nhiều trong các ngôn ngữ khác.
Đường ống nói riêng là những cách yếu để tăng hiệu suất thông qua thực hiện song song. Nó chỉ cho phép hai chương trình chạy song song và một trong hai chương trình có thể sẽ bị chặn trên I / O đến hoặc từ chương trình kia tại bất kỳ thời điểm nào.
Có những cách ngày sau xung quanh điều này, chẳng hạn như xargs -P
và GNUparallel
, nhưng điều này chỉ phá hủy đến điểm 4 ở trên.
Với hiệu quả không có khả năng tích hợp để tận dụng tối đa các hệ thống đa bộ xử lý, các tập lệnh shell luôn chậm hơn một chương trình được viết tốt bằng ngôn ngữ có thể sử dụng tất cả các bộ xử lý trong hệ thống. Để lấy configure
lại ví dụ về tập lệnh GNU Autoconf , nhân đôi số lượng lõi trong hệ thống sẽ làm rất ít để cải thiện tốc độ chạy của nó.
Ngôn ngữ kịch bản lệnh Shell không có con trỏ hoặc tham chiếu .
Điều này ngăn bạn thực hiện một loạt những điều dễ dàng thực hiện trong các ngôn ngữ lập trình khác.
Đối với một điều, việc không thể tham chiếu gián tiếp đến một cấu trúc dữ liệu khác trong bộ nhớ của chương trình có nghĩa là bạn bị giới hạn trong các cấu trúc dữ liệu tích hợp . Shell của bạn có thể có các mảng kết hợp , nhưng chúng được thực hiện như thế nào? Có một số khả năng, mỗi khả năng có sự đánh đổi khác nhau: cây đỏ đen , cây AVL và bảng băm là phổ biến nhất, nhưng có những cái khác. Nếu bạn cần một bộ trao đổi khác, bạn sẽ bị mắc kẹt, vì không có tài liệu tham khảo, bạn không có cách nào để cuộn bằng tay nhiều loại cấu trúc dữ liệu nâng cao. Bạn đang bị mắc kẹt với những gì bạn đã được đưa ra.
Hoặc, đó có thể là trường hợp bạn cần một cấu trúc dữ liệu thậm chí không có một giải pháp thay thế phù hợp được tích hợp trong trình thông dịch kịch bản lệnh shell của bạn, chẳng hạn như biểu đồ chu kỳ có hướng , mà bạn có thể cần để mô hình biểu đồ phụ thuộc . Tôi đã lập trình trong nhiều thập kỷ và cách duy nhất tôi có thể nghĩ để làm điều đó trong tập lệnh shell là lạm dụng hệ thống tệp , sử dụng liên kết tượng trưng làm tài liệu tham khảo giả. Đó là loại giải pháp bạn nhận được khi bạn chỉ dựa vào Turing-perfect, nó không cho bạn biết gì về việc giải pháp đó có thanh lịch, nhanh chóng hay dễ hiểu hay không.
Cấu trúc dữ liệu nâng cao chỉ đơn thuần là một sử dụng cho con trỏ và tham chiếu. Có hàng đống ứng dụng khác cho chúng , đơn giản là không thể thực hiện dễ dàng bằng ngôn ngữ kịch bản lệnh vỏ gia đình Bourne.
Tôi có thể tiếp tục và tiếp tục, nhưng tôi nghĩ rằng bạn đang nhận được điểm ở đây. Nói một cách đơn giản, có nhiều ngôn ngữ lập trình mạnh mẽ hơn cho các hệ thống loại Unix.
Đây là một lợi thế rất lớn, có thể bù đắp cho sự tầm thường của ngôn ngữ trong một số trường hợp.
Chắc chắn, và đó chính xác là lý do GNU Autoconf sử dụng một tập hợp con bị hạn chế có chủ đích của họ ngôn ngữ kịch bản shell của Bourne cho các configure
đầu ra tập lệnh của nó : để các configure
tập lệnh của nó sẽ chạy khá nhiều ở mọi nơi.
Bạn có thể sẽ không tìm thấy một nhóm tín đồ lớn hơn về tiện ích viết bằng phương ngữ vỏ Bourne có tính di động cao hơn các nhà phát triển GNU Autoconf, tuy nhiên sáng tạo của họ được viết chủ yếu bằng Perl, cộng với một số m4
và chỉ một chút vỏ kịch bản; chỉ đầu ra của Autoconf là tập lệnh shell Bourne thuần túy. Nếu điều đó không đặt ra câu hỏi về khái niệm "Bourne ở mọi nơi" hữu ích như thế nào, thì tôi không biết điều gì sẽ xảy ra.
Vì vậy, có giới hạn về mức độ phức tạp của các chương trình như vậy không?
Về mặt kỹ thuật, không, như quan sát Turing-đầy đủ của bạn cho thấy.
Nhưng đó không phải là điều tương tự như khi nói rằng các tập lệnh shell lớn tùy ý dễ viết, dễ gỡ lỗi hoặc thực thi nhanh.
Có thể viết, giả sử, một trình nén / giải nén tệp trong bash thuần?
Bash "thuần khiết", không có bất kỳ lời kêu gọi nào đến những thứ trong PATH
? Máy nén có thể sử dụng được echo
và sử dụng các chuỗi thoát hex, nhưng sẽ khá khó khăn để làm. Bộ giải nén có thể không thể viết theo cách đó do không thể xử lý dữ liệu nhị phân trong shell . Cuối cùng, bạn sẽ gọi ra od
và như vậy để dịch dữ liệu nhị phân sang định dạng văn bản, cách xử lý dữ liệu gốc của shell.
Khi bạn bắt đầu nói về việc sử dụng shell scripting theo cách nó được dự định, như keo để điều khiển các chương trình khác trong PATH
, cánh cửa mở ra, bởi vì bây giờ bạn chỉ giới hạn ở những gì có thể được thực hiện bằng các ngôn ngữ lập trình khác, nghĩa là bạn nói không có giới hạn nào cả. Một kịch bản sẽ thực tất cả sức mạnh của nó bằng cách gọi ra các chương trình khác trong PATH
không chạy nhanh như các chương trình nguyên khối được viết bằng các ngôn ngữ mạnh hơn, nhưng nó không chạy.
Và đó là điểm chính. Nếu bạn cần một chương trình để chạy nhanh, hoặc nếu nó cần phải mạnh mẽ theo cách riêng của nó thay vì mượn sức mạnh từ người khác, bạn không nên viết nó vào vỏ.
Một trò chơi video đơn giản?
Đây là Tetris trong vỏ . Các trò chơi khác như vậy có sẵn, nếu bạn đi tìm.
chỉ có các công cụ sửa lỗi rất hạn chế
Tôi sẽ đặt công cụ gỡ lỗi xuống vị trí thứ 20 trong danh sách các tính năng cần thiết để hỗ trợ lập trình lớn. Toàn bộ các lập trình viên phụ thuộc rất nhiều vào việc printf()
gỡ lỗi so với các trình sửa lỗi thích hợp, bất kể ngôn ngữ.
Trong shell, bạn có echo
và set -x
, cùng nhau là đủ để gỡ lỗi rất nhiều vấn đề.
sh
kịch bảnconfigure
được sử dụng như là một phần của quá trình xây dựng của rất nhiều un * x gói không phải là 'tương đối đơn giản.