12 คะแนน โดย GN⁺ 2025-08-13 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • แนะนำวิธีสร้างฟีเจอร์เติมคำอัตโนมัติด้วยแท็บใน Bash และ Zsh ที่แสดงคำอธิบายได้แม้กับคำที่เติมครบแล้ว
  • Bash และ Zsh ใช้ API สำหรับ tab completion คนละแบบ และมีเพียง Zsh ที่รองรับ การแสดงคำอธิบายในรายการเติมคำอัตโนมัติ มาให้โดยพื้นฐาน
  • ใช้ _generate_foo_completions เพื่อสร้างตัวเลือก และส่งกลับผ่าน COMPREPLY ใน Bash, และ compadd ใน Zsh
  • เพื่อให้ Bash แสดงคำอธิบายแบบเดียวกับ Zsh จึงใช้วิธี ใส่คำอธิบายไว้ในสตริงของตัวเลือก และลบคำอธิบายออกเฉพาะตอนที่มีตัวเลือกเดียว
  • แม้ในกรณีที่มีตัวเลือกเดียว ก็ยังปรับปรุงให้แสดงคำอธิบายเมื่อกด <TAB> ได้ โดย จงใจเพิ่มความกำกวม เข้าไป
  • สคริปต์สุดท้ายให้ประสบการณ์ใช้งานแบบเดียวกันในทั้งสองเชลล์ และรองรับครบทั้ง การเติมบางส่วน การเติมทั้งหมด และการแสดงคำอธิบายของตัวเลือก

ที่มาของปัญหา

  • การเติมคำอัตโนมัติด้วยแท็บ (tab-completion) มีประโยชน์มากเวลาไล่ดูคำสั่งหรือแฟล็ก โดยเฉพาะเมื่อเพิ่งเริ่มใช้ API หรือเครื่องมือ CLI
  • โดยปกติ Zsh จะแสดง คำอธิบายเมื่อมีหลายตัวเลือกเท่านั้น ส่วน Bash ต้องอาศัย การตั้งค่าเพิ่มเติม
  • แต่สำหรับ คำที่เติมครบอยู่แล้ว ทั้งสองเชลล์ จะไม่แสดงคำอธิบาย
  • ทำให้ผู้ใช้ต้องผ่านขั้นตอนที่ยุ่งยากดังนี้เพื่อดูคำอธิบาย
    • ลบตัวอักษรบางส่วนออกเพื่อให้ตรงกับหลายตัวเลือก
    • กดปุ่ม <TAB> เพื่อดูรายการตัวเลือก
    • มองหาและแยกคำอธิบายที่ต้องการด้วยสายตา
    • พิมพ์ตัวอักษรที่ลบไปกลับเข้าไปอีกครั้งเพื่อรันคำสั่ง

ภาพรวมของวิธีแก้

  • เมื่อมีตัวเลือกเดียว ก็ เพิ่มตัวเลือกหลอก (dummy completion) เข้าไปเพื่อทำให้เกิดความกำกวมในรายการตัวเลือก
  • วิธีนี้ทำให้ ทั้ง Bash และ Zsh แสดงรายการตัวเลือกพร้อมคำอธิบาย ได้
  • อย่างไรก็ตาม ต้องระวังไม่ให้ข้อความคำอธิบายถูกแทรกเข้าไปในคำสั่งจริง
โฆษณา

แนวคิดพื้นฐาน

  • การเติมคำอัตโนมัติด้วยแท็บจะทำงานโดยรับคำปัจจุบันและตำแหน่งเคอร์เซอร์เมื่อกด <TAB> แล้วคืนรายการตัวเลือกที่เป็นไปได้
  • Bash: กำหนดตัวเลือกลงในอาร์เรย์ COMPREPLY
  • Zsh: ลงทะเบียนตัวเลือกด้วยคำสั่ง compadd
  • ฟังก์ชัน _generate_foo_completions จะพิมพ์สตริงของตัวเลือกออกมา และในการใช้งานจริงสามารถสร้างแบบไดนามิกตามสถานะของ CLI ได้

รองรับ Bash และ Zsh พร้อมกัน

  • แยกเขียนตามเชลล์ด้วยฟังก์ชัน _complete_foo_bash และ _complete_foo_zsh
  • ใช้ if [ -n "${ZSH_VERSION:-}" ]; then ... elif [ -n "${BASH_VERSION:-}" ]; then ... fi เพื่อแยกกรณี
  • ผู้ใช้สามารถนำสคริปต์ไปลงใน .bashrc หรือ .zshrc แล้วใช้งานได้

