SSD Nodes Learn 🎉 VPS เริ่ม $5.50/เดือน
คู่มือ Matt Connorโดย Matt Connor

วิธีใช้งาน Bash Globbing และ Wildcards อย่างถูกต้อง

เรียนรู้การทำงานของ Bash Globbing และการขยายชื่อไฟล์ก่อนรันคำสั่ง พร้อมวิธีตั้งค่า nullglob, dotglob, globstar และการใช้ extglob เพื่อจัดการไฟล์อย่างแม่นยำใน Bash shell

สิ่งที่ Bash globbing ทำก่อนที่คำสั่งของคุณจะเริ่มทำงาน

Bash globbing คือกระบวนการที่ shell เปลี่ยนรูปแบบ (pattern) เช่น *.log ให้กลายเป็นรายการชื่อไฟล์จริงที่เรียงลำดับไว้แล้ว ก่อนที่คำสั่งจะเริ่มทำงาน rm *.log จะไม่ส่ง rm ที่เป็นรูปแบบ (pattern) ไปให้โปรแกรมโดยตรง Bash จะอ่านไดเรกทอรี คัดเลือกชื่อไฟล์ที่ตรงกับรูปแบบ นำมาเรียงลำดับ จากนั้นจึงรัน rm โดยใช้ชื่อไฟล์แต่ละชื่อเป็นอาร์กิวเมนต์แยกกัน โปรแกรม rm จะไม่ทราบเลยว่ามีการใช้รูปแบบ (pattern) เข้ามาเกี่ยวข้องในขั้นตอนก่อนหน้า

เกือบทุกเรื่องที่น่าประหลาดใจเกี่ยวกับ globbing ล้วนมีสาเหตุมาจากข้อเท็จจริงเพียงข้อเดียวนี้ การจับคู่ไฟล์จะถูกดำเนินการโดย shell เทียบกับไฟล์ที่มีอยู่จริงในขณะนั้น และคำสั่งจะมองเห็นเพียงรายการไฟล์ที่เสร็จสมบูรณ์แล้วเท่านั้น ชื่อเรียกอย่างเป็นทางการของกระบวนการนี้คือ pathname expansion ส่วนคำว่า globbing เป็นคำที่ใช้เรียกกันทั่วไป ทั้งสองคำนี้มีความหมายเหมือนกัน

สร้างโครงสร้างต้นไม้ขนาดเล็กเพื่อฝึกฝน

ตัวอย่างทั้งหมดด้านล่างนี้ทำงานกับไฟล์ชุดเดียวกัน ให้สร้างไฟล์เหล่านี้ในไดเรกทอรีชั่วคราว เพื่อไม่ให้กระทบกับไฟล์สำคัญของคุณ และสามารถลบออกได้ด้วยคำสั่งเดียว

lab=$(mktemp -d)
cd "$lab"
mkdir -p logs archive/2025 archive/2026
touch app.log app.log.1 app.log.2 app.log.10 error.log debug.LOG
touch notes.txt notes.md README 'weekly report.log'
touch .env .apprc
touch logs/nginx.log logs/nginx.log.1
touch archive/2025/dec.log archive/2026/jan.log

mktemp -d จะสร้างไดเรกทอรีใหม่และส่งคืนพาธของไดเรกทอรีนั้น ส่วน $(...) จะเก็บพาธดังกล่าวไว้ในตัวแปร lab หากไวยากรณ์นี้เป็นเรื่องใหม่สำหรับคุณ วิธีการที่ command substitution เก็บผลลัพธ์ของคำสั่ง จะอธิบายรายละเอียดทั้งหมดไว้

ตรวจสอบสิ่งที่คุณสร้างขึ้นและตรวจสอบเชลล์ของคุณ เนื่องจากมีหมายเหตุหนึ่งข้อที่ขึ้นอยู่กับเวอร์ชัน:

ls -a
bash --version

เนื้อหาทั้งหมดนี้ทำงานได้บน bash 4.0 ขึ้นไป ส่วนหมายเหตุ globskipdots ในช่วงท้ายจะใช้กับ bash 5.2 ขึ้นไป ซึ่งเป็นเวอร์ชันที่มาพร้อมกับ Ubuntu และ Debian รุ่นปัจจุบัน ณ เดือนสิงหาคม 2026

คำสั่งตัวอย่างที่ใช้ตลอดทั้งบทความคือ printf '%s\n' PATTERN ซึ่งจะแสดงผลลัพธ์ที่ตรงกันหนึ่งรายการต่อหนึ่งบรรทัดและไม่มีการเปลี่ยนแปลงใดๆ บนดิสก์ จึงปลอดภัยที่จะใช้ทดสอบกับรูปแบบใดก็ตามที่คุณยังไม่แน่ใจ

