Các lệnh Linux không phải lúc nào cũng hành xử giống nhau trên macOS. Có nhiều nguyên nhân đằng sau điều đó: sự khác biệt giữa tiện ích GNU và BSD, phạm vi của chuẩn POSIX, cách viết script shell, cùng các lớp bảo mật như SIP và TCC. Hiểu rõ những khác biệt này giúp bạn viết script shell di động và phát hiện các đặc thù GNU trước khi chúng gây lỗi trên BSD/macOS.

Thoạt nhìn mọi thứ đều quen thuộc

Có bao giờ bạn thắc mắc vì sao một lệnh chạy hoàn hảo trên Linux nhưng lại thất bại trên macOS? Ví dụ, lệnh sau đây lẽ ra phải chạy được trên Linux:

sed -i 's/debug=true/debug=false/' config.ini

Tôi đã chạy lệnh này nhiều lần trên Linux. Lần nào cũng thành công.

Thế nhưng đúng lệnh đó lại thất bại trên macOS. Tôi kiểm tra cú pháp, kiểm tra tệp, rồi chạy lại lần nữa. Nó vẫn thất bại.

Linux và macOS đều cung cấp môi trường dòng lệnh kiểu Unix. Chính sự tương đồng đó khiến ta dễ cho rằng các lệnh Linux sẽ hành xử y hệt trong Terminal của macOS. Phần lớn trường hợp đúng là như vậy, nhưng không phải lúc nào cũng thế.

Lệnh sed bị lỗi không phải là ngoại lệ ngẫu nhiên. Hầu hết lệnh Linux chạy trên macOS mà không cần thay đổi gì.

Sự nhất quán đó tạo nên một kỳ vọng: nếu một lệnh chạy được thì lệnh tiếp theo cũng phải chạy được.

Kỳ vọng này hợp lý. Linux và macOS đều cung cấp môi trường dòng lệnh kiểu Unix. Một hệ điều hành kiểu Unix tuân theo nhiều giao diện và nguyên tắc thiết kế do Unix khởi xướng. Kết quả là cả hai hệ thống đều phơi ra nhiều công cụ dòng lệnh giống nhau cùng những bố cục thư mục quen thuộc.

Mở Terminal trên macOS, bạn sẽ nhận ra các lệnh như ls, cd, pwd, cp, mv và ssh. Bạn cũng sẽ thấy những thư mục quen thuộc như /usr, /etc, /var và /tmp.

Với các tác vụ hằng ngày, trải nghiệm thường giống nhau. Điều đó khiến ta dễ cho rằng các công cụ bên dưới cũng hành xử giống nhau.

Chính giả định đó là nơi những khác biệt bắt đầu.

Cùng tên lệnh không có nghĩa là cùng một chương trình

Tên lệnh cho shell biết cần chạy cái gì. Nó không cho bạn biết lệnh đó được triển khai như thế nào.

Linux và macOS chia sẻ nhiều tên lệnh vì cả hai đều tuân theo POSIX. POSIX là chuẩn định nghĩa các giao diện dòng lệnh và hành vi hệ thống chung cho các hệ điều hành kiểu Unix. Chuẩn này quy định nhiều lệnh phải làm gì, nhưng không bắt mọi bản triển khai phải hỗ trợ cùng các phần mở rộng hay tùy chọn dòng lệnh.

Trên Linux, những lệnh như sed, grep, find và tar thường đến từ tiện ích GNU. Tiện ích GNU là tập hợp các công cụ dòng lệnh do Dự án GNU phát triển.

Trên macOS, nhiều lệnh trong số đó lại đến từ tiện ích BSD. Tiện ích BSD cung cấp cùng chức năng cốt lõi, nhưng được phát triển độc lập. Vì vậy, một số tùy chọn, giá trị mặc định và tính năng có khác biệt.

Hãy xét lệnh sed ở phần mở đầu.

Trên Linux:

sed -i 's/debug=true/debug=false/' config.ini

Chạy lệnh trên trên macOS sẽ cho lỗi kiểu:

sed: 1: "config.ini": extra characters at the end of s command

Đó là vì sed của BSD diễn giải config.ini thành đối số hậu tố sao lưu, rồi không còn gì để coi là tệp thực sự, nên nó cố đọc s/debug=true/debug=false/ như một tệp/script và bị kẹt.

Để lệnh chạy được trên macOS, bạn cần truyền một chuỗi rỗng làm đối số hậu tố sao lưu tường minh:

sed -i '' 's/debug=true/debug=false/' config.ini

