Skip to content

Bắt đầu với Lập trình Shell

Published: at 12:00 AM
[en]

Mặc dù chúng ta đã quá quen với shell script. Nhưng sau đây, tôi vẫn muốn giới thiệu về cách để viết 1 shell script cho những bạn đang cần nó.

1. Viết shell script như thế nào ?

Bước 1: dùng bất cứ chương trình gì có thể soạn thảo. Ví dụ: vi, nano, hoặc vscode

  • Bạn nên dùng các trình soạn thảo có hỗ trợ tô sáng cú pháp như gedit, vscode, hoặc vim để viết shell vì khi viết shell nó sẽ hiện màu sắc phân biệt giữa các kí tự và các từ khoá giúp bạn dễ kiểm soát lỗi.

Bước 2: sau khi viết xong phải gán quyền thực thi cho script Ví dụ:

chmod u+x tên_script
  • u+x cấp quyền thực thi cho người dùng sở hữu tệp
  • Bạn cũng có thể sử dụng chmod 755 tên_script để cấp quyền đọc và thực thi cho tất cả mọi người, nhưng chỉ cấp quyền ghi cho chủ sở hữu

Bước 3: thực thi script Cú pháp:

bash tên_script
sh tên_script
./tên_script

Cấu trúc một chương trình shell script như sau:

#!/bin/bash # <- dòng shebang chỉ định shell sẽ được sử dụng
command # <- lệnh
command
exit 0 # <- thoát với mã trạng thái thành công

Chú ý: lệnh exit 0 sẽ được mô tả kỹ trong phần Exit status

2. Biến trong shell

Trong linux shell thì có 2 kiểu biến:

  • Biến hệ thống (system variable): được tạo bởi Linux. Kiểu biến này thường được viết bằng ký tự in hoa (ví dụ: $HOME, $PATH, $USER).
  • Biến do người dùng định nghĩa: được tạo và quản lý bởi người dùng.

Định nghĩa biến: tên_biến=giá_trị

  • Một số quy định về biến trong shell:
  1. Tên bắt đầu bằng ký tự hoặc dấu gạch chân (_).

  2. Không được có khoảng trắng trước và sau dấu bằng khi gán giá trị cho biến

  3. Biến có phân biệt chữ hoa chữ thường

  4. Bạn có thể khai báo một biến có giá trị NULL như sau: var01= hoặc var01=""

  5. Không dùng các ký tự đặc biệt như ?, *, #, @, v.v. để đặt tên biến.

  6. Để gán kết quả của một lệnh cho biến, sử dụng thay thế lệnh:

    ngay_hien_tai=$(date)
    # hoặc sử dụng dấu nháy ngược (kiểu cũ)
    ngay_hien_tai=`date`

3. Sử dụng biến

Để truy xuất giá trị biến, dùng cú pháp sau: $tên_biến hoặc ${tên_biến} (cách thứ hai được ưa thích hơn để rõ ràng)

Ví dụ:

n=10
echo $n
echo ${n}  # Được ưa thích hơn để rõ ràng, đặc biệt khi nối chuỗi
ten="John"
echo "Xin chào ${ten}Doe"  # Sử dụng dấu ngoặc nhọn để phân tách biến rõ ràng

4. Lệnh echo

Dùng để hiển thị dòng văn bản, giá trị biến …

Cú pháp: echo [options] [chuỗi, biến…]

Các options:

-n: không in ký tự xuống dòng.
-e: cho phép hiểu những ký tự theo sau dấu \ trong chuỗi
\a: alert (tiếng chuông)
\b: backspace
\c: không xuống dòng
\n: xuống dòng
\r: về đầu dòng
\t: tab
\\: dấu \

Ví dụ:

echo -e "một hai ba \a\t\t bốn \n"

5. Tính toán trong Shell

Sử dụng expr

Cú pháp: expr op1 <phép toán> op2

Ví dụ:

expr 1 + 3
expr 2 - 1
expr 10 / 2
expr 20 % 3
expr 10 \* 3  # Lưu ý dấu gạch chéo ngược trước *
echo `expr 6 + 3`
x=4
z=`expr $x + 3`

Sử dụng let

Ví dụ:

let "z=$z+3"
let "z += 3"
let "z=$m*$n"

