Note07 grep กับการเน้นสายอักขระที่ตรงกับนิพจน์ปรกติให้เด่น (Highlight) --------------------------------------------------- การกำหนดให้ grep เน้นสายอักขระที่ตรงกับรูปแบบให้เด่น (highlight) โดยเน้นด้วยสีที่กำหนด ทำผ่านการกำหนดค่าของตัวแปร GREP_OPTIONS ทำได้ดังนี้ กำหนดไว้เป็นการถาวร กำหนดในแฟ้ม ~/.bash_profile หรือ .bash_rc โดยเพิ่มบรรทัดดังต่อไปนี้ export GREP_OPTIONS="--color=auto" export เป็นคำสั่งของเชลล์ ที่กำหนดให้เชลล์ส่งผ่านตัวแปรและค่า ไปยัง child process ทั้งหมดที่สร้างขึ้น หรืออาจใช้วิธีกำหนดนามแฝง (alias) ให้คำสั่ง egrep หมายถึงคำสั่ง egrep และตัวเลือก --color=auto คือ alias egrep="egrep --color=auto" เมื่อกำหนดนามแฝงไว้แล้ว ทุกครั้งที่มีการเรียกใช้คำสั่ง egrep ระบบจะเปลี่ยนแทนชื่อ egrep ด้วย egrep --color=auto ทำให้เกิดการเน้นสายอักขระที่ตรงกันให้เด่นทุกครั้งที่มีการเรียกใช้ ยกเว้นในกรณีที่มีการใช้ตัวเลือก -v (invert) ร่วมด้วย สำหรับค่าของตัวเลือก --color มีได้ 3 ค่าคือ always, auto, และ never ซึ่งมีความหมายดังนี้ always - กำหนดให้มีการเน้นให้เด่นตลอดเวลา รวมทั้งการส่งผลลัพธ์ผ่าน pipe หรือการเปลี่ยนทิศทางข้อมูลเข้า/ออก (redirection) ซึ่งอาจมีผลกับการทำงานของโปรแกรมที่ดำเนินการทีละตัวอักขระ เนื่องจาก grep กำหนดสีอักขระที่ต้องการเน้นด้วยการใช้รหัสสีซึ่งเป็น escape sequence ตามมาตรฐาน ANSI ซึ่งขึ้นต้นด้วยรหัส ESC [ ... ทำให้ได้อักขระที่เป็น escape sequence ปนไปกับข้อมูลออกด้วย auto - กำหนดให้แสดงการเน้นด้วยสีเฉพาะเมื่อมีการแสดงผลทางจอภาพเท่านั้น หากเป็นการส่งผลลัพธ์ผ่าน pipe หรือการเปลี่ยนทิศทางไปยีงแฟ้ม จะไม่มีการเน้นให้เด่น never - ปิดการใช้งานของการเน้นให้เดินด้วยสี หมายเหตุ: หากสนใจเรื่อง escape sequence ตามมาตรฐาน ANSI อ่านเพิ่มเติมได้จาก "ANSI escape code" ที่ https://en.wikipedia.org/wiki/ANSI_escape_code ในกรณีที่ต้องการให้ grep เน้นผลลัพธ์ให้เด่นเฉพาะการ login แต่ละครั้ง (session) ทำได้โดยใช้คำสั่ง ดังนี้ $ export GREP_OPTIONS="--color=auto" การเน้นให้เด่น มีค่าของสีโดยปริยายเป็น 1;31 หมายถึงการเน้นอักขระให้เด่นด้วยสีแดง (31) บนสีพื้นหลังเดิมของจอภาพที่ใช้ในขณะนั้น หากต้องการเปลี่ยนเป็นสีอื่น ทำได้โดยกำหนดสีที่ต้องการให้กับตัวแปร GREP_COLOR เช่น $ export GREP_COLOR="1;32" เป็นการเน้นให้เด่น ด้วยสีเขียวสด อย่างไรก็ดีคำสั่ง grep ในปัจจุบัน grep ไม่สนับสนุนให้มีการใช้ตัวแปร GREP_COLOR แล้ว เพราะสามารถดำเนินการผ่านตัวแปร GREP_COLORS ซึ่งกำหนดรูปแบบได้มากกว่า แต่ยังคงไว้เพื่อให้เข้ากันได้กับโปรแกรมรุ่นเก่า ทำให้ยังสามารถใช้กำหนดสีอย่างง่ายได้ สำหรับสีของตัวอักษรและพื้นหลัง ทำได้ 8 สี ดังนี้ สี พื้นหน้า พื้นหลัง Black 30 40 Red 31 41 Green 32 42 Yellow 33 43 Blue 34 44 Magenta 35 45 Cyan 36 46 White 37 47 หมายเหตุ: เมื่อมีเพิ่มเติมคำสั่งลงในแฟ้ม .bash_profile แล้ว หากต้องการให้คำสั่งนั้นมีผลในการทำงานทันที โดยไม่จำเป็นต้อง logout และ login ใหม่ ทำได้โดยใช้คำสั่ง source เพื่อให้เชลล์อ่านแฟ้มและทำงานใหม่ เช่น $ source ~/.bash_profile ตัวอย่างการใช้ตัวเลือก -n และ -o ของคำสัง grep ----------------------------------------- โดยใช้แบบฝึกหัดตอนที่ ๑ ข้อ ๑ จากปฏิบัติการสัปดาห์ที่แล้ว # กำหนดให้คำสั่ง cat แสดงหมายเลขบรรทัดของข้อมูลในแฟ้ม เพื่อใช้เปรียบเทียบกับผลลัพธ์ที่ได้จาก grep $ cat -n item1.dat 1 abababa 2 aaba 3 aabbaa 4 aba 5 aabababa # ตัวเลือก -n กำหนดให้ grep แสดงหมายเลขบรรทัดที่มีสายอักขระตรงกับนิพจน์ปรกติ $ egrep -n 'a(ab)*a' item1.dat 2:aaba 3:aabbaa 5:aabababa # บรรทัดที่ 3 มีสายอักขระที่ตรงกับนิพจน์ปรกติสองสุด คือ aa (หน้า) และ aa (หลัง) เมื่อกำหนดให้ grep เน้นสายอักขระ # ที่ตรงกับนพจน์ปรกติที่กำหนด จะเห็นเป็น "aa" bb "aa" เมื่อสายอักขระในเครื่องหมายคำพูดถูกเน้นให้เด่นด้วยสีแดง # ตัวเลือก -o สำหรับแสดงเฉพาะส่วนของบรรทัดที่ตรงกับนิพจน์ปรกติ เมื่อนำมาใช้ร่วมกับตัวเลข -n # เป็น -on จะแสดงหมายเลขบรรทัด แต่ละบรรทัดแสดงเฉพาะสายอักขระที่ตรงกับนิพจน์ปรกติ $ egrep -on 'a(ab)*a' item1.dat 2:aaba 3:aa # บรรทัดที่ 3 มีสองชุด, ชุดแรก 3:aa # ชุดที่สอง 5:aabababa สำหรับตัวเลือกข้อ 5 สายอักขระ "aa" ที่ตันบรรทัดตรงกับนิพจน์ปรกติอยู่แล้ว แต่เนื่องจาก * เป็น greedy operator ดังนั้นจึงพยายามจับคู่ต่อไป ซึ่งสามารถจับคู่ได้จนถึงอักขระตัวสุดท้ายของสายอักขระ ดังนั้นสายอักขระที่ตรงกับนิพจน์ปรกติจึงเป็น "aabababa" ไม่ใช่แต่เฉพาะสายอักขระ "aa" คำอธิบายแบบฝึกหัด Regular expression (regexp) ตอนที่ ๑ ---------------------------------------------------- ๑. นิพจน์ปรกติ 'a(ab)*a' อักษร a (literal) | | a (ab)* a | (ab)* -- * กำหนดให้ "หน่วย" ที่อยู่ข้างหน้ามีได้ตั้งแต่ศูนย์ตัวขึ้นไป "หน่วย" เป็นอักขระตัวเดียวโดยปริยาย หากเป็นกลุ่มของอักขระต้องกำหนดในวงเล็บ (ab)* -- จะมีกลุ่มอักษร ab หรือไม่ก็ได้ หรือมีหลายกลุ่มชุดก็ได้ หากเป็น ab* จะหมายถึง b ตัวเดียว ดังนั้นนิพจน์ปรกติ 'a(ab)*a' จะจับคู่ได้กับสายอักขระย่อย aa, aaba, aabababa, aababababa, ... กล่าวอีกนัยหนึ่งคือ นิพจน์ปรกติ 'a(ab)*a' จะจับคู่ได้กับสายอักขระที่มี aa เป็นสายอักขระย่อย ได้แก่ ข. aaba ค. aabbaa จ. aabababa -- การทดลองกำหนดให้ egrep อ่านข้อมูลจากแฟ้มที่กำหนด เช่น egrep 'a(ab)*a' item1.dat เนื่องจากสายอักขระในตัวเลือกพิมพ์ได้ยาก จึงแนะนำให้พิมพ์เก็บไว้ในแฟ้มจะได้ทดลองซ้ำได้งาย หากต้องการทดลองเพิ่มเติมกับคำอื่นๆ ทำได้โดยใช้คำสัง $ egrep 'a(ab)*a' หรือ $ grep -E 'a(ab)*a' เป็นการกำหนดให้ egrep อ่านบรรทัดข้อมูลจาก standard input เมื่อป้อนข้อมูลเสร็จและและกด หากสายอักขระที่ป้อนจับคู่ได้กับนิพจน์ปรกติที่กำหนด grep จะพิมพ์บรรทัดนั้นทางจอภาพ หากจับคู่ไม่ได้จะไม่มีการพิมพ์ วิธีการนี้จะช่วยทำให้เข้าใจการจับคู่ของ grep ได้ดีขึ้น และจะทำงานเช่นนี้ต่อเนื่องไป เมื่อต้องการเลิกให้กดแป้น Ctrl-D ซึ่งเท่ากับเป็นการพิมพ์อักขระที่มีรหัส ASCII = 4 ได้แก่ EOT - End of transmission (เลิกส่งข้อมูล) ซึ่งระบบปฏิบัติการ Unix กำหนดไว้ให้เป็นรหัสสำหรับปิดแฟ้ม (End-of-file) -- โดยปกติเมื่อนิพจน์ปรกติและสายอักขระที่กำหนดสามารถจับคู่กันได้ egrep จะแสดงผลสายอักขระนั้นทั้งบรรทัด ผู้ใช้สามารถเพิ่ม option -o (only-matching) เพื่อกำหนดให้พิมพ์เฉพาะสายอักขระย่อยที่ตรงกับนิพจน์ได้ ซึ่งจะช่วยให้เข้าใจการทำงานของ grep ดีขึ้น -- เมื่อเพิ่ม option -w เพื่อกำหนดให้การจับคู่เกิดขึ้นทั้งคำ เช่น egrep -w 'a(ab)*a' คือเป็นการจับคู่กับคำที่ขึ้นต้นด้วย a และลงท้ายด้วย a ตรงกลางอาจเป็นอักขระว่าง หรือเป็นสายอักขระย่อย ab, abab, ababab, ... ซึ่งจะไม่ตรงกับตัวเลือก ข้อ (ค). ๒. นิพจน์ปรกติ 'ab+c?' อักษร a (literal) | a b+ c? | | | c? -- ? กำหนดให้อักขระที่อยู่ข้างหน้ามีศูนย์หรือหนึ่งตัว -- มีอักษร c หรือไม่ก็ได้ b+ -- + กำหนดให้อักขระที่อยู่ข้างหน้ามีตั้งแต่หนึ่งตัวขึ้นไป -- b, bb, bbb, ... ดังนั้นนิพจน์ปรกติ 'ab+c?' จึงจับคู่ได้กับสายอักขระ ab, abb, abbb, ... abc, abbc, abbbc, ... ๓. นิพจน์ปรกติ 'a.[bc]+' อักษร a (literal) | . แทนอักขระใดๆ จำนวนหนึ่งตัว ยกเว้นรหัสขึ้นบรรทัดใหม่ (newline) | | a . [bc]+ | [bc] เป็น character class ที่มีสมาชิกสองตัว ใช้แทนอักขระใดๆ ที่เป็นสาชิกของเซตนี้เพียงหนึ่งตัว จะเป็น b หรือ c ก็ได้เพียงหนึ่งตัว [bc]+ -- แทน b, bb, bbb, ..., bc, bbc, ..., bcc, ..., c, cc, ccc, ... นิพจน์ปรกติ 'a.[bc]+' จึงต้องประกอบด้วยอักขระอย่างน้อยสามตัวคือ a <อักขระใดๆ> ตัวเลือกข้อ (จ). ac จึงไม่ตรงกับนิพจน์ที่กำหนด ๔. นิพจน์ปรกติ 'abc|xyz' ขอเพียงพบสายอักขระย่อย abc หรือ xyz เพียงชุดใดชุดหนึ่ง ก็สามารถจับคู่กับ 'abc|xyz' ได้แล้ว ๕. นิพจน์ปรกติ '[a-z]+[\.\?!]' อักษรตัวเล็ก (lowercase) ตั้งแต่หนึ่งตัวขึ้นไป สมนัยกับ [[:lower:]] | [a-z]+ [\.\?!] | อักขระเพียงตัวเดียวซึ่งอาจเป็น . หรือ ? หรือ ! ก็ได้ เนื่องจาก . และ ? เป็น meta-character เมื่อนำมาใช้เป็น literal จึงต้องนำหน้าด้วย \ ประเด็นที่น่าสงสัยคือนิพจน์ปรกติ '[a-z]+[\.\?!]' จับคู่ได้กับ จ. jump up. การจับคู่สามารถทำได้ไม่ว่าสายอักขระจะอยู่ที่ใดในบรรทัด หากใช้ option -o จะเห็นว่าจุดที่จับคู่ได้จริงคือ "up." -- ซึ่งตรงกับความหมายของนิพจน์ปรกติคือ ตัวอักษรตัวเล็กตั้งแต่หนึ่งตัวขึ้นไป ตามด้วยเครื่องหมาย . หรือ ? หรือ ! อย่างใดอย่างหนึ่ง หากต้องการให้นิพจน์ปรกติจับคู่กับข้อความทั้งบรรทัด (ไม่ใช่เฉพาะสายอักขระย่อย) เช่นในกรณีของข้อ (จ). หากกำหนดให้เริ่มจับคู่ตั้งแต่ต้นบรรทัด, ^ ไปจนถึงท้ายบรรทัด, $ คือ '^[a-z]+[\.\?!]$' นิพจน์นี้จะไม่สามารถจับคู่กับสายอักขระในข้อ (จ). ได้อีก ๖. นิพจน์ปรกติ '[a-zA-Z]*[^,]=' อักษรตัวเล็ก (lowercase) หรือตัวใหญ่ (uppercase) ซึ่งอาจไม่มีเลย มีตัวเดียว หรือหลายตัวก็ได้ | | สมนัยกับ[[:alpha:]] [a-zA-Z]* [^,] = <-- ปิดท้ายด้วย = | อักขระใดๆ ที่ไม่ใช่ลูกน้ำ , ตัวเลือก (ข). BotHEr,= -- ไม่ตรงเพราะมีลูกน้ำ , ก่อน = (ค). Ample --ไม่ตรง เพราะขาด = ที่ท้ายคำ ตัวเลือกข้อ (ก). และ (ง). คือ (ก). Butt= (ง). FIdDlE7h= อาจทำให้สับสนได้บ้างเพราะอาจมองได้ว่า Butt และ FIdDIE7h จับคู่ได้กับ [a-zA-Z]* ไปแล้ว จึงไม่มีอักขระที่จับคู่ได้กับ [^,] มีแต่ = ซึ่งเป็นอักขระตัวสุดท้าย ดังนั้น Butt= จึงไม่น่าจับคู่กับนิพจน์ '[a-zA-Z]*[^,]=' ได้ เหตุผลในข้อนี้อยู่ที่วิธีการจับคู่ของนิพจน์ปกติ วิธีการเชิงละโมบ (greedy algorithm) ของเครื่องหมายทำซ้ำ และการทำ Backtracking เมื่อไม่สามารถจับคู่นิพจน์ปรกติและสายอักขระได้ ดังนี้ Greediness เครื่องหมายทำซ้ำคือ *, +, ?, {m,n} เป็น greedy operator เนื่องจากกำหนดให้นำอักขระที่มากที่สุดเท่าที่จะเป็นไปได้จากสายอักขระมาจับคู่กับนิพจน์ปรกติ เช่นนิพจน์ 'xy{2,4}' จะเริ่มด้วยการจับคู่กับ "xyyyy" หากไม่ตรงกัน จึงลดลงเป็น "xyyy" จากนั้นจึงเป็น "xyy" ด้วยเหตุนี้นิพจน์ 'xy{2,4}' จึงจับคู่ได้กับสายอักขระ axyyabc, aaxyyabc, ... กล่าวอีกนัยหนึ่งคือนิพจน์ปรกติ 'xy{2,4}' สามารถจับคู่กับข้อความใดๆ ที่มีสายอักขระย่อย xyy อยู่ในข้อความนั้น ในทำนองเดียวกัน นิพจน์ปรกติ '<.+>' ซึ่งหมายถึงอักขระใดๆ กี่ตัวก็ได้ที่อยู่ระหว่างเครื่องหมาย < และ > เมื่อนำไปใช้จับคู่กับสายอักขระ "Hello world" น่าจะจับคู่ได้เฉพาะกับ แต่ในความเป็นจริง '<.+>' จับคูได้กับ "Hello world" ทั้งบรรทัด ตั้งแต่ "<" ตัวแรกของ จนถึง ">" ตัวสุดท้ายของ ซึ่งทำให้เกิดปัญหาเมื่อใช้นิพจน์ปรกติในการค้นหาและเปลี่ยนแทน (search & replace) สำหรับการเขียนโปรแกรมเช่น Perl และ Java เป็นต้น ดูหัวข้อ Laziness Backtracking เมื่อไม่สามารถจับคู่นิพจน์ปรกติกับข้อความต่อไปได้อีก จะทำการ backtrack โดยการลดอักขระที่วิธีการ greedy กำหนดไว้ลงหนึ่งตัว เช่นนิพจน์ปรกติ 'z*zzz' จะเริ่มด้วยการจับคู่กับ "zzzz" นั่นคือในครั้งแรก z* จะจับคู่กับ "zzzz" เมื่อไม่สามารถจับคู่กับ 'zzz' ที่เหลือใน regexp กับข้อความต่อไปได้ จะทำการลด z ที่ได้จากการจับคู่ของ z* ลงหนึ่งตัวเป็น "zzz" หากยังไม่สามารถจับคู่กับข้อความที่เหลือได้ จะทำการลด z ที่ได้จากการจับคู่ของ z* ลงอีกหนึ่งตัว ทำเช่นนี้เรื่อยไปจนกว่าจะจับคู่ได้สำเร็จ ดังแผนภาพ 'z* zzz' ------------ ---------------- z* = zzzz สายอักขระว่าง จับคู่ไม่ได้ ลดอักขระที่แทนด้วย z* ลงหนึ่งตัว z* = zzz z ยังจับคู่ไม่ได้ ลดลงอีกหนึ่งตัว z* = zz zz z* = z zzz 'z*zzz' จับคู่ได้กับ "zzzz" Laziness จากตัวอย่างนิพจน์ '<.+>' ในหัวข้อ Greediness หากใช้งานในภาษาหรือโปรแกรมที่มีการค้นและเปลี่ยนแทน หากต้องการเปลี่ยน html tag เป็นข้อความว่า จะเปลี่ยนแทนข้อความ "Hello world" ทั้งชุดด้วยคำว่า เพียงคำดียว ในกรณีที่ต้องการให้เปลี่ยนแทนเป็น Hello world ต้องลดความขยันของ repition operator (หรือทำให้เกียจคร้าน) โดยการเติม ? ต่อท้าย เป็น '<.+?>' เพื่อบังคับให้มีการทำซ้ำให้น้อยที่สุดเท่าที่เป็นไปได้ และเรียกการดำเนินการดังกล่าวนี้ว่า laziness เรื่อง Laziness นี้ เป็นเพียงแต่ต้องการให้รู้ไว้ ไม่สามารถทำการทดลองให้เห็นจริงได้ใน grep และ egrep ๗. นิพจน์ปรกติ '[a-z][\.\?!]\s+[A-Z]' สัญลักษณ์ใหม่ในนิพจน์ปรกติชุดนี้คือ \s เนื่องจาก s เป็นอักขระธรรมดาหรือ literal (ไม่ใช้ meta-character) เมื่อนำมาใช้เป็นอักขระตามหลัง backslash, \ แสดงว่าเป็น escape sequence \s เป็นตัวแทนของ whitespace characters โปรแกรม grep ของ GNU กำหนดให้ประกอบด้วย [ \t\r\n\f] เมื่อ \t แทน tab, \r แทน carriage return, \n แทน newline และ \f แทน form feed ในโปรแกรมอืนๆ อาจรวม \v ซึ่งใช้แทน vertcal tab ไว้ด้วย \s สำหรับโปรแกรม grep สมนัยกับ [^\v[:space:]] ๘. นิพจน์ปรกติ '(very )+(fat )?(ugly|tall)' นิพจน์ข้อนี้ต้องระวังเว้นวรรค ซึ่งอาจดูได้ไม่ชัดเจน ในนิพจน์ต่อไปนี้จะใช้ underscore, '_' เป็นตัวแทน ดังนี้ (very_)+(fat_)?(ugly|tall) ๙. นิพจน์ปรกติ '<[^>]+>' นิพจน์ในข้อนี้หากมีข้อสงสัย ให้ย้อนกลับไปอ่านคำอธิบายในข้อ ๖ เรื่องนี้น่าจะมีประโยชน์อยู้บ้างในการทำ web application ตอนที่ ๒ ------- ๑. วิเคราะห์ความต้องการ จาก output ที่กำหนดแสดงว่าต้องการ substring 'p' (อักษรตัวเล็ก) ตามด้วย 'oai' หรือ 'เว้นวรรค' ตัวใดตัวหนึ่ง และปืดท้ายด้วย 't' -- 'p' และ 't' เป็น literal -- oai เป็น character class '[oai ]' -- regexp = "p[oai ]t" ๒. regexp = "ap[ et/9o][^ ]" แสดงวิธีการวิเคราะห์เองได้หรือไม่? ตอนที่ ๓ ------- ๑. organise (UK English) และ organize (American English) regexp = "organi[sz]e" หรือ regexp = "organis|ze" ๒. เลขฐานสิบหกในภาษา C ซึ่งขึ้นต้นด้วย 0x หรือ 0X ตามด้วยเลขฐานสิบหก อักษร A-F ที่ใช้จะเป็นอักษรตัวเล็กหรือตัวใหญ่ก็ได้ ตัวเลขฐานสิบหกต้องมีอย่างน้อยหนึ่งหลัก regexp = "0[xX][0-9A-Fa-f]+" ๓. รหัสนิสิตคณะวิทยาการสารสนเทศ regexp สำหรับรหัสนิสิตภาคปกติ = "5[678]16[0-9]{4}" regexp สำหรับรหัสนิสิตภาคพิเศษ = "5[678]66[0-9]{4}" ๔. รหัสโทรศัพท์เคลื่อนที่ รหัสตัวแรกเป็น 0 รหัสตัวที่สองเป็น 8, 6, 9 รหัสตัวที่สามเป็น วรรค หรือ - รหัสตัวที่สี่ถึงเจ็ดเป็น 0-9 จำนวนสี่หลัก รหัสตัวที่แปดเป็น วรรค หรือ - รหัสตัวที่สี่เก้าถึงสิบสองเป็น 0-9 จำนวนสี่หลัก regexp สำหรับโทรศัพท์เคลื่อนที่ = "0[869][ -][0-9]{4}[ -][0-9]{4}" หรือ = "0[869]([ -][0-9]{4}){2}" ๕. URL หรือ web address อย่างง่าย ชื่อโพรโตคอลเป็น http หรือ https --> https? ตามด้วย :// --> https?:// ซึ่งอาจจะมีหรือไม่ก็ได้ -- (https?://)? ตามด้วยชื่อ Domain ของ host ซึ่งคั่นด้วยจุด มีตั้งแต่สามถึงห้าชุด [a-zA-Z0-9](\.[a-zA-Z0-9]){2,5} regexp สำหรับURL หรือ web address อย่างง่าย = "(https?://)?[a-zA-Z0-9](\.[a-zA-Z0-9]){2,5}"