TL;DR
macOS đi kèm với BSD sed, không phải GNU sed. BSD sed yêu cầu phải có phần mở rộng backup tường minh sau -i. Truyền vào chuỗi rỗng để bỏ qua việc tạo backup:
# Fix trên macOS: thêm '' sau -i
sed -i '' 's/foo/bar/g' filename.txt
Hoặc cài GNU sed qua Homebrew và dùng gsed với cú pháp Linux quen thuộc.
Lỗi đầy đủ
sed: 1: "filename": extra characters at the end of L command
Bạn chạy lệnh sed -i hoạt động hoàn hảo trên Linux, chuyển sang Mac thì nó bị lỗi. Thông báo lỗi đề cập đến "L command" dù bạn không dùng lệnh đó. Khó hiểu — nhưng nguyên nhân gốc rễ có lý khi bạn biết rằng có hai phiên bản sed không tương thích nhau.
Nguyên nhân gốc rễ
GNU sed và BSD sed xử lý flag -i khác nhau:
- GNU sed (Linux, hầu hết CI server):
-inhận một hậu tố tùy chọn. Viếtsed -i 's/…/…/' filethì GNU sed coi như hậu tố bị bỏ qua, chỉnh sửa trực tiếp tại chỗ mà không tạo backup. - BSD sed (macOS tích hợp sẵn):
-iluôn bắt buộc phải có hậu tố. Viếtsed -i 's/…/…/' filethì BSD sed lấy pattern thay thế của bạn làm phần mở rộng backup. Sau đó nó mở file đích và cố thực thi nội dung file đó như một sed script. Các ký tự đầu tiên trong file bị đọc sai thành các lệnh sed — thường rơi vào thứ gì đó mà BSD sed hiểu là lệnhl(chữ L thường) với các ký tự rác theo sau.
Thông báo kỳ lạ "extra characters at the end of L command" có nghĩa là: Tôi đã mở file của bạn như một sed script và không thể phân tích cú pháp nó. Apple đã đi kèm BSD sed trên mọi phiên bản macOS và chưa bao giờ tích hợp GNU sed theo mặc định.
Kiểm tra xem bạn đang dùng phiên bản nào:
sed --version 2>&1 | head -1
# GNU: in ra "sed (GNU sed) 4.x"
# macOS BSD: in ra "sed: illegal option -- -" hoặc thoát với mã lỗi khác 0
Fix 1 — Thêm chuỗi rỗng sau -i (dùng sed gốc của macOS)
BSD sed cần một đối số hậu tố. Chuỗi rỗng '' yêu cầu nó chỉnh sửa trực tiếp tại chỗ mà không tạo backup:
# Trước (bị lỗi trên macOS)
sed -i 's/old/new/g' config.txt
# Sau (hoạt động trên macOS)
sed -i '' 's/old/new/g' config.txt
Chuỗi rỗng phải là một token riêng biệt. Dấu nháy đặt sát -i sẽ tạo ra kết quả khó lường tùy theo shell:
# Sai — hành vi thay đổi tùy shell
sed -i'' 's/old/new/g' config.txt
# Đúng — có khoảng trắng tường minh trước ''
sed -i '' 's/old/new/g' config.txt
Muốn tạo bản sao lưu trước khi chỉnh sửa? Truyền vào một phần mở rộng thay vì chuỗi rỗng:
# Tạo config.txt.bak trước khi chỉnh sửa config.txt
sed -i '.bak' 's/old/new/g' config.txt
Fix 2 — Cài GNU sed qua Homebrew
Việc liên tục chuyển đổi script Linux sang macOS rất mất thời gian. Cài GNU sed giúp cú pháp nhất quán trên cả hai nền tảng:
brew install gnu-sed
Homebrew cài nó với tên gsed để tránh ghi đè binary của hệ thống. Dùng trực tiếp như sau:
gsed -i 's/old/new/g' config.txt
Muốn gõ sed bình thường? Thêm thư mục GNU tools vào PATH trong ~/.zshrc hoặc ~/.bashrc:
export PATH="$(brew --prefix)/opt/gnu-sed/libexec/gnubin:$PATH"
Sau khi source lại profile, sed --version sẽ hiển thị phiên bản GNU. Các script Linux của bạn sẽ chạy không cần chỉnh sửa từ đó trở đi.
Fix 3 — Viết script đa nền tảng
Không dùng Homebrew? Hãy kiểm tra hệ điều hành lúc chạy. Một hàm wrapper xử lý các lệnh gọi nhiều đối số gọn gàng và tránh được các vấn đề về dấu nháy khi lưu lệnh vào biến:
#!/usr/bin/env bash
sed_inplace() {
if [[ "$(uname)" == "Darwin" ]]; then
sed -i '' "$@"
else
sed -i "$@"
fi
}
sed_inplace 's/version=.*/version=2.0/' package.properties
Pattern này hoạt động trong Makefile, script deploy, và CI YAML — bất cứ nơi nào bạn không thể đảm bảo nền tảng nào sẽ chạy code.
Kiểm tra
Xác nhận việc chỉnh sửa đã thành công sau khi áp dụng fix:
# Chạy lệnh đã fix
sed -i '' 's/foo/bar/g' test.txt
# Kiểm tra kết quả
grep 'bar' test.txt
# Xác nhận không có file backup ngoài ý muốn được tạo ra
ls -la test.txt*
Nội dung thay thế hiển thị trong kết quả grep, không có file backup lạ trừ khi bạn muốn — vậy là xong.
Các vấn đề thường gặp tiếp theo
- Script chạy tốt ở local nhưng lỗi trên CI: Hầu hết các container CI (GitHub Actions, CircleCI) chạy Linux với GNU sed. Thêm wrapper
sed_inplaceở trên để cả hai môi trường đều đồng bộ. - sed -i '' hoạt động trong shell nhưng không hoạt động trong Makefile: Shell mặc định của Make là
/bin/sh, nơi xử lý dấu nháy khác nhau. ThêmSHELL := /bin/bashở đầu Makefile, hoặc escape cẩn thận:sed -i $'\'' $'\'' 's/…/…/' file. - Python hoặc Ruby gọi subprocess: Truyền
-ivà''là hai phần tử riêng biệt trong danh sách — không phải một chuỗi duy nhất.subprocess.run(['sed', '-i', '', 's/a/b/', 'file'])hoạt động đúng; gộp chúng thành một phần tử thì không.

