Lecture 14 คำชี้แจง ------ Lecture นี้แบ่งออกเป็น 3 ตอนคือ Here document, การใช้งานแฟ้ม และตัวอย่างโครงงาน shell script ตอนที่่ ๑ : Here Document ----------------------- Here document (หรือ heredoc) เป็นวิธีการรับข้อมูลเข้าสู่ script โดยไม่ต้องใช้แฟ้ม เป็นวิธีการที่สะดวกสำหรับข้อความขนาดเล็กที่ไม่ค่อยมีการเปลี่ยนแปลง มีรูปแบบการใช้งานดังนี้ คำสั่ง <<[-]ข้อความกำกับ ... ... ... เนื่อความของ here document ... ... ... ข้อความกำกับ Here document เป็นวิธีการเปลี่ยนทิศทางข้อมูลเข้า (redirection) แบบหนึ่งที่กำหนดให้เชลล์อ่านข้อมูลเข้าจากตำแหน่งปัจจุบัน(here) โดยเริ่มจากบรรทัดใต้ "ข้อความกำกับ" (หรือ limit string) และอ่านไปจนกว่าจะพบ "ข้อความกำกับ" ที่ตรงกัน ด้วยเหตุนี้"ข้อความกำกับ" จึงต้องเป็นหนึงเดียวและไม่ปรากฏอยู่ในเนื่อความของ here document และกำหนดว่าด้านหน้าและด้านหลังของ"ข้อความกำกับ" จะต้องไม่มี white space เลยทั้งสองชุด เพื่อให้สามารถจับคู่ได้อย่างแน่นอน นอกจากนี้แล้วข้อความกำกับที่ใช้ปิดท้าย here document ต้องอยู่ในตำแหน่งแรกของบรรทัดเท่านั้น ข้อสังเกต ------- 1. "คำสั่ง" เป็นโปรแกรมที่มีรับข้อมูลทาง standard input เช่น cat, sort, และ grep เป็นต้น 2. ต้องไม่มีวรรคระหว่าง << และ ข้อความกำกับที่จุดเริ่มต้น 3. [-] เป็น option เฉพาะของ bash สำหรับกำจัด Tab ที่อยู่ต้นบรรทัด ในกรณีที่ต้องการย่อหน้า here document ให้สอดคล้องกับโครงสร้างของโปรแกรม และต้องไม่มีวรรคระหว่าง << , - และข้อความกำกับ 4. ต้องไม่มีวรรคหน้าข้อความกำกับที่จุดสิ้นสุด Here document เป็นคุณลักษณะที่มีประโยชน์ และสามารถประยุกต์ใช้งานได้หลายแบบ ขอให้ศึกษาจากตัวอย่างต่อไปนี้ ตัวอย่างที่ 1 การเก็บข้อมูลรวมไว้ในแฟ้มโปรแกรม ------------------------------------- ข้อมูลบางอย่างเป็นข้อมูลที่เมื่อบันทึกแล้ว ไม่มีการเปลี่ยนแปลงแก้ไขเลย เช่น "วันเกิด" และ สถานที่เกิด เป็นต้น ข้อมูลตายคัวเหล่านี้สามารถเก็บรวมไว้กับ script เพื่อความสะดวกในการค้นหา โปรแกรมต่อไปนี้ใช้ในการค้นหาวันเกิดของบุคคล โดยเก็บชื่อและวันเกิดไว้ใน here document และใช้ grep (หรือ egrep) ค้นหาข้อมูลที่ต้องการ เนื่องจาก grep ใช้ค้นคำใดๆ ที่อยู่ในบรรทัดได้ โปรแกรมจึงสามารถให้คำตอบได้สองลักษณะ คือค้นว่าบุคคลที่กำหนดเกิดวันใด และมีบุคคลใดมี วันเกิดในเดือนที่กำหนดบ้าง โปรแกรมเป็นดังนี้ $ cat -n birthday 1 #!/bin/sh 2 3 egrep -i "$1" <<+ # Limit string เป็นเครื่องหมาย + เพียงตัวเดียว 4 Alex June 22 5 Babara February 3 6 Darlene May 8 7 Helen March 13 8 Jenny January 23 9 Nancy June 26 10 + # จุดสิ้นสุดของ here document บรรทัดที่ 4 - 9 เป็น here document แต่ละบรรทัดเป็นข้อมูลของแต่ละบุคคล การเพิ่มข้อมูลทำได้โดยการแทรกชื่อ และวันเกิดเข้าในโปรแกรม ตัวอย่างการทำงานของโปรแกรมเป็นดังนี้ $ birthday Jenny Jenny January 23 $ birthday June Alex June 22 Nancy June 26 หมายเหตุ ------- Limit string หรือ "ข้อความกำกับ" ในตัวอย่างนี้เป็นเครื่องหมาย "+" เพียงตัวเดียว ซึ่งทำงานได้ดี แต่อย่างไรก็ดีการใช้อักขระตัวเดียวเป็นข้อความกำกับ เป็นวิธีการที่ไม่น่าใช้โดยเฉพาะอักขระที่เป็นเครื่องหมายวรรคตอน และอักขระพิเศษ เพราะยากจะหลีกเลี่ยงที่จะไม่ให้ปรากฏในเนื้อความ ข้อความกำกับเป็นสายอักขระที่มีหลายตัวเป็นทางเลือกที่ดีกว่า ตัวอย่างที่ 2 script สำหรับสร้างโครงเอกสาร HTML ---------------------------------------- ดัดแปลงจาก - http://linuxcommand.org/ เกริ่นนำ: มีคำกล่าวว่าโปรแกรมเมอร์ที่เก่งเป็นคนที่ขี้เกียจเขียน ขี้เกียจพิมพ์ จึงเขียนโปรแกรมช่วยเพื่อลดการทำงานเหล่านี้ลงโปรแกรมต่อไปนี้ โปรแกรมใช้สำหรับสร้างโครงของเอกสาร html โดยใช้คำสั่ง echo ส่วนโปรแกรมที่สองใช้ here document $ cat -n html1.sh 1 #!/bin/sh 2 # A script to produce an HTML file 3 4 TITLE="System information for $(hostname).$(hostname -d)" 5 RIGHT_NOW=$(date +"%x %r %Z") 6 TIME_STAMP="Update on $RIGHT_NOW by $USER" 7 8 echo "" 9 echo "" 10 echo " " 11 echo " $TITLE" 12 echo " " 13 echo "" 14 echo "" 15 echo "" 16 echo "

$TITLE

" 17 echo "

$TIME_STAMP" 18 echo "" 19 echo "" หมายเหตุ 1. โปรแกรมนี้ใช้คำสั่ง hostname สำหรับแสดงชื่อ และ hostname -d สำหรับแสดง domain name ของเครื่องแม่ข่าย 2. format ของคำสั่ง date ที่ใช้งานเป็นดังนี้ %x แสดงวันที่ในรูป เดือน/วันที่/ปี ค.ศ. เช่น 04/14/2016 %r แสดงเวลาในระบบ 12 ชั่วโมง และ AM หรือ PM เช่น 08:47:10 AM ICT %Z แสดงตัวย่อของ Time Zone เช่น ICT (Indo China Time) เมื่อให้โปรแกรมทำงาน จะได้ผลลัพธ์เป็นโครงของเอกสาร HTML พร้อมข้อมูลที่กำหนดไว้ในตัวแปร ดังนี้ $ html1.sh System information for staff.informatics.buu.ac.th

System information for staff.informatics.buu.ac.th

Update on 04/14/2016 08:47:10 AM ICT by jira โปรแกรมทำได้งานตามที่ต้องการ ผู้ใช้สามารถเปลี่ยนทิศทาง output เป็นแฟ้ม *.html ที่ต้องการ เพื่อเพิ่มเนื้อหาของ Web page ให้สมบูรณ์ต่อไป แต่ทุกครั้งที่ต้องการเพิ่มเติม ต้องใช้คำสั่ง echo ทุกครั้ง หากนำ Here document มาใช้งาน จะช่วยให้แก้ไขได้สะดวกขึ้น ดังนี้ $ cat -n html2.sh 1 #!/bin/sh 2 # A script to produce an HTML file 3 4 TITLE="System information for $(hostname).$(hostname -d)" 5 RIGHT_NOW=$(date +"%x %r %Z") 6 TIME_STAMP="Update on $RIGHT_NOW by $USER" 7 8 cat < 10 11 12 $TITLE 13 14 15 16 17

$TITLE

18

