Hướng Dẫn Bash Scripting Cơ Bản Cho Người Mới Bắt Đầu

Hướng Dẫn Bash Scripting Cơ Bản Cho Người Mới Bắt Đầu

Định nghĩa bash shell scripting

Bash

Bash là một trình thông dịch lệnh, được sử dụng rộng rãi trên nhiều hệ điều hành và là trình thông dịch mặc định trên hầu hết các hệ thống gnu/linux. Tên “bash” là viết tắt của “bourne-again shell.”

Shell

Shell là một bộ xử lý macro cho phép thực thi các lệnh một cách tương tác hoặc không tương tác.

Scripting

Scripting cho phép thực hiện tự động các lệnh mà nếu không bạn sẽ phải chạy chúng từng lệnh một cách tương tác.

Nếu bạn đang tìm kiếm một giải pháp lưu trữ hiệu quả và linh hoạt cho các dự án tự động hóa bằng Bash Scripting, VPS Hosting là lựa chọn lý tưởng. Với khả năng tùy chỉnh cao và hiệu suất ổn định, VPS Hosting giúp bạn triển khai và quản lý các script một cách dễ dàng trên môi trường Linux.

Những kiến thức cơ bản về bash shell script

Đừng lo lắng nếu bạn chưa hiểu hết các định nghĩa về bash shell scripting phía trên. Điều này hoàn toàn bình thường – và đó chính là lý do bạn đang đọc hướng dẫn bash scripting này.

Nếu bạn chưa biết, bash scripting là một kỹ năng không thể thiếu đối với bất kỳ công việc quản trị hệ thống linux nào, mặc dù có thể nhà tuyển dụng không yêu cầu một cách trực tiếp.

Shell là gì?

Có lẽ ngay bây giờ, bạn đang ngồi trước máy tính, mở cửa sổ terminal và tự hỏi: “Tôi nên làm gì với cái này?”

Thực tế, cửa sổ terminal của bạn chứa shell, và shell cho phép bạn tương tác với máy tính thông qua các lệnh, từ đó truy xuất hoặc lưu trữ dữ liệu, xử lý thông tin và thực hiện các tác vụ đơn giản lẫn phức tạp.

Hãy thử ngay bây giờ! Sử dụng bàn phím, gõ một số lệnh như date, cal, pwd hoặc ls, sau đó nhấn phím ENTER.

Ví dụ:

linuxconfig.org:~$ date 
Fri 31 Mar 11:44:46 AEDT 2017 
linuxconfig.org:~$ cal 
March 2017 
Su Mo Tu We Th Fr Sa 
          1  2  3  4 
 5  6  7  8  9 10 11 
12 13 14 15 16 17 18 
19 20 21 22 23 24 25 
26 27 28 29 30 31 
linuxconfig.org:~$ pwd 
/home/linuxconfig linuxconfig.org:~$ ls 
hello-world.sh 
linuxconfig.org:~$

Những gì bạn vừa làm là sử dụng shell để truy xuất thông tin: lấy ngày giờ hiện tại (date), xem lịch (cal), kiểm tra thư mục hiện tại (pwd) và liệt kê các tệp, thư mục (ls).

Scripting là gì?

Bây giờ, hãy tưởng tượng rằng bạn phải thực hiện các lệnh trên mỗi ngày. Mỗi ngày, bạn buộc phải chạy tất cả các lệnh đó và lưu lại thông tin. Rất nhanh bạn sẽ thấy công việc này trở nên cực kỳ nhàm chán và dễ dẫn đến sai sót. Rõ ràng, giải pháp là thực hiện tự động tất cả các lệnh đó cùng lúc. Đây chính là lúc scripting trở thành cứu cánh của bạn.

Để hiểu rõ ý nghĩa của scripting, hãy sử dụng shell kết hợp với trình soạn thảo văn bản yêu thích (ví dụ như vi) để tạo một tệp mới có tên task.sh chứa tất cả các lệnh đã nói, mỗi lệnh ở một dòng riêng biệt. Sau đó, làm cho tệp này có thể thực thi bằng lệnh chmod +x task.sh. Cuối cùng, chạy script bằng cách nhập ./task.sh.

Ví dụ:

linuxconfig.org:~$ vi task.sh 
linuxconfig.org:~$ chmod +x task.sh 
linuxconfig.org:~$ ./task.sh 
Fri 31 Mar 12:56:09 AEDT 2017 
March 2017 
Su Mo Tu We Th Fr Sa 
... 
/home/linuxconfig 
hello-world.sh task.sh 
linuxconfig.org:~$

Như bạn có thể thấy, bằng cách sử dụng script, mọi tương tác với shell có thể được tự động hóa và lập trình sẵn. Hơn nữa, giờ đây có thể tự động thực thi tập lệnh shell mới của chúng ta, task.sh, hàng ngày vào bất kỳ thời điểm nào bằng cách sử dụng trình lập lịch công việc dựa trên thời gian cron và lưu đầu ra của tập lệnh vào một tệp mỗi khi nó được thực thi.

Tuy nhiên, đó là câu chuyện của một ngày khác. Hiện tại, hãy tập trung vào nhiệm vụ trước mắt.

Bash là gì?

Cho đến nay, chúng ta đã đề cập đến shell và scripting. Vậy bash có vai trò như thế nào? Như đã nói, bash là trình thông dịch mặc định trên nhiều hệ thống GNU/Linux – nghĩa là bạn đã sử dụng bash mà không hề nhận ra. Vì thế, script shell trước đó hoạt động ngay cả khi bạn không khai báo bash là trình thông dịch. Để xem trình thông dịch mặc định của bạn, hãy chạy:

$ echo $SHELL
/bin/bash

Có nhiều trình thông dịch shell khác như Korn shell, C shell,… Vì lý do đó, khi viết script, tốt nhất bạn nên khai báo rõ trình thông dịch dùng để thông dịch nội dung script.

Để khai báo trình thông dịch cho script của bạn là bash, hãy tìm đường dẫn đầy đủ tới tệp thực thi của bash bằng lệnh which bash, sau đó thêm dòng sau vào đầu tệp script:

#!/bin/bash

Ví dụ:

linuxconfig.org:~$ vi task.sh

Và sau đó chạy script:

linuxconfig.org:~$ ./task.sh

Từ giờ trở đi, mọi script của chúng ta sẽ bắt đầu với khai báo #!/bin/bash.

Tên tệp và quyền thực thi

Hãy cùng nói qua về quyền truy cập tệp và tên tệp. Bạn có thể đã nhận thấy rằng để thực thi một script, tệp cần được làm cho có thể thực thi bằng lệnh chmod +x filename. Theo mặc định, các tệp mới tạo ra không có quyền thực thi, bất kể phần mở rộng của tệp.

Trên hệ thống GNU/Linux, phần mở rộng của tệp chủ yếu chỉ mang tính biểu thị – ví dụ, tệp có đuôi .sh thường được hiểu là script shell, trong khi tệp có đuôi .jpg có thể là hình ảnh.

Lệnh file có thể được dùng để xác định loại tệp. Ví dụ:

linuxconfig.org:~$ file hello-world.sh 
hello-world.sh: Bourne-Again shell script, ASCII text executable 
linuxconfig.org:~$ cp hello-world.sh 0_xvz 
linuxconfig.org:~$ file 0_xvz 
0_xvz: Bourne-Again shell script, ASCII text executable 
linuxconfig.org:~$ vi 0_xvz 
linuxconfig.org:~$ file 0_xvz 
0_xvz: ASCII text

Như vậy, tên script 0_xvz là hợp lệ, tuy nhiên tốt nhất nên tránh đặt tên như vậy để tránh nhầm lẫn.

Thực thi script

Một cách đơn giản, một bash script chỉ là một tệp văn bản chứa các chỉ thị được thực thi từ trên xuống dưới. Cách mà các chỉ thị này được thông dịch phụ thuộc vào khai báo shebang hoặc cách bạn gọi script. Hãy xem ví dụ sau:

linuxconfig.org:~$ echo date > date.sh 
linuxconfig.org:~$ cat date.sh 
date 
linuxconfig.org:~$ ./date.sh 
bash: ./date.sh: Permission denied 
linuxconfig.org:~$ bash date.sh 
Thu 20 Jul 11:46:30 AEST 2017 
linuxconfig.org:~$ vi date.sh 
linuxconfig.org:~$ chmod +x date.sh 
linuxconfig.org:~$ ./date.sh 
Thu 20 Jul 11:46:49 AEST 2017 
linuxconfig.org:~$

Ngoài ra, bạn có thể gọi bash trực tiếp, ví dụ: $ bash date.sh, để thực thi script mà không cần làm cho tệp có quyền thực thi hay khai báo shebang.

Đường dẫn tương đối vs tuyệt đối

Trước khi lập trình script bash đầu tiên của mình, hãy tìm hiểu sơ qua cách điều hướng trong shell và sự khác biệt giữa đường dẫn tương đối và tuyệt đối.