ตัวแทนอักขระพิเศษ (wildcards) ทั้งสามแบบ: ดอกจัน, เครื่องหมายคำถาม และวงเล็บเหลี่ยม

  • * จับคู่กับอักขระจำนวนเท่าใดก็ได้ รวมถึงกรณีที่ไม่มีอักขระเลย
  • ? จับคู่กับอักขระเพียงหนึ่งตัวเท่านั้น
  • [...] จับคู่กับอักขระหนึ่งตัวที่เลือกมาจากชุดภายในวงเล็บเหลี่ยม
printf '%s\n' *.log
printf '%s\n' app.log.?
printf '%s\n' app.log.[0-9]
printf '%s\n' notes.*
printf '%s\n' *

ลองเปรียบเทียบผลลัพธ์บรรทัดที่สองกับรายการไฟล์ที่คุณสร้างขึ้น ? แทนอักขระเพียงหนึ่งตัวเท่านั้น ดังนั้นไฟล์ที่มีส่วนต่อท้ายสองหลักจึงไม่ตรงกับรูปแบบนี้ ให้ขยายรูปแบบเป็น app.log.[0-9]* แล้วรันคำสั่งอีกครั้งเพื่อดูความแตกต่าง

กฎสองข้อนี้ใช้กับตัวแทนอักขระทั้งสามแบบ ไม่มีตัวแทนอักขระใดที่สามารถจับคู่กับ / ได้ ดังนั้นรูปแบบการค้นหาจึงจำกัดอยู่เพียงภายในไดเรกทอรีระดับเดียวเท่านั้น และอักขระ . ที่อยู่หน้าสุดจะต้องระบุให้ตรงตัว ดังนั้นรูปแบบที่ขึ้นต้นด้วย * จะข้ามไฟล์ที่ซ่อนอยู่ทั้งหมด กฎข้อที่สองนี้เป็นเหตุผลว่าทำไม rm * จึงไม่แสดงไฟล์ที่ขึ้นต้นด้วยจุด (dotfiles) ของคุณ

ผลลัพธ์จากการทำ glob จะถูกเรียงลำดับตาม collation order ของ locale ปัจจุบัน ซึ่งเป็นการเรียงลำดับแบบตัวอักษรทีละตัว ดังนั้นส่วนต่อท้าย 10 จึงถูกเรียงไว้ก่อน 2 เพราะระบบไม่ได้อ่านค่าเหล่านั้นเป็นตัวเลข ให้ลองรัน printf '%s\n' app.log.* แล้วตามด้วย ls -v app.log.* เพื่อเปรียบเทียบทั้งสองคำสั่ง: ls -v จะเรียงลำดับตัวเลขที่แทรกอยู่ในชื่อไฟล์ตามค่าของตัวเลขนั้น และ sort -V จะทำหน้าที่เดียวกันในรูปแบบ pipeline

คลาสของอักขระและช่วง (Character classes and ranges)

ภายในวงเล็บก้ามปู คุณสามารถระบุรายการ ช่วง หรือการปฏิเสธได้

  • [ch] จับคู่อักขระหนึ่งตัวที่เป็นได้ทั้ง c หรือ h
  • [a-f] จับคู่อักขระหนึ่งตัวที่อยู่ในช่วง a ถึง f
  • [!0-9] จับคู่อักขระหนึ่งตัวที่ไม่ใช่ตัวเลข โดย [^0-9] มีความหมายเดียวกัน
printf '%s\n' [dn]*
printf '%s\n' *.[A-Z]*
printf '%s\n' [A-Z]*

ช่วง (Ranges) เป็นส่วนที่มักเกิดปัญหาเมื่อย้ายไปทำงานบนเครื่องที่ต่างกัน เนื่องจากช่วงจะถูกประมวลผลตามลำดับการจัดเรียง (collation order) ของ locale ปัจจุบันแทนที่จะเป็น ASCII ปกติ ดังนั้นในบางระบบ [a-z] อาจจับคู่กับตัวอักษรพิมพ์ใหญ่ด้วย Bash มีตัวเลือก globasciiranges ที่บังคับให้ใช้ลำดับแบบ ASCII ปกติ และในรุ่นใหม่ๆ จะเปิดใช้งานเป็นค่าเริ่มต้น ดังนั้นควรตรวจสอบการตั้งค่าของคุณด้วย shopt globasciiranges แทนที่จะคาดเดา สคริปต์ที่ต้องการผลลัพธ์การจับคู่ที่เหมือนกันทุกที่สามารถกำหนดลำดับเองได้โดยการตั้งค่า LC_ALL=C ไว้ในบรรทัดแยกต่างหากที่ส่วนบนของสคริปต์ ซึ่งจะทำให้ทุกช่วงกลับไปใช้ลำดับแบบ byte ปกติ

