Lesson 10: 에러 처리 및 디버깅

Lesson 10: 에러 처리 및 디버깅

난이도: ⭐⭐⭐

이전: 09_Process_Management.md | 다음: 11_Argument_Parsing.md


1. set 옵션 심화

set 명령어는 셸 동작 및 에러 처리를 제어합니다. 이러한 옵션을 이해하는 것은 견고한 스크립트를 작성하는 데 매우 중요합니다.

set -e (errexit)

#!/bin/bash

# Exit immediately if any command returns non-zero
set -e

echo "Starting..."
false  # This will cause the script to exit
echo "This won't be printed"

set -e의 주의사항

#!/bin/bash
set -e

# set -e does NOT exit in these cases:

# 1. Commands in conditions
if false; then
    echo "Won't execute"
fi
echo "Still running"

# 2. Commands with || or &&
false || echo "This runs"
echo "Still running"

# 3. Commands in a pipeline (except the last one, unless pipefail is set)
false | echo "Pipeline continues"
echo "Still running"

# 4. Commands in functions called in conditions
check_something() {
    false  # Won't exit the script if called in condition
    return 1
}

if check_something; then
    echo "Won't execute"
fi
echo "Still running after function"

# 5. Negated commands
! false  # Won't exit
echo "Still running after negation"

set -u (nounset)

#!/bin/bash

# Exit if accessing undefined variable
set -u

defined_var="hello"
echo "$defined_var"  # OK

# This will cause exit
# echo "$undefined_var"  # Error: undefined_var: unbound variable

# Safe way to check if variable is set
echo "${undefined_var:-default_value}"  # Prints: default_value

# Check if variable is set before using
if [ -n "${undefined_var+x}" ]; then
    echo "Variable is set: $undefined_var"
else
    echo "Variable is not set"
fi

# Another pattern: use empty string as default
value="${undefined_var:-}"
if [ -n "$value" ]; then
    echo "Value: $value"
else
    echo "Variable was undefined"
fi

set -o pipefail

#!/bin/bash

# Without pipefail
echo "Without pipefail:"
false | echo "Pipeline output"
echo "Exit status: $?"  # 0 (from echo)

# With pipefail
set -o pipefail
echo -e "\nWith pipefail:"
false | echo "Pipeline output"
echo "Exit status: $?"  # 1 (from false)

# Practical example
set -e
set -o pipefail

# This will exit the script if grep finds nothing
cat /var/log/syslog | grep "error" | head -10

# PIPESTATUS array contains exit codes of all pipeline commands
cat file.txt | grep "pattern" | sort | uniq
echo "Pipeline status: ${PIPESTATUS[@]}"
# Prints something like: 0 1 0 0
# (cat succeeded, grep failed, sort and uniq succeeded)

set 옵션 비교

옵션 설명 효과 사용 시기
set -e errexit 명령어 실패 시 종료 프로덕션 스크립트
set -u nounset 정의되지 않은 변수 접근 시 종료 오타를 조기에 발견
set -o pipefail pipefail 파이프라인 내 어떤 명령어라도 실패하면 실패 set -e와 함께
set -x xtrace 실행 전 명령어 출력 디버깅
set -v verbose 셸 입력 라인 출력 심층 디버깅
set -n noexec 명령어를 읽되 실행하지 않음 문법 검사
set -C noclobber 출력 리디렉션이 파일을 덮어쓰는 것을 방지 파일 보호

권장 스크립트 헤더

#!/bin/bash

# Strict mode
set -euo pipefail
IFS=$'\n\t'

# Now script will:
# - Exit on error (set -e)
# - Exit on undefined variable (set -u)
# - Exit if any pipeline command fails (set -o pipefail)
# - Use safe IFS (newline and tab only)

echo "Script running in strict mode"

set -e 일시적으로 비활성화

#!/bin/bash
set -e

# Method 1: Use || true
false || true  # Won't exit
echo "Still running"

# Method 2: Use explicit if
if command_that_might_fail; then
    echo "Success"
else
    echo "Failed, but handling it"
fi

# Method 3: Temporarily disable
set +e
command_that_might_fail
status=$?
set -e

if [ $status -ne 0 ]; then
    echo "Command failed with status $status"
fi

# Method 4: Use ! to negate (exit code becomes 0)
if ! command_that_might_fail; then
    echo "Command failed as expected"
fi

2. trap ERR

ERR 트랩은 명령어가 0이 아닌 종료 상태를 반환할 때 발생합니다 (set -e가 종료하지 않는 경우는 제외).

기본 ERR 트랩

#!/bin/bash
set -e

error_handler() {
    echo "Error occurred in script"
}

trap error_handler ERR

echo "Starting..."
false  # Triggers ERR trap
echo "This won't be reached"

에러 컨텍스트 가져오기

#!/bin/bash
set -e

error_handler() {
    local exit_code=$?
    local line_num=$1

    echo "========================================"
    echo "Error occurred!"
    echo "Exit code: $exit_code"
    echo "Line number: $line_num"
    echo "Command: $BASH_COMMAND"
    echo "========================================"

    # Exit with same code
    exit $exit_code
}

trap 'error_handler $LINENO' ERR

echo "Line 1"
echo "Line 2"
false  # Line 3 - will trigger error
echo "Line 4"

스택 트레이스 생성

