jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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.

greet "Alice" 42 call with args greet() { local name="$1" # Alice local age="$2" # 42 echo "Hi $name" # output / return via echo }
Call passes arguments as $1, $2 …; local scopes variables inside the body; echo sends data back to the caller via command substitution

Đị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 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 ifwhile, 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 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]}lib/math.sh nhưng ${0} là script gọi — khác nhau, nên khối if bị 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êuCơ chếVí dụ
Báo thành công / thất bạireturn 0 / return 1is_valid "$input" then check $? or if
Trả chuỗi hoặc sốecho / printf + $(...)name=$(trim "$raw")
Cả dữ liệu và statusecho the value, return the statussafe_divide example above
Dừng cả script khi lỗireturn 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 42 chỉ đặt exit status 42, 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 source thư viện trước).

Bài tập

Thử từng bài trước khi mở lời giải.

  1. Viết function max_of_two nhậ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.
  2. Viết is_empty return 0 nếu đối số là chuỗi rỗng, return 1 nếu không. Dùng trong if mà không cần kiểm $? thủ công.
  3. Tạo lib/greet.sh có function greet và khối self-test BASH_SOURCE. Source từ script thứ hai và gọi greet vớ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.