This programming guide is only available in Vietnamese. Switch to Vietnamese to read the full article.

Đến bài này, bạn đã biết cách sử dụng biến, điều kiện và vòng lặp. Giờ là lúc tổ chức code thành functions có thể tái sử dụng và viết script hoàn chỉnh với cấu trúc chuyên nghiệp. Đây là bước chuyển từ "người dùng terminal" sang "shell script developer".

1. Shebang & Cách Chạy Script

Dòng đầu tiên của mọi shell script nên là shebang — cho hệ điều hành biết dùng interpreter nào để chạy file:

shebang.sh
#!/bin/bash
# Shebang trỏ trực tiếp đến /bin/bash

#!/usr/bin/env bash
# Tốt hơn: dùng env để tìm bash trong $PATH (portable hơn)
# Trên một số hệ thống, bash nằm ở /usr/local/bin/bash

# === 3 cách chạy script ===

# Cách 1: ./script.sh (cần chmod +x trước)
chmod +x myscript.sh
./myscript.sh            # chạy trong subprocess MỚI

# Cách 2: bash script.sh (không cần chmod +x)
bash myscript.sh         # chạy trong subprocess mới

# Cách 3: source script.sh (chạy trong shell HIỆN TẠI)
source myscript.sh       # tương đương: . myscript.sh
# ⚠️ Biến, function được define trong script sẽ TỒN TẠI trong shell hiện tại!

# === Sự khác biệt quan trọng ===
# Tạo file test_source.sh:
# #!/bin/bash
# MY_VAR="hello from script"
# cd /tmp

# Chạy bằng ./test_source.sh:
#   → MY_VAR KHÔNG tồn tại trong shell cha
#   → Thư mục hiện tại KHÔNG thay đổi

# Chạy bằng source test_source.sh:
#   → MY_VAR CÓ tồn tại trong shell hiện tại
#   → Thư mục hiện tại THAY ĐỔI sang /tmp

2. Tạo Functions

functions.sh
#!/bin/bash

# === Cú pháp khai báo function ===
# Cách 1 (khuyên dùng):
greet() {
  echo "Xin chào, $1!"    # $1 = argument đầu tiên
}

# Cách 2 (có keyword function):
function goodbye {
  echo "Tạm biệt, $1!"
}

# Gọi function (KHÔNG có dấu ngoặc đơn)
greet "Quang"         # Xin chào, Quang!
goodbye "World"       # Tạm biệt, World!

# === Arguments trong function ===
show_info() {
  echo "Tên script: $0"       # tên file script (không phải tên function)
  echo "Argument 1: $1"
  echo "Argument 2: $2"
  echo "Tất cả args: $@"
  echo "Số lượng args: $#"
}

show_info "Alice" "Bob" "Charlie"

# === local keyword — biến cục bộ trong function ===
my_func() {
  local local_var="Tôi chỉ tồn tại trong function"
  global_var="Tôi tồn tại ở mọi nơi"
  echo "$local_var"
}

my_func
echo "$global_var"       # Tôi tồn tại ở mọi nơi
echo "$local_var"        # (rỗng — không tồn tại ngoài function)

# === Function thực tế: validate input ===
is_number() {
  local value="$1"
  if [[ "$value" =~ ^[0-9]+$ ]]; then
    return 0   # true (success)
  else
    return 1   # false (failure)
  fi
}

if is_number "42"; then
  echo "42 là số hợp lệ"
fi

if ! is_number "abc"; then
  echo "abc không phải số"
fi

3. Return vs Exit

return_exit.sh
#!/bin/bash

# === return N — thoát FUNCTION, trả về exit code (0-255) ===
check_file() {
  if [[ -f "$1" ]]; then
    return 0    # thành công
  else
    return 1    # thất bại
  fi
}

check_file "/etc/passwd"
echo "Exit code: $?"      # 0

# === exit N — thoát TOÀN BỘ SCRIPT ===
# Nếu bạn dùng exit trong function → thoát luôn script!
# Chỉ dùng exit ở main script level hoặc khi muốn dừng hoàn toàn.