#!/bin/bash
set -e

print_stack_trace() {
    local frame=0
    echo "Stack trace:"
    while caller $frame; do
        ((frame++))
    done | while read line func file; do
        echo "  at $func() in $file:$line"
    done
}

error_handler() {
    local exit_code=$?
    local line_num=$1

    echo "========================================"
    echo "ERROR: Command failed with exit code $exit_code"
    echo "  Line: $line_num"
    echo "  Command: $BASH_COMMAND"
    echo "========================================"

    print_stack_trace

    exit $exit_code
}

trap 'error_handler $LINENO' ERR

function level3() {
    echo "Level 3"
    false  # Error here
}

function level2() {
    echo "Level 2"
    level3
}

function level1() {
    echo "Level 1"
    level2
}

level1

고급 에러 핸들러

#!/bin/bash
set -euo pipefail

# Get detailed function stack
get_function_stack() {
    local i=0
    local stack=""

    while [ $i -lt ${#FUNCNAME[@]} ]; do
        local func="${FUNCNAME[$i]}"
        local line="${BASH_LINENO[$i-1]}"
        local src="${BASH_SOURCE[$i]}"

        # Skip the error handler itself
        if [ "$func" != "error_handler" ] && [ "$func" != "get_function_stack" ]; then
            stack="${stack}${func}() at ${src}:${line}\n"
        fi

        ((i++))
    done

    echo -e "$stack"
}

error_handler() {
    local exit_code=$?
    local line_num="${BASH_LINENO[0]}"
    local src="${BASH_SOURCE[1]}"

    echo "╔════════════════════════════════════════════════════════════"
    echo "║ ERROR DETECTED"
    echo "╠════════════════════════════════════════════════════════════"
    echo "║ Exit Code    : $exit_code"
    echo "║ Failed Command: $BASH_COMMAND"
    echo "║ Location     : $src:$line_num"
    echo "╠════════════════════════════════════════════════════════════"
    echo "║ Call Stack:"
    echo "╠════════════════════════════════════════════════════════════"
    get_function_stack
    echo "╚════════════════════════════════════════════════════════════"

    exit $exit_code
}

trap 'error_handler' ERR

# Test it
function inner_function() {
    echo "Inner function executing..."
    nonexistent_command  # This will fail
}

function outer_function() {
    echo "Outer function executing..."
    inner_function
}

outer_function

ERR vs EXIT 트랩

#!/bin/bash
set -e

# ERR trap: only on error
trap 'echo "ERR trap: Command failed"' ERR

# EXIT trap: always on exit
trap 'echo "EXIT trap: Script exiting"' EXIT

echo "Normal execution"
# On normal exit, only EXIT trap runs

# Uncomment to see both traps:
# false

3. 커스텀 에러 프레임워크

재사용 가능한 에러 처리 프레임워크를 구축하면 스크립트의 유지보수성이 향상됩니다.

에러 코드 열거형(Enum)

#!/bin/bash

# Define error codes as readonly constants
readonly E_SUCCESS=0
readonly E_GENERAL=1
readonly E_MISUSE=2
readonly E_NOINPUT=66
readonly E_NOUSER=67
readonly E_NOHOST=68
readonly E_UNAVAILABLE=69
readonly E_SOFTWARE=70
readonly E_OSERR=71
readonly E_OSFILE=72
readonly E_CANTCREAT=73
readonly E_IOERR=74
readonly E_TEMPFAIL=75
readonly E_PROTOCOL=76
readonly E_NOPERM=77
readonly E_CONFIG=78

# Map codes to messages
declare -A ERROR_MESSAGES=(
    [1]="General error"
    [2]="Misuse of shell command"
    [66]="Input file missing or unreadable"
    [67]="User does not exist"
    [68]="Host does not exist"
    [69]="Service unavailable"
    [70]="Internal software error"
    [71]="System error"
    [72]="Critical OS file missing"
    [73]="Cannot create output file"
    [74]="I/O error"
    [75]="Temporary failure"
    [76]="Protocol error"
    [77]="Permission denied"
    [78]="Configuration error"
)

# Get error message for code
get_error_message() {
    local code=$1
    echo "${ERROR_MESSAGES[$code]:-Unknown error}"
}

# Usage
echo "Error 66: $(get_error_message 66)"
echo "Error 77: $(get_error_message 77)"

에러 함수

#!/bin/bash
set -euo pipefail

# Color codes
readonly RED='\033[0;31m'
readonly YELLOW='\033[1;33m'
readonly NC='\033[0m'  # No Color

# Error levels
readonly ERROR_LEVEL_INFO=0
readonly ERROR_LEVEL_WARN=1
readonly ERROR_LEVEL_ERROR=2
readonly ERROR_LEVEL_FATAL=3

# Log with level
log_message() {
    local level=$1
    local message=$2
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')

    case $level in
        $ERROR_LEVEL_INFO)
            echo "[$timestamp] INFO: $message" >&2
            ;;
        $ERROR_LEVEL_WARN)
            echo -e "[$timestamp] ${YELLOW}WARN${NC}: $message" >&2
            ;;
        $ERROR_LEVEL_ERROR)
            echo -e "[$timestamp] ${RED}ERROR${NC}: $message" >&2
            ;;
        $ERROR_LEVEL_FATAL)
            echo -e "[$timestamp] ${RED}FATAL${NC}: $message" >&2
            ;;
    esac
}

