매개변수 확장과 변수 속성 ⭐⭐

매개변수 확장과 변수 속성 ⭐⭐

이전: 01_Shell_Fundamentals.md | 다음: 03_Arrays_and_Data.md


이 레슨에서는 외부 도구 없이 쉘에서 직접 변수를 조작할 수 있는 강력한 기능인 bash 매개변수 확장(Parameter Expansion)을 탐구합니다. 문자열 연산, 기본값, 변수 속성, 그리고 실용적인 패턴을 다룹니다.

1. 문자열 제거 연산자

매개변수 확장은 문자열의 시작 또는 끝에서 패턴을 제거하는 내장 연산자를 제공합니다. 이는 sed나 외부 도구를 사용하는 것보다 빠릅니다.

시작 부분에서 제거 (# and ##)

#!/usr/bin/env bash

# # removes shortest match from start
# ## removes longest match from start

filepath="/usr/local/bin/script.sh"

# Remove shortest match from start
echo "${filepath#*/}"      # usr/local/bin/script.sh
echo "${filepath#*/*/}"    # local/bin/script.sh

# Remove longest match from start
echo "${filepath##*/}"     # script.sh (basename)
echo "${filepath##*.}"     # sh (extension)

# Practical: extract filename from path
filename="${filepath##*/}"
echo "Filename: $filename"

# Remove path, keep filename
url="https://example.com/path/to/file.tar.gz"
file="${url##*/}"
echo "File: $file"  # file.tar.gz

끝 부분에서 제거 (% and %%)

#!/usr/bin/env bash

# % removes shortest match from end
# %% removes longest match from end

filepath="/usr/local/bin/script.sh"

# Remove shortest match from end
echo "${filepath%/*}"      # /usr/local/bin (dirname)
echo "${filepath%.*}"      # /usr/local/bin/script (remove extension)

# Remove longest match from end
echo "${filepath%%/*}"     # (empty, removes everything)
echo "${filepath%%.*}"     # /usr/local/bin/script

# Practical: get directory from path
directory="${filepath%/*}"
echo "Directory: $directory"

# Remove extension
filename="archive.tar.gz"
base="${filename%.*}"      # archive.tar
base="${filename%%.*}"     # archive (remove all extensions)
echo "Base: $base"

비교 표

연산자 방향 매칭 예제 결과
${var#pattern}` | 시작부터 | 최단 | `${path#*/} 첫 디렉터리 제거
${var##pattern}` | 시작부터 | 최장 | `${path##*/} Basename
${var%pattern}` | 끝부터 | 최단 | `${file%.*} 확장자 제거
${var%%pattern}` | 끝부터 | 최장 | `${file%%.*} 모든 확장자 제거

실용 예제

#!/usr/bin/env bash

# Extract components from URLs
url="https://user@example.com:8080/path/to/resource.html?query=1#anchor"

# Remove protocol
no_protocol="${url#*://}"
echo "No protocol: $no_protocol"
# user@example.com:8080/path/to/resource.html?query=1#anchor

# Extract domain
temp="${url#*://}"
domain="${temp%%/*}"
echo "Domain: $domain"  # user@example.com:8080

# Extract path
temp="${url#*://}"
temp="${temp#*/}"
path="/${temp%%\?*}"
echo "Path: $path"  # /path/to/resource.html

# Remove query string and anchor
clean_url="${url%%\?*}"
clean_url="${clean_url%%#*}"
echo "Clean URL: $clean_url"
# https://user@example.com:8080/path/to/resource.html

# Extract filename without extension from path
fullpath="/var/log/nginx/access.log.2024-02-13"
filename="${fullpath##*/}"      # access.log.2024-02-13
basename="${filename%%.*}"      # access
extension="${filename#*.}"      # log.2024-02-13
first_ext="${filename##*.}"     # 2024-02-13

echo "File: $filename"
echo "Base: $basename"
echo "Full ext: $extension"
echo "Last ext: $first_ext"

일괄 파일 처리

#!/usr/bin/env bash

# Remove extensions from multiple files
for file in *.tar.gz; do
    base="${file%.tar.gz}"
    echo "Extracting $file to $base/"
    mkdir -p "$base"
    tar xzf "$file" -C "$base"
done

# Convert file extensions
shopt -s nullglob
for file in *.jpeg; do
    newname="${file%.jpeg}.jpg"
    mv -v "$file" "$newname"
done

# Clean up numbered backups
for file in *.txt.{1..10}; do
    [ -f "$file" ] || continue
    original="${file%.*}"  # Remove .1, .2, etc.
    echo "Backup: $file -> original: $original"
done

2. 검색 및 치환

매개변수 확장은 패턴 검색과 치환을 지원하며, 간단한 문자열 연산을 위한 sed의 경량 대안을 제공합니다.

기본 검색 및 치환

#!/usr/bin/env bash

# / replaces first occurrence
# // replaces all occurrences

text="Hello World World World"

# Replace first occurrence
echo "${text/World/Bash}"     # Hello Bash World World

# Replace all occurrences
echo "${text//World/Bash}"    # Hello Bash Bash Bash

# Replace at start (prefix with #)
text="prefixSuffixprefix"
echo "${text/#prefix/START}"  # STARTSuffixprefix

# Replace at end (prefix with %)
echo "${text/%prefix/END}"    # prefixSuffixEND

# Delete pattern (replace with empty)
echo "${text//prefix/}"       # Suffix

패턴 매칭

#!/usr/bin/env bash

# Use glob patterns in search
path="/usr/local/bin:/usr/bin:/bin"

# Replace first path separator
echo "${path/:/ : }"          # /usr/local/bin : /usr/bin:/bin

# Replace all path separators
echo "${path//:/ : }"         # /usr/local/bin : /usr/bin : /bin

# Remove all digits
version="bash-5.1.16-release"
echo "${version//[0-9]/}"     # bash-..-release

# Remove all non-alphanumeric
string="Hello, World! 123"
echo "${string//[^a-zA-Z0-9]/}"  # HelloWorld123

실용 응용

#!/usr/bin/env bash

# Sanitize filenames
sanitize_filename() {
    local filename="$1"

    # Replace spaces with underscores
    filename="${filename// /_}"

    # Remove special characters
    filename="${filename//[^a-zA-Z0-9._-]/}"

    # Replace multiple underscores with single
    while [[ "$filename" =~ __ ]]; do
        filename="${filename//__/_}"
    done

    echo "$filename"
}

echo "$(sanitize_filename 'My Document (draft).txt')"
# My_Document_draft.txt

# Convert between separators
csv_to_tsv() {
    local line="$1"
    echo "${line//,/$'\t'}"
}

tsv_to_csv() {
    local line="$1"
    echo "${line//$'\t'/,}"
}

data="name,age,city"
echo "CSV: $data"
tsv="$(csv_to_tsv "$data")"
echo "TSV: $tsv"
echo "Back to CSV: $(tsv_to_csv "$tsv")"

# URL encoding (basic)
urlencode() {
    local string="$1"
    string="${string// /%20}"
    string="${string//&/%26}"
    string="${string//=/%3D}"
    string="${string//\?/%3F}"
    echo "$string"
}

url="search?q=hello world&lang=en"
echo "Encoded: $(urlencode "$url")"
# search%3Fq%3Dhello%20world%26lang%3Den

일괄 이름 변경

#!/usr/bin/env bash

# Rename files by replacing patterns
batch_rename() {
    local pattern="$1"
    local replacement="$2"
    local extension="${3:-*}"

    shopt -s nullglob
    for file in *."$extension"; do
        # Skip if pattern not found
        [[ "$file" != *"$pattern"* ]] && continue

        # Generate new name
        newname="${file//$pattern/$replacement}"

        # Rename if new name is different
        if [[ "$file" != "$newname" ]]; then
            echo "Renaming: $file -> $newname"
            mv -n "$file" "$newname"
        fi
    done
}

# Usage examples:
# batch_rename "draft" "final" "txt"
# batch_rename " " "_" "md"
# batch_rename "2024" "2025" "*"

# Remove date stamps from filenames
remove_datestamp() {
    shopt -s nullglob
    for file in *_[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9].*; do
        # Remove pattern _YYYY-MM-DD
        newname="${file/_[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/}"
        echo "Renaming: $file -> $newname"
        mv -n "$file" "$newname"
    done
}

# Lowercase all extensions
lowercase_extensions() {
    shopt -s nullglob
    for file in *.*; do
        name="${file%.*}"
        ext="${file##*.}"
        newext="${ext,,}"  # Lowercase (see section 5)

        if [[ "$ext" != "$newext" ]]; then
            echo "Renaming: $file -> $name.$newext"
            mv -n "$file" "$name.$newext"
        fi
    done
}

3. 부분 문자열 및 길이 연산

매개변수 확장을 사용하여 문자열의 일부를 추출하고 문자열 길이를 결정합니다.

문자열 길이

#!/usr/bin/env bash

# ${#var} returns string length

string="Hello, World!"
echo "Length: ${#string}"  # 13

# Length of empty string
empty=""
echo "Empty length: ${#empty}"  # 0

# Validate input length
validate_password() {
    local password="$1"
    local min_length=8
    local max_length=64

    if [ ${#password} -lt $min_length ]; then
        echo "Password too short (min: $min_length)" >&2
        return 1
    fi

    if [ ${#password} -gt $max_length ]; then
        echo "Password too long (max: $max_length)" >&2
        return 1
    fi

    echo "Password length OK: ${#password} characters"
    return 0
}

validate_password "abc"      # Too short
validate_password "SecureP@ssw0rd"  # OK

부분 문자열 추출

#!/usr/bin/env bash

# ${var:offset:length}
# Offset is 0-indexed
# Negative offset counts from end (bash 4.2+)

string="Hello, World!"

# Extract from position 0, length 5
echo "${string:0:5}"      # Hello

# Extract from position 7 to end
echo "${string:7}"        # World!

# Extract last 6 characters (negative offset)
echo "${string: -6}"      # World!
echo "${string:(-6)}"     # World! (alternative syntax)

# Extract 5 characters starting 6 from end
echo "${string: -6:5}"    # World

# Practical: extract date components
date="2024-02-13"
year="${date:0:4}"
month="${date:5:2}"
day="${date:8:2}"

echo "Year: $year, Month: $month, Day: $day"
# Year: 2024, Month: 02, Day: 13

# Extract time components
timestamp="2024-02-13T14:30:45Z"
time="${timestamp:11:8}"
echo "Time: $time"  # 14:30:45

# Truncate long strings
truncate() {
    local string="$1"
    local max_length="${2:-50}"

    if [ ${#string} -le $max_length ]; then
        echo "$string"
    else
        echo "${string:0:$max_length}..."
    fi
}

long_text="This is a very long string that needs to be truncated for display purposes."
echo "$(truncate "$long_text" 30)"
# This is a very long string th...

문자열 패딩

#!/usr/bin/env bash

# Pad string to specific width
pad_left() {
    local string="$1"
    local width="$2"
    local padchar="${3:- }"  # Default to space

    local len=${#string}
    if [ $len -ge $width ]; then
        echo "$string"
        return
    fi

    local padding_needed=$((width - len))
    printf "%${padding_needed}s%s" "" "$string" | tr ' ' "$padchar"
}

pad_right() {
    local string="$1"
    local width="$2"
    local padchar="${3:- }"

    local len=${#string}
    if [ $len -ge $width ]; then
        echo "$string"
        return
    fi

    printf "%-${width}s" "$string" | tr ' ' "$padchar"
}

# Format table
echo "$(pad_right 'Name' 20) $(pad_left 'Value' 10)"
echo "$(pad_right '----' 20 '-') $(pad_left '-----' 10 '-')"
echo "$(pad_right 'CPU Usage' 20) $(pad_left '45%' 10)"
echo "$(pad_right 'Memory' 20) $(pad_left '2.3 GB' 10)"
echo "$(pad_right 'Disk' 20) $(pad_left '123 GB' 10)"

# Zero-pad numbers
zero_pad() {
    local number="$1"
    local width="${2:-3}"
    printf "%0${width}d" "$number"
}

echo "File_$(zero_pad 5 3).txt"     # File_005.txt
echo "File_$(zero_pad 42 4).txt"    # File_0042.txt

실용 문자열 처리

#!/usr/bin/env bash

# Parse log timestamps
parse_log_line() {
    local line="$1"
    # Format: [2024-02-13 14:30:45] INFO: Message

    # Extract timestamp (chars 1-19)
    local timestamp="${line:1:19}"

    # Extract level (after timestamp + 2 chars for '] ')
    local rest="${line:22}"
    local level="${rest%%:*}"

    # Extract message (after level + ': ')
    local message="${rest#*: }"

    echo "Timestamp: $timestamp"
    echo "Level: $level"
    echo "Message: $message"
}

log_line="[2024-02-13 14:30:45] ERROR: Connection timeout"
parse_log_line "$log_line"

# Credit card masking
mask_credit_card() {
    local cc="$1"
    # Show only last 4 digits
    local masked="${cc:0:${#cc}-4}"
    masked="${masked//[0-9]/X}"
    local visible="${cc: -4}"
    echo "${masked}${visible}"
}

echo "$(mask_credit_card '1234567890123456')"
# XXXXXXXXXXXX3456

# Extract domain from email
extract_domain() {
    local email="$1"
    echo "${email#*@}"
}

extract_username() {
    local email="$1"
    echo "${email%@*}"
}

email="user@example.com"
echo "Username: $(extract_username "$email")"
echo "Domain: $(extract_domain "$email")"

4. 기본값과 대체값

매개변수 확장은 정의되지 않았거나 비어있는 변수를 우아하게 처리하는 연산자를 제공합니다.

기본값 연산자

#!/usr/bin/env bash

# ${var:-default}  Use default if unset or empty
# ${var-default}   Use default if unset (not if empty)
# ${var:=default}  Assign and use default if unset or empty
# ${var=default}   Assign and use default if unset (not if empty)
# ${var:+alternate} Use alternate if set and not empty
# ${var+alternate}  Use alternate if set (even if empty)
# ${var:?error}    Error if unset or empty
# ${var?error}     Error if unset (not if empty)

# :- Use default value
unset name
echo "${name:-Anonymous}"  # Anonymous (name still unset)
echo "$name"               # (empty)

name=""
echo "${name:-Anonymous}"  # Anonymous (empty treated as unset)

name="John"
echo "${name:-Anonymous}"  # John

# - Use default only if truly unset
unset value
echo "${value-default}"    # default
value=""
echo "${value-default}"    # (empty string, not default)

# := Assign default value
unset port
echo "${port:=8080}"       # 8080
echo "$port"               # 8080 (now assigned)

# :+ Use alternate value if set
unset debug
echo "Debug: ${debug:+enabled}"   # Debug: (empty)

debug="1"
echo "Debug: ${debug:+enabled}"   # Debug: enabled

# :? Error if unset
check_required() {
    local file="${1:?Error: filename required}"
    echo "Processing: $file"
}

# check_required           # Error: filename required
check_required "data.txt"  # Processing: data.txt

기본값을 사용한 설정

#!/usr/bin/env bash

# Configuration pattern with environment variables and defaults
DB_HOST="${DB_HOST:-localhost}"
DB_PORT="${DB_PORT:-5432}"
DB_NAME="${DB_NAME:-myapp}"
DB_USER="${DB_USER:-postgres}"
DB_PASSWORD="${DB_PASSWORD:?Error: DB_PASSWORD must be set}"

LOG_LEVEL="${LOG_LEVEL:-INFO}"
LOG_FILE="${LOG_FILE:-/var/log/app.log}"
MAX_CONNECTIONS="${MAX_CONNECTIONS:-100}"
TIMEOUT="${TIMEOUT:-30}"

cat <<EOF
Database Configuration:
  Host: $DB_HOST
  Port: $DB_PORT
  Database: $DB_NAME
  User: $DB_USER

Application Settings:
  Log Level: $LOG_LEVEL
  Log File: $LOG_FILE
  Max Connections: $MAX_CONNECTIONS
  Timeout: ${TIMEOUT}s
EOF

# Connect to database
connect_db() {
    psql -h "$DB_HOST" -p "$DB_PORT" -d "$DB_NAME" -U "$DB_USER"
}

# Usage with environment variables:
# DB_PASSWORD=secret ./script.sh
# DB_HOST=prod-db DB_PORT=5433 DB_PASSWORD=secret ./script.sh

선택적 기능

#!/usr/bin/env bash

# Enable optional features based on environment
VERBOSE="${VERBOSE:-}"
DEBUG="${DEBUG:-}"
DRY_RUN="${DRY_RUN:-}"

log_verbose() {
    [ -n "$VERBOSE" ] && echo "[VERBOSE] $*" >&2
}

log_debug() {
    [ -n "$DEBUG" ] && echo "[DEBUG] $*" >&2
}

execute() {
    local cmd="$*"

    log_debug "Command: $cmd"

    if [ -n "$DRY_RUN" ]; then
        echo "[DRY-RUN] Would execute: $cmd"
        return 0
    fi

    "$@"
}

# Main logic
log_verbose "Starting process..."
log_debug "Working directory: $(pwd)"

execute rm -f temp.txt
execute mkdir -p output

log_verbose "Process complete"

# Usage:
# ./script.sh                           # Normal mode
# VERBOSE=1 ./script.sh                 # Verbose mode
# DEBUG=1 ./script.sh                   # Debug mode
# DRY_RUN=1 ./script.sh                 # Dry-run mode
# VERBOSE=1 DEBUG=1 DRY_RUN=1 ./script.sh  # All flags

필수 변수 패턴

#!/usr/bin/env bash

# Check multiple required variables
check_required() {
    local missing=()

    for var in "$@"; do
        if [ -z "${!var}" ]; then
            missing+=("$var")
        fi
    done

    if [ ${#missing[@]} -gt 0 ]; then
        echo "Error: Required variables not set:" >&2
        printf '  %s\n' "${missing[@]}" >&2
        return 1
    fi

    return 0
}

# Define required variables
REQUIRED_VARS=(
    API_KEY
    API_SECRET
    ENDPOINT_URL
)

# Check all at once
if ! check_required "${REQUIRED_VARS[@]}"; then
    echo "Please set all required environment variables" >&2
    exit 1
fi

# Or check individually with custom errors
API_KEY="${API_KEY:?Error: API_KEY is required. Get one at https://example.com/api}"
API_SECRET="${API_SECRET:?Error: API_SECRET is required}"
ENDPOINT_URL="${ENDPOINT_URL:?Error: ENDPOINT_URL is required}"

echo "Configuration valid, proceeding..."

5. 대소문자 변환

Bash 4.0+는 대소문자 변환을 위한 매개변수 확장 연산자를 제공합니다.

대소문자 변환 연산자

#!/usr/bin/env bash

# ^   Uppercase first character
# ^^  Uppercase all characters
# ,   Lowercase first character
# ,,  Lowercase all characters

string="Hello World"

# Uppercase operations
echo "${string^}"      # Hello World (first char of first word)
echo "${string^^}"     # HELLO WORLD (all chars)

# Lowercase operations
echo "${string,}"      # hello World (first char)
echo "${string,,}"     # hello world (all chars)

# Pattern-based conversion (bash 4.0+)
string="hello world"
echo "${string^^[hw]}"  # Hello World (uppercase h and w)

string="HELLO WORLD"
echo "${string,,[HW]}"  # hELLO wORLD (lowercase H and W)

입력 정규화

#!/usr/bin/env bash

# Normalize user input to lowercase
normalize_input() {
    local input="$1"
    echo "${input,,}"
}

# Case-insensitive comparison
read -p "Continue? (yes/no): " answer
answer="$(normalize_input "$answer")"

case "$answer" in
    yes|y)
        echo "Continuing..."
        ;;
    no|n)
        echo "Aborting..."
        exit 0
        ;;
    *)
        echo "Invalid input: $answer" >&2
        exit 1
        ;;
esac

# Normalize file extensions
process_files() {
    shopt -s nullglob

    for file in *; do
        [ -f "$file" ] || continue

        # Get extension in lowercase
        ext="${file##*.}"
        ext_lower="${ext,,}"

        case "$ext_lower" in
            jpg|jpeg|png|gif)
                echo "Image: $file"
                ;;
            mp4|avi|mkv)
                echo "Video: $file"
                ;;
            txt|md|log)
                echo "Text: $file"
                ;;
            *)
                echo "Other: $file"
                ;;
        esac
    done
}

제목 대소문자

#!/usr/bin/env bash

# Convert to title case (capitalize first letter of each word)
to_title_case() {
    local string="$1"
    local result=""
    local word

    # Convert to lowercase first
    string="${string,,}"

    # Process each word
    for word in $string; do
        # Capitalize first letter
        result+="${word^} "
    done

    # Remove trailing space
    echo "${result% }"
}

echo "$(to_title_case 'hello world from bash')"
# Hello World From Bash

echo "$(to_title_case 'THE QUICK BROWN FOX')"
# The Quick Brown Fox

# Sentence case (first letter of first word only)
to_sentence_case() {
    local string="$1"
    string="${string,,}"
    echo "${string^}"
}

echo "$(to_sentence_case 'HELLO WORLD')"
# Hello world

실용 예제

#!/usr/bin/env bash

# Environment variable name normalization
normalize_env_var() {
    local name="$1"
    # Convert to uppercase and replace invalid chars with underscore
    name="${name^^}"
    name="${name//[^A-Z0-9_]/}"
    echo "$name"
}

echo "$(normalize_env_var 'my-app.config.port')"
# MY_APP_CONFIG_PORT

# SQL identifier quoting
quote_sql_identifier() {
    local identifier="$1"
    # Convert to lowercase (PostgreSQL convention)
    identifier="${identifier,,}"
    echo "\"$identifier\""
}

echo "SELECT * FROM $(quote_sql_identifier 'UserData')"
# SELECT * FROM "userdata"

# HTTP header normalization
normalize_http_header() {
    local header="$1"
    # Title-Case-With-Dashes

    local IFS='-'
    local words=($header)
    local result=""

    for word in "${words[@]}"; do
        word="${word,,}"
        result+="${word^}-"
    done

    echo "${result%-}"
}

echo "$(normalize_http_header 'content-type')"      # Content-Type
echo "$(normalize_http_header 'X-REQUEST-ID')"      # X-Request-Id
echo "$(normalize_http_header 'accept-encoding')"   # Accept-Encoding

6. 간접 참조

간접 참조(Indirect References)를 사용하면 한 변수의 값을 다른 변수의 이름으로 사용할 수 있습니다.

기본 간접 참조

#!/usr/bin/env bash

# ${!var} - indirect expansion

# Direct access
name="John"
echo "$name"      # John

# Indirect access
var_name="name"
echo "${!var_name}"  # John (value of $name)

# Set value indirectly using declare
declare "$var_name=Jane"
echo "$name"      # Jane

# More complex example
DB_HOST_DEV="localhost"
DB_HOST_PROD="db.example.com"
DB_HOST_STAGING="staging-db.example.com"

environment="PROD"
var_name="DB_HOST_${environment}"
echo "Connecting to: ${!var_name}"  # db.example.com

변수 이름 확장

#!/usr/bin/env bash

# ${!prefix*} - expands to names of variables starting with prefix
# ${!prefix@} - same but quoted (safer)

# Set multiple related variables
DB_HOST="localhost"
DB_PORT="5432"
DB_NAME="myapp"
DB_USER="admin"

# Get all DB_* variable names
echo "Database configuration variables:"
for var_name in ${!DB_*}; do
    echo "  $var_name = ${!var_name}"
done

# Output:
# Database configuration variables:
#   DB_HOST = localhost
#   DB_NAME = myapp
#   DB_PORT = 5432
#   DB_USER = admin

# Safer version with @ (properly quoted)
for var_name in "${!DB_@}"; do
    echo "  $var_name = ${!var_name}"
done

동적 설정

#!/usr/bin/env bash

# Load configuration for specific environment
load_config() {
    local env="${1:-dev}"
    env="${env^^}"  # Uppercase

    # Define configs for each environment
    CONFIG_HOST_DEV="localhost"
    CONFIG_PORT_DEV="8080"
    CONFIG_DB_DEV="dev_database"

    CONFIG_HOST_PROD="prod.example.com"
    CONFIG_PORT_PROD="443"
    CONFIG_DB_PROD="prod_database"

    CONFIG_HOST_STAGING="staging.example.com"
    CONFIG_PORT_STAGING="8443"
    CONFIG_DB_STAGING="staging_database"

    # Load variables for selected environment
    for var in ${!CONFIG_*}; do
        if [[ "$var" == *_${env} ]]; then
            # Extract base name (CONFIG_HOST_DEV -> CONFIG_HOST)
            local base_name="${var%_*}"
            # Set variable without environment suffix
            declare -g "$base_name=${!var}"
        fi
    done
}

# Load production config
load_config "prod"

echo "Host: $CONFIG_HOST"
echo "Port: $CONFIG_PORT"
echo "Database: $CONFIG_DB"

# Load dev config
load_config "dev"

echo "Host: $CONFIG_HOST"
echo "Port: $CONFIG_PORT"
echo "Database: $CONFIG_DB"

기능 플래그

#!/usr/bin/env bash

# Feature flag system
FEATURE_NEW_UI="enabled"
FEATURE_BETA_API="disabled"
FEATURE_ANALYTICS="enabled"
FEATURE_DARK_MODE="enabled"

is_feature_enabled() {
    local feature="$1"
    local var_name="FEATURE_${feature^^}"
    local status="${!var_name:-disabled}"

    [ "$status" = "enabled" ]
}

# Usage
if is_feature_enabled "new_ui"; then
    echo "Loading new UI..."
else
    echo "Loading classic UI..."
fi

if is_feature_enabled "dark_mode"; then
    echo "Dark mode: ON"
fi

# List all features
echo "Feature flags:"
for var in ${!FEATURE_*}; do
    feature="${var#FEATURE_}"
    status="${!var}"
    echo "  $feature: $status"
done

다중 환경 비밀 정보

#!/usr/bin/env bash

# Store secrets for multiple environments
SECRET_API_KEY_DEV="dev-key-12345"
SECRET_API_KEY_STAGING="staging-key-67890"
SECRET_API_KEY_PROD="prod-key-abcdef"

SECRET_DB_PASSWORD_DEV="dev-password"
SECRET_DB_PASSWORD_STAGING="staging-password"
SECRET_DB_PASSWORD_PROD="prod-password"

get_secret() {
    local secret_name="$1"
    local environment="${2:-dev}"
    environment="${environment^^}"

    local var_name="SECRET_${secret_name^^}_${environment}"
    local secret="${!var_name}"

    if [ -z "$secret" ]; then
        echo "Error: Secret $secret_name not found for $environment" >&2
        return 1
    fi

    echo "$secret"
}

# Usage
ENVIRONMENT="prod"
API_KEY="$(get_secret "api_key" "$ENVIRONMENT")"
DB_PASSWORD="$(get_secret "db_password" "$ENVIRONMENT")"

echo "API Key: ${API_KEY:0:10}..." # Show only first 10 chars
echo "Password: ${DB_PASSWORD:0:5}..."

7. declare 내장 명령어와 변수 속성

declare 내장 명령어는 변수 속성을 설정하고 변수 동작을 제어합니다.

declare 플래그

#!/usr/bin/env bash

# declare -flag variable=value

# -i: Integer attribute
declare -i count=0
count=count+1        # Arithmetic without $(( ))
echo "$count"        # 1

count="5 + 3"
echo "$count"        # 8 (evaluated as arithmetic)

count="hello"        # Invalid, becomes 0
echo "$count"        # 0

# -r: Readonly (const)
declare -r PI=3.14159
# PI=3.14  # Error: PI: readonly variable

# -l: Lowercase
declare -l lowercase="HELLO WORLD"
echo "$lowercase"    # hello world
lowercase="MIXED CaSe"
echo "$lowercase"    # mixed case

# -u: Uppercase
declare -u uppercase="hello world"
echo "$uppercase"    # HELLO WORLD
uppercase="Mixed CaSe"
echo "$uppercase"    # MIXED CASE

# -n: Nameref (reference to another variable)
name="John"
declare -n name_ref=name
echo "$name_ref"     # John
name_ref="Jane"
echo "$name"         # Jane

# -a: Indexed array
declare -a array=(one two three)
echo "${array[0]}"   # one

# -A: Associative array
declare -A config=([host]=localhost [port]=8080)
echo "${config[host]}"  # localhost

# -x: Export (make available to child processes)
declare -x ENVIRONMENT="production"
bash -c 'echo $ENVIRONMENT'  # production

# -p: Print variable definition
declare -p PI
# declare -r PI="3.14159"

Declare 플래그 비교

플래그 설명 예제
-i 정수(Integer) declare -i count=5
-r 읽기 전용(Readonly) declare -r CONST=100
-l 소문자(Lowercase) declare -l name="JOHN"
-u 대문자(Uppercase) declare -u name="john"
-n 이름 참조(Nameref) declare -n ref=var
-a 인덱스 배열(Indexed array) declare -a arr=(1 2 3)
-A 연관 배열(Associative array) declare -A map=([key]=val)
-x 내보내기(Export) declare -x VAR=value
-g 전역(Global, 함수 내) declare -g GLOBAL=1
-p 선언 출력(Print declaration) declare -p VAR
-f 함수 이름(Function names) declare -f func_name
-F 함수 이름만(Function names only) declare -F

정수 변수

#!/usr/bin/env bash

# Integer variables auto-evaluate arithmetic
declare -i counter=0
declare -i result

# Arithmetic without $(( ))
counter+=1
echo "Counter: $counter"  # 1

counter=counter+10
echo "Counter: $counter"  # 11

# Expressions evaluated
result=5*3+2
echo "Result: $result"    # 17

# Division
result=20/3
echo "Result: $result"    # 6 (integer division)

# Use in loops
declare -i i
for i in {1..5}; do
    echo "i = $i, i*2 = $((i*2))"
done

# Automatic base conversion
declare -i hex
hex=0xff
echo "$hex"  # 255

declare -i octal
octal=0755
echo "$octal"  # 493

읽기 전용 변수

#!/usr/bin/env bash

# Readonly variables (constants)
declare -r APP_NAME="MyApp"
declare -r VERSION="1.0.0"
declare -r AUTHOR="Your Name"

# Multiple readonly declarations
declare -r \
    MAX_CONNECTIONS=100 \
    TIMEOUT=30 \
    RETRY_COUNT=3

echo "$APP_NAME v$VERSION by $AUTHOR"

# Check if variable is readonly
if declare -p APP_NAME 2>/dev/null | grep -q 'declare -r'; then
    echo "APP_NAME is readonly"
fi

# Attempt to modify causes error
# APP_NAME="NewName"  # bash: APP_NAME: readonly variable

# Make existing variable readonly
config_file="/etc/app/config.yml"
readonly config_file
# config_file="/tmp/config"  # Error: readonly variable

대소문자 변환 속성

#!/usr/bin/env bash

# Automatic lowercase
declare -l email
email="User@EXAMPLE.COM"
echo "$email"  # user@example.com

# Automatic uppercase
declare -u env_name
env_name="production"
echo "$env_name"  # PRODUCTION

# Use in functions for input normalization
process_input() {
    declare -l normalized="$1"

    case "$normalized" in
        yes|y|true|1)
            return 0
            ;;
        no|n|false|0)
            return 1
            ;;
        *)
            echo "Invalid input: $1" >&2
            return 2
            ;;
    esac
}

process_input "YES" && echo "Confirmed"
process_input "No" && echo "This won't print"

이름 참조 변수

#!/usr/bin/env bash

# Nameref: reference to another variable
original="Hello"
declare -n reference=original

echo "$reference"    # Hello
reference="World"
echo "$original"     # World

# Use in functions to modify caller's variables
increment_var() {
    local -n var_ref=$1
    var_ref=$((var_ref + 1))
}

counter=10
increment_var counter
echo "$counter"  # 11

# Swap variables
swap() {
    local -n a=$1
    local -n b=$2
    local temp="$a"
    a="$b"
    b="$temp"
}

x=5
y=10
echo "Before: x=$x, y=$y"
swap x y
echo "After: x=$x, y=$y"
# Before: x=5, y=10
# After: x=10, y=5

# Pass arrays by reference
sum_array() {
    local -n arr=$1
    local sum=0
    local element

    for element in "${arr[@]}"; do
        sum=$((sum + element))
    done

    echo "$sum"
}

numbers=(1 2 3 4 5)
result=$(sum_array numbers)
echo "Sum: $result"  # 15

변수 검사

#!/usr/bin/env bash

# Print variable declarations
declare -i count=5
declare -r VERSION="1.0"
declare -a files=(a.txt b.txt)
declare -A config=([host]=localhost)

# Print specific variable
declare -p count
# declare -i count="5"

declare -p VERSION
# declare -r VERSION="1.0"

# Print all variables
declare -p | head -20

# Print only exported variables
declare -px

# Print all functions
declare -F

# Print specific function
declare -f sum_array

# Check variable attributes
is_readonly() {
    declare -p "$1" 2>/dev/null | grep -q 'declare -r'
}

is_integer() {
    declare -p "$1" 2>/dev/null | grep -q 'declare -i'
}

is_readonly "VERSION" && echo "VERSION is readonly"
is_integer "count" && echo "count is integer"

8. 실전 패턴

URL 파서

#!/usr/bin/env bash

# Complete URL parser using parameter expansion
parse_url() {
    local url="$1"

    # Extract protocol
    local protocol="${url%%://*}"
    local rest="${url#*://}"

    # Extract credentials if present
    local credentials=""
    local user=""
    local password=""
    if [[ "$rest" == *@* ]]; then
        credentials="${rest%%@*}"
        rest="${rest#*@}"
        user="${credentials%%:*}"
        password="${credentials#*:}"
    fi

    # Extract host and port
    local host_port="${rest%%/*}"
    local host="${host_port%%:*}"
    local port="${host_port#*:}"
    [ "$port" = "$host" ] && port=""  # No port specified

    # Extract path
    local path_query="${rest#*/}"
    [ "$path_query" = "$rest" ] && path_query=""  # No path
    local path="/${path_query%%\?*}"
    [ "$path" = "/$path_query" ] && path="/${path_query%%#*}"

    # Extract query string
    local query=""
    if [[ "$path_query" == *\?* ]]; then
        query="${path_query#*\?}"
        query="${query%%#*}"
    fi

    # Extract fragment
    local fragment=""
    if [[ "$url" == *#* ]]; then
        fragment="${url##*#}"
    fi

    # Print results
    echo "URL: $url"
    echo "  Protocol: $protocol"
    [ -n "$user" ] && echo "  User: $user"
    [ -n "$password" ] && echo "  Password: ${password:0:3}***"
    echo "  Host: $host"
    [ -n "$port" ] && echo "  Port: $port"
    echo "  Path: $path"
    [ -n "$query" ] && echo "  Query: $query"
    [ -n "$fragment" ] && echo "  Fragment: $fragment"
}

parse_url "https://user:pass@example.com:8080/path/to/resource.html?key=value&foo=bar#section"

설정 파일 파서

#!/usr/bin/env bash

# Parse key=value config files
parse_config() {
    local config_file="$1"
    local -n config_array=$2

    [ -f "$config_file" ] || return 1

    local line key value
    while IFS= read -r line; do
        # Skip empty lines and comments
        [[ "$line" =~ ^[[:space:]]*$ ]] && continue
        [[ "$line" =~ ^[[:space:]]*# ]] && continue

        # Remove inline comments
        line="${line%%#*}"

        # Trim whitespace
        line="${line#"${line%%[![:space:]]*}"}"
        line="${line%"${line##*[![:space:]]}"}"

        # Parse key=value
        key="${line%%=*}"
        value="${line#*=}"

        # Trim key and value
        key="${key#"${key%%[![:space:]]*}"}"
        key="${key%"${key##*[![:space:]]}"}"
        value="${value#"${value%%[![:space:]]*}"}"
        value="${value%"${value##*[![:space:]]}"}"

        # Remove quotes from value
        if [[ "$value" =~ ^\".*\"$ ]] || [[ "$value" =~ ^\'.*\'$ ]]; then
            value="${value:1:-1}"
        fi

        # Store in associative array
        config_array["$key"]="$value"
    done < "$config_file"
}

# Usage
declare -A config

cat > test_config.conf <<'EOF'
# Database configuration
db_host = localhost
db_port = 5432
db_name = "myapp"

# Application settings
app_name = My Application
log_level = INFO  # inline comment
timeout = 30
EOF

parse_config "test_config.conf" config

echo "Configuration loaded:"
for key in "${!config[@]}"; do
    echo "  $key = ${config[$key]}"
done

rm test_config.conf

경로 조작 도구킷

#!/usr/bin/env bash

# Complete path manipulation library

# Get absolute path
abspath() {
    local path="$1"

    if [[ "$path" = /* ]]; then
        echo "$path"
    else
        echo "$(pwd)/$path"
    fi
}

# Get directory name (like dirname)
dirname() {
    local path="$1"

    # Remove trailing slashes
    path="${path%/}"

    # Get directory part
    local dir="${path%/*}"

    # If no directory part, return .
    [ "$dir" = "$path" ] && dir="."

    echo "$dir"
}

# Get filename (like basename)
basename() {
    local path="$1"
    local suffix="$2"

    # Remove trailing slashes
    path="${path%/}"

    # Get filename
    local name="${path##*/}"

    # Remove suffix if provided
    if [ -n "$suffix" ]; then
        name="${name%$suffix}"
    fi

    echo "$name"
}

# Get file extension
get_extension() {
    local path="$1"
    local name="${path##*/}"

    # No extension if no dot or starts with dot
    [[ "$name" != *.* ]] && return
    [[ "$name" = .* ]] && return

    echo "${name##*.}"
}

# Remove extension
remove_extension() {
    local path="$1"
    echo "${path%.*}"
}

# Replace extension
replace_extension() {
    local path="$1"
    local new_ext="$2"
    local base="${path%.*}"
    echo "${base}.${new_ext}"
}

# Join paths
join_path() {
    local IFS='/'
    local joined="$*"

    # Remove duplicate slashes
    while [[ "$joined" =~ // ]]; do
        joined="${joined//\/\//\/}"
    done

    echo "$joined"
}

# Normalize path (remove ./ and ../)
normalize_path() {
    local path="$1"
    local IFS='/'
    local parts=($path)
    local result=()

    for part in "${parts[@]}"; do
        case "$part" in
            .|'')
                continue
                ;;
            ..)
                [ ${#result[@]} -gt 0 ] && unset 'result[-1]'
                ;;
            *)
                result+=("$part")
                ;;
        esac
    done

    local normalized="${result[*]}"
    [ "${path:0:1}" = / ] && normalized="/$normalized"
    echo "$normalized"
}

# Test the toolkit
echo "=== Path Manipulation Toolkit ==="
path="/usr/local/bin/script.sh"

echo "Original: $path"
echo "Directory: $(dirname "$path")"
echo "Filename: $(basename "$path")"
echo "Extension: $(get_extension "$path")"
echo "Without ext: $(remove_extension "$path")"
echo "Replace ext: $(replace_extension "$path" "bash")"
echo "Join: $(join_path "/usr" "local" "bin" "test.sh")"
echo "Normalize: $(normalize_path "/usr/./local/../local/bin//script.sh")"

연습 문제

문제 1: 고급 파일 이름 처리기

다음 기능을 가진 파일 이름 처리 스크립트를 작성하세요: - 다양한 형식의 날짜 스탬프 추출 (YYYYMMDD, YYYY-MM-DD, YYYY_MM_DD) - 날짜 스탬프 제거 또는 정규화 - 버전 번호 추출 (v1.0, version-2.3.4 등) - 파일 이름 정리 (특수 문자 제거, 공백 정규화) - 현재 날짜/버전으로 새 파일 이름 생성

예제:

process_filename "report_20240213_v1.3.pdf"
# Date: 2024-02-13
# Version: 1.3
# Base: report
# Extension: pdf
# Normalized: report_2024-02-13_v1.3.pdf

문제 2: 환경 변수 검증기

환경 변수를 검증하는 스크립트를 만드세요: - 필수 변수가 설정되어 있고 비어있지 않은지 확인 - 변수 타입 검증 (정수, 부울, 열거형) - 값 범위 및 제약 조건 확인 - 제안을 포함한 상세한 오류 메시지 제공 - .env 파일에서 로드 지원

예제:

validate_env PORT        --type int --range 1024-65535 --required
validate_env DEBUG       --type bool --default false
validate_env ENVIRONMENT --type enum --values "dev,staging,prod" --required
validate_env API_KEY     --type string --min-length 32 --required

문제 3: 고급 설정 파일 관리자

다음 기능을 가진 설정 파일 관리자를 작성하세요: - 섹션이 있는 INI 스타일 설정 파일 읽기 - 여러 데이터 타입 지원 (문자열, 정수, 부울, 배열) - 중첩 섹션 허용 (section.subsection.key) - get/set 연산 제공 - 스키마에 대한 값 검증 - 여러 설정 파일 병합

예제 설정:

[database]
host = localhost
port = 5432
databases = ["app", "cache", "logs"]

[database.pool]
min_size = 5
max_size = 20

문제 4: 문자열 템플릿 엔진

다음 기능을 가진 간단한 템플릿 엔진을 만드세요: - 변수 치환: Hello {{name}}! - 기본값 지원: {{var:default}} - 조건부 섹션: {{#if var}}...{{/if}} - 반복: {{#each items}}...{{/each}} - 필터: {{name | uppercase}} - 이스케이핑: {{!raw}}

예제:

template='Hello {{name:Guest}}! {{#if premium}}Welcome premium member{{/if}}'
render "$template" name="John" premium=true
# Output: Hello John! Welcome premium member

문제 5: 경로 해결 라이브러리

다음 기능을 가진 경로 해결 라이브러리를 구현하세요: - 상대 경로를 절대 경로로 해결 - 심볼릭 링크 처리 - 순환 심볼릭 링크 감지 - PATH에서 파일 찾기 - 변수가 포함된 경로 해결 ($HOME, ~ 등) - 크로스 플랫폼 지원 (Windows 경로 처리) - 안전성 검사 (디렉터리 순회 방지)

예제:

resolve_path "~/Documents/../Downloads/./file.txt"
# /home/user/Downloads/file.txt

find_in_path "python3"
# /usr/bin/python3

is_subpath "/var/www/html" "/var/www/html/../../../../etc/passwd"
# Error: Directory traversal detected

이전: 01_Shell_Fundamentals.md | 다음: 03_Arrays_and_Data.md

to navigate between lessons