2 điểm bởi GN⁺ 2024-02-07 | 1 bình luận | Chia sẻ qua WhatsApp
  • Đây là tài liệu tham chiếu mã nguồn mở tổng hợp các nguyên tắc thiết kế chương trình CLI và những hướng dẫn cụ thể, diễn giải lại triết lý UNIX truyền thống theo cách hiện đại, với độc giả chính là các nhà phát triển xây dựng công cụ dòng lệnh
  • CLI không còn chỉ là một nền tảng scripting đơn thuần mà đã phát triển thành text UI lấy con người làm trung tâm, và các nguyên tắc thiết kế cũng cần được cập nhật theo sự thay đổi này
  • Tính composability và sự thân thiện với con người không mâu thuẫn nhau; nếu tuân thủ các tập quán UNIX như standard input/output, pipe, mã thoát thì có thể đạt được cả hai cùng lúc
  • Tài liệu đưa ra các khuyến nghị cụ thể đến cả những chi tiết thường bị bỏ sót trong thực tế như help text, thông báo lỗi, định dạng đầu ra, tính tương tác, hệ thống cấu hình
  • Khả năng tương thích tương lai và niềm tin của người dùng đối với công cụ CLI được quyết định bởi độ ổn định của giao diện và tính minh bạch của dữ liệu phân tích, và hướng dẫn này đưa ra đường cơ sở cho các tiêu chí đó

Triết lý (Philosophy)

Thiết kế lấy con người làm trung tâm

  • Các lệnh UNIX truyền thống chủ yếu được thiết kế với giả định là sẽ được chương trình khác sử dụng, nhưng ngày nay CLI phần lớn do con người trực tiếp sử dụng nên cần thiết kế ưu tiên con người
  • CLI trong quá khứ là “machine-first”, nhưng hiện nay đã phát triển thành text UI “human-first”

Các thành phần nhỏ có thể kết hợp

  • Cốt lõi của triết lý UNIX là kết hợp các chương trình nhỏ và đơn giản để tạo nên hệ thống lớn hơn, và điều đó vẫn còn nguyên giá trị
  • Standard stdin/stdout/stderr, signal và mã thoát đảm bảo khả năng kết nối giữa các chương trình, còn JSON hỗ trợ trao đổi dữ liệu có cấu trúc hơn
  • Phần mềm nhất định sẽ trở thành một bộ phận của hệ thống lớn hơn, và việc nó có trở thành một bộ phận vận hành tốt hay không được quyết định ngay từ giai đoạn thiết kế

Tính nhất quán

  • Người dùng terminal đã quen tay với các convention hiện có, vì vậy CLI được khuyến nghị tuân theo các mẫu quen thuộc
  • Tuy nhiên, nếu tính nhất quán làm tổn hại đến khả năng sử dụng thì có thể phá vỡ thông lệ một cách cẩn trọng

Lượng thông tin phù hợp

  • Nếu một lệnh chờ hàng phút mà không có đầu ra nào thì đó là thông tin “quá ít”; còn nếu xả ra hàng đống debug log thì đó là “quá nhiều” thông tin
  • Sự cân bằng về lượng thông tin là yếu tố cực kỳ quan trọng để phần mềm hỗ trợ người dùng tốt

Khả năng khám phá (Ease of Discovery)

  • GUI hiển thị mọi tính năng ngay trên màn hình, còn CLI thường bị hiểu lầm là phải dựa vào trí nhớ
  • CLI cũng có thể dễ học hơn bằng cách mượn các kỹ thuật từ GUI như help text toàn diện, ví dụ phong phú, gợi ý lệnh tiếp theo

CLI như một cuộc đối thoại

  • Việc sử dụng CLI có cấu trúc đối thoại thông qua lặp đi lặp lại thử-sai; các kỹ thuật thiết kế như gợi ý sửa lỗi, hiển thị trạng thái trung gian, xác nhận trước thao tác nguy hiểm đều tận dụng đặc tính này
  • Tương tác tệ nhất là cuộc đối thoại mang tính đối kháng khiến người dùng bất lực, còn tốt nhất là sự trao đổi dễ chịu đem lại cảm giác thành tựu