# Convenience functions
info() { log_message $ERROR_LEVEL_INFO "$*"; }
warn() { log_message $ERROR_LEVEL_WARN "$*"; }
error() { log_message $ERROR_LEVEL_ERROR "$*"; }
fatal() { log_message $ERROR_LEVEL_FATAL "$*"; exit 1; }

# Die function with exit code
die() {
    local code=$1
    shift
    error "$@"
    exit $code
}

# Assert function
assert() {
    local condition=$1
    shift
    local message=$*

    if ! eval "$condition"; then
        die 1 "Assertion failed: $message"
    fi
}

# Usage examples
info "Script starting..."
warn "This is a warning"
error "This is an error (but doesn't exit)"
assert "[ -f /etc/passwd ]" "/etc/passwd must exist"
assert "[ 1 -eq 1 ]" "Math still works"
# fatal "Critical error - exiting"  # Uncomment to test
info "Script completed"

Try-Catch 시뮬레이션

#!/bin/bash

# Try-catch simulation using subshells
try() {
    # Execute commands in subshell
    # Return 0 if successful, 1 if any command fails
    ( eval "$*" )
    return $?
}

catch() {
    local exit_code=$1
    shift

    if [ $exit_code -ne 0 ]; then
        eval "$*"
        return 0
    fi

    return 1
}

# Usage
if try "echo 'Attempting operation'; false"; catch $? "echo 'Caught error!'"; then
    echo "Error was handled"
fi

# More complex example
perform_operation() {
    echo "Attempting risky operation..."

    # Simulate some work
    if [ $((RANDOM % 2)) -eq 0 ]; then
        echo "Operation succeeded"
        return 0
    else
        echo "Operation failed"
        return 1
    fi
}

if try "perform_operation"; catch $? "echo 'Operation failed, handling gracefully'"; then
    echo "Error was caught and handled"
else
    echo "Operation succeeded"
fi

# Alternative: using trap in subshell
try_with_trap() {
    (
        set -e
        trap 'return 1' ERR
        eval "$*"
    )
}

if try_with_trap "echo 'Working...'; false"; then
    echo "Success"
else
    echo "Caught error with trap method"
fi

완전한 에러 프레임워크

#!/bin/bash
set -euo pipefail

# ============================================================================
# ERROR HANDLING FRAMEWORK
# ============================================================================

readonly SCRIPT_NAME=$(basename "$0")
readonly SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

# Exit codes
readonly E_SUCCESS=0
readonly E_GENERAL=1
readonly E_INVALID_ARGS=2
readonly E_FILE_NOT_FOUND=66
readonly E_PERMISSION_DENIED=77

# Colors
readonly RED='\033[0;31m'
readonly YELLOW='\033[1;33m'
readonly GREEN='\033[0;32m'
readonly NC='\033[0m'

# Log file
LOG_FILE="${SCRIPT_DIR}/${SCRIPT_NAME}.log"

# Initialize logging
init_logging() {
    exec 3>&1 4>&2  # Save stdout and stderr
    exec 1> >(tee -a "$LOG_FILE")
    exec 2> >(tee -a "$LOG_FILE" >&2)
}

# Restore file descriptors
cleanup_logging() {
    exec 1>&3 2>&4
    exec 3>&- 4>&-
}

# Logging functions
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}

log_info() {
    log "INFO: $*"
}

log_warn() {
    log "${YELLOW}WARN${NC}: $*" >&2
}

log_error() {
    log "${RED}ERROR${NC}: $*" >&2
}

log_success() {
    log "${GREEN}SUCCESS${NC}: $*"
}

# Error handler
error_handler() {
    local exit_code=$?
    local line_num="${BASH_LINENO[0]}"

    log_error "Command failed with exit code $exit_code at line $line_num"
    log_error "Failed command: $BASH_COMMAND"

    cleanup_logging
    exit $exit_code
}

# Setup traps
setup_traps() {
    trap error_handler ERR
    trap cleanup_logging EXIT
}

# Die function
die() {
    local code=$1
    shift
    log_error "$*"
    exit $code
}

# Check if command exists
require_command() {
    local cmd=$1
    if ! command -v "$cmd" &>/dev/null; then
        die $E_GENERAL "Required command not found: $cmd"
    fi
}

# Check if file exists
require_file() {
    local file=$1
    if [ ! -f "$file" ]; then
        die $E_FILE_NOT_FOUND "Required file not found: $file"
    fi
}

# Check if directory exists
require_directory() {
    local dir=$1
    if [ ! -d "$dir" ]; then
        die $E_FILE_NOT_FOUND "Required directory not found: $dir"
    fi
}

# Validate number of arguments
validate_args() {
    local expected=$1
    local actual=$2

    if [ $actual -lt $expected ]; then
        die $E_INVALID_ARGS "Expected at least $expected arguments, got $actual"
    fi
}

# ============================================================================
# MAIN SCRIPT
# ============================================================================

main() {
    init_logging
    setup_traps

    log_info "Script started"

    # Validate requirements
    require_command "grep"
    require_command "sed"

    # Example operations
    log_info "Performing operations..."

    # This will succeed
    log_success "Operation completed successfully"

    # Uncomment to test error handling:
    # require_file "/nonexistent/file"
    # false

    log_info "Script completed successfully"
}

