Mới bắt đầu với Bash Scripting? Đây là cách thực hiện đúng

Tác giả Starlink, T.Mười 16, 2025, 09:00:06 CHIỀU

« Chủ đề trước - Chủ đề tiếp »

0 Thành viên và 1 Khách đang xem chủ đề.

Thực hành tốt nhất tạo nên sự hoàn hảo.

Các tập lệnh Bash rất mạnh mẽ, nhưng sức mạnh đi kèm với trách nhiệm lớn. Mã lệnh cẩu thả hoặc lập trình kém rất dễ gây ra thiệt hại thực sự, vì vậy, bạn nên cẩn thận và thực hành lập trình phòng thủ.

May mắn thay, Bash có một số cơ chế tích hợp sẵn giúp bảo vệ bạn. Nhiều cơ chế trong số này liên quan đến việc cập nhật cú pháp, thay thế các phương thức cũ và có vấn đề. Bạn có thể sử dụng những đề xuất này để giảm nguy cơ xảy ra lỗi, gỡ lỗi chương trình và xử lý các trường hợp ngoại lệ.


1. Sử dụng một câu Shebang hay

Dòng đầu tiên của tập lệnh shell của bạn phải luôn là một chú thích đặc biệt, được gọi là shebang hoặc hashbang, khai báo trình thông dịch nào sẽ chạy tập lệnh. Nó có thể là tên của một shell, một ngôn ngữ lập trình, hoặc - về lý thuyết - bất kỳ lệnh nào khác. Bạn có thể không cần shebang, nhưng để làm cho tập lệnh của bạn độc lập và cho phép nó quảng bá ngôn ngữ mà nó được viết, thì shebang là điều cần thiết.

Có hai trường phái chính về cách bạn nên xây dựng bài thuyết trình của mình. Trường phái đầu tiên mang tính truyền thống hơn và trông như thế này:

Mã nguồn [Chọn]
#!/bin/bash
echo "Hello, world"

Dòng lệnh này yêu cầu bất kỳ shell nào chạy tập lệnh phải chuyển giao cho một chương trình nằm trong thư mục /bin/bash. Mặc dù cách tiếp cận này khá ổn và gần như luôn hoạt động, một số người lại thích cách sau:

Mã nguồn [Chọn]
#!/usr/bin/env bash
echo "Hello, world"

Chỉ với một đối số, lệnh env sẽ chạy nó, nghĩa là lệnh này sẽ khiến env chạy bash và truyền tập lệnh cho nó. Sự khác biệt lớn ở đây là env sử dụng tên lệnh thay vì đường dẫn đầy đủ đến tệp thực thi. Sự khác biệt này cũng tương tự như khi bạn chạy một chương trình trên dòng lệnh:

Mã nguồn [Chọn]
ls -l *.md
/bin/ls -l *.md

Một tên lệnh đơn thuần, không có đường dẫn, sẽ chạy bất kỳ phiên bản nào của lệnh đó có ý nghĩa nhất trong ngữ cảnh. Nó có thể là một hàm shell, một bí danh, hoặc một tệp chương trình nằm ở đâu đó trong PATH của bạn. Quan trọng là, nếu bạn có các phiên bản trong, chẳng hạn như /bin, /usr/local/bin và ~/.local/bin, env thường sẽ chạy phiên bản "cục bộ nhất", thường là những gì bạn muốn.

Ưu điểm của phương pháp env là không quan trọng chương trình bash của bạn nằm trong /bin, /local/bin, ~/bin hay bất kỳ nơi nào khác, miễn là nó nằm trong PATH. Đây là lựa chọn linh hoạt hơn: nó sẽ hoạt động trên nhiều hệ thống đa dạng hơn, có thể không được thiết lập giống hệt hệ thống của bạn.

Trong khi đó, phiên bản /bin/bash sẽ đảm bảo chương trình ở một vị trí cụ thể sẽ chạy, ngay cả khi một chương trình khác được cài đặt ở nơi khác. Đây có thể là một lựa chọn an toàn hơn, vì một phiên bản bash khác không thể chiếm quyền điều khiển tập lệnh.