# === Trả về STRING từ function ===
# return chỉ trả số 0-255, KHÔNG trả string!
# Dùng command substitution để "trả về" string:

get_timestamp() {
  date +"%Y-%m-%d %H:%M:%S"    # echo ra stdout
}

# Bắt output bằng $()
current_time=$(get_timestamp)
echo "Thời gian: $current_time"

# === Pattern: function trả về giá trị qua stdout ===
to_uppercase() {
  echo "$1" | tr '[:lower:]' '[:upper:]'
}

result=$(to_uppercase "hello bash")
echo "$result"    # HELLO BASH

# === Pattern: function set biến global (tránh dùng nếu có thể) ===
get_system_info() {
  SYSTEM_OS=$(uname -s)
  SYSTEM_ARCH=$(uname -m)
  SYSTEM_HOSTNAME=$(hostname)
}

get_system_info
echo "OS: $SYSTEM_OS, Arch: $SYSTEM_ARCH, Host: $SYSTEM_HOSTNAME"

4. Xử lý Arguments với getopts

arguments.sh
#!/bin/bash

# === Biến đặc biệt cho arguments ===
echo "Script name: $0"
echo "First arg:   $1"
echo "Second arg:  $2"
echo "All args:    $@"    # mỗi arg là string riêng (khuyên dùng)
echo "All args:    $*"    # tất cả args gộp thành 1 string
echo "Arg count:   $#"

# === shift — dịch arguments sang trái ===
echo "Before shift: $1 $2 $3"   # a b c
shift                             # bỏ $1, $2→$1, $3→$2
echo "After shift:  $1 $2"      # b c

# === getopts — parse short options ===
# Cú pháp: getopts "optstring" variable
# Dấu : sau option = option yêu cầu argument

usage() {
  echo "Usage: $0 [-f file] [-o output] [-v] [-h]"
  echo "  -f FILE    Input file"
  echo "  -o OUTPUT  Output file"
  echo "  -v         Verbose mode"
  echo "  -h         Show help"
}

verbose=false
input_file=""
output_file=""

while getopts "f:o:vh" opt; do
  case $opt in
    f) input_file="$OPTARG" ;;
    o) output_file="$OPTARG" ;;
    v) verbose=true ;;
    h) usage; exit 0 ;;
    \?) echo "Invalid option: -$OPTARG" >&2; usage; exit 1 ;;
    :) echo "Option -$OPTARG requires an argument" >&2; exit 1 ;;
  esac
done

# Bỏ qua các options đã parse, giữ lại positional args
shift $((OPTIND - 1))

echo "Input: $input_file"
echo "Output: $output_file"
echo "Verbose: $verbose"
echo "Remaining args: $@"

# Chạy: ./arguments.sh -f data.csv -o result.txt -v extra1 extra2

# === Manual parsing cho long options ===
while [[ $# -gt 0 ]]; do
  case $1 in
    --file)
      input_file="$2"
      shift 2
      ;;
    --output)
      output_file="$2"
      shift 2
      ;;
    --verbose)
      verbose=true
      shift
      ;;
    --help)
      usage
      exit 0
      ;;
    --)
      shift
      break
      ;;
    -*)
      echo "Unknown option: $1" >&2
      exit 1
      ;;
    *)
      break
      ;;
  esac
done

5. Best Practices: Template Script Chuẩn

professional_template.sh
#!/usr/bin/env bash
#
# Script: process_data.sh
# Mô tả: Đọc file CSV, xử lý và xuất kết quả
# Tác giả: Your Name
# Ngày: 2026-06-24
#

# === Strict Mode — BẮT BUỘC cho mọi script nghiêm túc ===
set -euo pipefail
# -e: thoát ngay khi lệnh lỗi (exit code != 0)
# -u: báo lỗi khi dùng biến chưa khai báo
# -o pipefail: pipeline thất bại nếu BẤT KỲ lệnh nào trong pipe lỗi

# === Constants ===
readonly SCRIPT_NAME=$(basename "$0")
readonly SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
readonly LOG_FILE="/tmp/${SCRIPT_NAME}.log"