# Run main if not sourced
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
    main "$@"
fi

4. 방어적 코딩 패턴

방어적 코딩은 에러가 발생하기 전에 예방합니다.

입력 검증

#!/bin/bash

# Validate string is not empty
validate_not_empty() {
    local var=$1
    local name=$2

    if [ -z "$var" ]; then
        echo "Error: $name cannot be empty" >&2
        return 1
    fi
}

# Validate number
validate_number() {
    local var=$1
    local name=$2

    if ! [[ "$var" =~ ^[0-9]+$ ]]; then
        echo "Error: $name must be a positive integer" >&2
        return 1
    fi
}

# Validate range
validate_range() {
    local var=$1
    local min=$2
    local max=$3
    local name=$4

    if [ "$var" -lt "$min" ] || [ "$var" -gt "$max" ]; then
        echo "Error: $name must be between $min and $max" >&2
        return 1
    fi
}

# Validate email
validate_email() {
    local email=$1

    if ! [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
        echo "Error: Invalid email address" >&2
        return 1
    fi
}

# Validate IP address
validate_ip() {
    local ip=$1

    if ! [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
        echo "Error: Invalid IP address" >&2
        return 1
    fi

    # Check each octet is 0-255
    IFS='.' read -ra octets <<< "$ip"
    for octet in "${octets[@]}"; do
        if [ "$octet" -gt 255 ]; then
            echo "Error: Invalid IP address (octet > 255)" >&2
            return 1
        fi
    done
}

# Usage
username="john_doe"
validate_not_empty "$username" "username" || exit 1

age=25
validate_number "$age" "age" || exit 1
validate_range "$age" 0 150 "age" || exit 1

email="user@example.com"
validate_email "$email" || exit 1

ip="192.168.1.1"
validate_ip "$ip" || exit 1

echo "All validations passed"

명령어 존재 여부 확인

#!/bin/bash

# Method 1: Using command -v
check_command_v() {
    local cmd=$1
    if command -v "$cmd" &>/dev/null; then
        echo "$cmd is available"
        return 0
    else
        echo "$cmd is not available" >&2
        return 1
    fi
}

# Method 2: Using type
check_command_type() {
    local cmd=$1
    if type "$cmd" &>/dev/null; then
        echo "$cmd is available"
        return 0
    else
        echo "$cmd is not available" >&2
        return 1
    fi
}

# Method 3: Using which (less portable)
check_command_which() {
    local cmd=$1
    if which "$cmd" &>/dev/null; then
        echo "$cmd is available"
        return 0
    else
        echo "$cmd is not available" >&2
        return 1
    fi
}

# Require command with helpful message
require_command() {
    local cmd=$1
    local install_hint=${2:-""}

    if ! command -v "$cmd" &>/dev/null; then
        echo "Error: Required command '$cmd' not found" >&2
        if [ -n "$install_hint" ]; then
            echo "Install with: $install_hint" >&2
        fi
        exit 1
    fi
}

# Require one of multiple commands
require_one_of() {
    local found=0
    for cmd in "$@"; do
        if command -v "$cmd" &>/dev/null; then
            found=1
            break
        fi
    done

    if [ $found -eq 0 ]; then
        echo "Error: None of the required commands found: $*" >&2
        exit 1
    fi
}

# Usage
check_command_v "bash"
check_command_v "nonexistent_command" || echo "As expected"

require_command "grep"
require_command "curl" "apt-get install curl / brew install curl"

require_one_of "python3" "python"
require_one_of "vim" "nvim" "nano"

echo "All required commands are available"

안전한 임시 파일

#!/bin/bash

# Create temp file
create_temp_file() {
    local tmpfile
    tmpfile=$(mktemp) || {
        echo "Failed to create temp file" >&2
        return 1
    }
    echo "$tmpfile"
}

# Create temp directory
create_temp_dir() {
    local tmpdir
    tmpdir=$(mktemp -d) || {
        echo "Failed to create temp directory" >&2
        return 1
    }
    echo "$tmpdir"
}

# Create temp file with custom template
create_temp_file_template() {
    local prefix=$1
    local tmpfile
    tmpfile=$(mktemp "/tmp/${prefix}.XXXXXX") || {
        echo "Failed to create temp file" >&2
        return 1
    }
    echo "$tmpfile"
}

# Safe temp file with cleanup
safe_temp_file() {
    local tmpfile
    tmpfile=$(mktemp) || return 1

    # Register cleanup
    trap "rm -f '$tmpfile'" EXIT

    echo "$tmpfile"
}

# Usage
TMPFILE=$(create_temp_file)
trap "rm -f '$TMPFILE'" EXIT

echo "data" > "$TMPFILE"
cat "$TMPFILE"

TMPDIR=$(create_temp_dir)
trap "rm -rf '$TMPDIR'" EXIT

echo "Created temp dir: $TMPDIR"
touch "$TMPDIR/file1.txt"
touch "$TMPDIR/file2.txt"
ls -la "$TMPDIR"

# Custom template
LOGFILE=$(create_temp_file_template "myapp_log")
echo "Log file: $LOGFILE"

echo "Cleanup will happen automatically on exit"

락 파일

#!/bin/bash

# Simple lock file
acquire_lock_simple() {
    local lockfile=$1

    if [ -e "$lockfile" ]; then
        echo "Lock file exists, another instance may be running" >&2
        return 1
    fi

    echo $$ > "$lockfile"
    trap "rm -f '$lockfile'" EXIT
    return 0
}

# Atomic lock with mkdir
acquire_lock_atomic() {
    local lockfile=$1
    local max_attempts=${2:-10}
    local attempt=0

    while [ $attempt -lt $max_attempts ]; do
        if mkdir "$lockfile" 2>/dev/null; then
            trap "rmdir '$lockfile'" EXIT
            return 0
        fi

        ((attempt++))
        echo "Lock attempt $attempt/$max_attempts failed, retrying..." >&2
        sleep 1
    done

    echo "Failed to acquire lock after $max_attempts attempts" >&2
    return 1
}

# Lock with PID check
acquire_lock_with_pid_check() {
    local lockfile=$1

    if [ -e "$lockfile" ]; then
        local pid=$(cat "$lockfile" 2>/dev/null)

        # Check if process is still running
        if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
            echo "Another instance is running (PID: $pid)" >&2
            return 1
        else
            echo "Removing stale lock file" >&2
            rm -f "$lockfile"
        fi
    fi

    echo $$ > "$lockfile"
    trap "rm -f '$lockfile'" EXIT
    return 0
}

# Lock with flock (Linux)
acquire_lock_flock() {
    local lockfile=$1
    local fd=$2  # File descriptor to use

    # Open file descriptor
    eval "exec $fd>\"$lockfile\""

    # Try to acquire exclusive lock
    if flock -n "$fd"; then
        trap "flock -u '$fd'; exec $fd>&-; rm -f '$lockfile'" EXIT
        return 0
    else
        echo "Failed to acquire lock" >&2
        exec {fd}>&-
        return 1
    fi
}

# Usage
LOCKFILE="/tmp/myscript.lock"

if acquire_lock_with_pid_check "$LOCKFILE"; then
    echo "Lock acquired, doing work..."
    sleep 5
    echo "Work complete"
else
    echo "Could not acquire lock, exiting"
    exit 1
fi

# Alternative: using flock
# LOCKFILE="/tmp/myscript.flock"
# if acquire_lock_flock "$LOCKFILE" 200; then
#     echo "Flock acquired"
#     sleep 5
#     echo "Work complete"
# fi

안전한 파일 작업

#!/bin/bash

# Safe file copy with verification
safe_copy() {
    local src=$1
    local dst=$2

    # Check source exists
    if [ ! -f "$src" ]; then
        echo "Error: Source file does not exist: $src" >&2
        return 1
    fi

    # Check destination doesn't exist or confirm overwrite
    if [ -e "$dst" ]; then
        echo "Warning: Destination exists: $dst" >&2
        read -p "Overwrite? (y/n): " -n 1 -r
        echo
        if [[ ! $REPLY =~ ^[Yy]$ ]]; then
            echo "Copy cancelled" >&2
            return 1
        fi
    fi

    # Copy to temp file first
    local tmpfile="${dst}.tmp.$$"
    if ! cp "$src" "$tmpfile"; then
        echo "Error: Failed to copy file" >&2
        return 1
    fi

    # Verify copy
    if ! cmp -s "$src" "$tmpfile"; then
        echo "Error: Verification failed" >&2
        rm -f "$tmpfile"
        return 1
    fi

    # Move to final destination
    if ! mv "$tmpfile" "$dst"; then
        echo "Error: Failed to move temp file to destination" >&2
        rm -f "$tmpfile"
        return 1
    fi

    echo "Successfully copied $src to $dst"
    return 0
}

# Safe file write with backup
safe_write() {
    local file=$1
    local content=$2

    # Backup existing file
    if [ -f "$file" ]; then
        local backup="${file}.backup.$(date +%Y%m%d_%H%M%S)"
        if ! cp "$file" "$backup"; then
            echo "Error: Failed to create backup" >&2
            return 1
        fi
        echo "Created backup: $backup"
    fi

    # Write to temp file
    local tmpfile="${file}.tmp.$$"
    if ! echo "$content" > "$tmpfile"; then
        echo "Error: Failed to write to temp file" >&2
        return 1
    fi

    # Atomic move
    if ! mv "$tmpfile" "$file"; then
        echo "Error: Failed to move temp file" >&2
        rm -f "$tmpfile"
        return 1
    fi

    echo "Successfully wrote to $file"
    return 0
}

# Safe directory creation
safe_mkdir() {
    local dir=$1
    local mode=${2:-755}

    if [ -e "$dir" ]; then
        if [ ! -d "$dir" ]; then
            echo "Error: Path exists but is not a directory: $dir" >&2
            return 1
        fi
        echo "Directory already exists: $dir"
        return 0
    fi

    if ! mkdir -p -m "$mode" "$dir"; then
        echo "Error: Failed to create directory: $dir" >&2
        return 1
    fi

    echo "Created directory: $dir"
    return 0
}

# Usage
echo "test content" > /tmp/source.txt
safe_copy /tmp/source.txt /tmp/dest.txt
safe_write /tmp/output.txt "Hello, World!"
safe_mkdir /tmp/test_dir 755

5. ShellCheck 정적 분석

ShellCheck은 셸 스크립트에서 버그를 찾아내는 정적 분석 도구입니다.

일반적인 ShellCheck 경고

#!/bin/bash

# SC2086: Double quote to prevent word splitting
file="my file.txt"
cat $file        # BAD: will try to cat "my" and "file.txt"
cat "$file"      # GOOD: treats as single argument

# SC2046: Quote command substitution to prevent word splitting
for file in $(ls *.txt); do  # BAD
    echo "$file"
done

for file in *.txt; do        # GOOD
    echo "$file"
done

# SC2006: Use $(...) instead of `...`
result=`command`             # BAD (deprecated)
result=$(command)            # GOOD

# SC2155: Separate declaration and assignment to avoid masking return value
declare output=$(command)    # BAD: masks command's exit code
declare output               # GOOD
output=$(command)

# SC2164: Use || exit after cd in case it fails
cd /some/directory           # BAD: script continues if cd fails
cd /some/directory || exit   # GOOD

# SC2166: Prefer -a/-o over &&/|| in [ ] expressions
[ -f file && -r file ]       # BAD: doesn't work
[ -f file ] && [ -r file ]   # GOOD
[ -f file -a -r file ]       # GOOD (but [ ] is deprecated, use [[ ]])

# SC2006: Use [[ ]] instead of [ ] for better error handling
if [ $var = "value" ]; then  # BAD: fails if var is empty
    echo "match"
fi
if [[ $var = "value" ]]; then  # GOOD: handles empty var
    echo "match"
fi

# SC2143: Use grep -q instead of comparing output
if [ $(grep pattern file | wc -l) -gt 0 ]; then  # BAD
    echo "found"
fi
if grep -q pattern file; then  # GOOD
    echo "found"
fi

# SC2069: Redirecting stdout to stderr correctly
command 2>&1 >/dev/null      # BAD: wrong order
command >/dev/null 2>&1      # GOOD

# SC2181: Check exit code directly instead of $?
command
if [ $? -eq 0 ]; then        # BAD (acceptable but not ideal)
    echo "success"
fi
if command; then             # GOOD
    echo "success"
fi

ShellCheck 통합

#!/bin/bash

# Install ShellCheck
# Ubuntu/Debian: apt-get install shellcheck
# macOS: brew install shellcheck
# Or download from: https://www.shellcheck.net/

# Check a script
shellcheck script.sh

# Check with specific severity
shellcheck --severity=warning script.sh

# Output in different formats
shellcheck --format=gcc script.sh    # GCC-style
shellcheck --format=json script.sh   # JSON format
shellcheck --format=tty script.sh    # Colored terminal output

# Exclude specific warnings
shellcheck --exclude=SC2086,SC2046 script.sh

# Check all scripts in directory
find . -name '*.sh' -exec shellcheck {} \;

ShellCheck 설정

#!/bin/bash

# .shellcheckrc file (place in project root or ~/.shellcheckrc)
# Disable specific checks globally
disable=SC2086,SC2046

# Set shell dialect
shell=bash

# Enable optional checks
enable=quote-safe-variables

# Example .shellcheckrc:
cat > .shellcheckrc << 'EOF'
# Disable word splitting warnings
disable=SC2086

# Enable all optional checks
enable=all

# Source paths
source-path=SCRIPTDIR
EOF

코드 내 경고 억제

#!/bin/bash

# Suppress for next line
# shellcheck disable=SC2086
echo $unquoted_var

# Suppress for entire file
# shellcheck disable=SC2086,SC2046

# Suppress with explanation
# shellcheck disable=SC2086  # Intentional word splitting here
for word in $sentence; do
    echo "$word"
done

# Suppress for a block
# shellcheck disable=SC2086
{
    echo $var1
    echo $var2
    echo $var3
}
# shellcheck enable=SC2086

6. 디버깅 기법

효과적인 디버깅 기법은 문제를 신속하게 식별하고 수정하는 데 도움이 됩니다.

set -x (xtrace)

#!/bin/bash

# Enable xtrace (print commands before execution)
set -x

echo "This will be traced"
var="hello"
echo "$var"

# Disable xtrace
set +x

echo "This won't be traced"

향상된 추적을 위한 커스텀 PS4

#!/bin/bash

# Default PS4 is '+ '
# Customize it for more information

# Show line number
export PS4='+(${LINENO}): '
set -x
echo "Line number shown"
var="test"
echo "$var"
set +x

# Show line number and function name
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
set -x

my_function() {
    echo "Inside function"
    local x=10
    echo "$x"
}

my_function
set +x

# Show timestamp, line, and function
export PS4='[$(date +%T)] ${BASH_SOURCE}:${LINENO}: ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
set -x
echo "Timestamped trace"
set +x

선택적 디버깅

#!/bin/bash

# Debug flag
DEBUG=${DEBUG:-0}

debug() {
    if [ "$DEBUG" = "1" ]; then
        echo "DEBUG: $*" >&2
    fi
}

debug_start() {
    if [ "$DEBUG" = "1" ]; then
        set -x
    fi
}

debug_end() {
    if [ "$DEBUG" = "1" ]; then
        set +x
    fi
}

# Usage
debug "Script starting"

echo "Normal execution"

debug_start
# This section will be traced if DEBUG=1
var="test"
echo "$var"
result=$((var + 10))
debug_end

echo "More normal execution"

# Run with: DEBUG=1 ./script.sh

set -v (verbose)

#!/bin/bash

# Verbose mode: print shell input lines
set -v

# This prints the line itself before executing
echo "Hello"
var="world"
echo "$var"

set +v
echo "Verbose mode off"

Bash 디버거 (bashdb)

#!/bin/bash

# Install bashdb
# Ubuntu/Debian: apt-get install bashdb
# Or download from: http://bashdb.sourceforge.net/

# Run script with debugger
# bashdb script.sh

# Debugger commands:
# n     - next line
# s     - step into function
# c     - continue until breakpoint
# l     - list source code
# p var - print variable value
# b N   - set breakpoint at line N
# q     - quit debugger

# Example script to debug
function calculate() {
    local a=$1
    local b=$2
    local result=$((a + b))
    echo "$result"
}

x=10
y=20
sum=$(calculate $x $y)
echo "Sum: $sum"

# Run with: bashdb this_script.sh
# Then use 'n' to step through, 'p x' to print variables, etc.

디버그 로깅

#!/bin/bash

# Debug levels
readonly DEBUG_NONE=0
readonly DEBUG_ERROR=1
readonly DEBUG_WARN=2
readonly DEBUG_INFO=3
readonly DEBUG_TRACE=4

DEBUG_LEVEL=${DEBUG_LEVEL:-$DEBUG_INFO}

debug_log() {
    local level=$1
    shift
    local message=$*

    if [ "$level" -le "$DEBUG_LEVEL" ]; then
        local level_name
        case $level in
            $DEBUG_ERROR) level_name="ERROR" ;;
            $DEBUG_WARN)  level_name="WARN"  ;;
            $DEBUG_INFO)  level_name="INFO"  ;;
            $DEBUG_TRACE) level_name="TRACE" ;;
        esac

        echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level_name] $message" >&2
    fi
}

