Chuyển đến nội dung chính (Skip to content)
Mục lục bài viết
Linux Essentials

Linux Essentials 3.3: Biến Lệnh thành Tập lệnh

Linux Essentials 3.3: Biến Lệnh thành Tập lệnh

Viết shell script Bash đọc ticket giả lập, kiểm tra tham số và đường dẫn, dùng vòng lặp, tách stdout với stderr và kiểm chứng trạng thái trả về.

Tự động hóa một việc nhỏ có đầu vào rõ ràng

Sau khi lọc được ticket bằng lệnh tương tác, bạn có thể muốn dùng lại quy trình vào ngày hôm sau. Chép các lệnh vào một tệp là bước khởi đầu, nhưng script còn cần biết đầu vào nằm ở đâu, xử lý thế nào khi thiếu tệp và thông báo thành công có ý nghĩa gì. Nếu thiếu những điều này, tự động hóa chỉ giúp lặp lại nhầm lẫn nhanh hơn.

Bài thuộc mục tiêu 3.3 của Linux Essentials 010-160, liên hệ 013.3 của 010-200. Mốc biên tập: 23/10/2026. Bạn sẽ viết script Bash đọc một fixture ticket, kiểm tra tham số, chọn bản ghi OPEN và trả trạng thái khi đầu vào không hợp lệ. Lab chạy trong container Ubuntu 26.04.1 ARM64, Bash 5.3.9, với dữ liệu tự tạo; không thay đổi tài khoản hoặc dịch vụ production.

Xác định hợp đồng của script trước khi viết

Script nhận đúng một tham số: đường dẫn đến tệp văn bản thường mà tài khoản hiện tại đọc được. Mỗi dòng fixture có ba trường mã, trạng thái, nhóm, phân cách bằng dấu |. Tệp có newline ở cuối mỗi bản ghi, không có dấu phân cách bên trong trường. Script không nhận trách nhiệm phân tích CSV tùy ý, JSON hoặc dữ liệu đa dòng.

Đầu ra stdout là danh sách mã ticket OPEN. stderr ghi số lượng để minh họa cách tách dữ liệu với thông tin tổng hợp. Mã 2 báo số tham số không đúng; mã 3 báo đầu vào không phải tệp thường đọc được. Những mã này là quy ước của script bài học, không phải ý nghĩa bắt buộc của mọi chương trình Linux.

Một bài học nhỏ nên nói cả điều chưa làm: script chưa kiểm tra đầy đủ schema, chưa xử lý tệp thay đổi đồng thời và chưa bảo đảm báo cáo nhất quán cho hệ thống đang ghi liên tục. Dùng fixture tĩnh để kiểm chứng hành vi trước khi thiết kế công cụ công việc. Không gọi script là một giải pháp production chỉ vì nó chạy đúng bốn dòng mẫu.

Tạo fixture trong thư mục mới

Giữ cùng phiên Bash cho các bước. Khối dưới tạo thư mục tạm và tệp tickets.txt; không cần sudo. Dấu > ghi mới hoặc cắt ngắn tệp đích nếu tái sử dụng, vì vậy chỉ dùng trong đường dẫn mới do mktemp trả về. Không đổi đầu vào thành log có thông tin cá nhân để thử.

lpi_script=$(mktemp -d)
printf '%s\n' \
  'T001|OPEN|printer' \
  'T002|CLOSED|network' \
  'T003|OPEN|network' \
  'T004|OPEN|printer' > "$lpi_script/tickets.txt"
cat "$lpi_script/tickets.txt"

Bạn dự kiến thấy bốn dòng. Nếu phiên shell bị đóng, biến lpi_script sẽ không tự phục hồi. Ghi đường dẫn trong ghi chú lab thay vì đoán một thư mục tạm của lần chạy khác. Với dữ liệu công việc, nhật ký phải được loại bí mật trước khi chia sẻ.

Shebang, interpreter và cách gọi script

Dòng đầu #!/bin/bash khai báo interpreter cho cách thực thi trực tiếp được hệ thống hỗ trợ. Khi dùng bash đường-dẫn-script, bạn đã chọn Bash từ câu lệnh gọi; file chưa cần có execute bit để Bash đọc theo cách đó. Khi gọi trực tiếp bằng đường dẫn, quyền thực thi, quyền đi qua thư mục, interpreter và tùy chọn mount đều có thể ảnh hưởng.

