2 điểm bởi GN⁺ 2024-02-07 | 1 bình luận | Chia sẻ qua WhatsApp
  • Hướng dẫn này là bộ chỉ dẫn thiết kế mã nguồn mở nhằm hiện đại hóa nguyên tắc UNIX truyền thống để tạo ra CLI dễ dùng cho con người và vẫn vững chắc cho tự động hóa
  • Một CLI tốt phải hoạt động theo hướng ưu tiên con người nhưng vẫn tuân thủ các quy ước như stdout/stderr, mã thoát, pipe, JSON để có thể kết hợp tự nhiên với các chương trình khác
  • Trợ giúp, tài liệu, thông báo lỗi và đầu ra phải giúp người dùng biết bước tiếp theo cần làm gì, trong đó --help, ví dụ, gợi ý, hiển thị tiến trình và mô tả trạng thái đóng vai trò cốt lõi
  • Đối số, cờ, tương tác, cấu hình và biến môi trường phải tính đến cả kịch bản script lẫn dùng trong terminal, và an toàn hơn nếu không nhận trực tiếp giá trị bí mật qua cờ hay biến môi trường
  • Từ tên gọi, phân phối cho đến thu thập phân tích của CLI đều không được làm tổn hại cảm giác kiểm soát của người dùng và khả năng tương thích dài hạn; ngay cả khi phá vỡ quy ước thì mục đích cũng phải rõ ràng

Triết lý cơ bản của thiết kế CLI

  • Hướng dẫn này là tài liệu mã nguồn mở, cung cấp các nguyên tắc thiết kế chương trình dòng lệnh hiện đại và chỉ dẫn cụ thể dựa trên các nguyên tắc UNIX truyền thống
  • Trước đây dòng lệnh là môi trường ưu tiên máy móc gần với REPL chạy trên nền tảng scripting, nhưng CLI ngày nay mang tính ưu tiên con người mạnh hơn như một UI dựa trên văn bản để truy cập nhiều công cụ, hệ thống và nền tảng
  • Dòng lệnh có những ràng buộc cũ và tập quán khá kỳ lạ, nhưng có thể dùng trên gần như mọi laptop, vừa hỗ trợ tương tác vừa hỗ trợ tự động hóa, và thay đổi chậm hơn các thành phần hệ thống khác
  • Hướng dẫn này không đề cập tới các chương trình terminal toàn màn hình như emacs hay vim, cũng không phụ thuộc vào ngôn ngữ lập trình hoặc chuỗi công cụ cụ thể nào

CLI vừa ưu tiên con người vừa có thể kết hợp

  • Nếu CLI chủ yếu là công cụ do con người sử dụng thì cần ưu tiên con người trước
    • Nhiều chương trình CLI nhắm đến người dùng thật nhưng vẫn giữ nguyên thiết kế tương tác cũ vốn tập trung vào máy móc
  • Triết lý UNIX về những chương trình nhỏ phối hợp với nhau bằng giao diện gọn gàng vẫn còn rất quan trọng
    • Các quy ước như đầu vào chuẩn, đầu ra chuẩn, lỗi chuẩn, signal và mã thoát giúp chương trình có thể kết hợp với nhau
    • Văn bản thuần theo từng dòng rất dễ truyền qua pipe, còn JSON hữu ích khi cần dữ liệu có cấu trúc hơn
  • Tính nhất quán là chìa khóa để biến chi phí học CLI thành hiệu quả dài hạn
    • Khi theo các mẫu quen thuộc, người dùng có thể đoán lệnh và tùy chọn dễ hơn
    • Khi quy ước làm giảm tính dễ dùng thì vẫn có thể phá lệ một cách thận trọng
  • Một CLI tốt không nên quá im lặng cũng không nên xuất quá nhiều, mà phải giúp người dùng nhận được đúng lượng thông tin họ cần
  • CLI thường bị xem là kém khả năng khám phá hơn GUI, nhưng có thể tăng khả năng học bằng trợ giúp đầy đủ, ví dụ, gợi ý lệnh tiếp theo và hướng dẫn xử lý khi xảy ra lỗi

Tương tác hội thoại và độ vững chắc

  • Việc dùng dòng lệnh giống một cuộc đối thoại với nhiều lần thử và chỉnh sửa hơn là chỉ một lần chạy
    • Với đầu vào sai, có thể đưa ra phương án sửa nếu có thể
    • Có thể hiển thị rõ trạng thái trung gian của các tác vụ nhiều bước
    • Có thể yêu cầu xác nhận trước các thao tác nguy hiểm
  • CLI phải thực sự vững chắc, đồng thời cũng phải khiến người dùng cảm thấy nó đáng tin cậy
    • Phải xử lý đầu vào bất ngờ một cách mềm mại
    • Khi có thể, thao tác nên có tính idempotent
    • Không nên hiển thị stack trace trông đáng sợ trong đầu ra mặc định
  • Sự đồng cảm giúp người dùng cảm thấy phần mềm đứng về phía mình là điều quan trọng
    • Không phải là dùng emoji hay biến nó thành trò chơi, mà cốt lõi là thiết kế cẩn thận để người dùng thành công
  • Hệ sinh thái terminal có nhiều điểm không nhất quán và gây rối, nhưng chính nhờ mức ràng buộc thấp mà các phát minh mới trở nên khả thi
    • Hãy theo các mẫu hiện có, nhưng cũng có thể chủ động bỏ những tiêu chuẩn làm hại năng suất hoặc sự hài lòng của người dùng

Những quy tắc nền tảng bắt buộc phải tuân thủ

  • Khi có thể, nên dùng thư viện phân tích đối số dòng lệnh
  • Khi thành công, mã thoát phải là 0; khi thất bại phải trả về giá trị khác 0
    • Script xác định thành công hay thất bại của chương trình thông qua mã thoát
  • Đầu ra chính của lệnh phải được gửi tới stdout
    • Cả đầu ra máy có thể đọc được cũng nên mặc định đi tới stdout để pipe hoạt động đúng
  • Các thông điệp hiển thị cho người dùng như log hay lỗi phải được gửi tới stderr
    • Khi nối pipe, các thông điệp sẽ không bị trộn vào đầu vào của lệnh tiếp theo