error() { debug_log $DEBUG_ERROR "$*"; }
warn()  { debug_log $DEBUG_WARN "$*"; }
info()  { debug_log $DEBUG_INFO "$*"; }
trace() { debug_log $DEBUG_TRACE "$*"; }

# Usage
error "This is an error"
warn "This is a warning"
info "This is info"
trace "This is trace"

# Run with different levels:
# DEBUG_LEVEL=1 ./script.sh  # Only errors
# DEBUG_LEVEL=2 ./script.sh  # Errors and warnings
# DEBUG_LEVEL=4 ./script.sh  # Everything

7. 로깅 프레임워크

프로덕션 스크립트를 위한 포괄적인 로깅 프레임워크입니다.

간단한 로깅

#!/bin/bash

# Log to file
LOG_FILE="/var/log/myscript.log"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}

log "Script started"
log "Processing data..."
log "Script completed"

다단계 로깅

#!/bin/bash

# Log levels
readonly LOG_LEVEL_DEBUG=0
readonly LOG_LEVEL_INFO=1
readonly LOG_LEVEL_WARN=2
readonly LOG_LEVEL_ERROR=3
readonly LOG_LEVEL_FATAL=4

# Current log level
LOG_LEVEL=${LOG_LEVEL:-$LOG_LEVEL_INFO}