Không mặc định /bin/sh là Bash. Trên nhiều hệ, nó là một shell khác; cú pháp riêng của Bash có thể không chạy như bạn mong. Chọn rõ môi trường bài học, kiểm tra bash --version và không đổi symlink hệ thống để làm một script chạy được. Nếu cần script portable, đó là yêu cầu thiết kế riêng.

Một script có tên ở thư mục hiện tại cũng không chắc được tìm khi chỉ gõ tên. PATH quyết định nơi shell tìm lệnh theo tên trong các trường hợp phù hợp. Dùng đường dẫn rõ ràng trong lab; không thêm dấu . vào đầu PATH như cách sửa chung. Bash Reference Manual là nguồn đối chiếu cú pháp, tham số và cách gọi shell.

Hợp đồng đầu vào và nhánh thất bại của script report.sh trong lab.
Hình 1. Hợp đồng đầu vào và nhánh thất bại của script report.sh trong lab. Mở hình để xem chi tiết.

Viết script bằng here-document không mở rộng biến lúc tạo

Trong khối dưới, delimiter SCRIPT được quote ở đầu here-document. Nhờ vậy các biến như $1 và $# được giữ trong nội dung tệp để Bash xử lý khi script chạy, không bị shell đang tạo tệp thay trước. Dấu > chỉ ghi vào report.sh của thư mục lab mới.

cat > "$lpi_script/report.sh" <<'SCRIPT'
#!/bin/bash
if [ "$#" -ne 1 ]; then
  printf '%s\n' 'Usage: report.sh INPUT' >&2
  exit 2
fi
input=$1
if [ ! -f "$input" ] || [ ! -r "$input" ]; then
  printf '%s\n' 'Input must be a readable regular file.' >&2
  exit 3
fi
count=0
while IFS='|' read -r ticket status category; do
  if [ "$status" = OPEN ]; then
    printf '%s\n' "$ticket"
    count=$((count + 1))
  fi
done < "$input"
printf 'open=%s\n' "$count" >&2
SCRIPT

Các khoảng trắng trong biểu thức [ ... ] là một phần cú pháp cần giữ. $# là số tham số, $1 là tham số đầu tiên. Quoting quanh "$input" giữ đường dẫn có khoảng trắng thành một đối số. Nó không xác minh tệp đáng tin hoặc loại mọi rủi ro khi chương trình khác diễn giải nội dung; bài chỉ đọc văn bản theo hợp đồng đã nêu.

if chọn nhánh theo trạng thái lệnh kiểm tra. -f hỏi tệp thường, -r hỏi khả năng đọc theo kiểm tra hiện tại. Tệp vẫn có thể đổi giữa lúc kiểm tra và đọc; đây không phải cơ chế chống race cho môi trường đối kháng. Không mở rộng ví dụ thành xử lý dữ liệu không tin cậy với quyền cao.

Đọc vòng lặp và bộ đếm theo từng bản ghi

read -r tránh việc backslash bị dùng như escape theo cách đọc mặc định; IFS='|' xác định ký tự phân trường cho lệnh read đó. Tên biến ticket, status và category nhận các phần dữ liệu theo quy tắc read. Nếu dữ liệu sai cấu trúc, script hiện chưa báo lỗi schema đầy đủ. Vì thế fixture và điều kiện newline phải được giữ khi đánh giá kết quả.

Chuyển hướng done < "$input" đưa tệp vào stdin của vòng lặp. Nó tránh cách pipe vào while có thể tạo một subshell trong nhiều cấu hình Bash, khiến giá trị bộ đếm ngoài vòng lặp khác dự đoán. Không cần học mọi trường hợp subshell ngay; ở đây hãy nhận biết phạm vi thực thi có thể ảnh hưởng trạng thái biến.

Khi status bằng OPEN, script in mã ticket rồi tăng count bằng arithmetic expansion. printf dùng định dạng cố định; dữ liệu nằm ở đối số riêng. Không dùng nội dung ticket làm chuỗi định dạng hoặc ghép thành lệnh eval. Đây là cách giữ dữ liệu khỏi bị diễn giải thêm như cú pháp shell.

Kiểm tra cú pháp, hành vi và điều kiện thực thi là những bằng chứng khác nhau.
Hình 2. Kiểm tra cú pháp, hành vi và điều kiện thực thi là những bằng chứng khác nhau. Mở hình để xem chi tiết.

