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.
This is Part 5 of a 10-part series that takes you from “I copy-paste commands” to writing robust, production-grade shell scripts {Đâ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}. Parts 1–4 covered the shell, variables, conditionals, and loops {Phần 1–4 đã nói về shell, biến, điều kiện, và vòng lặp}. Now we package logic into reusable blocks — functions you can call, test, and share across scripts {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}.
Functions are how you stop copy-pasting the same ten lines and how you build a library of helpers your team can source {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}. Get them right and scripts read like a program; get them wrong and a missing local silently corrupts globals {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}.
What is a bash function? {Function bash là gì?}
A function is a named block of commands you define once and invoke many times {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}. Unlike external commands (ls, grep), functions run inside the current shell process — no fork+exec, no new binary {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}.
That matters for performance (cheap to call) and for scope: a function can read and write shell variables unless you deliberately restrict them with local {Đ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}.
Defining a function — two syntaxes {Định nghĩa function — hai cú pháp}
Bash accepts two equivalent forms {Bash chấp nhận hai dạng tương đương}:
Form 1: name() { ... } (POSIX-style) {Dạng 1: name() { ... } (kiểu POSIX)}
greet() {
echo "Hello, $1!"
}
The parentheses after the name are required but empty — they do not hold parameters {Dấu ngoặc sau tên bắt buộc nhưng rỗng — không chứa tham số}. Arguments are always accessed inside the body via $1, $2, etc. {Tham số luôn truy cập trong thân qua $1, $2, v.v.}.
Form 2: function name { ... } (bash keyword) {Dạng 2: function name { ... } (từ khóa bash)}
function greet {
echo "Hello, $1!"
}
This form is bash-specific and slightly more forgiving (you can omit the () after the name) {Dạng này chỉ dành cho bash và dễ viết hơn chút (có thể bỏ () sau tên)}. For portability within bash scripts, either form is fine — pick one style and stay consistent {Trong script bash, hai dạng đều ổn — chọn một style và giữ nhất quán}.
A minimal, complete example {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
Calling functions & passing arguments {Gọi function & truyền tham số}
You call a function like any other command: name followed by arguments separated by whitespace {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
Inside the function, positional parameters work exactly like script arguments {Trong function, tham số vị trí hoạt động y hệt tham số script}:
| Variable {Biến} | Meaning inside a function {Ý nghĩa trong function} |
|---|---|
$1, $2, … | The 1st, 2nd, … argument passed to this call {Tham số thứ 1, 2, … của lần gọi này} |
$# | Count of arguments to this call {Số tham số của lần gọi này} |
$@ | All arguments as separate words — use "$@" when forwarding {Mọi tham số thành từng từ — dùng "$@" khi chuyển tiếp} |
$* | All arguments as one string (rarely what you want) {Mọi tham số thành một chuỗi (hiếm khi cần)} |
Example that uses every positional variable {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
Rule {Quy tắc}: always quote
"$1","$@", etc. inside functions — same quoting discipline as the rest of the series {luôn quote"$1","$@", v.v. trong function — cùng kỷ luật quoting như các phần trước}.
return sets an exit code — not a value {return đặt exit code — không phải giá trị trả về}
This is the single most misunderstood fact about bash functions {Đây là điểm hiểu sai phổ biến nhất về function bash}: return does not send data back to the caller {return không gửi dữ liệu về caller}. It only sets the function’s exit status — an integer from 0 to 255 {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 with no argument is equivalent to return $? (the status of the last command) {return không đối số tương đương return $? (status của lệnh cuối)}. If you omit return entirely, the function’s exit status is whatever the last command in the body returned {Bỏ return hoàn toàn thì exit status là của lệnh cuối trong thân}.
Returning data — echo and capture {Trả dữ liệu — echo và capture}
To pass a string, number, or path back, print it and capture stdout {Để 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
The caller uses command substitution $(...) to read the function’s output {Caller dùng command substitution $(...) để đọc output của function}. This is idiomatic bash — not a hack {Đây là bash idiomatic — không phải mẹo vặt}.
You can combine both patterns: echo data and use return for success/failure {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
Scoping with local {Phạm vi với local}
By default, variables assigned inside a function are global — they overwrite same-named variables in the script or parent scope {Mặc định, biến gán trong function là global — ghi đè biến cùng tên trong script hoặc scope cha}. That causes subtle, hard-to-debug bugs {Gây bug tinh vi, khó debug}.
name="global"
leaky() {
name="leaked" # overwrites the global — no local!
}
leaky
echo "$name" # leaked (not "global")
Always declare function-scoped variables with local {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 can assign and declare in one line, just like a normal assignment {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 is a bash keyword — not available in POSIX sh {local là từ khóa bash — không có trong POSIX sh}. Since this series targets bash, use it everywhere inside functions {Series này nhắm bash, nên dùng local mọi nơi trong function}.
Reading a function’s exit status with $? {Đọc exit status của function qua $?}
After a function returns, $? holds its exit code — same as any command {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
Be careful: $? is volatile — the next command overwrites it {Cẩn thận: $? dễ mất — lệnh tiếp theo ghi đè}. Capture it immediately if you need it later {Capture ngay nếu cần dùng sau}:
myfunc
status=$? # save before anything else runs
echo "myfunc exited with $status"
In if and while conditions, bash checks the exit status automatically — you rarely need $? there {Trong điều kiện if và while, bash kiểm tra exit status tự động — hiếm khi cần $? ở đó}.
Sourcing a function library {Source thư viện function}
When several scripts share the same helpers, put functions in a library file and load it with source (or ., which is identical) {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 runs the file in the current shell, so all functions and variables defined in the library become available immediately {source chạy file trong shell hiện tại, nên mọi function và biến trong thư viện có ngay}. This is different from ./lib.sh, which would run the file as a separate script in a child shell {Khác ./lib.sh, chạy file như script riêng trong shell con}.
Use a path relative to the script with $(dirname "$0") so it works no matter where you invoke the script from {Dùng path tương đối script với $(dirname "$0") để chạy đúng dù gọi từ đâu}.
Dual-purpose files: run or source {File hai mục đích: chạy hoặc source}
A well-designed library file can be sourced by other scripts and run directly for a quick self-test {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ử}. Guard the “main” logic with the BASH_SOURCE pattern {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
- When you
source lib/math.sh,${BASH_SOURCE[0]}islib/math.shbut${0}is your calling script — they differ, so theifblock is skipped {Khisource 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}. - When you
./lib/math.sh, both refer tolib/math.sh— the self-test runs {Khi./lib/math.sh, cả hai đều làlib/math.sh— self-test chạy}.
This pattern keeps libraries testable without polluting the namespace of scripts that source them {Pattern này giữ thư viện test được mà không làm bẩn namespace của script source}.
return vs echo — when to use which {return vs echo — khi nào dùng cái nào}
| Goal {Mục tiêu} | Mechanism {Cơ chế} | Example {Ví dụ} |
|---|---|---|
| Signal success / failure {Báo thành công / thất bại} | return 0 / return 1 | is_valid "$input" then check $? or if |
| Pass a string or number back {Trả chuỗi hoặc số} | echo / printf + $(...) | name=$(trim "$raw") |
| Both data and status {Cả dữ liệu và status} | echo the value, return the status | safe_divide example above |
| Stop the whole script on failure {Dừng cả script khi lỗi} | return inside function; caller uses set -e or checks $? | Part 9 covers set -euo pipefail in depth |
Putting it together — a small utility script {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 "$@"
Every helper uses local, prints data via echo/printf, and signals errors through exit codes {Mọi helper dùng local, in dữ liệu qua echo/printf, và báo lỗi qua exit code}. main orchestrates the flow — a pattern you will see in every production script in this series {main điều phối luồng — pattern bạn sẽ thấy trong mọi script production của series}.
Mistakes beginners make {Lỗi người mới hay mắc}
- ❌ Using
returnto send back a string —return "hello"is a syntax error; evenreturn 42only sets exit status42, not a usable value {Dùngreturnđể 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}. - ❌ Forgetting
local— a loop counter or temp variable inside a function overwrites a global with the same name and breaks unrelated code later {Quênlocal— 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}. - ❌ Calling a function before it is defined — unlike some languages, bash does not hoist functions; the definition must appear above the call (or you must
sourcethe library first) {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ảisourcethư viện trước)}.
Exercises {Bài tập}
Try each before opening the solution {Thử từng bài trước khi mở lời giải}.
- Write a function
max_of_twothat takes two integers and echoes the larger one. Call it and store the result in a variable {Viết functionmax_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}. - Write
is_emptythat returns 0 if its argument is an empty string, returns 1 otherwise. Use it in anifwithout checking$?manually {Viếtis_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}. - Create
lib/greet.shwith agreetfunction and aBASH_SOURCEself-test block. Source it from a second script and callgreetwith your name {Tạolib/greet.shcó functiongreetvà khối self-testBASH_SOURCE. Source từ script thứ hai và gọigreetvới tên bạn}.
Solution {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"Run ./lib/greet.sh to see the self-test; run ./say-hi.sh to see the sourced call {Chạy ./lib/greet.sh để thấy self-test; chạy ./say-hi.sh để thấy lời gọi qua source}.
Takeaway {Điều cốt lõi}
Functions package reusable logic; arguments arrive as $1, $@, $# just like script args; return is only an exit code, so echo + $(...) is how you pass data back; local prevents globals from leaking; and source loads a shared library while the BASH_SOURCE guard lets that library double as a runnable self-test {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}.