레슨 12: 이식성과 모범 사례
레슨 12: 이식성과 모범 사례¶
난이도: ⭐⭐⭐⭐
이전: 11_Argument_Parsing.md | 다음: 13_Testing.md
1. POSIX sh vs Bash vs Zsh¶
셸 간의 차이점을 이해하면 이식성을 보장할 수 있습니다.
기능 비교¶
| 기능 | POSIX sh | Bash | Zsh | 참고 사항 |
|---|---|---|---|---|
| 배열(Arrays) | No | Yes | Yes | sh: 위치 매개변수 사용 |
[[ ]] 테스트 |
No | Yes | Yes | sh: [ ] 사용 |
| 프로세스 치환(Process substitution) | No | Yes | Yes | <(command) |
| Here strings | No | Yes | Yes | <<< "string" |
local 키워드 |
No* | Yes | Yes | *널리 지원되지만 POSIX는 아님 |
function 키워드 |
No | Yes | Yes | POSIX는 name() { } 사용 |
$RANDOM |
No | Yes | Yes | sh: /dev/urandom 사용 |
source |
No | Yes | Yes | sh: . 사용 |
echo -n |
No* | Yes | Yes | *대신 printf 사용 |
산술 $(( )) |
Yes | Yes | Yes | POSIX 호환 |
| 매개변수 확장(Parameter expansion) | 기본 | 확장 | 확장 | Bash는 더 많은 패턴 제공 |
| 연관 배열(Associative arrays) | No | Yes (4.0+) | Yes | sh: eval 트릭 사용 |
&>> 리다이렉션 |
No | Yes | Yes | sh: >>file 2>&1 사용 |
time 키워드 |
No | Yes | Yes | sh: /usr/bin/time 사용 |
select 루프 |
No | Yes | Yes | sh: 수동 메뉴 |
셸 감지¶
#!/bin/sh
# Detect which shell is running this script
detect_shell() {
if [ -n "$BASH_VERSION" ]; then
echo "Running in Bash: $BASH_VERSION"
elif [ -n "$ZSH_VERSION" ]; then
echo "Running in Zsh: $ZSH_VERSION"
elif [ -n "$KSH_VERSION" ]; then
echo "Running in Ksh: $KSH_VERSION"
else
echo "Running in unknown shell (possibly POSIX sh)"
fi
}
detect_shell
# Check if running in Bash
is_bash() {
[ -n "$BASH_VERSION" ]
}
if is_bash; then
echo "Bash-specific features available"
else
echo "Using POSIX-compatible features only"
fi
어떤 셸을 사용할지¶
#!/bin/sh
# Use POSIX sh when:
# - Maximum portability required
# - Script must run on embedded systems
# - Minimal dependencies
# - Alpine Linux (uses busybox sh)
# Use Bash when:
# - Advanced features needed (arrays, associative arrays)
# - Better error handling ([[ ]], set -euo pipefail)
# - More readable code
# - Linux systems (Bash is ubiquitous)
# Use Zsh when:
# - Interactive features needed
# - macOS default shell (10.15+)
# - Advanced globbing required
# Shebang choices:
#!/bin/sh # POSIX sh (maximum portability)
#!/bin/bash # Bash (common location)
#!/usr/bin/env bash # Bash (portable location lookup)
2. 피해야 할 일반적인 Bashism¶
Bashism은 POSIX sh에서 작동하지 않는 Bash 전용 기능입니다.
테스트 연산자: [[ ]] vs [ ]¶
#!/bin/sh
# BAD (Bashism): [[ ]]
# [[ $var == "value" ]]
# GOOD (POSIX): [ ]
var="value"
if [ "$var" = "value" ]; then
echo "Match"
fi
# BAD (Bashism): [[ with pattern matching ]]
# [[ $file == *.txt ]]
# GOOD (POSIX): use case
case "$file" in
*.txt) echo "Text file" ;;
*) echo "Other file" ;;
esac
# BAD (Bashism): [[ with regex ]]
# [[ $string =~ ^[0-9]+$ ]]
# GOOD (POSIX): use grep
if echo "$string" | grep -qE '^[0-9]+$'; then
echo "Number"
fi
# BAD (Bashism): [[ with && ]]
# [[ -f file && -r file ]]
# GOOD (POSIX): separate [ ] or use -a
if [ -f file ] && [ -r file ]; then
echo "File exists and is readable"
fi
# Alternative (but [ ] is deprecated):
if [ -f file -a -r file ]; then
echo "File exists and is readable"
fi
배열¶
#!/bin/sh
# BAD (Bashism): arrays
# array=(one two three)
# echo "${array[1]}"
# GOOD (POSIX): use positional parameters
set -- one two three
echo "$2" # Prints: two
# GOOD (POSIX): use space-separated string
items="one two three"
for item in $items; do
echo "$item"
done
# GOOD (POSIX): use newline-separated string
items="one
two
three"
IFS='
'
for item in $items; do
echo "$item"
done
프로세스 치환¶
#!/bin/sh
# BAD (Bashism): process substitution
# diff <(sort file1) <(sort file2)
# GOOD (POSIX): use temporary files
tmp1=$(mktemp)
tmp2=$(mktemp)
trap 'rm -f "$tmp1" "$tmp2"' EXIT
sort file1 > "$tmp1"
sort file2 > "$tmp2"
diff "$tmp1" "$tmp2"
Here Strings¶
#!/bin/sh
# BAD (Bashism): here string
# grep "pattern" <<< "$variable"
# GOOD (POSIX): echo with pipe
echo "$variable" | grep "pattern"
# GOOD (POSIX): here document
grep "pattern" << EOF
$variable
EOF
# GOOD (POSIX): printf with pipe
printf '%s\n' "$variable" | grep "pattern"
$RANDOM¶
#!/bin/sh
# BAD (Bashism): $RANDOM
# random_num=$RANDOM
# GOOD (POSIX): /dev/urandom
random_num=$(od -An -N2 -i /dev/urandom | tr -d ' ')
# GOOD (POSIX): awk with /dev/urandom
random_num=$(awk 'BEGIN{srand(); print int(rand()*32768)}')
# GOOD (POSIX): hexdump
random_num=$(hexdump -n 2 -e '/2 "%u"' /dev/urandom)
source vs .¶
#!/bin/sh
# BAD (Bashism): source
# source ./config.sh
# GOOD (POSIX): .
. ./config.sh
# Both work in Bash, but only . is POSIX
echo vs printf¶
#!/bin/sh
# BAD (not portable): echo -n
# echo -n "No newline"
# GOOD (POSIX): printf
printf "No newline"
# BAD (not portable): echo with backslashes
# echo "Line 1\nLine 2"
# GOOD (POSIX): printf
printf "Line 1\nLine 2\n"
# echo is only safe for simple strings without flags or escapes
echo "Simple string" # OK
# printf is always safe and portable
printf '%s\n' "Any string" # Always works
function 키워드¶
#!/bin/sh
# BAD (Bashism): function keyword
# function my_func() {
# echo "Hello"
# }
# GOOD (POSIX): no function keyword
my_func() {
echo "Hello"
}
# Call it
my_func
local 변수¶
#!/bin/sh
# BAD (not POSIX, but widely supported): local
# my_func() {
# local var="value"
# }
# GOOD (POSIX): no local keyword (variables are global)
my_func() {
# Use prefixed names to avoid conflicts
_myfunc_var="value"
echo "$_myfunc_var"
}
# Alternative: use subshell for isolation
my_func_isolated() {
(
var="value" # Only exists in subshell
echo "$var"
)
}
# Note: local is so widely supported that it's often used anyway
# even in "POSIX" scripts. Busybox sh supports it, for example.
완전한 POSIX vs Bash 예제¶
#!/bin/sh
# POSIX-compatible version
# Check if file is readable text file
is_readable_text_file() {
file="$1"
# Check exists and is regular file
if [ ! -f "$file" ]; then
return 1
fi
# Check readable
if [ ! -r "$file" ]; then
return 1
fi
# Check if text file (using file command)
case "$(file -b "$file")" in
*text*) return 0 ;;
*) return 1 ;;
esac
}
# Bash version (simpler)
#!/bin/bash
is_readable_text_file() {
local file=$1
[[ -f "$file" && -r "$file" && $(file -b "$file") == *text* ]]
}
3. Google Shell Style Guide 하이라이트¶
Google의 Shell Style Guide는 업계 모범 사례를 제공합니다.
파일 헤더¶
#!/bin/bash
#
# Script name: deploy.sh
# Description: Deploys application to production
# Author: John Doe <john@example.com>
# Date: 2024-01-15
# Version: 1.0.0
#
# Usage: deploy.sh [--dry-run] [--environment ENV] VERSION
#
# Copyright 2024 Company Name
# License: MIT
set -euo pipefail
함수 주석¶
#!/bin/bash
#######################################
# Processes a file and generates output.
# Globals:
# OUTPUT_DIR
# Arguments:
# $1 - Input file path
# $2 - Output format (json|xml)
# Outputs:
# Writes processed data to OUTPUT_DIR
# Returns:
# 0 on success, 1 on error
#######################################
process_file() {
local input_file=$1
local format=$2
# Implementation...
}
#######################################
# Cleanup function for trap.
# Globals:
# TEMP_FILES
# Arguments:
# None
# Outputs:
# Cleanup messages to stderr
#######################################
cleanup() {
# Implementation...
}
TODO 주석¶
#!/bin/bash
# TODO(username): Add error handling for network failures
# TODO(username): Implement retry logic with exponential backoff
# FIXME(username): This breaks when file has spaces in name
process_file() {
# ...
}
# NOTE: This function is deprecated, use process_file_v2 instead
process_file_v1() {
# ...
}
# HACK: Temporary workaround for bug in external tool
# Will be removed when tool is updated
workaround() {
# ...
}
네이밍 규칙¶
#!/bin/bash
# Constants: UPPER_CASE
readonly MAX_RETRIES=3
readonly DEFAULT_TIMEOUT=30
readonly CONFIG_FILE="/etc/app/config.conf"
# Environment variables: UPPER_CASE
export PATH="/usr/local/bin:$PATH"
export DEBUG_MODE=0
# Variables: lowercase_with_underscores
user_name="john"
file_path="/tmp/file.txt"
retry_count=0
# Functions: lowercase_with_underscores
process_file() {
local input_file=$1
# ...
}
calculate_checksum() {
local file=$1
# ...
}
# Private functions: _prefix (convention, not enforced)
_internal_helper() {
# ...
}
들여쓰기와 포매팅¶
#!/bin/bash
# Use 2 spaces for indentation
if [ "$condition" = "true" ]; then
echo "Indented with 2 spaces"
if [ "$nested" = "true" ]; then
echo "Nested also 2 spaces"
fi
fi
# Line length: max 80 characters (flexible to 100)
very_long_command --option1 value1 \
--option2 value2 \
--option3 value3
# Pipe formatting
cat file.txt \
| grep "pattern" \
| sort \
| uniq
# Or:
cat file.txt |
grep "pattern" |
sort |
uniq
인용 규칙¶
#!/bin/bash
# Always quote variables
filename="my file.txt"
cat "$filename" # GOOD
# cat $filename # BAD - word splitting
# Quote command substitutions
result="$(command)" # GOOD
# result=$(command) # BAD - not wrong, but inconsistent
# Single quotes for literal strings
echo 'No expansion happens here: $var'
# Double quotes for strings with variables
echo "Value: $var"
# Quote array expansions
files=("file1.txt" "file2.txt")
process "${files[@]}" # GOOD
# process ${files[@]} # BAD
# Don't quote arithmetic expansions
count=$((count + 1)) # GOOD
# Don't quote comparison integers
if [ $count -gt 10 ]; then # OK (but quoting doesn't hurt)
echo "Greater than 10"
fi
셸과 Python/Perl 중 무엇을 사용할지¶
#!/bin/bash
# Use shell for:
# - Simple scripts (< 100 lines)
# - Primarily calling other programs
# - File system operations
# - Simple data processing
# - System administration tasks
# Use Python/Perl for:
# - Complex data structures
# - Text processing with complex logic
# - Scripts > 100 lines
# - Need for libraries (HTTP, JSON, etc.)
# - Complex algorithms
# - Better error handling required
# - Testing required
# Example: When shell is appropriate
#!/bin/bash
# Simple backup script
tar czf "backup_$(date +%Y%m%d).tar.gz" /important/data
aws s3 cp "backup_$(date +%Y%m%d).tar.gz" s3://backups/
# Example: When Python is better
#!/usr/bin/env python3
# Complex data processing with error handling
import json
import requests
import logging
# ... complex logic ...
4. 보안 모범 사례¶
셸 스크립트에 대한 보안 고려 사항입니다.
입력 검증¶
#!/bin/bash
# Always validate and sanitize input
sanitize_filename() {
local filename=$1
# Remove path components
filename=$(basename "$filename")
# Remove dangerous characters
filename=$(echo "$filename" | tr -cd '[:alnum:]._-')
# Limit length
filename=${filename:0:255}
echo "$filename"
}
# Usage
user_input="../../etc/passwd"
safe_name=$(sanitize_filename "$user_input")
echo "Safe: $safe_name" # Prints: etcpasswd
# Validate numeric input
validate_number() {
local input=$1
if ! [[ "$input" =~ ^[0-9]+$ ]]; then
echo "Error: Not a valid number" >&2
return 1
fi
echo "$input"
}
# Validate against whitelist
validate_enum() {
local input=$1
shift
local valid_values=("$@")
for value in "${valid_values[@]}"; do
if [ "$input" = "$value" ]; then
return 0
fi
done
echo "Error: Invalid value: $input" >&2
echo "Valid values: ${valid_values[*]}" >&2
return 1
}
# Usage
if validate_enum "$user_choice" "start" "stop" "restart"; then
echo "Valid choice"
fi
인젝션 방지를 위한 인용¶
#!/bin/bash
# BAD: Command injection vulnerability
user_input="file.txt; rm -rf /"
cat $user_input # DANGEROUS!
# GOOD: Proper quoting
cat "$user_input" # Safe - treated as single filename
# BAD: SQL injection (if using sqlite3 directly)
query="SELECT * FROM users WHERE name = '$user_input'"
sqlite3 db.sqlite "$query" # VULNERABLE
# GOOD: Use parameterized queries (via here-doc or file)
sqlite3 db.sqlite << EOF
SELECT * FROM users WHERE name = '$user_input';
EOF
# Better: Use proper tools with parameter binding
# (For complex DB work, use Python/Perl/etc.)
# BAD: HTML injection
echo "<div>$user_input</div>" > output.html
# GOOD: Escape HTML entities
escape_html() {
local input=$1
input=${input//&/&}
input=${input//</<}
input=${input//>/>}
input=${input//\"/"}
input=${input//\'/'}
echo "$input"
}
safe_input=$(escape_html "$user_input")
echo "<div>$safe_input</div>" > output.html
eval 피하기¶
#!/bin/bash
# BAD: eval is dangerous
user_command="rm -rf /"
eval "$user_command" # NEVER DO THIS
# GOOD: Use case or if/else instead
case "$user_command" in
start) start_service ;;
stop) stop_service ;;
*) echo "Unknown command" >&2 ;;
esac
# BAD: Dynamic variable names with eval
eval "${prefix}_var=value"
# GOOD: Use associative arrays (Bash 4+)
declare -A config
config["${prefix}_var"]="value"
# If you MUST use eval, validate thoroughly
safe_eval() {
local cmd=$1
# Whitelist of allowed commands
case "$cmd" in
"echo "*)
eval "$cmd"
;;
*)
echo "Command not allowed" >&2
return 1
;;
esac
}
안전한 임시 파일¶
#!/bin/bash
# BAD: Predictable temp file names
tmpfile="/tmp/myapp_$$" # $$ is predictable
# GOOD: Use mktemp
tmpfile=$(mktemp) || exit 1
trap 'rm -f "$tmpfile"' EXIT
echo "data" > "$tmpfile"
# GOOD: Temp directory
tmpdir=$(mktemp -d) || exit 1
trap 'rm -rf "$tmpdir"' EXIT
# Set restrictive permissions
chmod 700 "$tmpdir"
# Create files in temp directory
echo "data" > "$tmpdir/file1.txt"
echo "more" > "$tmpdir/file2.txt"
PATH 강화¶
#!/bin/bash
# Set secure PATH
export PATH="/usr/local/bin:/usr/bin:/bin"
# Or use absolute paths for critical commands
/usr/bin/whoami
/bin/cat /etc/passwd
# Verify command location
require_command() {
local cmd=$1
local path
path=$(command -v "$cmd")
if [ -z "$path" ]; then
echo "Error: Command not found: $cmd" >&2
exit 1
fi
# Verify it's in expected location
case "$path" in
/usr/bin/*|/bin/*|/usr/local/bin/*)
echo "Using: $path" >&2
;;
*)
echo "Warning: Command in unexpected location: $path" >&2
;;
esac
}
require_command "grep"
require_command "awk"
최소 권한으로 실행¶
#!/bin/bash
# Check if running as root when not needed
if [ "$(id -u)" -eq 0 ]; then
echo "Error: Do not run this script as root" >&2
exit 1
fi
# Drop privileges if started as root
drop_privileges() {
local user=$1
if [ "$(id -u)" -eq 0 ]; then
echo "Dropping privileges to user: $user"
exec su - "$user" -c "$0 $*"
fi
}
# Require root for certain operations
require_root() {
if [ "$(id -u)" -ne 0 ]; then
echo "Error: This script must be run as root" >&2
exit 1
fi
}
# Check for sudo
has_sudo() {
sudo -n true 2>/dev/null
}
# Use sudo for specific commands only
safe_system_update() {
if has_sudo; then
sudo apt-get update
sudo apt-get upgrade -y
else
echo "Error: sudo access required" >&2
return 1
fi
}
5. 성능 최적화¶
효율적인 셸 스크립트 작성하기.
외부 명령 최소화¶
#!/bin/bash
# BAD: External command for string manipulation
basename=$(basename "$path")
dirname=$(dirname "$path")
# GOOD: Built-in parameter expansion
basename=${path##*/}
dirname=${path%/*}
# BAD: Using grep/sed for simple checks
if echo "$string" | grep -q "pattern"; then
echo "Found"
fi
# GOOD: Built-in pattern matching (Bash)
if [[ "$string" == *pattern* ]]; then
echo "Found"
fi
# BAD: Multiple echo calls
echo "Line 1"
echo "Line 2"
echo "Line 3"
# GOOD: Single printf or here-doc
printf '%s\n' "Line 1" "Line 2" "Line 3"
# Or:
cat << EOF
Line 1
Line 2
Line 3
EOF
서브셸 피하기¶
#!/bin/bash
# BAD: Subshell in loop
count=0
cat file.txt | while read line; do
((count++))
done
echo "$count" # Prints 0 (subshell!)
# GOOD: Process substitution or redirection
count=0
while read line; do
((count++))
done < file.txt
echo "$count" # Correct count
# BAD: Command substitution for variable assignment in loop
for i in {1..1000}; do
date_str=$(date +%Y%m%d) # Called 1000 times!
done
# GOOD: Calculate once
date_str=$(date +%Y%m%d)
for i in {1..1000}; do
# Use $date_str
done
mapfile/readarray 사용¶
#!/bin/bash
# BAD: Reading file in loop with command substitution
files=()
while IFS= read -r line; do
files+=("$line")
done < <(find . -name "*.txt")
# GOOD: Use mapfile (Bash 4+)
mapfile -t files < <(find . -name "*.txt")
# Or readarray (same as mapfile)
readarray -t files < <(find . -name "*.txt")
# Read from file
mapfile -t lines < file.txt
# Skip empty lines
mapfile -t lines < <(grep -v '^$' file.txt)
배치 작업¶
#!/bin/bash
# BAD: Multiple operations in loop
for file in *.txt; do
chmod 644 "$file"
done
# GOOD: Batch operation
chmod 644 *.txt
# BAD: Individual git operations
for commit in $commits; do
git show "$commit"
done
# GOOD: Single git command
git show $commits
# BAD: Multiple curl requests in sequence
for url in $urls; do
curl "$url"
done
# GOOD: Parallel curl with xargs
echo "$urls" | xargs -P 4 -n 1 curl
벤치마킹¶
#!/bin/bash
# Simple timing
echo "Method 1:"
time {
for i in {1..1000}; do
basename=$(basename "/path/to/file$i.txt")
done
}
echo "Method 2:"
time {
for i in {1..1000}; do
basename="${path##*/}"
done
}
# More detailed timing
benchmark() {
local iterations=${1:-100}
local code=$2
local start=$(date +%s%N)
for ((i=0; i<iterations; i++)); do
eval "$code"
done
local end=$(date +%s%N)
local duration=$(((end - start) / 1000000)) # Convert to milliseconds
echo "Duration: ${duration}ms for $iterations iterations"
echo "Average: $((duration / iterations))ms per iteration"
}
# Usage
benchmark 1000 'x=$(basename "/path/to/file.txt")'
benchmark 1000 'x=${path##*/}'
6. 코드 구성¶
유지보수를 위한 스크립트 구조화.
스크립트 템플릿¶
#!/bin/bash
#
# Script: script_name.sh
# Description: Brief description
# Author: Your Name
# Date: 2024-01-15
#
set -euo pipefail
# ============================================================================
# CONSTANTS
# ============================================================================
readonly SCRIPT_NAME=$(basename "$0")
readonly SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
readonly VERSION="1.0.0"
readonly DEFAULT_TIMEOUT=30
readonly MAX_RETRIES=3
# ============================================================================
# GLOBAL VARIABLES
# ============================================================================
VERBOSE=0
DRY_RUN=0
CONFIG_FILE=""
# ============================================================================
# HELPER FUNCTIONS
# ============================================================================
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >&2
}
error() {
log "ERROR: $*"
}
die() {
error "$*"
exit 1
}
# ============================================================================
# CORE FUNCTIONS
# ============================================================================
main_function_1() {
# Implementation
:
}
main_function_2() {
# Implementation
:
}
# ============================================================================
# MAIN
# ============================================================================
main() {
# Parse arguments
# Validate input
# Execute main logic
:
}
# Only run main if script is executed, not sourced
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
main "$@"
fi
모듈형 설계¶
#!/bin/bash
# config.sh - Configuration module
load_config() {
local config_file=$1
if [ ! -f "$config_file" ]; then
echo "Config file not found: $config_file" >&2
return 1
fi
# Source config file
# shellcheck source=/dev/null
. "$config_file"
}
# logger.sh - Logging module
setup_logger() {
LOG_FILE=${LOG_FILE:-/var/log/app.log}
LOG_LEVEL=${LOG_LEVEL:-INFO}
}
log_message() {
local level=$1
shift
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" >> "$LOG_FILE"
}
# network.sh - Network module
check_connectivity() {
local host=$1
ping -c 1 -W 2 "$host" &>/dev/null
}
download_file() {
local url=$1
local output=$2
curl -fsSL -o "$output" "$url"
}
# main.sh - Main script
#!/bin/bash
# Source modules
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/config.sh"
. "$SCRIPT_DIR/logger.sh"
. "$SCRIPT_DIR/network.sh"
main() {
load_config "/etc/app/config.conf"
setup_logger
if check_connectivity "example.com"; then
log_message INFO "Network available"
fi
}
main "$@"
구성 관리¶
#!/bin/bash
# Load configuration from file
load_config_ini() {
local config_file=$1
if [ ! -f "$config_file" ]; then
return 1
fi
# Parse INI-style config
while IFS='=' read -r key value; do
# Skip comments and empty lines
[[ "$key" =~ ^[[:space:]]*# ]] && continue
[[ -z "$key" ]] && continue
# Remove leading/trailing whitespace
key=$(echo "$key" | xargs)
value=$(echo "$value" | xargs)
# Export as environment variable
export "$key=$value"
done < "$config_file"
}
# Generate config file
generate_config() {
local output_file=$1
cat > "$output_file" << 'EOF'
# Application Configuration
# Database settings
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp
DB_USER=appuser
# API settings
API_KEY=your_api_key_here
API_TIMEOUT=30
# Application settings
DEBUG_MODE=0
LOG_LEVEL=INFO
MAX_CONNECTIONS=100
EOF
echo "Configuration written to: $output_file"
}
# Load config from environment, file, or defaults
load_config_priority() {
# 1. Defaults
DB_HOST=${DB_HOST:-localhost}
DB_PORT=${DB_PORT:-5432}
API_TIMEOUT=${API_TIMEOUT:-30}
# 2. Config file (if exists)
if [ -f "/etc/app/config.conf" ]; then
# shellcheck source=/dev/null
. "/etc/app/config.conf"
fi
# 3. Environment variables (highest priority, already loaded)
# Export final values
export DB_HOST DB_PORT API_TIMEOUT
}
7. 의존성 관리¶
스크립트 의존성 관리하기.
필수 명령어 확인¶
#!/bin/bash
# Check single command
require_command() {
local cmd=$1
if ! command -v "$cmd" &>/dev/null; then
echo "Error: Required command not found: $cmd" >&2
return 1
fi
}
# Check multiple commands
require_commands() {
local missing=()
for cmd in "$@"; do
if ! command -v "$cmd" &>/dev/null; then
missing+=("$cmd")
fi
done
if [ ${#missing[@]} -gt 0 ]; then
echo "Error: Required commands not found:" >&2
printf ' - %s\n' "${missing[@]}" >&2
return 1
fi
}
# Check with installation instructions
require_command_with_hint() {
local cmd=$1
local install_cmd=$2
if ! command -v "$cmd" &>/dev/null; then
cat << EOF >&2
Error: Required command not found: $cmd
To install, run:
$install_cmd
EOF
return 1
fi
}
# Usage
require_commands grep sed awk curl jq || exit 1
require_command_with_hint "jq" "apt-get install jq" || exit 1
require_command_with_hint "docker" "curl -fsSL https://get.docker.com | sh" || exit 1
버전 확인¶
#!/bin/bash
# Get command version
get_version() {
local cmd=$1
case "$cmd" in
bash)
bash --version | head -1 | grep -oP '\d+\.\d+\.\d+'
;;
python*)
$cmd --version 2>&1 | grep -oP '\d+\.\d+\.\d+'
;;
git)
git --version | grep -oP '\d+\.\d+\.\d+'
;;
docker)
docker --version | grep -oP '\d+\.\d+\.\d+'
;;
*)
echo "Unknown command: $cmd" >&2
return 1
;;
esac
}
# Compare versions
version_compare() {
local version1=$1
local version2=$2
if [ "$version1" = "$version2" ]; then
echo "0"
return
fi
local IFS=.
local i ver1=($version1) ver2=($version2)
# Fill empty positions with zeros
for ((i=${#ver1[@]}; i<${#ver2[@]}; i++)); do
ver1[i]=0
done
for ((i=0; i<${#ver1[@]}; i++)); do
if [[ -z ${ver2[i]} ]]; then
ver2[i]=0
fi
if ((10#${ver1[i]} > 10#${ver2[i]})); then
echo "1"
return
fi
if ((10#${ver1[i]} < 10#${ver2[i]})); then
echo "-1"
return
fi
done
echo "0"
}
# Require minimum version
require_version() {
local cmd=$1
local min_version=$2
local current_version
current_version=$(get_version "$cmd") || return 1
local cmp
cmp=$(version_compare "$current_version" "$min_version")
if [ "$cmp" -lt 0 ]; then
echo "Error: $cmd version $current_version is too old" >&2
echo " Minimum required version: $min_version" >&2
return 1
fi
echo "Using $cmd version $current_version" >&2
}
# Usage
require_version "bash" "4.0.0" || exit 1
require_version "git" "2.0.0" || exit 1
우아한 성능 저하(Graceful Degradation)¶
#!/bin/bash
# Use advanced features if available, fallback otherwise
use_color() {
if [ -t 1 ] && command -v tput &>/dev/null; then
# Terminal with tput support
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
RESET=$(tput sgr0)
elif [ -t 1 ]; then
# Terminal without tput
RED='\033[0;31m'
GREEN='\033[0;32m'
RESET='\033[0m'
else
# No terminal
RED=''
GREEN=''
RESET=''
fi
}
# Use jq if available, fallback to grep/sed
parse_json() {
local json_file=$1
local key=$2
if command -v jq &>/dev/null; then
jq -r ".$key" "$json_file"
else
# Fallback to grep/sed (fragile but works for simple cases)
grep "\"$key\"" "$json_file" | sed 's/.*: "\(.*\)".*/\1/'
fi
}
# Use parallel if available, fallback to xargs
parallel_execute() {
local cmd=$1
shift
local items=("$@")
if command -v parallel &>/dev/null; then
printf '%s\n' "${items[@]}" | parallel "$cmd"
else
printf '%s\n' "${items[@]}" | xargs -P 4 -I {} sh -c "$cmd {}"
fi
}
8. 배포 및 패키징¶
스크립트를 배포용으로 준비하기.
단일 파일 스크립트¶
#!/bin/bash
#
# Complete standalone script with everything embedded
#
set -euo pipefail
# Embed configuration
read -r -d '' DEFAULT_CONFIG << 'EOF' || true
DB_HOST=localhost
DB_PORT=5432
EOF
# Embed helper functions
error() { echo "ERROR: $*" >&2; }
info() { echo "INFO: $*"; }
# Main logic
main() {
info "Starting application"
# ...
}
main "$@"
자체 압축 해제 아카이브¶
#!/bin/bash
# Self-extracting script with embedded tarball
ARCHIVE_LINE=$(awk '/^__ARCHIVE__/ {print NR + 1; exit 0; }' "$0")
# Extract embedded archive
tail -n +${ARCHIVE_LINE} "$0" | tar xz -C /tmp
# Run installer
cd /tmp/installer
./install.sh
exit 0
__ARCHIVE__
# Compressed tarball data starts here (created with tar czf)
자체 압축 해제 아카이브 생성:¶
#!/bin/bash
# Create installer package
create_installer() {
local output_file=$1
local source_dir=$2
# Create header script
cat > "$output_file" << 'HEADER'
#!/bin/bash
ARCHIVE_LINE=$(awk '/^__ARCHIVE__/ {print NR + 1; exit 0; }' "$0")
tail -n +${ARCHIVE_LINE} "$0" | tar xz -C /tmp
cd /tmp/installer && ./install.sh
exit 0
__ARCHIVE__
HEADER
# Append tarball
tar czf - -C "$source_dir" . >> "$output_file"
chmod +x "$output_file"
echo "Created: $output_file"
}
create_installer "install.sh" "./installer_files"
Man 페이지¶
#!/bin/bash
# Generate man page
generate_manpage() {
local cmd_name=$1
local output_file=$2
cat > "$output_file" << 'EOF'
.TH MYTOOL 1 "January 2024" "mytool 1.0.0" "User Commands"
.SH NAME
mytool \- Brief description of mytool
.SH SYNOPSIS
.B mytool
[\fIOPTIONS\fR] \fICOMMAND\fR [\fIARGS\fR]
.SH DESCRIPTION
.B mytool
is a tool that does something useful.
Detailed description goes here.
.SH OPTIONS
.TP
.BR \-v ", " \-\-verbose
Enable verbose output
.TP
.BR \-h ", " \-\-help
Show help message
.SH EXAMPLES
.TP
mytool process file.txt
Process a file
.TP
mytool \-v \-\-output=result.txt input.txt
Verbose processing with output file
.SH EXIT STATUS
.TP
.B 0
Success
.TP
.B 1
General error
.SH AUTHOR
Written by Your Name.
.SH REPORTING BUGS
Report bugs to: bugs@example.com
.SH SEE ALSO
Full documentation at: https://example.com/docs
EOF
echo "Man page created: $output_file"
echo "Install with: cp $output_file /usr/local/share/man/man1/"
echo "View with: man $cmd_name"
}
generate_manpage "mytool" "mytool.1"
Bash 자동 완성¶
#!/bin/bash
# Bash completion script for mytool
_mytool_completion() {
local cur prev opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
# Options
opts="-v --verbose -o --output -h --help --version"
# Commands
commands="start stop restart status"
# Complete options
if [[ ${cur} == -* ]]; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
# Complete commands
if [ $COMP_CWORD -eq 1 ]; then
COMPREPLY=( $(compgen -W "${commands}" -- ${cur}) )
return 0
fi
# Complete files for --output
if [[ ${prev} == "-o" ]] || [[ ${prev} == "--output" ]]; then
COMPREPLY=( $(compgen -f -- ${cur}) )
return 0
fi
# Default: complete files
COMPREPLY=( $(compgen -f -- ${cur}) )
}
# Register completion
complete -F _mytool_completion mytool
# Install instructions:
# cp mytool-completion.bash /etc/bash_completion.d/mytool
9. 연습 문제¶
문제 1: POSIX Shell 변환기¶
다음을 수행하는 도구를 만드세요: - Bash 스크립트를 분석하고 Bashism을 식별 - POSIX 호환 대안 제안 - 선택적으로 자동 변환 시도 - 필요한 변경 사항 보고서 생성 - 변환된 스크립트의 구문 오류 테스트 - 테스트를 통해 기능 유지
문제 2: 성능 프로파일러¶
다음을 수행하는 프로파일링 도구를 구축하세요: - 각 함수의 실행 시간을 측정하도록 셸 스크립트 계측 - 병목 지점(가장 느린 함수) 식별 - 각 명령이 호출된 횟수 계산 - 분석 기반 최적화 제안 - 시각적 보고서 생성(HTML 또는 터미널 기반) - "이전"과 "이후" 성능 비교
문제 3: 보안 감사기¶
다음을 수행하는 보안 감사 도구를 개발하세요: - 일반적인 취약점(eval, 인용되지 않은 변수 등) 스캔 - 안전하지 않은 임시 파일 생성 확인 - 잠재적인 명령 인젝션 지점 식별 - 입력 검증 방법 확인 - 파일 권한 및 소유권 확인 - 심각도 수준이 포함된 보안 보고서 생성 - 발견된 각 문제에 대한 수정 제안
문제 4: 패키지 관리자¶
다음을 수행하는 셸 스크립트용 간단한 패키지 관리자를 만드세요:
- 적절한 디렉토리(/usr/local/bin)에 스크립트 설치
- 의존성 관리(필수 명령어 확인)
- 버전 업데이트 처리
- man 페이지 생성 및 설치
- bash 자동 완성 설정
- 정리와 함께 제거 지원
- 설치된 스크립트 레지스트리 유지
문제 5: 테스트 프레임워크¶
다음을 수행하는 셸 스크립트용 테스팅 프레임워크를 구현하세요: - 함수 단위 테스트 지원 - 외부 명령어 모킹 - 출력(stdout/stderr) 캡처 및 검증 - 종료 코드 테스트 - 어서션 제공(assert_equals, assert_contains 등) - 커버리지 보고서 생성 - CI/CD 시스템과 통합 - JUnit 스타일 XML 보고서 생성
이전: 11_Argument_Parsing.md | 다음: 13_Testing.md