Độ bền vững (Robustness)

  • Phần mềm cần bền vững cả trên thực tế lẫn trong cảm nhận của người dùng
  • Các điểm cốt lõi là xử lý đầu vào bất ngờ một cách mềm dẻo, giữ tính idempotence, thông báo tiến trình và hạn chế để lộ stack trace
  • Giảm các trường hợp ngoại lệ phức tạp và giữ mọi thứ đơn giản sẽ làm tăng độ bền vững

Sự đồng cảm (Empathy)

  • Công cụ CLI là công cụ sáng tạo của nhà phát triển nên cần đem lại cảm giác dễ chịu khi sử dụng
  • Hãy suy nghĩ kỹ về vấn đề và thiết kế sao cho người dùng cảm thấy công cụ đứng về phía mình

Hỗn loạn (Chaos)

  • Thế giới terminal đầy rẫy những điểm không nhất quán, nhưng chính sự hỗn loạn đó cũng là nguồn gốc của tự do sáng tạo
  • “Nếu một tiêu chuẩn rõ ràng gây hại cho năng suất hoặc sự hài lòng của người dùng, hãy vứt bỏ tiêu chuẩn đó.” — Jef Raskin

Hướng dẫn — Cơ bản (The Basics)

  • Hãy dùng thư viện parse tham số: các thư viện được khuyến nghị theo ngôn ngữ gồm Go(Cobra, cli), Python(Click, Typer, Argparse), Rust(clap), Node(oclif) và nhiều lựa chọn khác
  • Khi thành công, trả về mã thoát 0; khi thất bại, trả về mã khác 0 — đây là tiêu chí để script phân biệt thành công và thất bại
  • Đầu ra mặc định gửi tới stdout, còn các thông điệp như log hoặc lỗi gửi tới stderr

Hướng dẫn — Trợ giúp (Help)

  • Hiển thị help text chi tiết cho cờ -h hoặc --help, và áp dụng tương tự cho cả subcommand
  • Khi chạy không có tham số, hãy hiển thị trợ giúp ngắn gọn (bao gồm mô tả, 1~2 ví dụ, giải thích cờ, hướng dẫn --help)
    • jq được nhắc đến như một ví dụ triển khai tốt điều này
  • Hỗ trợ nhiều kiểu yêu cầu trợ giúp như --help, -h, help subcommand
  • Cung cấp liên kết tài liệu web và kênh phản hồi ở phần đầu của help text
  • Hãy đưa ví dụ lên trước — nên xây dựng mạch trình bày dẫn dắt dần sang các tình huống sử dụng phức tạp hơn
  • Đặt các cờ và lệnh thường dùng ở phần đầu của help text (tham khảo cách tổ chức của git)
  • Dùng định dạng như tiêu đề in đậm để người dùng dễ quét nội dung, nhưng theo cách độc lập với terminal
  • Khi người dùng nhập sai, có thể đoán ý định và gợi ý cách sửa — tuy nhiên cần cân nhắc kỹ việc tự động thực thi
    • Lỗi nhập sai có thể không chỉ là typo đơn giản mà còn là sai sót logic, và việc tự sửa cũng tạo gánh nặng phải hỗ trợ cú pháp đó lâu dài

Hướng dẫn — Tài liệu (Documentation)

  • Cung cấp tài liệu trên web — thiết yếu cho khả năng tìm kiếm và chia sẻ liên kết
  • Cung cấp tài liệu trong terminal — đồng bộ với phiên bản đã cài đặt và vẫn truy cập được khi offline
  • Cân nhắc cung cấp man page — có thể tạo bằng công cụ như ronn, và nên hỗ trợ truy cập qua subcommand như npm help ls