Sử dụng $((…)) (Phép tính mở rộng - Khuyến nghị)

Ví dụ:

z=$((z+3))
z=$(($m*$n))
# Bạn cũng có thể thực hiện các phép tính phức tạp hơn
ket_qua=$((10 + 5 * 2))  # Tuân theo thứ tự ưu tiên phép tính

Chú ý:

expr 20 \% 3 # 20 mod 3
expr 10 \* 3 # phép toán nhân, sử dụng \* chứ không phải * để phân biệt với ký tự thay thế.

Cú pháp dấu nháy ngược được sử dụng rất nhiều trong shell; khi một lệnh được đặt giữa dấu nháy ngược, shell sẽ thực thi lệnh đó và thay thế bằng kết quả.

Ví dụ:

a=`expr 10 \* 3`

–> a sẽ có giá trị là 10 x 3 = 30 In kết quả ra màn hình:

a=`expr 10 \* 3`
echo $a
30

6. Dấu ngoặc kép trong Shell

Có 3 loại dấu ngoặc kép trong shell:

": Nháy kép - Biến và thay thế lệnh được mở rộng, nhưng hầu hết các ký tự đặc biệt vẫn giữ nguyên ý nghĩa đặc biệt.
': Nháy đơn - Mọi thứ bên trong được hiểu theo nghĩa đen, không có sự mở rộng nào xảy ra.
`: Nháy ngược (hoặc $(command)) - Thực thi lệnh được bao quanh và thay thế bằng kết quả.

Ví dụ:

ten="Thế giới"
echo "Xin chào $ten"      # Kết quả: Xin chào Thế giới
echo 'Xin chào $ten'      # Kết quả: Xin chào $ten
echo "Hôm nay là `date`"  # Kết quả: Hôm nay là Thứ Tư 12 tháng 6 14:23:45 PDT 2024
# Cách hiện đại thay thế cho dấu nháy ngược
echo "Hôm nay là $(date)" # Kết quả: Hôm nay là Thứ Tư 12 tháng 6 14:23:45 PDT 2024

7. Trạng thái Exit

Mặc định trong Linux, khi một lệnh hoặc script thực thi, nó trả về một giá trị để xác định xem lệnh hoặc script đó có thực thi thành công không.

  1. Nếu giá trị trả về là 0 (zero) -> lệnh thực thi thành công
  2. Nếu giá trị trả về khác 0 (1-255) -> không thành công, với các giá trị khác nhau chỉ ra các loại lỗi khác nhau.

Giá trị đó gọi là Exit status

Để biết được giá trị trả về của một lệnh hay 1 script, sử dụng biến đặc biệt có sẵn của shell: $?

Ví dụ: Nếu bạn xoá 1 file không tồn tại trên đĩa cứng

rm unknown_file
echo $?  # Sẽ in ra một giá trị khác 0, thường là 1

Bạn có thể sử dụng trạng thái thoát trong các câu lệnh điều kiện:

if command; then
    # Lệnh thành công (trạng thái thoát là 0)
    echo "Lệnh thực hiện thành công"
else
    # Lệnh thất bại (trạng thái thoát khác 0)
    echo "Lệnh thất bại với trạng thái thoát $?"
fi

8. Lệnh read – đọc giá trị nhập từ bàn phím, file …

Dùng để lấy dữ liệu nhập từ bàn phím và lưu vào biến

Cú pháp: read [options] var1 var2 var3 … varN

Các tùy chọn phổ biến:

-p "nhắc nhở": Hiển thị lời nhắc trước khi đọc đầu vào
-s: Chế độ im lặng (không hiển thị ký tự, hữu ích cho mật khẩu)
-t seconds: Thời gian chờ sau số giây được chỉ định
-r: Chế độ thô (dấu gạch chéo ngược không được coi là ký tự thoát)

read không có tham số -> giá trị sẽ được chứa trong biến $REPLY

Ví dụ:

read -p "Nhập tên của bạn: " ten  # Hiển thị lời nhắc và lưu đầu vào vào $ten
read -s -p "Nhập mật khẩu: " mat_khau  # Chế độ im lặng cho đầu vào mật khẩu
echo
read  # Không có biến được chỉ định, đầu vào được lưu trong $REPLY
var="$REPLY"

Bình thường thì dấu \ cho phép xuống dòng để nhập tiếp dữ liệu trong read. Nếu read -r thì sẽ xử lý dấu gạch chéo ngược theo nghĩa đen.

Ví dụ:

read var
first line \
second line
echo "$var"
first line second line # <- kết quả

Nhưng với tham số -r thì sao?

read -r var
first line \
echo "$var"
first line \  # Dấu gạch chéo ngược được xử lý theo nghĩa đen

Lệnh read có thể dùng để đọc file. Nếu file chứa nhiều hơn 1 dòng thì chỉ có dòng thứ nhất được gán cho biến.

Nếu read với nhiều hơn 1 biến (read var1 var2 …) thì read sẽ dựa vào biến $IFS (Internal Field Separator) để gán dữ liệu cho các biến.

Mặc định thì $IFS bao gồm khoảng trắng, tab và ký tự xuống dòng.

Ví dụ:

read var < data_file  # Đọc dòng đầu tiên của data_file vào var

Nếu file có nhiều hơn 1 dòng:

read var1 var2 < data_file

Khi đó, mỗi biến sẽ chứa một chuỗi được phân tách bởi giá trị $IFS, chứ không phải 1 dòng, biến cuối cùng sẽ được chứa toàn bộ phần còn lại của dòng.

Để đọc toàn bộ file, sử dụng vòng lặp:

while read line
do
  echo "$line"
done < data_file

Để tùy chỉnh cách read phân tách đầu vào, hãy sửa đổi biến $IFS:

echo "liệt kê tất cả user"
OIFS=$IFS  # Sao lưu IFS ban đầu
IFS=:      # Đặt IFS thành dấu hai chấm cho định dạng file /etc/passwd
while read name passwd uid gid fullname ignore
do
  echo "$name $fullname"
done < /etc/passwd
IFS=$OIFS  # Khôi phục IFS ban đầu

Một cách tiếp cận sạch hơn là đặt IFS chỉ trong phạm vi của lệnh read:

while IFS=: read name passwd uid gid fullname ignore
do
  echo "$name $fullname"
done < /etc/passwd
# IFS vẫn không thay đổi bên ngoài vòng lặp

9. Tham số dòng lệnh

Khi bạn chạy một script với các đối số, bạn có thể truy cập các đối số đó bằng các biến đặc biệt:

$0  # Tên của script
$1  # Đối số đầu tiên
$2  # Đối số thứ hai
$3  # Đối số thứ ba, và tiếp tục...
$#  # Số lượng đối số được truyền vào
$@  # Tất cả các đối số dưới dạng các chuỗi riêng biệt: "$1" "$2" "$3" ...
$*  # Tất cả các đối số dưới dạng một chuỗi duy nhất: "$1 $2 $3 ..."

Ví dụ: Giả sử ta có script tên myself, để thực thi script này với các đối số:

./myself one two

Trong script, bạn có thể truy cập các tham số như sau:

echo "Tên script: $0"          # Kết quả: Tên script: ./myself
echo "Đối số đầu tiên: $1"     # Kết quả: Đối số đầu tiên: one
echo "Đối số thứ hai: $2"      # Kết quả: Đối số thứ hai: two
echo "Số lượng đối số: $#"     # Kết quả: Số lượng đối số: 2
echo "Tất cả đối số: $@"       # Kết quả: Tất cả đối số: one two

Bạn có thể xử lý tất cả các đối số trong một vòng lặp:

for arg in "$@"
do
  echo "Đối số: $arg"
done

10. Redirection

Hầu hết tất cả lệnh đều cho xuất kết quả ra màn hình (đầu ra chuẩn) hoặc lấy dữ liệu từ bàn phím (đầu vào chuẩn), nhưng với Linux bạn có thể chuyển hướng các luồng này.

Có ba luồng chuẩn:

  • Đầu vào chuẩn (stdin): File descriptor 0
  • Đầu ra chuẩn (stdout): File descriptor 1
  • Lỗi chuẩn (stderr): File descriptor 2

Các toán tử chuyển hướng phổ biến:

  1. > - Chuyển hướng stdout đến một file (ghi đè)
  2. >> - Chuyển hướng stdout đến một file (nối thêm)
  3. < - Chuyển hướng stdin từ một file
  4. 2> - Chuyển hướng stderr đến một file
  5. &> - Chuyển hướng cả stdout và stderr đến một file

Ví dụ:

ls > filename                # Chuyển hướng stdout đến filename (ghi đè)
ls >> filename               # Nối thêm stdout vào filename
cat < input.txt              # Đọc từ input.txt thay vì bàn phím
command 2> error.log         # Chuyển hướng stderr đến error.log
command > output.txt 2>&1    # Chuyển hướng cả stdout và stderr đến output.txt
command &> all.log           # Cú pháp ngắn gọn hơn để chuyển hướng cả stdout và stderr

Bạn cũng có thể chuyển hướng đến /dev/null để loại bỏ đầu ra:

command > /dev/null          # Loại bỏ stdout
command 2> /dev/null         # Loại bỏ stderr
command &> /dev/null         # Loại bỏ cả stdout và stderr

11. Pipes

Pipes cho phép bạn sử dụng đầu ra của một lệnh làm đầu vào cho lệnh khác.

Cú pháp: command1 | command2

Ví dụ:

ls -l | grep ".txt"          # Liệt kê các file và lọc các file .txt
cat file.txt | sort          # Sắp xếp nội dung của file.txt
ps aux | grep firefox        # Tìm các tiến trình firefox
history | grep git           # Tìm các lệnh git trong lịch sử
ls -l | wc -l                # Đếm số lượng file/thư mục

Bạn có thể nối nhiều pipes với nhau:

cat /var/log/syslog | grep ERROR | sort | uniq -c

12. Câu lệnh điều kiện

Câu lệnh if-else

if [ điều_kiện ]; then
    # các lệnh nếu điều kiện đúng
elif [ điều_kiện_khác ]; then
    # các lệnh nếu điều kiện khác đúng
else
    # các lệnh nếu tất cả điều kiện đều sai
fi

Các toán tử kiểm tra phổ biến:

  • Kiểm tra file: -e (tồn tại), -f (file thông thường), -d (thư mục), -r (có thể đọc)
  • Kiểm tra chuỗi: =, !=, -z (rỗng), -n (không rỗng)
  • Kiểm tra số: -eq, -ne, -lt, -le, -gt, -ge

Ví dụ:

if [ -f "$filename" ]; then
    echo "File tồn tại"
elif [ -d "$filename" ]; then
    echo "Đó là một thư mục"
else
    echo "File không tồn tại"
fi

Câu lệnh case

case $biến in
    mẫu1)
        # các lệnh cho mẫu1
        ;;
    mẫu2)
        # các lệnh cho mẫu2
        ;;
    *)
        # các lệnh mặc định
        ;;
esac

Ví dụ:

case $answer in
    [yY]|[yY][eE][sS])
        echo "Bạn đã trả lời có"
        ;;
    [nN]|[nN][oO])
        echo "Bạn đã trả lời không"
        ;;
    *)
        echo "Câu trả lời không hợp lệ"
        ;;
esac

13. Vòng lặp

Vòng lặp for

for biến in danh_sách
do
    # các lệnh
done

Ví dụ:

# Lặp qua danh sách các giá trị
for name in John Mary Steve
do
    echo "Xin chào $name"
done

# Lặp qua các file
for file in *.txt
do
    echo "Đang xử lý $file"
done

# Lặp qua các số
for i in {1..5}
do
    echo "Số: $i"
done

# Vòng lặp for kiểu C
for ((i=1; i<=5; i++))
do
    echo "Đếm: $i"
done

Vòng lặp while

while [ điều_kiện ]
do
    # các lệnh
done

Ví dụ:

count=1
while [ $count -le 5 ]
do
    echo "Đếm: $count"
    count=$((count+1))
done

Vòng lặp until

until [ điều_kiện ]
do
    # các lệnh
done

Ví dụ:

count=1
until [ $count -gt 5 ]
do
    echo "Đếm: $count"
    count=$((count+1))
done

Kết luận

Trên đây là một số khái niệm lập trình shell cơ bản giúp bạn bắt đầu viết các shell script của riêng mình. Lập trình shell là một công cụ mạnh mẽ để tự động hóa các tác vụ trong Linux và các hệ điều hành giống Unix.

Một số tài nguyên bổ sung để tiếp tục học:

Chúc bạn viết script vui vẻ!