bash: tip

ลองบนอูบุนตู

ls ให้มีสี
แก้ /etc/passwd ให้ชื่อเรามีเชลล์เป็น /bin/bash
$ sudo vi /etc/password
...
user1:x:1101:1001::/home/user1:/bin/bash
...

หรือผ่านคำสั่ง usermod
$ sudo usermod -s /bin/bash user1

grep ให้มีสี
แก้ตัวแปร GREP_OPTION
$ export GREP_OPTIONS='--color=auto'
เอามาจาก debian-administration - grep: highlighting matches in color
ดูที่มาของไอพี
$ wget -O country.txt "http://api.hostip.info/get_html.php?ip=$IP"
ผลิตเลขสุ่มแบบง่าย
$ echo $(head -1 /dev/urandom | od -N 2 | awk '{ print $2 }')
Topic: 

bash tips: ลบไฟล์ไวรัส

ลูกน้องเอาธัมบ์ไดรฟ์มาให้หาไวรัส
ผลปรากฎว่าพบไฟล์ที่เป็นนามสกุล exe เป็นจำนวนมากภายใต้ไดเรกทอรี่ย่อยเป็นร้อย
ทางแก้คือสแกนแล้วเก็บชื่อไฟล์ไว้ ตัวอย่างของเนื้อไฟล์ที่ถูกสแกนเก็บไว้ เช่น

PhotoShop 7.0/• วิธีทำตัวเยลลลี่ •_files/• วิธีทำตัวเยลลลี่ •_files.exe: W32.Autoit.Obfus FOUND
PhotoShop 7.0/• วิธีทำตัวหนังสือชอล์ค •_files/truehitsstat_files/truehitsstat_files.exe: W32.Autoit.Obfus FOUND
...

จะเห็นว่ามีรูปแบบที่เราจะตัดโดยใช้คำสั่ง cut ได้คือตั้งแต่เครื่องหมาย : เป็นต้นไป
คำสั่งที่ใช้คือ

cut -d: -f1

-d: คือใช้ : เป็นตัวแบ่ง
-f1 คื่อเมื่อแบ่งแล้ว เราจะเอาสดมถ์ที่ 1 มาใช้งาน

แต่เราจะแก้ไขเนื้อไฟล์ให้เหมาะสมเล็กน้อย คือตัดท่อนล่างของไฟล์ออก ให้เหลือเฉพาะชื่อไฟล์ที่ติดไวรัส
แล้วจึงเอาเนื้อไฟล์นั้นมาเป็นข้อมูลเข้า เพื่อจะมาลบไฟล์ที่ติดไวรัสจริง ๆ

แต่มีปัญหาเพิ่มคือ ชื่อไฟล์ประกอบด้วยช่องว่างจำนวนมาก ไม่สามารถใช้คำสั่ง for i in `cat file` ได้
ค้นกูเกิลดู พบว่าเขาใช้คำสั่ง while read VARIABLE

สรุปคำสั่งทั้งหมดมาเป็นขั้นตอนดังนี้
สมมุติว่าค้นหาไวรัสในธัมบ์ไดรฟ์ในไดเรกทอรี่ /media/disk

ไปที่ที่ทำงาน

$ cd /media/disk

สแกนไวรัส แล้วเก็บผลไว้ที่ ~/virus.txt

$ clamscan -i -r * > ~/virus.txt

-i คือให้แสดงเฉพาะไฟล์ที่ติดไวรัส
-r คือให้ขุดลึกลงไปในไดเรกทอรี่ย่อยด้วย

แก้ไขไฟล์เล็กน้อย โดยตัดรายงานส่วนท้ายออก
ยกตัวอย่างส่วนที่ตัดออกคือ

$ vi ~/virus.txt
----------- SCAN SUMMARY -----------
Known viruses: 203664
Engine version: 0.92
Scanned directories: 97
Scanned files: 1234
Infected files: 252
Data scanned: 378.68 MB
Time: 130.603 sec (2 m 10 s)

ทีนี้ก็ถึงเวลาลบจริง ๆ แล้ว ***ใช้ด้วยความระมัดระวังนะครับ***
ถ้ายังไม่แน่ใจ อาจแทนที่คำสั่ง rm ด้วยคำสั่ง ls ลองดูก่อน

$ cat ~/virus.txt | cut -d: -f1 | while read FILE; do rm "$FILE"; done

เสร็จแล้วครับ

หมายเหตุ

  • ต้องติดตั้ง clamscan ไว้ก่อน ถ้ายัง ติดตั้งด้วยคำสั่ง
    $ sudo aptitude install clamav
    $ sudo freshclam
  • ปกติการเมานต์ธัมบ์ไดรฟ์ที่เป็นระบบไฟล์ vfat เขาจะเมานต์ด้วยค่าปริยายด้วยรหัสอักขระภาษาอังกฤษ ทำให้อ่านชื่อไฟล์ภาษาไทยไม่รู้เรื่อง ซึ่งจริง ๆ แล้วต้องตั้งค่ารหัสอักขระเป็น utf-8
    เราสามารถบังคับด้วยการเมานต์ใหม่ หรือตั้งค่ารหัสอักขระถาวร ดูวิธีการได้ที่ th.gnome.org: เกร็ดการแก้รหัสอักขระ usb drive
  • ถ้ามีไฟล์ชื่อ runauto.. เราจะลบไฟล์นี้ตรง ๆ ไม่ได้ ต้องใช้คำสั่งว่า
    $ rm runaut~1 -rf
    ถึงจะลบออก

ที่มา

Topic: 

ลบไวรัสด้วย AVG Anti-Virus

เครื่องลูกข่ายวินโดวส์ติดไวรัส Win32/Heur ซึ่งใช้ clamav สแกนไม่พบ
ลองค้นกูเกิลดู พบ ubuntuclub แนะนำ AVG

ก่อนอื่นก็ไปดาวน์โหลดที่นี่ http://free.avg.com/us-en/download?prd=afl

แสดงตัวอย่างด้วยบรรทัดคำสั่งคือ
ดาวน์โหลดรุ่นฟรีมาใช้

$ wget http://download.avgfree.com/filedir/inst/avg85flx-r290-a2950.i386.deb
$ sudo dpkg -i avg85flx-r290-a2950.i386.deb

สั่งให้รัน daemon

$ sudo /etc/init.d/avgd start

สั่งอัปเดตข้อมูลไวรัส

$ sudo avgupdate

เนื่องจากรุ่นที่เรานำมาใช้งานเป็นรุ่นฟรี จึงไม่สามารถลบไฟล์ไวรัสได้ จึงใช้วิธีสั่งสแกนและรายงานไว้ในไฟล์ หลังจากนั้นจึงอ่านชื่อไฟล์จากรายงานขึ้นมาลบ

สมมุติว่าให้ค้นที่ /media/disk และให้รายงานไว้ที่ไฟล์ ~/virus.txt

$ avgscan -r ~/virus.txt /media/disk

เตรียมการสำหรับการลบ โดยการแก้ไขไฟล์ ~/virus.txt โดยตัดส่วนหัวและส่วนท้ายให้เรียบร้อย ให้เหลือแต่ชื่อไฟล์ที่ติดไวรัส

$ vi ~/virus.txt
/media/disk/filename1  Virus XXX
/media/disk/filename2  Virus YYY

สั่งลบ (*** ใช้ด้วยความระมัดระวังนะครับ ***)

$ cat ~/virus.txt | awk -F'  ' '{print $1}' | while read FILE; do rm "$FILE"; done

หลัง -F เคาะสองวรรค (AVG ใช้ช่องว่างสองช่องคั่นชื่อไฟล์และไวรัส ดังนั้นถ้าชื่อไฟล์มีช่องว่างสองช่อง คำสั่งนี้จะใช้งานไม่ได้)

เสร็จแล้วครับ

Topic: 

bash: Rip audio disc to mp3

มีงานต้องเก็บแผ่นซีดีธรรมะลงไว้ในฮาร์ดดิสก์เป็นจำนวนมาก
เพื่อให้สะดวก จึงทำเป็นสคริปต์ให้ใช้งานได้สะดวก
ลักษณะของโปรแกรมคือ เมื่อเราใส่แผ่นแล้วสั่งรันโปรแกรม เขาจะถอดไฟล์เสียงทั้งหมดมาเป็นไฟล์ mp3 ที่ไดเรกทอรี่ปัจจุบัน โดยเอาข้อมูลชื่อเพลงจากแผ่นมาเป็นชื่อไฟล์ ก่อนอื่นติดตั้งแพกเกจที่จำเป็นก่อน
$ sudo aptitude install lame cdda2wav สคริปต์มีดังนี้
$ sudo vi /usr/local/bin/d.audio2mp3
#!/bin/bash
# Rip audio disc to mp3
# 
# USAGE: $0 prefix
# exam1: $0 T2
#    -> T2-01-Title1.mp3
#       T2-02-Title2.mp3
#       ...
# in current dir
# 
# Requist: aptitude install lame cdda2wav

if [ $1 ]; then PREFIX="$1-"; fi

DEV='/dev/cdrom'
TMP="/tmp/$USER/`basename $0`_$RANDOM"

mkdir -p $TMP
pushd $TMP

#to wave
cdda2wav -L 1 -D $DEV -B

#to mp3
for i in *wav; do
    NUM=`echo ${i%.*} | cut -d_ -f 2`-
    TITLE=`grep 'Tracktitle=' ${i%.*}.inf | cut -d\'  -f2`
    lame -h -V 2 $i $PREFIX$NUM$TITLE.mp3
done