Hướng dẫn — Đầu ra (Output)

  • Ưu tiên hàng đầu là khả năng đọc của con người — dùng trạng thái TTY để xác định đầu ra có đang được con người đọc hay không
  • Text stream là giao diện phổ quát của UNIX, nên cũng cần hỗ trợ đầu ra máy có thể đọc được
  • Nếu đầu ra thân thiện với con người làm giảm khả năng tương thích với pipe, hãy cung cấp đầu ra văn bản thuần bằng cờ --plain
  • Khi truyền cờ --json, hãy hỗ trợ đầu ra định dạng JSON
  • Khi thành công, đầu ra nên ngắn gọn; nếu không cần thì không xuất gì — với script, nên hỗ trợ tùy chọn -q để tắt đầu ra
  • Thông báo cho người dùng khi trạng thái thay đổi — ví dụ tốt là git push hiển thị trạng thái của remote branch
  • Tổ chức đầu ra theo kiểu như git status, giúp kiểm tra trạng thái hiện tại của hệ thống thật dễ dàng và chỉ luôn bước tiếp theo
  • Màu sắc cần được dùng có chủ đích, và bắt buộc tắt màu trong các điều kiện như đang pipe, NO_COLOR, TERM=dumb, --no-color
  • Trong môi trường không phải TTY, không hiển thị animation hay spinner (để tránh làm bẩn log CI)
  • Chỉ dùng emoji hoặc ký hiệu khi chúng thực sự làm tăng độ rõ ràng (yubikey-agent được nêu làm ví dụ)
  • Thông tin chỉ nhà phát triển mới hiểu không nên có trong đầu ra mặc định, chỉ hiển thị ở verbose mode
  • Đừng dùng stderr như file log — về mặc định nên tránh in nhãn mức log (ERR, WARN)
  • Khi đầu ra quá nhiều, hãy cân nhắc dùng pager như less — chỉ bật trong môi trường TTY và nên dùng tùy chọn less -FIRX

Hướng dẫn — Lỗi (Errors)

  • Với các lỗi có thể dự đoán được, hãy viết lại thành thông báo con người có thể hiểu (ví dụ: “cần chạy chmod +w file.txt”)
  • Giữ tỷ lệ tín hiệu trên nhiễu — các lỗi cùng loại nên được gom và in dưới một tiêu đề chung
  • Thông tin quan trọng nên đặt ở cuối đầu ra — chữ đỏ chỉ nên dùng có chủ đích và hiếm khi
  • Khi xảy ra lỗi ngoài dự kiến, hãy kèm theo thông tin debug và cách gửi bug report
  • Hãy cấu hình URL gửi bug report sao cho tự điền sẵn thông tin, giúp việc gửi báo cáo dễ hơn

Hướng dẫn — Tham số và cờ (Arguments and Flags)

  • Tham số (args) là theo vị trí, còn cờ (flags) là theo tên — hãy ưu tiên cờ hơn tham số
  • Mọi cờ đều nên có phiên bản tên đầy đủ (ví dụ hỗ trợ đồng thời -h--help)
  • Cờ một ký tự chỉ nên giới hạn cho những cờ được dùng thường xuyên
  • Nếu đã có tiêu chuẩn, hãy dùng tên cờ chuẩn (-f/--force, -q/--quiet, -v, --json v.v.)
  • Giá trị mặc định nên được đặt thành giá trị phù hợp với đa số người dùng
  • Nếu không truyền tham số hay cờ, hãy yêu cầu nhập bằng prompt, nhưng không được ép prompt trong môi trường không tương tác
  • Trước thao tác nguy hiểm, hãy yêu cầu xác nhận — tùy mức độ rủi ro mà dùng xác nhận y/n, cung cấp dry-run, hoặc yêu cầu nhập trực tiếp văn bản
    • Phân loại mức rủi ro thành mild (xóa file), moderate (xóa thư mục, thay đổi tài nguyên từ xa), severe (xóa toàn bộ server)
  • Khi đọc/ghi file, hãy hỗ trợ dùng - để đọc từ stdin hoặc ghi ra stdout (ví dụ curl ... | tar xvf -)
  • Không nhận secret trực tiếp qua cờ — nên dùng cờ --password-file hoặc stdin (để tránh lộ qua output của ps hay lịch sử shell)