Thiết kế trợ giúp

  • Khi nhận -h--help, phải hiển thị phần trợ giúp đầy đủ
    • Các lệnh con cũng có thể có trợ giúp riêng
    • Không nên gán -h thêm nghĩa khác
  • Nếu chạy một lệnh cần đối số mà không truyền đối số nào, nên hiển thị phần trợ giúp ngắn gọn
    • Mô tả chương trình
    • Một hoặc hai ví dụ gọi lệnh
    • Giải thích các cờ
    • Hướng dẫn dùng --help để xem chi tiết hơn
  • Nên đưa đường dẫn hỗ trợ vào phần trợ giúp
    • Liên kết website hoặc GitHub là lựa chọn phổ biến
  • Nếu có tài liệu web thì phần trợ giúp nên liên kết tới đó
    • Nếu lệnh con có trang hoặc anchor riêng thì liên kết trực tiếp sẽ hữu ích hơn
  • Vì người dùng có xu hướng xem ví dụ trước khi đọc tài liệu, nên đặt ví dụ ở gần đầu phần trợ giúp là tốt nhất
    • Có thể cho thấy trước các trường hợp sử dụng phức tạp nhưng phổ biến
    • Nếu có nhiều ví dụ, phù hợp hơn là để trong lệnh cheat sheet riêng hoặc trên một trang web
  • Phần trợ giúp có thể được định dạng để dễ quét nhanh
    • Tiêu đề in đậm giúp tăng khả năng đọc
    • Cần xử lý theo cách độc lập với terminal để ký tự escape không bị hiện nguyên văn
  • Nếu người dùng nhập sai mà có thể đoán được ý định thì nên đưa ra gợi ý
    • Ví dụ brew update jq có thể hướng dẫn sang brew upgrade jq
    • Có thể hỏi người dùng có muốn chạy lệnh được gợi ý hay không, nhưng nên tránh tự động ép chạy
  • Nếu một lệnh cần nhận dữ liệu từ stdin nhưng stdin lại là terminal tương tác, thì nên lập tức hiển thị trợ giúp rồi thoát, hoặc in thông báo ra stderr

Tài liệu hóa

  • Trợ giúp cung cấp tóm tắt tức thời và hướng dẫn cho các tác vụ phổ biến, còn tài liệu sẽ trình bày chi tiết mục đích, điều không nhằm tới, cách hoạt động và cách dùng đầy đủ của công cụ
  • Nên cung cấp tài liệu dựa trên web
    • Có thể tìm kiếm và liên kết đến từng phần cụ thể
    • Tài liệu web là hình thức tài liệu toàn diện nhất
  • Cũng nên cung cấp tài liệu trong terminal
    • Có thể truy cập nhanh
    • Đồng bộ với phiên bản công cụ đã cài đặt
    • Vẫn hoạt động khi không có Internet
  • Có thể cân nhắc cung cấp trang man
    • Nhiều người dùng sẽ kiểm tra man mycmd trước
    • Giống như gitnpm, có thể truy cập trang man thông qua lệnh con help

Nguyên tắc đầu ra

  • Đầu ra dễ đọc cho con người là điều quan trọng nhất
    • Tiêu chí đơn giản để xác định một luồng đầu ra có dành cho con người đọc hay không là nó có phải TTY hay không
  • Nên cung cấp đầu ra mà máy có thể đọc được trong phạm vi không làm giảm tính tiện dụng
    • Luồng các dòng văn bản thuần là giao diện phổ quát của UNIX
    • Người dùng kỳ vọng có thể chuyển đầu ra sang các công cụ như grep và chúng sẽ hoạt động như mong đợi
  • Nếu đầu ra thân thiện với con người làm hỏng đầu ra thân thiện với máy, có thể cung cấp --plain
    • Trong đầu ra dạng bảng, việc chia một ô thành nhiều dòng có thể phá vỡ kỳ vọng một bản ghi trên một dòng
    • --plain cung cấp đầu ra bảng không bị chỉnh sửa để dùng cho script
  • Nếu truyền --json thì phải xuất JSON đã được định dạng
    • JSON dễ xử lý các cấu trúc dữ liệu phức tạp và có thể dùng cùng jq cùng nhiều công cụ JSON CLI khác
    • Nó cũng phù hợp để pipe trực tiếp với dịch vụ web qua curl
  • Khi thành công thì nên có đầu ra, nhưng giữ ngắn gọn
    • Lệnh UNIX truyền thống thường không in gì nếu không có vấn đề, nhưng với con người điều đó có thể trông như bị treo
    • Nếu cần im lặng cho script thì có thể dùng tùy chọn -q để ẩn đầu ra không thiết yếu
  • Các lệnh thay đổi trạng thái nên cho người dùng biết điều gì đã thay đổi
    • git push là ví dụ cho việc hiển thị tác vụ đang thực hiện và trạng thái mới của nhánh từ xa
  • Cần có cách dễ dàng để xem trạng thái hiện tại của hệ thống
    • git status hiển thị trạng thái kho lưu trữ cùng gợi ý về các lệnh có thể chạy tiếp theo
  • Các thao tác vượt qua ranh giới của thế giới nội bộ chương trình thường nên mang tính tường minh
    • Ví dụ đọc hoặc ghi các tệp mà người dùng không truyền qua đối số
    • Hoặc giao tiếp với máy chủ từ xa để tải tệp xuống
  • Màu sắc nên được dùng có chủ đích
    • Dùng quá nhiều sẽ làm mất ý nghĩa và khó đọc hơn
    • Nên tắt màu nếu không phải TTY, hoặc NO_COLOR đã được đặt, hoặc TERM=dumb, hoặc có truyền --no-color
  • Không nên xuất hoạt ảnh nếu stdout không phải terminal tương tác
    • Điều này giúp tránh làm log CI trở nên lộn xộn bởi các chỉ báo tiến trình
  • Khi xuất nhiều văn bản, có thể dùng pager như less
    • Tốt nhất chỉ dùng khi stdin hoặc stdout là terminal tương tác
    • less -FIRX có thể là một tập tùy chọn hợp lý