$TIME_STAMP 19 20 21 EOF ตัวอย่างที่ 3 การใช้งาน Here document เพื่อให้ while loop วนอ่านไปทีละบรรทัด ------------------------------------------------------------------ โปรแกรม ex2-1 นับจำนวนบรรทัดที่ได้จากคำสั่ง ls -l โดยการส่งผลลัพธ์ของ ls -l ผ่าน pipe ไปยัง while loop ซึ่งจะทำการอ่านข้อมูลจาก pipe เข้ามาทีละบรรทัดเก็บในตัวแปร line ทำการเพิ่มค่าตัวนับ จากนั้นจึงแสดงจำนวนบรรทัดที่นับได้ $ cat -n ex2-1 1 #!/bin/bash 2 count=0 3 4 ls -l | while read line; do 5 count=$(($count+1)) 6 done 7 8 echo "count = $count" เมื่อให้โปรแกรมทำงาน ให้คำตอบเป็น count = 0 เสมอไม่ว่าในไดเรกทอรีจะมีจำนวนแฟ้มเท่าใด หรือเป็นโปรแกมที่ไม่สามารถทำงานได้จริง ตัวอย่างนี้ต้องการแสดงปัญหา สาเหตุ และการแก้ไข ตามประเด็นดังนี้ 1. while loop ตั้งแต่คำสำคัญ while จนถึง done ถือเป็นคำสั่งเดียว เขียนให้อยู่ในบรรทัดเดียวกันได้ดังนี้ while read line; do count=$((count + 1)); done สังเกตการใช้ semicolon ในคำสั่ง 2. เมื่อ while loop มีฐานะเป็นคำสั่งเดียว จึงสามารถนำมาใช้ใน pipeline ได้เป็น ls -l | while read line; do count=$((count + 1)); done 3. ผลลัพธ์เป็น count = 0 เนื่องจาก shell จะทำการสร้าง subshell สำหรับแต่ละคำสั่งใน pipeline while loop ที่อยู่ด้านขวาของ pipe ทำงานใน subshell ของตนเอง ซึ่งจะได้รับการถ่ายทอดตัวแปร count มาจาก shell เพื่อนำมาใช้ใน loop เมื่อทำงานเสร็จ subshell จะสลายตัวไป โดยไม่มีการส่งค่ากลับ ค่าของตัวแปร count ที่นำมาแสดงจึงเป้นค่าเดิมที่อยู่ในสิ่งแวดล้อมของ shell การแก้ปัญหาทำได้โดยการใช้ here document เข้าช่วยดังนี้ $ cat -n ex2-2 1 #!/bin/sh 2 count=0 3 4 while read line; do 5 count=$(($count+1)) 6 done < $OUTFILE 10 # เนื่องจากเป็นการทำงานตามคำสั่งในวงเล็บ ( ... ), shell จะสร้าง subshell เพิ่อทำงาน จึงไม่สามารถ 11 # เรียกใช้หรืออ้างอิงตัวแปรอื่นที่อยู่นอก subshell หรือตัวแปรที่อยู่นอกวงเล็บได้ 12 # การกำกับ limit string ของ here document ด้วยเครื่องหมายคำพูดเดี่ยว เป็นการป้องกันเชลล์ขยายอักขระพิเศษ 13 # ที่มีอยู่ใน here document เพื่อจะสามารถให้ส่งอักขระเหล่านั้นออกไปยังแฟ้มในฐานะเป็นอักขระธรรมดา 14 # ------------------------------------------------------------------------------------------- 15 ( 16 cat <<'EOF' # กำกับ limit string (EOF) เพื่อป้องกันเชลล์ขยายค่า 17 #!/bin/sh # บรรทัดที่ 17 - 26 เป็นเนื้อความของ here document 18 19 echo "This is a generated shell script." 20 a=7 21 b=3 22 c=$(($a + $b)) # ไม่มีการขยายความอักขระพิเศษ 23 echo "c = $c" 24 25 exit 0 26 EOF 27 ) > $OUTFILE # ส่งผลลัพธ์ไปยังแฟ้มที่กำหนดในตัวแปร $OUTFILE 28 # ------------------------------------------------------------------------------------------- 29 30 # หากสร้าง output file ได้สำเร็จ เพิ่มสิทธิการ execute ให้กับแฟ้ม และแจ้งให้ผู้ใช้ทราบ 31 # ไม่สามารถทำได้ เช่น ไม่มีสิทธิ w ในไดเรกทอรี แสดง error message 32 if [ -f "$OUTFILE" ]; then 33 echo "Generated file will be named: $OUTFILE" 34 chmod u+x $OUTFILE 35 else 36 echo "Problem in creating file: \"$OUTFILE\"" 37 fi 38 39 exit 0 เมื่อให้โปรแกรมทำงานจะได้แฟ้ม newscript ซึ่งมีรายละเอียดดังนี้ $ cat -n newscript 1 #!/bin/sh 2 3 echo "This is a generated shell script." 4 a=7 5 b=3 6 c=$(($a + $b)) 7 echo "c = $c" 8 9 exit 0 เทคนิดนี้สามารถใช้สร้างโปรแกรมภาษา C, Perl, Python, รวมทั้งโปรแกรมในภาษาอื่นๆ, นอกจากนี้ยังใช้สร้าง Makefile และแฟ้มในลักษณะเดียวกันนี้ได้อีก ตัวอย่างที่ 5 โปรแกรมสำหรับสร้าง shell script อีกโปรแกรมหนึ่ง- bundle ------------------------------------------------------------ โปรแกรมที่มีการประยุกต์ใช้งาน Here document ได้ดีเยี่ยมโปรแกรมหนึ่งคือ bundle ซึ่งเขียนขึ้นโดย Brian Kernighan และRob Pike สำหรับใช้ในการรวมแฟ้มข้อมูลแบบ text file หลายแฟ้มเข้าด้วยกันเป็นแฟ้มเดียว (package) เพื่อความสะดวกในการนำไปติดตั้งในระบบอื่น การทำงานของโปรแกรมคล้ายกับการทำงานของโปรแกรม tar (tape archive) ซึ่งใช้ในการรวมแฟ้มเพื่อใช้ในการสำรองข้อมูล bundle เป็น shell script ที่ทำหน้าที่สร้าง shell script อีกโปรแกรมหนึ่ง โดยนำข้อมูลจากแฟ้มที่ต้องการจะรวมเข้าด้วยกัน สร้างเป็น Here Document เมื่อนำแฟ้มนี้ไปทำงานในระบบอื่นที่ต้องการติดตั้งแฟ้ม จะทำการแยกแฟ้มที่รวมมาออกเป็นแฟ้มย่อยเช่นเดิม รายละเอียดของโปรแกรมเป็นดังนี้ $ cat -n bundle 1 #!/bin/bash 2 #bundle: group file into distribution package 3 echo '#!/bin/sh' 4 echo '# To unbundle, sh or bash this file' 5 for i; do 6 echo "echo $i" 7 echo "cat > $i << 'End of $i'" 8 cat $i 9 echo "End of $i" 10 done การทดลองใช้งานโปรแกรม bundle -------------------------- เมื่อพิจารณาโปรแกรม bundle จะเห็นว่าเป็นโปรแกรมที่สั้นใช้เฉพาะคำสั่งพื้นฐาน แต่เป็นโปรแกรม ที่ทำความเข้าใจจาก source code ได้ยากโปรแกรมหนึ่ง การเริ่มต้นทำความเข้าใจจึงควรต้องทดลองใช้งานโปรแกรมและทำความเข้าใจเป็นลำดับไป การเตรียมก่าร ---------- การทดลองใช้โปรแกรม bundle ต้องมีแฟ้มข้อมูลอย่างน้อยสองแฟ้ม ในที่นี้กำหนดให้เป็น file1 และ file2 ซึ่งมีข้อมูลดังนี้ $ cat file1 Readonly Shell Variables: $0 - Name of the calling program $n - Value of the nth command line argument $* - All of the command line arguments $@ - All of the command line arguments $ cat file2 $# - Count of the command line arguments $$ - PID number of the current process $! - PID number of the most recent background $? - Exit status of the last task that was executed ทำการเรียกใช้โปรแกรม bundle โดยมี file1 และ file2 เป็น argument และทำการเปลี่ยนทิศทางผลลัพธ์ที่ได้ไปยังแฟ้ม package ดังนี้ $ bundle file1 file2 > package จะได้แฟ้ม package ซึ่งเป็น shell script ที่มีข้อมูลในแฟ้ม file1 และ fille2 เป็น Here Document จากนั้นจึงทำการลบแฟ้ม file1 และ file2 และทำการตรวจสอบสิทธิในการใช้งานแฟ้ม combine โดยใช้คำสั่ง ls ดังนี้ $ rm file1 file2 $ ls –l package -rw------- 1 jira staff 505 Jul 10 09:30 package จะเห็นว่าเป็นแฟ้มที่เจ้าของแฟ้มยังไม่มีสิทธิในการ execute จึงไม่สามารถเรียกให้ทำงานได้โดยตรง แต่สามารถเรียกให้ทำงานได้โดยกำหนดให้ shell ปัจจุบันสร้าง subshell และ execute แฟ้ม package ใน subshell นั้น ซึ่งสามารถทำงานได้ไม่ว่าผู้ใช้จะมีสิทธิในการ execute แฟ้มนั้นหรือไม่ก็ตาม $ sh package หรือ $ bash package คำอธิบายโปรแกรม -------------- echo '#!/bin/sh' echo '# To unbundle, sh this file' เป็นการส่งข้อความที่กำหนดออกไปยัง standard output แต่เนื่องจากมีการทำ output redirection ไว้จากcommand line ดังนั้นข้อความนี้จึงเป็นข้อมูลในแฟ้ม package และเพื่อป้องกันเชลล์ดำเนินก่ารกับอักขระพิเศษที่อาจมีในข้อความจึงต้องกำกับด้วย single quote for i เป็นการกำหนดโครงสร้างการทำซ้ำแบบ for เนื่องจากไม่มีคำสำคัญ in จึงเป็นการกำหนดให้นำ command line argument มาเป็นรายการที่ต้องดำเนินการ เมื่อมีการเรียกใช้ bundle ดังนี้ $ bundle file1 file2 > package รายการที่ต้องดำเนินการของโครงสร้าง for คือ file1 file2 สำหรับแฟ้ม package ไม่อยู่ในรายการนี้ เพราะเป็นการเปลี่ยนทิศทางของโปรแกรม bundle echo "echo $i 1>&2" เป็นการส่งข้อความ "echo $i 1>&2" ซึ่งในการทำงานรอบแรกของการทำงานจะเป็น "echo file1 1&2" ไปยังแฟ้ม package echo "cat > $i << 'End of $i'" เป็นการส่งข้อความ cat > file1 << End of file1 ไปยังแฟ้ม package ซึ่งเมื่อทำงาน คำสั่ง cat > $i << 'End of $i' เป็นการกำหนดให้ cat ทำการ redirect ข้อมูลจาก here document ที่มีสายอักขระ End of <ชื่อแฟ้มในตัวแปร $i> เป็น limit string เริ่มต้น cat $i เป็นการส่งข้อมูลในแฟ้มที่กำหนดด้วยตัวแปร $i ทาง standard output และจะถูกเปลี่ยนทิศทางต่อไปยังแฟ้ม package เพื่อทำหน้าที่เป็น Here document echo "End of $i" เป็นการส่งข้อความ End of file1 เป็นเครื่องหมายปิดท้าย Here document ในแฟ้ม package แฟ้มที่เกิดขึ้นจากการทำงานของโปรแกรม bundle ------------------------------------- แฟ้มที่เกิดขึ้นจากการทำงานของโปรแกรม bundle ตามตัวอย่างในหัวข้อ “การทดลองใช้งานโปรแกรม bundle” มีรายละเอียดดังนี้ $ cat package #!/bin/sh # To unbundle, sh this file echo file1 1>&2 # แสดงชื่อแฟ้ม file1 ออกทางจอภาพ cat > file1 << 'End of file1' # cat รับข้อมูลจาก Here document ส่งผลลัพธ์ Readonly Shell Variables: # ไปยังแฟ้ม file1 โดยมีข้อความ End of file1 $0 - Name of the calling program # เป็นเครื่องหมายกำหนดจุดเริ่มต้น $n - Value of the nth command line argument $* - All of the command line arguments $@ - All of the command line arguments End of file1 # จุดสิ้นสุดของ Here document ชุดที่ 1 echo file2 1>&2 cat > file2 << 'End of file2' # cat รับข้อมูลจาก Here document ส่งผลลัพธ์ $# - Count of the command line arguments # ไปยังแฟ้ม file2 โดยมีข้อความ End of file2 $$ - PID number of the current process # เป็นเครื่องหมายกำหนดจุดเริ่มต้น $! - PID number of the most recent background $? - Exit status of the last task that was executed End of file2 #จุดสิ้นสุดของ Here document ชุดที่ 2 แฟ้ม package เมื่อทำงานสามารถสร้างแฟ้ม file1 และ file2 ที่มีข้อมูลเหมือนเดิมได้อย่างไร เป็นคำถามที่ทิ้งไว้ให้ผู้อ่านต้องหาคำอธิบายด้วยตนเอง การใช้ here document ช่วยในการหาที่ผิดใน script - Anonymous here document ------------------------------------------------------------------- เราสามารถใช้เครื่องหมาย colon (:) แทนคำสั่งสำหรับรับข้อมูลจาก here document ได้ เรียกว่า Anonymous here document สำหรับใช้ในการทดสอบว่ามีการกำหนดค่าให้กับตัวแปรที่สนใจหรือไม่ เช่น $ cat -n dummy 1 #!/bin/sh 2 3 : <&2 # ส่ง standard output ออกทาง standard error 5 cat <<-EOT # ให้ cat อ่านข้อมูลเข้าจาก here document 6 script requires at least one parameter. 7 EOT 8 exit 1 # เลิกการทำงานของโพรเซส 9 } 10 # โปรแกรมหลัก 11 if [ $# -eq 0 ]; then # ไม่มี command line argument เรัยกใช้งานฟังก์ชัน usage 12 usage 13 fi 14 echo "argument(s) = $@" หลักและวิธีการที่ใช้ --------------- 1. บรรทัดที่ 3 - 9 เป็นการประกาศฟังก์ชัน usage() ซึ่งทำหน้าที่แสดง error mesage และเลิกทำงาน ฟังก์ชันใน shell script ต้องมีการการประกาศก่อนการใช้งาน โดยมีรูปแบบดังนี้ รูปแบบตามมาตรฐาน POSIX bash ใช้เพิ่มได้อีกรูปแบบหนึ่งคือ --------------------- ---------------------- ชื่อฟังก์ชัน() { function ชื่อฟังก์ชัน() { คำสั่ง คำสั่ง } } 2. บรรทัดที่ 5 - 7 แสดงการใช้งาน here document เป็นข้อมูลเข้าของคำสั่ง cat 3. บางคนเชื่อว่าการเขียนโปรแกรมให้อ่านเข้าใจง่าย เป็นงานของคน "อ่อนหัด" คนที่เก่งจริงๆ ต้องใช้วิธีการที่ซับซ้อน, พิสดาร, ยากต่อการทำความเข้าใจ ยากที่จะคาดเดา โปรแกรมตัวอย่างนี้ คือโปรแกรมในแนวคิดนั้น ทั้งการกำหนด error message ที่ไม่สื่อความหมาย, การใช้คำสั่ง exec กับ standard input และ standard output โดยไม่จำเป็น, และใช้ Here document กำหนดสายอักขระ เทคนิคดังกล่าวนี้ไม่มีความจำเป็นต้องใช้เลย ขอให้พิจาณาโปรแกรมต่อไปนี้ซึ่งมีการทำงานเทียบเท่ากัน แต่ง่ายและตรงไปตรงมา ชัดเจน เข้าใจง่ายตามหลักการ KISS (Keep It Simple Stupid!) $ cat -n ex2 1 #!/bin/sh 2 if [ $# -eq 0 ]; then 3 echo "script requires at least one parameter." 1>&2 4 exit 1 5 fi 6 echo "argument(s) = $@" ข้อคิด ------ 1. ถ้าคุณเป็นผู้ใช้ error message แบบนี้ชัดเจนดีพอหรือยัง หากยังควรปรับปรุงแก้ไขอย่างไร จึงจะสื่อความหมายได้ชัดเจน 2. จากโปรแกรมตัวอย่างทั้งสองแบบ คุณอยากเป็นคนเขียนโปรแกรมแบบไหน? ---------------------------------------------------------------------------------------------------- ตอนที่ ๒: การใช้งานแฟ้ม -------------------- เนื่องจากหน่วยความจำหลักของระบบคอมพิวเตอร์ซึ่งสร้างจาก Dynamic ram เป็นหน่วยความจำชนิด Volatile กล่าวคือจะเก็บข้อมูลไว้ได้เฉพาะในขณะที่มีไฟเลี้ยงอยู่เท่านั้น เมื่อไฟดับหรือปิดเครื่อง ข้อมูลที่เก็บไว้จะสูญหายไป ดังนั้นเมื่อต้องการเก็บข้อมูลใดไว้เป็นการถาวร จึงต้องจัดเก็บเป็น "แฟ้ม" ในหน่วยความจำรอง (หรือหน่วยเก็บช้อมูลความจุสูง - Mass storage) ด้วยเหตุนี้โปรแกรมส่วนใหญ่จึงต้องสามารถ "เข้าถึง" (หมายถึง อ่าน/เขียน) แฟ้มได้ นอกจากแฟ้มที่ใช้เก็บข้อมูลจริงแล้ว ระบบปฏิบัติการ Unix ยังกำหนดให้อุปกรณ์เป็นแฟ้มด้วย เรียกว่าแฟ้มแทนอุปกรณ์ (device file) ซึ่งกำหนดให้อยู่ใน /dev การ "เข้าถึง" แฟ้มใระดับของ shell มีขั้นตอน โดยทั่วไปดังนี้ ก. เปิด (open) แฟ้ม ผู้ใช้ต้องระบุเส้นทางและชื่อแฟ้ม, file descriptor ที่ต้องการ, และวิธีการทำงานกับแฟ้ม เรียกว่า mode ได้แก่ < เปิดแฟ้มเพื่ออ่านอย่างเดียว (read only หรืออ่าน) > เปิดแฟ้มเพื่อเขียนอย่างเดียว (write only หรือเขียนทับ) >> เปิดแฟ้มเพื่อเขียนข้อมูลต่อที่ท้ายแฟ้ม (append หรือเขียนต่อ) <> เปิดแฟ้มเพื่ออ่านและเขียน (read-write) การเปิดแฟ้มทำได้โดยใช้คำสั่ง exec ของ shell หากเปิดแฟ้มได้สำเร็จจะได้ค่า exit status เป็น 0, และสามารถนำ file descriptor ไปใช้นการทำงานกับแฟ้มนั้น ข. การดำเนินการกับแฟ้ม ทำได้สองแบบคือ อ่านข้อมูลจากแฟ้มนั้นเรียกว่า อ่าน (read) และส่งข้อมูลไปเบันทึกในแฟ้ม เรียกว่า เขียน (write) การดำเนินการดังกล่าวทำผ่าน file descriptor ค. เมื่อทำงานกับแฟ้มเสร็จแล้ว ต้องปืดแฟ้ม (close) โดยดำเนินการกับ file descriptor หมายเหตุ ------- 1. การสร้างและการเปิดแฟ้มสำหรับการเขียน (write หรืแ append) ส่วนใหญ่มักจะรวมเป็นกระบวนการเดียวกัน กล่าวคือเมื่อระบบได้รับคำร้องขอเปิดแฟ้มสำหรับการเขียน หากแฟ้มที่กำหนดไม่มีอยู่ระบบจะสร้างและเปิดแฟ้มนั้นให้ หากมีแฟ้มนั้นอยู่ แล้วและเเปิดสำหรับการเขียน ระบบจะกำจัดข้อมูลในแฟ้มนั้นทิ้งไปทั้งหมด ข้อมูลใหม่จะเริ่มเขียนที่ต้นแฟ้มเสมอ ในกรณีที่ เป็นการเปิดแฟ้มเพื่ออ่าน แฟ้มนั้นจะต้องมีอยู่จริง มิฉะนั้นจะเกิด error 2. เมื่อระบบสร้างโพรเซส จะทำการเปิดแฟ้มไว้ให้โดยอัตโนมัติสามแฟ้มคือ ก. แฟ้มแทนแป้นพิมพ์สำหรับข้อมูลเข้ามาตรฐาน (standard input) สำหรับอ่าน (read) มี file descriptor = 0 ข. แฟ้มแทนจอภาพสำหรับข้อมูลออกมาตรฐาน (standard output) สำหรับเขียน (write) มี file descriptor = 1 ค. แฟ้มแทนจอภาพสำหรับแสดงข้อความผิดพลาดมาตรฐาน (standard error) สำหรับเขียน (write) มี file descriptor = 2 การเปิดแฟ้ม --------- การเปิดแฟ้มที่กำหนดโดยใช้ file descriptor หรือเรียกอีกอย่างหนึ่งว่าเป็นการกำหนดให้ file descriptor "ชี้" ไปยังแฟ้มที่ระบุทำได้โดยใช้คำสั่ง exec ของ shell ซึ่งมีรูปแบบทั่วไปดังนี้ $ exec "ile-descriptor" "ภาวะการทำงาน" "ชื่อเส้นทางและแฟ้ม" เมื่อ file descriptor มีค่าตั้งแค่ 3 เป็นต้นไป และภาวะการทำงาน (mode) ของแฟ้มเป็นไปตามที่กำหนดในย่อหน้าก่อน เช่น $ exec 3> out1 # เปิดแฟ้ม out1 สำหรับการเขียนโดยกำหนดให้มี file descriptor เป็น 3 # หรือ กำหนดให้ file descriptor 3 ชี้ไปยังแฟ้ม out1 $ exec 4< in1 # เปิดแฟ้ม in1 สำหรับอ่าน โดยกำหนดให้มี file descriptor เป็น 4 $ exec 5<> tmp.log # เปิดแฟ้ม tmp.log สหรับอ่านและเขียน โดยมี file descriptor เป็น 5 $ exec 6>> other # เปืดแฟ้ม other สำหรับการ "เขียนต่อ" (append) โดยมี file descriptor เป็น 6 การปืดแฟ้ม --------- เมื่อหมดความจำเป็นในการใช้งานแล้ว ควรต้องทำการปิดแฟ้ม โดยใช้คำสั่ง exec ของ shell เช่นเดียวกัน โดยต้องกำหนดภาวะการทำงานของแฟ้มให้สมนัยกับตอนเปิดด้วย เช่น $ exec 3>&- # ปิดแฟ้มที่มี file descriptor = 3 สำหรับเขียน (>) $ exec 4<&- # ปิดแฟ้มที่มี file descriptor = 4, สำหรับอ่าน (<) $ exec 5<>&- # ปิดแฟ้มที่มี file descriptor = 5, สำหรับอ่านและเขียน (<>) $ exec 6>>&- # ปิดแฟ้มที่มี file descriptor = 6, สำหรับเขียนทับ (>>) การเขียนข้อมูลลงแฟ้ม ----------------- การเขียนข้อมูลลงแฟ้มใน shell script ทำได้โดยการเรียกใช้คำสั่ง หรือโปรแกรมอรรถประโยชน์ เช่น echo และ cat แต่อย่างไรก็ดีเนื่องจากคำสั่งเหล่านี้ออกแบบมาให่ส่งผลลัพธ์ออกทาง standard output เมื่อจะนำมาใช้กับแฟ้มจึงต้องอาศัย การรเปลี่ยนทิศทางข้อมูลออก (output redirection) ช่วยในการทำงาน ดังตัวอย่างต่อไปนี้ $ cat -n ex3 1 #!/bin/sh 2 3 exec 6> tmpfile 4 exec 1>&6 5 6 echo "a test of exec in open a file" 7 echo "the quick brown fox jumps over the lazy dog" 8 9 exec 6>&- อธิบายการทำงานของโปรแกรม 3 exec 6> tmpfile กำหนดให้เปิดแฟ้ม tmpfile สำหรับเขียนอย่างเดียว (เขียนทับ) และกำหนดให้มี file descriptor เป็น 6 4 exec 1>&6 กำหนดให้ standard output (file descriptor = 1) ส่งผลลัพธ์ไปยัง file descriptor 6 นับตั้งแต่จุดนี้เป็นต้นไปวิธีการนี้เรียกว่าการ duplicate file descriptor ซึ่งเป็นการกำหนดให้ file descriptor หนึ่ง ชี้ไปยังแฟ้มที่เปิดโดย file descriptor อื่นได้ ในตัวอย่างนี้เป็นการกำหนดให้ file descriptor = 1 (standard output) ชี้ไปยังแฟ้มที่เปิดโดย file descriptor = 6 หรือ 1>&6 บรรทัดที่ 6 - 7 เป็นการใช้ echo บันทึกข้อความลงในแฟ้มที่ชี้ด้วย file descriptor = 6 ในที่นี้คือแฟ้ม tmpfile ที่ทำเช่นนี้ได้เพราะมีการกำหนดวิธีการทำงานไว้แล้วในบรรทัดที่ 4 9 exec 6>&- กำหนดให้ปิดแฟ้มที่ชี้โดย file descriptor 6 หากตัดคำสั่งบรรทัดที่ 4 ออก คือไม่มีการ dupliacte file fdescriptor สามารถกำหนดได้โดยเพิ่ม 1>&6 ที่ท้ายบรรทัดของคำสั่ง echo แต่ละบรรทัดได้ดังนี้ $ cat -n ex3 1 #!/bin/sh 2 3 exec 6> tmpfile 4 5 6 echo "a test of exec in open a file" 1>&6 7 echo "the quick brown fox jumps over the lazy dog" 1>&6 8 9 exec 6>&- การอ่านข้อมูลจากแฟ้ม - คำสั่ง read ----------------------------- การอ่านข้อมูลจากแฟ้มทำได้โดยการเรียกใช้คำสั่ง read ของ shell มีรายละเอียดดังนี้ read - อ่านข้อมูลหนึ่งบรรทัด จาก standard input หรือ file descriptor ที่ระบุ เก็บในตัวแปร (การอ่านข้อมูลหนึ่งบรรทัด คือการอ่านอักขระทีละตัวจนกว่าจะพบ newline) รูปแบบการใช้งาน ------------- read [-option] ชื่อตัวแปร ในกรณีที่ผู้ใช้ไม่ได้กำหนดตัวแปรสำหรับรับค่า ระบบจะเก็บค่าที่ read อ่านไว้ในตัวแปร $REPLY ซึ่งเป็นตัวแปรของ bash option ที่สำคัญ ------------- -r - raw input หรือรับข้อมูลดิบ ไม่ขยายความ escape sequence ด้วย \ เช่น ไม่ขยาย \n ในข้อมูลเข้า เป็น newline และไม่ขยาย \ เป็นรหัสขึ้นบรรทัดใหม่ -p prompt - แสดงข้อความพร้อมรับที่กำหนด -u fd - อ่านจาก file descriptor ที่กำหนดด้วย fd แทนที่จะเป็น standard input option พิเศษเฉพาะของ bash ------------------------ -s - secure input ไม่แสดงอักขระที่ผู้ใช้พิมพ์บนจอภาพ เช่น การป้อนรหัสผ่าน -t timeout - เลิกรับข้อมูลหลังหมดเวลา timeout (หน่วยเป็นวินาที) ที่กำหนด ซึ่งจะคืนค่า exit status > 128 option เหล่านี้สามารถใช้ผสมกันได้ เช่น $ read -p "Password: " -s line แสดงข้อความพร้อมรับ "Password : โดยไม่แสดงอักขระที่ผู้ใช้พิมพ์ รับข้อมูลเก็บในตัวแปร line การอ่านข้อมูลจากแฟ้มทีละบรรทัด ------------------------- การอ่านข้อมูลจากแฟ้มทีละบรรทัดจนกว่าจะหมดแฟ้ม ทำได้โดยการวนอ่านข้อมูลซ้ำโดยใช้โครงสร้าง loop เช่น $ while read -r line; do # ดำเนินการกับตัวแปร $line done < data.in # ข้อมูลเข้ามาจากแฟ้ม data.in ตัวอย่างนี้ใช้วิธีการ redirect ข้อมูลเข้ามาจากแฟ้มที่กำหนด ในรูปแบบ while read ... ; do ... ; done < ชื่อแฟ้มข้อมูลเข้า เมื่อ read อ่านข้อมูลในแฟ้มหมดแล้ว (คือพบ end-of-file), read จะคืน exit status เป็นจำนวนบวก ทำให้เงื่อนไขของ while เป็นเท็จ จึงออกจาก loop ในขณะอ่านข้อมูล read จะทำการกำจัดอักขระทุกตัวที่กำหนดไว้ในตัวแปร IFS (Internal field separator) ซึ่งค่าโดยปริยายได้แก่ วรรค (space), ย่อหน้า (tab), และ ขึ้นบรรทัดใหม่ (newline) ซึ่งเมื่อเลือกใช้ option และวิธีการที่เหมาะสมสามารถแยกข้อมูลในบรรทัดออกเป็นคำได้ และถึงแม้จะไม่ได้ใช้ความสามารถนี้แต่ค่าของตัวแปร IFS จะทำให้ read กำจัดอักขระเหล่านี้ทั้งที่อยู่หน้าและตามหลังข้อมูล หากต้องการรักษาอักขระเหล่านี้ไว้ต้องทำการล้างค่าตัวแปร IFS ดังนี้ $ while IFS= read -r line; do # IFS= , กำหนดให้ IFS เป็น null # ดำเนินการกับตัวแปร $line done < file นอกจากจะใช้วิธีการ Redirection แล้ว ยังสามารถใช้งาน pipeline ได้ ดังตัวอย่างต่อไปนี้ $ cat -n ex6 1 #!/bin/bash 2 3 exec 5> foo # เปิดแฟ้ม foo สำหรับเขียน โดยกำหนดให้มี file descriptor เป็น 5 4 exec 6< bar # เปิดแฟ้ม bar สำหรับอ่าน โดยกำหนดให้มี file descriptor เป็น 6 5 6 cat <&6 | # กำหนดให้ cat อ่านข้อมูลจากแฟ้มที่ชี้โดย file descriptor 6 7 # ในโปรแกรมนี้คือแฟ้ม bar และส่งผลลัพธ์ผ่าน pipe ไปยังโครงสร้าง while 8 while read a; do # ซึ่งจะวนอ่านข้อมูลจากแฟ้มไปทีละบรรทัด 9 echo $a >&5 # และเขียนลงในแฟ้มที่ชี้โดย file description 5 ซึ่งได้แก่แฟ้ม foo 10 done 11 12 exec 5>&- # ปิดแฟ้มที่ชั้โดย file descriptor 5 และ 6 13 exec 6<&- ตัวอย่างนี้ใช้การเปิดแฟ้มที่ต้องการไว้ก่อน จากนั้นจึงกำหนดให้ cat อ่านข้อมูลจาก file descriptor ของแฟ้มนั้น ส่งผ่าน pipe ไปให้โครงสร้าง while ซึ่งเป็นเสมือนคำสั่งเดียว หรือเป็น pipeline ในรูปแบบ cat <&6 | while read ...; do ...; done ---------------------------------------------------------------------------------------------------- ตอนที่ ๓: ตัวอย่างโครงงาน ----------------------- บทนำ ---- วิธีการที่ดีในการทำความเข้าใจ shell script แบบหนึ่งคือ กำหนดปัญหาที่มีขนาดพอสมควร ทำความเข้าใจ วิเคราะห์ ร่างแนวความคิด และเขียนโปรแกรม และด้วยเหตุที่ shell เป็นตัวแปลภาษาแบบ Interpreter จึงสามารถพัฒนาโปรแกรมไปทีละส่วน ทดสอบ ทบทวนและตั้งปัญหาถามตัวเองว่ายังมีจุดใดควรปรับปรุงเปลี่ยนแปลง หรือสามารถปรับปรุงให้ดีขึ้นได้ ปัญหาเหล่านี้ก่อนเริ่มเขียนโปรแกรมจะมองไม่เห็น แต่จะเริ่มชัดเจนขึ้นเมื่อลงมือเขียน และจะชัดเจนมากเมื่อเขียนเสร็จ ทดสอบและทบทวน โปรแกรมต่อไปนี้แสดงแนวคิด การวิเคราะห์ โปรแกรม และคำถามสำหรับให้ผู้อ่านทบทวนในแต่ละตอน อาจไม่ใช่ตัวอย่างที่ดีนักแต่หวังว่าจะช่วยแสดงแนวทางบางอย่างในการเขียนโปรแกรมได้ โปรแกรมในโครงงานนี้ทำหน้าที่เข้ารหัสแฟ้มข้อมูลแบบ text file โดยใช้วิธีการที่ดัดแปลงมาจาก Caesar cipher ที่เคยใช้งานมาแล้ว โปรแกรมจะทำการอ่านข้อมูลจากแฟ้มครั้งละบรรทัด บรรทัดคู่และบรรทัดคี่จะเข้ารหัสโดยใช้กุญแจรหัสต่างกัน เพื่อให้คาดเดาข้อมูลจริงได้ยาก โปรแกรมรับชื่อแฟ้มที่ต้องการเข้ารหัสผ่าน Command line argument และส่งผลลัพธ์ที่ได้ออกทาง standard output เพื่อให้ผู้ใช้ตัดสินใจดำเนินการต่อเอง เช่น เปลี่ยนทิศทางข้อมูลออกไปยังแฟ้ม หรือส่งต่อไปยังโปรแกรมอื่นผ่าน pipeline ลักษณะเช่นนี้ทำให้โปรแกรมมีความยืดหยุ่น สามารถใช้งานได้หลายรูปแบบ ทุกครั้งที่ถูกเรียกใช้งาน โปรแกรมจะทำการบันทึกเก็บเหตุการณ์ และรายละเอียดต่างๆ ของการใช้งานไว้ใน log file ซึ่งจะเป็นประโยชน์ในการตรวจสอบ ติดตาม และบริหารจัดการ ข้อมูลที่ควรต้องเก็บจริงขึ้นอยู่กับลักษณะการใช้งานของโปรแกรม และ ระบบ เนื่องจากโครงงานนี้เป็นเพียงตัวอย่างทั่วไป จึงกำหนดให้เก็บ ชื่อผู้ใช้, IP address ของเครื่องที่เป็น client, วันเวลาที่เข้าใช้งาน และ คำสั่งและ argument ที่เรียกใช้ ทั้งนี้เพื่อให้สามารถใช้ log file เดียวกันสำหรับหลายโปรแกรมได้ โปรแกรมที่ดีควรต้องมีการตรวจจับความผิดพลาดทุกอย่างที่อาจเกิดขึ้น และหาทางป้องกันไว้ก่อน เพื่อไม่ให้โปรแกรมล่มกลางคัน โปรแกรมนี้ออกแบบให้มีการตรวจจับและป้องกันตามสมควร และทำก่อนที่จะมีการบันทึกข้อมูลลงใน log file กล่าวอีกนัยหนึ่งคือโปรแกรมจะมีการบันทึกข้อมูลลงใน log file เฉพาะเมื่อมีการเรียกใช้งานที่สมเหตุสมผลเท่านั้น โปรแกรมในโครงงานนี้จึงแบ่งออกเป็น 3 ส่วน คือ (ก). การเข้ารหัส ซึ่งเป็นส่วนที่สำคัญที่สุดของโปรแกรม, (ข). การจัดการกับ log file, และ (ค). การจัดการกับความผิดพลาดที่อาจเกิดขึ้น โดยจะแสดงการวิเคราะห์ เขียนโปรแกรม และการทบทวนเพื่อปรับปรุงแก้ไขในแต่ละตอน เมื่อครบทุกตอนแล้ว จึงจะรวมเข้าด้วยกันเป็นโปรแกรมเดียว เนื้อหาในแต่ละตอนมีรายละเอียดค่อนข้างมาก สร้างไว้สำหรับช่วยให้ผู้เริ่มต้นได้เห็นแนวทางการพัฒนาโปรแกรมแบบ script ผู้ที่เข้าใจแล้วอาจอ่านผ่านๆ หรือข้ามเนื้อบางตอนได้ หวังเป็นอย่างยิ่งว่าผู้อ่านคงพอได้ประโยชน์บ้าง (ก). การเข้ารหัสข้อมูล ------------------ กำหนดให้โปรแกรมอ่านข้อมูลจากแฟ้มข้อมูลเข้า ซึ่งเป็นแฟ้มแบบ text file ทำการเข้ารหัส และเขียนข้อมูลที่เข้ารหัสแล้วลงในแฟ้ม output file วิธีการเข้ารหัสที่ใช้ดัดแปลงมาจากการเข้ารหัสแบบ Caesar cipher โดยผู้ใช้สามารถกำหนด "กุญแจรหัส" ที่ต้องการได้ เช่น กุญแจรหัสเป็น "COMPUTER" ให้ทำการเขียนกุญแจรหัสก่อน ตามด้วยอักษรที่เหลือไปตามลำดับเป็นสองแถวเท่ากัน คือ C O M P U T E R A B D F G H I J K L N Q S V W X Y Z การเข้ารหัสและการถอดรหัสใช้วิธีเดียวกับ Caesar cipher คือ เปลี่ยนแทนอักษรแถวบนด้วยอักษรแถวล่าง และเปลี่ยนแทนอักษรแถวล่างด้วยแถวบน เพื่อให้บุคคลอื่นคาดเดารหัสได้ยากขึ้น จึงกำหนดให้ใช้กุญแจรหัสสองชุด ชุดแรกสำหรับเข้ารหัส ข้อความบรรทัดคู่ และอีกชุดหนึ่งสำหรับบรรทัดคี่ ชุดแรกกำหนดกุญแจรหัสเป็น "COMPUTER" ดังกล่าวแล้ว อีกชุดหนึ่งเป็น "SCIENCE" ซึ่งเป็นกุญแจรหัสมีอักษรซ้ำกัน จึงใช้วิธีการตัดตัวอักษรตัวหลังที่ซ้ำกับตัวหน้าออก เหลือใช้งานเป็นกุญแจรหัส เฉพาะ "SCIEN" จากนั้นจึงสร้างรหัสเปลี่ยนแทนดังนี้ S C I E N A B D F G H J K L M O P Q R T U V W X Y Z จะได้ "เซตของอักขระ" สำหรับใช้กับคำสั่ง tr (transliterate) สำหรับกุญแจรหัสทั้งสองแบบเป็น tr 'COMPUTERABDFGHIJKLNQSVWXYZ' 'HIJKLNQSVWXYZCOMPUTERABDFG' tr 'SCIENABDFGHJKLMOPQRTUVWXYZ' 'LMOPQRTUVWXYZSCIENABDFGHJK' แต่เนื่องจากแฟ้มข้อมูลเข้าอาจประกอบด้วยอักษรตัวเล็ก อักษรตัวใหญ่ หรือผสมกัน ดังนั้นก่อนเข้ารหัสจึงต้องแปลงข้อมูลในแฟ้มให้เป็นอักษรตัวใหญ่เสียก่อน โดยใช้คำสั่ง tr เช่นเดียวกัน คือ tr 'a-z' 'A-Z' # กำหนดด้วยพิสัย หรือ tr '[:lower:]' '[:upper]' # กำหนดด้วย character class ตามมาตรฐาน POSIX อย่างไรก็ดีการทำงานของโปรแกรมต้องไม่ทำให้แฟ้มข้อมูลเข้าเปลี่ยนแปลงไป จึงต้องเขียนข้อมูลที่แปลงแล้วลงในแฟ้มชั่วคราว เนื่องจาก Unix ระบบปฏิบัติการชนิด Multitask ดังนั้นจึงมีความเป็นไปได้ที่จะมีผู้ใช้หลายคน เรียกใช้โปรแกรมเดียวกัน และใช้ในไดเรกทอรีเดียวกัน ชื่อแฟ้มชั่วคราวจึงต้องมีเอกลักษณ์เฉพาะตัวไม่ซ้ำกัน วิธีการที่นิยมคือใช้หมายเลขโพรเซสเป็นชื่อแฟ้ม หรือส่วนหนึ่งของชื่อแฟ้ม เพราะระบบปฏิบัติการรับประกันว่าหมายเลขของโพรเซสที่ทำงานในขณะดียวกันจะไม่ซ้ำกัน เลย แฟ้มชั่วคราวในโครงการนี้กำหนดให้เป็นหมายเลขโพรเซสปัจจุบัน (เก็บอยู่ในตัวแปร $$) และมีส่วนขยายเป็น .tmp โดยโปรแกรเป็นผู้สร้างและลบเมื่อใช้งานเสร็จแล้ว การอ่านข้อมูลจากแฟ้มที่ต้องการเข้ารหัสทำโดยการเปลี่ยนทิศทางข้อมูลเข้า และเปลี่ยนทิศทางข้อมูลออก (ข้อมูลที่แปลงเป็นอักษรตัวใหญ่แล้ว) ไปยังแฟ้มชั่วคราว เป็นดังนี้ tr 'a-z' 'A-Z' < ชื่อแฟ้มที่ต้องการเข้ารหัส > $$.tmp จากนั้นจึงทำการเปิดแฟ้มชั่วคราว โดยกำหนดให้มี file descriptor เป็น 3 และกำหนดให้อ่านข้อมูลในแบบ raw mode ครั้งละบรรทัด เก็บในตัวแปร line เพื่อเข้ารหัสต่อไป โดยมีโครงการทำงานเป็น exec 3< $$.tmp while read -u3 -r line; do ... เข้ารหัสข้อความในบรรทัดคู่และคี่ตามที่กำหนด ... done การตรวจสอบบรรทัดคู่และคี่ --------------------- กำหนดให้ตัวแปร n เก็บเลขบรรทัดของข้อมูล และมีค่าเริ่มต้นเป็น 0 เมื่อทำการเข้ารหัสเสร็จในแต่ละบรรทัด จะทำการเพิ่มค่า n ขึ้นครั้งละ 1 และทำการ mod ด้วย 2 หรือ n = (n + 1) mod 2 วิธีการนี้จะทำให้ n มีค่าสลับระหว่าง 0 และ 1 หรือสลับค่าระหว่างเลขคู่และเลขคี่ การเลือกเข้ารหัสของแต่ละบรรทัดทำโดยใช้โครงสร้าง if หรือ case เช่น while read -u3 -r line; do case $n in 0) echo "$line" | tr 'COMPUTERABDFGHIJKLNQSVWXYZ' 'HIJKLNQSVWXYZCOMPUTERABDFG' ;; 1) echo "$line" |tr 'SCIENABDFGHJKLMOPQRTUVWXYZ' 'LMOPQRTUVWXYZSCIENABDFGHJK' esac n = $(((n + 1) % 2)) done หมายเหตุ ------- 1. เนื่องจากคำสั่ง tr กำหนดให้ข้อมูลเข้ามาจาก standard input และกำหนดให้ส่งผลลัพธ์ออกทาง standard output เท่านั้น, input และ output รูปแบบอื่น "จำเป็น" ต้องทำผ่าน i/o redirection หรือ pipeline 2. ผลลัพธ์ของ tr ส่งออกทาง standard output ซึ่งผุ้ใช้สามารถเปลี่ยนทิศทางไปยังแฟ้มที่ต้องการได้ จึงไม่จำเป็นต้องเปิดแฟ้มเพื่อเขียนผลลัพธ์ เมื่อเข้ารหัสข้อมูลเสร็จแล้ว ทำการปิดแฟ้มชั่วคราว และลบออก ด้วยคำสั่ง exec 3<&- rm $$.tmp เมื่อกำหนดให้ชื่อแฟ้มที่ต้องการเข้ารหัสเป็น command line argument โปรแกรมเฉพาะส่วนของการเข้ารหัสตามแนวคิดดัง กล่าวเป็นดังนี้ $ cat -n encode 1 #!/bin/bash 2 3 tr 'a-z' 'A-Z' < $1 > $$.tmp 4 exec 3< $$.tmp 5 6 n=0 7 while read -u3 -r line; do 8 case "$n" in 9 0) 10 echo "$line" | tr 'COMPUTERABDFGHIJKLNQSVWXYZ' \ 11 'HIJKLNQSVWXYZCOMPUTERABDFG' 12 ;; 13 14 1) 15 echo "$line" | tr 'SCIENABDFGHJKLMOPQRTUVWXYZ' \ 16 'LMOPQRTUVWXYZSCIENABDFGHJK' 17 ;; 18 esac 19 n=$(((n + 1) % 2)) 20 done 21 22 exec 3<&- 23 rm $$.tmp ขอให้สังเกตการใช้ \ ที่ท้ายบรรทัดที่ 10 และ 15 เพื่อทำให้บรรทัดที่ 10 - 11, และบรรทัดที่ 15 - 16 เป็นบรรทัดเดียวกันตามลำดับ เครื่องหมาย \ ที่ท้ายบรรทัด เรียกว่า line-continuation เป็น escape sequence ของ \ สำหรับยกเลิกการสร้างรหัสขึ้นบรรทัดใหม่จากการกดแป้น ------------------------------------------------ (ข). การดำเนินการกับ log file --------------------------- การใช้ file descriptor จะมีประโยชน์ในกรณีที่ต้องการเขียนข้อมูลลงในแฟ้มหลายแฟ้ม เช่น script ที่ต้องการเขียนผลลัพธ์เก็บใน output file, เก็บเหตุการณ์ และรายละเอียดต่างๆ ของการทำงานใน log file, และต้องการแสดง error message ที่อาจเกิดขึ้นด้วยทาง standard error นั่นคือ script นี้ต้องการช่องทางสำหรับส่งข้อมูลออก 3 ช่องทาง สำหรับ (ก). บันทึกผลลัพธ์ที่ได้จากการทำงาน - output file (ข). บันทึกเหตุการณ์ และรายละเอียดต่าง ๆ ในการทำงาน - log file (ค). แสดงความผิดพลาด - standard error แต่ละโพรเซสมีช่องทางแสดงผลโดยปริยายเพียงสองช่องทางคือ standard output และ standard error จึงต้องมีการดำเนินการกับแฟ้ม log file ด้วยตนเอง เนื่องจาก log file เป็นแฟ้มที่มีการบันทึกต่อเนื่องไปทุกครั้งที่มีการใช้โปรแกรม จึงควรบันทึกมีเฉพาะข้อมูลที่จำเป็นและเป็นประโยชน์สำหรับการตรวจสอบการทำงานของโปรแกรมนั้น ถ้าไม่รู้ว่าจะเริ่มต้นอย่างไรควรศึกษาว่าโปรแกรมอื่นทำอย่างไร หรือมีรูปแบบโดยทั่วไปอย่างไร เช่น ค้นจากระบบ WWW โดยใช้ "log file format" เป็นคำค้น (คำสำคัญสำหรับค้น) ตัวอย่างการค้นเช่น Common Log Format จาก https://en.wikipedia.org/wiki/Common_Log_Format เป็น log file ตามรูปแบบของ NCSA (National Center for Supercomputing - ศูนย์การประยุกต์ใช้ซูเปอร์คอมพิวเตอร์ แห่งชาติ) หรือเรียกว่า Common Log Format สำหรับผู้ให้บริการ httpd เช่น -------------------------------------------------------------------------------------------------------- ชื่อ client (RFC 1413) ชื่อผู้ใช้ บริการที่ client ร้องขอ จำนวนไบต์ที่ส่งให้ client | | | | 127.0.0.1 user-identifier frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326 | | | IP address ของ client วันเวลาใน format "%d/%b/%Y:%H:%M:%S %z" สถานะการทำงานของ httpd --------------------------------------------------------------------------------------------------------- รูปแบบนี้เป็นรูปแบบมาตรฐานสำหรับใช้กับ Web log analysis program เช่น Webalizer (GNU GPL) เป็นต้น ในกรณีของระบบUnix เก็บ log files ใน /var/log ซึ่งผู้ใช้โดยทั่วไปไม่มีสิทธิในการอ่าน ผู้อ่านต้องเป็น superuser หรืออยู่ในกลุ่มเดียวกับ root Log file สำหรับการทำงานของ shell script ในโครงงานนี้เป็นเพียงตัวอย่างแสดงการสร้างและการใช้งาน จึงแสดงการเก็บเฉพาะข้อมูลทั่วไป 1 บรรทัด ต่อการใช้งาน 1 ครั้ง โดยใช้ "วรรค" เป็นเครื่องหมายแยกฟิลด์ (Internal Field Separator - IFS) และ กำหนดข้อมูลเป็น 4 ฟิลด์ โดยมีรูปแบบดังนี้ <ชื่อผู้ใช้> <วันเวลาใน format "%d/%b/%Y:%H:%M:%S %z"> <"คำสั่งและ argument"> ข้อมูลที่หาได้โดยตรงจากตัวแปรระบบและคำสั่ง ----------------------------------- 1. ชื่อผู้ใช้ หรือชื่อ login เก็บอยู่ในตัวแปรของระบบคือ $USER และ $LOGNAME 2. "คำสั่งและ arguments" กำกับในเครื่องหมายคำพูดเพื่อให้เป็น field เดียวกัน - คำสั่ง เก็บในตัวแปร $0, arguments ทั้งหมดอยู่ในตวแปร $* หรือ $@ 3. วันเวลาใน format "%d/%b/%Y:%H:%M:%S %z" นำไปใช้กับคำสั่ง date ได้ทันที - คำสั่ง date +"%d/%b/%Y:%H:%M:%S %z" IP address ของ client --------------------- คำสั่งที่มีการแสดง IP address ของเครื่องคอมพิวเตอร์ที่ใช้เป็น client คือคำสั่ง who ซึ่งมีการแสดงผลดังนี้ ชือผู้ใช้ (name), ช่องติดต่อ (line), วันและเวลา (time), และ หมายเหตุ (comment - IP address หรือชื่อเครื่อง) ตัวอย่าง $ who jira pts/0 2016-04-11 12:48 (223.204.247.194) jira pts/1 2016-04-11 12:48 (223.204.247.194) pusit pts/0 2016-04-11 03:09 (10.20.64.195) bencha pts/2 2016-04-11 13:55 (dhcp64-197.informatics.buu.ac.th) ... วิเคราะห์ ------ 1. ฟิลด์ที่ต้องการใช้งานคือ ชื่อผู้ใช้ และชื่อเครื่อง client 2. ชื่อเครื่อง client อาจเป็น IP address หรือชื่อเครื่องและชื่อโดเมนก็ได้ 3. ชื่อผู้ใช้อาจซ้ำกันได้ ขึ้นอยู่กับจำนวน pseudo-terminal ที่ทำการ login 4. ชื่อผู้ใช้ และชื่อเครื่อง client มีความสั้นยาวไม่เท่ากัน ตัดผลลัพธ์ของ who เฉพาะฟิลด์ที่ 1 (ชื่อผู้ใช้) และฟิลด์ที่ 5 (ชื่อเครื่อง client) -- คำสั่ง cut โดยใช้ option -f (field) แต่การตัดข้อมูลโดยกำหนดฟิลด์ต้องระบุเครื่องหมายวรรคตอนที่ใช้คั่นระหว่างฟิลด์ (หรือ delimiter) ซึ่งค่าโดยปริยาย เป็น tab ลำดับงานสำหรับการแยก IP address ของผู้ใช้ปัจจุบัน ------------------------------------------ -- เลือกเฉพาะบรรทัดที่เป็นของผู้ใช้ปัจจุบัน โดยใช้ grep และกำหนดชื่อผู้ใช้ในตัวแปร $USER เป็นนิพจน์ปรกติ $ who | grep $USER -- ข้อมูลแต่ละฟิลด์ของ cut คั่นด้วยวรรค จำนวนวรรตในแต่ละบรรทัดอาจไม่เท่ากัน ซึ่งจะทำให้ cut ทำงานไม่ถูกต้อง - แก้ปัญหาโดยการยุบรวม (squeeze) วรรคที่มีทั้งหมด ให้เหลือเพียงวรรคเดียว โดยใช้ tr (transliterate) และ option -s (squeeze) เพื่อบีบวรรค ' ' ที่ซ้ำกันให้เหลื่อเพียงตัวเดียว $ who | grep $USER | tr -s ' ' -- ส่งผลลัพธ์ที่ได้ผ่าน pipe เพื่อตัดฟิลด์ที่ 1 และฟิลด์ที่ 5 (-f1,5) โดยมีวรรคเป็นตัวคั่นฟิลด์ (-d ' ') $ who | grep $USER | tr -s ' ' | cut -d ' ' -f1, 5 -- ในกรณีที่มีบรรทัดซ้ำกัน เลือกใช้เพียงบรรทัดเดียว โดยใช้ uniq (unique) $ who | grep $USER | tr -s ' ' | cut -d ' ' -f1, 5 | uniq -- ตัดชื่อเครื่อง client ซึ่งในขณะนี้เป็นฟิลด์ที่ 2 โดยใช้ cut $ who | grep $USER | tr -s ' ' | cut -d ' ' -f1, 5 | uniq | cut -d ' ' -f2 -- จะได้ชื่อเครื่อง หรือ IP address ในเครื่องหมายวงเล็บ ทำการกำจัดวงเล็บออก โดยใช้ tr -d '()' $ who | grep $USER | tr -s ' ' | cut -d ' ' -f1, 5 | cut -d ' ' -f2 | tr -d '()' -- นำผลลัพธ์ที่ได้กำหนดให้ตัวแปร client สำหรับใช้งาน $ client=$(who | grep $USER | tr -s ' | cut -d ' ' -f1, 5 | uniq | cut -d ' ' -f2 | tr -d '()') -- ขณะนี้ชื่อเครื่อง หรือ IP address อยู่ในตัวแปร $client พร้อมนำไปใช้งาน สรุปข้อมูลของ logfile ------------------ รูปแบบข้อมูลแต่ละบรรทัด <ชื่อผู้ใช้> <วันเวลาใน format "%d/%b/%Y:%H:%M:%S %z"> <"คำสั่งและ argument"> -- ที่มาของข้อมูลและการแสดงผล -- 1. ชื่อเครื่อง หรือ IP address ของ client เก็บอยู่ในตัวแปร $client 2. ชื่อผู้ใช้ เก็บอยู่ในตัวแปร $USER ของระบบ - การแสดงชื่อเครื่อง client และชื่อผู้ใช้ ด้วยคำสั่ง echo $ echo -n "$client $USER " 3. วันเวลา สร้างจากคำสั่ง date +"%d/%b/%Y:%H:%M:%S %z" - การแสดงวันเวลาให้เป็นส่วนหนึ่งของบรรทัดต่อจากชื่อเครื่องและชื่อผู้ใช้ โดยไม่ขึ้นบรรทัดใหม่เพราะต้องต่อด้วยคำสั่ง และ argument ไม่สามารถทำโดยตรงได้ ต้องทำผ่านคำสั่ง echo จึงต้องใช้การเปลี่ยนแทนคำสั่งด้วยผลลัพธ์ หรือทำcommand substitution ดังนี้ $ echo -n "[$(date +"%d/%b/%Y:%H:%M:%S %z")] " 4. "คำสั่งและ argument" เก็บในตัวแปร $0 และ $@ ก. ชื่อของคำสั่ง(หรือโปรแกรม) ในตัวแปร $0 ประกอบด้วยชื่อคำสั่งและ path ซึ่งอาจเป็น absolute path หรือ relative path แล้วแต่กรณี เช่น ./logging หากอยู่ในไดเรกทอรีปัจจุบัน หรือ /home/staff/jira/bin/logging เมื่อไม่อยู่ และ มีการกำหนดเส้นทางไว้ในตัวแปร $PATH ข. การตัดเส้นทางออกให้เหลือเฉพาะชื่อแฟ้มคำสั่ง ทำได้โดยใช้คำสั่ง basename ค. เครื่องหมายคำพูด (") เป็นอักขระพิเศษ เมื่อจะนำมาใช้งานเป็นอักขระธรรมดา ต้องกำกับด้วย \ ง. การแสดงผล "คำสั่งและ argument" ด้วยคำสั่ง echo จึงต้องใช้การเปลี่ยนแทนคำสั่งด้วยผลลัพธ์ และการกำกับ หรือ quote ด้วย backslash ดังนี้ $ echo \""$(basename $0) $@"\" โปรแกรมเฉพาะส่วนของการเขียนข้อมูลลงใน log file เป็นดังนี้ $ cat -n writelog 1 #!/bin/bash 2 3 exec 3>> $1 # open log-file for appending on fd 3 4 5 # sending data to log file 6 client=$( who | tr -s ' ' | cut -d ' ' -f1,5 | grep $USER \ 7 | uniq | cut -d ' ' -f2 | tr -d '()' \ 8 ) 9 echo -n "$client $USER " >&3 10 echo -n "[$(date +"%d/%b/%Y:%H:%M:%S %z")] " >&3 11 echo \""$(basename $0) $*"\" >&3 12 13 exec 3>&- คำถาม ----- 1. pipeline สำหรับสร้างชื่อเครื่องสำหรับนำมากำหนดให้ตัวแปร client ที่ผ่านมา คือ who | grep $USER | tr -s ' | cut -d ' ' -f1, 5 | uniq | cut -d ' ' -f2 | tr -d '()' ... (1) สามารถเขียนได้อีกอย่างหนึ่งโดยให้ผลลัพธ์การทำงานเช่นเดียวกัน คือ who | tr -s ' | cut -d ' ' -f1, 5 | grep $USER | uniq | cut -d ' ' -f2 | tr -d '()' ... (2) คำสั่งทั้งสองรูปแบบนี้มี "ประสิทธิภาพ" ในการทำงานเท่ากันหรือไม่? หากคำตอบคือ "ไม่" รูปแบบใดมีประสิทธิภาพดีกว่า จงอภิปรายพร้อมให้เหตุผลทางวิชาการประกอบ 2. แฟ้มที่ทำหน้าที่เป็น log file ควรให้ผู้ใช้เป็นผู้กำหนด หรือควรมีชื่อเฉพาะตัว จงอภิปรายข้อดี-ข้อเสีย พร้อมทั้ง เหตุผลที่ใช้สนับสนุน ------------------------------------------------ (ค). การจัดการกับ error -------------------- โปรแกรมนี้กำหนดให้ผู้ใช้ป้อนชื่อแฟ้มที่ต้องการเข้ารหัสผ่านทาง Command Line Argument จึงต้องตรวจสอบดังนี้ -- มี argument ตัวเดียว หรือไม่ -- argument เป็นชื่อแฟ้มที่มีอยู่จริงหรือไม่ ทดสอบว่า argument ($1) เป็นแฟ้มที่มีอยู่จริงหรือไม่ ด้วยคำสั่ง test -e $1 หรือ [ -e $1 ] -- แฟ้มเป็น ASCII text file ปกติหรือไม่ การทดสอบชนิดของแฟ้มทำได้โดยใช้คำสั่ง file ซึ่งแฟ้มประเภท text file มีการแสดงผลดังนี้ regular text file ASCII text script ที่ขึ้นต้นด้วย #!/bin/bash Bourne-Again shell script, ASCII text executable script ที่ขึ้นต้นด้วย #!/bin/sh POSIX shell script, ASCII text executable -- การตรวจสอบโดยใช้ pipeline ต่อไปนี้ คือ file $1 | grep 'ASCII text' จะได้ผลลัพธ์ทั้ง ASCII text, Bourne-Again shell script (ASCII text executable), และ POSIX shell script (ASCII text executable) -- หากต้องการเฉพาะ ASCII text ต้องตัดรายการที่มีคำว่า executable ออกไป โดยใช้ option -v (invert match) ของ grep file $1 | grep 'ASCII text' | grep -v 'executable' -- ตรวจสอบผลการทำงานจาก exit status ซึ่งหาก $1 เป็น ASCII text ปกติจะได้ exit status ($?) เป็น 0 และแสดงชือแฟ้ม ชื่อแฟ้ม: ASCII text หากเป็นแฟ้มชนิดอื่น จะได้ exit status ($?) เป็น 1 และไม่มีการแสดงผล -- กำจัดการแสดงผลเมื่อเป็น ASCII text ปกติ โดยการส่งผลลัพธ์ไปยัง /dev/null ซึ่งเป็นแฟ้มที่ไม่ได้แทนอุปกรณ์ใดเลย ข้อมูลที่ส่งไปยังแฟ้มนี้จะสูญหายไป จึงใช้กำจัดผลลัพธ์ที่ไม่ต้องการได้ โดยไม่มีผลต่อ exit status file $1 | grep 'ASCII text' | grep -v 'executable' > /dev/null โปรแกรมตามแนวคิดเฉพาะการตรวจสอบ error เป็นดังนี้ $ cat -n error 1 #!/bin/bash 2 3 ScriptName=$(basename $0) 4 5 if [ $# -eq 0 ]; then 6 echo "usage: $ScriptName " >&2 7 exit 1 8 fi 9 10 if [ ! -e $1 ]; then 11 echo "Error: $1 does not exist!" >&2 12 exit 1 13 fi 14 15 file $1 | grep 'ASCII text' | grep -v 'executable' > /dev/null 16 if [ $? != 0 ]; then 17 echo "Error: $1 is not normal ASCII text file" >&2 18 exit 1 19 fi คำถาม ----- 1. การที่โปรแกรมยอมให้เข้ารหัสเฉพาะแฟ้มที่เป็น ASCII text ธรรมดาที่ไม่ใช่ shell script เหมาะสมหรือไม่? จงอภิปราย พร้อมเหตุผลทางวิชาการ 2. เมื่อทดลองสร้าง symbolic link ไปยังแฟ้มที่เป็น ASCII text ธรรมดา เช่น ll -> test.txt และใช้คำสั่ง $ encode test.txt $ encode ll ปรากฎว่าสามารถเข้ารหัสได้ถูกต้องทั้งสองคำสั่ง แต่เมื่อใช้ $ error ll Error: ll is not normal ASCII text file ปัญหานี้ควรต้องแก้ไขอย่างไร? ------------------------------------------------ การรวม writelog, encode และ error เข้าเป็นโปรแกรมเดียวกัน --------------------------------------------------- เมื่อทำความเข้าใจการทำงานของโปรแกรมแต่ละโปรแกรม และตอบคำถามที่กำหนดไว้ในแต่ละตอนได้โดยมีเหตุผลที่ชัดเจน ก็ถึงเวลาแก้ไขข้อบกพร่อง และความไม่สมเหตุสมผลของแต่ละโปรแกรมตามที่คิดไว้ และทำการรวมโปรแกรมทั้งสามเข้าเป็นโปรแกรมเดียวกัน งานนี้จะทิ้งไว้เป็นแบบฝึกหัดของผู้อ่าน เพื่อจะได้ฝึกฝนและพัฒนาตนเองต่อไป