การกำหนดค่านั้นต้องเป็นคำสั่งแยกต่างหาก หากเขียนเป็นคำนำหน้า LC_ALL=C ls [a-z]* จะตั้งค่า locale ให้กับ ls เท่านั้น และเชลล์ได้ขยายรูปแบบ (pattern) ตาม locale ของตนเองไปก่อนที่ ls จะเริ่มทำงาน ซึ่งเป็นกฎเดียวกับที่ปรากฏในส่วนแรก

Brace expansion ไม่ใช่ globbing

Brace expansion ดูคล้ายกับ wildcard แต่มีพฤติกรรมที่แตกต่างกันโดยสิ้นเชิง Brace expansion จะทำงานก่อน globbing มาก และไม่เคยตรวจสอบข้อมูลบนดิสก์เลย

echo file{1,2,3}.txt
echo {01..10}
echo {a..e}
echo {0..20..5}

ไม่มีไฟล์เหล่านี้อยู่จริง แต่การขยายข้อความก็ยังคงเกิดขึ้น นี่คือความแตกต่างทั้งหมด: glob คือการตั้งคำถามกับระบบไฟล์ ในขณะที่ brace เป็นเพียงการสร้างข้อความขึ้นมา นี่คือเหตุผลว่าทำไม brace จึงเป็นเครื่องมือที่เหมาะสมสำหรับการสร้างสิ่งต่างๆ มากกว่าการค้นหา

mkdir -p site/{css,js,img}
cp app.log{,.bak}
touch report-{2025,2026}-{01,02}.csv

cp app.log{,.bak} จะขยายออกเป็นสองคำ คือ app.log และ app.log.bak เนื่องจากรายการที่ว่างเปล่าหน้าเครื่องหมายจุลภาคจะสร้างข้อความเดิมซ้ำออกมา ส่วน {01..10} จะคงการเติมเลขศูนย์ไว้ เนื่องจากเลขศูนย์นำหน้าในจุดสิ้นสุดด้านใดด้านหนึ่งจะทำให้ bash เติมเลขศูนย์ให้ตัวเลขที่สร้างขึ้นทุกตัวมีความกว้างเท่ากัน

ลำดับการขยายข้อความยังอธิบายถึงความล้มเหลวที่ผู้ใช้มักพบเจอได้ Bash จะขยาย brace ก่อน จากนั้นจึงเป็นตัวแปร และสุดท้ายคือการขยาย pathname

n=5
echo {1..$n}

Brace expansion ได้ทำงานเสร็จสิ้นไปแล้วในขณะที่ $n กลายเป็นตัวเลข ดังนั้นจึงไม่มีการสร้างช่วงตัวเลขขึ้นมา ให้ใช้ seq 1 "$n" หรือใช้ลูปสไตล์ C ที่เขียนในรูปแบบ for ((i=1; i<=n; i++)) แทน

จะเกิดอะไรขึ้นเมื่อรูปแบบไม่ตรงกับไฟล์ใดเลย

นี่คือโหมดความล้มเหลวรูปแบบแรกที่ทุกคนต้องเคยเจอ โดยค่าเริ่มต้น bash จะปล่อยรูปแบบที่ไม่ตรงกับไฟล์ใดเลยไว้ตามเดิม คำสั่งจะได้รับอาร์กิวเมนต์เป็นข้อความที่คุณพิมพ์ลงไปโดยตรง ดังนั้นคำสั่งจะได้รับอักขระ *, ., t, m และ p และพยายามจัดการกับอักขระเหล่านั้นในฐานะชื่อไฟล์

printf '%s\n' *.tmp

กรณีนี้ไม่มีอันตรายเมื่อใช้กับ printf แต่จะเป็นอันตรายกับคำสั่งที่ใช้สร้างไฟล์ การรัน touch *.tmp ในไดเรกทอรีที่ไม่มีไฟล์ .tmp จะเป็นการสร้างไฟล์ที่มีชื่อว่า *.tmp จริงๆ และการลบไฟล์ดังกล่าวในภายหลังจำเป็นต้องใช้เครื่องหมายคำพูดเพื่อป้องกันไม่ให้ shell ขยายผลรูปแบบนั้นอีกครั้ง: rm -- '*.tmp'

ตัวเลือกของ shell สองรายการสามารถเปลี่ยนกฎนี้ได้ ให้ลองใช้แต่ละตัวกับรูปแบบที่ไม่ตรงกับไฟล์ใดเลย แล้วจึงยกเลิกการตั้งค่า

shopt -s nullglob
printf '%s\n' *.tmp
shopt -u nullglob

shopt -s failglob
printf '%s\n' *.tmp
shopt -u failglob