popd
mv $TMP/*mp3 .

rm -rf $TMP
ทำให้รันได้
$ sudo chmod 777 /usr/local/bin/d.audio2mp3 เสร็จแล้ว
การใช้งานคือ ใส่แผ่น audio แล้วสั่งรัน d.audio2mp3 จะได้ไฟล์ mp3 มาอยู่ในไดเรคทอรี่ปัจจุบัน การทำงานของสคริปต์คือ
  • แปลงข้อมูลเสียงมาเป็นไฟล์ wav ด้วยโปรแกรม cdda2wav โดยนำไปใส่ในไดเรคทอรี่ชั่วคราวใน /tmp (เพื่อป้องกันผู้ใช้หลายคน จึงใส่ตัวแปร $USER ไว้ด้วย เพราะโค๊ดต้นฉบับ เขียนบนเซิร์ฟเวอร์ที่มีผู้ใช้หลายคน)
  • แปลงไฟล์ wav ที่ได้มาเป็น mp3 โดยใช้โปรแกรม lame (ซึ่งอาจมีปัญหาว่าตอนนี้หา lame บน lenny กับ sid ไม่ได้แล้ว ให้ลองดูวิธีปรุงขึ้นมาเองได้ที่ วิธีปรุง lame โดยคุณโดม http://thisk.org/dr/node/32)
    โดยเอาข้อมูล Tracktitle จากไฟล์นามสกุล inf มาเป็นชื่อไฟล์ mp3 ที่ได้ รวมถึงใส่คำนำหน้า (ตัวแปร $PREFIX) ให้ด้วย

update

  • 25570325: แพ็กเกจเปลี่ยน ไม่ต้องใช้สคริปต์แล้ว เพียงลงแพ็กเกจชื่อ icedax แล้วใช้คำสั่ง
    $ CDDA_DEVICE=/dev/sr0 cdda2mp3
Topic: 

bash: ทำบัตรเลขที่

มีโจทย์คือ ให้สร้างบัตรแบบเรียงเลขที่ ตั้งแต่ ๑ จนถึง ๕๐๐
ตัดสินใจใช้ bash + inkscape เพราะง่ายดี
โดยสมมุติว่าเราได้สร้างเอกสารเป็นไฟล์ inkscape ไว้แล้ว โดยให้ตัวเลขที่จะเรียงให้มีค่าเป็น XXXX เพื่อให้สะดวกในการใช้คำสั่ง grep โค๊ดที่สร้างขึ้น ทำแบบง่าย ๆ คือรันตัวเลขตั้งแต่ 1-500 แต่รุ่นนี้เป็นรุ่นทดสอบ ทำแค่ 1-10 พอ ขั้นตอนคือ
  • แปลงตัวเลขเป็นเลขไทย ด้วยคำสั่ง awk
  • นำไปแทนที่ในไฟล์ inkscape แล้วทำออกมาเป็นไฟล์ใหม่ ด้วยคำสั่ง sed
  • แล้วสั่งส่งออกมาเป็นไฟล์นามสกุล png ด้วยคำสั่งแบบบรรทัดคำสั่งของ inkscape เอง ด้วยพารามิเตอร์ -e
โค๊ดมีดังนี้
$ vi runcard
#!/bin/bash

FILE='card.svg'
QUAN=10
MARKER='XXXX'

for i in $(seq 1 $QUAN); do
    TNUM=`echo $i | awk '{ gsub ("0","๐"); gsub ("1","๑"); gsub ("2","๒"); gsub ("3","๓"); gsub ("4","๔"); gsub ("5","๕"); gsub ("6","๖"); gsub ("7","๗"); gsub ("8","๘"); gsub ("9","๙"); print }'`
    sed -e "s/$MARKER/$TNUM/g" $FILE > tmp.svg
    inkscape -d 300 -e tmp$i.png tmp.svg
done
$ chmod 755 runcard
เวลาใช้งานก็สั่งรันชื่อไฟล์ runcard เฉย ๆ จะได้ไฟล์ tmp1.png จนถึง tmp10.png ก็สามารถนำไฟล์เหล่านี้ไปพิมพ์งานได้ตามต้องการ
Topic: 

bash: ย้ายข้อมูลผู้ใช้ไปเครื่องใหม่

ต้องการโอนข้อมูลผู้ใช้ไปเครื่องใหม่

ถ้าเราคัดลอกไฟล์ /etc/passwd /etc/shadow /etc/group /etc/gshadow ไปทับเครื่องใหม่แบบตรง ๆ จะเกิดปัญหาเรื่องผู้ใช้ของระบบจะติดไปด้วย ซึ่งอาจมีค่า UID และ GID ที่ไม่ตรงกัน

ค้นกูเกิลได้วิธีการจากที่นี่ครับ Move or migrate user accounts from old Linux server to a new Linux server

เขาใช้หลักการที่ว่า UID ของผู้ใช้ทั่วไป จะมีค่ามากกว่า 1000 (ของ RedHat คือ 500)
และใช้ awk เป็นตัวกรอง

--- ข้อเขียนต่อจากนี้ไป ควรทดสอบกับเครื่องทดสอบ ก่อนใช้งานจริง---

ขั้นตอนตามต้นฉบับก็ไม่มากเท่าไหร่ แต่กลัวว่าเวลาย้ายจริงจะพิมพ์พลาด เลยเอามาเขียนเป็นสคริปต์เพื่อช่วยลดความผิดพลาดตอนพิมพ์บนบรรทัดคำสั่ง รวมทั้งเป็นการศึกษาการเขียนสคริปต์ของ bash ร่วมกันแล้วกันนะครับ

ตั้งชื่อว่า d.migrate-groupuser ผมใส่ไว้ใน /root
(ห้ามใส่ในพาธการค้นหาของระบบเด็ดขาด เพราะต้องมีการแก้ไขค่าก่อนใช้งานจริง)

ตอนใช้งานก็เปลี่ยนค่าตัวแปร TARGETMACHINE ให้เป็นชื่อเครื่องใหม่ที่เราจะโอนไป แล้วก็สั่งรันได้เลย

โปรแกรมจะทำงานดังนี้

  1. คัดลอกไฟล์ /etc/{passwd,shadow,group,gshadow} และบีบอัดไดเรกทอรี่ /home และ /var/spool/mail มาไว้ในไดเรกทอรี่ $MIGRATEDIR
  2. ผลิตสคริปต์ชื่อ d.import-groupuser เอาไว้สั่งรันที่เครื่องใหม่ และ d.rollback-groupuser เอาไว้สั่งทำย้อนกลับที่เครื่องใหม่เช่นกัน
  3. โอนไฟล์ทั้งหมดใน $MIGRATEDIR ไปยังเครื่องใหม่

หลังจากนั้น เราก็สั่งรันสคริปต์ d.import-groupuser ที่เครื่องใหม่ได้เลย

*** ใช้ด้วยความระมัดระวัง ***

เริ่มเลย
# vi /root/d.migrate-groupuser

#!/bin/bash
#PREREQUISITE:
#  1. INSTALL PACKAGE: openssh-client
#  2. EDIT THIS FILE, CHANGE VARIABLE "TARGETMACHINE" TO REAL TARGET 
#THEN RUN AS root

MIGRATEDIR="/root/migrategroupuser"    #MIGRATE DIR
UGIDLIMIT=1000    #UID&GID OF USER DATA: DEBIAN=1000, REDHAT=500

# EDIT TARGETMACHINE 
TARGETMACHINE="newserver"              #COPY TO THIS MACHINE
TARGETDIR="/root/importgroupuser"      #COPY DATA TO THIS DIR

if [ "$1" != "OK" ]; then
    PROG=`basename $0`
    cat << EOF

*** DON'T PLACE THIS SCRIPT IN SYSTEM SEARCH PATH
*** USE WITH CARE
*** EDIT TARGETMACHINE VARIABLE THEN RUN AS ROOT

Move or migrate user accounts from old Linux server to a new Linux server
FROM: http://www.cyberciti.biz/faq/howto-move-migrate-user-accounts-old-to-new-server/

USE WITH CARE, PLEASE BACKUP OLD DATA, RUN AS ROOT
- COPY FILTERED /etc/{passwd,group,shadow,gshadow} TO $MIGRATEDIR WITH EXT .mig 
- BACKUP /home, /var/spool/mail TO $MIGRATEDIR .tar.gz
- TRANSFER ALL FILES IN $MIGRATEDIR TO root@$TARGETMACHINE:$TARGETDIR WITH scp
 
USAGE:
    $PROG OK

('OK' is safety argument)
DON'T FORGET TO EDIT TARGETMACHINE VARIABLES

EOF
    exit 1
fi

if [ ! `which scp` ]; then
    echo "Please install 'openssh-client' first."
    exit 1
fi

mkdir -p $MIGRATEDIR

echo "
Copy /etc/{password,group,shadow,gshadow} to $MIGRATEDIR ..."
for i in /etc/{passwd,group,shadow,gshadow}; do
    j=`basename $i`
    awk -v LIMIT=$UGIDLIMIT -F: '($3>=LIMIT) && ($3!=65534)' $i > $MIGRATEDIR/$j.mig
done

echo "
gzip /home ..."
tar -zcpf $MIGRATEDIR/home.tar.gz /home/*

echo "
gzip /var/spool/mail ..."
tar -zcpf $MIGRATEDIR/mail.tar.gz /var/spool/mail/*

#------------------------------------------------

echo "
Generate import script ..."
IMPORTPROG="d.import-groupuser"
cat > $MIGRATEDIR/$IMPORTPROG << VIRTUAL_EOF
#!/bin/bash

IMPORTDIR="/root/importgroupuser"     #COPY DATA TO THIS DIR
BACKUPDIR="/root/backupgroupuser"     #BACKUP OLD DATA

if [ "\$1" != "OK" ]; then
    PROG=\`basename \$0\`
    cat << EOF

Move or migrate user accounts from old Linux server to a new Linux server
FROM: http://www.cyberciti.biz/faq/howto-move-migrate-user-accounts-old-to-new-server/

USE WITH CARE, PLEASE BACKUP OLD DATA, RUN AS ROOT
- BACKUP /etc/{passwd,group,shadow,gshadow} TO \$BACKUPDIR/etc
- BACKUP /home /var/spool/mail TO \$BACKUPDIR/tar 
- ADD NEW GROUP-USER DATA FROM \$IMPORTDIR TO /etc
- ADD NEW /home AND /var/spool/mail TO /
 
USAGE:
    \$PROG OK

('OK' is safety argument.)
    
EOF
    exit 1
fi


if [ ! -d \$IMPORTDIR ]; then
    echo "\$IMPORTDIR not exist, program aborted"
    exit 1
fi


echo "
Backup old data and add migrate data to /etc/{passwd,group,shadow,gshadow} ..."
mkdir -p \$BACKUPDIR
pushd \$BACKUPDIR
ls | while read FILE; do
    mv "\$FILE" "\$FILE.bak"
done
mkdir -p {etc,tar}
for i in {passwd,group,shadow,gshadow}; do
    cp /etc/\$i etc
    cat \$IMPORTDIR/\$i.mig >> /etc/\$i
done

echo "
Backup /home and /var/spool/mail in \$BACKUPDIR/tar ..."
tar -zcpf tar/home.tar.gz /home/*
tar -zcpf tar/mail.tar.gz /var/spool/mail/*
popd

echo "
Extract imported data in \$IMPORTDIR to /home and /var/spool/mail ..."
pushd /
echo "
gunzip \$IMPORTDIR/home.tar.gz to / ..." 
tar -zxf \$IMPORTDIR/home.tar.gz

echo "
gunzip \$IMPORTDIR/mail.tar.gz to / ..." 
tar -zxf \$IMPORTDIR/mail.tar.gz
popd

echo "
Import finished."
echo
echo "Please delete these files to finish the work:"
echo "  \$BACKUPDIR"
echo "  \$IMPORTDIR/d.import-groupuser"
echo "  \$IMPORTDIR/d.rollback-groupuser"
echo

VIRTUAL_EOF
chmod 700 $MIGRATEDIR/$IMPORTPROG
#------------------------------------------------

echo "Generate rollback script ..."
ROLLBACKPROG="d.rollback-groupuser"
cat > $MIGRATEDIR/$ROLLBACKPROG << VIRTUAL_EOF
#!/bin/bash

BACKUPDIR="/root/backupgroupuser"     #BACKUP OLD DATA

if [ "\$1" != "OK" ]; then
    PROG=\`basename $0\`
    cat << EOF

Move or migrate user accounts from old Linux server to a new Linux server
FROM: http://www.cyberciti.biz/faq/howto-move-migrate-user-accounts-old-to-new-server/

USE WITH CARE, PLEASE BACKUP OLD DATA, RUN AS ROOT, NO WARNING
- COPY BACKUP DATA IN \$BACKUPDIR/etc TO /etc
- *** REMOVE OLD /home AND /var/spool/mail ***
- COPY BACKUP DATA IN \$BACKUPDIR/tar TO /home and /var/spool/mail
- ROLLBACK \$BACKUPDIR/*.bak
 
USAGE:
    \$PROG OK

('OK' is safety argument.)

EOF
    exit 1
fi

if [ ! -d \$BACKUPDIR/etc ]; then
    echo "\$BACKUPDIR/etc not exist, program aborted"
    exit 1
fi
if [ ! -d \$BACKUPDIR/tar ]; then
    echo "\$BACKUPDIR/tar not exist, program aborted"
    exit 1
fi


echo "
Copy \$BACKUPDIR/etc to /etc ..."
pushd \$BACKUPDIR
cp etc/* /etc
popd


echo "
*** Remove /home and /var/spool/mail *** ..."
rm -rf /home /var/spool/mail

echo "
Copy \$BACKUPDIR/tar to /home and /var/spool/mail ..."
pushd /
tar -zxf \$BACKUPDIR/tar/home.tar.gz
tar -zxf \$BACKUPDIR/tar/mail.tar.gz
popd

echo "
Remove last rollback data ..."
pushd \$BACKUPDIR
rm -rf etc tar
for i in *.bak; do
    mv \$i \${i%.bak}
done

echo "
Rollback finished."

VIRTUAL_EOF
chmod 700 $MIGRATEDIR/$ROLLBACKPROG
#------------------------------------------------

echo "
Transfer data to root@$TARGETMACHINE:$TARGETDIR, enter $TARGETMACHINE root password:"
echo "COMMAND RUN: scp -r $MIGRATEDIR root@$TARGETMACHINE:$TARGETDIR"
scp -r $MIGRATEDIR/* root@$TARGETMACHINE:$TARGETDIR

echo "
Finished.

Next, run $MIGRATEDIR/$IMPORTPROG at $TARGETMACHINE
(*** Use $ROLLBACKPROG to undo the job, BUT DO USE WITH CARE ***)
"

เปลี่ยนสถานะให้รันได้
# chmod 700 /root/d.migrate-groupuser

ทดลองรัน
เริ่มที่เครื่องเก่า
# /root/d.migrate-groupuser OK

Copy /etc/{password,group,shadow,gshadow} to /root/migrategroupuser ...

gzip /home ...
tar: Removing leading `/' from member names

gzip /var/spool/mail ...
tar: Removing leading `/' from member names

Generate import script ...
Generate rollback script ...

Transfer data to root@newserver:/root/importgroupuser, enter newserver root password:
COMMAND RUN: scp -r /root/migrategroupuser root@newserver:/root/importgroupuser
root@newserver's password: <<<---NEWSERVER_ROOT_PASSWORD
d.import-groupuser                                            100% 1668     1.6KB/s   00:00    
d.rollback-groupuser                                          100% 1291     1.3KB/s   00:00    
group.mig                                                     100%   90     0.1KB/s   00:00    
gshadow.mig                                                   100%    0     0.0KB/s   00:00    
home.tar.gz                                                   100%   61MB  10.2MB/s   00:06    
mail.tar.gz                                                   100%   10KB  10.4KB/s   00:00    
passwd.mig                                                    100% 1287     1.3KB/s   00:00    
shadow.mig                                                    100% 2937     2.9KB/s   00:00    

Finished.

Next, run /root/migrategroupuser/d.import-groupuser at newserver
(*** Use d.rollback-groupuser to undo the job, BUT DO USE WITH CARE ***)

ย้ายไปทำที่เครื่อง newserver
# /root/importgroupuser/d.import-groupuser OK

Backup old data and add migrate data to /etc/{passwd,group,shadow,gshadow} ...
~/backupgroupuser ~

Backup /home and /var/spool/mail in /root/backupgroupuser/tar ...
tar: Removing leading `/' from member names
tar: Removing leading `/' from member names
~

Extract imported data in /root/importgroupuser to /home and /var/spool/mail ...
/ ~

gunzip /root/importgroupuser/home.tar.gz to / ...

gunzip /root/importgroupuser/mail.tar.gz to / ...
~

Import finished.

Please delete these files to finish the work:
  /root/backupgroupuser
  /root/importgroupuser/d.import-groupuser
  /root/importgroupuser/d.rollback-groupuser

เสร็จแล้ว เครื่องใหม่จะมีชื่อผู้ใช้งานและข้อมูลของผู้ใช้ครบตามเครื่องเก่าทุกประการ
หากสำเร็จเป็นที่พอใจแล้ว ควรลบไฟล์ต่าง ๆ ตามคำแนะนำนะครับ

Topic: 

bash: ลองทำ cron ตรวจ apt-proxy

แพกเกจ apt-proxy เป็นแพกเกจที่ใช้เป็นคลังเก็บแพกเกจที่เครือข่ายเราใช้ประจำ ใช้ง่ายและสะดวก แต่ชอบตายบ่อย จะเข้าไปรื้อดูการทำงาน ก็ยากเกินความสามารถ เลยลองเขียนเป็น cron แก้ปัญหาเฉพาะหน้าไปก่อน การทำงานของสคริปต์ก็ไม่มีอะไรมาก แค่ตรวจว่าการใช้คำสั่ง aptitude update นานเกิน 60 วินาทีหรือไม่ ถ้านานก็ให้เริ่ม apt-proxy ใหม่ แค่นี้เอง
(สามารถปรับช่วงเวลารอที่ตัวแปร TIME_WAIT ตามความเร็วของเน็ตที่มีอยู่จริง) # vi /usr/local/sbin/d.cron-check-apt-proxy
#!/bin/bash
TIME_WAIT='60'

function sub_wait() {
        sleep $TIME_WAIT 
        echo `date +%F-%R-%s`
}

function update_apt_proxy() {
        aptitude update
        echo `date +%F-%R-%s`
}

T1=`sub_wait` &
T2=`update_apt_proxy` &
wait
if [ "$T1" \< "$T2" ]; then
        echo "apt-proxy update longer than $TIME_WAIT seconds, restart apt-proxy." 
        /etc/init.d/apt-proxy restart
fi
# chmod 755 /usr/local/bin/d.cron-check-apt-proxy ตั้ง crontab ให้รันทุกชั่วโมง
# crontab -e
...
#CHECK apt-proxy EVERY 60 MIN
0 * * * *   /usr/local/sbin/d.cron-check-apt-proxy
...
เสร็จแล้ว ลองใช้ดูก่อน แล้วจะรายงานผลต่อไปตรับ update
  • เจอสาเหตุแล้ว มาจากตั้ง squid3 เป็นแบบ transparent ไว้ ทางแก้คือ ยกเลิก transparent หรือไม่ก็ปรับ apt-proxy ให้ไปใช้ http_proxy ที่พอร์ต 8080 (หรือพอร์ตอื่นที่ตั้งไว้ใน squid) เรียบร้อยแล้ว - แต่สคริปต์นี้ก็ยังน่าใช้อยู่ดี อาจปรับเป็นทุก 3 ชั่วโมงก็ได้

bash: สคริปต์คัดลอกผู้ใช้

สคริปต์คัดลอกผู้ใช้จากระบบปัจจุบันไปยังไดเรคทอรี่ที่ติดตั้งลินุกซ์อีกตัวหนึ่ง
มีประโยชน์สำหรับติดตั้งลินุกซ์หลายตัว และต้องการให้ผู้ใช้เหมือนกับระบบปัจจุบัน ตัวอย่างการใช้งานเช่น

  • ติดตั้งลินุกซ์ผ่าน debootstrap
  • ติดตั้งลินุกซ์โดยการเมานต์ live cd แล้วคัดลอก squashfs มาติดตั้งโดยตรง

ข้อกำหนดคือ

  • ต้องใช้สิทธิ์ root ในการรัน
  • ต้องเป็นลินุกซ์ที่มีการเก็บไฟล์ผู้ใช้แบบมาตรฐาน คือเก็บที่ไฟล์ /etc/passwd, /etc/group และ /etc/shadow

สคริปต์มีดังนี้

$ sudo vi /usr/local/sbin/transfer_users.sh
#!/bin/bash

function usage() {
    cat <<EOF
Usage: $0 DESTINATION
Transfer users from current linux system to DESTINATION directory that have another linux system.
Run as root.
EOF
    exit 1
}
    
DEST=$1

if [ ! "$UID" == "0" ]; then
    echo "Please run as root."
    usage
fi
if [ ! -d "$DEST" ]; then
    echo "DESTINATION directory not found."
    usage
fi
if [ ! -d "$DEST/etc" ]; then
    echo "DESTINATION/etc directory not found."
    usage
fi

US=`ls /home`
TMP=/tmp/${RANDOM}.txt

#SORT ON UID
for i in $US; do
    UUID=`grep ":/home/${i}:" /etc/passwd | cut -d: -f3`
    echo "${UUID}:${i}" >> $TMP
done

#PROCESS EACH USER
for i in `cat $TMP | sort`; do
    UUID=`echo $i | cut -d: -f1`
    U=`echo $i | cut -d: -f2`

    PASSWDLINE=`grep ":/home/${U}:" /etc/passwd`

    #/etc/group
    GNUM=`echo $PASSWDLINE | cut -d: -f4`
    GROUPLINE=`grep $GNUM /etc/group`
    GNAME=`echo $GROUPLINE | cut -d: -f1`
    OLDGROUP=`grep $GNAME $DEST/etc/group`
    if [ "$OLDGROUP" == "" ]; then
        echo $GROUPLINE >> $DEST/etc/group
    elif [ "$OLDGROUP" != "$GROUPLINE" ]; then
        sed -i "s/$OLDGROUP/$GROUPLINE/g" $DEST/etc/group
    fi

    #/etc/passwd
    OLDPASS=`grep ":/home/${U}:" $DEST/etc/passwd`
    if [ "$OLDPASS" == "" ]; then
        echo $PASSWDLINE >> $DEST/etc/passwd
    elif [ "$OLDPASS" != "$PASSWDLINE" ]; then
        sed -i "s/$OLDPASS/$PASSWDLINE/g" $DEST/etc/passwd
    fi

    #/etc/shadow
    SHADOWLINE=`grep "${U}:" /etc/shadow | grep -v '*'`
    OLDSHADOW=`grep "${U}:" $DEST/etc/shadow | grep -v '*'`
    if [ "$OLDSHADOW" == "" ]; then
        echo $SHADOWLINE >> $DEST/etc/shadow
    elif [ "$OLDSHADOW" != "$SHADOWLINE" ]; then
        sed -i "s#$OLDSHADOW#$SHADOWLINE#g" $DEST/etc/shadow
    fi

    #GROUP MEMBERS
    for j in `groups $U | cut -d: -f2`; do
        for k in `echo $j`; do
            if [ "$k" == "$GNAME" ]; then
                continue
            fi

            OLDLINE=`grep "${k}:" $DEST/etc/group`
            if ! echo $OLDLINE | grep $U ; then
                if [ "${OLDLINE: -1}" == ":" ]; then
                    sed -i "s/$OLDLINE/${OLDLINE}${U}/g" $DEST/etc/group
                else
                    sed -i "s/$OLDLINE/${OLDLINE},${U}/g" $DEST/etc/group
                fi
            fi
        done
    done
    
    #/HOME
    if [ ! -d "$DEST/home/$U" ]; then
        mkdir -p "$DEST/home/$U"
    fi
    chown -R ${U}:${GNAME} $DEST/home/$U
done

#for i in passwd group shadow; do
#    cp $DEST/etc/$i $DEST/etc/${i}-
#done
rm $TMP

ตัวอย่างเช่น เราติดตั้งลินุกซ์อีกอันไว้ที่ /dev/sdaXX

$ sudo mount /dev/sdaXX /mnt/tmp
$ sudo /usr/local/sbin/transfer_users.sh /mnt/tmp

ผู้ใช้ทั้งหมดใน /home/* จะถูกคัดลอกไปยัง /mnt/tmp/home/ ตามต้องการ

Topic: 

bash: สคริปต์บล๊อกผู้ใช้ Drupal

ทำสคริปต์บล๊อกผู้ใช้ Drupal จากบรรทัดคำสั่ง (bash)

$ vi drupal_blockuser.sh
#!/bin/bash

#FROM: https://drupal.org/node/118759

if [ "$#" == "0" ]; then
    echo "Script to block drupal users by uid"
    echo "Usage: $0 UID1 UID2 UID3 ..."
    exit 1
fi

SITE=http://www.example.com    #NO END SLASH
NAME=drupal_admin_user
PASS=drupal_admin_password

STATUS=0    #0:block,1:unblock
OPLOGIN="Log%20in"
OPSUBMIT="Save"
TMP=/tmp/$RANDOM.txt
COOKIES=/tmp/$RANDOM.txt

#LOGIN
wget -q -o /dev/null -O /dev/null \
    --keep-session-cookies --save-cookies $COOKIES --load-cookies $COOKIES \
    --post-data="name=${NAME}&pass=${PASS}&op=${OPLOGIN}&form_id=user_login" \
    "${SITE}/?q=user/login"

#DO BLOCK
while (( "$#" )); do
    ID=$1
    wget -q -o /dev/null -O $TMP \
        --keep-session-cookies --save-cookies $COOKIES --load-cookies $COOKIES \
        "${SITE}/?q=user/${ID}/edit"
    let "LINE=`grep -n 'value="user_profile_form"' $TMP | cut -d: -f1`-1"
    TOKEN=`sed -n -e "${LINE}p" $TMP | awk -F'value="' '{ print $2 }' | cut -d\" -f1`
    USER1=`grep 'id="edit-name"' $TMP | awk -F'value="' '{ print $2 }' | cut -d\" -f1`
    EMAIL=`grep 'id="edit-mail"' $TMP | awk -F'value="' '{ print $2 }' | cut -d\" -f1`
    wget -q -o /dev/null -O /dev/null \
        --keep-session-cookies --save-cookies $COOKIES --load-cookies $COOKIES \
        --post-data="status=${STATUS}&op=${OPSUBMIT}&name=${USER1}&mail=${EMAIL}&form_token=${TOKEN}&form_id=user_profile_form" \
        "${SITE}/?q=user/${ID}/edit"
    echo "USER:\"$USER1\" --- EMAIL:\"$EMAIL\" --- Blocked."

    shift
done

rm $TMP
rm $COOKIES
$ chmod 700 ./drupal_blockuser.sh

วิธีใช้งานก็สั่ง

$ ./drupal_blockuser.sh UID1 UID2 UID3 ...

อย่าลืมแก้ตัวแปร SITE, NAME, PASS ให้เข้ากับงานเรา

Topic: 

bash: สคริปต์บล๊อกผู้ใช้ Drupal แบบอัตโนมัติ

ปรับปรุงสคริปต์ให้สามารถบล๊อกโดยอัตโนมัติ

$ vi drupal_auto_blockuser.sh
#!/bin/bash
# AUTO BLOCK DRUPAL USER SCRIPT, CHECK SPAM FROM google.co.th
# FROM: https://drupal.org/node/118759

SITE=http://www.example.com     # NO TRAILING SLASH
NAME=drupal_admin_user
PASS=drupal_admin_password
UIDFILE="site_last_uid.txt"

SPAMDATA="spamdata.txt"
SLEEP=10    #REDUCE SERVER LOAD
STATUS=0    #0:block,1:unblock
OPLOGIN="Log%20in"
OPSUBMIT="Save"
TMP=/tmp/$RANDOM.txt
COOKIES=/tmp/$RANDOM.txt
GGCOOKIES=/tmp/$RANDOM.txt
AGENT="Mozilla/5.0 (X11; Linux x86_64; rv:25.0) Gecko/20130712 Firefox/25.0"
QURL="https://www.google.co.th/search?q="

# LOGIN
wget -q -o /dev/null -O /dev/null \
    --keep-session-cookies --save-cookies $COOKIES --load-cookies $COOKIES \
    --post-data="name=${NAME}&pass=${PASS}&op=${OPLOGIN}&form_id=user_login" \
    "${SITE}/?q=user/login"

# PREVIOUS USER ID
PREVUID=`cat $UIDFILE`
let "STARTUID=${PREVUID}+1"

# GET LAST USER ID
wget -q -o /dev/null -O $TMP \
    --keep-session-cookies --save-cookies $COOKIES --load-cookies $COOKIES \
    "${SITE}/?q=admin/user/user"
LASTUID=`grep -m1 'class="form-item" id="edit-accounts-' $TMP | awk -F'class="form-item" id="edit-accounts-' '{ print $2 }' | cut -d\- -f1`

#DO CHECK
for ID in `eval echo {$STARTUID..$LASTUID}`; do
    wget -q -o /dev/null -O $TMP \
        --keep-session-cookies --save-cookies $COOKIES --load-cookies $COOKIES \
        "${SITE}/?q=user/${ID}/edit"

    LINE0=`grep -n 'value="user_profile_form"' $TMP | cut -d: -f1`
    # PREVENT BLOCKED USER ERROR
    if ! [ "$LINE0" ]; then
        continue
    fi
    let "LINE=${LINE0}-1"
    TOKEN=`sed -n -e "${LINE}p" $TMP | awk -F'value="' '{ print $2 }' | cut -d\" -f1`
    USER1=`grep 'id="edit-name"' $TMP | awk -F'value="' '{ print $2 }' | cut -d\" -f1`
    EMAIL=`grep 'id="edit-mail"' $TMP | awk -F'value="' '{ print $2 }' | cut -d\" -f1`

    # QUERY FOR SPAM
    wget -q -o /dev/null -O $TMP \
        --keep-session-cookies --save-cookies $GGCOOKIES --load-cookies $GGCOOKIES \
        -U "$AGENT" "${QURL}${EMAIL}"

    ISSPAM=0
    KEYWORD=""
#***********************
    ### BEWARE THIS WHILE LOOP, VARIABLE $ISSPAM IS LOCAL, USE REDIRECT INSTEAD OF PIPE ###
    #cat $SPAMDATA | while read TXT; do
    #    if [ "$TXT" ]; then
    #        if grep -m1 "$TXT" $TMP > /dev/null; then
    #            ISSPAM=1
    #            break
    #        fi
    #    fi
    #done
#***********************
    while read TXT; do
        if [ "$TXT" ]; then
            if grep -m1 "$TXT" $TMP > /dev/null; then
                ISSPAM=1
                KEYWORD=$TXT
                break
            fi
        fi
    done < <(cat $SPAMDATA )

    if [ "$ISSPAM" != "0" ]; then
        #DO BLOCK
        wget -q -o /dev/null -O /dev/null \
            --keep-session-cookies --save-cookies $COOKIES --load-cookies $COOKIES \
            --post-data="status=${STATUS}&op=${OPSUBMIT}&name=${USER1}&mail=${EMAIL}&form_token=${TOKEN}&form_id=user_profile_form" \
            "${SITE}/?q=user/${ID}/edit"
        echo "UID:\"$ID\", USER:\"$USER1\", EMAIL:\"$EMAIL\", KEYWORD:\"$KEYWORD\" --- Blocked."
        sleep $SLEEP
    else
        echo "--- UID:\"$ID\", USER:\"$USER1\", EMAIL:\"$EMAIL\", not found as spam."
    fi
    sleep $SLEEP
done

# SAVE LAST UID
echo $LASTUID > $UIDFILE

rm $TMP
rm $COOKIES
rm $GGCOOKIES
$ chmod 700 drupal_auto_blockuser.sh

สร้างไฟล์ spamdata.txt

ใส่ค่า spam keyword

$ vi spamdata.txt
did not match any documents
Spam
ไม่ตรงกับเอกสารใด

สร้างไฟล์ site_last_uid.txt

ใส่ค่า uid ของผู้ใช้คนสุดท้ายที่ไม่ต้องการตรวจ (สมมุติว่าเป็นผู้ใช้คนที่ 1000)

$ vi site_last_uid.txt
1000

รัน

$ ./drupal_auto_blockuser.sh

ครั้งต่อไปก็แค่สั่งรัน โดยไม่ต้องปรับแต่งอะไรอีก เว้นแต่มี spam keyword เพิ่มเติมก็ไปแก้ไฟล์ spamdata.txt

ปรับปรุง

  • เพิ่ม cookies ให้กับ google เพื่อไม่ให้ google คิดว่าเป็น bot
Topic: 

bash: สคริปต์แก้ Boot record ของ NTFS

update 2556-12-03

ลองใช้สคริปต์กับพาร์ติชั่นที่ขนาดไม่เท่ากันแล้วปรากฎว่าใช้ไม่ได้ เพราะ NTFS เก็บข้อมูลหลายอย่างมากกว่าแค่จุดเริ่มต้นและขนาด (ดูที่ NTFS Partition Boot Sector)

วิธีที่ได้ผลกว่าคือ ฟอร์แมตไดร์ฟไว้ก่อน -> เก็บ boot sector ไว้ 72 ไบต์ -> ทำ ntfsclone -> เอา boot recort ที่เก็บไว้มาเขียนทับ

ตัวอย่างเช่น จะคัดลอก ไดร์ฟ /dev/sda1 ไปยัง /dev/sdb1 ขั้นตอนจะเป็นดังนี้

$ sudo mkfs.ntfs -f /dev/sdb1
$ sudo dd if=/dev/sdb1 of=sdb1.img bs=72 count=1
$ sudo ntfsclone -O /dev/sdb1 /dev/sda1
$ sudo dd if=sdb1.img of=/dev/sdb1

*** สคริปต์ด้านล่างนี้ ล้าสมัยแล้ว ***

แก้ปัญหาเวลาใช้ ntfsclone ในการ restore พาร์ติชั่น NTFS มาลงในฮาร์ดดิสก์ลูกใหม่ ซึ่งจุดเริ่มต้นและขนาดอาจไม่เท่าของเดิม

วิธีการคือใช้ข้อมูลจากตาราง Master Boot Record ปัจจุบัน มาเขียนทับ boot record ของพาร์ติชั่น NTFS ที่ต้องการ โดยใช้เชลล์สคริปต์

$ vi ntfs_fix_boot_sector.sh
#!/bin/bash

function usage() {
    cat <<EOF
Fix NTFS boot record:
Usage: $0 DEVICE
Example: $0 /dev/sda1
EOF
    exit 1
}

if [ ! "$1" ]; then
    usage
fi

PART=$1

if [ ! -b "$PART" ]; then
    echo -e "$PART not found. Exit.\n"
    usage
fi

BSF="`echo $PART | tr '/' '_'`.img"
BSFB=${BSF}.bak
BSFD="${BSF}_`date +%F`.bak"

function reverse_byte () {  #reverse_byte HEXSTR
    local S=$1  #HEXSTR
    local B
    local C
    while [ "$S" ]; do
        B=${S:(-2)}
        S=${S:0:-2}
        C="${C}\\x${B}"
    done
    echo $C
}

function replace_byte () {  #replace_byte OFFSET LENGTH NUMBER
    local O=$1  #OFFSET
    local L=$2  #LENGTH
    local N=$3  #NUMBER
    let NL=${L}*2
    XN=`printf "%0${NL}x" $N`
    RXN=`reverse_byte $XN`
    #echo "printf $RXN | dd of=$BSF bs=1 seek=$O count=$L conv=notrunc"
    printf $RXN | sudo dd of=$BSF bs=1 seek=$O count=$L conv=notrunc > /dev/null 2>&1
}

sudo dd if=$PART of=$BSF bs=512 count=1 > /dev/null 2>&1

TMP="/tmp/$0_${RANDOM}.txt"

HDD=$PART
while [ "`echo ${HDD:(-1)} | tr '0123456789' ' '`" == " " ]; do
    HDD=${HDD:0:-1}
done

sudo fdisk -l $HDD > $TMP

if ! cat $TMP | grep $PART | grep NTFS > /dev/null 2>&1; then
    echo -e "$PART is not NTFS partition. Exit.\n"
    sudo rm $TMP
    sudo rm $BSF
    usage
fi

if [ ! -f "$BSFB" ]; then
    cp $BSF $BSFB
else
    cp $BSF $BSFD
fi

echo "Fixing $PART ..."

HEADS=`cat $TMP | grep 'sectors/track' | cut -d, -f1 | cut -d\  -f1`
SECTORS=`cat $TMP | grep 'sectors/track' | cut -d, -f2 | cut -d\  -f2`
START=`cat $TMP | grep "${PART} " | awk -F' ' '{ print $2 }'`
END=`cat $TMP | grep "${PART} " | awk -F' ' '{ print $4 }'`
if [ "$START" == "*" ]; then
    START=`cat $TMP | grep "${PART} " | awk -F' ' '{ print $3 }'`
    END=`cat $TMP | grep "${PART} " | awk -F' ' '{ print $4 }'`
fi
let LENGTH=$END-$START

OFFSET_HEADS=26
OFFSET_SECTORS=24
OFFSET_START=28
OFFSET_LENGTH=40
LEN_HEADS=1
LEN_SECTORS=1
LEN_START=4
LEN_LENGTH=4

for i in HEADS SECTORS START LENGTH; do
    A="OFFSET_${i}"
    B="LEN_${i}"
    replace_byte ${!A} ${!B} ${!i}
done

echo "Fix with heads=$HEADS, sectors/track=$SECTORS, start=$START, length=$LENGTH"

sudo dd if=$BSF of=$PART > /dev/null 2>&1

sudo rm $TMP

cat <<EOF

Command used:
sudo dd if=$BSF of=$PART

Revert with command:
sudo dd if=$BSFB of=$PART
EOF

ตัวอย่าง สมมุติว่าพาร์ติชั่นที่ต้องการเป็น /dev/sda1 คำสั่งคือ

$ ntfs_fix_boot_record.sh /dev/sda1

ได้ผลลัพธ์คือ

Fixing /dev/sda1 ...
Fix with heads=255, sectors/track=63, start=2048, length=62914559

Command used:
sudo dd if=_dev_sda1.img of=/dev/sda1

Revert with command:
sudo dd if=_dev_sda1.img.bak of=/dev/sda1

ที่มา:

bash: เกร็ดคำสั่ง find

bash: เกร็ดคำสั่ง find

(ศึกษาเพราะต้องการเอาไฟล์ในคลังของ apt-proxy เฉพาะไฟล์ใหม่ ๆ เลยต้องการลบไฟล์เก่า ๆ ทิ้ง เพื่อให้ขนาดคลังแพ็กเกจเล็กลง)

เริ่มเลยครับ
ต้องการค้นหาไฟล์ชื่อ *Doc*
$ find /PATH/TO/FILE -name '*Doc*'

ค้นหาและลบไฟล์
$ find /PATH/TO/FILE -name '*Doc*' -exec rm {} \;

ค้นหาไฟล์ที่เก่ากว่า 5 วันลงไป
$ find /PATH/TO/FILE -mtime +5

ค้นหาไฟล์เก่าตั้งแต่ 5 วันขึ้นมา
$ find /PATH/TO/FILE -mtime -5

ค้นหาไฟล์เก่ากว่า 1 ปีลงไป และลบไฟล์เหล่านั้นทิ้ง
$ find /PATH/TO/FILE -mtime +365 -exec rm {} \;

ค้นหาไฟล์ที่มีขนาด 0 byte และลบไฟล์
$ find /PATH/TO/FILE -type f -size 0 -exec rm {} \;

*** ใช้ด้วยความระมัดระวังนะครับ ***

วันนี้แค่นี้ก่อนครับ

อ้างอิง

bash: แตะ Shell Script

ขออนุญาตเขียนแบบกองโจรนะครับ โดยมือใหม่ เพื่อมือใหม่ครับ

เอามาจาก tldp: BASH Programming - Introduction HOW-TO

ศึกษาเพิ่มเติมได้จาก tldp: Bash Guide for Beginners
และในเชิงลึก จาก tldp: Advanced Bash-Scripting Guide

1.เกริ่น

เชลล์สคริปต์ พูดง่าย ๆ ก็คือการนำคำสั่งในเชลล์ของลินุกซ์มาเรียงต่อกันให้ทำงานตามที่เราต้องการ โดยเพิ่มโครงสร้างการวนรอบ และฟังก์ชั่นต่าง ๆ เติมเข้ามา เพื่อให้การทำงานได้ตามที่เราต้องการ ซึ่งจะเหมาะมากกับงานแบบ batch หรืองานแบบ schedule

ฉะนั้นการที่จะเขียนโค๊ดให้ได้ดี จึงต้องศึกษาจดจำคำสั่งต่าง ๆ ของเชลล์ให้ได้เท่าที่เราต้องการใช้งาน (จำหมดคงไม่ไหว)
คำสั่งต่าง ๆ สามารถดูได้ที่ gnu.org: Bash Reference Manual

สำหรับเดเบียน หากต้องการใช้งาน bash แบบเต็มรูป (ไม่อั้นความสามารถ) อาจต้องปรับแต่งเล็กน้อย
เปลี่ยนให้เชลล์ของเราเป็น bash แทน sh ใช้คำสั่ง

$ chsh -s /bin/bash

สำหรับเอดิเตอร์ ถ้าใช้ vi ควรติดตั้ง vim-full และอย่าลืมแก้ไขไฟล์ vimrc ให้แสดงสีด้วย เพื่อให้ดูโค๊ดได้ง่ายขึ้น

$ sudo aptitude install vim-full
$ vi ~/.vimrc
syntax on
:wq

2.เริ่มเขียน

2.1 สคริปต์ Hello World

สมมุติตั้งชื่อสคริปต์ว่า hello.sh

$ vi hello.sh
#!/bin/bash          
echo Hello World
:wq

อย่าลืมเปลี่ยนสถานะเพื่อให้สคริปต์สามารถรันได้

$ chmod 755 hello.sh

เริ่มรัน

$ ./hello.sh
Hello World

เรียบร้อยแล้ว

บรรทัดแรก เรียกว่า hash-bang เป็นการบอกให้เชลล์รู้ว่า โค๊ดที่เราเขียนนี้จะถูกประมวลผลด้วยโปรแกรมอะไร ในที่นี้คือ /bin/bash
บรรทัดที่สอง เป็นการสั่งให้พิมพ์ Hello World ออกทางจอภาพ

2.2 สคริปต์สำหรับสำรองข้อมูล

จากตัวอย่างข้างบน ผมเขียนอธิบายโดยละเอียดโดยใช้เอดิเตอร์ vi แต่เพื่อให้กระชับเข้า จะขอละเลยการใช้เอดิเตอร์ โดยจะเขียนเฉพาะโค๊ดอย่างเดียวครับ

#!/bin/bash          
tar -cvzf /tmp/my-backup.tgz /home/USER/

บรรทัดที่สองให้เปลี่ยนคำว่า USER เป็นชื่อเรา
เป็นการสั่งให้ใช้คำสั่ง tar ทำการสำรองข้อมูลพร้อมบีบอัดข้อมูลในไดเรคทอรี่ของบ้านเราไปสู่ไฟล์ชื่อ /tmp/my-backup.tgz

3. การเปลี่ยนทิศข้อมูล (Redirection)

ใช้สัญญลักษณ์ > ใสการเปลี่ยนทิศ

3.1 ข้อมูลมาตรฐาน

ข้อมูลมาตรฐานในเชลล์จะมีอยู่ 4 ชนิด คือข้อมูลเข้า(stdin), ข้อมูลแสดงผล(stdout), ข้อมูลข้อผิดพลาด(stderr), และแฟ้มข้อมูล(file)
ในทางปฏิบัติ เราสามารถเปลี่ยนทิศทางของข้อมูลเหล่านี้ไปมาได้ โดยมีมาตรฐานคือ 1 จะหมายถึงข้อมูลแสดงผล(stdout) และ 2 จะหมายถึงข้อมูลความผิดพลาด(stderr)

เช่น

3.2 ตัวอย่างเปลี่ยน stdout ไปเป็น file

$ ls -l > ls-l.txt

จะเปลี่ยนการแสดงผลของคำสั่ง ls -l ไปเก็บไว้ที่ไฟล์ชื่อ ls-l.txt ดังนั้นคำสั่งตามตัวอย่างนี้จะไม่แสดงอะไรออกมาทางจอภาพ แต่จะเก็บไว้ที่ไฟล์แทน หากเราต้องการดูผล สามารถใช้คำสั่งแสดงผลของไฟล์ได้คือ

$ cat ls-l.txt

3.3 ตัวอย่างเปลี่ยน stderr ไปเป็น file

$ grep da * 2> grep-errors.txt

ตัวอย่างนี้เป็นการค้นหาข้อความ da ในทุกไฟล์ (*) และหากเกิดข้อผิดพลาดขึ้น จะนำข้อความผิดพลาดไปเก็บไว้ที่ไฟล์ชื่อ grep-errors.txt

3.4 ตัวอย่างเปลี่ยน stdout ไปเป็น stderr

$ grep da * 1>&2

เป็นการค้นหาข้อความ da ในทุกไฟล์ (*) โดยนำการแสดงผลไปใส่ไว้ใน stderr แทนการแสดงผลปกติ แต่ในกรณีนี้เราป้อนคำสั่งทางแป้นพิมพ์ stdout และ stderr คือจอภาพเหมือนกัน จึงไม่เห็นความแตกต่าง แต่หากคำสั่งนี้ไปอยู่ในสคริปต์ที่เรากำหนดให้ stderr เป็นไฟล์ error-log การแสดงผลก็จะถูกเปลี่ยนทิศไปตามนั้น

3.5 ตัวอย่างเปลี่ยน stderr ไปเป็น stdout

$ grep da * 2>&1

เป็นการค้นหาข้อความ da ในทุกไฟล์ (*) โดยหากเกิดข้อผิดพลาดขึ้น จะแสดงผลข้อผิดพลาดออกมาทาง stdout ซึ่งในที่นี้คือจอภาพเหมือนกัน

3.6 ตัวอย่างเปลี่ยน stderr และ stdout ไปยัง file

$ rm -f $(find /home/USER -name core) &> /dev/null

คำสั่งนี้เป็นการค้นหาไฟล์ในไดเรคทอรี่ /home/USER ที่มีชื่อว่า core (find /home/USER -name core)
เมื่อพบแล้วก็จัดการลบทิ้งโดยไม่เตือน (rm -f)
โดยโยกการแสดงผลทั้งหมด (ทั้ง stderr และ stdout - ใช้สัญญลักษณ์ &>) ไปยังไฟล์ชื่อ /dev/null ซึ่งเป็นไฟล์พิเศษ หมายความว่ายกเลิกการแสดงผลทั้งหมด
(คำสั่งนี้ค่อนข้างอันตราย เพราะลบโดยไม่เตือน โปรดทดลองด้วยความระมัดระวังครับ)

4. การส่งต่อผลลัพธ์ หรือ ไปป์ (Pipes)

4.1 ความหมาย

ไปป์เป็นการส่งต่อผลลัพธ์จากคำสั่งหนึ่งไปเป็นค่านำเข้าของอีกคำสั่งหนึ่ง

4.2 ตัวอย่างไปป์

$ ls -l | sed -e "s/[aeio]/u/g"

ตัวอย่างนี้จะนำเอาผลลัพธ์ที่ได้จากคำสั่ง ls -l ส่งต่อไปให้คำสั่ง sed -e "s/[aeio]/u/g"
ซึ่งจะแปลงการแสดงผลจากอักขระ a หรือ e หรือ i หรือ o ไปเป็นอักขระ u ทั้งหมด

เราอาจเขียนคำสั่งเทียบเท่าได้ดังนี้

$ ls -l > temp.txt
$ sed -e "s/[aeio]/u/g" temp.txt
$ rm temp.txt

จะเห็นว่าการทำไปป์ ลดขั้นตอนไปมาก คงเหลือเพียงบรรทัดเดียว

4.3 ตัวอย่างไปป์ที่สอง

$ ls -l | grep "\.txt$"

ตัวอย่างนี้จะส่งผลลัพธ์จากคำสั่ง ls -l ต่อไปให้คำสั่ง grep "\.txt$" คือให้แสดงเฉพาะไฟล์ที่มีนามสกุลเป็น .txt เท่านั้น
มีค่าเท่ากับคำสั่ง ls แบบใส่พารามิเตอร์กรอง

$ ls -l *.txt

หมายเหตุ
รูปแบบ "\.txt$" เป็นรูปแบบของ Regular Expression ซึ่งใช้มากในเชลล์สคริปต์ มีความหมายว่า "ที่ต้องลงท้ายด้วย .txt"

5. ตัวแปร (Variables)

ตัวแปรในเชลล์สคริปต์ ไม่มีชนิดข้อมูล คือเราสามารถใช้ตัวแปรแทนตัวเลขหรืออักขระใด ๆ ก็ได้
โดยในขั้นตอนกำหนดค่า ไม่ต้องใช้เครื่องหมายใด ๆ นำหน้า แต่ตอนอ้างถึง ต้องใช้เครื่องหมาย $ นำหน้าตัวแปร

5.1 ตัวอย่างสคริปต์ Hello World แบบใช้ตัวแปร

#!/bin/bash          
STR="Hello World!"
echo $STR

ให้ผลลัพธ์เหมือนตัวอย่างที่ 2.1
ข้อควรระวังคือ

  • การกำหนดค่าให้ตัวแปร อย่าเว้นวรรคระหว่างตัวแปรกับเครื่องหมาย =
  • หากลืมใส่เครื่องหมาย $ จะหมายถึงการแสดงผลข้อความว่า STR เฉย ๆ

5.2 ตัวอย่างสคริปต์สำรองข้อมูลแบบใช้ตัวแปร

#!/bin/bash          
OF=/tmp/my-backup-$(date +%Y%m%d).tgz
tar -cvzf $OF /home/USER/

ให้ผลลัพธ์คล้ายตัวอย่าง 2.2 แต่เพิ่มการใช้ตัวแปรลอยในคำสั่ง $(date +%Y%m%d) ซึ่งมีผลทำให้ชื่อไฟล์ข้อมูลสำรองมีวันที่ต่อท้ายชื่อด้วย

5.3 ตัวแปรท้องถิ่น

ตัวแปรในเชลล์สคริปต์ทุกตัว จะเป็นตัวแปรรวม (Global) คือทุก ๆ ส่วนของโปรแกรมจะเห็นเหมือนกันหมด
แต่ในกรณีที่เราต้องการให้เห็นเฉพาะในฟังก์ชั่นที่เราต้องการ เราสามารถกำหนดให้ตัวแปรเป็นตัวแปรท้องถิ่นได้ด้วยคำสั่ง local
เช่น

#!/bin/bash
HELLO=Hello 
function hello {
        local HELLO=World
        echo $HELLO
}

echo $HELLO
hello
echo $HELLO

สคริปต์นี้ตัวแปร HELLO ในโปรแกรมหลัก กับในฟังก์ชั่นจะเป็นตัวแปรคนละตัวกัน

6. ประโยคเงื่อนไข

6.1 รูปแบบ

มีรูปแบบคือ

if [EXPRESSION]; then
    CODE IF 'EXPRESSION' IS TRUE.
[elif [EXPRESSION-ELIF]; then
    CODE IF 'EXPRESSION-ELIF' IS TRUE.]
[else
    CODE IF NOTHING IS TRUE.]
fi

6.2 ตัวอย่าง if ... then

#!/bin/bash
if [ "foo" = "foo" ]; then
    echo expression evaluated as true
fi

โค๊ดนี้จะเป็นจริงเสมอ ดังนั้นข้อความ "expression evaluated as true" จะถูกพิมพ์ออกมาเสมอ

6.3 ตัวอย่าง if ... then ... else

#!/bin/bash
if [ "foo" = "foo" ]; then
   echo expression evaluated as true
else
   echo expression evaluated as false
fi

โค๊ดนี้จะเป็นจริงเสมอ ดังนั้นข้อความ "expression evaluated as true" จะถูกพิมพ์ออกมาเสมอ

6.4 ตัวอย่างแบบใช้ตัวแปร

#!/bin/bash
T1="foo"
T2="bar"
if [ "$T1" = "$T2" ]; then
    echo expression evaluated as true
else
    echo expression evaluated as false
fi

ตัวอย่างนี้จะเป็นเท็จเสมอ
สังเกตุการใช้ตัวแปรในการเปรียบเทียบ ควรให้ตัวแปรอยู่ในเครื่องหมายคำพูดเสมอ เพื่อป้องการการผิดพลาดจากการแทนค่าที่ซับซ้อน หรือการที่มีช่องว่างในค่าตัวแปร

7.การวนรอบ โดยใช้คำสั่ง for, while และ until

คำสั่ง for มีลักษณะคล้าย for ในภาษาไพธอน มีรูปแบบเป็น

for VAR in SCOPE; do
    COMMAND
done

คำสั่ง while มีรูปแบบเป็น

while [CONDITION]; do
    COMMAND
done

ถ้าเงื่อนไข CONDITION เป็นจริง ก็จะทำคำสั่ง COMMAND คำสั่ง until รูปแบบตรงกันข้ามกับ while โดยมีรูปแบบเป็น

until [CONDITION]; do
    COMMAND
done

คือจะทำคำสั่ง COMMAND จนกว่าเงื่อนไข CONDITION จะเป็นจริง

7.1 ตัวอย่าง for

#!/bin/bash
for i in $( ls ); do
    echo item: $i
done

เป็นการนำคำสั่ง ls ไปเป็นตัวแปรชั่วคราวในการกำหนดขอบเขตให้กับตัวแปร i ในคำสั่ง for ในที่นี้จะทำการแสดงผลว่า item: FILENAME ...

7.2 ตัวอย่าง for อีกแบบ

#!/bin/bash
for i in `seq 1 10`; do
    echo $i
done

เป็นการนำผลจากคำสั่ง seq 1 10 ไปกำหนดขอบเขตให้กับตัวแปร i ในคำสั่ง for อาจเขียนเลียนแบบตัวอย่าง 7.1 ได้เหมือนกันดังนี้

#!/bin/bash
for i in $( seq 1 10 ); do
    echo $i
done

7.3 ตัวอย่าง while

#!/bin/bash 
COUNTER=0
while [  $COUNTER -lt 10 ]; do
    echo The counter is $COUNTER
    let COUNTER=COUNTER+1 
done

เป็นการแสดงค่าตัวแปร COUNTER ที่เพิ่มขึ้นทีละ 1 จาก 0 ถึง 9 โปรดสังเกตุการใช้ตัวแปรเก็บค่าตัวเลข, การเปรียบเทียบตัวเลขโดยใช้ตัวเปรียบเทียบ -lt (less than) และการกำหนดเพิ่มค่าให้กับตัวแปรแบบตัวเลขโดยใช้คำสั่ง let

7.4 ตัวอย่าง until

#!/bin/bash 
COUNTER=20
until [  $COUNTER -lt 10 ]; do
    echo COUNTER $COUNTER
    let COUNTER-=1
done

จะแสดงตัวเลขตั้งแต่ 20 ลดลงทีละ 1 จนถึง 10

8.ฟังก์ชั่น (functions)

ในการใช้งานเชลล์สคริปต์แบบจริงจัง เราจำเป็นต้องเขียนฟังก์ชั่นเพื่อประโยชน์ในการเรียกใช้งานแบบซ้ำ ๆ เพื่อให้ประหยัดการเขียนโค๊ด และให้โค๊ดดูง่าย
มีรูปแบบเป็น

function FUNCTION_NAME {
    COMMAND
}

หรือ

 FUNCTION_NAME () {
    COMMAND
}

โปรแกรมจะเว้นไม่ถูกเรียกทำงานในช่วงตั้งแต่ชื่อฟังก์ชั่นจนกระทั่งจบบล๊อก { COMMAND }
เรานิยมวางฟังก์ชั่นไว้ที่ต้นโปรแกรม เพื่อให้สามารถถูกเรียกจากโค๊ดหลักได้

8.1 ตัวอย่างฟังก์ชั่น

#!/bin/bash 
function quit {
    exit
}
function hello {
    echo Hello!
}
hello
quit
echo foo

ตัวอย่างนี้ บรรทัดที่ 10 คือคำสั่ง echo foo จะไม่ถูกเรียกใช้ เนื่องจากโปรแกรมจะหลุดสู่เชลล์ในบรรทัดที่ 9 คือคำสั่ง quit

8.2 ตัวอย่างฟังก์ชั่นที่มีการส่งผ่านค่าตัวแปร

#!/bin/bash 
function quit {
    exit
}  
function ex {
    echo $1 
}  
ex Hello
ex World
quit
echo foo

จากตัวอย่าง จะเห็นการส่งผ่านข้อความเข้าไปในฟังก์ชั่น ex ด้วยตัวแปร $1
ในทำนองเดียวกัน ถ้ามีการส่งผ่านตัวแปรหลายตัว ก็จะใช้รูปแบบเป็น $2, $3, ...
โดยเรียกใช้งานด้วยรูปแบบ ex VAR1 VAR2 VAR3 ... ตามลำดับ

9.การติดต่อผู้ใช้ (User Interfaces)

9.1 ใช้คำสั่ง select ในการสร้างหัวข้อให้เลือก

#!/bin/bash
OPTIONS="Hello Quit"
select opt in $OPTIONS; do
    if [ "$opt" = "Quit" ]; then
        echo done
        exit
    elif [ "$opt" = "Hello" ]; then
        echo Hello World
    else
        clear
        echo bad option
    fi
done

ตัวอย่างนี้จะสร้างหัวข้อ 1) และ 2) จากตัวแปร OPTIONS เพื่อมาให้เลือก โดยจะวนรอบถามไปเรื่อย ๆ จนกว่าจะพบคำสั่ง exit ให้ออกจากการวนรอบ

9.2 ใช้การตรวจสอบว่ามีการใส่ค่าพารามิเตอร์หรือไม่

#!/bin/bash        
if [ -z "$1" ]; then 
   echo usage: $0 directory
   exit
fi
SRCD=$1
TGTD="/var/backups/"
OF=home-$(date +%Y%m%d).tgz
tar -cZf $TGTD$OF $SRCD

บรรทัดที่ 2 จะตรวจว่ามีการใส่พารามิเตอร์ให้กับโปรแกรมหรือไม่ (if [ -z "$1" ] -z หมายถึงการตรวจสอบว่ามีค่าหรือไม่)
ถ้าไม่มีการใส่ค่าพารามิเตอร์ โปรแกรมจะทำคำสั่งในบรรทัดที่ 3 คือแสดงการใช้งาน ($0 คือชื่อโปรแกรมนี้) และบรรทัดที่ 4 คือออกจากโปรแกรม
แต่ถ้ามีการใส่ค่าพารามิเตอร์ถูกต้อง ก็จะทำบรรทัดที่ 6 ต่อไปจนจบ ซึ่งในที่นี้คือการบีบอัดทำสำเนาให้กับไดเรกทอรี่ที่เราให้เป็นพารามิเตอร์ ($1) ในชื่อไฟล์ว่า /var/backups/home-YYYYMMDD

9.3 หยุดถามผู้ใช้ด้วยคำสัง read

#!/bin/bash
echo Please, enter your name
read NAME
echo "Hi $NAME!"

สังเกตุการใช้คำสั่ง read กำหนดค่าให้ตัวแปร NAME ไม่ต้องใช้เครื่องหมาย $ นำหน้าตัวแปร

อาจรอรับค่าทีละหลายตัวแปรได้ด้วย โดยคั่นแต่ละตัวแปรด้วยช่องว่าง

#!/bin/bash
echo Please, enter your firstname and lastname
read FN LN 
echo "Hi! $LN, $FN !"

10.เกร็ดอื่น ๆ

10.1 การสั่งรันสคริปต์และคำสั่ง source

การสั่งรันสคริปต์ในเชลล์ มีเกร็ดคือ

  • ถ้าเราใส่ชื่อสคริปต์พร้อมพาธ เชลล์จะค้นหาสคริปต์จากชื่อเต็มที่เราใส่ เช่น
    $ /bin/ls
  • ถ้าเราใส่ชื่อสคริปต์โดด ๆ เชลล์จะค้นหาสคริปต์จากตัวแปร $PATH โดยไม่สนใจไดเรคทอรี่ปัจจุบัน เช่น
    $ mycode

    หากค้นไม่พบ จะแสดงข้อผิดพลาด
    แต่หากต้องการสั่งรันสคริปต์ในไดเรคทอรี่ปัจจุบัน เราต้องใช้คำสั่งอ้างอิงคือ

    $ ./mycode

เมื่อสคริปต์ถูกรันจนจบแล้ว ค่าของตัวแปรต่าง ๆ ในสคริปต์จะถูกลบไปด้วย ยกเว้นถ้าเราใช้คำสั่ง source หรือคำสั่ง .
เชลล์จะรันคำสั่งนั้นโดยถือเสมือนเป็นสภาพแวดล้อมเดียวกัน ดังนั้นค่าตัวแปรต่าง ๆ ในสคริปต์จะยังคงค้างอยู่ในเชลล์
โดยเมื่อใช้คำสั่งนี้แล้ว การค้นหาสคริปต์ เชลล์จะค้นหาจากตัวแปร $PATH ก่อน ตามด้วยไดเรคทอรี่ปัจจุบันด้วย
เช่น ถ้าสคริปต์ mycode มีเนื้อไฟล์เป็น

#!/bin/bash
ABC="This is new ABC"

ทดลองรันได้ดังนี้

$ ABC="Old ABC"
$ echo $ABC
Old ABC
$ ./mycode
$ echo $ABC
Old ABC
$ . mycode
$ echo $ABC
This is new ABC

10.2 การแทนค่าตัวเลข

เราใช้ $((ARITHMATIC)) หรือ $[ARITHMATIC] ในการแทนค่าตัวแปร
ดังนี้

$ echo $(1+1)
bash: 1+1: command not found

$ echo 1+1
1+1
$ echo $((1+1))
2
$ echo $[1+1]
2

10.3 bash อยู่ที่ไหน

บรรทัดเริ่มต้นของสคริปต์ หลังเครื่องหมาย #! (hash-bang) เราต้องใส่พาธของโปรแกรม bash ให้เต็ม
สำหรับเดเบียน อยู่ที่ /bin/bash อยู่แล้ว แต่หากเป็นดิสโตรอื่น อาจค้นหาว่าโปรแกรม bash อยู่ที่ไหน โดยใช้คำสั่งเหล่านี้

$ which bash
$ whereis bash
$ find / -name bash

10.4 ดูค่าที่โปรแกรมส่งออกมา

หลายโปรแกรมของเชลล์มีการส่งค่าออกมา (Return value) อาจเพื่อแจ้งสถานะการรันว่ารันสำเร็จหรือไม่อย่างไร หรืออาจส่งออกเป็นค่าที่จะนำไปประมวลผลต่อก็ตาม เราสามารถใช้ตัวแปรพิเศษ $? ในการดูผลลัพธ์ของโปรแกรมได้
เช่น

#!/bin/bash
cd /dada &> /dev/null
echo rv: $?
cd $(pwd) &> /dev/null
echo rv: $?

กรณีนี้ ไดเรคทอรี่ /dada เป็นไดเรคทอรี่ที่เราแกล้งพิมพ์ผิดไว้ เพื่อดูว่าสคริปต์จะส่งออกค่าออกมาเป็นอย่างไร ซึ่งจะได้ผลออกมาเป็น 1 และ 0 ตามลำดับ คือ 1 หมายถึงมีข้อผิดพลาดในโปรแกรม และ 0 หมายถึงรันสำเร็จ ไม่มีข้อผิดพลาดใด ๆ

10.5 จับการแสดงผลใส่ตัวแปร

เราสามารถนำผลลัพธ์ของโปรแกรมมาใส่ในตัวแปร ด้วยการสั่งภายใต้เครื่องหมาย ` (grave accent)
เช่น

#!/bin/bash
DBS=`mysql -u root  -e "show databases"`
for b in $DBS ;
do
    mysql -u root -e "show tables from $b"
done

เป็นการนำผลลัพธ์ของคำสั่งแรกคือ mysql -u root -e "show databases" มาใส่ในตัวแปร DBS เพื่อทำเป็นขอบเขตให้กับตัวแปร b ในคำสั่ง for อีกครั้งหนึ่ง
ตามตัวอย่างจะแสดงผลทุกตารางในทุกฐานข้อมูลของ mysql

11. ตัวดำเนินการ (operators) และคำสั่งน่าสนใจ

11.1 ตัวดำเนินการเปรียบเทียบตัวอักษร (String comparison operators)

  • [ "$s1" = "$s2" ] หรือ [ "$s1" == "$s2" ] เป็นจริง ถ้า s1 เท่ากับ s2
  • [ "$s1" != "$s2" ] เป็นจริง ถ้า s1 ไม่เท่ากับ s2
  • [[ "$s1" < "$s2" ]] หรือ [ "$s1" \< "$s2" ] เป็นจริง ถ้า s1 น้อยกว่า s2
  • [[ "$s1" > "$s2" ]] หรือ [ "$s1" \> "$s2" ] เป็นจริง ถ้า s1 มากกว่า s2
  • [ -n "$s1" ] เป็นจริง ถ้า s1 มีค่าใด ๆ
  • [ -z "$s1" ] เป็นจริง ถ้า s1 ไม่มีค่า

11.2 ตัวอย่างการเปรียบเทียบอักษร

#!/bin/bash
S1='string'
S2='String'
if [ "$S1"="$S2" ]; then
    echo "S1('$S1') is not equal to S2('$S2')"
fi
if [ "$S1"="$S1" ]; then
    echo "S1('$S1') is equal to S1('$S1')"
fi

11.3 ตัวดำเนินการทางคณิตศาลตร์ (Arithmetic operators)

  • + การบวก
  • - การลบ
  • * การคูณ
  • / การหาร
  • % การหาเศษจากตัวหาร (remainder)

11.4 ตัวเปรียบเทียบทางคณิตศาตร์ (Arithmetic relational operators

  • -lt น้อยกว่า (<)
  • -gt มากกว่า (>)
  • -le น้อยกว่าหรือเท่ากับ (<=)
  • -ge มากกว่าหรือเท่ากับ (>=)
  • -eq เท่ากับ (==)
  • -ne ไม่เท่ากับ (!=)

11.5 คำสั่งควรรู้

sed (stream editor)
sed เป็นเอดิเตอร์แบบบรรทัดคำสั่ง มีการใช้งานที่พลิกแพลงหลากหลายมาก ตัวอย่าง
$ sed 's/old/new/g' /tmp/dummy

นำเอาเนื้อไฟล์ /tmp/dummy มาแทนที่ old ด้วย new และแสดงออกทางจอภาพ

$ sed 12,18d /tmp/dummy

นำเอาเนื้อไฟล์ /tmp/dummy มาแสดงทางจอภาพ โดยเว้นไม่แสดงบรรทัดที่ 12 ถึงบรรทัดที่ 18

ดูรายละเอียดเพิ่มเติมได้ที่ gentoo: Sed by example

awk (manipulation of datafiles, text retrieval and processing)
awk เป็นทั้งโปรแกรมและภาษาในการค้นหาข้อความในไฟล์จากรูปแบบที่เรากำหนดให้
สมมุติว่าไฟล์ /tmp/dummy มีเนื้อไฟล์คือ
test123
test
tteesstt

ตัวอย่างการใช้งานคือ

$ awk '/test/ {print}' /tmp/dummy
test123
test 

ดูรายละเอียดเพิ่มเติมได้ที่ gentoo: Awk by example

grep (print lines matching a search pattern)
grep เป็นโปรแกรมที่ใช้บ่อยในการค้นข้อความในไฟล์ และยังมีความสามารถในการสรุปผลการนับข้อความด้วย
ตัวอย่าง
$ man grep | grep "standard" -c
8

เป็นการค้นคำว่า standard ในการแสดงผลของคำสั่ง man grep ว่ามีอยู่กี่คำ คำตอบคือ 8

ดูตัวอย่างเพิ่มเติมที่ tdlp: Examples using grep

wc (counts lines, words and bytes)
wc ใช้ในการนับคำ, นับบรรทัด และนับจำนวนหน่วยความจำที่ถูกใช้ในไฟล์ เป็นไบต์
ตัวอย่าง
$ wc --words --lines --bytes /tmp/dummy
 3  3 22 /tmp/dummy
sort (sort lines of text files)
sort ใช้จัดเรียงข้อมูล
สมมุติว่าไฟล์ /tmp/dummy มีเนื้อว่า
b
c
a

ตัวอย่างคำสั่งคือ

$ sort /tmp/dummy
a
b
c

คือการนำเอาเนื้อไฟล์ /tmp/dummy มาจัดเรียง และแสดงผลออกทางจอภาพ

bc (a calculator programming language)
bc เป็นเครื่องคิดเลขแบบใช้บรรทัดคำสั่ง
ตัวอย่างเช่น
$ echo 1+1
1+1
$ echo 1+1 | bc
2

หรือใช้แบบโต้ตอบ

$ bc -q
1 == 5
0

0.05 == 0.05
1

5 != 5
0

2 ^ 8
256

sqrt(9)
3

while (i != 9) {
i = i + 1;
print i
}
123456789

quit
tput (initialize a terminal or query terminfo database)
tput ใช้ในการตั้งค่าหรือแสดงค่าต่าง ๆ ของเทอร์มินัล
เช่น
$ tput cup 10 4

เลื่อนเคอร์เซอร์ไปยังบรรทัดที่ 10 สดมภ์ที่ 4

$ tput reset

ล้างจอภาพ มีค่าเท่ากับคำสั่ง clear

$ tput cols

แสดงจำนวนสดมภ์ (ความกว้าง) ของจอเทอร์มินัล

12.ตัวอย่างสคริปต์

12.1 ตัวอย่างสคริปต์ดูรายชื่อไฟล์ในไดเรคทอรี่ย่อย

#!/bin/bash 
function listdir {
    local PAT="$1"
    local ROOT="$2"
    for i in *; do
        if [ -d "$i" ]; then
            local CUR="$ROOT/$i"
            pushd "$i" &>/dev/null
            listdir "$PAT" "$CUR"
            popd &>/dev/null
        fi
    done
    if [ ! -z "$( ls -d $PAT 2>/dev/null )" ]; then
        echo "Directory: $ROOT"
        ls -d $PAT 2>/dev/null
        echo 
    fi
}

if [ -z "$1" ]; then
   echo List file in PATTERN recursive into directories.
   echo Usage: $0 "PATTERN"
   exit
fi
PATTERN="$1"
echo "List $PATTERN"
listdir "$PATTERN" "."

ให้ผลคล้ายคำสั่ง

$ find * -name PATTERN

12.2 ตัวอย่างสคริปต์บีบอัดสำรองข้อมูล

#!/bin/bash          
SRCD="/home/"
TGTD="/var/backups/"
OF=home-$(date +%Y%m%d).tgz
tar -cZf $TGTD$OF $SRCD

12.3 เปลี่ยนชื่อไฟล์ทีละหลายไฟล์

#!/bin/sh
# renna: rename multiple files according to several rules
# written by felix hudson Jan - 2000

#first check for the various 'modes' that this program has
#if the first ($1) condition matches then we execute that portion of the
#program and then exit

# check for the prefix condition
if [ $1 = p ]; then

#we now get rid of the mode ($1) variable and prefix ($2)
  prefix=$2 ; shift ; shift

# a quick check to see if any files were given
# if none then its better not to do anything than rename some non-existent
# files!!

  if [$1 = ]; then
     echo "no files given"
     exit 0
  fi

# this for loop iterates through all of the files that we gave the program
# it does one rename per file given
  for file in $*
    do
    mv ${file} $prefix$file
  done

#we now exit the program
  exit 0
fi

# check for a suffix rename
# the rest of this part is virtually identical to the previous section
# please see those notes
if [ $1 = s ]; then
  suffix=$2 ; shift ; shift

   if [$1 = ]; then
    echo "no files given"
   exit 0
   fi

 for file in $*
  do
   mv ${file} $file$suffix
 done

 exit 0
fi

# check for the replacement rename
if [ $1 = r ]; then

  shift

# i included this bit as to not damage any files if the user does not specify
# anything to be done
# just a safety measure

  if [ $# -lt 3 ] ; then
    echo "usage: renna r [expression] [replacement] files... "
    exit 0
  fi

# remove other information
  OLD=$1 ; NEW=$2 ; shift ; shift

# this for loop iterates through all of the files that we give the program
# it does one rename per file given using the program 'sed'
# this is a sinple command line program that parses standard input and
# replaces a set expression with a give string
# here we pass it the file name ( as standard input) and replace the nessesary
# text

  for file in $*
  do
    new=`echo ${file} | sed s/${OLD}/${NEW}/g`
    mv ${file} $new
  done
exit 0
fi

# if we have reached here then nothing proper was passed to the program
# so we tell the user how to use it
echo "usage;"
echo " renna p [prefix] files.."
echo " renna s [suffix] files.."
echo " renna r [expression] [replacement] files.."
exit 0

# done!

12.4 เปลี่ยนชื่อไฟล์แบบง่าย

#!/bin/bash
# renames.sh
# basic file renamer

criteria=$1
re_match=$2
replace=$3

for i in $( ls *$criteria* ); 
do
    src=$i
    tgt=$(echo $i | sed -e "s/$re_match/$replace/")
    mv $src $tgt
done

13. การค้นหาที่ผิดในสคริปต์

เราใช้พารามิเตอร์ -x ต่อท้ายคำสั่งในบรรทัดแรก

#!/bin/bash -x

จะมีผลว่าเชลล์จะแสดงทุกคำสั่งที่ถูกรันออกมาทางจอภาพ

จบแล้วจ้า

bash: แปลงชื่อไฟล์เป็นตัวเล็ก

ต้องการแปลงชื่อไฟล์เป็นตัวเล็ก ค้นไปค้นมาปรากฎว่ามีอยู่ในแพกเกจ bash-doc เป็นตัวอย่างอยู่แล้ว

ต้องติดตั้งก่อน
$ sudo aptitude install bash-doc

ขมายมาใช้
$ sudo cp /usr/share/doc/bash-doc/examples/scripts.v2/lowercase /usr/local/bin
$ sudo chmod 755 /usr/local/bin/lowercase

เรียกใช้ด้วยคำสั่ง lowercase $FILENAME

นอกจากตัวอย่างในการแปลงไฟล์แล้ว ยังมีโปรแกรมอรรถประโยชน์อีกเยอะแยะในแพกเกจนี้ หากสนใจสามารถติดตั้งและศึกษาดูได้ครับ

ถ้าจะแปลงเป็นตัวใหญ่ก็แค่กลับ จาก tr A-Z a-z ไปเป็น tr a-z A-Z ก็เสร็จแล้ว
ทำเป็นตัวอย่างคือ
$ sudo vi /usr/local/bin/uppercase

#! /bin/bash
#
# original from
# @(#) lowercase.ksh 1.0 92/10/08
# 92/10/08 john h. dubois iii (john@armory.com)
#
# conversion to bash v2 syntax done by Chet Ramey

Usage="Usage: $name file ..."
phelp()
{
echo "$name: change filenames to upper case.
$Usage
Each file is moved to a name with the same directory component, if any,
and with a filename component that is the same as the original but with
any lower case letters changed to upper case."
}

name=${0##*/}

while getopts "h" opt; do
    case "$opt" in
    h)  phelp; exit 0;;
    *)  echo "$Usage" 1>&2; exit 2;;
    esac
done

shift $((OPTIND - 1))

for file; do
    filename=${file##*/}
    case "$file" in
    */*)    dirname=${file%/*} ;;
    *)    dirname=. ;;
    esac
    nf=$(echo $filename | tr a-z A-Z)
    newname="${dirname}/${nf}"
    if [ "$nf" != "$filename" ]; then
    mv "$file" "$newname"
    echo "$0: $file -> $newname"
    else
    echo "$0: $file not changed."
    fi
done

$ sudo chmod 755 /usr/local/bin/uppercase

เสร็จแล้ว เรียกใช้ด้วยคำสัง uppercase $FILENAME