Không có cách tiếp cận nào đúng hơn cách kia; chúng chỉ đơn giản là khác nhau. Điều quan trọng là hiểu được sự khác biệt và chọn cách tiếp cận phù hợp với tình huống của bạn. Nếu bạn chỉ viết kịch bản cho mục đích sử dụng của riêng mình, thì cả hai cách đều không quá quan trọng.

2. Luôn trích dẫn các biến của bạn

Ít có điều gì gây ra nhiều vấn đề hơn trên Linux so với cách tiếp cận khoảng trắng, vốn tách biệt các lệnh với các đối số của chúng, và mỗi đối số với các đối số khác. Nếu không cẩn thận, khoảng trắng rất dễ gây ra vấn đề, đặc biệt là khi bạn bắt đầu làm việc với các biến.

Hãy lấy ví dụ này:

Mã nguồn [Chọn]
#!/bin/bash

FILENAME="docs/Letter to bank.doc"
ls $FILENAME

Khi Bash mở rộng một biến, nó thực hiện theo đúng nghĩa đen; dòng cuối cùng sẽ tương đương với:

Mã nguồn [Chọn]
ls docs/Letter to bank.doc
Vì khoảng trắng phân tách các đối số, Bash sẽ hiểu đây là lệnh gọi ls với ba đối số: "docs/Letter", "to" và "bank.doc:"

Để tránh vấn đề này, hãy đảm bảo bạn luôn trích dẫn các biến khi sử dụng chúng, như thế này:

Mã nguồn [Chọn]
ls "$FILENAME"
Bạn có thể đã phát hiện ra các tập lệnh cũng bao gồm tên biến trong dấu ngoặc nhọn, như thế này:

Mã nguồn [Chọn]
ls "${FILENAME}"
Đó cũng là một ý tưởng hay, mặc dù nó không cần thiết trong ví dụ cụ thể này. Đặt tên biến trong dấu ngoặc nhọn giúp dễ theo dõi hơn với các văn bản khác, chẳng hạn như:

Mã nguồn [Chọn]
echo "_${FILENAME}_ is one of my favourite files"
Nếu không có dấu ngoặc nhọn, Bash sẽ cố gắng tìm biến có tên là FILENAME_ và sẽ thất bại.

3. Dừng tập lệnh của bạn khi có lỗi

Ít có điều gì nguy hiểm bằng lỗi không được kiểm tra. Trong một tập lệnh shell, bạn có thể gọi nhiều lệnh khác nhau, hy vọng chúng sẽ thành công. Bạn nên kiểm tra điều đó thật kỹ lưỡng, nhưng đây là một mạng lưới an toàn hữu ích sẽ giúp bảo vệ bạn:

Mã nguồn [Chọn]
set -e
Sổ tay hướng dẫn Bash mô tả chức năng của cài đặt này như sau: Thoát ngay lập tức nếu đường ống, có thể bao gồm một lệnh đơn giản, một danh sách hoặc một lệnh hợp thành, trả về trạng thái khác không.

Nói một cách đơn giản, tập lệnh của bạn sẽ dừng lại nếu có sự cố xảy ra mà bạn chưa xử lý. Hãy xem ví dụ này:

Mã nguồn [Chọn]
#!/bin/bash

touch /file
echo "Now do something with that file..."

Đoạn mã ở đây giả định rằng thao tác chạm sẽ thành công, nhưng giả định đó rất nguy hiểm:

Thêm lệnh gọi tới set -e sẽ khiến tập lệnh dừng ngay khi lệnh touch không thành công:

Lệnh set có thể thay đổi nhiều tùy chọn khác nhau để điều khiển cách shell hoạt động. Xem thêm, ví dụ, thiết lập pipefail:

