วิธีใช้งาน 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.logmktemp -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}.csvcp 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 failglobnullglob จะทำให้รูปแบบที่ไม่ตรงกับไฟล์ใดเลยถูกขยายผลเป็นค่าว่างเปล่า ดังนั้นคำสั่งจะทำงานด้วยจำนวนอาร์กิวเมนต์ที่น้อยกว่าที่คุณเขียน นี่คือสิ่งที่คุณต้องการเมื่อใช้งานภายในลูป หากปิด 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 nocaseglobnocaseglob ช่วยให้การจับคู่ชื่อไฟล์ไม่คำนึงถึงตัวพิมพ์เล็กหรือใหญ่ ดังนั้นส่วนต่อท้าย .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