Hướng dẫn — Tính tương tác (Interactivity)

  • Chỉ hiển thị prompt và các yếu tố tương tác khi stdin là TTY
  • Khi truyền --no-input, hãy vô hiệu hóa mọi prompt
  • Khi nhập mật khẩu, hãy tắt echo (không hiện nội dung đang gõ lên màn hình)
  • Hãy hướng dẫn rõ ràng để người dùng có thể thoát ra bất kỳ lúc nào — Ctrl-C phải luôn hoạt động

Hướng dẫn — Subcommand (Subcommands)

  • Giữ sự nhất quán về tên cờ và định dạng đầu ra giữa các subcommand
  • Với công cụ phức tạp, dùng cấu trúc subcommand 2 tầng theo dạng noun verb hoặc verb noun (ví dụ docker container create)
  • Tránh các subcommand có tên mơ hồ hoặc quá giống nhau (ví dụ không nên dùng đồng thời update và upgrade)

Hướng dẫn — Độ bền vững (Robustness Guidelines)

  • Hãy kiểm tra đầu vào ngay từ sớm, và kết thúc sớm với thông báo lỗi dễ hiểu nếu dữ liệu không hợp lệ
  • Tính phản hồi quan trọng hơn tốc độ — hãy hiển thị một điều gì đó trong vòng 100ms
  • Với tác vụ kéo dài, hãy cung cấp thanh tiến trình (progress bar) — có thể dùng thư viện Python(tqdm), Go(schollz/progressbar), Node(node-progress)
  • Khi xử lý song song, hãy cẩn thận để đầu ra không bị trộn lẫn
  • Hãy thiết lập timeout mạng — bao gồm cả giá trị mặc định, để tránh chờ vô hạn
  • Sau lỗi tạm thời, nên thiết kế sao cho khi thử lại có thể tiếp tục từ trạng thái trước đó
  • Áp dụng thiết kế crash-only — cấu trúc có thể thoát ngay mà không cần dọn dẹp, nhằm đảm bảo tính idempotence

Hướng dẫn — Khả năng tương thích tương lai (Future-proofing)

  • Khi thay đổi, hãy giữ theo hướng bổ sung mà vẫn tương thích ngược
  • Trước khi có thay đổi phá vỡ tương thích, hãy cảnh báo trước ngay trong chương trình
  • Thay đổi đầu ra dành cho con người nhìn chung là chấp nhận được — với script, nên hướng người dùng dùng --plain hoặc --json
  • Không dùng catch-all subcommand — vì sau này sẽ phát sinh vấn đề không thể thêm subcommand với tên đó
  • Không tự động cho phép viết tắt subcommand — chỉ cho phép alias tường minh và duy trì chúng ổn định
  • Cấm “time bomb” — giảm tối đa phụ thuộc bên ngoài để chương trình vẫn có thể chạy sau 20 năm nữa

Hướng dẫn — Tín hiệu và ký tự điều khiển (Signals)

  • Khi nhận Ctrl-C (signal INT), hãy thoát ngay lập tức, và nếu có dọn dẹp thì phải đặt timeout cho việc đó
  • Trong lúc dọn dẹp, nếu người dùng bấm Ctrl-C lần nữa thì hãy hướng dẫn rằng có thể buộc thoát (tham khảo ví dụ Docker Compose)
  • Chương trình cần được thiết kế với giả định rằng nó có thể được khởi động khi công việc dọn dẹp trước đó chưa hoàn tất

Hướng dẫn — Cấu hình (Configuration)

