แนวทางสำหรับ Command Line Interface
(clig.dev)แนวทางโอเพนซอร์สที่อัปเดตให้ทันสมัย โดยยังคงยึดตามหลักการ Unix แบบดั้งเดิม
- ปรัชญาการออกแบบ CLI
→ ให้ความสำคัญกับคนเป็นอันดับแรก
→ องค์ประกอบย่อยที่เรียบง่ายและทำงานร่วมกันได้
→ รักษาความสม่ำเสมอระหว่างโปรแกรม
→ พูดเท่าที่จำเป็น (เอาต์พุตไม่น้อยหรือมากเกินไป)
→ ทำให้ค้นพบได้ง่าย (มี help ที่ครอบคลุม, ตัวอย่าง, แนะนำคำสั่งถัดไปที่ควรรัน, แนะนำสิ่งที่ควรทำเมื่อเกิดข้อผิดพลาด)
→ เหมือนการสนทนาทั่วไป
→ แข็งแรงทนทาน
→ เห็นอกเห็นใจผู้ใช้
→ ความสับสน: หากต้องฝ่าฝืนกฎ ให้ระบุเจตนาและจุดประสงค์ให้ชัดเจน
- แนวทาง CLI
→ พื้นฐาน
✓ ควรใช้ไลบรารีสำหรับ parsing บรรทัดคำสั่ง: Go(Cobra,cli), Node(oclif), Python (Click,Typer), Ruby(TTY)
✓ เมื่อสำเร็จให้คืนค่า 0, เมื่อผิดพลาดให้คืนค่าอื่นที่ไม่ใช่ 0
✓ เอาต์พุตให้ส่งไปที่ stdout
✓ log, error และข้อความอื่น ๆ ให้ส่งไปที่ stderr
→ help
✓ หากรันโดยไม่ระบุ option ให้แสดง help (-h, --help)
✓ โดยปกติให้แสดง help แบบกระชับ
· โปรแกรมนี้ทำอะไร
· ตัวอย่างการเรียกใช้หนึ่งหรือสองแบบ
· คำอธิบายของ flag (ถ้ามีไม่มาก)
· --help สำหรับคำอธิบายเพิ่มเติม
✓ เมื่อใช้ option -h, --help ให้แสดง help แบบเต็ม
✓ ระบุช่องทางสำหรับรับ feedback/issue
✓ ใน help ควรมีลิงก์ไปยังเอกสารเวอร์ชันเว็บ
✓ อธิบายด้วยตัวอย่าง
✓ ถ้ามีตัวอย่างมาก ควรนำไปไว้ที่อื่น (cheatsheet หรือเว็บเพจ)
✓ ไม่ต้องใส่ใจกับ man page มากนัก (คนใช้ไม่มาก และยังใช้บน Windows ไม่ได้)
✓ ถ้า help ยาว ให้ pipe ผ่าน pager
✓ แสดง flag และคำสั่งที่ใช้บ่อยที่สุดไว้ตอนต้นของ help
✓ ใช้ formatting ใน help (เช่น ตัวหนา)
✓ ถ้าผู้ใช้ทำอะไรผิด และคุณพอเดาได้ว่าเขาหมายถึงอะไร ให้แนะนำสิ่งนั้น
✓ หากคำสั่งของคุณคาดหวังว่าจะรับบางอย่างผ่าน pipe แต่ stdin เป็น interactive terminal ให้แสดง help แล้วจบทันที
→ เอาต์พุต
✓ เอาต์พุตแบบ Human-readable (อ่านได้โดยมนุษย์) สำคัญที่สุด
✓ หากไม่กระทบต่อการใช้งาน ควรมีเอาต์พุตแบบ machine-readable ด้วย
✓ หากความเป็น human-readable ทำให้ machine-readable เป็นไปไม่ได้ ควรมี option --plain เพื่อให้ใช้งานร่วมกับ grep / awk เป็นต้นได้
✓ เมื่อรับ --json ให้แสดงผลเป็นรูปแบบ JSON
✓ เมื่อสำเร็จ เอาต์พุตควรไม่มีจะดีที่สุด แต่ถ้าจำเป็นต้องมี ให้กระชับ และรองรับ option -q เพื่อตัดเอาต์พุตที่ไม่จำเป็น
✓ หากมีการเปลี่ยนสถานะ ให้บอกผู้ใช้ (ดูเอาต์พุตของ git push)
✓ ทำให้ดูสถานะปัจจุบันของระบบได้ง่าย
✓ แนะนำคำสั่งที่ผู้ใช้สามารถรันต่อได้ (เหมือนที่ git status แสดง git add / restore)
✓ การกระทำที่ออกไปนอกขอบเขตภายในของโปรแกรมต้องชัดเจน เช่น อ่านหรือเขียนไฟล์ที่ผู้ใช้ไม่ได้สั่ง (cache) หรือเชื่อมต่อกับเซิร์ฟเวอร์ระยะไกล (ดาวน์โหลดไฟล์)
✓ ใช้ ASCII art เพื่อเพิ่มความหนาแน่นของข้อมูล
✓ ใช้สีอย่างมีเจตนา อย่าใช้พร่ำเพรื่อ
✓ หากไม่ใช่ terminal หรือผู้ใช้ร้องขอ ให้ปิดสี
✓ หาก stdout ไม่ใช่ interactive terminal อย่าแสดง animation
✓ ใช้สัญลักษณ์/อีโมจิเฉพาะเมื่อช่วยให้ชัดเจนขึ้น
✓ โดยปกติอย่าแสดงข้อมูลที่มีแต่ผู้สร้างเท่านั้นที่เข้าใจ
✓ อย่าใช้ stderr เป็นเหมือนไฟล์ log (อย่างน้อยอย่าตั้งเป็นค่าเริ่มต้น ควรแสดงระดับ log เช่น ERR, WARN เฉพาะในโหมดละเอียด)
✓ หากต้องแสดงข้อความจำนวนมาก ให้ใช้เครื่องมือแบ่งหน้าอย่าง less
→ Error
✓ จับ error แล้วเขียนข้อความใหม่ให้อ่านเข้าใจได้สำหรับคน
✓ Signal-to-noise ratio สำคัญ หาก error เดียวกันเกิดหลายครั้ง ให้รวมแสดงพร้อมหัวข้ออธิบาย
✓ ควรคิดว่าผู้ใช้จะเห็นจุดไหนก่อนเป็นอย่างแรก
✓ หากเกิดข้อผิดพลาดที่ไม่คาดคิด/อธิบายไม่ได้ ให้แสดงข้อมูล debug/trace และอธิบายวิธีส่งบั๊กนี้ให้ผู้พัฒนา
✓ ทำให้ส่ง bug report ได้โดยแทบไม่ต้องออกแรงเพิ่ม (เช่น สร้าง URL ที่รวมข้อมูลครบแล้ว และเพียงเปิดในเบราว์เซอร์ก็รายงานได้เลย)
→ Argument & Flags : อาร์กิวเมนต์และแฟลก
✓ อาร์กิวเมนต์: พารามิเตอร์ตามตำแหน่ง ลำดับสำคัญ cp bar foo กับ cp foo bar ต่างกัน
✓ แฟลก: พารามิเตอร์ที่มีชื่อ เช่น -r แบบอักษรเดียว หรือ --recursive แบบหลายอักษร โดยทั่วไปลำดับไม่สำคัญ
อาจรวมค่าของผู้ใช้ไว้ด้วย เช่น --file foo.txt หรือ --file=foo.txt
✓ ควรเลือกใช้แฟลกมากกว่าอาร์กิวเมนต์ แม้จะต้องพิมพ์มากกว่า แต่ชัดเจนกว่า หากมีอาร์กิวเมนต์มากจะขยายความสามารถภายหลังได้ยาก
✓ ควรมีทั้งเวอร์ชันสั้นและเวอร์ชันเต็มของแฟลก หากใช้เวอร์ชันเต็มในสคริปต์ก็ไม่ต้องมีคำอธิบายเพิ่ม
✓ ใช้แฟลกแบบอักษรเดียวเฉพาะกับแฟลกที่ใช้บ่อยเป็นหลัก
✓ สำหรับการทำงานง่าย ๆ ก็อาจรับหลายอาร์กิวเมนต์ได้
✓ หากต้องมีอาร์กิวเมนต์ที่ต่างกันตั้งแต่สองตัวขึ้นไป อาจเป็นสัญญาณว่าคุณกำลังทำอะไรผิด
✓ แฟลกควรใช้ชื่อมาตรฐาน (ถ้ามีอยู่แล้ว)
-a --all , -d --debug , -f --force , -h --help , -o --output , -p --port , -q --quiet , -u --user
✓ ค่าเริ่มต้นควรเป็นสิ่งที่เหมาะกับผู้ใช้ส่วนใหญ่
✓ หากผู้ใช้ระบุอาร์กิวเมนต์/แฟลกที่ต้องรับค่า แต่ไม่ได้ส่งค่ามา ให้ถามผู้ใช้เพื่อรับค่า
✓ ควรมีวิธีส่งค่าผ่านอาร์กิวเมนต์/แฟลกเสมอ และไม่ควรบังคับให้ต้องกรอกผ่าน prompt เท่านั้น
✓ ก่อนทำสิ่งที่อันตราย ควรขอการยืนยันเสมอ
✓ หากอินพุตหรือเอาต์พุตเป็นไฟล์ ควรรองรับ - เพื่อรับจาก stdin หรือส่งออกไปยัง stdout
$ curl https://example.com/something.tar.gz | tar xvf -
✓ หากแฟลกรับค่าเพิ่มเติมได้ ควรยอมรับคำพิเศษอย่าง none ด้วย เช่น ssh -F none
✓ หากเป็นไปได้ ให้ออกแบบอาร์กิวเมนต์ แฟลก และ subcommand โดยไม่ขึ้นกับลำดับ
✓ ค่าอาร์กิวเมนต์ที่มีความอ่อนไหว (เช่น รหัสผ่าน) ควรเปิดให้ป้อนผ่านไฟล์ได้
→ Interactivity
✓ ใช้ prompt หรือฟีเจอร์ interactive เฉพาะเมื่อ stdin เป็น interactive terminal เท่านั้น
✓ หากมีการส่ง --no-input มา อย่าใช้ prompt หรือฟีเจอร์ interactive ใด ๆ
✓ เมื่อต้องรับรหัสผ่าน อย่าแสดงค่าที่ผู้ใช้พิมพ์
✓ ทำให้ผู้ใช้ออกได้ง่าย (อย่าทำแบบ vim) ให้ Ctrl-C ใช้งานได้ หากเป็นกรณีรันโปรแกรมอย่าง ssh, tmux จนใช้ Ctrl-C ไม่ได้ ให้แสดงอย่างชัดเจนว่ามี escape sequence ที่ขึ้นต้นด้วย ~ แบบเดียวกับ SSH
→ Subcommands
✓ เครื่องมือที่ซับซ้อนสามารถลดความซับซ้อนได้ด้วย subcommand
✓ นอกจากนี้ หากมีหลายเครื่องมือที่เกี่ยวข้องกันอย่างใกล้ชิด ก็สามารถรวมไว้ในคำสั่งเดียวเพื่อให้ใช้งานสะดวกขึ้นได้
✓ ควรสม่ำเสมอระหว่าง subcommand ต่าง ๆ แฟลกเดียวกันควรมีความหมายเดียวกัน และมีรูปแบบเอาต์พุตคล้ายกัน
✓ ใช้ชื่อที่สม่ำเสมอระหว่าง subcommand หลายชั้น
✓ อย่าใส่คำสั่งที่ชื่อคล้ายกันหรือทำให้สับสน เช่น update กับ upgrade
→ Robustness
✓ ตรวจสอบอินพุตของผู้ใช้ทั้งหมด เช็กให้เร็ว และแสดงข้อผิดพลาดที่เข้าใจได้
✓ ความตอบสนองสำคัญกว่าความเร็ว
✓ หากใช้เวลานาน ให้แสดงความคืบหน้า
✓ ถ้าเป็นไปได้ ให้ทำงานแบบขนาน แต่ต้องคิดให้รอบคอบ
✓ ควรมี timeout
✓ ทำให้เป็น idempotent (รันซ้ำแล้วผลลัพธ์ไม่เปลี่ยน) เพื่อให้เมื่อเกิดข้อผิดพลาด ผู้ใช้สามารถกดลูกศรขึ้นใน shell แล้วรันต่อได้จากเดิม
✓ ทำให้เป็น crash-only ซึ่งเป็นขั้นต่อไปของ idempotence หากไม่จำเป็นต้อง cleanup หลังงานเสร็จ หรือเลื่อน cleanup ไปครั้งถัดไปได้ โปรแกรมก็ควรจบได้ทันทีเมื่อเกิดความล้มเหลวหรือถูกหยุด
✓ ผู้คนจะใช้โปรแกรมของคุณผิดวิธีแน่นอน
→ Future-proofing
✓ หากทำได้ การเปลี่ยนแปลงควรเป็นแบบ additive อย่าทำลายความเข้ากันได้ด้วยการเปลี่ยนพฤติกรรมเดิม แต่ให้เพิ่มแฟลกใหม่แทน
✓ หากไม่ใช่การเปลี่ยนแบบ additive ควรเตือนก่อน
✓ การเปลี่ยนเอาต์พุตสำหรับมนุษย์ส่วนใหญ่ถือว่าใช้ได้
✓ แม้จะมี subcommand ที่คนใช้บ่อย ก็อย่าสร้าง catch-all subcommand ที่รันสิ่งนั้นให้โดยไม่ต้องระบุชัดเจน
✓ อย่าอนุญาตตัวย่อของคำสั่ง subcommand แบบตามอำเภอใจ
✓ อย่าสร้าง “ระเบิดเวลา” ที่วันหนึ่งจะหยุดทำงาน
→ Signals and control Characters
✓ หากผู้ใช้กด Ctrl-C (INT signal) ให้หยุดโดยเร็วที่สุด
✓ หากผู้ใช้กด Ctrl-C ระหว่าง cleanup ที่ใช้เวลานาน ให้เพิกเฉยก่อน และหากกดอีกครั้งจึงค่อยบังคับปิด
^CGracefully stopping... (press Ctrl+C again to force)
→ Configuration
✓ ควรทำตามสเปก XDG (X Desktop Group)
✓ หากจะแก้ไขการตั้งค่าที่ไม่ใช่ของโปรแกรมคุณเอง ให้ขอการยืนยันจากผู้ใช้ และบอกให้ชัดว่าจะทำอะไร
✓ พารามิเตอร์การตั้งค่าควรถูกใช้ตามลำดับความสำคัญ
แฟลก > ตัวแปรสภาพแวดล้อมของ shell > การตั้งค่าระดับโปรเจกต์ (.env) > การตั้งค่าผู้ใช้ > การตั้งค่าระบบ
→ Environment Variables
✓ ตัวแปรสภาพแวดล้อมมีไว้สำหรับพฤติกรรมที่เปลี่ยนไปตาม context ที่คำสั่งถูกรัน
✓ เพื่อให้พกพาได้สูงสุด ชื่อตัวแปรสภาพแวดล้อมควรมีเฉพาะตัวพิมพ์ใหญ่ ตัวเลข และขีดล่าง และต้องไม่ขึ้นต้นด้วยตัวเลข
✓ หากเป็นไปได้ ควรใช้ค่าแบบบรรทัดเดียว (single-line) สำหรับตัวแปรสภาพแวดล้อม
✓ อย่าใช้ชื่อที่เป็นที่ใช้กันอย่างกว้างขวางอยู่แล้ว
✓ หากเป็นไปได้ ควรตรวจสอบและใช้ตัวแปรสภาพแวดล้อมทั่วไป
NO_COLOR, DEBUG, EDITOR, HTTP_PROXY, SHELL, TERM, TERMINFO, HOME, TMPDIR, PAGER, LINES ..
✓ หากจำเป็น ให้โหลดตัวแปรสภาพแวดล้อมจาก .env
✓ อย่าใช้นามสกุล .env กับไฟล์ตั้งค่า
→ Naming
✓ ชื่อควรเป็นคำที่เรียบง่ายและจำง่าย
✓ ใช้ตัวพิมพ์เล็กเท่านั้น และใช้ - (dash) เท่าที่จำเป็นจริง ๆ
✓ ให้สั้นเท่าที่ทำได้
✓ พิมพ์บนคีย์บอร์ดได้ง่าย
→ Distribution
✓ หากเป็นไปได้ ให้แจกจ่ายเป็น single binary
✓ ถอนการติดตั้งได้ง่าย
→ Analytics
✓ อย่าส่งข้อมูลการใช้งานและข้อมูล crash ของเครื่องมือกลับมาหาคุณโดยไม่ได้รับความยินยอมจากผู้ใช้
3 ความคิดเห็น
ขอบคุณสำหรับเนื้อหาดี ๆ ครับ
ดูเหมือนว่าด้วย Rust และ Go ที่ช่วยให้สร้างเป็น single binary ได้อย่างดี ทำให้มีเครื่องมือ command line ดี ๆ เพิ่มขึ้นเรื่อย ๆ
รวมเครื่องมือ command line ที่มีประโยชน์ช่วงนี้ https://th.news.hada.io/topic?id=793
ยูทิลิตี command line ของ rust ที่ช่วยเพิ่มประสิทธิภาพการทำงาน https://th.news.hada.io/topic?id=2958
การพัฒนาก็ยิ่งสะดวกขึ้นและทรงพลังมากขึ้นเรื่อย ๆ
สร้างแอป Command Line ด้วย Rust https://th.news.hada.io/topic?id=972
Caporal.js - เฟรมเวิร์กครบชุดสำหรับพัฒนา Node CLI https://th.news.hada.io/topic?id=2378
create-node-cli - สร้าง CLI ได้ง่ายด้วย Node.js https://th.news.hada.io/topic?id=3268
สร้าง GUI สำหรับทุกภาษาและเครื่องมือ CLI ด้วย Gooey https://th.news.hada.io/topic?id=582
ink - สร้าง CLI ด้วย React https://th.news.hada.io/topic?id=2041
ผมแปลแบบคร่าว ๆ ไปพร้อมกับได้เรียนรู้อะไรเยอะเลยเหมือนกัน พอทำเสร็จแล้วก็คิดขึ้นมาว่า.. หรือจริง ๆ น่าจะแปลทั้ง repo ไปเลยจะดีกว่า ^^;;