การแสดงคำอธิบายใน Zsh

  • ใช้รูปแบบ ชื่อ: คำอธิบาย กับสตริงของตัวเลือก
  • Zsh: ส่งชื่อและคำอธิบายด้วยอาร์เรย์ขนานผ่าน compadd -d raw -- $trimmed
  • Bash: ส่งเข้า COMPREPLY เฉพาะตัวเลือกที่ตัดส่วนคำอธิบายออกแล้วเท่านั้น (เพราะปกติไม่รองรับคำอธิบาย)
โฆษณา

ทำให้ Bash แสดงคำอธิบาย

  • ถ้ามีหลายตัวเลือก ให้แสดงสตริงที่มีคำอธิบายติดมาด้วยตรง ๆ
  • ถ้ามีตัวเลือกเดียว ให้ตัดคำอธิบายออกเฉพาะกรณีนั้น
  • อาศัยพฤติกรรมของการเติมคำอัตโนมัติใน Bash ที่จะแทรกเฉพาะคำนำหน้าร่วมกัน เพื่อไม่ให้คำอธิบายถูกใส่ลงไปในอินพุตจริง

แสดงคำอธิบายได้แม้มีตัวเลือกเดียว

  • เพื่อให้กด <TAB> แล้วเห็นคำอธิบายได้แม้คำจะเติมครบแล้ว จึง เพิ่มตัวเลือกหลอก เพื่อสร้างความกำกวมโดยตั้งใจ
  • Zsh: เพิ่มตัวเลือกหลอกลงในทั้งสองอาร์เรย์ขนาน (raw, trimmed)
  • Bash: ถ้ามีตัวเลือกเดียว ให้เพิ่มเฉพาะชื่อเข้าไปใน trimmed

ผลลัพธ์สุดท้าย

  • เมื่อมีหลายตัวเลือก จะแสดงทั้งชื่อและคำอธิบาย
  • เมื่อมีตัวเลือกเดียว ก็ยังกด <TAB> เพื่อดูคำอธิบายได้
  • ทั้ง Bash และ Zsh ให้ประสบการณ์ใช้งานแบบเดียวกัน
  • ตัวอย่างการใช้งาน:
    $ foo <TAB>  
    apple: a common fruit banana: starchy and high in potassium  
    apricot: sour fruit... cherry: small and sweet...  
    