Thứ tự ưu tiên áp dụng cấu hình (cao → thấp):

  • Cờ → biến môi trường của shell hiện tại → cấu hình cấp dự án (.env) → cấu hình cấp người dùng → cấu hình toàn hệ thống

Khuyến nghị theo từng loại cấu hình:

  • Cấu hình thay đổi theo từng lần gọi (mức debug, dry-run): dùng cờ

  • Cấu hình khác nhau theo dự án hoặc máy (đường dẫn, màu sắc, HTTP proxy): kết hợp cờ + biến môi trường

  • Cấu hình dùng chung cho toàn dự án (kiểu Makefile, package.json): dùng file được quản lý phiên bản

  • Tuân thủ XDG Base Directory spec — khuyến nghị đường dẫn cấu hình dựa trên ~/.config (được hỗ trợ bởi yarn, fish, neovim, tmux v.v.)

  • Khi tự động sửa file cấu hình của chương trình khác, bắt buộc phải có sự đồng ý của người dùng


Hướng dẫn — Biến môi trường (Environment Variables)

  • Biến môi trường phù hợp với hành vi thay đổi theo ngữ cảnh thực thi
  • Tên chỉ nên dùng chữ hoa, số và dấu gạch dưới, không được bắt đầu bằng số
  • Khuyến nghị giá trị một dòng — giá trị nhiều dòng có thể gây vấn đề tương thích với lệnh env
  • Hãy ưu tiên kiểm tra các biến môi trường phổ biến như NO_COLOR, DEBUG, EDITOR, HTTP_PROXY, SHELL, TMPDIR, HOME, PAGER
  • Khuyến nghị hỗ trợ đọc file .env theo từng dự án — nhưng .env không phải là vật thay thế cho file cấu hình chính thức
    • Hạn chế của .env: không nằm trong version control, không có lịch sử, chỉ có một kiểu dữ liệu chuỗi, dễ gặp vấn đề mã hóa
  • Không đọc secret từ biến môi trường — vì chúng lan sang mọi process, có thể bị lộ qua log, Docker inspect hoặc systemctl show
    • Secret chỉ nên nhận qua file credential, pipe, socket AF_UNIX hoặc dịch vụ quản lý secret

Hướng dẫn — Đặt tên (Naming)

  • Dùng từ đơn giản và dễ nhớ — nếu quá chung chung thì dễ va chạm với lệnh khác
  • Chỉ dùng chữ thường và khi cần thì dấu gạch nối (curl là ví dụ tốt, DownloadURL là ví dụ xấu)
  • Giữ tên ngắn gọn, nhưng những tên cực ngắn như cd, ls, ps nên được dành cho utility phổ dụng
  • Trường hợp đổi tên từ plumfigdocker compose, tiền thân của Docker Compose, là ví dụ thực tế cho thấy sự tiện khi gõ là tiêu chí quan trọng trong đặt tên

Hướng dẫn — Phân phối (Distribution)

  • Nếu có thể, hãy phân phối dưới dạng một binary duy nhất — có thể dùng PyInstaller v.v.
  • Nếu không thể có một binary duy nhất, hãy dùng trình cài đặt gói native của nền tảng
  • Nêu rõ cách gỡ cài đặt ở cuối hướng dẫn cài đặt

Hướng dẫn — Dữ liệu phân tích (Analytics)

  • Không gửi dữ liệu sử dụng hoặc dữ liệu crash nếu chưa có sự đồng ý của người dùng
  • Nếu có thu thập, hãy công khai rõ những gì được thu thập, lý do, cách ẩn danh hóa và thời gian lưu giữ
  • Khuyến nghị opt-in mặc định — nếu dùng kiểu opt-out thì phải thông báo rõ ngay lần chạy đầu tiên hoặc trên website
    • Giới thiệu ba ví dụ: Angular.js (opt-in tường minh), Homebrew (Google Analytics, công khai trong FAQ), Next.js (thống kê ẩn danh bật mặc định)
  • Có thể dùng các lựa chọn thay thế cho analytics như đo lường tài liệu web, đo số lượt tải xuống, phỏng vấn trực tiếp người dùng