Hãy hình dung hệ thống tập tin GNU/Linux như một tòa nhà nhiều tầng. Thư mục gốc (được biểu thị bằng /) là cửa chính, mở ra toàn bộ hệ thống tập tin (tòa nhà), cho phép truy cập tới tất cả các thư mục (các tầng/phòng) và tệp tin (người).
Để vào phòng room1 ở tầng 3, trước tiên bạn phải đi qua cửa chính /, sau đó di chuyển tới tầng 3 (ví dụ, thư mục level3/), rồi vào phòng room1. Vậy đường dẫn tuyệt đối của phòng đó là /level3/room1.
Nếu bạn đang ở tầng 3 và muốn vào phòng room2 trong cùng tầng, bạn có thể dùng đường dẫn tương đối: ../room2, thay vì phải dùng đường dẫn tuyệt đối /level3/room2.

Lệnh pwd là “la bàn” giúp bạn biết chính xác vị trí hiện tại trong hệ thống tập tin. Hãy cùng xem ví dụ sử dụng lệnh cdpwd:

linuxconfig.org:~$ cd / 
linuxconfig.org:/$ pwd 
/ 
linuxconfig.org:/$ cd home/ 
linuxconfig.org:/home$ pwd 
/home 
linuxconfig.org:/home$ cd .. 
linuxconfig.org:/$ pwd 
/ 
linuxconfig.org:/$ ls 
bin   etc        initrd.img.old    lib64  media  proc  sbin  tmp  vmlinuz 
boot  home        lib              libx32  mnt   root  srv  usr   vmlinuz.old 
dev   initrd.img lib32         lost+found opt    run   sys   var
linuxconfig.org:/$ cd /etc/ 
linuxconfig.org:/etc$ cd ..
/home/linuxconfig/ 
linuxconfig.org:~$ pwd /home/linuxconfig 
linuxconfig.org:~$ cd - 
/etc 
linuxconfig.org:/etc$ pwd 
/etc 
linuxconfig.org:/etc$ cd - 
/home/linuxconfig 
linuxconfig.org:~$ pwd 
/home/linuxconfig 
linuxconfig.org:~$ cd ../../ 
linuxconfig.org:/$ pwd
/ 
linuxconfig.org:/$ cd 
linuxconfig.org:~$ pwd 
/home/linuxconfig 
linuxconfig.org:~$

Quick Tip:

  • cd không có tham số để nhanh chóng chuyển đến thư mục người dùng của bạn.

  • cd - để chuyển đổi giữa hai thư mục vừa truy cập.

Việc điều hướng trong hệ thống tập tin GNU/Linux khá đơn giản, nhưng đối với nhiều người lại khá rối rắm. Hãy làm quen với việc điều hướng trước khi chuyển sang các phần tiếp theo của hướng dẫn này.

Script “Hello World” cho bash shell

Giờ đã đến lúc tạo script bash đầu tiên, script đơn giản nhất chỉ in ra “Hello World” bằng lệnh echo.
Tạo một tệp mới tên hello-world.sh chứa nội dung sau:

#!/bin/bash

echo "Hello World"

Sau đó, làm cho script có thể thực thi với lệnh chmod +x hello-world.sh và chạy script bằng cách nhập ./hello-world.sh:

$ chmod +x hello-world.sh 
$ linuxconfig.org:~$ ./hello-world.sh 
Hello World
$

Một ví dụ video thay thế cũng cho thấy cách tạo script hello-world.sh sử dụng lệnh which bash để lấy đường dẫn đầy đủ của bash, đồng thời chuyển hướng đầu ra (sử dụng ký hiệu >) để tạo tệp mới hello-world.sh:

linuxconfig.org:~$ which bash > hello-world.sh 
linuxconfig.org:~$ vi hello-world.sh 
linuxconfig.org:~$ chmod +x hello-world.sh 
linuxconfig.org:~$ ./hello-world.sh 
Hello World 
linuxconfig.org:~$

Tên tệp và quyền truy cập

Tiếp theo, hãy nói qua về quyền truy cập và tên tệp. Như bạn đã biết, để thực thi một script, tệp cần được cấp quyền thực thi với lệnh chmod +x filename.
Trên GNU/Linux, phần mở rộng của tệp thường chỉ giúp bạn nhận diện loại tệp – ví dụ, tệp có đuôi .sh thường là script shell, tệp có đuôi .jpg có thể là ảnh.

Lệnh file có thể được dùng để xác định loại tệp:

linuxconfig.org:~$ file hello-world.sh 
hello-world.sh: Bourne-Again shell script, ASCII text executable 
linuxconfig.org:~$ cp hello-world.sh 0_xvz 
linuxconfig.org:~$ file 0_xvz 
0_xvz: Bourne-Again shell script, ASCII text executable 
linuxconfig.org:~$ vi 0_xvz 
linuxconfig.org:~$ file 0_xvz 
0_xvz: ASCII text

Như vậy, tên script 0_xvz hoàn toàn hợp lệ, tuy nhiên bạn nên tránh đặt tên như vậy nếu có thể.

Thực thi script

Như đã đề cập, một bash script chỉ là một tệp văn bản chứa các chỉ thị sẽ được thực hiện từ trên xuống dưới. Cách các chỉ thị được thông dịch phụ thuộc vào khai báo shebang hoặc cách bạn gọi script.

Hãy xem ví dụ:

linuxconfig.org:~$ echo date > date.sh 
linuxconfig.org:~$ cat date.sh 
date 
linuxconfig.org:~$ ./date.sh 
bash: ./date.sh: Permission denied 
linuxconfig.org:~$ bash date.sh 
Thu 20 Jul 11:46:30 AEST 2017 
linuxconfig.org:~$ vi date.sh 
linuxconfig.org:~$ chmod +x date.sh 
linuxconfig.org:~$ ./date.sh 
Thu 20 Jul 11:46:49 AEST 2017 
linuxconfig.org:~$

Bạn cũng có thể gọi bash trực tiếp với bash date.sh, giúp chạy script mà không cần cấp quyền thực thi hay khai báo shebang.

Đường dẫn tương đối vs. tuyệt đối

Cuối cùng, trước khi chúng ta lập trình tập lệnh Bash chính thức đầu tiên, hãy cùng thảo luận ngắn gọn về cách điều hướng trong shell và sự khác biệt giữa đường dẫn tệp tương đối và tuyệt đối.

Có lẽ cách so sánh dễ hiểu nhất để giải thích sự khác biệt giữa đường dẫn tương đối và tuyệt đối là hình dung hệ thống tệp GNU/Linux như một tòa nhà nhiều tầng. Thư mục gốc / (cửa chính của tòa nhà) cung cấp lối vào toàn bộ hệ thống tệp (tòa nhà), từ đó cho phép truy cập vào tất cả các thư mục (các tầng/phòng) và tệp (con người).

Để đi đến phòng 1 trên tầng 3, trước tiên chúng ta cần đi qua cửa chính /, sau đó di chuyển đến tầng 3 (level3/), và từ đó vào phòng 1 (room1). Do đó, đường dẫn tuyệt đối đến phòng này là /level3/room1.

Từ đây, nếu chúng ta muốn đến phòng 2 trên cùng tầng 3, trước tiên cần rời khỏi vị trí hiện tại (room1) bằng cách nhập ../, sau đó thêm tên phòng room2. Như vậy, đường dẫn tương đối đến room2 trong trường hợp này là ../room2. Vì chúng ta đã ở tầng 3, nên không cần phải rời khỏi toàn bộ tòa nhà và sử dụng đường dẫn tuyệt đối /level3/room2 qua cửa chính.

May mắn thay, GNU/Linux cung cấp một công cụ định hướng đơn giản để giúp bạn điều hướng trong hệ thống tệp, đó là lệnh pwd. Khi được thực thi, lệnh này sẽ luôn hiển thị vị trí hiện tại của bạn. Ví dụ sau sẽ sử dụng các lệnh cd và pwd để điều hướng trong hệ thống tệp GNU/Linux bằng cả đường dẫn tuyệt đối và tương đối.

linuxconfig.org:~$ cd / 
linuxconfig.org:/$ pwd 
/ 
linuxconfig.org:/$ cd home/ 
linuxconfig.org:/home$ pwd 
/home 
linuxconfig.org:/home$ cd .. 
linuxconfig.org:/$ pwd 
/ 
linuxconfig.org:/$ ls ... 
linuxconfig.org:/$ cd /etc/ 
linuxconfig.org:/etc$ cd ../home/linuxconfig/ 
linuxconfig.org:~$ pwd 
/home
/linuxconfig 
linuxconfig.org:~$ cd - 
/etc 
linuxconfig.org:/etc$ pwd 
/etc 
linuxconfig.org:/etc$ cd - 
/home/linuxconfig 
linuxconfig.org:~$ pwd 
/ home/linuxconfig