# Log file
LOG_FILE="${LOG_FILE:-/var/log/myscript.log}"

# Log function
log_message() {
    local level=$1
    local level_num=$2
    shift 2
    local message=$*

    # Only log if level is high enough
    if [ "$level_num" -ge "$LOG_LEVEL" ]; then
        local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
        echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
    fi
}

# Convenience functions
debug() { log_message "DEBUG" $LOG_LEVEL_DEBUG "$*"; }
info()  { log_message "INFO"  $LOG_LEVEL_INFO "$*"; }
warn()  { log_message "WARN"  $LOG_LEVEL_WARN "$*"; }
error() { log_message "ERROR" $LOG_LEVEL_ERROR "$*"; }
fatal() { log_message "FATAL" $LOG_LEVEL_FATAL "$*"; exit 1; }

# Usage
debug "Debug message"
info "Info message"
warn "Warning message"
error "Error message"
# fatal "Fatal error"  # Uncomment to test

파일 및 콘솔 로깅

#!/bin/bash

LOG_FILE="/var/log/myscript.log"

# Setup logging
setup_logging() {
    # Create log file if it doesn't exist
    touch "$LOG_FILE" 2>/dev/null || {
        echo "Cannot create log file: $LOG_FILE" >&2
        LOG_FILE="/tmp/myscript.log"
        echo "Using temporary log file: $LOG_FILE" >&2
    }

    # Redirect stdout and stderr to log file AND console
    exec > >(tee -a "$LOG_FILE")
    exec 2> >(tee -a "$LOG_FILE" >&2)
}