nullglob จะทำให้รูปแบบที่ไม่ตรงกับไฟล์ใดเลยถูกขยายผลเป็นค่าว่างเปล่า ดังนั้นคำสั่งจะทำงานด้วยจำนวนอาร์กิวเมนต์ที่น้อยกว่าที่คุณเขียน นี่คือสิ่งที่คุณต้องการเมื่อใช้งานภายในลูป หากปิด nullglob ไว้ for f in *.tmp จะทำงานหนึ่งครั้งโดยมี f เก็บค่ารูปแบบตัวอักษรตรงๆ ซึ่งถือเป็นบั๊กในเกือบทุกสคริปต์ที่เขียนในลักษณะนี้ แต่หากเปิด nullglob ไว้ ตัวลูปจะไม่ทำงานเลย

nullglob มีความเสี่ยงในตัว เพราะคำสั่งที่ถูกตัดอาร์กิวเมนต์ออกจนหมดก็ยังคงทำงานอยู่ ls *.nope จะกลายเป็น ls เปล่าๆ ซึ่งจะแสดงรายการไฟล์ทั้งหมดในไดเรกทอรี grep -l needle *.nope จะกลายเป็น grep -l needle ซึ่งไม่มีไฟล์ให้อ่าน ทำให้มันรอรับข้อมูลจาก standard input และดูเหมือนว่าโปรแกรมค้าง ให้เปิด nullglob เฉพาะช่วงที่จำเป็นต้องใช้ภายในลูป และปิดหลังจากนั้น

failglob ใช้วิธีที่ต่างออกไป คือหากรูปแบบไม่ตรงกับไฟล์ใดเลยจะถือว่าเป็นข้อผิดพลาด bash จะรายงานข้อผิดพลาดนั้นและคำสั่งจะไม่ทำงานเลย นี่เป็นการตั้งค่าที่ดีสำหรับ interactive shell เพราะช่วยป้องกันไม่ให้รูปแบบที่พิมพ์ผิดถูกส่งไปยัง rm ในฐานะชื่อไฟล์จริงๆ

ไฟล์ที่ซ่อนอยู่: dotglob

shopt -s dotglob
printf '%s\n' *
shopt -u dotglob

ให้รันบล็อกคำสั่งดังกล่าวแล้วเปรียบเทียบรายการที่ได้กับ printf '%s\n' * แบบปกติจากก่อนหน้านี้ dotglob จะยกเลิกกฎการขึ้นต้นด้วยจุด ดังนั้น * จึงจับคู่กับชื่อไฟล์ที่ซ่อนอยู่ด้วย รายการ . และ .. จะยังคงถูกยกเว้นภายใต้ dotglob เสมอ

หากไม่มี dotglob การใช้ .* เป็นวิธีปกติในการเข้าถึงไฟล์ที่ซ่อนอยู่ ซึ่งในอดีตเคยเป็นวิธีที่อันตราย บน bash เวอร์ชันก่อน 5.2 นั้น .* จะจับคู่กับ . และ .. ด้วย ส่งผลให้คำสั่งแบบ recursive อย่าง chmod -R 755 .* เข้าไปทำงานในไดเรกทอรีแม่โดยตรง Bash 5.2 ได้เพิ่มตัวเลือก globskipdots ซึ่งเปิดใช้งานเป็นค่าเริ่มต้น โดยจะกันไม่ให้ . และ .. รวมอยู่ในการขยายผลลัพธ์ (expansion) ทุกครั้ง ให้รัน shopt globskipdots ก่อนที่คุณจะใช้งานฟีเจอร์นี้ เนื่องจากเครื่องรุ่นเก่าจะไม่มีตัวเลือกดังกล่าว

globstar: การจับคู่ไฟล์แบบไล่ระดับลงไปในโครงสร้างไดเรกทอรี

shopt -s globstar
printf '%s\n' **/*.log
printf '%s\n' **/
shopt -u globstar

เมื่อตั้งค่า globstar แล้ว ** ที่เป็นส่วนประกอบของพาธแบบสมบูรณ์จะจับคู่กับไฟล์และไดเรกทอรีในทุกระดับความลึก รวมถึงระดับศูนย์ ดังนั้น **/*.log จึงครอบคลุมถึงไดเรกทอรีปัจจุบัน รวมถึง logs/ และทุกสิ่งที่อยู่ภายใต้ archive/ ส่วน **/ ที่มีเครื่องหมายทับปิดท้ายจะจับคู่เฉพาะไดเรกทอรีเท่านั้น ซึ่งเป็นวิธีที่รวดเร็วในการตรวจสอบโครงสร้างของไดเรกทอรี

หากไม่ได้ตั้งค่า globstar ไว้ ** จะทำงานเหมือนกับ * ทั่วไปและจะจำกัดอยู่เพียงไดเรกทอรีเดียว ให้ลองรันรูปแบบเดิมโดยปิดตัวเลือกนี้แล้วเปรียบเทียบผลลัพธ์: รูปแบบการค้นหาแบบเรียกซ้ำ (recursive pattern) ที่คัดลอกมาจากแหล่งอื่นแล้วให้ผลลัพธ์น้อยกว่าที่คาดไว้ มักเกิดจากสาเหตุนี้ globstar ถูกปิดไว้เป็นค่าเริ่มต้นใน bash ทุกเวอร์ชัน