1 bình luận

 
GN⁺ 2024-02-07
Ý kiến trên Hacker News
  • Hiện nay nhiều người không biết dòng lệnh là gì, và cũng không quan tâm vì sao nên dùng nó.

    • Điều này cũng đúng vào thập niên 1980, nhưng hiện nay số người biết về dòng lệnh còn nhiều hơn bao giờ hết. Có thể nói đây là thời kỳ hoàng kim của CLI (giao diện dòng lệnh).
  • Trong script, không nên cho phép viết tắt tùy ý các subcommand. Ví dụ, nếu cho phép mycmd ins hoặc mycmd i thay cho mycmd install, thì sau này sẽ không thể thêm lệnh mới bắt đầu bằng i.

    • Nên tránh dùng tham số ngắn trong script. Tham số ngắn giúp con người gõ ít hơn khi sử dụng, nhưng trong script thì viết tường minh có chi phí thấp hơn, và xét theo tỷ lệ đọc/viết thì cách đó đáng mong muốn hơn.
  • Hãy cân nhắc tùy chọn --dry-run. Tính năng cho xem trước công việc nào sẽ được thực hiện mà không tạo ra thay đổi thực tế rất hữu ích để học cách dùng công cụ và kiểm tra xem đã thiết lập đúng các tùy chọn phức tạp hay chưa.

  • Nếu stdout không phải là terminal tương tác, đừng hiển thị animation. Điều này giúp tránh việc thanh tiến trình trong log CI biến thành một cây thông Noel.

    • Tuyệt đối đừng hiển thị animation trên stdout. stderr dùng cho logging, cung cấp thông tin, v.v., còn stdout phải cung cấp đầu ra hữu ích bất kể có phải tty hay không.
  • Chỉ nên dùng ký hiệu và emoji khi chúng thực sự làm tăng tính rõ ràng.

    • Ký hiệu và emoji có thể được render không nhất quán giữa các terminal, đồng thời cũng dễ gây chia rẽ theo sở thích người dùng, nên cần dùng cực kỳ thận trọng.
  • Dòng lệnh Unix hiện nay một mặt thì "hữu ích đáng kinh ngạc", mặt khác lại có "khiếm khuyết trong thiết kế".

    • Có thể thấy sự hữu ích của dòng lệnh Unix nếu nghĩ đến lượng thời gian cần thiết để làm cùng một việc đó bằng C hay Rust.
    • Khiếm khuyết trong thiết kế bắt nguồn từ việc giao diện dòng lệnh phải đồng thời để con người và máy móc đều có thể đọc được. Không có cách chính thống nào để giải quyết vấn đề này.
  • Trừ khi CLI rất lớn và cần lồng nhiều cấp (ví dụ như aws), phần lớn ứng dụng nên in toàn bộ tùy chọn trong phần trợ giúp và để người dùng dùng less để tìm nội dung cần thiết.

  • Theo truyền thống, các lệnh UNIX được viết với giả định rằng chúng chủ yếu sẽ được dùng bởi các chương trình khác.

    • Trên thực tế, chúng được định hướng để dùng tương tác trong login shell. Có sự phân chia giữa các chương trình tạo ra đầu ra và các bộ lọc văn bản "im lặng", còn những chương trình phức tạp thì được viết bằng C.
  • Đừng đọc mật khẩu từ biến môi trường.

    • Chỉ nên nhận mật khẩu qua file thông tin xác thực, pipe, socket AF_UNIX, dịch vụ quản lý bí mật hoặc cơ chế IPC khác.
  • Cuốn sách toàn diện nhất về guideline cho CLI là sách của Eric Raymond.

    • Dù đã khá lâu, nhưng khi lướt qua clig.dev có thể thấy quan điểm đã thay đổi khá nhiều theo thời gian.