Bash & Shell Scripting · Part 5 — Functions, Arguments & Sourcing
Bash functions: define with name() or function, pass $1/$@, return exit codes not strings, capture output with $(), scope with local, source library files — bilingual, with exercises.
Đây là Phần 5 của series 10 bài đưa bạn từ “copy-paste lệnh” đến viết shell script chắc chắn, mức production. Phần 1–4 đã nói về shell, biến, điều kiện, và vòng lặp. Giờ ta đóng gói logic thành khối tái sử dụng — function bạn gọi, kiểm thử, và chia sẻ giữa các script.
Function là cách bạn ngừng copy-paste cùng mười dòng và cách bạn xây thư viện helper mà team có thể source. Làm đúng thì script đọc như chương trình; làm sai thì thiếu local âm thầm làm hỏng biến global.
Function bash là gì?
Một function là khối lệnh có tên, định nghĩa một lần và gọi nhiều lần. Khác lệnh ngoài (ls, grep), function chạy trong shell hiện tại — không fork+exec, không binary mới.
Điều đó quan trọng cho hiệu năng (gọi rẻ) và phạm vi: function có thể đọc/ghi biến shell trừ khi bạn cố ý giới hạn bằng local.
Định nghĩa function — hai cú pháp
Bash chấp nhận hai dạng tương đương:
Dạng 1: name() { ... } (kiểu POSIX)
greet() {
echo "Hello, $1!"
}
Dấu ngoặc sau tên bắt buộc nhưng rỗng — không chứa tham số. Tham số luôn truy cập trong thân qua $1, $2, v.v..
Dạng 2: function name { ... } (từ khóa bash)
function greet {
echo "Hello, $1!"
}
Dạng này chỉ dành cho bash và dễ viết hơn chút (có thể bỏ () sau tên). Trong script bash, hai dạng đều ổn — chọn một style và giữ nhất quán.
Ví dụ tối thiểu, đầy đủ:
#!/usr/bin/env bash
say_twice() {
local word="$1"
echo "$word"
echo "$word"
}
say_twice "ping"
# ping
# ping
Gọi function & truyền tham số
Gọi function như lệnh khác: tên rồi tham số cách nhau bởi khoảng trắng:
greet "Alice"
greet "Bob" "ignored-extra" # $2 exists but greet only uses $1
Trong function, tham số vị trí hoạt động y hệt tham số script:
| Biến | Ý nghĩa trong function |
|---|---|
$1, $2, … | Tham số thứ 1, 2, … của lần gọi này |
$# | Số tham số của lần gọi này |
$@ | Mọi tham số thành từng từ — dùng "$@" khi chuyển tiếp |
$* | Mọi tham số thành một chuỗi (hiếm khi cần) |
Ví dụ dùng mọi biến vị trí:
show_args() {
echo "Count: $#"
echo "All (quoted): $*"
echo "Each:"
for arg in "$@"; do
echo " - $arg"
done
}
show_args one "two words" three
# Count: 3
# All (quoted): one two words three
# Each:
# - one
# - two words
# - three
Quy tắc: luôn quote
"$1","$@", v.v. trong function — cùng kỷ luật quoting như các phần trước.
return đặt exit code — không phải giá trị trả về
Đây là điểm hiểu sai phổ biến nhất về function bash: return không gửi dữ liệu về caller. Nó chỉ đặt exit status của function — số nguyên từ 0 đến 255.
is_even() {
local n="$1"
if (( n % 2 == 0 )); then
return 0 # success = "yes, it is even"
else
return 1 # failure = "no, it is odd"
fi
}
is_even 4
echo "$?" # 0
is_even 7
echo "$?" # 1
return không đối số tương đương return $? (status của lệnh cuối). Bỏ return hoàn toàn thì exit status là của lệnh cuối trong thân.
Trả dữ liệu — echo và capture
Để trả chuỗi, số, hoặc path, in ra rồi capture stdout:
double() {
local n="$1"
echo $(( n * 2 )) # this is the "return value"
}
result=$(double 21)
echo "double(21) = $result" # double(21) = 42
Caller dùng command substitution $(...) để đọc output của function. Đây là bash idiomatic — không phải mẹo vặt.
Có thể kết hợp: echo dữ liệu và dùng return cho thành công/thất bại:
safe_divide() {
local a="$1" b="$2"
if (( b == 0 )); then
echo "error: divide by zero" >&2
return 1
fi
echo $(( a / b ))
return 0
}
if quotient=$(safe_divide 10 2); then
echo "10 / 2 = $quotient" # 10 / 2 = 5
else
echo "division failed"
fi
Phạm vi với local
Mặc định, biến gán trong function là global — ghi đè biến cùng tên trong script hoặc scope cha. Gây bug tinh vi, khó debug.
name="global"
leaky() {
name="leaked" # overwrites the global — no local!
}
leaky
echo "$name" # leaked (not "global")
Luôn khai báo biến trong function bằng local:
name="global"
safe() {
local name="scoped"
echo "inside: $name"
}
safe
echo "outside: $name" # global (unchanged)
local có thể gán và khai báo một dòng, như gán thường:
timestamp() {
local fmt="${1:-%Y-%m-%d %H:%M:%S}"
date "+$fmt"
}
local là từ khóa bash — không có trong POSIX sh. Series này nhắm bash, nên dùng local mọi nơi trong function.
Đọc exit status của function qua $?
Sau khi function trả về, $? giữ exit code — giống mọi lệnh:
file_exists() {
[[ -f "$1" ]]
}
if file_exists "/etc/hosts"; then
echo "hosts file is there"
else
echo "missing"
fi
Cẩn thận: $? dễ mất — lệnh tiếp theo ghi đè. Capture ngay nếu cần dùng sau:
myfunc
status=$? # save before anything else runs
echo "myfunc exited with $status"
Trong điều kiện if và while, bash kiểm tra exit status tự động — hiếm khi cần $? ở đó.
Source thư viện function
Khi nhiều script dùng chung helper, đặt function vào file thư viện rồi nạp bằng source (hoặc ., giống hệt):
# lib/strings.sh
trim() {
local s="$1"
# remove leading/trailing whitespace
s="${s#"${s%%[![:space:]]*}"}"
s="${s%"${s##*[![:space:]]}"}"
echo "$s"
}
#!/usr/bin/env bash
# deploy.sh
source "$(dirname "$0")/lib/strings.sh"
name=$(trim " Alice ")
echo "[$name]" # [Alice]
source chạy file trong shell hiện tại, nên mọi function và biến trong thư viện có ngay. Khác ./lib.sh, chạy file như script riêng trong shell con.
Dùng path tương đối script với $(dirname "$0") để chạy đúng dù gọi từ đâu.
File hai mục đích: chạy hoặc source
File thư viện tốt có thể được source bởi script khác và chạy trực tiếp để tự kiểm thử. Bảo vệ logic “main” bằng pattern BASH_SOURCE:
#!/usr/bin/env bash
# lib/math.sh — reusable math helpers
add() {
local a="$1" b="$2"
echo $(( a + b ))
}
mul() {
local a="$1" b="$2"
echo $(( a * b ))
}
# Only run self-test when executed directly, not when sourced
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
echo "self-test: add 3 4 = $(add 3 4)"
echo "self-test: mul 3 4 = $(mul 3 4)"
fi
- Khi
source lib/math.sh,${BASH_SOURCE[0]}làlib/math.shnhưng${0}là script gọi — khác nhau, nên khốiifbị bỏ qua. - Khi
./lib/math.sh, cả hai đều làlib/math.sh— self-test chạy.
Pattern này giữ thư viện test được mà không làm bẩn namespace của script source.
return vs echo — khi nào dùng cái nào
| Mục tiêu | Cơ chế | Ví dụ |
|---|---|---|
| Báo thành công / thất bại | return 0 / return 1 | is_valid "$input" then check $? or if |
| Trả chuỗi hoặc số | echo / printf + $(...) | name=$(trim "$raw") |
| Cả dữ liệu và status | echo the value, return the status | safe_divide example above |
| Dừng cả script khi lỗi | return inside function; caller uses set -e or checks $? | Part 9 covers set -euo pipefail in depth |
Gắn lại — script tiện ích nhỏ
#!/usr/bin/env bash
# backup-dir.sh — archive a directory with a timestamped name
set -euo pipefail
log() {
local level="$1"
shift
printf '[%s] %s: %s\n' "$(date +%H:%M:%S)" "$level" "$*"
}
archive_name() {
local dir="$1"
local base
base=$(basename "$dir")
local stamp
stamp=$(date +%Y%m%d-%H%M%S)
echo "${base}-${stamp}.tar.gz"
}
create_archive() {
local src="$1"
local dest="$2"
tar -czf "$dest" -C "$(dirname "$src")" "$(basename "$src")"
}
main() {
local target="${1:?usage: backup-dir.sh <directory>}"
[[ -d "$target" ]] || { log ERROR "not a directory: $target"; return 1; }
local outfile
outfile=$(archive_name "$target")
log INFO "archiving $target → $outfile"
create_archive "$target" "$outfile"
log INFO "done: $outfile"
}
main "$@"
Mọi helper dùng local, in dữ liệu qua echo/printf, và báo lỗi qua exit code. main điều phối luồng — pattern bạn sẽ thấy trong mọi script production của series.
Lỗi người mới hay mắc
- Dùng
returnđể trả chuỗi —return "hello"là lỗi cú pháp; kể cảreturn 42chỉ đặt exit status42, không phải giá trị dùng được. - Quên
local— biến đếm hoặc biến tạm trong function ghi đè global cùng tên và làm hỏng code không liên quan phía sau. - Gọi function trước khi định nghĩa — khác một số ngôn ngữ, bash không hoist function; định nghĩa phải ở trên lời gọi (hoặc phải
sourcethư viện trước).
Bài tập
Thử từng bài trước khi mở lời giải.
- Viết function
max_of_twonhận hai số nguyên và echo số lớn hơn. Gọi nó và lưu kết quả vào biến. - Viết
is_emptyreturn 0 nếu đối số là chuỗi rỗng, return 1 nếu không. Dùng trongifmà không cần kiểm$?thủ công. - Tạo
lib/greet.shcó functiongreetvà khối self-testBASH_SOURCE. Source từ script thứ hai và gọigreetvới tên bạn.
Lời giải
#!/usr/bin/env bash
# Exercise 1
max_of_two() {
local a="$1" b="$2"
if (( a > b )); then
echo "$a"
else
echo "$b"
fi
}
winner=$(max_of_two 14 9)
echo "max: $winner" # max: 14# Exercise 2
is_empty() {
[[ -z "$1" ]]
}
if is_empty ""; then
echo "string is empty"
else
echo "string has content"
fi
# string is empty
if is_empty "hi"; then
echo "string is empty"
else
echo "string has content"
fi
# string has content# Exercise 3 — lib/greet.sh
#!/usr/bin/env bash
greet() {
local name="${1:-stranger}"
echo "Hello, $name!"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
greet "self-test"
fi# Exercise 3 — say-hi.sh
#!/usr/bin/env bash
source "$(dirname "$0")/lib/greet.sh"
greet "vinxi"Chạy ./lib/greet.sh để thấy self-test; chạy ./say-hi.sh để thấy lời gọi qua source.
Điều cốt lõi
Function đóng gói logic tái dùng; tham số vào qua $1, $@, $# như script; return chỉ là exit code, nên echo + $(...) là cách trả dữ liệu; local chặn global rò rỉ; source nạp thư viện dùng chung còn guard BASH_SOURCE cho thư viện vừa source vừa chạy self-test.