Mã nguồn [Chọn]
set -o pipefail
Điều này đảm bảo rằng một đường ống sẽ thoát với trạng thái khác không, để biểu thị lỗi, nếu bất kỳ thành phần nào của nó gặp sự cố. Theo mặc định, lỗi xảy ra sớm trong đường ống có thể dễ dàng bị bỏ qua.

3. Trả ơn: Dừng lại khi thất bại

Thất bại do lỗi là một cách xử lý tổng quát quan trọng, nhưng bạn cũng nên tìm cách xử lý các lỗi cụ thể và thực hiện hành động thích hợp. Một cách đơn giản để kiểm tra lỗi là kiểm tra trạng thái thoát của lệnh.

Bạn có thể kiểm tra trạng thái thoát của lệnh bằng cách kiểm tra biến $? sau khi bạn chạy lệnh đó:

Mã nguồn [Chọn]
cd "$DIR"

if [ $? -ne 0 ]; then
exit
fi

Để viết tắt, bạn cũng có thể sử dụng các toán tử logic của Bash:

Mã nguồn [Chọn]
cd "$DIR" || (echo "bad"; exit)
4. Gỡ lỗi từng lệnh

Một tùy chọn shell có giá trị cao khác là xtrace:

Mã nguồn [Chọn]
set -o xtrace
Tùy chọn này khiến shell in ra các lệnh trước khi thực thi chúng, điều này rất hữu ích khi gỡ lỗi:


Shell hiện sẽ in ra từng lệnh khi chạy, bao gồm cả các đối số của lệnh đó.

Có nhiều tùy chọn khác có thể giúp bạn kiểm soát hành vi của shell bằng cách sử dụng set. Tôi thực sự khuyên bạn nên tìm hiểu về set tích hợp trong hướng dẫn sử dụng Bash.

5. Sử dụng tham số dài khi gọi các lệnh khác

Các lệnh Linux có thể gây nhầm lẫn vì chúng có xu hướng sử dụng các tùy chọn chỉ có một chữ cái:

Mã nguồn [Chọn]
rm -rf filename
Với các lệnh thường dùng, vấn đề này ít nghiêm trọng hơn, nhưng có quá nhiều lệnh và tùy chọn nên chắc chắn bạn sẽ gặp phải những điều mới lạ. Thực hành lập trình tốt sẽ đảm bảo tập lệnh của bạn dễ đọc, dù là với thành viên khác trong nhóm, người mà bạn chưa từng giao tiếp, hay chính bạn vào một thời điểm nào đó trong tương lai.

Sau đây là một lệnh tương đương dễ đọc hơn nhiều so với lệnh trước:

Mã nguồn [Chọn]
rm --recursive --force filename
Nhiều lệnh hiện đại, hoặc phiên bản hiện đại của các lệnh đã có từ lâu, hỗ trợ các tùy chọn dài như thế này, bắt đầu bằng dấu "--" và là các từ đầy đủ thay vì các chữ cái đơn lẻ. Bạn không thể kết hợp chúng như các tùy chọn chữ cái đơn lẻ, nhưng chúng dễ đọc hơn nhiều.

Bạn không cần phải gõ đầy đủ các tùy chọn này mỗi khi sử dụng lệnh nếu bạn nhớ được cách ngắn gọn hơn. Tuy nhiên, trong các tập lệnh shell của riêng bạn, đặc biệt là những tập lệnh bạn chia sẻ với người khác, việc sử dụng các tùy chọn dài là một hình thức tự ghi chú mã mà bạn nên luôn hướng tới.

6. Sử dụng ký hiệu hiện đại để thay thế lệnh

Trong các tập lệnh Bash, có hai cách để chạy lệnh và ghi lại đầu ra của lệnh đó bên trong một biến:

Mã nguồn [Chọn]
VAR=$(ls)
VAR2=`ls`

Bạn sẽ thấy cả hai cách này được sử dụng, vậy cách nào tốt hơn?

Phương pháp backtick thực ra đã lỗi thời; nó hơi bất tiện vì nhiều lý do, chẳng hạn như nó không hỗ trợ lồng nhau tốt lắm. Vì vậy, hãy luôn ưu tiên hình thức hiện đại, sử dụng dấu ngoặc đơn.