Thông báo lỗi

  • Nếu biến lỗi thành tài liệu, có thể giảm thời gian người dùng phải đi tìm tài liệu
  • Cần bắt các lỗi có thể dự đoán và viết lại để con người hiểu được
    • Ví dụ: hướng dẫn theo kiểu không thể ghi vào file.txt và có thể cần chmod +w file.txt
  • Tỷ lệ tín hiệu trên nhiễu rất quan trọng
    • Càng nhiều đầu ra không liên quan, người dùng càng khó xác định vấn đề
    • Nếu có nhiều lỗi cùng loại, có thể nhóm chúng dưới một tiêu đề giải thích chung
  • Thông tin quan trọng nhất nên được đặt ở cuối đầu ra
    • Ánh mắt người dùng dễ bị thu hút bởi chữ màu đỏ, nên chỉ dùng có chủ ý và tiết chế
  • Nếu lỗi là điều không mong đợi hoặc khó giải thích, nên cung cấp thông tin debug·traceback và cách gửi báo lỗi
    • Để tránh làm người dùng bị quá tải, có thể ghi log debug vào tệp thay vì terminal
  • Việc gửi báo lỗi nên được làm cho dễ dàng
    • Ví dụ như cung cấp URL đã điền sẵn thông tin nếu có thể

Đối số và cờ

  • Đối số là tham số theo vị trí, còn cờ là tham số có tên
    • Trong cp foo bar, đường dẫn tệp là đối số
    • -r, --recursive, --file foo.txt là cờ
  • Khi có thể, nên ưu tiên cờ hơn đối số
    • Phải gõ nhiều hơn nhưng ý nghĩa rõ ràng
    • Dễ thay đổi cách nhập trong tương lai hơn
  • Mọi cờ đều nên có tên dài
    • Ví dụ dùng cả -h--help
    • Điều này hữu ích để thể hiện rõ ý nghĩa trong script
  • Cờ một ký tự chỉ nên dùng cho các tùy chọn thường dùng
    • Nhờ đó có thể giữ lại không gian tên ngắn cho các cờ sẽ thêm sau này
  • Nếu áp dụng cùng một thao tác đơn giản lên nhiều tệp, nhiều đối số là phù hợp
    • Dạng như rm file1.txt file2.txt file3.txt rất hợp với globbing
  • Nếu có từ 2 đối số trở lên mang ý nghĩa khác nhau thì có khả năng thiết kế đang có vấn đề
    • Ngoại lệ là các thao tác phổ biến và cốt lõi như cp <source> <destination>; sự ngắn gọn ở đây đáng để ghi nhớ
  • Nếu đã có tên cờ chuẩn thì nên làm theo
    • -a, --all
    • -d, --debug
    • -f, --force
    • --json
    • -h, --help
    • -n, --dry-run
    • --no-input
    • -o, --output
    • -p, --port
    • -q, --quiet
    • -u, --user
    • --version
  • Giá trị mặc định nên là hành vi đúng cho đa số người dùng
    • Nếu giả định người dùng sẽ tìm đúng cờ và nhớ dùng nó mỗi lần, trải nghiệm của phần lớn người dùng sẽ trở nên tệ hơn
  • Nếu không có đầu vào thì có thể hỏi bằng prompt, nhưng prompt không được là bắt buộc
    • Luôn phải có cách truyền đầu vào bằng cờ hoặc đối số
    • Nếu stdin không phải terminal tương tác thì phải bỏ qua prompt và yêu cầu các cờ hoặc đối số cần thiết
  • Nên xác nhận trước các thao tác nguy hiểm
    • Trong chạy tương tác, có thể yêu cầu nhập y hoặc yes
    • Trong chạy không tương tác, có thể yêu cầu -f hoặc --force
    • Với thao tác nghiêm trọng, có thể yêu cầu nhập trực tiếp tên đối tượng sẽ bị xóa hoặc cung cấp cờ như --confirm="name-of-thing"
  • Nếu nhận đầu vào·đầu ra tệp thì nên hỗ trợ - cho stdin hoặc stdout
    • Nhờ đó có thể nối với đầu ra của lệnh khác mà không cần tệp tạm
  • Khi có thể, nên xử lý thứ tự đối số, cờ và lệnh con một cách độc lập
    • Vì người dùng thường thêm tùy chọn vào cuối lệnh trước đó rồi chạy lại
  • Không nên đọc trực tiếp giá trị bí mật từ cờ
    • Giá trị của --password có thể lộ ra trong đầu ra ps hoặc lịch sử shell
    • Nên cân nhắc đầu vào từ tệp như --password-file hoặc từ stdin

Tương tác

  • Prompt hoặc các yếu tố tương tác chỉ nên dùng khi stdin là TTY
    • Trong script hoặc khi nhận đầu vào qua pipe, prompt sẽ không hoạt động, nên cần báo lỗi cho biết phải truyền cờ nào
  • Nếu truyền --no-input thì không được thực hiện prompt hay hành vi tương tác
    • Nếu cần đầu vào thì phải thất bại và hướng dẫn cách truyền bằng cờ
  • Khi nhận mật khẩu, không nên hiển thị giá trị mà người dùng đang gõ
    • Thường xử lý bằng cách tắt echo của terminal
  • Người dùng phải có cách thoát ra
    • Ngay cả khi đang bị chặn bởi network I/O v.v., Ctrl-C vẫn phải hoạt động
    • Nếu là wrapper như SSH, tmux, telnet, nơi không thể thoát bằng Ctrl-C, thì phải chỉ rõ cách thoát