setup_logging

echo "This goes to both console and log file"
echo "Error message" >&2

로그 회전

#!/bin/bash

LOG_FILE="/var/log/myscript.log"
MAX_LOG_SIZE=$((10 * 1024 * 1024))  # 10 MB
MAX_LOG_FILES=5

rotate_logs() {
    local log_file=$1
    local max_size=$2
    local max_files=$3

    # Check if rotation needed
    if [ ! -f "$log_file" ]; then
        return 0
    fi

    local size=$(stat -f%z "$log_file" 2>/dev/null || stat -c%s "$log_file" 2>/dev/null)
    if [ "$size" -lt "$max_size" ]; then
        return 0
    fi

    # Rotate old logs
    local i=$max_files
    while [ $i -gt 1 ]; do
        local prev=$((i - 1))
        if [ -f "${log_file}.${prev}" ]; then
            mv "${log_file}.${prev}" "${log_file}.${i}"
        fi
        ((i--))
    done

    # Move current log
    mv "$log_file" "${log_file}.1"
    touch "$log_file"

    echo "Log rotated at $(date)" >> "$log_file"
}

# Check and rotate logs before starting
rotate_logs "$LOG_FILE" "$MAX_LOG_SIZE" "$MAX_LOG_FILES"

# Now log normally
echo "Log entry at $(date)" >> "$LOG_FILE"

