Lập trình shell

Phần 2 - Thao tác nâng cao với biến; vòng lặp và rẽ nhánh

Nguyễn Hải Châu (nhchau@gmail.com)
Trường Đại học Công nghệ, ĐHQGHN

Các biến sẵn có của bash

  • $BASH: Đường dẫn tới bash
  • $BASHPID: Số hiệu tiến trình bash hiện hành
  • $BASH_VERSION: Phiên bản bash
  • $BASH_VERSIONINFO[n]: Phiên bản bash
  • $HOME
  • $HOSTNAME
  • $HOSTTYPE

Các biến sẵn có của bash

  • $UID: user ID
  • $EUID: effective UserID
  • $PATH: Đường dẫn
  • $SECONDS: Script đã chạy được bao nhiêu giây
  • $1, .., \$9, \$#, \$*, \$@
  • $!: PID của script mới nhất đang chạy nền
  • $$: PID của script
  • $?: Trạng thái exit
  • $_: Tham số cuối cùng của lệnh vừa thực hiện

Các biến sẵn có của bash

echo $BASH, $BASH_VERSION, $BASHPID
for n in 0 1 2 3 4 5
do
    echo "BASH_VERSINFO[$n] = ${BASH_VERSINFO[$n]}"
done  
## /usr/bin/bash, 4.3.42(1)-release, 24016
## BASH_VERSINFO[0] = 4
## BASH_VERSINFO[1] = 3
## BASH_VERSINFO[2] = 42
## BASH_VERSINFO[3] = 1
## BASH_VERSINFO[4] = release
## BASH_VERSINFO[5] = x86_64-redhat-linux-gnu

Khai báo biến (declare/typing)

  • -r readonly: Biến chỉ đọc
  • -i integer: Biến nguyên
  • -a array: Biến mảng
  • -f function: Tên hàm
  • -x export: Biến có thể export giá trị ra ngoài môi trường của script

Sinh số giả ngẫu nhiên

  • $RANDOM: Sinh số giả ngẫu nhiên từ 0 đến 32767
echo $RANDOM
echo $RANDOM
echo $RANDOM
## 25371
## 10948
## 16566