Lệnh con

  • Nếu công cụ đủ phức tạp, có thể giảm độ phức tạp bằng một tập lệnh con
    • Gộp nhiều công cụ liên quan vào một lệnh có thể giúp dễ dùng và dễ khám phá hơn
    • Cũng thuận tiện để chia sẻ cờ toàn cục, trợ giúp, cấu hình và cơ chế lưu trữ
  • Cần có tính nhất quán giữa các lệnh con
    • Cùng một ý nghĩa thì nên dùng cùng tên cờ
    • Định dạng đầu ra cũng nên tương tự
  • Với lệnh con nhiều tầng, nên dùng hệ thống đặt tên nhất quán
    • Mẫu hai tầng gồm danh từ và động từ như docker container create là khá phổ biến
    • Cả noun verb lẫn verb noun đều có thể dùng, nhưng noun verb có vẻ phổ biến hơn
  • Tránh các lệnh có tên mơ hồ hoặc quá giống nhau
    • Nếu cùng có updateupgrade thì có thể gây nhầm lẫn

Độ bền

  • Cần xác thực mọi dữ liệu do người dùng nhập vào
    • Vì có thể có dữ liệu không hợp lệ, nên cần kiểm tra sớm và dừng lại với lỗi dễ hiểu
  • Khả năng phản hồi quan trọng hơn tốc độ
    • Tốt nhất là xuất ra thứ gì đó cho người dùng trong vòng 100ms
    • Ngay cả trước khi gửi yêu cầu mạng cũng nên in thông báo để không trông như bị treo
  • Với các tác vụ mất nhiều thời gian, cần hiển thị tiến trình
    • Nếu không có đầu ra trong một lúc, chương trình sẽ trông như bị hỏng
    • Nếu chỉ báo tiến trình đứng yên quá lâu ở một chỗ, có thể dùng ước lượng thời gian còn lại hoặc hoạt ảnh để cho thấy công việc vẫn đang tiếp tục
  • Hãy xử lý song song nếu có thể, nhưng phải thận trọng
    • Việc hiển thị tiến độ của các tác vụ song song trong shell là khó, và nếu đầu ra bị trộn lẫn thì sẽ gây rối
    • Nhiều thanh tiến trình của docker pull là một ví dụ cho thấy điều gì đang diễn ra
    • Dù có ẩn log phía sau thanh tiến trình khi chạy bình thường, khi xảy ra lỗi vẫn phải in log để có thể gỡ lỗi
  • Các tác vụ mạng phải có timeout
    • Timeout phải có thể cấu hình được và cần có giá trị mặc định hợp lý
  • Phải có khả năng phục hồi sau khi thất bại
    • Sau lỗi tạm thời, khi chạy lại bằng <up><enter>, cần có thể tiếp tục từ điểm đã dừng
  • Nếu có thể, cách tiếp cận crash-only là tốt
    • Khi lỗi hoặc bị ngắt, có thể thoát ngay lập tức, còn việc dọn dẹp có thể để sang lần chạy sau
  • Người dùng có thể dùng công cụ theo những cách ngoài dự đoán
    • Họ có thể bọc nó trong script, dùng trên Internet không ổn định, chạy nhiều instance cùng lúc, hoặc chạy trong môi trường chưa được kiểm thử

Tương thích tương lai

  • Subcommand, đối số, cờ, tệp cấu hình và biến môi trường đều là giao diện, và phải được duy trì để hoạt động lâu dài
  • Nếu có thể, các thay đổi nên mang tính bổ sung
    • Thay vì phá vỡ hành vi của cờ hiện có, có thể thêm cờ mới
  • Nếu cần thay đổi không mang tính bổ sung, phải cảnh báo trước
    • Khi dùng cờ không còn được khuyến nghị, cần thông báo về thay đổi sắp tới và cả cách tương thích tương lai có thể dùng ngay từ bây giờ
  • Đầu ra dành cho con người nhìn chung có thể thay đổi mà không sao
    • Nên hướng người dùng dùng --plain hoặc --json trong script để có đầu ra ổn định
  • Nên tránh subcommand catch-all
    • Cách xử lý tự động thành run nếu không phải subcommand đã biết có thể làm hỏng cách dùng hiện có khi sau này thêm subcommand mới
  • Không nên cho phép viết tắt tùy ý cho subcommand
    • Nếu cho phép install thành i hoặc ins, sau này sẽ khó thêm các lệnh khác bắt đầu bằng i
    • Alias phải rõ ràng và được duy trì ổn định
  • Cần cân nhắc liệu 20 năm nữa lệnh có còn chạy theo cùng cách hay không
    • Không nên tạo ra quả bom hẹn giờ khiến lệnh ngừng hoạt động vì phụ thuộc Internet bên ngoài thay đổi hoặc biến mất

Tín hiệu và ký tự điều khiển

  • Khi người dùng gửi Ctrl-C, tức tín hiệu INT, chương trình phải thoát nhanh nhất có thể
    • Trước khi bắt đầu dọn dẹp, phải lập tức thông báo điều gì đó
    • Mã dọn dẹp phải có timeout
  • Nếu Ctrl-C được nhấn lại trong lúc đang dọn dẹp lâu, phải có thể bỏ qua bước dọn dẹp
    • Docker Compose thông báo rằng nếu nhấn Ctrl-C thêm một lần nữa trong lúc thoát, có thể lập tức buộc dừng container
  • Chương trình phả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 được thực hiện

