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.

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}.

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

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 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 ifwhile, 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 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]} is lib/math.sh but ${0} is your calling script — they differ, so the if block is skipped {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}.
  • When you ./lib/math.sh, both refer to lib/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 1is_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 statussafe_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 return to send back a string — return "hello" is a syntax error; even return 42 only sets exit status 42, not a usable value {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}.
  • ❌ Forgetting local — a loop counter or temp variable inside a function overwrites a global with the same name and breaks unrelated code later {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}.
  • ❌ 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 source the 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ải source thư 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}.

  1. Write a function max_of_two that takes two integers and echoes the larger one. Call it and store the result in a variable {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. Write is_empty that returns 0 if its argument is an empty string, returns 1 otherwise. Use it in an if without checking $? manually {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. Create lib/greet.sh with a greet function and a BASH_SOURCE self-test block. Source it from a second script and call greet with your name {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}.
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}.