Cả hai lệnh đều sửa tệp tại chỗ. Khác biệt nằm ở tùy chọn -i. Tùy chọn -i không được POSIX định nghĩa. sed của GNU và sed của BSD thêm nó một cách độc lập, và mỗi bên chọn một cú pháp khác nhau. sed của GNU cho phép -i mà không cần phần mở rộng sao lưu. sed của BSD lại yêu cầu phần mở rộng sao lưu. Một chuỗi rỗng ('') báo cho sed của BSD không tạo bản sao lưu.

sed chỉ là một ví dụ. Những lệnh như find, date, stat, xargs và tar cũng có các phần mở rộng riêng của GNU và của BSD. Phần lớn khác biệt là nhỏ. Chúng trở nên quan trọng khi bạn sao chép lệnh giữa Linux và macOS, hoặc viết script shell phải chạy trên cả hai hệ thống.

Điểm cần nhớ:

Tên lệnh thường giống nhau. Bản triển khai đằng sau nó thì có thể không. Chính sự phân biệt đó giải thích nhiều khác biệt lệnh giữa Linux và macOS.

Khi script shell không còn di động

Một lệnh chạy được trong terminal không phải lúc nào cũng di động trong script shell.

Script shell là một tệp văn bản chứa các lệnh để shell thực thi. Script kết hợp nhiều lệnh với nhau, nên càng dễ phụ thuộc vào hành vi khác nhau giữa Linux và macOS.

Một số khác biệt gây lỗi ngay lập tức. Những trường hợp đó thường dễ sửa.

Vấn đề khó hơn xuất hiện khi script chạy thành công nhưng cho ra kết quả bất ngờ. Script trông có vẻ đúng, nhưng kết quả lại sai.

Hãy xét lệnh này:

find . -type f -printf '%f\n'

Trên nhiều hệ thống Linux, lệnh này in ra tên của từng tệp khớp.

Trên macOS, nó thất bại vì bản triển khai BSD của find không hỗ trợ tùy chọn -printf. -printf là phần mở rộng của GNU, không thuộc chuẩn POSIX.

Nếu bạn chạy lệnh trên trên macOS (với find của BSD), bạn sẽ nhận lỗi kiểu như sau:

find: -printf: unknown primary or operator

Đây là vấn đề di động phổ biến. Một script có thể chạy hoàn hảo trên hệ điều hành này vì nó phụ thuộc vào phần mở rộng không tồn tại trên hệ điều hành khác.

Nếu một script shell phải chạy trên cả Linux lẫn macOS, hãy viết nó theo chuẩn POSIX bất cứ khi nào có thể. POSIX định nghĩa một tập giao diện chung cho các hệ điều hành kiểu Unix. Những tính năng nằm ngoài chuẩn đó, thường gọi là phần mở rộng, có thể không tồn tại hoặc hành xử khác trên nền tảng khác. Apple khuyến nghị tránh các phần mở rộng đặc thù nền tảng khi viết script shell đa nền tảng.

Điểm cần nhớ:

Một script shell di động phụ thuộc vào nhiều thứ hơn là tên lệnh. Nó còn phụ thuộc vào việc dùng những tính năng được POSIX định nghĩa hoặc có mặt trên mọi hệ điều hành bạn định hỗ trợ.

Vì sao sudo không phải lúc nào cũng giải quyết được vấn đề

Trên Linux, lỗi quyền thường có nghĩa là người dùng hiện tại không có đủ quyền. Chạy lệnh với sudo thường giải quyết được vấn đề.

Trên macOS, cách đó không phải lúc nào cũng hiệu quả.

Bạn vẫn có thể thấy lỗi kiểu:

Operation not permitted

ngay cả khi đã chạy lệnh với sudo.

Lý do là macOS bổ sung các lớp bảo mật nằm ngoài quyền tệp truyền thống của Unix.

Một trong những lớp đó là System Integrity Protection (SIP). SIP bảo vệ những phần quan trọng của hệ điều hành khỏi bị sửa đổi. Nó cũng hạn chế thay đổi đối với các tệp và thư mục được bảo vệ, ngay cả với tiến trình chạy bằng quyền root. Apple thiết kế SIP nhằm giảm tác động của phần mềm độc hại và những thay đổi hệ thống ngoài ý muốn.

Một lớp khác là Transparency, Consent, and Control (TCC). TCC là khung bảo mật quyền riêng tư của Apple. Nó kiểm soát quyền truy cập vào dữ liệu người dùng nhạy cảm, chẳng hạn các thư mục Desktop, Documents và Downloads. TCC đánh giá ứng dụng đang yêu cầu truy cập, chứ không chỉ tài khoản người dùng đang chạy lệnh.