Cấu hình và biến môi trường

  • Cách cấu hình nên thay đổi theo mức độ cụ thể, tính ổn địnhđộ phức tạp
    • Các thiết lập thường xuyên thay đổi theo từng lần chạy thì cờ là phù hợp
    • Các thiết lập tương đối ổn định nhưng thay đổi theo người dùng, dự án hoặc máy thì cờ và biến môi trường là phù hợp
    • Các thiết lập ổn định cho mọi người dùng trong một dự án thì tệp cấu hình dành riêng cho lệnh, được quản lý bằng phiên bản, là phù hợp
  • Nên tuân theo XDG Base Directory Specification
    • Mục đích là hỗ trợ vị trí cấu hình dùng chung như ~/.config để giảm số lượng dotfile trong thư mục home
  • Nếu tự động sửa các tệp không phải cấu hình của chính chương trình mình, cần có sự đồng ý của người dùng và cho biết chính xác sẽ làm gì
    • Tốt hơn là tạo tệp cấu hình mới thay vì chèn thêm vào tệp cấu hình hệ thống hiện có
  • Thứ tự ưu tiên cấu hình phải được áp dụng từ cao xuống thấp
    • Cờ
    • Biến môi trường của shell đang chạy
    • Cấu hình cấp dự án
    • Cấu hình cấp người dùng
    • Cấu hình toàn hệ thống
  • Biến môi trường phù hợp với hành vi thay đổi theo ngữ cảnh mà lệnh được chạy
  • Tên biến môi trường nên chỉ dùng chữ hoa, số và dấu gạch dưới để có tính tương thích tối đa, và không nên bắt đầu bằng số
  • Giá trị nên là một dòng nếu có thể
    • Giá trị nhiều dòng gây vấn đề về tính tiện dụng khi dùng cùng lệnh env
  • Không nên tùy tiện chiếm dụng các tên đã được dùng rộng rãi
    • Có thể tham khảo danh sách biến môi trường chuẩn POSIX
  • Nếu có thể, nên kiểm tra các biến môi trường phổ biến
    • NO_COLOR, FORCE_COLOR
    • DEBUG
    • EDITOR
    • HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, NO_PROXY
    • SHELL
    • TERM, TERMINFO, TERMCAP
    • TMPDIR
    • HOME
    • PAGER
    • LINES, COLUMNS
  • Khi phù hợp, có thể đọc biến môi trường từ .env
    • Hữu ích cho các biến hầu như không thay đổi khi làm việc trong một thư mục cụ thể
  • .env không phải là sự thay thế cho tệp cấu hình chính thức
    • Thường không được lưu trong quản lý mã nguồn
    • Chỉ có kiểu chuỗi
    • Dễ trở nên lộn xộn
    • Dễ phát sinh vấn đề mã hóa
    • Dễ chứa thông tin xác thực nhạy cảm và dữ liệu khóa
  • Không nên đọc giá trị bí mật từ biến môi trường
    • Biến môi trường có thể bị lộ qua tiến trình, log, docker inspect, systemctl show, v.v.
    • Bí mật nên được nhận qua tệp thông tin xác thực, pipe, socket AF_UNIX, dịch vụ quản lý bí mật hoặc các cơ chế IPC khác

Tên, phân phối, thu thập phân tích

  • Tên chương trình CLI nên đơn giản và dễ nhớ vì người dùng sẽ gõ thường xuyên
    • Nếu quá chung chung, nó có thể xung đột với lệnh khác hoặc gây nhầm lẫn cho người dùng
  • Tên nên dùng chữ thường và dấu gạch nối khi cần
    • curl là tên tốt, còn DownloadURL là ví dụ không tốt
  • Tên nên ngắn, nhưng tên quá ngắn thì phù hợp hơn với các tiện ích rất phổ biến như cd, ls, ps
  • Tên phải dễ gõ
    • Có trường hợp tên ban đầu của Docker Compose là plum, nhưng vì khó gõ bằng một tay nên đã đổi thành fig
  • Nếu có thể, nên phân phối dưới dạng một binary duy nhất
    • Nếu binary duy nhất là khó, nên dùng trình cài đặt gói mặc định của nền tảng để phân phối theo cách dễ gỡ bỏ
    • Các công cụ theo ngôn ngữ, ví dụ như code linter, có thể giả định rằng người dùng có trình thông dịch của ngôn ngữ đó
  • Phải dễ gỡ bỏ
    • Vì nhiều trường hợp người dùng muốn gỡ ngay sau khi cài, nên tốt hơn là đặt hướng dẫn gỡ bỏ ngay bên dưới hướng dẫn cài đặt
  • Chỉ số sử dụng có thể giúp cải thiện công cụ, nhưng người dùng CLI kỳ vọng họ kiểm soát được môi trường của mình
  • Không được gửi dữ liệu sử dụng hoặc crash nếu chưa có sự đồng ý
    • Phải nêu rõ thu thập gì, vì sao thu thập, được ẩn danh đến mức nào, ẩn danh bằng cách nào, và lưu giữ trong bao lâu
    • Lý tưởng nhất là opt-in; nếu chọn opt-out với thu thập mặc định, phải thông báo rõ trên website hoặc ở lần chạy đầu tiên và phải dễ tắt
  • Cũng có thể cân nhắc các phương án thay thế cho việc thu thập phân tích
    • Gắn đo lường vào tài liệu web
    • Gắn đo lường vào lượt tải xuống
    • Trao đổi trực tiếp với người dùng và khuyến khích phản hồi cũng như yêu cầu tính năng qua tài liệu và kho mã nguồn