7. Khai báo giá trị mặc định

Một cú pháp nâng cao tiện dụng khác, cho phép bạn chỉ định các giá trị biến mặc định mà không cần viết thêm mã để kiểm tra chuỗi rỗng:

Mã nguồn [Chọn]
CMD=${PAGER:-more}
Trong ví dụ này, giá trị của $CMD sẽ là giá trị của biến môi trường PAGER nếu được đặt và là "more" nếu không.

Bạn thậm chí có thể lồng các giá trị mặc định. Điều này cho phép bạn hỗ trợ một đối số dòng lệnh, với các phương án dự phòng cho một biến môi trường, sau đó là một giá trị mặc định, ví dụ:

Mã nguồn [Chọn]
DIR=${1:-${HOME:-/Users/bobby/home}}   
8. Hãy nêu rõ các tùy chọn bằng dấu gạch ngang kép

Cũng như khoảng trắng trong tên tệp có thể gây ra vấn đề, nhiều ký tự khác cũng vậy. Một ví dụ điển hình là trường hợp tệp có dấu "-:" ở đầu.

Mã nguồn [Chọn]
echo "nothing much" > -a-silly-filename
Bạn có thể xác nhận sự tồn tại của tệp này bằng cách liệt kê thư mục của nó:


Nhưng tương tác trực tiếp với tệp bằng cách sử dụng tên của tệp sẽ gây ra sự cố:


Giống như hầu hết các lệnh khác, lệnh ls coi một đối số bắt đầu bằng dấu "-" là một tùy chọn, do đó gây ra lỗi "tùy chọn không được nhận dạng". Vấn đề này có vẻ nhỏ nhặt, nhưng nó sẽ trở nên nghiêm trọng hơn nhiều nếu bạn xem xét một lệnh như thế này:

Mã nguồn [Chọn]
rm *
Nếu bạn có một tập tin trong thư mục có tên "-rf", điều này có thể dẫn đến thảm họa!

Bạn có thể tránh được rất nhiều vấn đề bằng cách giữ tên tệp đơn giản: tên tệp chữ thường az mà không có bất kỳ ký tự nào khác sẽ không bao giờ gây ra vấn đề. Tuy nhiên, bạn nên luôn lập trình một cách thận trọng trong các tập lệnh và chương trình của riêng mình để tránh sự cố.

Biện pháp bảo vệ tốt nhất chống lại loại vấn đề này là cú pháp "dấu gạch ngang kép", trông như thế này:

Mã nguồn [Chọn]
rm -- *.md
Dấu gạch ngang kép tuyên bố rõ ràng rằng "mọi thứ sau đây là một đối số", nghĩa là rm sẽ không diễn giải bất kỳ tên tệp lạ nào như thể chúng là các tùy chọn.

9. Sử dụng Biến cục bộ trong Hàm

Bạn có thể đã nghe nói rằng biến toàn cục không an toàn hoặc được khuyến cáo không nên sử dụng. Mặc dù sự thật có thể phức tạp hơn, nhưng thường thì tốt hơn là nên tránh sử dụng biến toàn cục trừ khi bạn thực sự biết mình đang làm gì.

Trong một tập lệnh shell, các biến mặc định là toàn cục, ngay cả bên trong các hàm:

Mã nguồn [Chọn]
#!/bin/bash

function run {
    DIR=`pwd`
    echo "doing something..."
}

DIR="/usr/local/bin"
run
echo $DIR

Rất dễ vô tình sử dụng lại tên biến và quên rằng bạn đang thay đổi giá trị của nó trong toàn bộ tập lệnh, chứ không chỉ bên trong hàm đang chạy. Tuy nhiên, cách khắc phục rất đơn giản: chỉ cần khai báo nó là một biến cục bộ:

Mã nguồn [Chọn]
function run {
    local DIR=`pwd`
}