# === Logging Function ===
log() {
  local level="$1"
  shift
  local message="$*"
  local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
  echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
}

log_info()  { log "INFO"  "$@"; }
log_warn()  { log "WARN"  "$@"; }
log_error() { log "ERROR" "$@"; }

# === Cleanup Function — chạy khi script thoát (kể cả lỗi) ===
cleanup() {
  local exit_code=$?
  if [[ -f "${TMP_FILE:-}" ]]; then
    rm -f "$TMP_FILE"
    log_info "Đã dọn dẹp file tạm: $TMP_FILE"
  fi
  if [[ $exit_code -ne 0 ]]; then
    log_error "Script thoát với mã lỗi: $exit_code"
  fi
  exit $exit_code
}
trap cleanup EXIT      # Luôn chạy cleanup khi thoát
trap 'exit 1' INT TERM # Ctrl+C → cleanup → exit

# === Usage/Help ===
usage() {
  cat <<EOF
Usage: $SCRIPT_NAME [OPTIONS] <input_file>

Đọc file CSV, xử lý dữ liệu và xuất kết quả.

Options:
  -o, --output FILE   File kết quả (mặc định: stdout)
  -d, --delimiter C   Ký tự phân tách (mặc định: ,)
  -v, --verbose       Hiển thị chi tiết
  -h, --help          Hiện hướng dẫn này

Examples:
  $SCRIPT_NAME data.csv
  $SCRIPT_NAME -o result.txt -d ";" data.csv
EOF
}

# === Parse Arguments ===
output_file=""
delimiter=","
verbose=false

while [[ $# -gt 0 ]]; do
  case $1 in
    -o|--output)    output_file="$2"; shift 2 ;;
    -d|--delimiter) delimiter="$2"; shift 2 ;;
    -v|--verbose)   verbose=true; shift ;;
    -h|--help)      usage; exit 0 ;;
    -*)             log_error "Option không hợp lệ: $1"; usage; exit 1 ;;
    *)              break ;;
  esac
done

# === Validate Input ===
input_file="${1:-}"
if [[ -z "$input_file" ]]; then
  log_error "Thiếu file đầu vào"
  usage
  exit 1
fi

if [[ ! -f "$input_file" ]]; then
  log_error "File không tồn tại: $input_file"
  exit 1
fi

# === Main Logic ===
main() {
  log_info "Bắt đầu xử lý: $input_file"
  TMP_FILE=$(mktemp)

  local line_count=0
  while IFS="$delimiter" read -r col1 col2 col3; do
    (( line_count++ ))
    $verbose && log_info "Dòng $line_count: $col1 | $col2 | $col3"
    echo "$col1,$col2,$col3" >> "$TMP_FILE"
  done < "$input_file"

  # Xuất kết quả
  if [[ -n "$output_file" ]]; then
    cp "$TMP_FILE" "$output_file"
    log_info "Kết quả đã ghi vào: $output_file"
  else
    cat "$TMP_FILE"
  fi

  log_info "Hoàn tất! Đã xử lý $line_count dòng."
}

main

6. Câu hỏi trắc nghiệm ôn tập

Trắc nghiệm 1: source vs ./

Script setup.sh chứa export API_KEY="abc123". Sau khi chạy ./setup.sh, bạn gõ echo $API_KEY trong terminal. Kết quả là gì?

Trắc nghiệm 2: return trong function

return trong function Bash có thể trả về giá trị gì?

Trắc nghiệm 3: set -euo pipefail

Tác dụng của set -u là gì?

Tải file code thực hành minh họa bài học

File script mẫu hoàn chỉnh với functions, argument parsing, error handling và logging:

Tải về functions_scripts.sh

Related Articles

Bài viết liên quan trong series

Lesson 5: Text Processing in Bash: Pipeline, grep, sed, awk & Redirection Bài 5: Xử Lý Text trong Bash: Pipeline, grep, sed, awk & Redirection Lesson 3: Control Flow: Conditionals, Loops & Exit Codes Bài 3: Luồng Điều Khiển — Điều Kiện, Vòng Lặp & Exit Codes Back to Bash Series Overview Quay lại Lộ trình Bash Series