Bash เป็นภาษาโปรแกรมมิ่งที่ทรงพลัง ออกแบบมาอย่างสมบูรณ์แบบสำหรับการใช้งานบนบรรทัดคำสั่ง (Command Line) และในเชลล์สคริปต์ (Shell Scripts) บทความชุดนี้ที่ประกอบด้วย 3 ส่วน (ซึ่งอิงจากหลักสูตรการเรียนรู้ Linux แบบเรียนรู้ด้วยตนเอง 3 เล่มของผม) จะสำรวจการใช้ Bash ในฐานะภาษาโปรแกรมบนอินเทอร์เฟซบรรทัดคำสั่ง (CLI)
บทความแรกได้สำรวจการเขียนโปรแกรมบรรทัดคำสั่งพื้นฐานด้วย Bash รวมถึงการใช้ตัวแปรและตัวดำเนินการควบคุม (Control Operators) บทความนี้ ซึ่งเป็นบทความที่สอง จะเจาะลึกถึงประเภทต่างๆ ของตัวดำเนินการตรรกะ (Logical Operators) ได้แก่ ไฟล์ (File), สตริง (String), ตัวเลข (Numeric) และตัวดำเนินการทั่วไป (Miscellaneous) ที่ให้การควบคุมขั้นตอนการทำงาน (Execution-Flow Control Logic) รวมถึงประเภทต่างๆ ของการขยายเชลล์ (Shell Expansions) ใน Bash บทความที่สามและเป็นบทความสุดท้ายในชุดนี้จะสำรวจลูป (Loops) for, while, และ until ที่ช่วยให้สามารถทำงานซ้ำได้
ตัวดำเนินการตรรกะเป็นพื้นฐานในการตัดสินใจในโปรแกรมและการรันชุดคำสั่งที่แตกต่างกันตามการตัดสินใจเหล่านั้น ซึ่งมักเรียกว่า "การควบคุมโฟลว์" (Flow Control)
ตัวดำเนินการตรรกะ (Logical Operators)
Bash มีชุดตัวดำเนินการตรรกะจำนวนมากที่สามารถนำมาใช้ในนิพจน์เงื่อนไข (Conditional Expressions) ได้ รูปแบบพื้นฐานที่สุดของโครงสร้างควบคุม if คือการทดสอบเงื่อนไข แล้วจึงรันรายการคำสั่งโปรแกรมหากเงื่อนไขเป็นจริง (True) มีตัวดำเนินการ 3 ประเภทหลัก ได้แก่ ตัวดำเนินการไฟล์, ตัวเลข และตัวดำเนินการที่ไม่ใช่ตัวเลข (Non-numeric) แต่ละตัวดำเนินการจะคืนค่า True (0) หากเงื่อนไขเป็นจริง และ False (1) หากเงื่อนไขเป็นเท็จ
ไวยากรณ์การทำงานของตัวดำเนินการเปรียบเทียบเหล่านี้ คือ อาร์กิวเมนต์ 1 หรือ 2 ตัว พร้อมกับตัวดำเนินการ วางไว้ภายในวงเล็บเหลี่ยม [[ และ ]] ตามด้วยรายการคำสั่งที่จะรันหากเงื่อนไขเป็นจริง และรายการคำสั่งทางเลือก (Optional) หากเงื่อนไขเป็นเท็จ:
if [ arg1 operator arg2 ] ; then list
or
if [ arg1 operator arg2 ] ; then list ; else list ; fi
ช่องว่าง (Spaces) ในนิพจน์เปรียบเทียบเป็นสิ่งจำเป็น ตามที่แสดงไว้ข้างต้น วงเล็บเหลี่ยมเดี่ยว [ และ ] เป็นสัญลักษณ์ดั้งเดิมของ Bash ที่เทียบเท่ากับคำสั่ง test:
if test arg1 operator arg2 ; then list
นอกจากนี้ยังมีไวยากรณ์รุ่นใหม่ที่มีข้อดีบางประการและบางซิสแอดมินชอบใช้ รูปแบบนี้มีความเข้ากันได้ (Compatible) น้อยกว่ากับรุ่นต่างๆ ของ Bash และเชลล์อื่นๆ เช่น ksh (Korn Shell) รูปแบบดังกล่าวมีลักษณะดังนี้:
if [[ arg1 operator arg2 ]] ; then list
ตัวดำเนินการไฟล์ (File Operators)
ตัวดำเนินการไฟล์เป็นชุดตัวดำเนินการตรรกะที่ทรงพลังภายใน Bash ตารางที่ 1 (Figure 1) แสดงตัวดำเนินการมากกว่า 20 ตัวที่ Bash สามารถทำได้กับไฟล์ ผมใช้ตัวเหล่านี้บ่อยมากในสคริปต์ของผม
| ตัวดำเนินการ (Operator) | คำอธิบาย (Description) |
|---|---|
-a filename | True หากไฟล์มีอยู่ ไม่ว่าจะว่างเปล่าหรือมีข้อมูล กรณีที่มีอยู่จะคืนค่า True |
-b filename | True หากไฟล์มีอยู่และเป็นบล็อกสเปเชียลไฟล์ (Block Special File) เช่น ฮาร์ดดิสก์ /dev/sda หรือ /dev/sda1 |
-c filename | True หากไฟล์มีอยู่และเป็นแคракเทอร์สเปเชียลไฟล์ (Character Special File) เช่น อุปกรณ์ TTY เช่น /dev/TTY1 |
-d filename | True หากไฟล์มีอยู่และเป็นไดเรกทอรี (Directory) |
-e filename | True หากไฟล์มีอยู่ เหมือนกับ -a ด้านบน |
-f filename | True หากไฟล์มีอยู่และเป็นไฟล์ปกติ (Regular File) ไม่ใช่ไดเรกทอรี ไฟล์สเปเชียลอุปกรณ์ หรือลิงก์ ฯลฯ |
-g filename | True หากไฟล์มีอยู่และเป็น set-group-id (SETGID) |
-h filename | True หากไฟล์มีอยู่และเป็นซิมบอลิกลิงก์ (Symbolic Link) |
-k filename | True หากไฟล์มีอยู่และ "sticky bit" ถูกตั้งค่า |
-p filename | True หากไฟล์มีอยู่และเป็นเนมด์ไพป์ (Named Pipe / FIFO) |
-r filename | True หากไฟล์มีอยู่และอ่านได้ (Readable) คือมีบิตอ่าน (Read bit) ตั้งค่าไว้ |
-s filename | True หากไฟล์มีอยู่และมีขนาดมากกว่าศูนย์ ไฟล์ที่มีอยู่แต่ขนาดเป็นศูนย์จะคืนค่า False |
-t fd | True หากตัวระบุไฟล์ (File Descriptor) fd เปิดอยู่และอ้างอิงถึงเทอร์มินัล |
-u filename | True หากไฟล์มีอยู่และบิต set-user-id ถูกตั้งค่า |
-w filename | True หากไฟล์มีอยู่และเขียนได้ (Writable) |
-x filename | True หากไฟล์มีอยู่และรันได้ (Executable) |
-G filename | True หากไฟล์มีอยู่และเป็นของ Effective Group ID |
-L filename | True หากไฟล์มีอยู่และเป็นซิมบอลิกลิงก์ (เหมือน -h) |
-N filename | True หากไฟล์มีอยู่และถูกแก้ไขหลังจากอ่านครั้งล่าสุด |
-O filename | True หากไฟล์มีอยู่และเป็นของ Effective User ID |
-S filename | True หากไฟล์มีอยู่และเป็นโซคเก็ต (Socket) |
file1 -ef file2 | True หาก file1 และ file2 อ้างอิงถึงหมายเลขอุปกรณ์และ iNode เดียวกัน |
file1 -nt file2 | True หาก file1 ใหม่กว่า file2 (ตามวันที่แก้ไข) หรือ file1 มีอยู่แต่ file2 ไม่มี |
file1 -ot file2 | True หาก file1 เก่ากว่า file2 หรือ file2 มีอยู่แต่ file1 ไม่มี |
ตารางที่ 1: ตัวดำเนินการไฟล์ของ Bash
ตัวอย่างเช่น เริ่มต้นด้วยการทดสอบความมีอยู่ของไฟล์:
[student@studentvm1 testdir]$ File="TestFile1" ; if [ -e $File ] ; then echo "The file $File exists." ; else echo "The file $File does not exist." ; fi
The file TestFile1 does not exist.
[student@studentvm1 testdir]$
ต่อไป สร้างไฟล์สำหรับทดสอบชื่อ TestFile1 ขณะนี้ไม่จำเป็นต้องมีข้อมูล:
[student@studentvm1 testdir]$ touch TestFile1
ง่ายดายที่จะเปลี่ยนค่าตัวแปร $File แทนที่จะใช้สตริงข้อความชื่อไฟล์ในหลายที่ในโปรแกรม CLI สั้นๆ นี้:
[student@studentvm1 testdir]$ File="TestFile1" ; if [ -e $File ] ; then echo "The file $File exists." ; else echo "The file $File does not exist." ; fi
The file TestFile1 exists.
[student@studentvm1 testdir]$
ตอนนี้ ทดสอบว่าไฟล์มีอยู่และมีความยาวไม่เป็นศูนย์ (คือมีข้อมูล) คุณต้องการทดสอบ 3 เงื่อนไข: 1. ไฟล์ไม่มีอยู่ 2. ไฟล์มีอยู่แต่อยู่ว่าง 3. ไฟล์มีอยู่และมีข้อมูล ดังนั้นคุณต้องการชุดการทดสอบที่ซับซ้อนกว่า—ใช้ส่วน elif ในโครงสร้าง if-elif-else เพื่อทดสอบเงื่อนไขทั้งหมด:
[student@studentvm1 testdir]$ File="TestFile1" ; if [ -s $File ] ; then echo "$File exists and contains data." ; fi
[student@studentvm1 testdir]$
ในกรณีนี้ ไฟล์มีอยู่แต่ไม่มีข้อมูล เพิ่มข้อมูลเข้าไปและลองใหม่:
[student@studentvm1 testdir]$ File="TestFile1" ; echo "This is file $File" > $File ; if [ -s $File ] ; then echo "$File exists and contains data." ; fi
TestFile1 exists and contains data.
[student@studentvm1 testdir]$
การทดสอบนี้ใช้ได้ แต่แม่นยำสำหรับเงื่อนไขเฉพาะเพียง 1 ใน 3 เงื่อนไข เพิ่มส่วน else เพื่อให้แม่นยำขึ้น และลบไฟล์เพื่อทดสอบโค้ดใหม่นี้อย่างเต็มที่:
[student@studentvm1 testdir]$ File="TestFile1" ; rm $File ; if [ -s $File ] ; then echo "$File exists and contains data." ; else echo "$File does not exist or is empty." ; fi
TestFile1 does not exist or is empty.
สร้างไฟล์ว่างเพื่อทดสอบ:
[student@studentvm1 testdir]$ File="TestFile1" ; touch $File ; if [ -s $File ] ; then echo "$File exists and contains data." ; else echo "$File does not exist or is empty." ; fi
TestFile1 does not exist or is empty.
เพิ่มเนื้อหาลงในไฟล์และทดสอบอีกครั้ง:
[student@studentvm1 testdir]$ File="TestFile1" ; echo "This is file $File" > $File ; if [ -s $File ] ; then echo "$File exists and contains data." ; else echo "$File does not exist or is empty." ; fi
TestFile1 exists and contains data.
ตอนนี้ เพิ่มส่วน elif เพื่อแยกแยะระหว่างไฟล์ที่ไม่มีอยู่กับไฟล์ที่ว่างเปล่า:
[student@studentvm1 testdir]$ File="TestFile1" ; touch $File ; if [ -s $File ] ; then echo "$File exists and contains data." ; elif [ -e $File ] ; then echo "$File exists and is empty." ; else echo "$File does not exist." ; fi
TestFile1 exists and is empty.
[student@studentvm1 testdir]$ File="TestFile1" ; echo "This is $File" > $File ; if [ -s $File ] ; then echo "$File exists and contains data." ; elif [ -e $File ] ; then echo "$File exists and is empty." ; else echo "$File does not exist." ; fi
TestFile1 exists and contains data.
[student@studentvm1 testdir]$
ตอนนี้คุณมีโปรแกรม CLI ของ Bash ที่สามารถทดสอบเงื่อนไข 3 แบบนี้ได้ แต่อนาคตก็ยังมีความเป็นไปได้อีกมากมาย
จะเห็นโครงสร้างตรรกะของคำสั่งซ้อนที่ซับซ้อนได้ง่ายขึ้น หากจัดเรียงคำสั่งโปรแกรมคล้ายกับสคริปต์ที่บันทึกในไฟล์ รูปที่ 2 (Figure 2) แสดงลักษณะการเขียนแบบนี้ การเยื้อง (Indentation) ของคำสั่งในแต่ละส่วนของโครงสร้าง if-elif-else ช่วยทำให้ตรรกะชัดเจนขึ้น
File="TestFile1"
echo "This is $File" > $File
if [ -s $File ]
then
echo "$File exists and contains data."
elif [ -e $File ]
then
echo "$File exists and is empty."
else
echo "$File does not exist."
fi
รูปที่ 2: โปรแกรมบรรทัดคำสั่งที่เขียนใหม่ในรูปแบบสคริปต์
ตรรกะที่ซับซ้อนอย่างนี้ยาวเกินไปสำหรับโปรแกรม CLI ส่วนใหญ่ แม้คำสั่งในตัว (Built-in) ของ Linux หรือ Bash ทุกคำสั่งจะใช้ได้ในโปรแกรม CLI แต่เมื่อโปรแกรม CLI ยาวและซับซ้อนขึ้น จะมีเหตุผลมากกว่าที่จะสร้างสคริปต์ที่เก็บในไฟล์และรันได้ทุกเมื่อ ทั้งตอนนี้และอนาคต
ตัวดำเนินการเปรียบเทียบสตริง (String Comparison Operators)
ตัวดำเนินการเปรียบเทียบสตริงช่วยให้เปรียบเทียบสตริงอักขระ英数字 (Alphanumeric) ได้ มีเพียงไม่กี่ตัว ซึ่งแสดงในตารางที่ 3 (Figure 3)
| ตัวดำเนินการ (Operator) | คำอธิบาย (Description) |
|---|---|
-z string | True หากความยาวของสตริงเป็นศูนย์ |
-n string | True หากความยาวของสตริงไม่ใช่ศูนย์ |
string1 == string2 | True หากสตริงทั้งสองเท่ากัน ควรใช้ = เดี่ยวกับคำสั่ง test เพื่อความเข้ากันได้กับ POSIX เมื่อใช้กับคำสั่ง [[ จะทำการจับคู่รูปแบบ (Pattern Matching) ตามที่อธิบายไว้ (Compound Commands) |
string1 != string2 | True หากสตริงทั้งสองไม่เท่ากัน |
string1 < string2 | True หาก string1 เรียงก่อน string2 ตามลำดับเล็กซิโคกราฟิก (Lexicographically) (อ้างอิงลำดับการเรียงเฉพาะ Locale สำหรับอักขระ英数字และพิเศษทั้งหมด) |
string1 > string2 | True หาก string1 เรียงหลัง string2 ตามลำดับเล็กซิโคกราฟิก |
ตารางที่ 3: ตัวดำเนินการตรรกะสตริงของ Bash
ก่อนอื่น ให้ดูความยาวของสตริง เครื่องหมายคำพูด (Quotes) รอบ $MyVar ในการเปรียบเทียบต้องมีอยู่ เพื่อให้การเปรียบเทียบทำงานได้ (คุณควรยังคงทำงานใน ~/testdir)
[student@studentvm1 testdir]$ MyVar="" ; if [ -z "" ] ; then echo "MyVar is zero length." ; else echo "MyVar contains data" ; fi
MyVar is zero length.
[student@studentvm1 testdir]$ MyVar="Random text" ; if [ -z "" ] ; then echo "MyVar is zero length." ; else echo "MyVar contains data" ; fi
MyVar is zero length.
คุณสามารถทำได้อีกทางหนึ่งดังนี้:
[student@studentvm1 testdir]$ MyVar="Random text" ; if [ -n "$MyVar" ] ; then echo "MyVar contains data." ; else echo "MyVar is zero length" ; fi
MyVar contains data.
[student@studentvm1 testdir]$ MyVar="" ; if [ -n "$MyVar" ] ; then echo "MyVar contains data." ; else echo "MyVar is zero length" ; fi
MyVar is zero length
บางครั้งคุณอาจต้องการทราบความยาวที่แน่นอนของสตริง ซึ่งไม่ใช่การเปรียบเทียบ แต่เกี่ยวข้อง น่าเสียดายที่ไม่มีวิธีง่ายๆ ในการหาความยาวของสตริง มีวิธีทำได้ 2-3 วิธี แต่ผมคิดว่าการใช้คำสั่ง expr (evaluate expression) เป็นวิธีที่ง่ายที่สุด อ่าน man page ของ expr เพื่อดูรายละเอียดเพิ่มเติม โปรดทราบว่าต้องมีเครื่องหมายคำพูดรอบสตริงหรือตัวแปรที่ทดสอบ
[student@studentvm1 testdir]$ MyVar="" ; expr length "$MyVar"
0
[student@studentvm1 testdir]$ MyVar="How long is this?" ; expr length "$MyVar"
17
[student@studentvm1 testdir]$ expr length "We can also find the length of a literal string as well as a variable."
70
เกี่ยวกับตัวดำเนินการเปรียบเทียบ ผมใช้การทดสอบมากในสคริปต์เพื่อตรวจสอบว่าสตริงสองตัวเท่ากัน (เหมือนกันทุกประการ) ผมใช้รุ่น non-POSIX ของตัวดำเนินการเปรียบเทียบนี้:
[student@studentvm1 testdir]$ Var1="Hello World" ; Var2="Hello World" ; if [ "$Var1" == "$Var2" ] ; then echo "Var1 matches Var2" ; else echo "Var1 and Var2 do not match." ; fi
Var1 matches Var2
[student@studentvm1 testdir]$ Var1="Hello World" ; Var2="Hello world" ; if [ "$Var1" == "$Var2" ] ; then echo "Var1 matches Var2" ; else echo "Var1 and Var2 do not match." ; fi
Var1 and Var2 do not match.
ลองทดลองทดสอบตัวดำเนินการเหล่านี้ด้วยตัวเองอีกบ้าง
ตัวดำเนินการเปรียบเทียบตัวเลข (Numeric Comparison Operators)
ตัวดำเนินการตัวเลขทำการเปรียบเทียบระหว่างอาร์กิวเมนต์ตัวเลขสองตัว เหมือนกับชั้นอื่นๆ ส่วนใหญ่เข้าใจง่าย
| ตัวดำเนินการ (Operator) | คำอธิบาย (Description) |
|---|---|
arg1 -eq arg2 | True หาก arg1 เท่ากับ arg2 |
arg1 -ne arg2 | True หาก arg1 ไม่เท่ากับ arg2 |
arg1 -lt arg2 | True หาก arg1 น้อยกว่า arg2 |
arg1 -le arg2 | True หาก arg1 น้อยกว่าหรือเท่ากับ arg2 |
arg1 -gt arg2 | True หาก arg1 มากกว่า arg2 |
arg1 -ge arg2 | True หาก arg1 มากกว่าหรือเท่ากับ arg2 |
ตารางที่ 4: ตัวดำเนินการตรรกะเปรียบเทียบตัวเลขของ Bash
นี่คือตัวอย่างง่ายๆ ตัวแรกตั้งค่าตัวแปร $X เป็น 1 แล้วทดสอบว่า $X เท่ากับ 1 หรือไม่ ในตัวที่สอง X เป็น 0 ดังนั้นการเปรียบเทียบจึงไม่เป็นจริง
[student@studentvm1 testdir]$ X=1 ; if [ $X -eq 1 ] ; then echo "X equals 1" ; else echo "X does not equal 1" ; fi
X equals 1
[student@studentvm1 testdir]$ X=0 ; if [ $X -eq 1 ] ; then echo "X equals 1" ; else echo "X does not equal 1" ; fi
X does not equal 1
[student@studentvm1 testdir]$
ลองทดลองทดสอบเพิ่มเติมด้วยตัวเอง
ตัวดำเนินการทั่วไป (Miscellaneous Operators)
ตัวดำเนินการทั่วไปเหล่านี้แสดงว่าตัวเลือกเชลล์ (Shell Option) ถูกตั้งค่าหรือตัวแปรเชลล์มีค่า แต่ไม่ได้ค้นหาค่าของตัวแปร แค่ตรวจสอบว่ามีค่าหรือไม่
| ตัวดำเนินการ (Operator) | คำอธิบาย (Description) |
|---|---|
-o optname | True หากตัวเลือกเชลล์ optname เปิดใช้งาน (ดูรายการตัวเลือกในคำอธิบายของตัวเลือก -o ของ Bash set builtin ใน man page ของ Bash) |
-v varname | True หากตัวแปรเชลล์ varname ถูกตั้งค่า (มีการกำหนดค่า) |
-R varname | True หากตัวแปรเชลล์ varname ถูกตั้งค่าและเป็นการอ้างอิงชื่อ (Name Reference) |
ตารางที่ 5: ตัวดำเนินการตรรกะทั่วไปของ Bash
ลองทดลองทดสอบตัวดำเนินการเหล่านี้ด้วยตัวเอง
การขยาย (Expansions)
Bash รองรับหลายประเภทของการขยายและการแทนที่ (Substitutions) ที่มีประโยชน์มาก ตาม man page ของ Bash มี 7 รูปแบบของการขยาย บทความนี้จะสำรวจ 5 รูปแบบ ได้แก่ การขยายทิลเด (Tilde Expansion), การขยายเลขคณิต (Arithmetic Expansion), การขยายพาธเนม (Pathname Expansion), การขยายวงเล็บปีกกา (Brace Expansion) และการแทนที่คำสั่ง (Command Substitution)
การขยายวงเล็บปีกกา (Brace Expansion)
การขยายวงเล็บปีกกาเป็นวิธีสร้างสตริงชุดต่างๆ ได้ตามต้องการ (เครื่องมือนี้ใช้ด้านล่างเพื่อสร้างไฟล์จำนวนมากสำหรับทดลองกับอักขระรูปแบบพิเศษ) สามารถใช้สร้างรายการสตริงชุดต่างๆ และแทรกเข้าไปในตำแหน่งเฉพาะภายในสตริงคงที่ (Static String) หรือที่ปลายทั้งสองข้างของสตริงคงที่ อาจยากที่จะจินตนาการ ดังนั้นลองทำจริงดีกว่า
ก่อนอื่น ดูว่าการขยายวงเล็บปีกกาทำอะไร:
[student@studentvm1 testdir]$ echo {string1,string2,string3}
string1 string2 string3
ดูไม่ค่อยช่วยอะไร แต่ลองใช้แบบนี้ดู:
[student@studentvm1 testdir]$ echo "Hello "{David,Jen,Rikki,Jason}.
Hello David. Hello Jen. Hello Rikki. Hello Jason.
ดูมีประโยชน์—อาจประหยัดการพิมพ์มาก ลองอีกตัว:
[student@studentvm1 testdir]$ echo b{ed,olt,ar}s
beds bolts bars
ผมอาจเล่าต่อไปได้ แต่อย่างน้อยคุณเข้าใจแนวคิดแล้ว
การขยายทิลเด (Tilde Expansion)
การขยายทิลเด (~) น่าจะเป็นการขยายที่พบบ่อยที่สุด เมื่อใช้ในคำสั่งเช่น cd ~/Documents เชลล์ Bash จะขยายมันเป็นทางลัดไปยังไดเรกทอรีบ้าน (Home Directory) เต็มรูปแบบของผู้ใช้
ใช้โปรแกรม Bash เหล่านี้เพื่อสังเกตผลของการขยายทิลเด:
[student@studentvm1 testdir]$ echo ~
/home/student
[student@studentvm1 testdir]$ echo ~/Documents
/home/student/Documents
[student@studentvm1 testdir]$ Var1=~/Documents ; echo $Var1 ; cd $Var1
/home/student/Documents
[student@studentvm1 Documents]$
การขยายพาธเนม (Pathname Expansion)
การขยายพาธเนมเป็นคำอธิบายที่ฟังดูซับซ้อนสำหรับการขยายรูปแบบไฟล์-กล็อบบิ้ง (File-Globbing Patterns) โดยใช้อักขระ ? และ * ให้กลายเป็นชื่อเต็มของไดเรกทอรีที่ตรงกับรูปแบบ ไฟล์กล็อบบิ้ง (File Globbing) หมายถึงอักขระรูปแบบพิเศษที่ให้ความยืดหยุ่นมากในการจับคู่ชื่อไฟล์ ไดเรกทอรี และสตริงอื่นๆ เมื่อทำการดำเนินการต่างๆ อักขระรูปแบบพิเศษเหล่านี้ช่วยให้จับคู่อักขระเดี่ยว หลายตัว หรืออักขระเฉพาะในสตริงได้
- ? — จับคู่อักขระใดๆ เพียงตัวเดียวในตำแหน่งที่ระบุภายในสตริง
- * — จับคู่อักขระใดๆ จำนวนศูนย์หรือมากกว่า ในตำแหน่งที่ระบุภายในสตริง
การขยายนี้ใช้กับชื่อไดเรกทอรีที่ตรงกัน เพื่อดูการทำงาน ให้แน่ใจว่า testdir เป็นไดเรกทอรีทำงานปัจจุบัน (PWD) และเริ่มด้วยการแสดงรายการธรรมดา (เนื้อหาในไดเรกทอรีบ้านของผมจะต่างจากของคุณ):
[student@studentvm1 testdir]$ ls
chapter6 cpuHog.dos dmesg1.txt Documents Music softlink1 testdir6 Videos
chapter7 cpuHog.Linux dmesg2.txt Downloads Pictures Templates testdir
testdir cpuHog.mac dmesg3.txt file005 Public testdir tmp
cpuHog Desktop dmesg.txt link3 random.txt testdir1 umask.test
[student@studentvm1 testdir]$
ตอนนี้ แสดงรายการไดเรกทอรีที่ขึ้นต้นด้วย Do, testdir/Documents, และ testdir/Downloads:
Documents:
Directory01 file07 file15 test02 test10 test20 testfile13 TextFiles
Directory02 file08 file16 test03 test11 testfile01 testfile14
file01 file09 file17 test04 test12 testfile04 testfile15
file02 file10 file18 test05 test13 testfile05 testfile16
file03 file11 file19 test06 test14 testfile09 testfile17
file04 file12 file20 test07 test15 testfile10 testfile18
file05 file13 Student1.txt test08 test16 testfile11 testfile19
file06 file14 test01 test09 test18 testfile12 testfile20
Downloads:
[student@studentvm1 testdir]$
ผลลัพธ์ไม่ตรงที่ต้องการ มันแสดงเนื้อหาภายในไดเรกทอรีที่ขึ้นต้นด้วย Do เพื่อแสดงเฉพาะไดเรกทอรี ไม่รวมเนื้อหาภายใน ให้ใช้ตัวเลือก -d
[student@studentvm1 testdir]$ ls -d Do*
Documents Downloads
[student@studentvm1 testdir]$
ในทั้งสองกรณี เชลล์ Bash ขยายรูปแบบ Do* ให้กลายเป็นชื่อของไดเรกทอรีสองตัวที่ตรงกับรูปแบบ แต่ถ้ามีไฟล์ที่ตรงกับรูปแบบด้วยจะเป็นอย่างไร?
[student@studentvm1 testdir]$ touch Downtown ; ls -d Do*
Documents Downloads Downtown
[student@studentvm1 testdir]$
นี่แสดงไฟล์ด้วย ดังนั้น ไฟล์ใดๆ ที่ตรงกับรูปแบบก็จะถูกขยายเป็นชื่อเต็มของมันเช่นกัน
การแทนที่คำสั่ง (Command Substitution)
การแทนที่คำสั่งเป็นรูปแบบหนึ่งของการขยาย ที่อนุญาตให้สตรีมข้อมูล STDOUT ของคำสั่งหนึ่งถูกใช้เป็นอาร์กิวเมนต์ของอีกคำสั่งหนึ่ง เช่น เป็นรายการไอเท็มที่จะประมวลผลในลูป man page ของ Bash กล่าวว่า: "Command substitution allows the output of a command to replace the command name." (การแทนที่คำสั่งอนุญาตให้เอาต์พุตของคำสั่งแทนที่ชื่อคำสั่ง) ผมคิดว่าค่อนข้างแม่นยำ แม้จะดูค่อนข้าง深奥 (Obtuse)
มี 2 รูปแบบของการแทนที่นี้ คือ `command` และ $(command) ในรูปแบบเก่าที่ใช้แบ็กทิก (`) การใช้แบ็กสแลช (\) ในคำสั่งจะเก็บความหมายตามตัวอักษร (Literal Meaning) แต่เมื่อใช้ในรูปแบบวงเล็บใหม่ แบ็กสแลชจะมีความหมายเป็นอักขระพิเศษ โปรดสังเกตอีกว่า รูปแบบวงเล็บใช้วงเล็บเดี่ยวเพียงคู่เดียวในการเปิดและปิดคำสั่ง
ผมใช้ความสามารถนี้บ่อยในโปรแกรมบรรทัดคำสั่งและสคริปต์ โดยที่ผลลัพธ์ของคำสั่งหนึ่งถูกใช้เป็นอาร์กิวเมนต์สำหรับอีกคำสั่งหนึ่ง
เริ่มต้นด้วยตัวอย่างง่ายที่ใช้ทั้งสองรูปแบบของการขยายนี้ (อีกครั้ง ให้แน่ใจว่า testdir เป็น PWD):
[student@studentvm1 testdir]$ echo "Todays date is `date`"
Todays date is Sun Apr 7 14:42:46 EDT 2019
[student@studentvm1 testdir]$ echo "Todays date is $(date)"
Todays date is Sun Apr 7 14:42:59 EDT 2019
[student@studentvm1 testdir]$
ตัวเลือก -w ของยูทิลิตี้ seq เพิ่มศูนย์นำหน้า (Leading Zeros) ให้กับตัวเลขที่สร้างขึ้น เพื่อให้กว้างเท่ากันทุกตัว คือมีจำนวนหลักเท่ากันไม่ว่าค่าจะเป็นเท่าใด ทำให้ง่ายต่อการเรียงลำดับตามตัวเลข
ยูทิลิตี้ seq ใช้สร้างลำดับตัวเลข:
[student@studentvm1 testdir]$ seq 5
1
2
3
4
5
[student@studentvm1 testdir]$ echo `seq 5`
1 2 3 4 5
[student@studentvm1 testdir]$
ตอนนี้คุณสามารถทำอะไรที่มีประโยชน์กว่านี้ได้ เช่น สร้างไฟล์ว่างจำนวนมากสำหรับทดสอบ:
[student@studentvm1 testdir]$ for I in $(seq -w 5000) ; do touch file-$I ; done
ในการใช้งานนี้ คำสั่ง seq -w 5000 สร้างรายการตัวเลข 1 ถึง 5,000 โดยใช้การแทนที่คำสั่งเป็นส่วนหนึ่งของคำสั่ง for รายการตัวเลขจะถูกใช้โดยคำสั่ง for เพื่อสร้างส่วนตัวเลขของชื่อไฟล์
การขยายเลขคณิต (Arithmetic Expansion)
Bash สามารถคำนวณคณิตศาสตร์จำนวนเต็ม (Integer Math) ได้ แต่ค่อนข้าง煩瑣 (Cumbersome) (จะเห็นเร็วๆ นี้) ไวยากรณ์สำหรับการขยายเลขคณิตคือ $((arithmetic-expression)) ใช้วงเล็บคู่ (Double Parentheses) ในการเปิดและปิดนิพจน์
การขยายเลขคณิตทำงานคล้ายการแทนที่คำสั่งในโปรแกรมหรือสคริปต์เชลล์ ค่าที่คำนวณได้จากนิพจน์จะแทนที่นิพจน์เพื่อประเมินค่าต่อไปโดยเชลล์
อีกครั้ง เริ่มจากสิ่งง่ายๆ:
[student@studentvm1 testdir]$ echo $((1+1))
2
[student@studentvm1 testdir]$ Var1=5 ; Var2=7 ; Var3=$((Var1*Var2)) ; echo "Var 3 = $Var3"
Var 3 = 35
การหารต่อไปนี้ให้ผลศูนย์ เพราะผลลัพธ์จะเป็นค่าทศนิยมน้อยกว่า 1:
[student@studentvm1 testdir]$ Var1=5 ; Var2=7 ; Var3=$((Var1/Var2)) ; echo "Var 3 = $Var3"
Var 3 = 0
นี่คือการคำนวณง่ายๆ ที่ผมทำบ่อยในสคริปต์หรือโปรแกรม CLI ที่บอกว่ามีหน่วยความจำเสมือน (Virtual Memory) ทั้งหมดเท่าไหร่ในโฮสต์ Linux คำสั่ง free ไม่ให้ข้อมูลนี้:
[student@studentvm1 testdir]$ RAM=`free | grep ^Mem | awk '{print $2}'` ; Swap=`free | grep ^Swap | awk '{print $2}'` ; echo "RAM = $RAM and Swap = $Swap" ; echo "Total Virtual memory is $((RAM+Swap))" ;
RAM = 4037080 and Swap = 6291452
Total Virtual memory is 10328532
ผมใช้ตัวอักขระ ` เพื่อจำกัดส่วนของโค้ดที่ใช้สำหรับการแทนที่คำสั่ง
ผมใช้การขยายเลขคณิตของ Bash ส่วนใหญ่สำหรับตรวจสอบปริมาณทรัพยากรระบบในสคริปต์ แล้วเลือกเส้นทางการรันโปรแกรมตามผลลัพธ์
สรุป
บทความนี้ ซึ่งเป็นบทความที่สองในชุดเรื่อง Bash ในฐานะภาษาโปรแกรม ได้สำรวจตัวดำเนินการตรรกะไฟล์ สตริง ตัวเลข และทั่วไปของ Bash ที่ให้การควบคุมโฟลว์การทำงาน รวมถึงประเภทต่างๆ ของการขยายเชลล์
บทความที่สามในชุดนี้จะสำรวจการใช้ลูปสำหรับทำงานวนซ้ำประเภทต่างๆ