Đó là lý do sudo đôi khi không đủ. Một lệnh trong Terminal vẫn có thể thất bại vì bản thân Terminal chưa được cấp quyền truy cập dữ liệu được bảo vệ. Trong nhiều trường hợp, cấp Full Disk Access cho Terminal hoặc ứng dụng terminal của bạn sẽ giải quyết các lỗi truy cập liên quan đến TCC.

Không phải mọi lỗi Operation not permitted đều do SIP hay TCC gây ra. Quyền tệp truyền thống, cờ tệp hoặc các chính sách bảo mật khác cũng có thể tạo ra cùng lỗi đó. Tuy nhiên, với người dùng Linux chuyển sang macOS, SIP và TCC là những nguyên nhân phổ biến nhất khiến sudo không hành xử như mong đợi.

Điểm cần nhớ:

Trên Linux, sudo thường đủ để giải quyết vấn đề quyền. Trên macOS, quyền nâng cao chỉ là một phần của mô hình bảo mật. SIP và TCC vẫn có thể ngăn một lệnh truy cập hoặc sửa đổi tài nguyên được bảo vệ.

Cách lập trình viên Linux thích nghi với macOS

Những lập trình viên giàu kinh nghiệm hiếm khi cố biến macOS thành Linux. Thay vào đó, họ xây dựng một môi trường phát triển có thể đoán trước.

Bước đầu tiên là hiểu biến môi trường PATH. PATH là danh sách có thứ tự các thư mục mà shell tìm kiếm để định vị một tệp thực thi. Nếu nhiều tệp thực thi trùng tên, shell chạy tệp đầu tiên mà nó tìm thấy.

Nhiều lập trình viên dùng trình quản lý gói Homebrew để cài thêm công cụ dòng lệnh. Nó cài đặt phần mềm mà không thay thế các tiện ích đi kèm hệ điều hành.

Một gói cài đặt phổ biến là GNU Coreutils. GNU Coreutils là tập hợp các tiện ích dòng lệnh chuẩn, bao gồm ls, cp, mv và sort. Để tránh xung đột với các tiện ích BSD mà macOS cung cấp, Homebrew cài nhiều lệnh kèm tiền tố g, chẳng hạn gls và gcp. Nếu bạn thích tên lệnh chuẩn, bạn có thể cấu hình PATH để tìm trong thư mục gnubin của Homebrew trước các thư mục hệ thống.

Cách làm tương tự áp dụng cho các tiện ích GNU khác, bao gồm gnu-sed, grep và findutils. Cài chúng sẽ đưa các bản triển khai GNU lên sẵn. Shell chỉ dùng chúng nếu bạn gọi lệnh có tiền tố, chẳng hạn gsed, hoặc cấu hình PATH để ưu tiên bản GNU.

Cài tiện ích GNU không làm thay đổi cách macOS vận hành. Nó chỉ thay đổi bản triển khai which mà shell của bạn chạy.

Điểm cần nhớ:

Homebrew đưa các công cụ GNU lên sẵn, nhưng PATH quyết định bản triển khai which mà shell thực thi. Một môi trường phát triển có thể đoán trước bắt đầu từ việc biết lệnh của bạn phân giải tới tệp thực thi which nào.

Kết luận

Linux và macOS chia sẻ nhiều tên lệnh vì cả hai đều cung cấp môi trường dòng lệnh kiểu Unix. Sự tương đồng đó khiến ta dễ kỳ vọng hành vi giống nhau.

Tên lệnh cho shell biết cần chạy cái gì. Nó không cho bạn biết lệnh đó được triển khai thế nào hay phụ thuộc vào những tính năng đặc thù nền tảng which. POSIX định nghĩa một tập giao diện chung cho các hệ điều hành kiểu Unix, nhưng cũng cho phép các bản triển khai cung cấp phần mở rộng riêng. Kết quả là hai lệnh cùng tên có thể hành xử khác nhau trên các hệ điều hành khác nhau.

Nguyên tắc đó cũng giải thích phần còn lại của những khác biệt bạn đã thấy. Một số đến từ tiện ích GNU và BSD. Một số khác đến từ các tính năng bảo mật của macOS như System Integrity Protection (SIP) và Transparency, Consent, and Control (TCC). Script shell trở nên kém di động khi phụ thuộc vào những phần mở rộng không có mặt ở mọi nơi.

Không khác biệt nào là ngẫu nhiên. Mỗi khác biệt đều theo một quyết định thiết kế của hệ điều hành hoặc của tiện ích triển khai lệnh đó.