ข้อจำกัดที่ควรทราบคือ ** จะไม่ติดตาม symbolic link ไปยังไดเรกทอรีอื่น หากโครงสร้างไดเรกทอรีมีการเชื่อมต่อด้วย symlinks จำเป็นต้องใช้ find -L แทน

nocaseglob สำหรับชื่อไฟล์ที่ตัวพิมพ์เล็กและใหญ่ไม่ตรงกัน

shopt -s nocaseglob
printf '%s\n' *.log
shopt -u nocaseglob

nocaseglob ช่วยให้การจับคู่ชื่อไฟล์ไม่คำนึงถึงตัวพิมพ์เล็กหรือใหญ่ ดังนั้นส่วนต่อท้าย .LOG ที่เป็นตัวพิมพ์ใหญ่จะจับคู่กับรูปแบบ .log ที่เป็นตัวพิมพ์เล็กได้ ให้ลองเปรียบเทียบรายการนี้กับรายการจากส่วนแรก จากนั้นให้ปิดตัวเลือกนี้ทันที เพราะหากเปิดทิ้งไว้ มันจะเปลี่ยนวิธีการจับคู่สำหรับทุกคำสั่งที่ตามมาใน shell นั้น ซึ่งเป็นสิ่งที่สร้างความสับสนและตรวจสอบหาสาเหตุได้ยากในภายหลัง

nocaseglob มีผลเฉพาะกับการขยายชื่อไฟล์ (filename expansion) เท่านั้น ส่วนตัวเลือกที่เกี่ยวข้องสำหรับการจับคู่รูปแบบภายในคำสั่ง case หรือการทดสอบด้วยวงเล็บคู่คือ nocasematch ซึ่งทั้งสองตัวเลือกนี้ต้องตั้งค่าแยกกัน

extglob: รูปแบบที่ระบุว่า "ไม่ใช่สิ่งนี้"

Extended patterns ถูกปิดใช้งานโดยค่าเริ่มต้น ให้เปิดใช้งานโดยเขียนไว้ในบรรทัดแยกต่างหาก:

shopt -s extglob

มีรูปแบบทั้งหมด 5 แบบ โดยแต่ละแบบจะรับรายการของ pattern ที่คั่นด้วย |:

  • ?(list) จับคู่การปรากฏศูนย์หรือหนึ่งครั้ง
  • *(list) จับคู่การปรากฏศูนย์หรือมากกว่านั้น
  • +(list) จับคู่การปรากฏหนึ่งหรือมากกว่านั้น
  • @(list) จับคู่ทางเลือกใดทางเลือกหนึ่งเพียงอย่างเดียว
  • !(list) จับคู่ทุกสิ่งที่ไม่ได้ตรงกับทางเลือกใดเลย

Extended pattern จะต้องถูกอ่านโดย shell ที่เปิดตัวเลือกนี้ไว้แล้ว ดังนั้นชุดคำสั่งด้านล่างจึงต้องอยู่ในสคริปต์ขนาดเล็กที่เริ่มทำงานด้วย bash -O extglob โดย flag -O จะตั้งค่าตัวเลือก shopt ก่อนที่ shell ใหม่จะอ่านบรรทัดแรกของไฟล์ ซึ่งทำให้ทุก pattern ในไฟล์ถูกแยกวิเคราะห์ (parse) อย่างถูกต้อง ย่อหน้าสุดท้ายของส่วนนี้คือเหตุผลว่าทำไมลำดับการทำงานจึงมีความสำคัญ

cat > patterns.sh <<'EOF'
printf '%s\n' @(app|error).log
printf '%s\n' app.log.+([0-9])
printf '%s\n' *.@(md|txt)
printf '%s\n' !(*.log)
printf '%s\n' !(*.log|*.md|README)
EOF
bash -O extglob patterns.sh

+([0-9]) หมายถึงตัวเลขหนึ่งตัวหรือมากกว่า ดังนั้นจึงครอบคลุม suffix ที่เป็นตัวเลขที่มีความยาวเท่าใดก็ได้ นี่คือวิธีแก้ไขข้อจำกัด ? จากก่อนหน้านี้ และควรลองรันทั้งสอง pattern ต่อเนื่องกันเพื่อดูผลลัพธ์

รูปแบบการปฏิเสธ (negation) คือคำตอบสำหรับโจทย์ "ทุกอย่างยกเว้น..." และเป็นเหตุผลหลักที่ต้องเปิดใช้งาน extglob มีสองสิ่งที่มักทำให้ผู้ใช้ประหลาดใจ

