Thai ID - Series - Hard Link ---------------------------- ตอนที่ ๑ เลขประจำตัวประชาชนไทย --------------------------- จากปฏิบัติการสัปดาห์ที่ 11 ซึ่งกำหนดให้ศึกษาความหมายและการคำนวณเลขตรวจสอบ "เลขประจำตัวประชาชนไทย" จาก https://th.wikipedia.org/wiki/เลขประจำตัวประชาชนไทย และให้ออกแบบและเขียน ก. โปรแกรมคำนวณเลขตรวจสอบ จากเลขประจำตัวประชาชน 12 หลัก ข. โปรแกรมตรวจสอบเลขประจำตัวประชาชน 13 หลัก ว่าเป็นชุดตัวเลขที่สมเหตสมผลหรือไม่ การวิเคราะห์งาน ------------ เลขประจำตัวประชาชนไทย มี 13 หลัก เช่น "1-2345-67890-12-3" แบ่งเป็น 5 กลุ่ม แยกกันด้วย "วรรค" กลุ่มที่ 1 รหัสประเภท 1 หลัก 0-8 กลุ่มที่ 2 รหัสสำนักทะเบียน 4 หลัก 0-9 กลุ่มที่ 3 รหัสกลุ่มบุคคล 5 หลัก 0-9 กลุ่มที่ 4 ลำดับของบุคคล 2 หลัก 0-9 กลุ่มที่ 5 เลขตรวจสอบ 1 หลัก 0-9 ตัวอย่างเลขประจำตัวประชาชนที่ถูกต้อง สำหรับทดสอบการทำงานของโปรแกรม ---------------------------------------------------------- จะหาตัวอย่างเลขประจำตัวประชาชน นอกจากของตัวเราเองมาจากไหน? - เขียนโปรแกรมสุ่มตัวเลข 12 หลัก นำมาคำนวณหาเลขตรวจสอบ - ใช้บริการจาก web site ที่ให้บริการสุ่มเลขบัตร 13 หลัก เช่น tools.necz.net/idcard.php สุ่มสร้างเลขประจำตัวประชาชน มา 10 ชุด เลือกให้มีเลขตรวจสอบต่างกัน เช่น 4 0217 52666 74 0 3 2662 43365 06 1 3 5218 08126 88 2 2 1810 86342 65 3 3 0216 24345 36 5 5 7536 88672 15 6 2 6085 70805 61 7 4 8728 84486 71 8 5 3355 71402 18 9 คำนวณค่า x จากเลขประจำตัวประชาชน 12 หลักแรก, N[1] - N[12] จาก x = ( 13*N[1] + 12*N[2] + 11*N[3] + ... + 2*N[2] ) (mod 11) และคำนวณเลขตรวจสอบซึ่งเป็นตัวเลขหลักที่ 13, N[13] จาก N[13] = 1 - x, if x <= 1 = 11 - x, if x > 1 ความสัมพันธ์ระหว่างค่าของ x และเลขตรวจสอบ x N[13] --- ------- 0 1 1 0 2 9 3 8 ... ... 9 2 10 1 จะเห็นได้ว่าเมื่อ x = 0 และ x = 10, เลขตรวจสอบมีค่าเป็น 1 เหมือนกัน จึงตรวจสอบด้วยการคำนวณได้ยาก ต้องใช้วิธีนำตัวเลข 12 หลักแรก มาคำนวณหาเลขตรวจสอบ แล้วนำมาเปรียบเทียบกับเลขหลักที่ 13 หากเป็นค่าเดียวกันจึงจะเป็นเลขประจำตัวประชาชนที่สมเหตุสมผล การคำนวณหาเลขตรวจสอบมีการทำงานแบบเดียวกับการคำนวณเลขตรวจสอบของ ISBN-10 เพียงแต่มีจำนวนหลักมากกว่า จึงต้องปรับค่าเริ่มต้นให้สอดคล้องกัน และเนื่องจากกำหนดให้ตัวหารทีค่าเป็น 11 และต้องการให้เลขตรวจสอบมีค่าในช่วง 0 - 9 จึงต้องมีการคำนวณค่าตามเงื่อนไขซึ่งมีสองกรณี โปรแกรมตรวจสอบความสมเหตุสมผลเช่นในกรณีนี้ เป็นโปรแกรมที่ให้คำตอบว่า "ใช่" หรือ "ไม่ใช่", "จริง" หรือ "เท็จ" กล่าวอีกนัยหนึ่งคือ ให้ผลลัพธ์เป็นจำนวนตรรกะ (boolean) -- การคืนค่าให้แก่ผู้เรียกใช้ทำผ่าน exit status คือ 0 เมื่อเลขประจำตัวประชาชนสมเหตุสมผล และค่าอื่นที่ไม่ใช่ 0 เมื่อไม่สมเหตุสมผล การตรวจสอบความสมเหตุสมผลขออาร์กิวเมนต์ที่ได้รับ ------------------------------------------ 1. การตรวจสอบจำนวนอาร์กิวเมนต์, ตัวแปร $# 2. การตรวจสอบว่าข้อมูลที่ได้รับอยู่ในรูปแบบที่ถูกต้องหรือไม่ ทำได้ง่ายเพราะมีรูปแบบตายตัว เขียนเป็นนิพจน์ปรกติแบบขยาย (extended regular expression) ได้ดังนี้ "[0-8] [0-9]{4} [0-9]{5} [0-9]{2} [0-9]" # ใช้ character class, หรือ "[0-8] [[:digit:]]{4} [[:digit:]]{5} [[:digit:]]{2} [[:digit:]]" # ใช้ POSIX character class และเมื่อทดสอบรูปแบบเช่นนี้ได้ จึงไม่มีความจำเป็นใด ที่จะต้องทดสอบว่าข้อมูลเป็น "สายอักขระว่าง" หรือไม่ เนื่องจาก สายอักขระว่างไม่ตรงกับนิพจน์ปรกติชุดนี้อยู่แล้ว ประกอบกับโอกาสที่ผู้ใช้กำหนดอาร์กิวเมนต์เป็นสายอักขระว่างมีน้อย แต่หากต้องการจะทดสอบก็ทำได้ เพียงแต่เป็นการทำงานซ้ำซ้อน ทำให้โปรแกรมยาว และซับซ้อนโดยไม่จำเป็น โปรแกรมตรวจสอบความถูกต้องของเลขประจำตัวประชาชนเป็นดังนี้ $ cat tid.sh #!/bin/bash # --------------------------------------------------------- # ตรวจสอบขความสมเหตุสมผลของข้อมูลที่ได้รับ และจัดการกับความผิดพลาด # --------------------------------------------------------- # ตรวจสอบจำนวนอารฺกิวเมนต์ if [ $# -lt 1 ]; then echo "$(basename $0) - validate Thai ID ISBN-10" echo "Usage: $(basename $0) <13-digit-ID>" exit 1 fi # ตรวจสอบรูปแบบของข้อมูลว่าตรงกับรูปแบบของเลขประจำตัวประชาชนหรือไม่ if ( echo $1 | egrep -qv "[0-8] [0-9]{4} [0-9]{5} [0-9]{2} [0-9]" ); then echo "Error: invalid Thai ID (wrong pattern)" exit 2 fi # ----------------------------------------- # ตรวจสอบความสมเหตุสมผลของเลขประจำตัวประชาชน # ----------------------------------------- # สร้างรายการของเลขประจำตัวประชาชนแต่ละหลัก # 1. กำจัดวรรคทั้งหมด list=$(echo $1 | tr -d " ") # 2. ตัดเลขประจำตัว 12 หลักแรก (จากทางซ้าย) เพื่อใช้คำนวณเลขตรวจสอบ id=$(echo $list | cut -c1-12) # 3. ตัดเลขหลักสุดท้าย (เลขตรวจสอบที่ผู้ใช้ป้อน) cd=$(echo $list | cut -c13) # 4. แยกเลขประจำตัวออกเป็นรายการของตัวเลข 12 ตัว list=$(echo $id | fold -w1) # กำหนดค่าเริ่มต้นของ sum และ weight sum=0; w=13 # คำนวณเลขตรวจสอบจากสูตร x = 13*N[1] + 12*N[2] + 11N[3] + ... + 2*N[12] (mod 11) และ # N[13] = 1 - x, if x <= 1 หรือ N[13] = 11 - x, if x > 1 for n in $list; do sum=$(($sum + $n * $w)) w=$(($w - 1)) done x=$(($sum % 11)) if [ $x -le 1 ]; then x=$((1 - $x)) else x=$((11 - $x)) fi # คืนผลการตรวจสอบให้ผู้เรียกผ่าน exit status if [ $x -eq $cd ]; then exit 0 # valid ID else exit 3 # invalid ID fi การเรียกใช้งานคำสั่ง --------------- เนื่องจากเลขประจำตัวประชาชน ใช้เครื่องหมายวรรคคั่นระหว่างกลุ่ม ซึ่งเป็นเครื่องหมายเดียวกับที่เชลล์ใช้แยกอาร์กิวเมนต์ การเรียกใช้งานจึงต้องกำกับเลขประจำตัวประชาชนด้วยเครื่องหมายคำพูด เพื่อให้เป็นอาร์กิวเมนต์เพียงตัวเดียว เช่น $ tid.sh "5 7536 88672 15 6" ในกรณีที่ข้อมูลเข้าเป็นตัวเลขที่มีรูปแบบถูกต้อง ไม่ว่าเลขประจำตัวประชาชนนั้นจะสมเหตุสมผลหรือไม่ โปรแกรมจะไม่มีการแสดงผลลัพธ์ใด เป็นหน้าที่ของผู้ใช้ต้องตรวจสอบ exit status ทันทีที่โปรแกรมทำงานจบลง $ echo $? ผลลัพธ์ของโปรแกรม ---------------- หากผลลัพธ์เป็น 0 แสดงว่าเป็นเลขประจำตัวประชาชนที่สมเหตุสมผล หากค่านี้ไม่เป็น 0 (ในกรณีของโปรแกรมนี้ค่าเป็น 3) แสดงว่าเป็นค่าที่ไม่ถูกต้อง วิธีการนี้เป็นแนวปฏิบัติที่ดี เมื่อมีการเรียกใช้โปรแกรมนี้จากโปรแกรมอื่นที่จำเป็นต้องตรวจสอบความสมเหตุสมผลของเลขประจำตัวประชาชน เช่น if ! tid.sh "5 7536 88672 15 6"; then echo "Error: invalid Thai ID" exit fi ... ดำเนินการกับเลขประจำตัวประชาชนนั้นต่อไป ... หมายเหตุ: ในกรณีที่ต้องการเพิ่มการตรวจสอบสายอักขระว่าง ต้องทำความเข้าใจเรื่องการกำกับ (quote) ให้ดี ดังตัวอย่างต่อไปนี้ # ส่วนของโปรแกรมที่ใช้ทดสอบสายอักขระว่าง ขอให้สังเกตการกำกับตัวแปร $1 ด้วยเครื่องหมายคำพูด # เพื่อให้ "ค่า" ของ $1 เป็นสายอักขระเพียงชุดเดียว ไม่แยกออกเป็นหลายชุดตาม "วรรค" ที่มี if [ -z "$1" ]; then echo "Error: invalid Thai ID (null string)" exit 4 fi ตอนที่ ๒ การสร้างสมาชิกของอนุกรม ---------------------------- การคำนวณเลขตรวจสอบของเลขมาตรฐานสากลประจำหนังสือชนิด 13 หลัก (ISBN-13) ตัวอย่างข้อมูล ---------- 978-3-16-148410-0 978-0-8493-9640-3 978-616-7757-37-7 สูตรการคำนวณเลขตรวจสอบ ---------------------- เลขตรวจสอบ (ตัวเลขหลักที่ 13, d[13]) คำนวณจาก d[13] = 10 - { d[1] + 3*d[2] + d[3] + 3*d[4] + ... + d[11] + 3*d[12] (mod 10)} เมื่อน้ำหนักที่ใช้เป็นตัวคูณของตัวเลขแต่ละหลัก มีค่าเป็น 1 และ 3 สลับกันไป กล่าวอีกนัยหนึงคือน้ำหนักเป็นอนุกรม: 1, 3, 1, 3, ... ข้อสังเกต: ตัวเลขมีค่าสูง - ต่ำสลับกัน การจะเปลี่ยนค่าจากสูงเป็นต่ำ น่าจะเกิดจากการหารเอาเศษ ค่าสูงสุดของอนุกรมเป็น 3 แสดงว่าเศษมีค่าระหว่าง 0-3 นั่นคือ ตัวหารเป็น 4 กำหนดให้สมาชิกของอนุกรมเป็นตัวแปร n, มีค่าเริ่มเป็น 1 n = 1; คำนวณค่าสมาชิกตัวแรก, n = 1 w = n (mod 4); { 1 (mod 4) = 1 } n = n + 2; { เตรียมค่า n สำหรับการคำนวณสมาชิกตัวต่อไป } สมาชิกตัวที่สอง, n = 3 w = n (mod 4); { 3 (mod 4) = 3 } n = n + 2; สมาชิกตัวที่สาม, n = 5 w = n (mod 4); { 5 (mod 4) = 1 } n = n + 2; สมาชิกตัวที่สี่, n = 7 w = n (mod 4); { 7 (mod 4) = 3 } n = n + 2; ... เมื่อวิเคราะห์งานได้แล้ว การ coding เป็น shell script ก็ไม่ยาก ในที่นี้แสดงเฉพาะส่วนของโปรแกรมการคำนวณอนุกรม 1, 3, 1, 3, ... เพื่อใช้เป็นน้ำหนัก #!/bin/bash n=1 # ค่าเริมต้น for i in {1..12}; do # {1..12} คือรูปย่อของรายการ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 w=$(($n % 4)) n=$(($n + 2)) echo -n "$w, " done echo ตอนที่ ๓ การหา hard link ---------------------- ความต้องการ: โปรแกรมสำหรับหา hard link ของแฟ้ม ใน directory ที่กำหนด ในกรณีที่ผู้ใช้ไม่ได้กำหนดชื่อ directory ให้ ดำเนินการค้นหาใน directory ปัจจุบัน รูปแบบของคำสั่ง: links <ชื่อแฟ้ม> [<ชื่อ directory>] แนวความคิดทั่วไป: แฟ้มที่มี hard link เมื่อใช้คำสั่ง ls –l จะมีการแสดงจำนวน link ใน column ที่ 2 โดยมีค่าตั้งแต่ 2 ขึ้นไป และการแสดงผลของ hard link จะมีรูปแบบเดียวกับแฟ้มจริงทำให้แยกความแตกต่างได้ยาก แต่เนื่องจากแฟ้มที่เป็น hard link ซึ่งกันและกันจะมี i-node number เป็นค่าเดียวกัน เมื่อได้ค่า i-node ของแฟ้มที่กำหนดได้แล้ว สามารถใช้คำสั่ง find ในการหาแฟ้มอื่นที่มีค่า i-node เดียวกันได้ ตัวแปรในโปรแกรม -------------- $file ชื่อแฟ้มที่ต้องการหา hard link $directory ชื่อ directory ที่ต้องการค้น ผู้ใช้จะกำหนดหรือไม่ก็ได้ $linkcnt จำนวน link ของแฟ้ม $file $inode หมายเลข inode ของแฟ้ม $fie Utilities ของระบบปฏิบัติการ Unix ที่ต้องใช้ ------------------------------------- ls –l <ชื่อแฟ้ม> แสดงค่าจำนวน link ของแฟ้มที่กำหนด ls –i <ชื่อแฟ้ม> แสดงค่า i-node no. ของแฟ้มที่กำหนด find <ชื่อ directory> –inum <หมายเลข i-node> -print ค้นหาและแสดงรายชื่อแฟ้มที่มี inode ตรงกับแฟ้มที่กำหนดใน $file โดยเริ่มค้นจาก $directory $ cat lnks.sh #!/bin/bash # หา hard link ของแฟ้มที่กำหนด # ผู้เรียกต้องกำหนดชื่อแฟ้ม ชื่อไดเรกทอรีจะมีหรือไม่ก็ได้, จำนวนอาร์กิวเมนต์ที่ถูกต้องอยู่ในช่วง 1-2 if [ $# -eq 0 -o $# -gt 2 ]; then echo "Usage: $(basename $0) file [directory]" 1>&2 exit 1 fi # ถ้าชื่อแรกเป็นไดเรกทอรี, ไม่สามารถทำงานได้ if [ -d "$1" ]; then echo "First argument cannot be a directory." 1>&2 echo "Usage: lnks file [directory]" 1>&2 exit 1 else file="$1" # ชื่อแรกเป็นชื่อแฟ้ม fi # ถ้ามีอาร์กิวเมนต์เพียงตัวเดียว ผู้ใช้ไม่ได้กำหนดไดเรกทอรี if [ $# -eq 1 ]; then directory="." # กำหนดให้เป็นไดเรกทอรีปัจจุบัน (ค่าโดยปริยาย) elif [ -d "$2" ]; then # ไดเรกทอรีที่กำหนดมีอยู่จริงหรือไม่ directory="$2" # มีอยู่จริง, กำหนดชื่อให้กับตัวแปร else echo "Optional second argument must be a directoty." 1>&2 echo "Usage: lnks file [directory]" 1>&2 exit 1 fi # ตรวจสอบว่าแฟ้มที่ผู้ใช้กำหนดมีอยู่จริง และเป็นแฟ้มธรรมดา - ไม่ใช่แฟ้มพิเศษ เช่น # symbolic link,named pipe, device file, หรือ socket if [ ! -f "$file" ]; then echo "lnks: $file not found or special file" 1>&2 exit 1 fi # หาจำนวน link ของแฟ้ม เป็นฟิลด์ที่ 2 ของคำสั่ง ls -l, วิธีการที่สะดวกในการแยกฟิลด์โดยไม่ต้อง # ใช้คำสั่ง awk หรือ cut คือกำหนดให้แยกผลลัพธ์ของ "ls -l" เก็บใน postional paramter ด้วยคำสั่ง # set :- จำนวน link จึงอยู่ในตัวแปร $2 set -- $(ls -l "$file") # set --, คำสั่ง set ไม่มีตัวเลือก linkcnt=$2 # เก็บจำนวน link ในตัวแปร # แฟ้มจะมี hard link ได้ ก็ต่อเมื่อมีจำนวน link ตั้งแต่ 2 ขึ้นไป if [ "$linkcnt" -eq 1 ]; then echo "lnks: no other hard link to $file" 1>&2 exit 0 fi # อ่านหมายเลข i-node ของแฟ้มนั้นโดยใช้คำสั่ง "ls -i" โดยใช้คำสั่ง set, จะได้หมายเลข i-node เป็นฟิลด์แรก set -- $(ls -i "$file") inode=$1 # หาแฟ้มที่มีหมายเลข i-node ตรงกับที่กำหนดโดยใช้คำสั่ง find, การค้นอาจใช้เวลานาน จึงควรแจ้งให้ผูใช้ทราบว่ากำลังทำงานอยู่ echo "lnks: using find to search for links ... " 1>&2 find "$directory" -xdev -inum "$inode" -print หมายเหตุ ------- 1. ความหมายของตำสั่ง find ไดเรกทอรีเริ่มค้น ค้นแฟ้มที่มีหมายเลข i-node ที่ระบุ | | find "$directory" -xdev -inum "$inode" -print | | ห้ามค้นข้ามระบบแฟ้ม เมื่อพบแฟ้มแล้วให้แสดงผลทาง stdout 2. บรรทัดคำสั่ง echo สำหรับแสดงข่าวสารความผิดพลาด (error message) ลงท้ายด้วย "1>&2" เพื่อเปลี่ยนทิศทางการแสดงผล เช่น echo "Usage: $(basename $0) file [directory]" 1>&2 ทั้งนี้เนื่องจาก echo เป็นโปรแกรมที่ทำการส่งข้อความที่กำหนดออกทาง standard output (file descriptor = 1)เสมอ เมื่อนำมาใช้ในการแสดงข่าวสารความผิดพลาด (error message) จึงต้องเปลี่ยนทิศทางจาก standard output ไปยัง standard error (file descriptor = 2) มีไว้สำหรับเมื่อผู้ใช้ทำการแยกผลลัพธ์ (output) และข่าวสารความผิดพลาด (error) ไปยังแฟ้มแยกกัน โปรแกรมจะได้ทำงานถูกต้องในทุกกรณึ การเปลี่ยนทิศทางเช่นนี้ทำเฉพาะเมื่อใช้คำสั่ง echo ในการแสดงข่าวสารความผิดพลาดเท่านั้น ไม่ต้องใช้งานเมื่อใช้ echo แสดงผลลัพธ์ตามปกติ การทดสอบการทำงานของโปรแกรม Links -------------------------------- การทดสอบการทำงานของโปรแกรมต้องมีการเตรียมการสร้างไดเรกทอรี สร้างแฟ้ม และสร้าง hard link ของแฟ้มนั้น รายละเอียดการทดลองอยู่ในปฏิบัติการสำหรับสัปดาห์นี้ (Prac12.txt)