1 ความคิดเห็น

 
GN⁺ 2025-08-13
ความคิดเห็นจาก Hacker News
  • ถ้าโปรแกรมที่สนใจบน fish มี man page ก็แค่รัน fish_update_completions
    • คำสั่งนี้จะพาร์ส man page ทั้งหมดในระบบแล้วสร้างไฟล์ autocomplete ไว้ที่ ~/.cache/fish/generated_completions/
    • ถ้า man page ไม่ดีพอหรือไม่มี ก็เขียนเองแล้วส่งกลับไปที่ upstream ได้
    • ฟอร์แมตของ fish เรียบง่าย แค่อ่านเอกสารทางการก็พอ
    • ตัวอย่าง: curl ของ -L → 'Follow redirects', -O → 'Write output to file named as remote file'
    • ตอนแชร์หน้าจอ คนมักคิดว่าฉันใช้ zsh กับปลั๊กอินต่าง ๆ แต่จริง ๆ แล้วใช้ fish ที่สวยได้ด้วยการตั้งค่าพื้นฐานล้วน ๆ
    • แต่ก็น่าจะปรับปรุงเรื่อง การเติมคำแบบไม่ยึดคำนำหน้า ที่ทำให้ car TAB ขยายเป็น blkdiscard ได้ จนเกิดอาการเพี้ยนแม้ cargo จะไม่ได้อยู่ใน PATH
    • สำหรับโปรแกรมที่ไม่มี man page และมีแค่ --help ก็สงสัยว่า fish มีอะไรคล้าย _gnu_generic ของ zsh หรือ complete -F _longopt ของ bash ไหม
    • ฝั่ง zsh ก็มีสคริปต์สร้าง autocomplete จาก man page เช่นกัน → zsh-manpage-completion-generator
    • บน OpenSUSE พอพิมพ์ zypper search fish-completion แล้วมีแพ็กเกจโผล่มาเกิน 200 ตัว เลยรู้สึกว่ามีอะไรแปลก ๆ
  • สิ่งที่น่าหงุดหงิดคือ bash autocomplete ค่อย ๆ “ฉลาดขึ้น” จนถ้าคิดว่าตำแหน่งเคอร์เซอร์ปัจจุบันไม่เหมาะกับชื่อไฟล์ ก็จะไม่ยอมเติมไฟล์/ไดเรกทอรีให้
    • ฉันว่า fallback กลับไปเป็น การเติมชื่อไฟล์ เสมอน่าจะดีกว่า
    • เพราะแบบนี้เลยเคยคิดจะปิดสคริปต์ completion ทั้งหมดไปเลย
    • ใน bash มีฟังก์ชันอย่าง complete-filename ที่ bind กับ M-/ ซึ่งจะเติมเฉพาะชื่อไฟล์โดยไม่สนบริบท
    • บางคำสั่งการเติมไฟล์พังสนิท เลยต้องอ้อมด้วยการเติมให้เสร็จด้วย ls ก่อนแล้วค่อยเปลี่ยนคำสั่ง
    • ถ้าไฟล์มีอยู่แต่รันไม่ได้ การขึ้นข้อความอย่าง “file foo.exe exists but it isn't executable” จะทำให้สับสนน้อยกว่า
    • พอลองรัน complete -r แล้วทำงานได้ตามต้องการ ก็ดูเหมือนว่าปัญหาอยู่ที่สคริปต์ bash-completion
    • มันให้ความรู้สึกอึดอัดคล้าย frontend validation ในเว็บฟอร์มที่พอพิมพ์อีเมลได้แค่ตัวแรกก็เด้ง “Invalid email!” ทันที
  • นี่เป็นบทความที่ฉันเขียนเอง หวังว่าคนอื่นจะอ่านแล้วสนใจเหมือนกัน
    • แทนที่จะโหลดสคริปต์ completion ของ zsh ทุกครั้ง ถ้าติดตั้งไว้ใน $fpath มันจะถูกแคชไว้ ทำให้ ความเร็วตอนเริ่มต้น ดีขึ้น
    • ตอนแจกจ่ายผ่าน Homebrew ก็สามารถติดตั้ง completions ให้อัตโนมัติได้
    • completion ของ zsh มีขนาดใหญ่จนเรียนรู้ยาก แต่ที่บริษัทก็ค่อย ๆ ขัดเกลามันไปเรื่อย ๆ โดยเพิ่ม option แยกตาม playbook, การเติม tag อัตโนมัติ ฯลฯ ให้กับ wrapper script ของ ansible
  • ช่วงนี้เริ่มใช้ไลบรารี usage ในการพัฒนา CLI → มันรวมกับ clap ได้ และสร้าง completions, argparse, man page ได้
    • ยังลังเลอยู่ว่าคุ้มไหมที่จะเอาไปแทนบล็อก argparse ในสคริปต์ fish แต่ก็ดีกว่า optparse มาก
    • ฉันเองก็กำลังทำเครื่องมือกำหนดสเปก CLI คล้าย ๆ กันอยู่ และเพิ่งเพิ่มการรองรับ fish เข้าไป
  • ขอแนะนำ fx.wtf ที่รองรับการเติมฟิลด์ JSON อัตโนมัติบน bash/zsh
    • มันเบากว่า ijq แต่ก็อาจจะมีประโยชน์ตามสถานการณ์
  • ทูโทเรียลการสร้าง completer ด้วยฟังก์ชัน built-in ของ zsh → zsh-completions-howto
  • สงสัยว่ามี standard flag ที่ช่วยให้โปรแกรมไม่ต้องเขียน bash script เองหรือเปล่า
    • บน zsh ใช้ _gnu_generic เพื่อทำ completion แบบง่ายจาก --help ได้
    • มีตัวอย่างอย่าง clap_complete ของ Rust, ออปชัน --generate ของ ripgrep, หรือการสร้าง completion ตอนรันไทม์ของ PHP Symfony
    • ถ้ามีมาตรฐาน --completion ที่ parser library ทั่วไปช่วยทำให้ได้อัตโนมัติก็คงดี
  • บน ksh สามารถทำ completion แบบง่ายได้ด้วยการประกาศ array อย่างเดียว
    • เช่น ใส่ชื่อ signal ไว้ใน array complete_kill_1 ก็จะเติมอาร์กิวเมนต์ตัวแรกของคำสั่ง kill ได้
    • สงสัยว่านี่เป็นไวยากรณ์ของ ksh93 หรือเป็นสิ่งที่ backport มายัง oksh
  • ตัวอย่าง snippet completion ของ zsh แบบง่ายที่ฉันทำ: ในฟังก์ชัน set-java-home ใช้ _describe เพื่อเติมรายการ ~/apps/java/* เป็นเวอร์ชัน
    • โครงสร้างเรียบง่ายมาก แทบจะเป็น one-liner
  • อาจเป็นเพราะหน้าที่หลักของ LLM คือการเติมข้อความอัตโนมัติ GitHub Copilot เลยช่วยทำ completion ของ bash และ zsh ในเทอร์มินัลของ vscode ได้ค่อนข้างดี