รูปแบบนี้จับคู่ทั้งไดเรกทอรีและไฟล์ ดังนั้น ls !(*.log) จะแสดงรายการเนื้อหาภายในไดเรกทอรีใดก็ตามที่อยู่ในผลลัพธ์ แทนที่จะแสดงเพียงชื่อไดเรกทอรี ให้ใช้ ls -d หรือการดูตัวอย่างด้วย printf เพื่อดูชื่อเหล่านั้นโดยตรง

นอกจากนี้ยังปฏิบัติตามกฎของ dot เหมือน glob อื่นๆ ดังนั้น !(*.log) จะครอบคลุมชื่อที่มองเห็นได้ทั้งหมดที่ไม่ได้ลงท้ายด้วย .log ส่วนชื่อที่ซ่อนอยู่จำเป็นต้องตั้งค่า dotglob เพิ่มเติมด้วย

เครื่องหมาย ! ใน !(...) คือการปฏิเสธแบบ glob ซึ่งไม่มีความเกี่ยวข้องกับ การขยายประวัติและอักขระตกใจ (bang character) ซึ่งเป็นฟีเจอร์แยกต่างหากที่ทำงานในขั้นตอนที่ต่างกัน

อีกหนึ่งกับดักที่มักเกิดขึ้นในขั้นตอนการ parse คือ Bash จะ parse คำสั่งทั้งหมดให้เสร็จก่อนที่จะรันส่วนใดส่วนหนึ่ง ดังนั้นการเปิดใช้งาน extglob และใช้ extended pattern ในบรรทัดเดียวกันจะล้มเหลว เพราะ pattern จะถูก parse ในขณะที่ตัวเลือกยังคงปิดอยู่ ให้เก็บ shopt -s extglob ไว้ในบรรทัดของตัวเอง ก่อนที่จะถึงส่วนของ pattern และวางไว้ใกล้ส่วนบนของสคริปต์ ส่วนเนื้อหาของฟังก์ชันจะถูก parse เมื่อมีการนิยามฟังก์ชัน ดังนั้นตัวเลือกจะต้องเปิดอยู่ก่อนการนิยาม ไม่ใช่ก่อนการเรียกใช้งาน การเริ่ม shell ด้วย bash -O extglob ตามวิธีในชุดคำสั่งด้านบน จะช่วยจัดการลำดับการทำงานให้เรียบร้อยก่อนที่การ parse ใดๆ จะเริ่มต้นขึ้น

Shell เป็นผู้ขยาย pattern ไม่ใช่คำสั่ง

นี่คือรูปแบบความล้มเหลวที่สอง ซึ่งอธิบายถึงสาเหตุของบั๊กจำนวนมาก

grep -l needle *.log
find . -name '*.log'
find . -name *.log

บรรทัดแรกปล่อยให้ bash ทำการจับคู่ และ grep จะได้รับรายการชื่อไฟล์ บรรทัดที่สองใส่เครื่องหมายคำพูดครอบ pattern ไว้ ดังนั้น find จึงได้รับอักขระ 5 ตัวคือ *.log และทำการจับคู่ด้วยตัวเองในทุกระดับชั้นใต้จุดเริ่มต้น บรรทัดที่สามคือบั๊ก: bash จะขยาย pattern ก่อนโดยเทียบกับไดเรกทอรีปัจจุบันเท่านั้น ทำให้ find ได้รับคำสั่งให้ค้นหาชื่อเฉพาะเพียงชื่อเดียว ในไดเรกทอรีที่มีผลลัพธ์ตรงกันเพียงชื่อเดียว มันจะค้นหาสิ่งที่ผิดพลาดโดยไม่มีการแจ้งเตือน แต่หากมีผลลัพธ์ตั้งแต่สองรายการขึ้นไป find จะรายงานข้อผิดพลาดด้านการใช้งาน เนื่องจากชื่อไฟล์ส่วนเกินถูกส่งเข้ามาในตำแหน่งที่มันคาดหวังจะเป็นนิพจน์ (expression)

กฎมีสั้นๆ คือ หาก pattern นั้นมีไว้สำหรับคำสั่ง ให้ใส่เครื่องหมายคำพูดครอบไว้ หากมีไว้สำหรับ shell ให้ปล่อยไว้โดยไม่ต้องใส่เครื่องหมายคำพูด การแบ่งแยกนี้ใช้กับ pattern ของ --exclude ใน tar และ rsync รวมถึงตัวกรอง --include ใน grep ด้วยเช่นกัน

Glob ไม่ใช่ regular expression แม้ว่าจะใช้อักขระร่วมกัน ใน glob นั้น * หมายถึงชุดอักขระใดๆ ก็ได้ แต่ใน regular expression นั้น * หมายถึงอักขระตัวก่อนหน้าซ้ำศูนย์ครั้งหรือมากกว่า ดังนั้น grep '*.log' จึงมีความหมายต่างจากที่เห็นโดยสิ้นเชิง