Quick Tip:

  • cd không có tham số để chuyển ngay đến thư mục người dùng.

  • cd - để chuyển đổi giữa hai thư mục vừa truy cập.

Script “Hello World”

Bây giờ, hãy tạo script bash đơn giản đầu tiên của bạn để in ra “Hello World” bằng lệnh echo.

Tạo một tệp mới tên hello-world.sh với nội dung:

#!/bin/bash

echo "Hello World"

Sau đó, cấp quyền thực thi và chạy script:

$ chmod +x hello-world.sh 
$ linuxconfig.org:~$ ./hello-world.sh 
Hello World
$

Tập lệnh Shell Bash sao lưu đơn giản

Hãy cùng thảo luận về cách thực thi lệnh trên dòng lệnh và cách các lệnh GNU/Linux phù hợp với quá trình tạo tập lệnh shell một cách chi tiết hơn.

Bất kỳ lệnh nào có thể thực thi trực tiếp thành công trong terminal Bash cũng có thể được sử dụng theo cùng một cách trong tập lệnh shell Bash. Thực tế, không có sự khác biệt giữa việc thực thi lệnh trực tiếp trên terminal và trong tập lệnh shell, ngoại trừ việc tập lệnh shell cho phép thực thi nhiều lệnh một cách tự động, không cần tương tác, như một quá trình duy nhất.

Mẹo nhanh:

Dù tập lệnh của bạn có phức tạp đến đâu, đừng cố gắng viết toàn bộ nó trong một lần. Hãy phát triển dần dần bằng cách kiểm tra từng dòng lệnh chính bằng cách chạy trực tiếp trên terminal trước. Khi lệnh chạy thành công, hãy đưa nó vào tập lệnh shell của bạn.

Ngoài ra, hầu hết các lệnh đều chấp nhận tùy chọn (options) và đối số (arguments).

  • Tùy chọn (options): Dùng để thay đổi hành vi của lệnh nhằm tạo ra các kết quả đầu ra khác nhau, thường được bắt đầu bằng dấu -.

  • Đối số (arguments): Chỉ định đối tượng mà lệnh sẽ thao tác, chẳng hạn như tệp, thư mục, văn bản, v.v.

Mỗi lệnh trong GNU/Linux đều có một trang hướng dẫn (manual page) giúp bạn tìm hiểu chức năng của lệnh cũng như các tùy chọn và đối số mà lệnh hỗ trợ.

Bạn có thể sử dụng lệnh man để hiển thị trang hướng dẫn của bất kỳ lệnh nào. Ví dụ, để xem hướng dẫn của lệnh ls, hãy chạy:

man ls

Để thoát khỏi trang hướng dẫn, nhấn phím q.

Dưới đây là một ví dụ về cách sử dụng các tùy chọn và đối số với lệnh ls:

#!/bin/bash 
# Script 
bash này được sử dụng để sao lưu thư mục home của người dùng tới /tmp/. user=$(whoami) input=/home/$user output=/tmp/${user}_home_$(date +%Y-%m-%d_%H%M%S).tar.gz tar -czf $output $input echo "Backup of $input completed! Details about the output backup file:" ls -l $output

Chạy lệnh sao lưu:

linuxconfig.org:~$ tar -czf /tmp/myhome_directory.tar.gz /home/linuxconfig/ 
tar: Removing leading `/' from member names 
linuxconfig.org:~$ ls -l /tmp/myhome_directory.tar.gz 
-rw-r--r-- 1 linuxconfig 
linuxconfig 8447 Jul 26 09:21 /tmp/myhome_directory.tar.gz 
linuxconfig.org:~$ rm /tmp/myhome_directory.tar.gz

Sau đó tạo script backup:

linuxconfig.org:~$ which bash > backup.sh 
linuxconfig.org:~$ echo 'tar -czf /tmp/myhome_directory.tar.gz /home/linuxconfig/' >> backup.sh 
linuxconfig.org:~$ vi backup.sh 
linuxconfig.org:~$ chmod +x backup.sh 
linuxconfig.org:~$ ./backup.sh tar: Removing leading `/' from member names 
linuxconfig.org:~$ ls -l /tmp/myhome_directory.tar.gz -rw-r--r-- 1 linuxconfig 
linuxconfig 8433 Jul 26 09:22 /tmp/myhome_directory.tar.gz 
linuxconfig.org:~$

Quick Tip:
Dùng man tar để tìm hiểu thêm về các tùy chọn của lệnh tar.

Biến (Variables)

Biến là cốt lõi của lập trình. Chúng cho phép bạn lưu trữ dữ liệu, thay đổi và sử dụng lại trong suốt script. Tạo một script mới tên welcome.sh với nội dung:

#!/bin/bash

greeting="Welcome"
user=$(whoami)
day=$(date +%A)

echo "$greeting back $user! Today is $day, which is the best day of the entire week!"
echo "Your Bash shell version is: $BASH_VERSION. Enjoy!"

Khi chạy script:

$ ./welcome.sh 
Welcome back linuxconfig! Today is Wednesday, which is the best day of the entire week!
Your Bash shell version is: 4.4.12(1)-release. Enjoy!

Giải thích:

  • Biến greeting được gán chuỗi “Welcome”.

  • Biến user nhận giá trị từ lệnh whoami. Đây là kỹ thuật gọi là command substitution.

  • Tương tự, biến day lưu ngày hiện tại từ lệnh date +%A.

  • Lệnh echo in ra thông điệp với các biến được thay thế bằng giá trị của chúng.

Quick Tip:
Không nên đặt tên biến riêng tư bằng chữ in HOA vì chúng dành riêng cho các biến nội bộ của shell.

Ví dụ về biến trên dòng lệnh:

linuxconfig.org:~$ a=4 
linuxconfig.org:~$ b=8 
linuxconfig.org:~$ echo $a 
4 
linuxconfig.org:~$ echo $b 
8 
linuxconfig.org:~$ echo $[$a + $b] 
12

Bây giờ, sau khi đã tìm hiểu về biến trong Bash, chúng ta có thể cập nhật tập lệnh sao lưu của mình để tạo tên tệp đầu ra có ý nghĩa hơn bằng cách thêm ngày và giờ khi sao lưu thư mục home của người dùng thực sự được thực hiện.

Hơn nữa, tập lệnh này sẽ không còn bị ràng buộc với một người dùng cụ thể. Từ bây giờ, tập lệnh backup.sh của chúng ta có thể được chạy bởi bất kỳ người dùng nào mà vẫn sao lưu đúng thư mục home của họ:

#!/bin/bash

# This bash script is used to backup a user's home directory to /tmp/.

user=$(whoami)
input=/home/$user
output=/tmp/${user}_home_$(date +%Y-%m-%d_%H%M%S).tar.gz

tar -czf $output $input
echo "Backup of $input completed! Details about the output backup file:"
ls -l $output

Bạn có thể đã nhận thấy rằng tập lệnh trên giới thiệu hai khái niệm mới trong lập trình Bash:

  1. Dòng chú thích: Tập lệnh backup.sh mới của chúng ta chứa các dòng chú thích. Mọi dòng bắt đầu bằng ký tự # (ngoại trừ dòng shebang #!/bin/bash) sẽ không được Bash thực thi và chỉ phục vụ như ghi chú nội bộ của lập trình viên.

  2. Mở rộng tham số (parameter expansion):

    • Cách sử dụng ${parameter} cho phép mở rộng biến trong Bash.

    • Trong trường hợp này, dấu {} là bắt buộc vì biến $user được theo sau bởi các ký tự không thuộc tên biến.

Dưới đây là đầu ra của tập lệnh sao lưu đã cập nhật:

$ ./backup.sh 
tar: Removing leading `/' from member names
Backup of /home/linuxconfig completed! Details about the output backup file:
-rw-r--r-- 1 linuxconfig linuxconfig 8778 Jul 27 12:30 /tmp/linuxconfig_home_2017-07-27_123043.tar.gz
Đầu vào, đầu ra và chuyển hướng lỗi

Thông thường, các lệnh được thực thi trên dòng lệnh GNU/Linux có thể tạo ra đầu ra, yêu cầu đầu vào hoặc hiển thị thông báo lỗi. Đây là một khái niệm quan trọng không chỉ trong lập trình shell mà còn khi làm việc với dòng lệnh GNU/Linux nói chung.

Mỗi khi bạn thực thi một lệnh, có thể xảy ra ba kết quả:

  1. Lệnh tạo ra đầu ra như mong đợi.

  2. Lệnh phát sinh lỗi.

  3. Lệnh không tạo ra bất kỳ đầu ra nào.

Ví dụ:

linuxconfig.org:~$ ls -l foobar 
ls: cannot access 'foobar': No such file or directory 
linuxconfig.org:~$ touch foobar 
linuxconfig.org:~$ ls -l foobar 
-rw-r--r-- 1 linuxconfig 
linuxconfig 0 Jul 28 10:08 foobar

Sự khác biệt giữa stdout và stderr

Hai lệnh ls -l foobar trên đều tạo ra đầu ra hiển thị trên terminal, nhưng bản chất chúng khác nhau:

  • Lệnh đầu tiên cố gắng liệt kê tệp foobar, nhưng tệp không tồn tại, do đó tạo ra đầu ra lỗi stderr.

  • Lệnh thứ hai thực thi thành công sau khi tệp được tạo bởi touch foobar, do đó tạo ra đầu ra chuẩn stdout.

Sự khác biệt giữa stdout và stderr rất quan trọng vì chúng có thể được chuyển hướng một cách độc lập:

  • Dùng > để chuyển hướng stdout sang một tệp.

  • Dùng 2> để chuyển hướng stderr sang một tệp.

  • Dùng &> để chuyển hướng cả stdout và stderr.

Ví dụ:

linuxconfig.org:~$ ls foobar barfoo 
ls: cannot access 'barfoo': No such file or directory foobar 
linuxconfig.org:~$ ls foobar barfoo > stdout.txt 
ls: cannot access 'barfoo': No such file or directory 
linuxconfig.org:~$ ls foobar barfoo 2> stderr.txt foobar 
linuxconfig.org:~$ ls foobar barfoo &> stdoutandstderr.txt 
linuxconfig.org:~$ cat stdout.txt foobar 
linuxconfig.org:~$ cat stderr.txt 
ls: cannot access 'barfoo': No such file or directory 
linuxconfig.org:~$ cat stdoutandstderr.txt 
ls: cannot access 'barfoo': No such file or directory foobar

Mẹo nhanh

Nếu bạn không chắc chắn một lệnh tạo ra stdout hay stderr, hãy thử chuyển hướng đầu ra của nó:

  • Nếu lệnh có thể được chuyển hướng thành công bằng 2>, tức là nó tạo ra stderr.

  • Nếu chuyển hướng bằng > thành công, tức là nó tạo ra stdout.

Cập nhật tập lệnh backup.sh để loại bỏ thông báo lỗi stderr

Khi chạy tập lệnh sao lưu, bạn có thể thấy thông báo từ lệnh tar:

tar: Removing leading `/' from member names

Mặc dù chỉ là thông báo mang tính chất thông tin, nhưng nó được gửi đến stderr. Thông báo này đơn giản chỉ ra rằng đường dẫn tuyệt đối đã bị loại bỏ để tránh ghi đè lên bất kỳ tệp nào khi giải nén.

Bây giờ, chúng ta có thể loại bỏ thông báo này bằng cách chuyển hướng stderr sang /dev/null bằng 2> /dev/null:

#!/bin/bash

# This bash script is used to backup a user's home directory to /tmp/.

user=$(whoami)
input=/home/$user
output=/tmp/${user}_home_$(date +%Y-%m-%d_%H%M%S).tar.gz

tar -czf $output $input 2> /dev/null
echo "Backup of $input completed! Details about the output backup file:"
ls -l $output

Khi chạy phiên bản mới của tập lệnh backup.sh, không còn hiển thị thông báo lỗi từ tar.

Tìm hiểu về stdin – Nhập dữ liệu vào shell

Ngoài stdout và stderr, Bash shell còn có bộ mô tả nhập stdin.

  • Thông thường, stdin là bàn phím, nơi bạn nhập dữ liệu.

  • Tuy nhiên, bạn cũng có thể chuyển hướng đầu vào từ một tệp bằng ><.

Ví dụ:

  • Chúng ta sử dụng cat để nhập văn bản từ bàn phím và chuyển hướng đầu ra vào file1.txt.

  • Sau đó, chúng ta đọc nội dung từ file1.txt bằng cách sử dụng <.

linuxconfig.org:~$ 
cat > file1.txt 
I am using keyboard to input text. 
Cat command reads my keyboard input, converts it to stdout which is instantly redirected to file1.txt 
That is, until I press CTRL+D 
linuxconfig.org:~$ cat < file1.txt 
I am using keyboard to input text. 
Cat command reads my keyboard input, converts it to stdout which is instantly redirected to file1.txt 
That is, until I press CTRL+D

Bây giờ bạn đã hiểu cách chuyển hướng đầu vào và đầu ra trong Bash, điều này sẽ giúp bạn kiểm soát tốt hơn tập lệnh của mình!

Hàm (Functions)

Hàm (functions) cho phép lập trình viên tổ chức và tái sử dụng mã nguồn, giúp tăng hiệu suất, tốc độ thực thi và độ dễ đọc của toàn bộ tập lệnh.

Bạn hoàn toàn có thể viết một tập lệnh mà không cần sử dụng bất kỳ hàm nào. Tuy nhiên, điều này có thể dẫn đến mã nguồn cồng kềnh, kém hiệu quả và khó bảo trì.

Mẹo nhanh:

Khi nhận thấy mã nguồn của bạn có hai dòng lệnh giống nhau, hãy cân nhắc sử dụng một hàm để tối ưu hóa mã.

Bạn có thể nghĩ về hàm như một cách để nhóm nhiều lệnh thành một lệnh duy nhất. Điều này đặc biệt hữu ích khi bạn cần thực hiện một loạt lệnh nhiều lần trong suốt quá trình chạy tập lệnh.

Trong Bash, hàm được khai báo bằng từ khóa function, theo sau là thân hàm được đặt trong {}.

Ví dụ về hàm trong Bash

Đoạn mã sau định nghĩa một hàm đơn giản user_details, được sử dụng để in thông tin người dùng. Tập lệnh này sẽ gọi hàm hai lần, do đó in thông tin người dùng hai lần khi thực thi.

#!/bin/bash
# Định nghĩa hàm user_details
function user_details {
echo "User Name: $(whoami)"
echo "Home Directory: $HOME"
}
# Gọi hàm hai lần
user_details
user_details

📌 Lưu ý:

  • Tên hàm là user_details.

  • Thân hàm chứa hai lệnh echo.

  • Mỗi lần gọi hàm, cả hai lệnh echo sẽ được thực thi.

  • Hàm phải được định nghĩa trước khi gọi, nếu không tập lệnh sẽ báo lỗi function not found.

Ví dụ chạy tập lệnh:

$ ./function.sh
User Name: linuxconfig
Home Directory: /home/linuxconfig
User Name: linuxconfig
Home Directory: /home/linuxconfig

Như bạn thấy, user_details giúp nhóm nhiều lệnh thành một lệnh duy nhất.

Kỹ thuật thụt lề trong Bash

Trong ví dụ trên, các lệnh echo trong user_details được thụt lề một TAB sang phải, giúp mã dễ đọc và dễ bảo trì hơn.

Không có quy tắc cố định về cách thụt lề trong Bash, bạn có thể chọn:

  • Dùng TAB

  • Dùng 4 dấu cách ( )

Cải tiến tập lệnh backup.sh với hàm

Bây giờ, chúng ta sẽ nâng cấp tập lệnh backup.sh để thêm hai hàm mới. Hai hàm này sẽ đếm số lượng tệp và thư mục sẽ được nén vào tệp sao lưu.

#!/bin/bash

# This bash script is used to backup a user's home directory to /tmp/.

user=$(whoami)
input=/home/$user
output=/tmp/${user}_home_$(date +%Y-%m-%d_%H%M%S).tar.gz

# The function total_files reports a total number of files for a given directory. 
function total_files {
        find $1 -type f | wc -l
}

# The function total_directories reports a total number of directories
# for a given directory. 
function total_directories {
        find $1 -type d | wc -l
}

tar -czf $output $input 2> /dev/null

echo -n "Files to be included:"
total_files $input
echo -n "Directories to be included:"
total_directories $input

echo "Backup of $input completed!"

echo "Details about the output backup file:"
ls -l $output

Giải thích cập nhật tập lệnh

  • Hàm total_files sử dụng find để tìm tất cả tệp trong thư mục được cung cấp, sau đó đếm số lượng bằng wc -l.

  • Hàm total_directories tương tự, nhưng đếm thư mục thay vì tệp.

  • Trước khi sao lưu, tập lệnh hiển thị số lượng tệp và thư mục được nén.

Mẹo nhanh:

Muốn tìm hiểu thêm về find, wcecho? Chạy lệnh:

$ man find

Kết quả khi chạy tập lệnh backup.sh

$ ./backup.sh 
Files to be included:19
Directories to be included:2
Backup of /home/linuxconfig completed!
Details about the output backup file:
-rw-r--r-- 1 linuxconfig linuxconfig 5520 Aug 16 11:01 /tmp/linuxconfig_home_2017-08-16_110121.tar.gz
So sánh số và chuỗi

Chúng ta cũng cần biết cách so sánh các giá trị số và chuỗi trong bash. Dưới đây là bảng so sánh cơ bản:

Mô tả So sánh số So sánh chuỗi
Ít hơn -lt hoặc <
Lớn hơn -gt hoặc >
Bằng -eq hoặc = =
Không bằng -ne hoặc != !=
Ít hơn hoặc bằng -le
Lớn hơn hoặc bằng -ge