1 bình luận

 
GN⁺ 2024-02-07
Ý kiến trên Hacker News
  • Nói “ngày nay đa số thậm chí không biết dòng lệnh là gì” thì đúng, nhưng ngay cả vào thập niên 1980 mà TFA lấy làm chuẩn cũng vậy
    Khác biệt là hiện nay số người biết và có thể dùng dòng lệnh là nhiều nhất trong lịch sử, tăng ít nhất ở mức một chữ số, có khi là hai chữ số, nên có thể gọi đây là thời hoàng kim của CLI

    • Để tách nhỏ một khối monolith, đang gắn công cụ dòng lệnh vào một số phần của ứng dụng
      Trạng thái toàn cục và phụ thuộc làm cản trở việc suy luận, và cuối cùng cũng khiến debug lẫn tối ưu hiệu năng trở nên khó khăn
      Khi tách các đoạn mã 500 dòng, 1.000 dòng, 5.000 dòng, 10.000 dòng, đôi khi 50.000 dòng ra để chạy riêng, rất nhiều thứ trở nên rõ ràng hơn nhiều
      Nếu làm đúng, còn có thể dẫn đến cấu trúc Functional Core, Imperative Shell, vì dòng lệnh phù hợp với triết lý Unix nên có nhiều thao tác không gây tác dụng phụ và ít thao tác có tác dụng phụ
      Có thể tạo một lệnh nhỏ sinh và xuất trạng thái hệ thống ngay trước khi một quyết định tệ được đưa ra, và thực tế có thể chạy an toàn cả trong môi trường production
      Như vậy cũng dễ trao các công cụ này cho những người cần nằm trong bus factor để họ tham gia hơn
    • Nếu đổi “người” thành “người dùng máy tính” thì ý chính của tác giả là đúng
      Người trong một làng văn minh hẻo lánh ở Amazon nghĩ gì về terminal thì không liên quan
    • Cần phân biệt số tuyệt đối và tỷ lệ
      Khi đánh giá sự thay đổi về chất của một đối tượng đã tăng quy mô, phải nhìn vào tỷ lệ; chỉ nhìn số tuyệt đối thì sẽ có câu đúng nhưng vô nghĩa
      Ví dụ, số người nghe jazz ngày nay về tuyệt đối có thể nhiều hơn thời hoàng kim văn hóa của thể loại này, nhưng đó không phải vì jazz phổ biến hơn, mà vì bản thân số người nghe nhạc đã tăng
      Vì vậy khó có thể nói jazz ở Mỹ ngày nay quan trọng và có ảnh hưởng hơn thời hoàng kim của nó
    • Điểm cốt lõi là liệu điều đó có đúng cả về tỷ lệ trong số người dùng máy tính thập niên 1980 hay không
  • Mong là cũng cân nhắc tùy chọn --dry-run để cho xem trước thao tác nào sẽ xảy ra mà không thực sự thay đổi gì
    Nó cực kỳ hữu ích khi học một công cụ, hoặc kiểm tra xem mình đã dùng đúng các tùy chọn phức tạp và glob tệp trước khi chạy một thao tác khó hoàn tác hay chưa

    • Nếu lệnh không thể hoàn tác và có tác dụng phụ lớn, tốt hơn nên đặt mặc định là --dry-run, còn khi thực thi thật thì yêu cầu cờ --execute
    • WhatIf của PowerShell là một trong những tính năng tôi thích nhất
      Không biết nếu thiếu nó thì đã xoay xở thế nào, và nó đã cứu tôi khỏi vài lần suýt phá hỏng hoàn toàn mọi thứ
    • Ngược lại, tôi thích để hành vi mặc định là không thực hiện thay đổi thật, và nếu muốn chạy thật thì phải truyền --commit
      Với các script làm những việc không thể hoàn tác hoặc khó hoàn tác, cách này cho cảm giác an toàn hơn
    • Tôi tò mò về ví dụ các công cụ dòng lệnh có cờ dry run
      Tôi đang bối rối không biết nên thiết kế các công cụ mình đang làm như thế nào
      Nếu gọi API để lấy dữ liệu, tôi không biết làm sao biến việc đó thành dry run
      Có phải đưa ví dụ giả và đầu ra giả không?
    • Tôi nghĩ dry-run nên là một hàm có thể pipe vào bất kỳ lệnh nào: do_thing.py | dry-run
      Có thể nghĩ như việc yêu cầu ở prompt: “hãy giải thích từng bước quá trình thực hiện”
  • Không chỉ là đừng hiển thị animation nếu đầu ra chuẩn không phải terminal tương tác, mà tuyệt đối không nên hiển thị animation trên stdout
    Nhìn chung TFA khá tốt, nhưng tôi thấy tiếc khi tìm phần giải thích khác biệt giữa stderr và stdout mà lại không có
    stderr không chỉ dành cho lỗi, mà là nơi dành cho toàn bộ log và đầu ra thông tin; nếu là terminal thì cũng có thể là vùng để đưa animation vào
    stdout phải là đầu ra thực sự hữu ích, và phải nhất quán bất kể có phải terminal hay không
    Ví dụ trong echo foo | mysed 's/oo/aa/' | cat, stdout của mysedfaa, còn stderr nên chứa log như thông tin phiên bản hoặc nội dung đã tìm thấy
    Tôi không muốn phải vật lộn với công cụ bằng grep chỉ để lấy đầu ra thật, cũng không muốn việc bỏ | cat làm hành vi thay đổi theo cách khiến debug khó hơn

    • Nếu chỉnh lại một chút quy tắc “stdout là đầu ra hữu ích”, thì stdout nên là thứ người dùng đã yêu cầu
      Nếu yêu cầu --help thì nên đi ra stdout, và nếu yêu cầu log thì log cũng nên đi ra stdout
      Nếu là thông tin không được yêu cầu thì stderr mới đúng
    • Quy tắc tốt, nhưng tên gọi thì đáng tiếc
      Nếu có thể quay ngược thời gian, tôi nghĩ nếu đặt tên là stdin, stdout, stdext thì mọi người có khả năng tuân theo quy ước này hơn
      Nhưng vì tên là stderr nên lập trình viên sẽ hợp lý mà nghĩ đó là “dùng để báo lỗi”, và nếu không phải lỗi thì đưa vào stdout
      Dù vậy, về cấu trúc chương trình thì cách này tốt hơn, và dù ban đầu có thể khó hiểu hơn, nó cho phép làm nhiều việc hữu ích hơn với đầu ra nên vẫn có lợi
    • Vậy thì tôi thắc mắc animation nên hiển thị ở đâu
      Tôi cũng không nghĩ màu sắc thật sự là thông tin
  • Làm ơn tránh lời khuyên “hãy dùng ký hiệu và emoji ở những chỗ giúp rõ ràng hơn”
    yubikey-agent được nêu làm ví dụ thể hiện đúng những điều tôi ghét ở README GitHub và giao diện người dùng kiểu đùa cợt
    Về mặt kỹ thuật, ký hiệu và emoji có thể được render khác nhau tùy terminal, khiến thông điệp trở nên khó hiểu
    Về mặt thẩm mỹ, mức độ chấp nhận sự nghịch ngợm và vui tươi ở mỗi người rất khác nhau, nên chỉ nên dùng hết sức tiết chế và chỉ khi thật sự biết mình đang làm gì

    • Tôi thì thích kiểu đầu ra như vậy
      Tôi cũng thích đầu ra terminal có màu, và emoji cũng có màu nên giúp nắm nhanh bức tranh tổng thể của tình huống
      Tôi thích cả tô sáng cú pháp, và nếu không có thì cảm thấy đọc code khó hơn nhiều
      Không phải ai cũng như vậy; cũng như tôi không kỳ vọng gu của mình trở thành mặc định, gu ngược lại cũng không nên bị áp làm mặc định
    • Nếu là tính năng tùy chọn thì tốt
      Có người thích, có người ghét, nên mặc định cứ tắt và cho người dùng chọn
    • Với vai trò hướng dẫn, tôi muốn giới hạn vốn từ emoji dùng trong đầu ra tối đa hai cái
      Ví dụ chỉ cần một cái cho thành công, một cái cho thất bại là đủ
      Và thông tin mà emoji truyền tải bắt buộc cũng phải được lặp lại bằng văn bản
    • Rất đồng cảm
      Lần đầu xem CLI Guidelines, tôi cũng dừng đọc ngay ở phần đó
      Lần này tôi đã đọc cả phần còn lại, và nhìn chung khá ổn
  • Tôi hiểu rằng một số CLI rất lớn như aws nên cần lồng nhau, nhưng việc phải lần theo CLI lồng nhau thật sự rất khó chịu
    Với đa số ứng dụng, tôi thích chúng in ra mọi tùy chọn trong phần help, rồi để tôi dùng less tìm cái mình cần hơn

    • Tôi đang làm khá nhiều ứng dụng TUI, và gắn cho mọi ứng dụng một frontend TUI tốt để những người ít kỹ thuật hơn, chẳng hạn QA, cũng dùng được
      TUI này thuần túy là UI/UX tùy chọn; nếu muốn thì dùng, và nó chuyển các thiết lập đã chọn sang một tệp được tạo riêng hoặc một hàm
      Tệp hoặc hàm đó luôn có thể được gọi trực tiếp từ terminal, và cũng cung cấp mọi tùy chọn cùng phần trợ giúp mà TUI dùng
      Vì vậy nó cũng có thể dùng trong script, và hữu ích cho người dùng ở nhiều mức độ thành thạo khác nhau
    • Còn tùy vào số lượng lệnh con và mức độ rõ ràng của các lệnh đó
      Nếu bạn biết lệnh con mình cần, việc thu hẹp tùy chọn xuống chỉ những thứ cần thiết khá hữu ích, nhưng nếu không biết thì có thể rất khổ
      Lục một danh sách tùy chọn dài bằng grep cũng mệt, nên cần có sự cân bằng
    • Đó chẳng phải là vai trò của trang man sao?
    • Cách bung --help của lệnh con vào lệnh cha có thể hữu ích
      Ví dụ git stash --help hiển thị nguyên phần trợ giúp của git stash, bao gồm từng mục bên trong
  • Cách giải thích rằng “theo truyền thống, các lệnh UNIX được viết chủ yếu với giả định rằng chương trình khác sẽ dùng chúng” là không chính xác
    Ban đầu chúng chủ yếu được nhắm tới việc sử dụng tương tác trong login shell
    Có những chương trình tạo đầu ra ra stdout (ls, cat, find, tty, who, date) và những bộ lọc văn bản im lặng (tr, grep, cut, uniq, sort, wc), và ở thời đó người ta có thể làm các tác vụ tính toán cơ bản bằng lệnh một dòng
    Các chương trình phức tạp được viết bằng C, và sau khi các ngôn ngữ chuyên biệt theo miền như sed và AWK xuất hiện, một số chương trình xử lý nhiều chuỗi mới được chuyển sang shell
    Shell không phải là một môi trường lập trình đúng nghĩa, và cũng chưa từng được thiết kế cho mục đích đó

    • Có trường hợp một chương trình C 10.000 dòng chỉ cần 10 dòng shell, và cũng có trường hợp một chương trình shell 1.000 dòng lẽ ra chỉ cần 100 dòng C
      Ngày nay nhiều khi làm cả hai bằng Python hoặc Lua sẽ tốt hơn, nhưng shell và C là những thứ được cài đặt phổ biến nhất
    • Dù có “đúng nghĩa” hay không, shell vẫn là một ngôn ngữ lập trình, và trong Unix thời kỳ đầu điều đó còn rõ hơn nhiều
      Việc tách nhánh thành sh, bash, ksh, csh là một ví dụ hay
      Tôi cho rằng xem bộ công cụ POSIX tiêu chuẩn như thư viện chuẩn của nhiều ngôn ngữ shell là khá hợp lý
  • Dòng lệnh Unix hiện nay một mặt thì “cực kỳ hữu ích”, mặt khác lại hỏng ngay từ thiết kế
    Cứ nghĩ xem nếu viết đoạn dưới đây bằng C hoặc Rust thì mất bao lâu, tính hữu ích là rất rõ:
    curl -sS https://go.dev/doc/devel/release | html2text | grep -o -P '\bgo\d+\.\d+\.\d+\b' | sort -V | uniq | tail -1
    Nhưng vì sao nó hỏng về mặt thiết kế thì có thể xem https://news.ycombinator.com/item?id=29747034
    Vấn đề là giao diện dòng lệnh phải vừa dễ đọc với con người, vừa dễ đọc với máy, và không có cách chuẩn nào để giải quyết chuyện này

    • Chính ví dụ đó cho thấy rõ vì sao nó hỏng về mặt thiết kế
      Vốn dĩ có thể đã có một lời giải chuẩn cho vấn đề vừa để người vừa để máy đọc được
      Apple đã tạo Human Interface Guidelines để cụ thể hóa ý nghĩa của các trừu tượng thị giác trong UI desktop
      Nhưng dòng lệnh không được tạo ra bởi những người suy nghĩ như các nhà thiết kế Apple, mà bởi những người nghĩ rằng “lười biếng, nôn nóng, kiêu ngạo là đức tính tốt, vậy làm sao diễn đạt thứ mình muốn bằng ít code nhất”
      Ở thời điểm đó đây cũng không hẳn là lựa chọn sai, từng byte đều quan trọng, nhưng quyết định thiết kế ấy giờ đã cắm chặt vào một chuỗi công cụ không thể dịch chuyển
      Để có thứ tốt hơn hiện nay, có lẽ phải gần như từ bỏ chuỗi công cụ POSIX, và rất khó tưởng tượng việc xây được một UX dễ khám phá và nhất quán về khái niệm trên nền tảng hiện tại
    • Suy cho cùng máy tính tồn tại để phục vụ chúng ta, nên lời giải cuối cùng phải là làm cho máy đọc giỏi như con người
    • Thực tế thường không phải là “đồng thời”, mà là con người và máy dùng cùng một lệnh ở những thời điểm khác nhau
      Ngay cả tại cùng một thời điểm cũng có nhiều cách cho phép các đầu ra khác nhau
      Chỉ là phải có một chuẩn mà mọi người đều tuân theo, và chính ở điểm đó mọi thứ trở nên phức tạp
    • Ví dụ đó không những không chứng minh được rằng thiết kế bị hỏng, mà còn dường như cho thấy một cách giải quyết tương đối dễ
      Nếu vấn đề là rất ít bản triển khai ls cho phép kết thúc tên tệp bằng ký tự NUL thay vì xuống dòng, thì lời giải trông quá rõ ràng đến mức tôi có cảm giác mình đang bỏ sót điều gì đó
      Có lý do nào khiến giao diện shell từ chối chuyện này không?
    • Cách được bàn trong bài là chế độ đầu ra cho máy đọc như --json
  • Nói “đừng đọc giá trị bí mật từ biến môi trường” rồi đề xuất tệp thông tin xác thực, pipe, socket AF_UNIX, dịch vụ quản lý bí mật và các cơ chế giao tiếp liên tiến trình khác; tôi thắc mắc trong số đó cái nào tiện nhất và có tính di động tốt nhất
    Cũng tò mò liệu dịch vụ quản lý bí mật chỉ được dùng trong công việc hay cả trong dự án cá nhân

    • Tôi là một trong các tác giả của CLI Guidelines
      Có một bài viết đi sâu hơn một chút về việc xử lý giá trị bí mật trên dòng lệnh tại https://smallstep.com/blog/command-line-secrets/
      Tệp thông tin xác thực là một lựa chọn đơn giản và có tính di động tốt
      Tệp vốn đã có mô hình phân quyền, và không phụ thuộc vào dịch vụ bên ngoài hay API độc quyền
      Nếu chương trình nhận tệp thông tin xác thực thì cũng tương thích với systemd credentials
      systemd credentials an toàn hơn so với tệp thông tin xác thực không mã hóa, được mã hóa và cũng có thể ràng buộc với TPM, nhưng phần mềm sử dụng thông tin xác thực không cần tự hỗ trợ TPM
    • Sẽ tốt nếu cho phép chương trình chỉ định lệnh để lấy giá trị bí mật
      Ví dụ mbsync(https://isync.sourceforge.io/mbsync.html) có khoảng ba cách để cung cấp mật khẩu xác thực IMAP
      Nếu không đặt mật khẩu thì khi chạy sẽ hiện prompt, cũng có thể đặt mật khẩu dạng văn bản thuần trong tệp cấu hình nhưng như vậy bất tiện khi chia sẻ
      Ngoài ra có thể cấu hình lệnh để lấy mật khẩu, nhờ đó ủy quyền xử lý cho trình quản lý mật khẩu như pass(1)(https://www.passwordstore.org/) hoặc một prompt đồ họa tương tác
    • Dịch vụ quản lý bí mật có lẽ là tiện nhất
      Tài liệu tốt nên có thể thiết lập dễ dàng mà không cần tự xây thêm
      Các trình quản lý bí mật như Doppler(https://Doppler.com) hoặc AWS Secrets Manager(https://aws.amazon.com/secrets-manager/) có lợi thế là bảo vệ giá trị bí mật ở nơi an toàn và giảm thiểu mức độ lộ lọt của chúng
      Thậm chí có thể giảm việc lộ cho cả lập trình viên nội bộ, giúp ngăn các vụ rò rỉ dữ liệu vốn có thể dễ dàng tránh được
      Những vụ rò rỉ như vậy có thể đặt cả công ty vào rủi ro và ngày càng phổ biến
  • Bài liên quan: Command Line Interface Guidelines - https://news.ycombinator.com/item?id=38053692 - tháng 10 năm 2023, 1 bình luận
    Command Line Interface Guidelines - https://news.ycombinator.com/item?id=31651161 - tháng 6 năm 2022, 1 bình luận
    Command Line Interface Guidelines - https://news.ycombinator.com/item?id=25492119 - tháng 12 năm 2020, 5 bình luận

  • Thật sự rất khó chịu khi một số terminal tự động chạy lệnh nếu chuỗi dán từ clipboard có ký tự xuống dòng ở cuối
    Cá nhân tôi cho rằng giao diện dòng lệnh không nên làm như vậy

    • Trong Bash có thể dùng bind 'set enable-bracketed-paste on'
    • iTerm trên macOS sẽ yêu cầu xác nhận nếu bạn cố dán chuỗi có chứa xuống dòng
      Nếu thấy phiền thì cũng có thể tắt đi
    • Fish shell theo mặc định chỉ chèn đúng như mong đợi và cho phép chỉnh sửa lệnh trước khi thực thi
      Nếu cần thì nhiều dòng cũng được, và phải nhấn Enter mới chạy
      Fish shell thường có các giá trị mặc định hợp lý, và ngoài prompt ra thì tôi vẫn chưa tìm thấy phần nào đặc biệt muốn tùy biến
    • Có thể đáng thử dùng trình quản lý clipboard
      Hầu hết những cái tôi từng dùng đều có tùy chọn loại bỏ ký tự xuống dòng khỏi nội dung clipboard
    • Nếu biết chắc chỉ có tối đa một ký tự xuống dòng, tôi nhập # trước rồi dán
      Ngay cả khi ký tự xuống dòng được diễn giải thì toàn bộ dòng sẽ trở thành chú thích nên an toàn, và trước khi chạy thật chỉ cần xóa # ở đầu dòng