ตัวแปร (variable) ก็มีลำดับการทำงานเช่นเดียวกัน การทำ globbing จะเกิดขึ้นหลังจากขยายค่าตัวแปร ดังนั้นตัวแปรที่เก็บ pattern ไว้โดยไม่ใส่เครื่องหมายคำพูดจะถูกขยายโดยเทียบกับไดเรกทอรี

pat='*.log'
printf '%s\n' $pat
printf '%s\n' "$pat"

เมื่อไม่ใส่เครื่องหมายคำพูด bash จะทำ globbing ผลลัพธ์จากตัวแปรนั้น แต่เมื่อใส่เครื่องหมายคำพูด pattern จะคงความเป็นตัวอักษรตรงตัวไว้ มีผลลัพธ์ที่เกี่ยวข้องที่คุณควรยึดถือคือ คำที่เกิดจากการทำ globbing จะไม่ถูกแบ่งด้วยช่องว่างอีก ดังนั้น for f in *.log จึงจัดการชื่อไฟล์ที่มีช่องว่างเป็นชื่อเดียว ในขณะที่ for f in $(ls *.log) จะแบ่งชื่อนั้นออกเป็นสองคำ ให้วนลูปผ่าน glob โดยตรงแทน

ตรวจสอบรูปแบบคำสั่งที่อาจก่อให้เกิดความเสียหายก่อนใช้งานจริง

อย่าให้ rm เป็นคำสั่งแรกที่คุณใช้ทดสอบรูปแบบคำสั่งใหม่ ให้รันรูปแบบนั้นกับสิ่งที่ไม่มีอันตราย ตรวจสอบรายการที่แสดงผล แล้วจึงเปลี่ยนเฉพาะคำสั่งที่อยู่ส่วนหน้าของบรรทัด

target='!(*.log)'
printf '%s\n' $target
ls -ld -- $target
rm -- $target

การเขียนรูปแบบคำสั่งลงใน target เพียงครั้งเดียวจะช่วยป้องกันไม่ให้บรรทัดทั้งสามคลาดเคลื่อนจากกัน และ $target ถูกปล่อยไว้โดยไม่ใส่เครื่องหมายคำพูดโดยเจตนา เพื่อให้ bash สามารถขยายค่า (expand) ได้ printf '%s\n' จะพิมพ์ชื่อไฟล์ออกมาบรรทัดละหนึ่งชื่อโดยไม่มีการแก้ไขใดๆ ls -ld จะแสดงรายการแต่ละรายการโดยตรงแทนการแสดงเนื้อหาภายในไดเรกทอรี และคอลัมน์โหมดจะระบุว่ารายการใดเป็นไดเรกทอรี ซึ่งเป็นสิ่งที่ต้องตรวจสอบก่อนการลบแบบ recursive การอ่านบิตสิทธิ์ drwxr-xr-x ครอบคลุมรายละเอียดของคอลัมน์นี้

ให้เรียกคำสั่งก่อนหน้ากลับมาด้วยปุ่มลูกศรขึ้นและแก้ไขเฉพาะส่วนที่เป็นคำสั่ง เพื่อให้รูปแบบคำสั่งยังคงเหมือนเดิม การพิมพ์รูปแบบคำสั่งใหม่จากความจำมักเป็นจุดที่ทำให้เกิดข้อผิดพลาด

ใส่ -- ไว้หน้ารูปแบบคำสั่ง มันทำหน้าที่ระบุจุดสิ้นสุดของออปชัน ดังนั้นชื่อไฟล์ที่ขึ้นต้นด้วย - จะถูกปฏิบัติเป็นชื่อไฟล์แทนที่จะเป็นแฟล็ก

หากคุณต้องการปิดการทำงานของ globbing ในช่วงหนึ่งของสคริปต์ set -f จะใช้สำหรับปิด และ set +f จะใช้สำหรับเปิดใช้งานอีกครั้ง

นิสัยนี้มีความสำคัญเนื่องจาก rm ไม่มีคำสั่งย้อนกลับ (undo) การกู้คืนไฟล์ที่ถูกลบด้วย rm -rf นั้นทำได้ช้าและมักไม่สมบูรณ์ ดังนั้นการตรวจสอบก่อนใช้งานจริงจึงมีต้นทุนต่ำกว่าผลลัพธ์ที่ตามมามาก

ทำความสะอาดไดเรกทอรีที่ใช้ฝึกฝนเมื่อเสร็จสิ้น พิมพ์ path ออกมาและอ่านให้แน่ใจก่อน เพราะ rm -rf ที่ชี้ไปยังตัวแปรผิดตัวคืออุบัติเหตุที่หัวข้อนี้ต้องการป้องกัน

cd ~
echo "$lab"
rm -rf -- "$lab"

FAQ