Hiểu được điều đó, việc gỡ lỗi trở nên dễ hơn nhiều. Thay vì hỏi "Vì sao lệnh này thất bại?", hãy hỏi "Lệnh này giả định điều gì về nền tảng?"

Câu hỏi đó thường dẫn tới câu trả lời nhanh hơn nhiều.

Điểm cần nhớ:

Linux và macOS chia sẻ nhiều tên lệnh, nhưng không phải lúc nào cũng chia sẻ cùng hành vi. Hiểu bản triển khai, chuẩn và hệ điều hành đằng sau một lệnh là cách đáng tin cậy nhất để giải thích vì sao nó hành xử khác.

Câu hỏi thường gặp (FAQ)

H: Vì sao lệnh Linux không phải lúc nào cũng chạy trên macOS?

Đ: Không. macOS không dựa trên Linux. macOS được xây trên Darwin, hệ điều hành mã nguồn mở của Apple. Darwin kết hợp nhân XNU với các thành phần bắt nguồn từ BSD và những dự án mã nguồn mở khác. Linux và macOS đều là hệ điều hành kiểu Unix, nhưng dùng nhân khác nhau và tiện ích dòng lệnh khác nhau.

H: Vì sao sed -i chạy trên Linux nhưng không chạy trên macOS?

Đ: Hầu hết bản phân phối Linux đều có sed của GNU, còn macOS có sed của BSD.

Cả hai đều hỗ trợ sửa tại chỗ, nhưng dùng cú pháp khác nhau cho tùy chọn -i. sed của GNU cho phép -i mà không cần phần mở rộng sao lưu. sed của BSD yêu cầu phần mở rộng sao lưu. Nếu bạn không muốn tạo tệp sao lưu, hãy truyền một chuỗi rỗng:

sed -i '' 's/foo/bar/' file.txt

H: Vì sao find -printf chạy trên Linux nhưng không chạy trên macOS?

Đ: -printf là phần mở rộng của GNU.

Bản triển khai BSD của find đi kèm macOS không hỗ trợ nó vì nó không thuộc đặc tả POSIX.

H: Vì sao một số lệnh Linux hành xử khác trên macOS?

Đ: Linux và macOS đều cung cấp môi trường dòng lệnh kiểu Unix, nên nhiều tên lệnh giống nhau. Tuy nhiên, các bản triển khai bên dưới thường khác nhau. Hầu hết bản phân phối Linux dùng tiện ích GNU, còn macOS dùng tiện ích BSD. Dù cả hai đều tuân theo POSIX cho hành vi cốt lõi, chúng hỗ trợ các phần mở rộng, tùy chọn và giá trị mặc định khác nhau.

H: Vì sao sudo không sửa được mọi lỗi quyền trên macOS?

Đ: sudo cấp quyền nâng cao, nhưng macOS còn áp dụng thêm các cơ chế bảo mật. Ví dụ, System Integrity Protection (SIP) bảo vệ những phần quan trọng của hệ điều hành, còn Transparency, Consent, and Control (TCC) bảo vệ quyền truy cập dữ liệu người dùng nhạy cảm. Những cơ chế này có thể từ chối truy cập ngay cả khi lệnh chạy bằng quyền root.

Không phải mọi lỗi Operation not permitted đều do SIP hay TCC gây ra, nhưng chúng là những nguyên nhân phổ biến khiến người dùng Linux gặp vấn đề quyền trên macOS.

H: Tôi có thể làm cho macOS hành xử như Linux không?

Đ: Bạn có thể làm môi trường dòng lệnh trở nên quen thuộc hơn, nhưng không thể khiến macOS hành xử y hệt Linux.

Nhiều lập trình viên cài tiện ích GNU bằng Homebrew rồi cấu hình shell dùng những công cụ đó. Cách này giảm bớt vấn đề tương thích, nhưng macOS vẫn dùng nhân, mô hình bảo mật và tiện ích hệ thống mặc định của riêng nó.

H: Tôi có nên thay tiện ích BSD bằng tiện ích GNU không?

Đ: Thường thì không. Homebrew cài tiện ích GNU song song với tiện ích BSD đi kèm macOS. Với nhiều công cụ, bản GNU dùng tiền tố g, chẳng hạn gsed, nên không xung đột với bản hệ thống. Bạn có thể cấu hình PATH nếu muốn shell ưu tiên bản GNU.

H: Làm sao kiểm tra lệnh mà shell đang chạy?

Đ: Dùng:

command -v sed