Thao tác string: Độ dài

  • Độ dài: ${#string}, expr length $str, expr "$string":'.*'
  • Độ dài của chuỗi con nằm trong chuỗi: expr match "$string" '$substring', expr "$string" : '$substring'
stringZ=abcABC123ABCabc
echo ${#stringZ}                 # 15
echo `expr length $stringZ`      # 15
echo `expr "$stringZ" : '.*'`    # 15
## 15
## 15
## 15

Thao tác string: Tìm kiếm chuỗi con

stringZ=abcABC123ABCabc
#       |------|
#       12345678
echo `expr match "$stringZ" 'abc[A-Z]*.2'`   # 8
echo `expr "$stringZ" : 'abc[A-Z]*.2'`       # 8
## 8
## 8
  • String index: expr index $string $substring
stringZ=abcABC123ABCabc
expr index $stringZ "ABC"
## 4

Thao tác string: Cắt chuỗi con

  • ${string:position}
  • ${string:position:length}
stringZ=abcABC123ABCabc
echo ${stringZ:5}
echo ${stringZ:5:3}
## C123ABCabc
## C12
  • expr substr $string $position $length
  • expr match "$string" '\($substring\)'
  • expr "$string" : '\($substring\)'

Thực hành với string

  • 3.1. Cho danh bạ điện thoại tel.csv là một file text có nhiều dòng, mỗi dòng có các trường thông tin cách nhau bởi dấu ,. Hãy tính số lượng các số điện thoại MobiFone, VinaPhone, VietTel, ... trong file đó.
head -6 tel.csv
## 1,08485876443,"KIM Van Chien",,"a"
## 2,0912025544,"Đỗ Thị Phi Nga","Giảng viên chính Thạc sỹ","Khoa Sư phạm Tiếng Anh/Faculty of English"
## 3,0983669908,"Bùi Minh Đức","Thạc Sỹ","Khoa Công nghệ Thông tin"
## 4,0949844646,"Bùi Thu Hà","Tiến sĩ","Khoa Sinh Học"
## 5,01648462188,"bui thuy dung",,
## 6,0904394041,"Bùi Tiến Long",,"Trung tâm Thông tin - Thư viện"

Thao tác string: Xóa chuỗi con

  • ${string#substring}: Xóa chuỗi con ngắn nhất tính từ đầu $string
  • ${string##substring}: Xóa chuỗi con dài nhất tính từ đầu $string
  • ${string%substring}: Xóa chuỗi con ngắn nhất tính từ cuối $string
  • ${string%%substring}: Xóa chuỗi con dài nhất tính từ cuối $string
stringZ=abcABC123ABCabc
echo ${stringZ#a*C}
echo ${stringZ##a*C}
echo ${stringZ%A*c}
echo ${stringZ%%A*c}
## 123ABCabc
## abc
## abcABC123
## abc

Thao tác string: Thay thế chuỗi con

  • ${string/substring/replacement}: Thay thế xuất hiện đầu tiên của substring bằng replacement
  • ${string//substring/replacement}: Thay thế toàn bộ substring bằng replacement
  • ${string/#substring/replacement}: Thay thế substring ở đầu xâu bằng replacement
  • ${string/%substring/replacement}: Thay thế substring ở cuối xâu bằng replacement
stringZ=abcABC123ABCabc
echo ${stringZ/ABC/...}
echo ${stringZ//ABC/...}
echo ${stringZ/#abc/...}
echo ${stringZ/%abc/...}
## abc...123ABCabc
## abc...123...abc
## ...ABC123ABCabc
## abcABC123ABC...

Thay thế tham số

  • ${parameter-default}, ${parameter:-default}: Nếu parameter không có giá trị, sử dụng giá trị default
  • ${parameter=default}, ${parameter:=default}: Nếu parameter không có giá trị, gán parameter bằng default
var1=1
var2=2
echo $var3
echo ${var1-var2}
echo ${var3-$var2}
## 
## 1
## 2

Thay thế tham số

  • ${parameter+altvalue}, ${parameter:+alt_value}: Nếu parameter được gán, sử dụng altvalue, ngược lại NULL
  • ${parameter?errmsg}, ${parameter:?err_msg}: Nếu parameter được gán, sử dụng giá trị, in ra errmsg và thoát khỏi script với giá trị 1
var1=1
echo $var2
echo ${var1+xyz}
echo ${var1+xyz}
## 
## xyz
## xyz

Thay thế tham số

  • ${!varprefix*}, ${!varprefix@}: Tên các biến bắt đầu bằng varprefix
# Tên các biến môi trường bắt đầu với chữ H
echo ${!H*}
echo ${!H@}
## HISTCMD HISTCONTROL HISTSIZE HOME HOSTNAME HOSTTYPE
## HISTCMD HISTCONTROL HISTSIZE HOME HOSTNAME HOSTTYPE

Cấu trúc rẽ nhánh

  • if ... then ... elif ... fi
if [ condition1 ]
then
    command...
elif [ condition2 ]
    command...
else
    command...
fi

Rẽ nhánh case (in)/esac

case $variable in
    "$condition1")
    command...
    ;;
    "$condition2")
    command...
    ;;
    ...
    *) # default
    command...
    ;;
esac

Ví dụ case

#!/usr/bin/bash
case "$1" in
  start) echo start service;;
  stop) echo stop service;;
  restart) echo restart service;;
  *) echo wrong option!
     echo "Usage: case start|stop|restart"
     ;;
esac

Rẽ nhánh select

#!/usr/bin/bash
select variable [in list]
do
    command
    break
done
  • select thường được sử dụng để làm menu tương tác

Ví dụ rẽ nhánh

#!/bin/bash
select opt in "Print document" "Edit document" "Quit" ; do
    if [ "$opt" = "Quit" ]; then
        echo done
        exit
    elif [ "$opt" = "Edit document" ]; then
        echo Editing...
    elif [ "$opt" = "Print document" ]; then
        echo Printing...
    else
        clear
        echo Wrong option
    fi
done

Vòng lặp for

for arg in [list]
do
    command...
done
for arg in a b c
do
    echo $arg
done
## a
## b
## c

Vòng lặp while...do

while [ condition ]
do
    command...
done
cat while.sh
./while.sh p1 p2 p3
## while [ $# -gt 0 ]
## do
##  echo $1
##  shift
## done
## p1
## p2
## p3

Vòng lặp until...do

until [ condition is true ]
do
    command...
done
cat until.sh
./until.sh p1 p2 p3
## until [ $# -eq 0 ]
## do
##  echo $1
##  shift
## done
## p1
## p2
## p3

Một số lưu ý về vòng lặp

  • Câu lệnh break để kết thúc vòng lặp ngay khi break được gọi
  • Vòng lặp có thể lồng nhau

Điều khiển vòng lặp bằng break và continue

  • break và continue có ý nghĩa giống như break và continue trong C hoặc các ngôn ngữ khác
  • continue N ngắt tất cả các vòng lặp và tiếp tục ở N mức cao hơn - nếu có vòng lặp lồng nhau

Thay thế lệnh

  • Sử dụng để đưa output của một lệnh, hoặc một nhóm lệnh vào một ngữ cảnh khác
script=`command`
echo Command output is $script
multi=$(command1;command2)
echo Multiple output is $multi
s1=`who`
s2=$(ls|wc -l; date)
echo Who logged in? $s1
echo Multi: $s2
## Who logged in? chau tty2 2016-11-01 20:37 (:1)
## Multi: 115 Sat Nov 5 12:18:08 ICT 2016

Mở rộng các toán tử số học

  • Sử dụng expr thay cho let hoặc (())
a=1
b=2
c=`expr $a + $b`
echo $c
## 3

Sử dụng mảng

declare -a arrayvar
arrayvar[1]=23
arrayvar[4]=35
echo $((arrayvar[1]+arrayvar[4]))
echo $((arrayvar[1]+arrayvar[3]))
barray=( 1 2 3 4 5 )
echo ${barray[1]} ${barray[3]}
echo ${arrayvar[1]} ${arrayvar[3]}
## 58
## 23
## 2 4
## 23

Tham chiếu gián tiếp

a=a_character
a_character=C
echo $a
eval tmp=\$$a
echo $tmp
## a_character
## C

Tham chiếu gián tiếp

a="ps -fu chau | head -9"
eval $a
## UID        PID  PPID  C STIME TTY          TIME CMD
## chau      1514     1  0 Nov01 ?        00:00:00 /usr/lib/systemd/systemd --user
## chau      1518  1514  0 Nov01 ?        00:00:00 (sd-pam)
## chau      1530     1  0 Nov01 ?        00:00:00 /usr/bin/gnome-keyring-daemon --daemonize --login
## chau      1533  1510  0 Nov01 tty2     00:00:00 /usr/libexec/gdm-x-session --run-script gnome-session
## chau      1542  1533  0 Nov01 tty2     00:00:10 /usr/libexec/gnome-session-binary
## chau      1553  1514  0 Nov01 ?        00:00:08 /usr/bin/dbus-daemon --session --address=systemd: --nofork --nopidfile --systemd-activation --syslog-only
## chau      1588  1542  0 Nov01 tty2     00:01:40 goldendict
## chau      1593  1514  0 Nov01 ?        00:00:00 /usr/libexec/at-spi-bus-launcher

Hàm

  • Cách khai báo một hàm trong shell:
function function_name {
    command...
    return value
}
  • Chú ý giá trị trả lại value chỉ nằm trong khoảng 0 đến 255 (1 byte)
  • Cách gọi một hàm với tham số: function_name p1 p2 ... trong đó p1, p2 ... là các tham số
  • Hàm có thể gọi đến hàm khác hoặc đến chính nó (đệ qui)

Thực hành hàm đệ qui

  • 3.2. Hãy viết hàm in ra dãy Fibonacci \(f\) từ thứ 1 đến thứ \(n\). Hàm có 1 tham số vào là một số nguyên dương \(n\). Dãy Fibonacci được mô tả như sau:
    • \(f_1=1\)
    • \(f_2=1\)
    • \(f_n=f_{n-1}+f_{n-2}\) \(\forall n>2\)

Debug

  • Một script có lỗi ở dòng 9:
./bug1.sh
## ./bug1.sh: line 9: [37: command not found
  • Các cách debug:
    • Chèn các câu lệnh echo vào vùng code có lỗi để theo dõi các biến
    • Sử dụng bộ lọc tee để lưu lại dòng dữ liệu ở các đoạn code lỗi
    • Sử dụng các option -n, -v, -x để bật và +n, +v, +x để tắt các chế độ debug tương ứng.
    • Sử dụng lệnh assert kèm điều kiện kiểm tra biến
    • Sử dụng $LINENO và lệnh sẵn có caller
    • Bắt tín hiệu bằng trap

Debug

  • bash -n script kiểm tra cú pháp của script và không thực thi script đó
  • bash -v script hiển thị mỗi lệnh trước khi thực hiện lệnh đó
  • bash -x script hiển thị kết quả mỗi lệnh sau khi thực hiện lệnh
  • Các option -n, -v, -x có thể sử dụng kết hợp:
    • -nv: Kiểm tra cú pháp + hiển thị mỗi dòng lệnh được kiểm tra
    • -xv: Hiển thị lệnh trước khi thực hiện và kiểm tra kết quả sau khi thực hiện

.bash_profile.bashrc

  • Hai tệp đặc biệt của bash
    • Nằm trong thư mục $HOME của mỗi người sử dụng
    • Thường được tự động tạo ra khi người quản trị tạo một người sử dụng mới
    • Là các bash script
  • .bash_profile: Được tự động thực hiện khi người sử dụng login vào hệ thống
  • .bashrc: Được tự động thực hiện mỗi lần bash được gọi đến
  • Nếu $HOME không có .bash_profile.bashrc, .profile sẽ được gọi thay thế

Nội dung .bash_profile

head -15 $HOME/.bash_profile
## # .bash_profile
## 
## # Get the aliases and functions
## if [ -f ~/.bashrc ]; then
##  . ~/.bashrc
## fi
## 
## # User specific environment and startup programs
## 
## export PATH=/usr/java/default/bin:$PATH:$HOME/.local/bin:$HOME/bin:$HOME/Android/Sdk/tools:$HOME/Android/Sdk/platform-tools:/usr/lib/rstudio/bin
## export PATH=$PATH:$HOME/bin:/usr/local/cuda/bin
## export LD_LIBRARY_PATH=/lib64:/usr/lib64:/lib:/usr/lib:$HOME/lib
## export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/cuda/lib64:/lib
## 
## # export CLASSPATH=/usr/share/java:$HOME/source/java:/$HOME/tmp/t/citygml4j-2.0ea-java6/lib

Nội dung .bashrc

  • Chú ý: ~ tương đương với $HOME
head -15 ~/.bashrc
## # .bashrc
## 
## # Source global definitions
## if [ -f /etc/bashrc ]; then
##  . /etc/bashrc
## fi
## 
## # User specific aliases and functions
## alias ssh='ssh -D9999 -X'
## alias n4m="simple-mtpfs $HOME/Nexus4"
## alias n4u="fusermount -u $HOME/Nexus4"
## # alias skype="cd $HOME/bin/sky;./skype >/dev/null 2> /dev/null&"
## alias texcomp='latex $1.tex;dvipdf $1.dvi'
## # alias tb='psql --host=112.137.129.222 --port=4488 --username=postgres --password --dbname=csdl_taybac'
## alias tb='psql --host=112.137.132.48 --port=4488 --username=postgres --password --dbname=csdl_taybac'

Thực hành

  • 3.3. Viết một script giải nén "vạn năng": Script tự nhận dạng các phần mở rộng của tệp nén (ví dụ .zip, .gz, .7z...) và gọi đến các chương trình giải nén tương ứng.
  • 3.4. Viết script có tên là saferm. Script này xóa file một cách an toàn, nghĩa là lệnh saferm myfile sẽ không xóa myfile mà chuyển myfile vào một thư mục ~/.TRASH. Các file trong ~/.TRASH sẽ bị xóa định kỳ sau 36 giờ. Tương ứng với saferm cần viết một script saferestore để khôi phục file nếu như bị file đó bị xóa nhầm.
  • 3.5. Hãy viết một script quét các thư mục $HOME của người sử dụng nằm trong /home và tính tổng dung lượng lưu trữ của từng người sử dụng để kiểm soát quota. Script cần in ra dữ liệu có ba cột: tên người sử dụng, thư mục $HOME và dung lượng hiện đang sử dụng. Danh sách người sử dụng lấy trong /etc/passwd.
  • 3.6. Hãy liệt kê các thư mục rỗng trong một cây thư mục cho trước. Gợi ý: Sử dụng lệnh find.