Kiểm tra cú pháp trước, kiểm tra hành vi sau

bash -n "$lpi_script/report.sh"
bash "$lpi_script/report.sh" "$lpi_script/tickets.txt" \
  > "$lpi_script/open-ids.txt" 2> "$lpi_script/summary.txt"
cat "$lpi_script/open-ids.txt"
cat "$lpi_script/summary.txt"

bash -n kiểm tra cú pháp mà không thực hiện nội dung script theo cách chạy thông thường. Nó không chứng minh đường dẫn tồn tại, logic đúng hoặc dữ liệu hợp lệ. Sau khi chạy fixture, stdout dự kiến có T001, T003, T004; stderr có open=3. Đọc cả hai để không nhầm một tệp trống với kết quả không có yêu cầu mở.

Hai redirect ghi đè các tệp kết quả nếu chạy lại cùng đường dẫn. Giữ chúng trong lab và không đặt một tệp kết quả trùng đầu vào. Khi cần công cụ công việc, thiết kế cách tạo kết quả mới, kiểm tra rồi thay thế có phục hồi, thay vì coi mọi kết quả đã ghi là sẵn sàng gửi đi.

Kiểm tra các nhánh thất bại có chủ ý

Chạy thiếu tham số là một ca kiểm thử, không phải lỗi bất ngờ. Thu mã trả về ngay trong nhánh else vì một lệnh khác sẽ thay $?. Không dùng echo trước rồi mới hỏi trạng thái của script. Trong ví dụ, thông báo được giữ ở tệp lab, không cần giấu bằng chuyển mọi stderr vào /dev/null.

if bash "$lpi_script/report.sh" 2> "$lpi_script/noargs.txt"; then
  printf '%s\n' 'Không mong đợi: script chấp nhận thiếu tham số.'
else
  lpi_rc=$?
  printf 'status=%s\n' "$lpi_rc"
fi

Mã dự kiến là 2. Với một đường dẫn không tồn tại được truyền làm tham số, mã dự kiến là 3. Thử thêm một bản sao fixture có khoảng trắng trong tên để xác nhận quoting giữ đường dẫn. Các ca này kiểm tra hợp đồng đầu vào, chưa chứng minh toàn bộ lỗi filesystem hoặc thiếu dung lượng đã được xử lý.

for và thực thi trực tiếp trong phạm vi lab

Một vòng for có thể duyệt danh sách giá trị; quoting quyết định ranh giới từng mục. Trong ví dụ dưới, có đúng hai lần in, dù mỗi giá trị có khoảng trắng. Đừng dùng for trên kết quả ls để xử lý tên tệp tùy ý: khoảng trắng và ký tự đặc biệt có thể làm dữ liệu bị tách sai.

for label in 'first item' 'second item'; do
  printf '<%s>\n' "$label"
done
chmod u+x "$lpi_script/report.sh"
"$lpi_script/report.sh" "$lpi_script/tickets.txt"

chmod thay quyền của đúng script lab, không thay đệ quy cả thư mục. Execute bit chưa đủ nếu filesystem bị mount noexec; container thử nghiệm của bài dùng vùng /tmp cho phép thực thi để kiểm tra cách gọi trực tiếp. Không gỡ chính sách noexec của máy tổ chức để vượt một bài tập; bạn có thể kiểm tra cách gọi interpreter trong môi trường được phép.

Tự đánh giá trước khi dùng lại

Vì sao cần quote delimiter của here-document? Để giữ biến trong nội dung script cho thời điểm chạy. bash -n đạt có chứng minh báo cáo đúng? Không; còn cần fixture và kiểm tra đầu ra. Script thiếu tham số trả 2 có nghĩa mọi lệnh Linux dùng 2 cho cùng lỗi? Không; đó là quy ước của script này.

Bạn đạt bài khi giải thích được ba mã trạng thái trong phạm vi ví dụ, tái tạo ba mã ticket OPEN và kiểm tra được đường dẫn có khoảng trắng. Giữ script cùng fixture và phiên bản môi trường để người khác tái hiện. Khi nhu cầu chuyển sang dữ liệu phức tạp, hãy xem lại định dạng, cơ chế lỗi và công cụ phù hợp trước khi kéo dài script nhập môn.

Bài liên quan trong bộ LPI

Tìm hiểu thêm