Ví dụ so sánh số:

linuxconfig.org:~$ a=1 
linuxconfig.org:~$ b=2 
linuxconfig.org:~$ [ $a -lt $b ] 
linuxconfig.org:~$ echo $? 0 
linuxconfig.org:~$ [ $a -gt $b ] 
linuxconfig.org:~$ echo $? 1

Ví dụ so sánh chuỗi:

linuxconfig.org:~$ [ "apples" = "oranges" ] 
linuxconfig.org:~$ echo $? 
1

Một ví dụ script so sánh:

#!/bin/bash

string_a="UNIX"
string_b="GNU"

echo "Are $string_a and $string_b strings equal?"
[ $string_a = $string_b ]
echo $?

num_a=100
num_b=100

echo "Is $num_a equal to $num_b ?" 
[ $num_a -eq $num_b ]
echo $?

Khi chạy script, kết quả sẽ là:

$ chmod +x compare.sh 
$ ./compare.sh 
Are UNIX and GNU strings equal?
1
Is 100 equal to 100 ?
0

Quick Tip:
Không so sánh chuỗi với số bằng các toán tử số học vì sẽ gây lỗi “integer expression expected.”

Câu lệnh điều kiện (Conditional Statements)

Bây giờ, đã đến lúc chúng ta thêm logic vào tập lệnh sao lưu bằng cách sử dụng một vài câu lệnh điều kiện. Các câu lệnh điều kiện cho phép lập trình viên thực hiện việc ra quyết định trong tập lệnh shell dựa trên các điều kiện hoặc sự kiện nhất định.

Các câu lệnh điều kiện mà chúng ta đang nói đến, dĩ nhiên, là if, then và else. Ví dụ, chúng ta có thể cải thiện tập lệnh sao lưu của mình bằng cách thực hiện một kiểm tra hợp lý để so sánh số lượng tệp và thư mục trong thư mục nguồn mà chúng ta dự định sao lưu với tệp sao lưu được tạo ra. Pseudocode cho kiểu triển khai này sẽ như sau:

IF số lượng tệp giữa nguồn và đích là bằng nhau
THÌ in thông báo OK,
ELSE in thông báo ERROR.

Hãy bắt đầu bằng cách tạo một tập lệnh Bash đơn giản minh họa cấu trúc if/then/else cơ bản.

#!/bin/bash

num_a=100
num_b=200

if [ $num_a -lt $num_b ]; then
    echo "$num_a is less than $num_b!"
fi

Hiện tại, phần else được bỏ qua một cách có chủ ý, chúng ta sẽ bổ sung nó sau khi hiểu rõ logic của tập lệnh trên. Lưu tập lệnh với tên ví dụ if_else.sh và thực thi nó:

linuxconfig.org:~$ chmod +x if_else.sh 
linuxconfig.org:~$ ./if_else.sh 
100 is less than 200! 
linuxconfig.org:~$ vi if_else.sh 
linuxconfig.org:~$ ./if_else.sh 
linuxconfig.org:~$

Các dòng 3 – 4 được sử dụng để khởi tạo các biến số nguyên. Trên dòng 6, chúng ta bắt đầu một khối điều kiện if. Chúng ta so sánh hai biến và nếu kết quả so sánh trả về true, thì trên dòng 7 lệnh echo sẽ thông báo rằng giá trị của biến $num_a nhỏ hơn so với biến $num_b. Dòng 8 kết thúc khối điều kiện bằng từ khóa fi.

Quan sát quan trọng từ việc thực thi tập lệnh là, trong trường hợp biến $num_a lớn hơn $num_b, tập lệnh của chúng ta không phản hồi gì. Đây chính là lúc mà mảnh ghép cuối cùng, câu lệnh else, trở nên hữu ích. Hãy cập nhật tập lệnh của bạn bằng cách thêm khối else và thực thi nó:

#!/bin/bash

num_a=400
num_b=200

if [ $num_a -lt $num_b ]; then
    echo "$num_a is less than $num_b!"
else
    echo "$num_a is greater than $num_b!"
fi

Dòng 8 giờ đây chứa phần else của khối điều kiện. Nếu kết quả so sánh trên dòng 6 trả về false, thì đoạn mã dưới dòng else (trong trường hợp này là dòng 9) sẽ được thực thi.

Ví dụ về kết quả thực thi:

linuxconfig.org:~$ ./if_else.sh 
100 is less than 200! 
linuxconfig.org:~$ vi if_else.sh 
linuxconfig.org:~$ ./if_else.sh 
800 is greater than 200! 
linuxconfig.org:~$

Bài tập:
Bạn có thể viết lại tập lệnh if_else.sh để đảo ngược logic thực thi sao cho khối else được thực thi nếu biến $num_a nhỏ hơn biến $num_b?

Trang bị kiến thức cơ bản về các câu lệnh điều kiện, chúng ta giờ có thể cải thiện tập lệnh của mình để thực hiện một kiểm tra hợp lý bằng cách so sánh sự khác biệt giữa tổng số tệp trước và sau khi thực hiện lệnh sao lưu. Dưới đây là phiên bản cập nhật mới của tập lệnh backup.sh:

#!/bin/bash

user=$(whoami)
input=/home/$user
output=/tmp/${user}_home_$(date +%Y-%m-%d_%H%M%S).tar.gz

function total_files {
        find $1 -type f | wc -l
}

function total_directories {
        find $1 -type d | wc -l
}

function total_archived_directories {
        tar -tzf $1 | grep  /$ | wc -l
}

function total_archived_files {
        tar -tzf $1 | grep -v /$ | wc -l
}

tar -czf $output $input 2> /dev/null

src_files=$( total_files $input )
src_directories=$( total_directories $input )

arch_files=$( total_archived_files $output )
arch_directories=$( total_archived_directories $output )

echo "Files to be included: $src_files"
echo "Directories to be included: $src_directories"
echo "Files archived: $arch_files"
echo "Directories archived: $arch_directories"

if [ $src_files -eq $arch_files ]; then
        echo "Backup of $input completed!"
        echo "Details about the output backup file:"
        ls -l $output
else
        echo "Backup of $input failed!"
fi

Có vài bổ sung quan trọng trong tập lệnh trên. Các điểm thay đổi nổi bật nhất:

  • Dòng 15 – 21: Định nghĩa hai hàm mới trả về tổng số tệp và thư mục được bao gồm trong tệp sao lưu nén được tạo ra.

  • Sau khi thực hiện sao lưu, dòng 23 được thực thi. Trên các dòng 25 – 29, chúng ta khai báo các biến mới để giữ số lượng tệp và thư mục của nguồn và đích.

  • Các biến liên quan đến số lượng tệp được sao lưu được sử dụng trên các dòng 36 – 42 trong khối điều kiện if/then/else. Khối này sẽ chỉ in thông báo thành công khi tổng số tệp của nguồn và tệp sao lưu đích là bằng nhau (theo như khai báo trên dòng 36).

Ví dụ kết quả thực thi tập lệnh:

$ ./backup.sh 
Files to be included: 24
Directories to be included: 4
Files archived: 24
Directories archived: 4
Backup of /home/linuxconfig completed!
Details about the output backup file:
-rw-r--r-- 1 linuxconfig linuxconfig 235569 Sep 12 12:43 /tmp/linuxconfig_home_2017-09-12_124319.tar.gz

Tóm lại, chúng ta đã bổ sung logic vào tập lệnh sao lưu bằng cách sử dụng các câu lệnh điều kiện. Nếu tổng số tệp trong thư mục nguồn bằng với tổng số tệp được sao lưu, tập lệnh thông báo sao lưu thành công và hiển thị chi tiết tệp sao lưu; nếu không, nó thông báo rằng quá trình sao lưu đã thất bại.

Tham số vị trí (Positional Parameters)

Cho đến nay, tập lệnh sao lưu của chúng ta đã hoạt động rất tốt. Chúng ta có thể đếm số lượng tệp và thư mục được bao gồm trong tệp sao lưu nén kết quả. Hơn nữa, tập lệnh của chúng ta còn thực hiện một kiểm tra hợp lý (sanity check) để xác nhận rằng tất cả các tệp đã được sao lưu chính xác. Nhược điểm là chúng ta luôn bị buộc phải sao lưu thư mục của người dùng hiện tại. Sẽ thật tuyệt nếu tập lệnh đủ linh hoạt để cho phép quản trị viên hệ thống sao lưu thư mục home của bất kỳ người dùng hệ thống nào chỉ bằng cách trỏ tập lệnh tới thư mục home của họ.

Khi sử dụng các tham số vị trí (bash positional parameters), việc này trở nên khá đơn giản. Các tham số vị trí được truyền qua các đối số dòng lệnh và có thể truy cập trong tập lệnh bằng các biến $1, $2$N. Trong quá trình thực thi tập lệnh, bất kỳ đối số bổ sung nào được cung cấp sau tên chương trình sẽ được coi là các đối số và có sẵn trong suốt quá trình chạy tập lệnh. Hãy xem ví dụ sau:

linuxconfig.org:~$ which bash > param.sh 
linuxconfig.org:~$ vi param.sh 
linuxconfig.org:~$ chmod +x param.sh 
linuxconfig.org:~$ ./param.sh 1 2 3 4 
1 2 4
4 
1 2 3 4 
linuxconfig.org:~$ ./param.sh hello bash scripting world 
hello bash world 
4 
hello bash scripting world 
linuxconfig.org:~$

Hãy cùng xem xét chi tiết tập lệnh bash được sử dụng ở trên:

#!/bin/bash

echo $1 $2 $4
echo $#
echo $*
  • Trên dòng 3, chúng ta in ra các tham số vị trí thứ nhất, thứ hai và thứ tư theo đúng thứ tự như chúng được cung cấp khi thực thi tập lệnh. Tham số thứ ba mặc dù có sẵn nhưng được bỏ qua có chủ ý ở dòng này.

  • Sử dụng $# ở dòng 4, chúng ta in ra tổng số đối số được cung cấp. Điều này rất hữu ích khi cần kiểm tra số lượng đối số mà người dùng đã cung cấp khi chạy tập lệnh.

  • Cuối cùng, $* ở dòng 5 được sử dụng để in ra tất cả các đối số.

Trang bị kiến thức về các tham số vị trí, bây giờ chúng ta hãy cải tiến tập lệnh backup.sh để nó nhận đối số từ dòng lệnh. Mục tiêu ở đây là cho phép người dùng quyết định thư mục nào sẽ được sao lưu. Trong trường hợp người dùng không cung cấp đối số nào khi chạy tập lệnh, theo mặc định tập lệnh sẽ sao lưu thư mục home của người dùng hiện tại. Tập lệnh mới được trình bày như sau:

#!/bin/bash                                                                                                                                                                                                                                                                    
                                                                                                                                                                                                                                                                               
# This bash script is used to backup a user's home directory to /tmp/.                                                                                                                                                                                                         
                                                                                                                                                                                                                                                                               
if [ -z $1 ]; then                                                                                                                                                                                                                                                             
        user=$(whoami)                                                                                                                                                                                                                                                         
else                                                                                                                                                                                                                                                                           
        if [ ! -d "/home/$1" ]; then                                                                                                                                                                                                                                           
                echo "Requested $1 user home directory doesn't exist."                                                                                                                                                                                                         
                exit 1                                                                                                                                                                                                                                                         
        fi                                                                                                                                                                                                                                                                     
        user=$1                                                                                                                                                                                                                                                                
fi                                                                                                                                                                                                                                                                             
                                                                                                                                                                                                                                                                               
input=/home/$user                                                                                                                                                                                                                                                              
output=/tmp/${user}_home_$(date +%Y-%m-%d_%H%M%S).tar.gz                                                                                                                                                                                                                       
                                                                                                                                                                                                                                                                               
function total_files {                                                                                                                                                                                                                                                         
        find $1 -type f | wc -l                                                                                                                                                                                                                                                
}                                                                                                                                                                                                                                                                              
                                                                                                                                                                                                                                                                               
function total_directories {                                                                                                                                                                                                                                                   
        find $1 -type d | wc -l                                                                                                                                                                                                                                                
}                                                                                                                                                                                                                                                                              
                                                                                                                                                                                                                                                                               
function total_archived_directories {                                                                                                                                                                                                                                          
        tar -tzf $1 | grep  /$ | wc -l                                                                                                                                                                                                                                         
}                                                                                                                                                                                                                                                                              
                                                                                                                                                                                                                                                                               
function total_archived_files {                                                                                                                                                                                                                                                
        tar -tzf $1 | grep -v /$ | wc -l                                                                                                                                                                                                                                       
}                                                                                                                                                                                                                                                                              
                                                                                                                                                                                                                                                                               
tar -czf $output $input 2> /dev/null                                                                                                                                                                                                                                           
                                                                                                                                                                                                                                                                               
src_files=$( total_files $input )
src_directories=$( total_directories $input )

arch_files=$( total_archived_files $output )
arch_directories=$( total_archived_directories $output )

echo "Files to be included: $src_files"
echo "Directories to be included: $src_directories"
echo "Files archived: $arch_files"
echo "Directories archived: $arch_directories"

if [ $src_files -eq $arch_files ]; then
        echo "Backup of $input completed!"
        echo "Details about the output backup file:"
        ls -l $output
else
        echo "Backup of $input failed!"
fi

Các điểm mới được giới thiệu trong bản cập nhật tập lệnh backup.sh trên, ngoài phần code từ dòng 5 đến 13 đã khá dễ hiểu, được giải thích như sau:

  • Dòng 5: Sử dụng tùy chọn -z của Bash kết hợp với câu lệnh điều kiện if để kiểm tra xem tham số vị trí $1 có chứa giá trị hay không. Tùy chọn -z trả về true nếu độ dài của chuỗi (ở đây là biến $1) bằng 0. Nếu đúng như vậy, ta gán biến $user bằng tên người dùng hiện tại.

  • Dòng 8: Ngược lại, ta kiểm tra xem thư mục home của người dùng được yêu cầu có tồn tại hay không bằng tùy chọn -d. Lưu ý dấu chấm than ! trước -d đóng vai trò phủ định. Theo mặc định, tùy chọn -d trả về true nếu thư mục tồn tại, nên dấu ! sẽ đảo ngược logic. Nếu thư mục không tồn tại, ta in ra thông báo lỗi ở dòng 9.

  • Dòng 10: Sử dụng lệnh exit 1 để dừng thực thi tập lệnh với giá trị thoát là 1, biểu thị rằng đã có lỗi xảy ra.

  • Nếu kiểm tra thư mục thành công, ở dòng 12, biến $user được gán bằng tham số vị trí $1 như người dùng yêu cầu.

Ví dụ về kết quả thực thi tập lệnh:

$ ./backup.sh 
Files to be included: 24
Directories to be included: 4
Files archived: 24
Directories archived: 4
Backup of /home/linuxconfig completed!
Details about the output backup file:
-rw-r--r-- 1 linuxconfig linuxconfig 235709 Sep 14 11:45 /tmp/linuxconfig_home_2017-09-14_114521.tar.gz

$ ./backup.sh abc123
Requested abc123 user home directory doesn't exist.

$ ./backup.sh damian
Files to be included: 3
Directories to be included: 1
Files archived: 3
Directories archived: 1
Backup of /home/damian completed!
Details about the output backup file:
-rw-r--r-- 1 linuxconfig linuxconfig 2140 Sep 14 11:45 /tmp/damian_home_2017-09-14_114534.tar.gz
Bash loops

Cho đến thời điểm hiện tại, tập lệnh sao lưu của chúng ta hoạt động như mong đợi và tính khả dụng của nó đã được cải thiện đáng kể so với mã ban đầu được giới thiệu ở phần đầu của hướng dẫn lập trình này. Giờ đây, chúng ta có thể dễ dàng sao lưu bất kỳ thư mục người dùng nào chỉ bằng cách trỏ tập lệnh tới thư mục home của người dùng đó bằng cách sử dụng các tham số vị trí trong quá trình thực thi tập lệnh.

Vấn đề chỉ xuất hiện khi chúng ta cần sao lưu nhiều thư mục người dùng mỗi ngày. Nhiệm vụ này sẽ nhanh chóng trở nên nhàm chán và tốn thời gian. Ở giai đoạn này, sẽ thật tuyệt nếu chúng ta có thể sao lưu bất kỳ số lượng thư mục home nào được chọn chỉ với một lần chạy tập lệnh backup.sh.

May mắn thay, Bash đã hỗ trợ chúng ta, vì nhiệm vụ này có thể được thực hiện bằng cách sử dụng các vòng lặp. Vòng lặp (loops) là các cấu trúc lặp được sử dụng để lặp qua một số lượng nhiệm vụ nhất định cho đến khi tất cả các mục trong danh sách chỉ định được hoàn thành hoặc cho đến khi các điều kiện định trước được đáp ứng. Có ba kiểu vòng lặp cơ bản mà chúng ta có thể sử dụng:

Vòng lặp For

Vòng lặp for được sử dụng để lặp qua một khối mã cho bất kỳ số lượng phần tử nào được cung cấp trong danh sách. Hãy bắt đầu với ví dụ về vòng lặp for đơn giản:

linuxconfig.org:~$ for i in 1 2 3; do echo $i; done 
1 
2 
3 
linuxconfig.org:~$

Vòng lặp for trên sử dụng lệnh echo để in ra tất cả các phần tử 1, 2 và 3 trong danh sách. Việc sử dụng dấu chấm phẩy cho phép chúng ta thực thi vòng lặp for trên một dòng lệnh duy nhất. Nếu chuyển đoạn vòng lặp trên vào tập lệnh Bash, mã sẽ trông như sau:

#!/bin/bash

for i in 1 2 3; do
    echo $i
done

Vòng lặp for bao gồm bốn từ khóa dành riêng cho shell: for, in, do, done. Mã trên có thể được hiểu theo cách sau: với MỖI phần tử trong DANH SÁCH (1, 2 và 3), gán tạm thời phần tử đó cho biến i, sau đó THỰC HIỆN lệnh echo $i để in ra phần tử đó dưới dạng STDOUT và tiếp tục cho đến khi tất cả các phần tử trong danh sách được xử lý.

In số là điều thú vị, nhưng hãy thử làm điều gì đó có ý nghĩa hơn. Sử dụng phép thế lệnh (command substitution) như đã được giải thích ở phần trước của hướng dẫn, chúng ta có thể tạo ra bất kỳ danh sách nào để làm thành phần của cấu trúc vòng lặp for. Ví dụ về vòng lặp for hơi tinh vi hơn dưới đây sẽ đếm số ký tự của mỗi dòng trong một tệp bất kỳ:

linuxconfig.org:~$ vi items.txt 
linuxconfig.org:~$ cat items.txt 
bash 
scripting 
tutorial 
linuxconfig.org:~$ for i in $( cat items.txt ); do echo -n $i | wc -c; done 
4 
9 
8 
linuxconfig.org:~$

Vâng, khi đã làm chủ, sức mạnh của GNU Bash là vô hạn! Hãy dành thời gian để thử nghiệm trước khi tiến xa hơn.

Bài tập:
Viết lại vòng lặp for đếm ký tự ở trên để in ra tên của tất cả các tệp và thư mục trong thư mục làm việc hiện tại của bạn cùng với số ký tự mà tên mỗi tệp và thư mục đó có. Kết quả của vòng lặp for nên trông tương tự như:

0_xvz has 5
backup.sh has 9
compare.sh has 10
date.sh has 7
file1.txt has 9
foobar has 6
function.sh has 11
hello-world.sh has 14
if_else.sh has 10
items.txt has 9

Vòng lặp While

Vòng lặp tiếp theo trong danh sách của chúng ta là vòng lặp while. Vòng lặp này hoạt động dựa trên một điều kiện nhất định. Nghĩa là, nó sẽ tiếp tục thực thi mã được bao quanh giữa do và done miễn là điều kiện đó đúng. Ngay khi điều kiện trở nên sai, vòng lặp sẽ dừng lại. Hãy xem ví dụ sau:

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

Vòng lặp while trên sẽ tiếp tục thực hiện khối lệnh bên trong chỉ khi biến counter nhỏ hơn 3. Điều kiện được đặt trên dòng 4. Trong mỗi vòng lặp, trên dòng 5 biến counter được tăng thêm 1. Ngay khi biến counter đạt giá trị 3, điều kiện trên dòng 4 trở nên sai và vòng lặp while kết thúc.

Ví dụ thực thi:

linuxconfig.org:~$ cat while.sh 
#!/bin/bash 
counter=0 
while [ $counter -lt 3 ]; do 
let counter+=1 
echo $counter 
done 
linuxconfig.org:~$ ./while.sh 
1 
2 
3 
linuxconfig.org:~$

Vòng lặp Until

Vòng lặp cuối cùng mà chúng ta sẽ đề cập trong hướng dẫn này là vòng lặp until. Vòng lặp until thực hiện chính xác ngược lại so với vòng lặp while. Vòng lặp until cũng hoạt động dựa trên một điều kiện định trước. Tuy nhiên, khối lệnh giữa do và done sẽ được thực hiện lặp đi lặp lại chỉ cho đến khi điều kiện đó thay đổi từ sai thành đúng. Việc thực thi của vòng lặp until được minh họa qua ví dụ sau:

#!/bin/bash
  
counter=6
until [ $counter -lt 3 ]; do
    let counter-=1
    echo $counter
done

Nếu bạn đã hiểu ví dụ về vòng lặp while, thì vòng lặp until sẽ khá dễ hiểu. Tập lệnh bắt đầu với biến counter được đặt là 6. Điều kiện trên dòng 4 của vòng lặp until là tiếp tục thực thi khối lệnh cho đến khi điều kiện trở thành đúng.

Ví dụ thực thi:

linuxconfig.org:~$ cat until.sh 
#!/bin/bash 
counter=6 until [ $counter -lt 3 ]; do
let counter-=1
echo $counter
done linuxconfig.org:~$ ./until.sh 
5 
4 
3 
2 
linuxconfig.org:~$

Ứng dụng vòng lặp vào tập lệnh backup.sh

Ở giai đoạn này, chúng ta có thể chuyển đổi hiểu biết về vòng lặp thành một tính năng cụ thể. Tập lệnh sao lưu hiện tại của chúng ta chỉ có khả năng sao lưu một thư mục mỗi lần thực thi. Sẽ thật tuyệt nếu có thể sao lưu tất cả các thư mục được cung cấp làm đối số dòng lệnh khi chạy tập lệnh.

Hãy xem xét tập lệnh được cập nhật dưới đây, nó đã triển khai tính năng mới này:

#!/bin/bash
    
# This bash script is used to backup a user's home directory to /tmp/.
    
function backup {
    
    if [ -z $1 ]; then
    	user=$(whoami)
    else 
    	if [ ! -d "/home/$1" ]; then
    		echo "Requested $1 user home directory doesn't exist."
    		exit 1
    	fi
    	user=$1
    fi 
    
    input=/home/$user
    output=/tmp/${user}_home_$(date +%Y-%m-%d_%H%M%S).tar.gz
    
    function total_files {
    	find $1 -type f | wc -l
    }
    
    function total_directories {
    	find $1 -type d | wc -l
    }
    
    function total_archived_directories {
    	tar -tzf $1 | grep  /$ | wc -l
    }
    
    function total_archived_files {
    	tar -tzf $1 | grep -v /$ | wc -l
    }
    
    tar -czf $output $input 2> /dev/null
    
    src_files=$( total_files $input )
    src_directories=$( total_directories $input )
    
    arch_files=$( total_archived_files $output )
    arch_directories=$( total_archived_directories $output )
    
    echo "########## $user ##########"
    echo "Files to be included: $src_files"
    echo "Directories to be included: $src_directories"
    echo "Files archived: $arch_files"
    echo "Directories archived: $arch_directories"
    
    if [ $src_files -eq $arch_files ]; then
    	echo "Backup of $input completed!"
    	echo "Details about the output backup file:"
    	ls -l $output
    else
    	echo "Backup of $input failed!"
    fi
}
    
for directory in $*; do
    backup $directory 
done;

Sau khi xem xét tập lệnh trên, bạn có thể nhận thấy rằng một hàm mới có tên backup đã được tạo ra từ dòng 5 đến dòng 57. Hàm này bao gồm toàn bộ mã mà chúng ta đã viết trước đó. Phần định nghĩa hàm kết thúc tại dòng 57, sau đó chúng ta triển khai một vòng lặp for từ dòng 59 đến dòng 61 để thực thi hàm backup cho mỗi thư mục người dùng được cung cấp làm đối số. Nếu bạn nhớ, biến $* chứa tất cả các đối số được cung cấp khi tập lệnh được thực thi. Hơn nữa, một thay đổi về mặt thẩm mỹ ở dòng 44 đảm bảo rằng đầu ra của tập lệnh dễ đọc hơn bằng cách tách từng khối thông tin sao lưu của mỗi thư mục bằng một dòng dấu thăng (hash).

Kết quả thực thi sẽ trông như sau:

$ ./backup.sh linuxconfig damian
########## linuxconfig ##########
Files to be included: 27
Directories to be included: 4
Files archived: 27
Directories archived: 4
Backup of /home/linuxconfig completed!
Details about the output backup file:
-rw-r--r-- 1 linuxconfig linuxconfig 236173 Oct 23 10:22 /tmp/linuxconfig_home_2017-10-23_102229.tar.gz
########## damian ##########
Files to be included: 3
Directories to be included: 1
Files archived: 3
Directories archived: 1
Backup of /home/damian completed!
Details about the output backup file:
-rw-r--r-- 1 linuxconfig linuxconfig 2140 Oct 23 10:22 /tmp/damian_home_2017-10-23_102230.tar.gz
Bash arithmetics (Số học trong bash)

Trong phần cuối của hướng dẫn lập trình Bash này, chúng ta sẽ thảo luận về một số kiến thức cơ bản của các phép toán số học trong Bash. Các phép toán số học trong Bash sẽ tăng thêm mức độ tinh vi và tính linh hoạt cho các tập lệnh của chúng ta vì chúng cho phép tính toán các số ngay cả với độ chính xác số học. Có nhiều cách khác nhau để thực hiện các phép toán số học trong Bash. Hãy cùng xem qua một vài ví dụ đơn giản.

Mở Rộng Số Học (Arithmetic Expansion)