ทำไม find . -name *.log ถึงค้นหาไฟล์ผิดพลาด?

เพราะ bash จะทำการขยาย *.log ก่อนที่ find จะเริ่มทำงาน เชลล์จะจับคู่รูปแบบกับไดเรกทอรีปัจจุบันแล้วส่งชื่อไฟล์ที่ได้ไปยัง find ซึ่งจะค้นหาชื่อนั้นในทุกระดับความลึก หากมีไฟล์ตั้งแต่ 2 ไฟล์ขึ้นไปที่ตรงกับรูปแบบ find จะได้รับอาร์กิวเมนต์เกินมาในจุดที่คาดหวังนิพจน์และรายงานข้อผิดพลาดในการใช้งาน ให้ใส่เครื่องหมายคำพูดครอบรูปแบบเป็น find . -name '*.log' เพื่อให้ find เป็นผู้จับคู่ด้วยตนเอง โดยใช้กฎและการเรียกซ้ำของตัวมันเอง

การที่รูปแบบของฉันปรากฏเป็นข้อความปกติหมายความว่าอย่างไร?

หมายความว่าไม่มีไฟล์ใดตรงกับรูปแบบ โดยปกติแล้ว bash จะปล่อยรูปแบบที่ไม่พบไว้ตามเดิมและส่งอักขระดิบไปยังคำสั่ง ซึ่งคำสั่งจะมองว่านั่นคือชื่อไฟล์ ให้ตั้งค่า shopt -s failglob เพื่อเปลี่ยนรูปแบบที่ไม่พบให้เป็นข้อผิดพลาดที่จะหยุดการทำงานของคำสั่ง หรือใช้ shopt -s nullglob เพื่อให้มันขยายเป็นค่าว่าง ภายใต้ nullglob ให้ตรวจสอบว่าคำสั่งยังคงทำงานได้ถูกต้องเมื่อไม่มีอาร์กิวเมนต์ เพราะ ls *.nope จะกลายเป็น ls เปล่าๆ และแสดงรายการไฟล์ทั้งหมดในไดเรกทอรี

ฉันจะจับคู่ไฟล์ทุกไฟล์ยกเว้นรูปแบบเดียวใน bash ได้อย่างไร?

เปิดใช้งานรูปแบบขยายด้วย shopt -s extglob ในบรรทัดแยกต่างหาก จากนั้นใช้รูปแบบการปฏิเสธ !(*.log) จะจับคู่ชื่อที่ไม่ได้ลงท้ายด้วย .log และ !(*.log|*.md) จะยกเว้นทั้งสองอย่าง ไฟล์ที่ซ่อนอยู่จะไม่รวมอยู่ในผลลัพธ์เว้นแต่จะตั้งค่า dotglob ไว้ด้วย และไดเรกทอรีจะถูกรวมอยู่ด้วย ดังนั้นควรตรวจสอบรูปแบบด้วย printf '%s\n' !(*.log) ก่อนที่จะส่งต่อไปยัง rm

ทำไม * ถึงข้ามไฟล์ที่ซ่อนอยู่?

จุดนำหน้าต้องถูกจับคู่แบบตรงตัว ดังนั้น * จึงไม่จับคู่ชื่อที่ขึ้นต้นด้วยจุด ให้ใช้ .* เพื่อเข้าถึงชื่อที่ซ่อนอยู่ หรือตั้งค่า shopt -s dotglob เพื่อรวมไฟล์เหล่านั้นเข้ากับรูปแบบปกติทุกรูปแบบ รายการ . และ .. จะถูกยกเว้นเสมอภายใต้ dotglob และ bash เวอร์ชัน 5.2 ขึ้นไปจะกันไฟล์เหล่านี้ออกจาก .* ด้วยเช่นกันผ่านตัวเลือก globskipdots ซึ่งถูกเปิดใช้งานเป็นค่าเริ่มต้น

ฉันจำเป็นต้องใช้ globstar เพื่อให้ ** ทำงานหรือไม่?

จำเป็น หากไม่มี shopt -s globstar bash จะถือว่า ** เป็น * ธรรมดา ซึ่งจะจับคู่ภายในไดเรกทอรีเดียวแล้วหยุดแค่นั้น เมื่อเปิด globstar แล้ว ** ที่เป็นส่วนประกอบของพาธทั้งหมดจะจับคู่ได้ในทุกระดับความลึก ดังนั้น **/*.log จะเข้าถึงไดเรกทอรีปัจจุบันและทุกไดเรกทอรีย่อยด้านล่าง และ **/ จะจับคู่เฉพาะไดเรกทอรีเท่านั้น โปรดทราบว่า ** ไม่ติดตาม symbolic link ไปยังไดเรกทอรี ดังนั้นให้ใช้ find -L สำหรับโครงสร้างต้นไม้ที่สร้างจาก symlink

#bash#globbing#shell#linux#scripting