- Trải nghiệm hoạt động của terminal được tạo nên không chỉ từ hệ điều hành, shell, terminal emulator mà còn từ các quy ước của từng chương trình riêng lẻ, và những quy ước này thực ra nhất quán hơn nhiều so với tưởng tượng
- POSIX chủ yếu xử lý tương tác giữa terminal emulator, hệ điều hành, shell và một số tiện ích cốt lõi, nhưng không quy định đến hành vi chi tiết của các chương trình như
htop
- Các mẫu lặp lại như
Ctrl-C, q, Ctrl-D, 16 màu ANSI, key binding kiểu readline, tắt màu khi xuất qua pipe, hay ý nghĩa stdin/stdout của - xuất hiện rất thường xuyên
- Những hành vi tưởng như là quy luật tự nhiên thực ra phải do chương trình hoặc thư viện nhập liệu tự triển khai; ví dụ như thoát
ipython bằng Ctrl-D hay hủy tìm kiếm trong prompt-toolkit bằng Ctrl-C
- Khi tạo một chương trình terminal mới hoặc muốn hiểu cách một công cụ hoạt động, thực tế hơn là xem kỳ vọng mặc định và các ngoại lệ trước, thay vì nghĩ rằng “điều gì cũng có thể xảy ra”
Bốn tác nhân tạo nên hành vi của terminal
- Những gì xảy ra trong terminal nhìn chung có thể xem là kết quả do bốn tác nhân cùng tạo nên
- hệ điều hành
- shell
- terminal emulator
- chương trình đang chạy hiện tại như
top, vim, cat
- Trong những tổ hợp như bash, GNOME Terminal, Linux, ba yếu tố đầu tương đối quen thuộc, và một số hành vi đã được chuẩn hóa bởi POSIX
- Các chương trình riêng lẻ có vẻ như có thể làm bất cứ điều gì, nhưng trong trải nghiệm sử dụng thực tế, chúng vận hành theo những cách khá nhất quán
Ít tiêu chuẩn, nhưng có quy ước
- Gần như không có tiêu chuẩn thực sự nào quy định một cách toàn diện hành vi của các chương trình terminal
- Có hai tài liệu tham khảo gần nhất
- POSIX: chủ yếu nói về tương tác giữa terminal emulator, hệ điều hành và shell, đồng thời cũng quy định một phần hành vi của các tiện ích cốt lõi như
cp
- command line interface guidelines: bộ hướng dẫn về các quy ước của giao diện dòng lệnh
- Các quy tắc này không phải là chỉ dẫn bắt buộc mà tác giả chương trình phải tuân theo, mà gần hơn với các quy ước kỹ thuật được quan sát trong 20 năm sử dụng terminal
- Có rất nhiều ngoại lệ, và các ngoại lệ đó thường cũng có lý do hợp lý
Có những quy tắc chương trình phải tự triển khai
- Vị trí file cấu hình, đầu ra
--help, hay quy ước ghi đầu ra thông thường vào stdout và lỗi vào stderr là những trách nhiệm tương đối rõ ràng của chương trình
- Ngược lại, có những hành vi trông rất tự nhiên nhưng thực tế chương trình hoặc thư viện nhập liệu phải tự xử lý trực tiếp
- Ví dụ tiêu biểu là hành vi dùng
Ctrl-D để thoát REPL
- Với
cat, không cần triển khai riêng vì trong cooked mode hệ điều hành sẽ trả về EOF
- Với
ipython, việc xử lý Ctrl-D được triển khai bằng mã của prompt-toolkit
- Hiểu được trách nhiệm triển khai này sẽ khiến việc mỗi chương trình có chi tiết hành vi hơi khác nhau bớt khó hiểu hơn
Quy tắc 1: Chương trình không tương tác kết thúc bằng Ctrl-C
- Chương trình không tương tác thường sẽ kết thúc khi nhấn
Ctrl-C
- Lý do là nếu chương trình không cài signal handler cho
SIGINT thì mặc định nó sẽ thoát khi nhận Ctrl-C
- Quy tắc này không áp dụng nguyên vẹn cho các chương trình tương tác như
python3, bc, less
- Trong chương trình tương tác,
Ctrl-C có thể không phải để thoát chương trình mà để ngắt tác vụ đang chạy
- Ví dụ là tìm kiếm trong
less hoặc đoạn mã Python đang chạy trong python3
- Thư viện xử lý nhập liệu prompt-toolkit mà
ipython dùng có mã để dùng Ctrl-C hủy tìm kiếm
Quy tắc 2: TUI thường thoát bằng q
- Các chương trình TUI như
less, htop thường sẽ thoát khi nhấn q
- Quy tắc này không áp dụng cho những chương trình mà việc thoát bằng
q không tự nhiên
tmux
- trình soạn thảo văn bản
Quy tắc 3: REPL thoát bằng Ctrl-D trên dòng trống
- Các REPL như
python3, ed thường sẽ thoát khi nhấn Ctrl-D trên một dòng trống
- Nếu chạy các chương trình như
cat trong cooked mode, hệ điều hành sẽ trả về EOF khi nhấn Ctrl-D trên dòng trống
sqlite3, python3, fish, bash thực tế không dùng cooked mode, nhưng chúng triển khai phím tắt này để mô phỏng hành vi mặc định
- Có thể xem ví dụ triển khai ở hai nơi
- REPL của Erlang là một ngoại lệ vì không thoát bằng
Ctrl-D
Quy tắc 4: Không vượt quá 16 màu cơ bản
- Các chương trình terminal nhìn chung không dùng nhiều màu ngoài 16 màu ANSI cơ bản
- Nếu chỉ định trực tiếp màu hex thì rất dễ xung đột với màu nền mà người dùng đang dùng
- Ví dụ, chữ màu
#EEEEEE gần như không nhìn thấy trên nền trắng nhưng có thể ổn trên nền tối
- Nếu dùng 16 màu cơ bản, khả năng cao người dùng đã cấu hình chúng trong terminal emulator để phù hợp với màu nền
- Cách này cũng giảm bớt các giả định về khả năng hỗ trợ màu của terminal emulator
- Có ngoại lệ là các trình soạn thảo văn bản
- Helix mặc định dùng nền tím thay vì màu ANSI cơ bản
- Helix không được xem là chương trình “cốt lõi”, và người dùng không thích bảng màu đó có thể đổi theme
Quy tắc 5: Hỗ trợ key binding kiểu readline ở một mức nào đó
- Gần như mọi chương trình, nếu phù hợp về ngữ cảnh, đều hỗ trợ key binding kiểu readline ở một mức độ nào đó
- Nhiều chương trình không dùng trực tiếp
readline, nhưng mô phỏng key binding theo phong cách emacs/readline
- Không phải chương trình nào cũng mô phỏng theo đúng cùng một cách
- atuin có vẻ dùng
Ctrl-A làm prefix, vì vậy Ctrl-A không đưa con trỏ về đầu dòng
- Các chương trình này còn tự triển khai buffer cắt/dán riêng, nên có thể xóa cả dòng bằng
Ctrl-U rồi dán lại bằng Ctrl-Y
- Cũng có ngoại lệ
- Các chương trình như
git, cat, nc không hỗ trợ chỉnh sửa dòng ngoài backspace, Ctrl-W, Ctrl-U
- Trình soạn thảo văn bản có cách chỉnh sửa riêng của từng chương trình
- Chủ đề liên quan được bàn thêm trong entering text in the terminal is complicated
Quy tắc 5.1: Ctrl-W xóa từ cuối cùng
- Ngoài trình soạn thảo văn bản ra, hầu như chưa thấy chương trình nào mà
Ctrl-W không xóa từ cuối cùng
- Trong cooked mode, hệ điều hành mặc định sẽ xóa từ cuối khi nhấn
Ctrl-W, và xóa toàn bộ dòng khi nhấn Ctrl-U
- Nhiều chương trình mô phỏng hành vi mặc định này
- Ngoài trình soạn thảo văn bản thì khó nghĩ ra ngoại lệ nào khác
Quy tắc 6: Tắt màu khi ghi vào pipe
- Phần lớn chương trình sẽ vô hiệu hóa màu khi đầu ra là pipe
- Ví dụ
rg blah sẽ làm nổi bật blah khi in ra terminal, nhưng sẽ tắt phần nhấn mạnh khi ghi ra pipe hoặc file
ls --color=auto sẽ dùng màu khi ghi ra terminal, và không dùng màu khi ghi ra pipe
- Định dạng cũng có thể thay đổi tùy theo việc đầu ra có phải terminal hay không
ls sẽ sắp xếp file theo cột khi in ra terminal
- ripgrep sẽ nhóm các kết quả khớp cùng với tiêu đề
- Nếu muốn ép hiển thị màu, có thể dùng
unbuffer để khiến đầu ra của chương trình trông như đang ghi vào tty
unbuffer rg blah | less -R
- Một số chương trình cũng cho phép ép màu bằng cờ
--color
- Ví dụ trên cũng có thể viết thành
rg --color=always | less -R
Quy tắc 7: - nghĩa là stdin hoặc stdout
- Thông thường nếu truyền
- thay cho tên file, chương trình sẽ đọc từ stdin hoặc ghi ra stdout tùy ngữ cảnh phù hợp
- Để format mã Python trong clipboard bằng
black rồi chép lại vào clipboard, có thể chạy như sau
pbpaste | black - | pbcopy
pbpaste là chương trình trên Mac; trên Linux có thể làm việc tương tự với xclip
- Có vẻ phần lớn chương trình đều triển khai cách này khi nó có ý nghĩa, dù cũng có thể có khá nhiều ngoại lệ
Vì sao mất nhiều thời gian để học các quy tắc này
- Những quy tắc như vậy thường không được học một cách tường minh ngay từ đầu, mà được tiếp thu qua nhiều bước
- Trước tiên học được rằng có một quy tắc nào đó tồn tại, chẳng hạn “
Ctrl-C dùng để kết thúc chương trình”
- Sau đó phát hiện các ngoại lệ, như
find thì thoát nhưng less thì không
- Rồi dần dần vô thức nhận ra mẫu hình rằng chương trình không tương tác sẽ thoát, còn chương trình tương tác thì có thể chỉ ngắt tác vụ hiện tại
- Chỉ về sau mới có thể hệ thống hóa thành quy tắc rõ ràng
- Phần lớn hiểu biết về terminal vẫn còn nằm ở mức nhận diện mẫu một cách vô thức
- Việc viết các quy tắc này ra thành văn bản có thể giúp người khác học hành vi của terminal nhanh hơn một chút
1 bình luận
Ý kiến trên Hacker News
Ngoài ra, các chương trình terminal nên tôn trọng màu chữ/màu nền mặc định của người dùng và không thay đổi chúng khi không có lý do
Ngay cả khi dùng màu, nội dung vẫn phải đọc được bất kể nền/chữ mặc định và bảng màu của terminal là gì; nếu chỉ dùng màu để truyền đạt ý nghĩa thì ý nghĩa đó sẽ biến mất khi sao chép/dán hoặc trong terminal không hỗ trợ màu
Emoji hay các ký tự không phải ASCII không cần thiết cũng nên được dùng thận trọng; trong dạng đầu ra chuẩn cuộn liên tục, tốt hơn là không xóa các thông tin quan trọng như cảnh báo hiển thị tạm thời hay tên tệp
Nếu muốn dùng nhiều màu sắc sặc sỡ và hoạt ảnh, tối thiểu nên cung cấp tùy chọn cấu hình để dễ dàng tắt chúng
Tôi không biết nguyên nhân gốc đã được sửa chưa, nhưng nó đã khiến tôi phải tạo các PR workaround cho nhiều dự án mã nguồn mở, phần nào từ bỏ ý đồ nghệ thuật ban đầu: https://github.com/docker-archive/toolbox/issues/695
Tuy nhiên có thể cung cấp màu tùy chỉnh của người dùng
Nói chung, chúng giống như một bản tổng hợp những điều có thể giả định rằng hầu hết mọi chương trình terminal đều đã tuân theo
Ngày nay, một số tác giả tạo công cụ với giả định rằng người dùng đang dùng phông chữ đã được patch, điều này đôi khi làm giảm mạnh khả năng sử dụng
Bài viết hay
Vì bài nói rằng các quy ước như vậy khó học, tôi cũng muốn giới thiệu Command Line Interface Guidelines, tài liệu tham khảo tôi ưa thích khi nghĩ về CLI: https://clig.dev
Nó bao gồm các quy tắc trong bài gốc như thoát bằng Ctrl-C, nhận
-trên đầu vào chuẩn, tắt màu khi dùng pipe, và còn nhiều nội dung khácBao gồm tài liệu POSIX, GNU, Unix, hướng dẫn Heroku CLI, và hướng dẫn ứng dụng CLI 12-factor
Điểm “đầu ra thông thường nên in ra stdout, lỗi nên in ra stderr” thực sự rất quan trọng
Đầu ra chuẩn chỉ nên dành cho dữ liệu mà chương trình được yêu cầu tạo ra. Nếu người dùng yêu cầu dữ liệu JSON, thì đầu ra chuẩn phải chỉ chứa đúng đối tượng JSON đó, tuyệt đối không có thứ gì khác
Cái tên lỗi chuẩn thực ra được đặt hơi sai; nó giống một “luồng người dùng chuẩn” để gửi mọi thông báo mà người dùng cần đọc trong terminal hơn. Không chỉ thông báo lỗi, mà cả thông báo trạng thái và đầu ra chi tiết cũng thuộc về đây
Nhờ vậy đầu ra của chương trình có thể được pipe mượt mà sang chương trình khác, còn thông tin không phải dữ liệu vẫn đi ra terminal hoặc có thể được redirect sang nơi khác
Sẽ tốt nếu chương trình có thể dễ dàng tạo thêm file descriptor kết nối terminal cho các mục đích cụ thể, và tài liệu hóa các số như mã thoát trong manual. Để tương thích, mặc định có thể gửi tới stdout hoặc stderr; tôi đang định thử nghiệm điều này một chút
Có 6 luồng khác nhau, và từng luồng có thể được chặn riêng hoặc cấu hình khác nhau: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_output_streams?view=powershell-7.4
grepsẽ không hoạt độngBổ sung vài ý: nếu bạn mới nghe tới các key binding readline/emacs như Ctrl-E, Ctrl-W, thì nên biết rằng hầu hết nguồn nhập trên macOS đều dùng các key binding này
Nếu dùng macOS, bạn có thể thử Ctrl-E, Ctrl-W, Ctrl-U ngay trong thanh địa chỉ trình duyệt lúc này
Nếu đang dùng một chương trình dòng lệnh hoàn toàn không hỗ trợ chỉnh sửa dòng, bạn có thể cài
rlwrapvà chạy REPL bên dưới nó. Ví dụ Standard ML of New Jersey có REPL nhưng không có tính năng chỉnh sửa dòng, nên có thể bổ sung bằngrlwrap smlnjVề câu “đừng dùng quá 16 màu”, tôi muốn đi xa hơn và nói rằng “đừng dùng quá 8 màu, hoặc ít nhất hãy cho phép cấu hình màu”. Các bảng màu phổ biến như Solarized và Base 16 mặc định đôi khi dùng các màu “sáng” cho nhiều mức xám, nên màu xanh lá sáng mà tác giả hình dung trên thực tế có thể lại là cùng một màu xám với văn bản thông thường
Trình giả lập terminal Alacritty tôi dùng cũng vậy, và hầu hết trình quản lý tệp có tab cũng hoạt động như thế. Theo trí nhớ thì Windows Explorer giờ cũng có vẻ như vậy, nhưng tôi đã không dùng Windows khá lâu rồi
Quy ước thoát bằng Ctrl-D trong REPL lúc nào cũng khiến tôi vấp ở GHCi
Thường thì tôi nhấn Ctrl-D, rồi bối rối vì chẳng có gì xảy ra, sau đó mới nhớ ra trong GHCi không dùng được và chạy
:qKhi đó Ctrl-D đã chèn một ký tự vô hình ở đầu dòng nhập, gây lỗi “lexical error at character '\EOT'”, nên phải nhập lại
:qmà không có tiền tố vô hình đó thì GHCi mới thoátTôi cũng không rõ cách khác là gì; có thiết lập nào thay đổi hành vi này không?
Một thứ còn thiếu là hành vi xử lý
~như thư mục homeCái này có vẻ là tính năng của shell, không phải POSIX API. Trong Go,
os.ReadFile("~/.bashrc")không hoạt động, nhưng trong shell thìecho ~/~/~được mở rộng thành kiểu như/home/jrockway/~/~yourprogram ~/path/to/filethì luôn được, nhưng trong REPL hỏi tên tệp thì có thể không. Thực tế nhiều phần mềm hỗ trợ điều này, nên tôi nghĩ nó đáng được xếp vào “những việc hầu hết chương trình TUI làm”Trong mở rộng từ, mở rộng dấu ngã là bước đầu tiên, sau đó là tách trường, mở rộng tên đường dẫn và loại bỏ dấu nháy
Đặc tả shell liên quan nằm ở https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_06, và có thể xem toàn bộ đặc tả tại https://pubs.opengroup.org/onlinepubs/9799919799/
Nếu công cụ thêm mở rộng dấu ngã, nó phải biết thư mục home của người dùng hiện tại, khiến việc triển khai phức tạp hơn, và có thể sinh ra các triển khai không đầy đủ kiểu chỉ hỗ trợ
~chứ không hỗ trợ~nameTrong shell POSIX,
~nameđược mở rộng thành thư mục home của người dùng đó, nên phải tra cứu thư mục home của người dùng tùy ý, bao gồm cả người dùng hệ thống. Tùy cấu hình NSS, việc này có thể phức tạp hơn nhiều so với đọc/etc/passwdNếu là TUI tương tác thì triển khai mở rộng dấu ngã có thể hữu ích, nhưng trong trường hợp đó nó phải đầy đủ như shell. Chương trình CLI tốt nhất là không nên làm kiểu mở rộng “ma thuật” như vậy
Sẽ nảy sinh các vấn đề như nếu có tệp tên
"~"thì làm gì,"~username"thì sao, escape/quote thế nào, nếu không có$HOMEhoặc nó khác với home thật thì sao, trên Windows thì thế nàoMột sự thật ít người biết là trong ssh, nếu sau một dòng mới bạn nhập
"~."thì phía client sẽ cưỡng bức đóng kết nối và server không thể làm gì, nên tốt nhất đừng đưa nó vào workflow./myprogram --config=~/.config/myprogramTất nhiên nhiều trình phân tích cờ có thể dùng dạng shell sẽ mở rộng hộ là
./myprogram --config ~/.config/myprogram, nên coi như chúng biết điều này hoặc tình cờ hỗ trợNhưng vì trí nhớ cơ bắp, tôi cứ nhất định gõ
=. Có lẽ là thói quen từ thư viện phân tích cờ ở công ty cũ, và cũng vì lý do đó tôi luôn dùng--underscores_like_thisthay vì--hyphens-like-normal-peopleĐặc biệt vì
..được xử lý ở cấp OS API, nên rất dễ kỳ vọng POSIX cũng sẽ xử lý~như vậyLý do chính tôi thích CLI hơn GUI, và đôi khi hơn cả TUI, là vì CLI cho cảm giác nhất quán hơn nhiều
Có các quy ước, và việc tuân theo tất cả quy ước trong CLI dễ hơn nhiều so với thiết kế một GUI tốt. Vì vậy kết quả thường có chất lượng cao hơn
Tôi đã nghĩ nhiều về cách đưa thuộc tính này sang GUI, nhưng đến giờ câu trả lời tốt nhất chỉ là kiểu “tốn rất nhiều công sức” hoặc “hạ thấp kỳ vọng”
Các OS khác không thiết lập được phím meta cho phím tắt GUI, nên các xung đột kiểu
ctrl-cxảy ra nhiều hơn hẳnCác giá trị mặc định của framework ứng dụng làm nền cho app native trên macOS cung cấp rất nhiều thao tác bàn phím chung. Nếu dùng lớp thanh menu mặc định và các lệnh mặc định, Command O, N, Q, X, C, V, v.v. gần như có sẵn miễn phí; dùng trường văn bản chuẩn thì điều hướng bằng Command/Option/Fn + phím mũi tên cũng tự động có
Windows cũng khá ổn về mặt này, nhưng việc phím bổ trợ mặc định là Ctrl có thể cản trở việc dùng ứng dụng dựa trên terminal, và có vẻ nhiều framework UI bên trong OS có các mặc định khác nhau
Trong thế giới Linux, có lẽ chỉ khả thi nếu ai đó thiết kế một framework ứng dụng hoàn chỉnh, các bản phân phối chuẩn hóa nó và port ứng dụng sang đó. Trình quản lý cửa sổ không muốn ra lệnh hành vi bên trong cửa sổ, các GUI toolkit không muốn định nghĩa mặc định cho toàn OS, còn môi trường desktop và bản phân phối cũng không muốn nhận trách nhiệm ép dùng hoặc port sang một UI framework
Chương trình không nên thêm tệp vào thư mục home mà nên tôn trọng các thiết lập như XDG_CONFIG_HOME
Nó không cố thuyết phục ứng dụng nên hành xử thế nào, mà liệt kê những hành vi thường thấy trong ứng dụng
Một ngoại lệ của quy tắc “Ctrl-W phải xóa từ cuối cùng” là mysql(1)
mysql được liên kết với editline thay vì readline, nên theo mặc định Ctrl-W không xóa từ cuối cùng mà xóa hết đến đầu dòng
Có một thời gian tôi phải dùng nó, và những truy vấn đã dày công viết cứ biến mất nguyên cả dòng, đúng là muốn phát điên
Hồi đại học, tôi từng xui xẻo quyết định dùng thử sqlplus để kết nối tới cơ sở dữ liệu Oracle; không những các phím tắt terminal như Ctrl-W hoàn toàn không hoạt động, mà còn không thể di chuyển con trỏ lùi trong dòng, cũng không có duyệt lịch sử để quay lại lệnh trước đó
Chỉ cần gõ sai một ký tự là phải nhập lại toàn bộ từ đầu; nhập một dấu
/thì có thể chạy lại nguyên lệnh cuối cùng, nhưng chẳng giúp gì cho việc sửa lỗi gõ nhầmCtrl-L cũng không xóa màn hình được, thay vào đó phải tự chạy
clear scr. Có lẽ đặt lệnh đơn giản làclearthì quá hiển nhiênTôi muốn bổ sung rằng các tiến trình chạy lâu nên đọc lại cấu hình khi nhận SIGHUP
Khác biệt rất lớn, và đáng tiếc là mục này không đủ phổ biến để được đưa vào danh sách gốc
Các tiến trình chạy lâu phản ứng với SIGHUP thường chạy tách rời, không có TTY điều khiển, nên gọi chúng là “chương trình terminal” thì hơi gượng
Cần cẩn thận để ứng dụng không vô tình cache không chỉ trạng thái cấu hình mà cả trạng thái được suy ra từ cấu hình; nếu không, chúng sẽ lệch nhau
Với ứng dụng đơn giản thì có thể dễ, nhưng phần lớn ứng dụng cần file cấu hình lại không đơn giản đến vậy