구조화된 로깅

#!/bin/bash

# Structured logging with key=value pairs
structured_log() {
    local level=$1
    shift

    local timestamp=$(date -u '+%Y-%m-%dT%H:%M:%S.%3NZ')
    local pid=$$
    local script=$(basename "$0")

    # Start with standard fields
    local log_entry="timestamp=$timestamp level=$level pid=$pid script=$script"

    # Add custom fields
    while [ $# -gt 0 ]; do
        log_entry="$log_entry $1"
        shift
    done

    echo "$log_entry"
}

# Usage
structured_log INFO "event=startup" "version=1.0.0"
structured_log INFO "event=processing" "user=john" "action=login" "status=success"
structured_log ERROR "event=error" "error=connection_failed" "host=db.example.com"

# Output:
# timestamp=2024-01-15T10:30:45.123Z level=INFO pid=12345 script=myscript.sh event=startup version=1.0.0
# timestamp=2024-01-15T10:30:46.234Z level=INFO pid=12345 script=myscript.sh event=processing user=john action=login status=success
# timestamp=2024-01-15T10:30:47.345Z level=ERROR pid=12345 script=myscript.sh event=error error=connection_failed host=db.example.com

# This format is easily parseable by log analysis tools

8. 연습 문제

문제 1: 견고한 파일 프로세서

포괄적인 에러 처리를 갖춘 여러 파일을 처리하는 스크립트를 작성하세요. 스크립트는 다음을 수행해야 합니다: - 디렉토리 경로를 인수로 받음 - 디렉토리가 존재하고 읽기 가능한지 검증 - 디렉토리 내 각 .txt 파일 처리 - set -euo pipefail 사용 - trap을 사용한 적절한 에러 처리 구현 - 모든 작업(성공 및 실패)을 로그 파일에 기록 - 종료 시(정상 또는 에러) 임시 파일 정리 - Ctrl+C를 정리와 함께 우아하게 처리

문제 2: 입력 검증 라이브러리

다음을 검증하는 함수를 가진 재사용 가능한 입력 검증 라이브러리를 생성하세요: - 이메일 주소(RFC 호환 정규식) - 전화번호(미국 형식: (123) 456-7890) - URL(http/https) - 신용카드 번호(Luhn 알고리즘) - 날짜(YYYY-MM-DD 형식, 유효한 날짜) - 각 함수는 유효하면 0, 무효하면 1 반환 - 무엇이 잘못되었는지 설명하는 에러 메시지 포함 - 각 검증 함수에 대한 테스트 작성

문제 3: 에러 복구를 갖춘 데이터베이스 백업

다음을 수행하는 백업 스크립트를 작성하세요: - PostgreSQL 데이터베이스에 연결 - pg_dump로 백업 생성 - 백업 압축 - S3에 업로드(또는 원격 서버에 복사) - 백업 무결성 검증 - 각 단계를 처리하기 위해 try-catch 패턴 사용 - 지수 백오프로 실패한 작업을 최대 3회 재시도 - 성공 또는 실패 시 알림(이메일 또는 로그) 전송 - 임시 파일의 적절한 정리 구현 - 구조화된 로깅 사용

문제 4: ShellCheck CI 통합

다음을 수행하는 CI 스크립트를 생성하세요: - 리포지토리에서 모든 .sh 파일 찾기 - 각 파일에 shellcheck 실행 - 결과 수집 및 포맷 - 에러(경고 아님)가 발견되면 CI 실패 - 요약 리포트 생성 - 선택적으로 HTML 리포트 생성 - 특정 파일/디렉토리 제외 허용 - 커스텀 shellcheck 설정 지원

문제 5: 디버그 모드 프레임워크

다음을 수행하는 포괄적인 디버그 모드 프레임워크를 구현하세요: - DEBUG 환경 변수를 레벨과 함께 받음: 0(없음), 1(에러), 2(경고), 3(정보), 4(디버그), 5(추적) - 각 레벨에 다른 색상 사용 - 추적 출력에 타임스탬프, 라인 번호, 함수 이름 포함 - 콘솔 및 파일 모두에 로그 - 로그 회전 구현 - debug(), info(), warn(), error(), fatal() 함수 제공 - key=value 쌍으로 구조화된 로깅 지원 - 코드의 특정 섹션에 대해 활성화/비활성화 가능 - 성능 타이밍(작업 지속 시간) 포함


이전: 09_Process_Management.md | 다음: 11_Argument_Parsing.md

to navigate between lessons