Mở rộng số học có lẽ là phương pháp đơn giản nhất để thực hiện các phép tính cơ bản. Chỉ cần đặt bất kỳ biểu thức toán học nào bên trong cặp dấu ngoặc kép đôi (( ... )). Hãy cùng thực hiện một số phép tính cộng, trừ, nhân và chia đơn giản với số nguyên:

linuxconfig.org:~$ a=$(( 12 + 5 )) 
linuxconfig.org:~$ echo $a 
17 
linuxconfig.org:~$ echo $(( 12 + 5 )) 
17 
linuxconfig.org:~$ echo $(( 100 - 1 )) 
99 
linuxconfig.org:~$ echo $(( 3 * 11 )) 
33 
linuxconfig.org:~$ division=$(( 100 / 10 )) 
linuxconfig.org:~$ echo $division 
10 
linuxconfig.org:~$ x=10; y=33 
linuxconfig.org:~$ z=$(( $x * $y )) 
linuxconfig.org:~$ echo $z 
330 
linuxconfig.org:~$

Bài tập:
Bạn có thể sử dụng mở rộng số học để thực hiện phép toán lấy phần dư (modulus) không? Ví dụ, kết quả của phép toán 99 % 10 là bao nhiêu?

Lệnh expr

Một lựa chọn khác thay cho mở rộng số học là sử dụng lệnh expr. Sử dụng expr cho phép chúng ta thực hiện một phép toán số học mà không cần đặt biểu thức toán học của mình bên trong dấu ngoặc hoặc dấu ngoặc kép. Tuy nhiên, đừng quên escape (tránh) dấu nhân * để tránh lỗi cú pháp từ expr:

linuxconfig.org:~$ expr 2 + 2 
4 
linuxconfig.org:~$ 
expr 6 * 6 
expr: syntax error 
linuxconfig.org:~$ expr 6 \* 6 
36 
linuxconfig.org:~$ expr 6 / 3 
2 
linuxconfig.org:~$ expr 1000 - 999 
1 
linuxconfig.org:~$

Lệnh let

Tương tự như lệnh expr, chúng ta cũng có thể thực hiện các phép toán số học trong Bash bằng lệnh let. Lệnh let đánh giá một biểu thức toán học và lưu kết quả vào một biến. Chúng ta đã gặp lệnh let trong một ví dụ trước đó khi sử dụng nó để tăng giá trị số nguyên. Ví dụ dưới đây cho thấy một số phép toán cơ bản sử dụng lệnh let cũng như tăng giảm số nguyên và các phép toán mũ như x³:

linuxconfig.org:~$ let a=2+2
linuxconfig.org:~$ echo $a
4
linuxconfig.org:~$ let b=4*($a-1)
linuxconfig.org:~$ echo $b
12
linuxconfig.org:~$ let c=($b**3)/2
linuxconfig.org:~$ echo $c
864
linuxconfig.org:~$ let c++
linuxconfig.org:~$ echo $c
865
linuxconfig.org:~$ let c--
linuxconfig.org:~$ echo $c
864
linuxconfig.org:~$

Lệnh bc

Sau vài phút thử nghiệm với các phương pháp số học trên Bash, có lẽ bạn đã nhận thấy rằng chúng hoạt động tốt với các số nguyên, nhưng khi xử lý số thập phân thì lại có phần hạn chế. Để nâng tầm phép toán số học trong Bash lên một cấp độ hoàn toàn mới, chúng ta cần sử dụng lệnh bc. Lệnh bc với cú pháp đúng cho phép thực hiện các phép tính phức tạp hơn chỉ với số nguyên.

Tài liệu hướng dẫn sử dụng của lệnh bc khá rộng, kéo dài hơn 500 dòng. Tuy nhiên, không có gì phải ngại khi chỉ trình bày một số phép toán cơ bản. Ví dụ dưới đây sẽ thực hiện phép chia với 2 và 30 chữ số thập phân, và tính căn bậc hai của 50 với 50 chữ số thập phân. Theo mặc định, lệnh bc sẽ cho kết quả dưới dạng số nguyên. Sử dụng scale=x để chỉ thị cho bc hiển thị kết quả dưới dạng số thực:

linuxconfig.org:~$ echo '8.5 / 2.3' | bc
3
linuxconfig.org:~$ echo 'scale=2;8.5 / 2.3' | bc
3.69
linuxconfig.org:~$ echo 'scale=30;8.5 / 2.3' | bc
3.695652173913043478260869565217
linuxconfig.org:~$ squareroot=$( echo 'scale=50;sqrt(50)' | bc )
linuxconfig.org:~$ echo $squareroot
7.07106781186547524400844362104849039284835937688474
linuxconfig.org:~$

Áp dụng kiến thức số học vào tập lệnh backup.sh

Hãy cùng vận dụng kiến thức mới về số học trong Bash để một lần nữa cải tiến tập lệnh backup.sh, nhằm thực hiện việc đếm tổng số tệp và thư mục đã được lưu trữ cho tất cả người dùng.

#!/bin/bash
    
# This bash script is used to backup a user's home directory to /tmp/.
function backup {
    
    if [ -z $1 ]; then
        user=$(whoami)
    else 
        if [ ! -d "/home/$1" ]; then
                echo "Requested $1 user home directory doesn't exist."
                exit 1
        fi
        user=$1
    fi 
    
    input=/home/$user
    output=/tmp/${user}_home_$(date +%Y-%m-%d_%H%M%S).tar.gz
    
    function total_files {
        find $1 -type f | wc -l
    }
    
    function total_directories {
        find $1 -type d | wc -l
    }
    
    function total_archived_directories {
        tar -tzf $1 | grep  /$ | wc -l
    }
    
    function total_archived_files {
        tar -tzf $1 | grep -v /$ | wc -l
    }
    
    tar -czf $output $input 2> /dev/null
    
    src_files=$( total_files $input )
    src_directories=$( total_directories $input )
    
    arch_files=$( total_archived_files $output )
    arch_directories=$( total_archived_directories $output )
    
    echo "########## $user ##########"
    echo "Files to be included: $src_files"
    echo "Directories to be included: $src_directories"
    echo "Files archived: $arch_files"
    echo "Directories archived: $arch_directories"

    if [ $src_files -eq $arch_files ]; then
        echo "Backup of $input completed!"
        echo "Details about the output backup file:"
        ls -l $output
    else
        echo "Backup of $input failed!"
    fi
}
    
for directory in $*; do
    backup $directory 
    let all=$all+$arch_files+$arch_directories
done;
    echo "TOTAL FILES AND DIRECTORIES: $all"

Giải thích chi tiết:

  • Trên dòng 60, chúng ta sử dụng phép cộng với lệnh let để cộng dồn tất cả các tệp đã lưu trữ (bao gồm cả tệp và thư mục) vào biến all.

  • Mỗi vòng lặp for, khi sao lưu cho từng người dùng, sẽ cộng thêm số lượng đã được lưu trữ của người đó vào biến all.

  • Cuối cùng, kết quả tổng số tệp và thư mục được in ra bằng lệnh echo trên dòng 62.

Ví dụ thực thi tập lệnh:

$ ./backup.sh linuxconfig damian
########## linuxconfig ##########
Files to be included: 27
Directories to be included: 6
Files archived: 27
Directories archived: 6
Backup of /home/linuxconfig completed!
Details about the output backup file:
-rw-r--r-- 1 linuxconfig linuxconfig 237004 Dec 27 11:23 /tmp/linuxconfig_home_2017-12-27_112359.tar.gz
########## damian ##########
Files to be included: 3
Directories to be included: 1
Files archived: 3
Directories archived: 1
Backup of /home/damian completed!
Details about the output backup file:
-rw-r--r-- 1 linuxconfig linuxconfig 2139 Dec 27 11:23 /tmp/damian_home_2017-12-27_112359.tar.gz
TOTAL FILES AND DIRECTORIES: 37
Kết luận

Mặc dù hướng dẫn này đã cung cấp một cái nhìn tổng quan về bash scripting, vẫn còn rất nhiều kiến thức bổ ích chưa được đề cập. Trước khi tiếp tục khám phá các chủ đề nâng cao hơn, hãy đảm bảo rằng bạn đã nắm vững các khái niệm cơ bản đã được trình bày ở đây. Ngoài việc tìm kiếm thông tin trên Google, bạn cũng có thể tham khảo nhiều nguồn tài liệu trực tuyến khác như sách, diễn đàn, hoặc các khóa học trực tuyến để hỗ trợ bạn khi gặp khó khăn. Luyện tập và nghiên cứu thêm sẽ giúp bạn làm chủ bash scripting một cách hiệu quả.

Để thực hành Bash Scripting một cách hiệu quả mà không tốn kém, bạn có thể mua VPS giá rẻ từ các nhà cung cấp uy tín. Những gói VPS này cung cấp môi trường Linux đầy đủ tính năng, giúp bạn thử nghiệm và triển khai các script mà không cần đầu tư vào phần cứng đắt tiền.

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *