3.3 Bài 2
Chứng chỉ: |
Linux Essentials |
---|---|
Phiên bản: |
1.6 |
Chủ đề: |
3 Sức mạnh của Dòng lệnh |
Mục tiêu: |
3.3 Biến các Lệnh thành một Têp lệnh |
Bài: |
2 trên 2 |
Giới thiệu
Trong phần trước, chúng ta đã sử dụng ví dụ đơn giản này để minh họa tệp lệnh Bash:
#!/bin/bash # A simple script to greet a single user. if [ $# -eq 1 ] then username=$1 echo "Hello $username!" else echo "Please enter only one argument." fi echo "Number of arguments: $#."
-
Tất cả các tệp lệnh phải bắt đầu bằng shebang để xác định đường dẫn đến trình thông dịch.
-
Tất cả các tệp lệnh nên bao gồm các chú thích để mô tả việc sử dụng chúng.
-
Têp lệnh cụ thể này làm việc với đối số; đối số sẽ được chuyển đến tệp lệnh khi nó được gọi.
-
Tệp lệnh này chứa câu lệnh if; câu lệnh này kiểm tra các điều kiện của biến tích hợp
$#
. Biến này sẽ được đặt thành số lượng của đối số. -
Nếu số lượng của đối số được truyền cho tệp lệnh bằng 1 thì giá trị của đối số đầu tiên sẽ được truyền cho một biến mới có tên
username
và tệp lệnh sẽ lặp lại lời chào tới người dùng. Nếu ngược lại, một thông báo lỗi sẽ được hiển thị. -
Cuối cùng, tệp lệnh sẽ lặp lại số lượng đối số. Điều này rất hữu ích trong việc gỡ lỗi.
Đây là một ví dụ hữu ích để bắt đầu giải thích một số tính năng khác của tệp lệnh Bash.
Mã Thoát
Bạn sẽ nhận thấy rằng tệp lệnh của chúng ta có hai trạng thái có thể xảy ra: một là nó sẽ in ra "Hello <user>!"
, hai là sẽ in ra một thông báo lỗi. Điều này khá là bình thường đối với nhiều tiện ích cốt lõi của chúng ta. Hãy thử cat
- lệnh mà chắc chắn bạn đang dần trở nên quen thuộc.
Hãy so sánh việc sử dụng thành công cat
với tình huống sử dụng thất bại. Hãy lưu ý rằng ví dụ của chúng ta ở trên là một tệp lệnh có tên new_script.sh
.
$ cat -n new_script.sh 1 #!/bin/bash 2 3 # A simple script to greet a single user. 4 5 if [ $# -eq 1 ] 6 then 7 username=$1 8 9 echo "Hello $username!" 10 else 11 echo "Please enter only one argument." 12 fi 13 echo "Number of arguments: $#."
Lệnh này thành công và bạn sẽ nhận thấy rằng cờ -n
cũng đã in số dòng. Chúng rất hữu ích trong việc gỡ lỗi tệp lệnh, nhưng hãy lưu ý rằng chúng không phải là một phần của tệp lệnh.
Bây giờ, chúng ta sẽ kiểm tra giá trị của một biến tích hợp sẵn mới là $?
. Hiện tại, ta hãy chỉ chú ý đến đầu ra:
$ echo $? 0
Bây giờ, hãy xem xét tình huống mà trong đó cat
bị lỗi. Đầu tiên, chúng ta sẽ thấy một thông báo lỗi; sau đó, ta sẽ kiểm tra giá trị của $?
.
$ cat -n dummyfile.sh cat: dummyfile.sh: No such file or directory $ echo $? 1
Lời giải thích cho hành vi này là bởi bất kỳ tác vụ thực thi nào của tiện ích cat
cũng sẽ trả về một mã thoát. Mã thoát sẽ cho chúng ta biết lệnh đã thành công hay đã gặp lỗi. Mã thoát 0 cho ta biết lệnh đã hoàn tất thành công. Điều này đúng với hầu hết mọi lệnh Linux mà bạn sẽ làm việc cùng. Bất kỳ mã thoát nào khác đều sẽ chỉ ra một loại lỗi nào đó. Mã thoát của lệnh chạy cuối cùng sẽ được lưu trữ trong biến $?
.
Người dùng thường sẽ không thấy được mã thoát, nhưng chúng lại rất hữu ích khi viết tệp lệnh. Giả sử một tệp lệnh mà trong đó chúng ta có thể sao chép tệp vào một ổ đĩa mạng từ xa. Có nhiều nguyên nhân khiến tác vụ sao chép có thể không thành công, ví dụ như khi máy cục bộ của chúng ta không được kết nối với mạng hoặc ổ đĩa từ xa có thể đã đầy. Bằng cách kiểm tra mã thoát của tiện ích sao chép, chúng ta có thể cảnh báo người dùng về các sự cố khi chạy tệp lệnh.
Triển khai mã thoát là một bài tập thực hành rất hữu ích. Vì vậy, chúng ta sẽ thực hiện nó ngay bây giờ. Ta có hai hướng trong tệp lệnh, thành công và thất bại. Hãy sử dụng số 0 (không) để biểu thị thành công và 1 (một) để biểu thị thất bại.
1 #!/bin/bash 2 3 # A simple script to greet a single user. 4 5 if [ $# -eq 1 ] 6 then 7 username=$1 8 9 echo "Hello $username!" 10 exit 0 11 else 12 echo "Please enter only one argument." 13 exit 1 14 fi 15 echo "Number of arguments: $#."
$ ./new_script.sh Carol Hello Carol! $ echo $? 0
Hãy lưu ý rằng lệnh echo
trên dòng 15 đã bị bỏ qua hoàn toàn. Việc sử dụng exit
sẽ kết thúc tệp lệnh ngay lập tức và giúp ta không gặp lại dòng đó nữa.
Xử lý nhiều Đối số
Cho đến nay, tệp lệnh của chúng ta chỉ có thể xử lý từng tên người dùng một. Bất kỳ số lượng đối số nào ngoài một sẽ gây ra lỗi. Hãy cùng khám phá những cách chúng ta có thể dùng để làm cho tệp lệnh này linh hoạt hơn.
Phản ứng bản năng đầu tiên của người dùng có thể sẽ là sử dụng nhiều biến vị trí hơn như $2
, $3
, v.v. Thật không may là chúng ta không thể đoán trước số lượng đối số mà người dùng có thể chọn để sử dụng. Một cách hữu ích trong việc giải quyết vấn đề này là tìm hiểu về nhiều biến tích hợp hơn.
Chúng ta sẽ thay đổi logic của tệp lệnh: không có đối số nào sẽ gây ra lỗi, nhưng với bất kỳ số lượng đối số nào khác thì lệnh sẽ thành công. Tệp lệnh mới này sẽ được gọi là friendly2.sh
.
1 #!/bin/bash 2 3 # a friendly script to greet users 4 5 if [ $# -eq 0 ] 6 then 7 echo "Please enter at least one user to greet." 8 exit 1 9 else 10 echo "Hello $@!" 11 exit 0 12 fi
$ ./friendly2.sh Carol Dave Henry Hello Carol Dave Henry!
Có hai biến tích hợp có chứa tất cả các đối số được chuyển đến tệp lệnh: $@
và $*
. Cả hai tệp lệnh này đều sẽ hoạt động như nhau trong hầu hết các trường hợp. Bash sẽ phân tích cú pháp của các đối số và tách từng đối số khi nó gặp một khoảng cách giữa chúng. Trên thực tế, nội dung của $@
sẽ trông như thế này:
|
|
|
|
|
|
Nếu bạn đã quen thuộc với các ngôn ngữ lập trình khác, bạn có thể nhận ra đây là biến mảng (array). Mảng trong Bash có thể được tạo đơn giản bằng cách đặt khoảng trắng giữa các phần tử như biến FILES
trong tệp lệnh arraytest
bên dưới:
FILES="/usr/sbin/accept /usr/sbin/pwck/ usr/sbin/chroot"
Nó chứa một danh sách với nhiều mục. Cho đến nay, điều này không được coi là hữu ích lắm vì chúng ta vẫn chưa có cách nào để xử lý các mục này một cách riêng lẻ.
Vòng lặp For
Hãy tham khảo ví dụ arraytest
ở trên. Nếu bạn còn nhớ, trong ví dụ này, chúng ta đang chỉ định một mảng của riêng chúng ta có tên là FILES
. Điều ta cần là một cách để “giải nén” biến này và lần lượt truy cập từng giá trị riêng lẻ. Để làm được điều đó, chúng ta phải sử dụng một cấu trúc gọi là vòng lặp for (for loop). Cấu trúc này có trong tất cả các ngôn ngữ lập trình. Có hai biến mà chúng ta sẽ đề cập đến: một là phạm vi, hai giá trị riêng lẻ mà chúng ta đang thực hiện. Sau đây là toàn bộ tệp lệnh:
#!/bin/bash FILES="/usr/sbin/accept /usr/sbin/pwck/ usr/sbin/chroot" for file in $FILES do ls -lh $file done
$ ./arraytest lrwxrwxrwx 1 root root 10 Apr 24 11:02 /usr/sbin/accept -> cupsaccept -rwxr-xr-x 1 root root 54K Mar 22 14:32 /usr/sbin/pwck -rwxr-xr-x 1 root root 43K Jan 14 07:17 /usr/sbin/chroot
Nếu bạn tham khảo lại ví dụ friendly2.sh
ở trên, bạn có thể thấy rằng chúng ta đang làm việc với một loạt các giá trị chứa trong một biến duy nhất là $@
. Để rõ ràng hơn, ta sẽ gọi biến thứ hai là username
. Tệp lệnh của chúng ta bây giờ sẽ trông như thế này:
1 #!/bin/bash 2 3 # a friendly script to greet users 4 5 if [ $# -eq 0 ] 6 then 7 echo "Please enter at least one user to greet." 8 exit 1 9 else 10 for username in $@ 11 do 12 echo "Hello $username!" 13 done 14 exit 0 15 fi
Hãy nhớ rằng biến mà bạn xác định ở đây có thể được đặt tên theo bất kỳ tên nào mà bạn muốn, và rằng tất cả các dòng bên trong do… done
sẽ được thực thi một lần cho mỗi phần tử của mảng. Hãy quan sát đầu ra từ tệp lệnh của chúng ta:
$ ./friendly2.sh Carol Dave Henry Hello Carol! Hello Dave! Hello Henry!
Bây giờ, hãy giả sử rằng chúng ta muốn làm cho đầu ra của mình trông có vẻ giống lời chào của con người hơn một chút, cụ thể là lời chào chỉ nằm trên một dòng.
1 #!/bin/bash 2 3 # a friendly script to greet users 4 5 if [ $# -eq 0 ] 6 then 7 echo "Please enter at least one user to greet." 8 exit 1 9 else 10 echo -n "Hello $1" 11 shift 12 for username in $@ 13 do 14 echo -n ", and $username" 15 done 16 echo "!" 17 exit 0 18 fi
Một vài lưu ý:
-
Sử dụng
-n
vớiecho
sẽ loại bỏ ký tự xuống dòng sau khi in. Điều này có nghĩa là tất cả các echo sẽ in trên cùng một dòng, và dòng mới sẽ chỉ được in sau dấu!`
trên dòng 16. -
Lệnh
shift
sẽ xóa phần tử đầu tiên của mảng, để:
|
|
|
|
|
|
trở thành:
|
|
|
|
Hãy quan sát đầu ra:
$ ./friendly2.sh Carol Hello Carol! $ ./friendly2.sh Carol Dave Henry Hello Carol, and Dave, and Henry!
Sử dụng Biểu thức Chính quy để Thực hiện Kiểm tra Lỗi
Có thể chúng ta muốn xác minh tất cả các đối số mà người dùng đang nhập. Ví dụ: có lẽ ta muốn đảm bảo rằng tất cả các tên được chuyển đến friendly2.sh
sẽ chỉ chứa các chữ cái, và mọi ký tự hoặc số đặc biệt đều sẽ gây ra lỗi. Để thực hiện kiểm tra lỗi này, ta sẽ sử dụng grep
.
Hãy nhớ lại rằng chúng ta có thể sử dụng biểu thức chính quy với grep
.
$ echo Animal | grep "^[A-Za-z]*$" Animal $ echo $? 0
$ echo 4n1ml | grep "^[A-Za-z]*$" $ echo $? 1
^
và $
lần lượt chỉ ra phần đầu và phần cuối của dòng. [A-Za-z]
cho biết phạm vi chữ cái, chữ hoa hoặc chữ thường. *
là định lượng và nó sẽ sửa đổi phạm vi chữ cái của chúng ta sao cho số 0 khớp với nhiều chữ cái. Tóm lại, grep
của chúng ta sẽ thành công nếu đầu vào chỉ có chữ cái và sẽ không thành công trong trường hợp ngược lại.
Điều tiếp theo cần lưu ý là grep
đang trả lại mã thoát dựa trên việc có khớp hay không. Kết quả khớp sẽ trả về 0
và kết quả không khớp sẽ trả về 1
. Chúng ta có thể sử dụng điều này để kiểm tra các đối số bên trong tệp lệnh của mình.
1 #!/bin/bash 2 3 # a friendly script to greet users 4 5 if [ $# -eq 0 ] 6 then 7 echo "Please enter at least one user to greet." 8 exit 1 9 else 10 for username in $@ 11 do 12 echo $username | grep "^[A-Za-z]*$" > /dev/null 13 if [ $? -eq 1 ] 14 then 15 echo "ERROR: Names must only contains letters." 16 exit 2 17 else 18 echo "Hello $username!" 19 fi 20 done 21 exit 0 22 fi
Ở dòng 12, chúng ta đang chuyển hướng đầu ra tiêu chuẩn sang /dev/null
, đây là một cách đơn giản để chặn nó. Chúng ta không muốn thấy bất kỳ đầu ra nào từ lệnh grep
mà chỉ muốn kiểm tra mã thoát của nó xảy ra trên dòng 13. Cũng lưu ý rằng chúng ta đang sử dụng mã thoát 2
để biểu thị một đối số không hợp lệ. Thông thường, ta nên sử dụng các mã thoát khác nhau để chỉ ra các lỗi khác nhau; theo cách này, người dùng có kinh nghiệm sẽ có thể sử dụng các mã thoát này để khắc phục sự cố.
$ ./friendly2.sh Carol Dave Henry Hello Carol! Hello Dave! Hello Henry! $ ./friendly2.sh 42 Carol Dave Henry ERROR: Names must only contains letters. $ echo $? 2
Bài tập Hướng dẫn
-
Hãy đọc nội dung của
script1.sh
bên dưới:#!/bin/bash if [ $# -lt 1 ] then echo "This script requires at least 1 argument." exit 1 fi echo $1 | grep "^[A-Z]*$" > /dev/null if [ $? -ne 0 ] then echo "no cake for you!" exit 2 fi echo "here's your cake!" exit 0
Đầu ra của các lệnh này là gì?
-
./script1.sh
-
echo $?
-
./script1.sh cake
-
echo $?
-
./script1.sh CAKE
-
echo $?
-
-
Hãy đọc nội dung của tệp
script2.sh
:for filename in $1/*.txt do cp $filename $filename.bak done
Hãy mô tả mục đích của tệp lệnh này theo ý hiểu của bạn.
Bài tập Mở rộng
-
Hãy tạo một tệp lệnh sẽ lấy bất kỳ số lượng đối số nào từ người dùng và chỉ in những đối số đó là các số lớn hơn 10.
Tóm tắt
Trong bài học này, bạn đã học về:
-
Mã thoát là gì, ý nghĩa và cách triển khai chúng.
-
Cách kiểm tra mã thoát của lệnh.
-
Vòng lặp
for
là gì và cách sử dụng chúng với mảng. -
Cách sử dụng
grep
, biểu thức chính quy và mã thoát để kiểm tra đầu vào của người dùng trong tệp lệnh.
Các lệnh được dùng trong bài tập:
shift
-
Loại bỏ phần tử đầu tiên của một mảng.
Các biến đặc biệt:
$?
-
Chứa mã thoát của lệnh cuối cùng được thực thi.
$@
,$*
-
Chứa tất cả các đối số được chuyển đến tệp lệnh dưới dạng một mảng.
Đáp án Bài tập Hướng dẫn
-
Hãy đọc nội dung của
script1.sh
bên dưới:#!/bin/bash if [ $# -lt 1 ] then echo "This script requires at least 1 argument." exit 1 fi echo $1 | grep "^[A-Z]*$" > /dev/null if [ $? -ne 0 ] then echo "no cake for you!" exit 2 fi echo "here's your cake!" exit 0
Đầu ra của các lệnh này là gì?
-
Lệnh:
./script1.sh
Đầu ra:
This script requires at least 1 argument.
-
Lệnh:
echo $?
Đầu ra:
1
-
Lệnh:
./script1.sh cake
Đầu ra:
no cake for you!
-
Lệnh:
echo $?
Đầu ra:
2
-
Lệnh:
./script1.sh CAKE
Đầu ra:
here’s your cake!
-
Lệnh:
echo $?
Đầu ra:
0
-
-
Hãy đọc nội dung của tệp
script2.sh
:for filename in $1/*.txt do cp $filename $filename.bak done
Hãy mô tả mục đích của tệp lệnh này theo ý hiểu của bạn.
Tệp lệnh này sẽ tạo các bản sao dự phòng của tất cả các tệp kết thúc bằng
.txt
trong thư mục con được xác định trong đối số đầu tiên.
Đáp án Bài tập Mở rộng
-
Hãy tạo một tệp lệnh sẽ lấy bất kỳ số lượng đối số nào từ người dùng và chỉ in những đối số đó là các số lớn hơn 10.
#!/bin/bash for i in $@ do echo $i | grep "^[0-9]*$" > /dev/null if [ $? -eq 0 ] then if [ $i -gt 10 ] then echo -n "$i " fi fi done echo ""