command -v được POSIX định nghĩa và cho biết shell sẽ chạy tệp thực thi nào. Bạn cũng có thể dùng:

which sed

Nhiều hệ thống cung cấp which, nhưng command -v là lựa chọn di động hơn cho script shell.

H: Làm sao viết script shell chạy trên cả Linux lẫn macOS?

Đ: Hãy dùng các tính năng POSIX bất cứ khi nào có thể. Tránh các phần mở rộng riêng của GNU hoặc BSD trừ khi script của bạn nhắm tới một hệ điều hành cụ thể. Nếu script phải hỗ trợ cả Linux lẫn macOS, hãy kiểm thử trên cả hai nền tảng thay vì giả định hành vi giống hệt nhau.

H: POSIX là gì?

Đ: POSIX (Portable Operating System Interface) là chuẩn định nghĩa các giao diện và hành vi chung cho các hệ điều hành kiểu Unix. Chuẩn này quy định cách lệnh và lời gọi hệ thống nên hành xử, nhưng cho phép các bản triển khai thêm phần mở rộng riêng.

H: Nếu Linux và macOS đều tuân theo POSIX, vì sao vẫn có khác biệt?

Đ: POSIX định nghĩa một mức nền chung, không phải mọi tính năng. Nhiều tùy chọn thường dùng là phần mở rộng của GNU hoặc BSD và không thuộc chuẩn. Vì vậy, các bản triển khai khác nhau có thể cung cấp thêm chức năng mà vẫn tuân thủ POSIX.

H: Tiện ích GNU và tiện ích BSD khác nhau thế nào?

Đ: Tiện ích GNU là công cụ dòng lệnh do Dự án GNU phát triển và thường được dùng trên Linux.

Tiện ích BSD bắt nguồn từ dự án Berkeley Software Distribution (BSD) và được đưa vào macOS.

Cả hai đều cung cấp các lệnh tương tự, nhưng một số tùy chọn, giá trị mặc định và hành vi có khác biệt.

H: Điều gì làm nên một script shell di động?

Đ: Một script shell di động tránh các phần mở rộng riêng của GNU hoặc BSD bất cứ khi nào có thể. Nó dựa trên các tính năng do POSIX định nghĩa và được kiểm thử trên mọi hệ điều hành mà nó cần hỗ trợ.

H: System Integrity Protection (SIP) là gì?

Đ: System Integrity Protection (SIP) là tính năng bảo mật của macOS, bảo vệ các tệp và thư mục hệ thống quan trọng khỏi bị sửa đổi, ngay cả với tiến trình chạy bằng quyền root.

H: Transparency, Consent, and Control (TCC) là gì?

Đ: Transparency, Consent, and Control (TCC) là khung bảo mật quyền riêng tư của Apple. Nó kiểm soát quyền truy cập của ứng dụng vào dữ liệu người dùng nhạy cảm, chẳng hạn tệp trong các thư mục Desktop, Documents và Downloads, và yêu cầu người dùng đồng ý trước khi cấp quyền truy cập.

H: Quyền Unix và TCC khác nhau thế nào?

Đ: Quyền Unix quyết định liệu người dùng có quyền truy cập một tệp hay không.

TCC quyết định liệu một ứng dụng có được phép truy cập dữ liệu người dùng được bảo vệ hay không.

Trên macOS, cả hai kiểm tra đều có thể cần phải thành công.

H: Homebrew là gì?

Đ: Homebrew là trình quản lý gói cho macOS. Nó cài đặt và quản lý phần mềm mà không thay thế các tiện ích do hệ điều hành cung cấp.

H: Vì sao các lệnh GNU cài bằng Homebrew thường bắt đầu bằng g?

Đ: Homebrew cài nhiều tiện ích GNU kèm tiền tố g, chẳng hạn gsed và gcp, để tránh xung đột với các bản BSD đã có sẵn trong macOS.

H: Biến môi trường PATH là gì?

Đ: PATH là danh sách có thứ tự các thư mục mà shell tìm kiếm để định vị chương trình thực thi. Khi nhiều tệp thực thi trùng tên, shell chạy tệp đầu tiên mà nó tìm thấy trong PATH.

H: Vì sao cài tiện ích GNU không tự động thay đổi hành vi lệnh?

Đ: Cài tiện ích GNU chỉ đưa chúng lên sẵn. Shell vẫn tiếp tục dùng tệp thực thi which xuất hiện trước trong PATH. Trừ khi PATH được cập nhật hoặc lệnh GNU được gọi tường minh (ví dụ gsed), bản BSD vẫn sẽ chạy.