Skip to content

Bash 치트시트

스크립팅과 자동화를 위한 Unix 셸 및 명령 언어.

01

변수 & 문자열

변수 & 할당

Bash 변수는 기본적으로 타입이 없는 문자열입니다; 값이 숫자인 경우 산술이 작동합니다. 할당에서 = 주위에 절대 공백을 넣지 마세요 — 'name = Alice'는 name이라는 명령을 실행하려고 시도합니다. 명령 치환에 $(...)를 사용하세요(백틱보다 선호). export는 변수를 자식 프로세스에서 사용 가능하게; readonly는 불변으로 만듭니다. 환경 변수(내보낸)는 스크립트와 시작한 프로그램에 상속됩니다.

bash
# no spaces around = in assignments
name="Alice"
age=30
PI=3.14
active=true

# use variables with $ prefix
echo "Hello, $name!"
echo "Age: $age"

# command substitution: capture command output
today=$(date +%Y-%m-%d)
files=$(ls | wc -l)
echo "Today is $today, $files files"

# readonly and environment variables
readonly MAX=100
export PATH="$PATH:/opt/bin"
env | grep PATH

문자열 매개변수 확장

매개변수 확장은 Bash의 가장 강력한 기능 중 하나이며 ${...}로 둘러싸입니다. ${#var}는 길이를 제공; ${var:offset:length}는 부분 문자열 추출(음수 오프셋은 끝에서부터 계산 — - 앞의 공백에 주의). ,,와 ^^는 대소문자 변환(Bash 4+). :-는 var를 설정하지 않고 기본값을 제공; :=는 부작용으로 설정; :?는 오류로 중단 — 스크립트의 필수 인수 검사에 좋습니다.

bash
s="Hello World"
echo ${#s}                # length: 11
echo ${s:0:5}             # substring: Hello
echo ${s:6}               # from index 6: World
echo ${s: -5}             # last 5 chars: World

# case conversion (Bash 4+)
echo ${s,,}               # lowercase: hello world
echo ${s^^}               # uppercase: HELLO WORLD
echo ${s^l}               # capitalize first if 'l': Hello World

# default values
echo ${name:-Guest}       # use Guest if name unset
echo ${name:=Guest}       # assign Guest if unset, then use
echo ${name:?required}    # error and exit if unset

문자열 검색 & 치환

/ 패턴은 치환; //은 모두 치환; #은 시작에서 매칭; %는 끝에서 매칭. ##와 %%는 탐욕적(가장 긴 매칭)이고 #와 %는 비탐욕적입니다. 이것은 경로 조작에 필수적입니다: ${path##*/}는 basename을 추출(basename 명령처럼), ${path%/*}는 디렉토리를 추출(dirname처럼). 이를 마스터하면 sed와 cut 같은 외부 명령에 대한 많은 호출을 피할 수 있습니다.

bash
s="Hello World Hello"

# replace first occurrence
echo ${s/Hello/Hi}        # Hi World Hello

# replace all occurrences
echo ${s//Hello/Hi}       # Hi World Hi

# replace at beginning / end
echo ${s/#Hello/Hi}       # Hi World Hello (prefix)
echo ${s/%Hello/Hi}       # Hello World Hello (no suffix match)

# delete patterns (replace with empty)
echo ${s//Hello/}         #  World 
path="/usr/local/bin/app"
echo ${path##*/}          # app (longest prefix)
echo ${path%/*}           # /usr/local/bin (shortest suffix)

# extract filename and extension
file="report.tar.gz"
echo ${file%.*}           # report.tar (remove ext)
echo ${file##*.}          # gz (keep ext)

인용 & 특수 문자

작은따옴표는 모든 것을 문자 그대로 보존합니다 — 변수 확장 없음, 이스케이프 처리 없음(작은따옴표 안에 작은따옴표를 포함할 수도 없습니다). 큰따옴표는 $, 백틱, \ 확장을 허용하면서 공백과 특수 문자를 보존합니다 — 단어 분할 및 glob 버그를 방지하기 위해 변수 주위에 거의 항상 큰따옴표를 선호하세요. \n과 \t 같은 ANSI-C 이스케이프에는 $'...'을 사용하세요.

bash
# single quotes: literal, no expansion
echo 'Hello $USER'         # Hello $USER

# double quotes: expansion happens
echo "Hello $USER"         # Hello alice

# escape special chars with backslash
echo "Price: \\$5.00"      # Price: $5.00
echo 'It\'s a test'       # It's a test (escape inside single)

# concatenate strings
a="Hello"
b="World"
c="$a, $b!"
echo $c                    # Hello, World!

# multi-line string
msg="Line 1
Line 2
Line 3"
echo "$msg"

Read & 사용자 입력

read는 사용자 입력을 캡처합니다. -p는 프롬프트 표시, -t는 타임아웃 설정(0이 아닌 값 반환), -a는 배열로 읽습니다. 파일을 줄별로 읽을 때 항상 'IFS= read -r'을 사용하세요 — IFS=는 선행/후행 공백을 보존하고, -r은 백슬래시 이스케이프를 비활성화하여 백슬래시가 문자 그대로 보존됩니다. 이것이 파일을 줄별로 처리하는 정석적인 안전 패턴입니다.

bash
# read a single value
echo -n "Enter your name: "
read name
echo "Hi, $name!"

# read multiple values
read -p "First and last: " first last

# read with timeout (seconds)
read -t 5 -p "Quick! " answer || echo "too slow"

# read into an array
echo "Enter colors:"
read -a colors
echo "First: ${colors[0]}"

# read line by line from a file
while IFS= read -r line; do
    echo ">>> $line"
done < file.txt
02

배열

인덱스 배열

Bash 배열은 0부터 인덱싱됩니다. @와 *는 모든 요소로 확장 — 공백이 있는 요소를 올바르게 처리하려면 항상 인용하세요("${arr[@]}"). ${#arr[@]}는 개수를 제공. +=는 추가. unset은 인덱스에 간격을 남깁니다; 재인덱스하려면 arr=("${arr[@]}")을 사용하세요. ${arr[@]:start:count}로 슬라이싱하면 부분 집합을 반환합니다.

bash
# create and populate
fruits=("apple" "banana" "cherry")
fruits[3]="date"

# access elements
echo ${fruits[0]}         # apple
echo ${fruits[@]}         # all: apple banana cherry date
echo ${fruits[*]}         # same as @ in most contexts

# count and slice
echo ${#fruits[@]}        # count: 4
echo ${fruits[@]:1:2}     # elements 1-2: banana cherry

# append
fruits+=("elderberry")

# iterate
for f in "${fruits[@]}"; do
    echo "- $f"
done

# delete an element
unset fruits[1]            # removes "banana" (gap remains)

연관 배열(맵)

연관 배열(declare -A)은 문자열 키를 값에 매핑합니다, 다른 언어의 사전처럼 — Bash 4+에서 사용 가능. 키를 얻으려면 ${!arr[@]}, 값은 ${arr[@]}를 사용하세요. -v 테스트는 키가 존재하는지 확인합니다. 인덱스 배열과 달리 키는 임의의 문자열이므로 공백이 있는 키를 인용하는 것이 필수입니다. 이것은 Bash 4+가 필요합니다(macOS는 기본적으로 Bash 3을 제공).

bash
# must declare before use (Bash 4+)
declare -A ages

# key-value assignment
ages[alice]=30
ages[bob]=25
ages["carol smith"]=28

# access by key
echo ${ages[alice]}       # 30
echo ${ages[bob]}         # 25

# all keys and values
echo ${!ages[@]}          # keys: alice bob carol smith
echo ${ages[@]}           # values: 30 25 28
echo ${#ages[@]}          # count: 3

# iterate keys
for name in "${!ages[@]}"; do
    echo "$name is ${ages[$name]}"
done

# check if key exists
if [[ -v ages[alice] ]]; then
    echo "alice exists"
fi

배열 반복 패턴

세 가지 주요 반복 스타일이 있습니다: 값별("${arr[@]}"에서 for x), 인덱스별("${!arr[@]}"에서 for i), C 스타일. 위치가 필요할 때 인덱스별 형식이 유용합니다. 공백이 있는 요소가 단일 항목으로 보존되도록 항상 "${arr[@]}"를 인용하세요. 루프에서 +=로 배열을 빌드는 것이 결과를 누적하는 관용적인 방법입니다.

bash
arr=(one two three four five)

# iterate by value
for item in "${arr[@]}"; do
    echo "$item"
done

# iterate by index
for i in "${!arr[@]}"; do
    echo "[$i] = ${arr[$i]}"
done

# C-style indexed loop
for ((i=0; i<${#arr[@]}; i++)); do
    echo "${arr[$i]}"
done

# map/transform: build a new array
upper=()
for w in "${arr[@]}"; do
    upper+=("$(echo $w | tr a-z A-Z)")
done
echo "${upper[@]}"

명령 출력에서 배열

mapfile/readarray(Bash 4+)는 줄을 배열로 한 번에 읽습니다 — 큰 파일의 경우 while 루프보다 훨씬 빠릅니다. 구분된 문자열을 분할하려면 IFS를 설정하고 read -ra를 사용하세요. 파일 이름의 경우(공백이나 줄 바꿈이 포함될 수 있음), 항상 find -print0와 read -d ''를 사용하여 널 바이트로 분할하세요 — 파일 이름에 대한 유일한 안전한 구분자입니다. < <(...) 프로세스 치환은 서브셸 없이 루프에 공급합니다.

bash
# split a string into an array
csv="a,b,c,d"
IFS=',' read -ra parts <<< "$csv"
echo ${parts[2]}          # c

# read lines of a file into an array
mapfile -t lines < file.txt
echo "${lines[0]}"        # first line
echo "${#lines[@]}"       # line count

# alternative: readarray (same as mapfile)
readarray -t lines2 < file.txt

# from command output (words split on IFS)
files=($(ls *.txt))
echo "${files[@]}"

# safer: read null-delimited output
while IFS= read -r -d '' f; do
    files+=("$f")
done < <(find . -name "*.txt" -print0)

특수 변수

이 특수 변수는 스크립팅에 필수적입니다. $0은 스크립트 경로; $1-$9는 위치 인수(두 자리는 ${10} 사용). 공백이 있는 인수를 보존하려면 항상 "$@"를 인용하세요 — "$*"는 하나의 문자열로 결합합니다. $?는 종료 상태(0 = 성공)를 제공하며 모든 오류 검사의 기초입니다. $$와 $!는 PID 파일과 프로세스 관리에 유용합니다. shift는 $1을 폐기하고 나머지를 아래로 이동합니다.

bash
# positional parameters
echo $0       # script name
echo $1       # first argument
echo $#       # argument count
echo $@       # all arguments (separate words)
echo "$@"     # all arguments (preserves quoting)
echo $*       # all arguments (single string)

# process and shell info
echo $$       # current shell PID
echo $!       # PID of last background command
echo $?       # exit status of last command (0 = success)
echo $-       # current shell option flags
echo $_       # last argument of previous command

# shift arguments
echo "$1 $2"  # a b
shift
echo "$1 $2"  # b c

# iterate all args safely
for arg in "$@"; do
    echo "arg: $arg"
done
03

제어 흐름 & 테스트

If / Elif / Else

[ ]는 POSIX 테스트 명령입니다(이식 가능하지만 제한적); [[ ]]는 패턴 매칭(와일드카드가 있는 ==), 정규식(=~), && / || 연산자를 지원하고 변수를 인용할 필요가 없는 Bash의 향상된 버전입니다. Bash 스크립트에서 [[ ]]를 선호하세요. [ ]에서 빈 값이나 공백이 포함된 값으로 인한 구문 오류를 피하기 위해 항상 변수를 인용하세요. -f, -r, -d, -e 테스트는 파일 존재와 속성을 검사합니다.

bash
# basic if
if [ "$age" -ge 18 ]; then
    echo "adult"
elif [ "$age" -ge 13 ]; then
    echo "teen"
else
    echo "child"
fi

# modern [[ ]] test (Bash-specific, safer)
if [[ $name == "Alice" ]]; then
    echo "hi Alice"
fi

# pattern matching in [[ ]]
if [[ $file == *.txt ]]; then
    echo "text file"
fi

# regex matching
if [[ $email =~ ^[a-z]+@[a-z]+.[a-z]+$ ]]; then
    echo "valid email"
fi

# multiple conditions
if [[ -f file.txt && -r file.txt ]]; then
    echo "readable file"
fi

테스트 연산자

파일 테스트(-f, -d, -r 등)는 파일 시스템 속성을 검사합니다. 문자열 테스트는 =와 !=(또는 [[ ]]에서 ==)를 사용; -z는 빈 문자열, -n은 비어 있지 않은 문자열 검사. 정수 비교는 -eq, -ne, -lt, -gt, -le, -ge를 사용 — [ ]에서 정수에 < >를 사용하지 마세요(리다이렉트됩니다!). [[ ]]와 (( ))에서는 익숙한 < > <= >= 연산자를 사용할 수 있습니다. 올바른 조건문을 작성하려면 이 연산자들을 암기하는 것이 필수적입니다.

bash
# file tests
[ -f file ]       # exists and is regular file
[ -d dir ]        # exists and is directory
[ -e path ]       # exists (any type)
[ -r file ]       # readable
[ -w file ]       # writable
[ -x file ]       # executable
[ -s file ]       # exists and is non-empty
[ file1 -nt file2 ]  # newer than
[ file1 -ot file2 ]  # older than

# string tests
[ -z "$s" ]       # empty string
[ -n "$s" ]       # non-empty string
[ "$a" = "$b" ]   # equal (use == in [[ ]])
[ "$a" != "$b" ]  # not equal

# integer tests
[ $n -eq 5 ]      # equal
[ $n -ne 5 ]      # not equal
[ $n -lt 5 ]      # less than
[ $n -gt 5 ]      # greater than
[ $n -le 5 ]      # less or equal
[ $n -ge 5 ]      # greater or equal

Case 문

case는 Bash의 switch 동등물입니다 — 값을 glob 패턴에 대해 매칭합니다. |는 대안을 분리. ;;는 분기를 종료(break처럼). *) 패턴은 기본값입니다. 패턴은 와일드카드(*, ?, [abc])를 지원하지만 전체 정규식은 아닙니다. case는 단일 값에 대한 디스패치를 위해 긴 if-elif 체인보다 깔끔하며, 명령줄 하위 명령(start/stop/restart)을 구축하는 표준 방법입니다.

bash
case $1 in
    start)
        echo "Starting service..."
        systemctl start app
        ;;
    stop)
        echo "Stopping service..."
        systemctl stop app
        ;;
    restart)
        $0 stop
        $0 start
        ;;
    status)
        systemctl status app
        ;;
    --help|-h)
        echo "Usage: $0 {start|stop|restart|status}"
        ;;
    *)
        echo "Unknown command: $1" >&2
        exit 1
        ;;
esac

# pattern matching in case
case $file in
    *.jpg|*.png)    echo "image" ;;
    *.mp3|*.wav)    echo "audio" ;;
    *.txt)          echo "text" ;;
    *)              echo "other" ;;
esac

산술 & (( ))

$(( ))는 산술 표현식을 평가하고 결과를 반환합니다. (( )) 내부에서 변수 이름에는 $가 필요 없습니다 — 이름만 사용하세요. (( ))는 명령으로 결과가 0이 아닌 경우 종료 상태 0(참)을 반환하여 숫자 조건에 이상적입니다. Bash는 정수 산술만 수행합니다; 부동소수점에는 bc나 awk를 사용하세요. 연산자: + - * / % ** 및 C 스타일 비교 및 비트 연산자가 모두 작동합니다.

bash
# arithmetic expansion
x=5
y=$((x + 3))
echo $y              # 8
echo $((x * 2))      # 10
echo $((2 ** 10))    # 1024 (exponent)
echo $((17 % 5))     # 2 (modulo)
echo $((17 / 5))     # 3 (integer division)

# (( )) condition: no $ needed, returns exit status
x=10
if (( x > 5 && x < 20 )); then
    echo "in range"
fi

# increment / decrement
((x++))             # post-increment
((x--))             # post-decrement
((++x))             # pre-increment

# bitwise operators
echo $((5 & 3))     # 1 (AND)
echo $((5 | 2))     # 7 (OR)
echo $((5 << 1))    # 10 (left shift)

단락 & 삼항

&&와 ||는 간결한 제어 흐름으로 두 배 역할을 하는 단락 연산자입니다: 'cmd1 && cmd2'는 cmd1이 성공할 때만 cmd2를 실행; 'cmd1 || cmd2'는 cmd1이 실패할 때만 cmd2를 실행. 이는 한 줄로 간단한 if-else를 대체합니다. 'A && B || C' 패턴은 삼항을 모방하지만 B가 실패할 수 있는 경우 미묘하게 버그가 있습니다 — 견고한 코드의 경우 실제 if 문을 사용하세요. 콜론(:)은 빈 분기에 유용한 no-op 명령입니다.

bash
# && and || as short-circuit control flow
[[ -f file.txt ]] && echo "exists"
[[ -f file.txt ]] || echo "missing"

# combined: do this OR fail
mkdir -p build && cd build && make || exit 1

# ternary-like (Bash doesn't have a real ternary)
result=$([[ $x -gt 0 ]] && echo "positive" || echo "non-positive")

# default value via &&
echo ${name:-"anonymous"}

# run command, fallback on failure
ping -c1 host && echo "up" || echo "down"

# null command as no-op
if condition; then
    :    # do nothing
fi
04

루프 & 반복

For 루프

for 루프는 단어 목록을 반복합니다. 중괄호 확장 {1..5}는 시퀀스를 생성합니다(선택적 단계 {start..end..step} 포함). C 스타일 for ((init; cond; update))는 숫자 카운터에 가장 적합합니다. *.txt로 파일을 반복할 때 인용되지 않은 glob는 안전하게 확장되지만, 일치하는 파일이 없으면 리터럴 '*.txt'를 얻습니다 — 빈 목록을 대신 얻으려면 shopt -s nullglob를 활성화하세요.

bash
# iterate a list
for fruit in apple banana cherry; do
    echo "$fruit"
done

# iterate an array
colors=("red" "green" "blue")
for c in "${colors[@]}"; do
    echo "Color: $c"
done

# range with brace expansion
for i in {1..5}; do
    echo "Number: $i"
done

# range with step
for i in {0..20..2}; do echo $i; done    # 0 2 4 ... 20

# C-style for loop
for ((i=0; i<5; i++)); do
    echo "Iteration $i"
done

# iterate command output
for file in *.txt; do
    echo "Processing $file"
done

While & Until 루프

while은 조건이 성공하는 동안(종료 0) 실행; until은 성공할 때까지 실행 — 반대입니다. 'while read' 패턴은 파일을 줄별로 처리하는 정석적 방법입니다; 안전을 위해 항상 'IFS= read -r'을 사용하세요. while 루프로 파이핑하면 서브셸에서 실행되므로 변수 변경이 지속되지 않습니다 — 변수를 유지해야 할 때 프로세스 치환 '< <(cmd)'을 대신 사용하세요.

bash
# while: runs while condition is true
count=0
while [ $count -lt 5 ]; do
    echo "Count: $count"
    ((count++))
done

# read lines from a file
while IFS= read -r line; do
    echo "Line: $line"
done < input.txt

# infinite loop with break
while true; do
    read -p "Quit? (y/n) " ans
    [[ $ans == "y" ]] && break
done

# until: runs until condition is true (opposite of while)
n=0
until [ $n -ge 3 ]; do
    echo "n=$n"
    ((n++))
done

# while reading from a pipeline
seq 1 5 | while read n; do
    echo "got $n"
done

Break, Continue & Select

break는 루프를 종료(break N은 N개의 중첩 루프 종료); continue는 다음 반복으로 점프. select는 목록에서 대화형 번호 매기기 메뉴를 만듭니다 — 중단될 때까지 반복합니다. 이 제어 문은 for, while, until 루프에서 작동합니다. break N 형식은 깊이 중첩된 루프를 탈출하는 데 필수적이지만 코드를 따라가기 어렵게 만들 수 있으므로 드물게 사용하세요.

bash
# break exits the loop
for i in {1..10}; do
    [[ $i -eq 5 ]] && break
    echo $i       # prints 1 2 3 4
done

# continue skips to next iteration
for i in {1..6}; do
    (( i % 2 == 0 )) && continue
    echo $i       # prints 1 3 5 (odd only)
done

# break out of nested loops
for i in 1 2 3; do
    for j in a b c; do
        [[ $j == "b" ]] && break 2   # break both loops
        echo "$i$j"
    done
done

# select menu
select opt in "Start" "Stop" "Quit"; do
    case $opt in
        Start) echo "starting" ;;
        Stop)  echo "stopping" ;;
        Quit)  break ;;
        *)     echo "invalid" ;;
    esac
done

안전하게 파일 반복

ls 출력 파싱은 고전적 버그입니다 — 공백, 줄 바꿈 또는 특수 문자가 있는 파일 이름이 손상시킵니다. for 루프에서 셸 glob(*.txt)을 직접 사용하고 변수를 인용하세요. 재귀 또는 복잡한 검색의 경우 find -print0을 'read -d '''에 파이핑하세요 — 널 바이트가 파일 이름에 대한 유일한 안전한 구분자입니다. 일치하지 않는 glob이 리터럴 패턴 대신 빈 목록을 생성하도록 nullglob를 활성화하세요.

bash
# BAD: breaks on filenames with spaces
for f in $(ls *.txt); do
    cat "$f"
done

# GOOD: glob expands safely
for f in *.txt; do
    [[ -f "$f" ]] || continue
    cat "$f"
done

# BEST: handle spaces, newlines, and no matches
shopt -s nullglob dotglob
for f in *.txt; do
    echo "Processing: $f"
done

# find with -print0 and while read -d ''
while IFS= read -r -d '' f; do
    echo "Found: $f"
done < <(find . -type f -name "*.txt" -print0)

# process files modified today
for f in $(find . -mtime -1 -print); do
    echo "Recent: $f"
done

범위 & 시퀀스 생성

중괄호 확장 {a..b}는 파싱 시간에 시퀀스를 생성합니다 — 빠르고 내장. 문자, 숫자, 제로 패딩 및 단계를 지원합니다({start..end..step}). seq는 더 많은 포맷 옵션(-f는 printf 스타일)이 있는 외부 명령입니다. 중괄호 확장은 또한 여러 인수를 만듭니다: echo file{1..3}.{txt,log}는 6개의 파일 이름을 생성합니다. 간단한 범위에는 중괄호 확장을 선호하세요; 사용자 정의 포맷이 필요할 때 seq를 사용하세요.

bash
# brace expansion (not a loop, but generates lists)
echo {1..10}                 # 1 2 3 4 5 6 7 8 9 10
echo {a..e}                  # a b c d e
echo {01..10}                # zero-padded: 01 02 ... 10
echo file{1..3}.txt          # file1.txt file2.txt file3.txt

# seq command
seq 1 5                      # 1 2 3 4 5
seq 1 2 10                   # 1 3 5 7 9 (step 2)
seq -f "%03d" 1 5            # 001 002 003 004 005

# use seq in a for loop
for i in $(seq 1 5); do
    echo $i
done

# generate a list of dates
for d in $(seq -f "2024-01-%02g" 1 31); do
    echo "$d"
done
05

함수

함수 정의 & 호출

Bash의 함수는 name() 또는 function name으로 정의됩니다. 괄호 없이 이름으로 공백으로 구분된 인수를 전달하여 호출하세요. 전역 범위 오염을 피하기 위해 함수 내 변수에는 항상 'local'을 사용하세요 — 사용하지 않으면 할당이 누출됩니다. Bash 함수는 직접 값을 반환할 수 없습니다(return은 종료 상태 0-255); 문자열을 반환하려면 echo하고 $()로 캡처하세요.

bash
# two equivalent syntaxes
greet() {
    echo "Hello, $1!"
}
function greet2() {
    echo "Hi, $1!"
}

# call with arguments (no parens)
greet "Alice"               # Hello, Alice!
greet2 "Bob"                # Hi, Bob!

# local variables (function-scoped)
counter() {
    local count=0
    ((count++))
    echo $count
}

# return a value via echo + command substitution
add() {
    echo $(($1 + $2))
}
result=$(add 3 4)           # 7

인수 & 매개변수

함수 내부에서 $1, $@, $#는 스크립트의 인수가 아닌 함수의 인수를 참조합니다 — 스크립트 수준 매개변수를 가립니다. $FUNCNAME은 함수 이름을 보유합니다(디버깅에 유용). 함수 내부의 shift는 함수의 매개변수에만 영향을 미칩니다. 스크립트의 인수를 함수에 전달하려면 func "$@"를 사용하세요. 이 가리기 때문에 함수는 재사용 가능한 빌딩 블록입니다.

bash
# all positional params work inside functions
show_args() {
    echo "Function name: $FUNCNAME"
    echo "First arg:  $1"
    echo "Second arg: $2"
    echo "All args:   $@"
    echo "Arg count:  $#"
}

show_args a b c
# Function name: show_args
# First arg:  a
# Second arg: b
# All args:   a b c
# Arg count:  3

# pass all script args to a function
process "$@"

# shift within a function (local to it)
parse() {
    while (($#)); do
        echo "arg: $1"
        shift
    done
}

반환 값 & 종료 상태

Bash의 'return'은 종료 상태(0-255)만 설정하므로 함수는 부울 테스트로 두 배 역할을 합니다. 문자열을 반환하려면 echo하고 $()로 캡처하세요. 여러 값의 경우 nameref(local -n) — Bash 4.3+ — 를 사용하여 호출자의 변수를 이름으로 할당할 수 있게 합니다. 이것이 복잡한 데이터를 반환하는 가장 깨끗한 방법입니다. 함수를 재진입 불가능하게 만들므로 반환 값에 전역 변수를 사용하지 마세요.

bash
# return sets exit status (0-255), not a value
is_even() {
    if (($1 % 2 == 0)); then
        return 0    # true/success
    else
        return 1    # false/failure
    fi
}

# use as a condition
if is_even 4; then
    echo "4 is even"
fi

# chain with && and ||
is_even 4 && echo "yes" || echo "no"

# return a string via echo (capture with $())
to_upper() {
    echo "${1^^}"
}
UP=$(to_upper hello)        # HELLO

# return multiple values via global or nameref
set_pair() {
    local -n _r=$1
    _r=(10 20)
}
set_pair result
echo "${result[@]}"       # 10 20

변수 범위

Bash의 변수는 기본적으로 전역입니다 — 함수 내부에서 할당된 경우에도요! 이는 흔한 버그 원인입니다. 함수 내부 변수에는 항상 'local'을 사용하세요. 'declare -g'는 함수 내에서 전역을 명시적으로 만듭니다. 재귀는 작동하지만 Bash에서 느립니다(각 호출이 $()를 위해 서브셸을 포크); 드물게 사용하세요. factorial 예제가 패턴을 보여줍니다: 지역 변수 + 재귀 호출 + 산술.

bash
# global by default
x=1
modify() {
    x=2          # changes the GLOBAL x!
    local y=3    # local to function
    echo "inside: x=$x y=$y"
}

modify
echo "outside: x=$x"       # x=2 (modified!)
echo "outside: y=$y"       # empty (y is local)

# declare -g for global inside a function
set_global() {
    declare -g NEW_VAR=42
}
set_global
echo $NEW_VAR              # 42

# recursion with local
factorial() {
    local n=$1
    if (( n <= 1 )); then
        echo 1
    else
        local prev=$(factorial $((n - 1)))
        echo $((n * prev))
    fi
}
echo $(factorial 5)        # 120

라이브러리 & 소싱

source(또는 .)는 현재 셸에서 파일을 실행하여 함수와 변수를 사용 가능하게 합니다 — 이것이 재사용 가능한 라이브러리를 구축하는 방법입니다. 일반적인 패턴은 스크립트가 소싱하는 도우미 함수가 있는 utils.sh입니다. 스크립트 실행(서브셸에서 실행)과 달리 소싱된 코드는 호출자의 환경을 수정할 수 있습니다. 이것은 .bashrc와 .bash_profile이 작동하는 방식이기도 합니다 — 셸 시작 시 소싱됩니다.

bash
# utils.sh — a reusable library
#!/bin/bash
log() {
    echo "[$(date +%H:%M:%S)] $*" >&2
}

die() {
    log "ERROR: $*"
    exit 1
}

confirm() {
    read -p "$1 [y/N] " ans
    [[ $ans =~ ^[Yy]$ ]]
}

# main.sh — use the library
source ./utils.sh        # or . ./utils.sh

log "Starting script"
confirm "Continue?" || die "aborted"
log "Done"

# source vs execute
# source: runs in current shell (shares variables)
# ./script.sh: runs in a subshell (isolated)
06

텍스트 처리

grep — 패턴 검색

grep은 패턴과 일치하는 줄을 찾습니다. -i는 대소문자 무시, -v는 반전, -n은 줄 번호 표시, -r은 재귀, -E는 확장 정규식 사용(+, |, {} 등). -A/-B/-C는 매칭 주변의 컨텍스트 줄 표시 — 오류 이해에 매우 유용. 재귀 검색 중 파일 유형을 필터링하려면 --include를 사용하세요. grep은 매칭이 발견되면 종료 상태 0을 반환하여 조건에서 유용합니다: if grep -q pattern file; then...

bash
# basic search
grep "error" app.log
grep -i "error" app.log          # case-insensitive
grep -v "debug" app.log          # invert (non-matching lines)
grep -c "error" app.log          # count matches
grep -n "error" app.log          # line numbers
grep -w "error" app.log          # whole word only

# recursive search
grep -rn "TODO" src/             # recursive + line numbers
grep -rl "TODO" src/             # files with matches only

# extended regex (or use egrep)
grep -E "error|warn|fatal" log
grep -E "^[0-9]{4}-" log         # lines starting with date

# context lines
grep -A 2 "error" log            # 2 lines after
grep -B 2 "error" log            # 2 lines before
grep -C 2 "error" log            # 2 lines before and after

# search multiple files
grep "function" *.js --include="*.js" -r .

sed — 스트림 에디터

sed는 비대화형으로 텍스트 스트림을 편집합니다. s 명령은 치환; g는 전역으로 만듭니다. -i는 제자리 편집(항상 -i 없이 먼저 테스트!). 구분자는 임의의 문자일 수 있습니다 — 슬래시 이스케이프를 피하기 위해 경로에 | 또는 #을 사용하세요. sed는 줄별로 처리하므로 여러 줄 작업은 N 명령이나 홀드 공간이 필요합니다. 복잡한 편집의 경우 awk나 perl이 더 명확할 수 있습니다. 셸 확장을 방지하려면 항상 sed 스크립트를 인용하세요.

bash
# substitute (replace)
sed 's/old/new/' file.txt           # first occurrence per line
sed 's/old/new/g' file.txt          # all occurrences (global)
sed 's/old/new/3' file.txt          # 3rd occurrence only
sed 's/old/new/gi' file.txt         # global + case-insensitive

# in-place editing
sed -i 's/foo/bar/g' file.txt       # edit file in place
sed -i.bak 's/foo/bar/g' file.txt   # keep a backup

# delete lines
sed '/^#/d' file.txt                # delete comment lines
sed '/^$/d' file.txt                # delete blank lines
sed '5d' file.txt                   # delete line 5
sed '5,10d' file.txt                # delete lines 5-10

# print specific lines
sed -n '10,20p' file.txt            # print lines 10-20
sed -n '/pattern/p' file.txt        # print matching lines

# use a different delimiter for paths
sed 's|/usr/local|/opt|g' paths.txt

# multiple commands
sed -e 's/a/A/g' -e 's/b/B/g' file.txt

awk — 열 처리

awk는 열 데이터를 위한 미니 프로그래밍 언어입니다. $1, $2...는 필드; $0은 전체 줄; $NF는 마지막 필드. -F는 입력 구분자 설정; OFS는 출력 설정. BEGIN은 처리 전에 실행, END는 후에 실행. NR은 레코드(줄) 번호; NF는 필드 개수. awk는 CSV/TSV 처리, 로그 분석 및 보고서 생성에 이상적입니다 — 간단한 추출 이상에서는 cut보다 훨씬 강력합니다.

bash
# print columns (default separator: whitespace)
awk '{print $1}' file.txt           # first column
awk '{print $1, $3}' file.txt       # columns 1 and 3
awk '{print $NF}' file.txt          # last column
awk -F: '{print $1}' /etc/passwd    # split on :

# filter and print
awk '$3 > 100' file.txt             # lines where col 3 > 100
awk '$1 == "ERROR" {print $2}' log  # col 2 of ERROR lines
awk 'NR == 5' file.txt              # 5th line only
awk 'NR >= 10 && NR <= 20' file.txt # lines 10-20

# BEGIN and END blocks
awk 'BEGIN {print "Start"} {sum += $1} END {print sum}' nums.txt

# field separator and output
awk -F, 'BEGIN {OFS="|"} {print $1, $2}' csv.txt

# count lines matching a pattern
awk '/error/' file.txt              # like grep
awk '/error/ {count++} END {print count}' log

# built-in variables: NR (row), NF (fields), FS, OFS
awk '{print NR, NF, $0}' file.txt

cut, tr, sort & uniq

cut은 간단한 필드/문자 추출입니다 — 빠르지만 제한적(인용 지원 없음). tr은 문자 변환(대소문자 변환이나 구분자 교체에 좋음)하고 -d는 삭제. sort는 줄 정렬(-n 숫자, -r 역순, -k 필드). uniq는 인접한 중복만 제거하므로 항상 sort를 먼저 파이핑하세요. uniq -c와 sort -rn은 빈도 분석의 고전적 패턴입니다: 'sort | uniq -c | sort -rn'이 가장 일반적인 줄을 보여줍니다.

bash
# cut: extract fields or characters
cut -d: -f1 /etc/passwd            # field 1, delimiter :
cut -c1-5 file.txt                 # characters 1-5
cut -d, -f2,3 data.csv             # fields 2 and 3

# tr: translate or delete characters
echo "Hello" | tr 'a-z' 'A-Z'      # HELLO
echo "a,b,c" | tr ',' ' '          # a b c
echo "hello" | tr -d 'l'           # heo (delete)
tr -s ' ' < file.txt               # squeeze repeated spaces

# sort lines
sort file.txt                      # alphabetical
sort -n nums.txt                   # numeric
sort -rn nums.txt                  # reverse numeric
sort -t: -k3 -n /etc/passwd        # by 3rd field, numeric
sort -u file.txt                   # sort + unique

# uniq: filter or count adjacent duplicates
sort file.txt | uniq               # unique lines
sort file.txt | uniq -c            # count occurrences
sort file.txt | uniq -d            # only duplicates
sort file.txt | uniq -u            # only unique lines

파이프 & 리다이렉션

파이프는 stdout을 stdin에 연결하여 강력한 파이프라인을 구축합니다. >는 stdout 리다이렉트(>>는 추가), 2>는 stderr 리다이렉트, &>는 둘 다 캡처. Here-doc(<<EOF)은 여러 줄 문자열 공급; 구분자 인용('EOF')은 변수 확장을 비활성화. 프로세스 치환 <(cmd)는 명령의 출력을 임시 파일로 취급 — 서브셸 없이 while 루프에 공급하고 diff처럼 파일을 기대하는 명령에 필수적.

bash
# pipe: chain commands (stdout of one -> stdin of next)
cat file | grep "x" | sort | uniq -c | sort -rn | head

# redirect stdout
echo "hello" > file.txt            # overwrite
echo "world" >> file.txt           # append

# redirect stderr
command 2> error.log               # stderr to file
command 2>&1                       # stderr to stdout
command > all.log 2>&1             # both to same file
command &> all.log                 # both (Bash shortcut)

# stdin from file
command < input.txt

# here-string
grep "x" <<< "some text with x"

# here-document (multi-line input)
cat << EOF
Line 1
Line 2 with $VAR expansion
EOF

# quoted delimiter disables expansion
cat << 'EOF'
Literal $VAR no expansion
EOF

# process substitution
diff <(ls dir1) <(ls dir2)         # compare outputs
while read line; do echo "$line"; done < <(grep x file)
07

파일 & 디렉토리 작업

find — 파일 검색

find는 가장 강력한 파일 검색 도구입니다. -name은 glob 매칭(대소문자 무시는 -iname); -type은 f/d/l 필터; -mtime/-mmin은 수정 시간으로 필터(- = 이내, + = 보다 오래됨); -size는 크기로 필터. -exec는 각 결과에서 명령 실행; {}는 파일 이름, \;는 파일별 실행, +는 일괄 처리. -delete는 삭제를 위해 -exec rm보다 빠르지만 안전합니다. 항상 -delete 없이 먼저 find를 테스트하세요!

bash
# search by name
find . -name "*.js"
find . -iname "*.JS"               # case-insensitive
find / -name "*.conf" 2>/dev/null  # suppress permission errors

# search by type
find . -type f -name "*.txt"       # files only
find . -type d -name src           # directories only
find . -type l                     # symlinks

# search by time and size
find . -mtime -7                   # modified < 7 days ago
find . -mtime +30                  # modified > 30 days ago
find . -mmin -60                   # modified < 60 min ago
find . -size +10M                  # larger than 10MB
find . -size -1k                   # smaller than 1KB
find . -empty                      # empty files/dirs

# act on results (-exec)
find . -name "*.log" -exec rm {} \;
find . -name "*.js" -exec wc -l {} +
find . -type f -exec chmod 644 {} \;

# safe deletion with -delete
find /tmp -type f -mtime +7 -delete

권한 & 소유권

권한은 세 개의 세트: 소유자, 그룹, 기타. 각 숫자는 r(4)+w(2)+x(1)이므로 755 = rwxr-xr-x. chmod는 권한 변경; chown은 소유자/그룹 변경. 기호 표기(u+x, g-w)는 증분 변경에 더 명확. umask는 새 파일의 기본 권한 설정(파일은 666에서, 디렉토리는 777에서 차감). 웹 서버의 경우 디렉토리는 755, 파일은 644가 표준; 프로덕션에서 777을 절대 사용하지 마세요.

bash
# view permissions
ls -l file.txt
# -rw-r--r-- 1 user group 1024 Jan 1 10:00 file.txt
#  ^^^ ^^^ ^^^
#  owner group others

# chmod: change permissions
chmod 755 script.sh        # rwxr-xr-x
chmod 644 file.txt         # rw-r--r--
chmod +x script.sh         # add execute for all
chmod u+x,g-w file.txt     # symbolic: user +x, group -w
chmod -R 755 directory/    # recursive

# chown: change owner
chown alice file.txt
chown alice:developers file.txt
chown -R alice:developers project/

# umask: default permissions for new files
umask                      # show current (e.g. 022)
umask 077                  # new files: 600, dirs: 700

# numeric permission reference:
# 7=rwx 6=rw 5=rx 4=r 3=wx 2=w 1=x 0=none

복사, 이동, 제거 & 링크

cp -r은 디렉토리 재귀 복사; -i는 덮어쓰기 전 프롬프트(더 안전); -p는 타임스탬프와 권한 보존. mv는 이동이자 이름 변경. rm -rf는 위험 — 프롬프트 없이 재귀 삭제; 항상 경로를 다시 확인. 하드 링크는 동일한 inode를 가리킴(동일한 파일, 파일 시스템을 가로지르거나 디렉토리에 링크 불가); 심볼릭 링크는 경로 참조(무엇이든 링크 가능하지만 대상이 이동하면 깨짐). 심볼릭 링크에는 ln -s를 사용하세요.

bash
# copy
cp file.txt backup.txt
cp -r src/ dest/            # recursive (directories)
cp -i file.txt dest/        # interactive (prompt before overwrite)
cp -u file.txt dest/        # update (only if newer)
cp -p file.txt dest/        # preserve attributes

# move / rename
mv old.txt new.txt
mv file.txt /other/dir/
mv -i file.txt dest/        # prompt before overwrite

# remove
rm file.txt
rm -r directory/            # recursive
rm -f file.txt              # force (no error if missing)
rm -rf node_modules/        # force + recursive (DANGER)

# links
ln target hardlink          # hard link (same inode)
ln -s target symlink        # symbolic link (path reference)
ln -sf target symlink       # force recreate symlink

# view link target
readlink symlink
readlink -f symlink         # canonical absolute path

아카이브 & 압축

tar는 파일을 하나의 아카이브로 묶음; gzip/bzip2/xz가 압축. 플래그: c=생성, x=추출, t=목록, f=파일, z=gzip, j=bzip2, J=xz, v=자세히. .tar.gz는 Unix 표준; .zip은 Windows에서 일반. bzip2는 gzip보다 잘 압축하지만 느림; xz는 최고지만 가장 느림. 특정 디렉토리로 추출하려면 -C를 사용하세요. 압축 시 원본을 보존하려면 -k 플래그를 사용하세요.

bash
# tar + gzip (most common)
tar -czf archive.tar.gz dir/      # create
tar -xzf archive.tar.gz           # extract
tar -xzf archive.tar.gz -C /opt/  # extract to /opt
tar -tzf archive.tar.gz           # list contents

# tar + bzip2 (better compression)
tar -cjf archive.tar.bz2 dir/
tar -xjf archive.tar.bz2

# zip / unzip
zip -r archive.zip dir/
unzip archive.zip
unzip archive.zip -d /target

# gzip / gunzip (single files)
gzip file.txt           # -> file.txt.gz
gunzip file.txt.gz      # -> file.txt
gzip -k file.txt        # keep original

# xz (best compression)
tar -cJf archive.tar.xz dir/
tar -xJf archive.tar.xz

# mnemonic: eXtract, Create, List, File, gZip, bZip2, xz

파일 내용 & 비교

cat은 전체 파일 덤프; head/tail은 끝 표시; tail -f는 라이브 업데이트 스트리밍(로그에 필수). less는 검색(/)과 탐색이 있는 대화형 페이저. diff는 파일 비교; -u는 patch가 사용하는 통합 형식 생성. comm은 정렬된 두 파일을 비교하여 각각에 고유한 줄이나 공통 줄 표시 — 목록 비교에 유용. comm은 정렬된 파일이 필요하므로 항상 입력을 먼저 정렬하세요.

bash
# view file contents
cat file.txt               # entire file
head -n 20 file.txt        # first 20 lines
tail -n 20 file.txt        # last 20 lines
tail -f app.log            # follow (live updates)
less file.txt              # pager (q to quit, / to search)

# line/word/char count
wc file.txt                # lines words bytes
wc -l file.txt             # lines only
wc -w file.txt             # words only

# compare files
diff file1.txt file2.txt
diff -u file1.txt file2.txt    # unified format (for patches)
diff -r dir1/ dir2/            # recursive

# patch
diff -u old new > change.patch
patch < change.patch
patch -R < change.patch        # reverse

# find duplicate lines across files
comm -12 <(sort a) <(sort b)   # lines in both
comm -23 <(sort a) <(sort b)   # only in a
comm -13 <(sort a) <(sort b)   # only in b
08

프로세스 관리 & 시그널

백그라운드 작업 & 작업 제어

&를 추가하면 명령이 백그라운드에서 실행되어 즉시 반환. jobs는 활성 작업 목록; fg/bg는 포그라운드와 백그라운드 사이를 이동. Ctrl+Z는 포그라운드 작업 일시 중단(SIGTSTP 전송). wait는 백그라운드 작업이 완료될 때까지 차단 — 병렬 작업을 시작하는 스크립트에 필수. disown은 작업을 셸의 작업 테이블에서 제거하여 로그아웃 후에도 계속 실행(nohup과 달리 이미 실행 중인 작업에서 작동).

bash
# run in background (append &)
sleep 100 &
# [1] 12345   (job 1, PID 12345)

# list jobs
jobs
jobs -l            # with PIDs

# bring to foreground / send to background
fg                 # foreground the current job
fg %1              # foreground job 1
bg %2              # background job 2 (continue running)

# suspend a running foreground job: Ctrl+Z
# then resume in background:
bg

# wait for background jobs
sleep 5 &
wait                # wait for all background jobs
wait $!             # wait for the last background job
wait 12345          # wait for specific PID

# disown: detach from shell (survives logout)
long_task &
disown %1

kill & 시그널

kill은 프로세스에 시그널을 보냅니다. SIGTERM(기본값)은 프로세스에 정상 종료 요청 — 정리 가능. SIGKILL(-9)은 강제적이고 즉각적; 프로세스가 잡거나 무시할 수 없어 리소스를 나쁜 상태로 남길 수 있습니다. 항상 먼저 SIGTERM을 시도하고, 기다린 다음 필요할 때만 SIGKILL. killall/pkill은 이름으로 종료. pkill -f는 전체 명령줄 매칭(더 유연). 모든 시그널 이름을 나열하려면 kill -l을 사용하세요.

bash
# send signals to processes
kill 12345              # SIGTERM (graceful, default)
kill -15 12345          # SIGTERM explicitly
kill -9 12345           # SIGKILL (force, cannot be caught)
kill -HUP 12345         # SIGHUP (reload config)

# kill by name
killall nginx           # kill all nginx processes
pkill -f "python app.py"  # match full command line

# common signals
# SIGTERM (15) - graceful termination (default)
# SIGKILL (9)  - force kill (cannot be caught/ignored)
# SIGINT (2)   - interrupt (Ctrl+C)
# SIGHUP (1)   - hangup (often reload config)
# SIGSTOP (19) - pause (cannot be caught)
# SIGCONT (18) - resume
# SIGTSTP (20) - terminal stop (Ctrl+Z)

# list all signals
kill -l

# priority: try SIGTERM first, SIGKILL as last resort
kill 12345; sleep 2; kill -9 12345 2>/dev/null

trap — 시그널 처리

trap은 스크립트가 시그널을 받을 때 실행할 명령을 등록 — 정리에 필수. EXIT 의사 시그널은 모든 종료(정상, 오류 또는 종료)에서 발생하여 임시 파일 제거에 완벽. 항상 정리하는 트랩이 정리할 리소스를 만들기 전에 트랩을 설정하세요. 일반적인 패턴: trap cleanup EXIT INT TERM. trap '' SIGNAL은 무시; trap - SIGNAL은 기본 복원. 이것이 견고한 스크립트가 중단된 경우에도 정리를 보장하는 방법입니다.

bash
# clean up on exit (EXIT signal)
trap 'rm -f $TMPFILE' EXIT
TMPFILE=$(mktemp)
echo "working with $TMPFILE"
# when script exits (normally or via error), TMPFILE is removed

# handle Ctrl+C (SIGINT)
trap 'echo "Caught Ctrl+C, exiting"; exit 130' INT

# handle multiple signals
cleanup() {
    echo "Cleaning up..."
    rm -rf "$WORKDIR"
    exit
}
trap cleanup INT TERM EXIT

# ignore a signal
trap '' INT              # ignore Ctrl+C
trap - INT               # restore default behavior

# reload on SIGHUP
trap 'echo "reloading config"; source config.sh' HUP

# list current traps
trap -p

프로세스 검사

ps는 프로세스의 스냅샷 표시; top/htop은 실시간 표시. pstree는 부모-자식 계층 표시. pgrep는 이름으로 PID 찾기(ps|grep보다 안전). lsof -i :PORT는 어느 프로세스가 포트를 사용하는지 찾기 — '포트 사용 중' 오류 디버깅에 필수. ss(소켓 통계)는 netstat의 현대적 대체. ps aux | grep 패턴은 어디에나 있지만 pgrep이 더 깔끔. 리소스 독점자를 찾으려면 --sort를 사용하세요.

bash
# list processes
ps aux                   # all processes (BSD style)
ps -ef                   # all processes (System V style)
ps -ef | grep nginx      # find nginx processes

# interactive viewer (like Task Manager)
top                      # real-time, all processes
htop                     # nicer interactive viewer (install separately)

# process tree
pstree                   # tree view
pstree -p                # with PIDs

# find process by name/port
pgrep -f "node app"      # print PIDs matching
pgrep -fl "node"         # full command line

# who's listening on a port
lsof -i :8080            # process using port 8080
ss -tlnp                 # all listening TCP ports
netstat -tlnp            # legacy alternative

# process resource usage
top -b -n 1 | head       # snapshot
ps aux --sort=-%mem | head  # top memory users

nohup, disown & tmux

nohup은 프로세스가 SIGHUP을 무시하게 하여 로그아웃 후에도 살아남게 합니다 — 출력은 nohup.out으로. disown은 이미 실행 중인 백그라운드 작업에 대해 동일. setsid는 새 세션에서 프로세스를 시작하여 완전히 분리. 장기 실행 대화형 작업의 경우 tmux나 screen이 더 나음: 나중에 다시 연결할 수 있는 전체 터미널 세션을 유지하여 텍스트 편집기도 연결 끊김에서 살아남음. 이것이 시스 관리자가 원격 서버를 관리하는 방법입니다.

bash
# nohup: run immune to hangups (survives logout)
nohup ./long_script.sh &
# output goes to nohup.out by default
nohup ./server.sh > server.log 2>&1 &

# disown: detach an already-running job
./long_task &
disown %1

# setsid: start in a new session
setsid ./daemon.sh &

# tmux: terminal multiplexer (persistent sessions)
tmux                    # start new session
tmux new -s work        # named session
tmux ls                 # list sessions
tmux attach -t work     # reattach
# inside tmux: Ctrl+B then D to detach
# processes keep running after detach

# screen: alternative to tmux
screen -S work
screen -ls
screen -r work

# check if a process is running
pgrep -x nginx && echo "running" || echo "stopped"
09

스크립팅 & 고급 주제

Shebang & 스크립트 구조

shebang(#!)은 커널에 사용할 인터프리터를 알려줍니다. #!/usr/bin/env bash가 가장 이식 가능. 잘 구조화된 스크립트는 안전을 위해 'set -euo pipefail'로 시작하고, usage()를 정의하고, getopts(짧은 플래그용) 또는 수동 루프(긴 플래그용)로 인수를 파싱. OPTIND는 다음 인수 추적; 파싱된 옵션을 지나 shift하여 $1이 첫 번째 위치 인수가 되게. 이 구조가 스크립트를 견고하고 사용자 친화적으로 만듭니다.

bash
#!/usr/bin/env bash
# ^ portable shebang (finds bash via PATH)
# #!/bin/bash          # absolute path (common)
# #!/usr/bin/bash       # on some systems
# #!/bin/sh             # POSIX sh (most portable)

set -euo pipefail        # strict mode (see below)

# script metadata
readonly PROGNAME=$(basename "$0")
readonly VERSION="1.0.0"

usage() {
    cat << EOF
Usage: $PROGNAME [OPTIONS] FILE
Options:
  -h, --help     Show this help
  -v, --version  Show version
  -o OUTPUT      Output file
EOF
}

# parse options with getopts
while getopts "hvo:" opt; do
    case $opt in
        h) usage; exit 0 ;;
        v) echo $VERSION; exit 0 ;;
        o) output=$OPTARG ;;
        *) usage; exit 1 ;;
    esac
done
shift $((OPTIND - 1))

엄격 모드(set -euo pipefail)

set -e는 명령이 실패(0이 아닌 반환)하면 즉시 종료 — 오류를 조기에 잡음. set -u는 설정되지 않은 변수 참조를 오류로 처리. pipefail은 파이프라인의 어떤 명령이든 실패하면 0이 아닌 값 반환(기본적으로 마지막 명령의 상태만 중요). 함께 대부분의 버그를 잡습니다. 예상되는 실패를 허용하려면 'cmd || true', 명시적 검사에는 'if ! cmd'를 사용하세요. 일부 명령(grep, test)은 정상적으로 0이 아닌 값을 반환하므로 감싸세요.

bash
# the 'strict mode' trio
set -e             # exit on error (any non-zero status)
set -u             # error on unset variable
set -o pipefail    # pipeline fails if any command fails
# combined: set -euo pipefail

# example: without -e, errors are silently ignored
mkdir /root/nope   # fails (permission denied)
echo "still running"  # this runs anyway!

# with set -e, the script exits at the failure
set -e
mkdir /root/nope   # script exits here
echo "never reached"

# handle expected failures
set -e
if ! grep -q "x" file; then
    echo "not found"   # grep returning 1 won't kill script
fi

# temporarily disable -e
set +e
risky_command
set -e

# || true to ignore a command's failure
mkdir -p dir || true

디버깅 기술

set -x(xtrace)는 실행 전에 각 명령을 인쇄 — #1 디버깅 도구. PS4는 프롬프트 사용자 정의(줄 번호 추가가 문제 위치 파악에 도움). bash -n은 실행 없이 구문 검사. trap ERR는 오류 발생 시 명령 실행, 무엇이 잘못되었는지 로깅에 완벽. 복잡한 스크립트의 경우 의심스러운 함수를 set -x/+x로 감싸서 해당 부분만 추적. set -e와 결합하여 첫 번째 오류에서 중지하고 검사.

bash
# trace execution: print each command before running
set -x              # turn on
set +x              # turn off
bash -x script.sh   # run with tracing from the start

# verbose: print input lines as read
set -v

# combine: -xv for full detail
bash -xv script.sh

# print commands with line numbers
export PS4='+ ${BASH_SOURCE}:${LINENO}: '
set -x

# check syntax without executing
bash -n script.sh   # syntax check only

# step through interactively
bash -x script.sh 2>&1 | less

# debug a specific function
debug_func() {
    set -x
    actual_func "$@"
    set +x
}

# use trap ERR to catch errors
trap 'echo "Error on line $LINENO" >&2' ERR

산술 & 수학

Bash는 $(( ))로 정수 산술만 수행. 부동소수점의 경우 bc로 표현식을 파이핑(scale=N은 소수 자릿수 설정)하거나 awk 사용. bc -l은 수학 라이브러리 로드(sqrt, sin, cos 등). $RANDOM은 0-32767 의사 난수 정수; 암호학적 무작위성에는 /dev/urandom 사용. (( )) 명령은 C처럼 +=, -=, *=, /=, %= 및 ++/-- 지원. 심각한 수학의 경우 Python이나 awk가 더 나은 선택.

bash
# integer arithmetic
echo $((5 + 3))         # 8
echo $((10 / 3))        # 3 (integer division)
echo $((10 % 3))        # 1 (modulo)
echo $((2 ** 8))        # 256 (exponent)

# floating point with bc
echo "scale=2; 10/3" | bc       # 3.33
echo "3.14 * 2" | bc            # 6.28
result=$(echo "scale=4; $a / $b" | bc)

# floating point with awk
awk 'BEGIN {printf "%.2f\n", 10/3}'   # 3.33
awk "BEGIN {print $a * $b}"

# math functions with bc -l
echo "scale=4; s(0)" | bc -l    # 0 (sine, radians)
echo "scale=4; sqrt(16)" | bc -l # 4.0000

# random numbers
echo $RANDOM              # 0-32767
echo $((RANDOM % 100))    # 0-99
head -c 4 /dev/urandom | od -An -tu4   # 32-bit random

# increment/decrement
n=5
((n++)); echo $n          # 6
((n+=10)); echo $n        # 16

정규식 & 패턴 매칭

Bash에는 세 가지 패턴 시스템이 있습니다: 파일 이름용 globs(*.txt, ?, [abc]), 더 복잡한 매칭용 확장 globs(extglob: !(), @(), *()), 그리고 ERE 정규식([[ ]]에서 =~). =~는 그룹을 BASH_REMATCH로 캡처(인덱스 0 = 전체 매칭, 1+ = 그룹). 정규식은 ERE 구문 사용(grep -E처럼). 파일 이름 패턴에서 강력한 부정과 교대를 위해 extglob 활성화. 정규식은 입력 검증을 위한 Bash의 가장 유용한 기능 중 하나입니다.

bash
# glob patterns (filename matching)
*.txt          # any .txt file
file?.txt      # single char: file1.txt, fileA.txt
file[0-9].txt  # digit: file0.txt ... file9.txt
file[!0-9].txt # NOT a digit

# extended globs (shopt -s extglob)
shopt -s extglob
echo !(*.bak)       # everything except .bak files
echo @(*.jpg|*.png) # jpg or png
echo *(foo)         # zero or more "foo"

# regex with =~ in [[ ]]
if [[ $email =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
    echo "valid email"
fi

# capture groups with BASH_REMATCH
if [[ $date =~ ^([0-9]{4})-([0-9]{2})-([0-9]{2})$ ]]; then
    echo "Year: ${BASH_REMATCH[1]}"
    echo "Month: ${BASH_REMATCH[2]}"
    echo "Day: ${BASH_REMATCH[3]}"
fi

# case-insensitive regex
shopt -s nocasematch
if [[ $s =~ ^hello ]]; then echo "starts with hello"; fi
10

grep 심층

기본 grep 패턴

grep은 패턴을 사용하여 텍스트를 검색합니다. -i는 대소문자 무시; -w는 전체 단어 매칭('error' 검색 시 'errors' 매칭 방지); -v는 반전(매칭되지 않는 줄); -c는 개수; -n은 줄 번호 표시; -l은 매칭된 파일 이름만; -h는 여러 파일 검색 시 파일 이름 접두사 억제. 기본적으로 grep은 메타문자에 백슬래시 이스케이프가 필요한 기본 정규식(BRE)을 사용. 더 깔끔한 구문의 확장 정규식에는 -E, 리터럴 검색의 경우 더 빠른 고정 문자열에는 -F를 사용하세요.

bash
# Basic search (literal string)
grep "error" logfile.txt

# Case-insensitive
grep -i "error" logfile.txt

# Whole word match
grep -w "error" logfile.txt

# Invert match (lines NOT matching)
grep -v "debug" logfile.txt

# Count matches
grep -c "error" logfile.txt

# Show line numbers
grep -n "error" logfile.txt

# Multiple files with filename prefix
grep "error" *.log

# Suppress filename prefix (single file behavior)
grep -h "error" *.log

# Only show filenames with matches
grep -l "error" *.log

정규식이 있는 grep

grep -E(또는 egrep)는 더 깔끔한 구문의 확장 정규식 사용: +, ?, |, ()가 이스케이프 없이 작동. ^와 $는 줄의 시작/끝에 고정. []는 문자 클래스 정의; {}는 반복 지정. -P는 \d, \w, 룩어헤드 기능의 Perl 호환 정규식(PCRE) 활성화 — 하지만 GNU 전용이며 이식 불가. 복잡한 정규식의 경우 기본적으로 PCRE 같은 구문을 사용하고 더 빠른 ripgrep(rg)을 고려. $와 * 같은 특수 문자의 셸 해석을 방지하려면 항상 패턴을 인용.

bash
# Extended regex (-E or egrep)
grep -E "error|warning|fatal" logfile.txt

# Anchors: ^ start, $ end
grep -E "^ERROR:" logfile.txt      # lines starting with ERROR:
grep -E "completed$" logfile.txt   # lines ending with completed

# Character classes
grep -E "[0-9]{4}-[0-9]{2}-[0-9]{2}" dates.txt  # date format
grep -E "[A-Z][a-z]+" names.txt                  # Capitalized words

# Quantifiers
grep -E "ab+c" file.txt    # one or more b's
grep -E "ab*c" file.txt    # zero or more b's
grep -E "ab?c" file.txt    # zero or one b

# Alternation and grouping
grep -E "(cat|dog) food" file.txt

# PCRE (-P, GNU grep only)
grep -P "\d{3}-\d{4}" phones.txt  # \d for digits

grep 컨텍스트 & 출력 제어

컨텍스트 플래그(-B, -A, -C)는 주변 줄 표시, 로그 항목 이해에 필수. -o는 매칭된 부분만 출력(URL, 숫자 등 추출에 유용). --color는 터미널에서 매칭 강조. -r은 재귀 검색(심볼릭 링크 제외); -R은 심볼릭 링크 따름. --include/--exclude/--exclude-dir로 검색할 파일 필터 — 큰 코드베이스에 매우 유용(항상 node_modules, .git, vendor 제외). -a는 바이너리 파일을 텍스트로 강제. 코드 검색의 경우 ripgrep(rg)이 합리적인 기본값이 있는 현대적이고 더 빠른 대안.

bash
# Show context: lines before/after match
grep -B 2 "error" log.txt    # 2 lines Before
grep -A 3 "error" log.txt    # 3 lines After
grep -C 2 "error" log.txt    # 2 lines Context (both)

# Only show matched part (not full line)
grep -o "https?://[^ ]+" urls.txt

# Color highlight matches
grep --color=auto "error" log.txt

# Recursive search
grep -r "TODO" ./src/
grep -R "TODO" ./src/        # follows symlinks

# Include/exclude file patterns
grep -r --include="*.py" "import" .
grep -r --exclude="*.test.js" "TODO" .
grep -r --exclude-dir=node_modules "TODO" .

# Binary files
grep -a "pattern" binary_file  # treat as text

stdin & 파이프라인이 있는 grep

grep은 파이프라인에서 가장 강력. [n]ginx 트릭은 grep이 자체 프로세스와 매칭하는 것을 방지: 괄호는 패턴이 프로세스 목록의 리터럴 'grep' 문자열과 매칭되지 않게. zgrep은 수동 압축 해제 없이 압축 파일 검색. pgrep은 특수 프로세스 파인더(ps | grep보다 나음). grep -rl은 패턴을 포함한 파일 목록; xargs grep에 파이핑하면 그 파일 내에서 다른 패턴 검색 — 일반적인 코드 고고학 기술. 대화형 코드 검색의 경우 소스 코드를 위해 설계된 ripgrep이나 ack를 사용.

bash
# Search command output
ps aux | grep nginx
ps aux | grep "[n]ginx"  # trick: prevents matching grep itself

# Search compressed logs
zcat log.gz | grep "error"
zgrep "error" log.gz     # direct

# Chain multiple greps
cat log.txt | grep "error" | grep -v "timeout" | grep -c

# Extract and filter
grep -oE "[0-9.]+" response.txt | sort -n | uniq

# Find processes excluding grep
pgrep -f "node server.js"  # better than ps | grep

# Search command history
history | grep "git rebase"

# Find files containing pattern, then search more
grep -rl "config" . | xargs grep "database"

grep 종료 상태 & 스크립팅

grep의 종료 상태는 스크립팅에 이상적: 0(매칭 발견), 1(매칭 없음), 2(오류). -q(조용히)는 순수 조건 검사를 위해 출력 억제. set -e 스크립트에서 grep이 1(매칭 없음)을 반환하면 스크립트가 종료 — 이를 방지하려면 '|| true'를 사용. while-read 패턴은 매칭된 줄을 한 번에 하나씩 처리. grep -c는 개수 반환(매칭이 없으면 0), 숫자로 비교 가능. 이 스크립팅 기능이 grep을 로그 모니터링, 검증 스크립트 및 CI/CD 검사용 빌딩 블록으로 만듦.

bash
# grep returns exit codes for scripting
# 0 = match found, 1 = no match, 2 = error

if grep -q "error" /var/log/syslog; then
    echo "Errors found!"
    # send alert, etc.
fi

# Silent check (-q), no output
grep -q "^$" file.txt && echo "has blank lines"

# Use in while loop
grep "pattern" file.txt | while read -r line; do
    echo "Found: $line"
done

# Count and branch
errors=$(grep -c "ERROR" log.txt)
if [ "$errors" -gt 10 ]; then
    echo "Too many errors: $errors"
fi

# grep with set -e (exits on non-zero)
set -e
grep "missing" file.txt || true  # prevent exit
11

sed 심층

sed 치환

sed(스트림 에디터)는 텍스트를 줄별로 변환. s 명령은 텍스트 치환: s/pattern/replacement/flags. g(전역)는 줄당 모든 발생 치환; 없으면 첫 번째 매칭만 치환. 작업을 특정 줄로 제한 가능(번호, 범위 또는 패턴별). -i는 제자리 편집(위험 — 항상 -i 없이 먼저 테스트, 또는 백업을 위해 -i.bak 사용). sed는 각 줄을 독립적으로 처리. 구분자는 /일 필요 없음 — 패턴에 슬래시가 포함된 경우(예: 파일 경로) s|old|new|g 사용.

bash
# Basic substitution (first occurrence per line)
sed 's/old/new/' file.txt

# Replace all occurrences
sed 's/old/new/g' file.txt

# Case-insensitive (GNU sed)
sed 's/old/new/gi' file.txt

# Replace Nth occurrence
sed 's/old/new/2' file.txt  # 2nd occurrence only

# Only on lines matching pattern
sed '/error/s/old/new/' file.txt

# Only on specific line numbers
sed '5s/old/new/' file.txt      # line 5 only
sed '5,10s/old/new/g' file.txt  # lines 5-10
sed '$s/old/new/' file.txt      # last line

# In-place edit (modifies file)
sed -i 's/old/new/g' file.txt
sed -i.bak 's/old/new/g' file.txt  # keeps backup

정규식 & 캡처 그룹이 있는 sed

sed -E(또는 -r)는 더 깔끔한 구문의 확장 정규식 사용(괄호/중괄호 앞에 백슬래시 없음). 캡처 그룹은 치환에서 \1, \2 등으로 참조. &는 전체 매칭을 나타냄. 날짜 재포맷팅 예제가 강력함을 보여줌: 연, 월, 일을 개별적으로 캡처하고 재배열. 여러 명령은 세미콜론으로 체인 가능(s/.../.../;s/.../.../). 패턴에서 특수 문자(., *, [ 등)를 항상 이스케이프하고 치환에서 /를 이스케이프(또는 다른 구분자 사용).

bash
# Extended regex (-E or -r)
sed -E 's/[0-9]+/NUMBER/g' file.txt

# Capture groups with \1, \2 (BRE)
sed 's/\(foo\)\(bar\)/\2\1/' file.txt  # swap: foobar -> barfoo

# Capture groups with () (ERE, cleaner)
sed -E 's/(foo)(bar)/\2\1/' file.txt

# Reformat date: 2025-01-15 -> 15/01/2025
sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3\/\2\/\1/' file.txt

# Add prefix to lines
sed 's/^/>> /' file.txt

# Add suffix
sed 's/$/ <<</' file.txt

# Remove leading/trailing whitespace
sed 's/^[ \t]*//;s/[ \t]*$//' file.txt

# Use & for full match
sed 's/[0-9]\{3\}/[&]/g' file.txt  # wrap 3 digits in brackets

sed Delete & Print

d 명령은 줄 삭제; p는 줄 인쇄. -n(자동 인쇄 없음)에서는 명시적 p 명령만 출력 생성 — 이것이 sed를 선택적 프린터(grep처럼)로 만듦. 빈 줄 삭제(sed '/^$/d')는 일반적인 정리. sed -n '5,10p'는 head/tail 조합과 동일. ~ 구문(0~3p)은 3번째 줄마다 인쇄(GNU 확장). 기억: -n 없이 sed는 모든 줄을 인쇄(아마도 수정); -n에서는 p를 사용하지 않는 한 아무것도 인쇄 안 함. 이 이중성이 sed를 에디터이자 필터로 만듦.

bash
# Delete lines
sed '5d' file.txt              # delete line 5
sed '5,10d' file.txt           # delete lines 5-10
sed '/pattern/d' file.txt      # delete matching lines
sed '/^$/d' file.txt           # delete blank lines
sed '/^[ \t]*$/d' file.txt    # delete whitespace-only lines
sed '$d' file.txt              # delete last line

# Print only specific lines (like head/tail)
sed -n '5p' file.txt           # print line 5 only
sed -n '5,10p' file.txt        # print lines 5-10
sed -n '/pattern/p' file.txt   # print matching lines (like grep)
sed -n '5,${/pattern/p}' file.txt  # from line 5 to end

# Print with line numbers
sed '=' file.txt | sed 'N;s/\n/\t/'

# Print every Nth line
sed -n '0~3p' file.txt  # every 3rd line (GNU)

sed 여러 줄 & 홀드 공간

sed의 홀드 공간은 여러 줄 작업을 허용 — 패턴 공간(현재 줄)을 홀드 공간에 저장하고 나중에 검색 가능. h/H는 홀드에 복사/추가; g/G는 홀드에서 복사/추가; x는 교환. 줄 결합 트릭(:a;N;$!ba;s/\n/ /g)은 전체 파일을 패턴 공간으로 읽은 다음 줄 바꿈을 치환. N은 다음 줄을 패턴 공간에 추가. 이 고급 기능은 sed를 튜링 완전으로 만들지만 매우 암호 같음. 복잡한 여러 줄 변환의 경우 awk나 Perl이 더 읽기 쉬움. 간단한 줄 기반 편집에는 sed; 필드 기반 또는 여러 줄 논리에는 awk를 사용.

bash
# Join lines (replace newline with space)
sed ':a;N;$!ba;s/\n/ /g' file.txt

# Join every 2 lines
sed 'N;s/\n/ /' file.txt

# Print paragraph (blocks separated by blank lines)
sed '/./{H;d;};x;s/\n/ /g' file.txt

# Reverse line order (tac alternative)
sed '1!G;h;$!d' file.txt

# Double-space a file
sed 'G' file.txt

# Remove last line of each paragraph
sed -n '/^$/{p;h;};/./{H;};/^$/{x;s/\n.*//;p;}' file.txt

# Hold space (h,H,g,G,x) stores lines between cycles
# h: copy pattern space to hold
# H: append pattern space to hold
# g: copy hold to pattern space
# G: append hold to pattern space
# x: exchange pattern and hold

파이프라인 & 스크립트의 sed

sed는 텍스트 변환을 위한 파이프라인에서 뛰어남. 변수를 치환할 때 해석을 방지하기 위해 특수 문자(&와 \)를 이스케이프. 여러 -e 플래그가 여러 명령을 순차적으로 적용. -f는 파일에서 명령 읽기(복잡한 스크립트에 유용). CSV-to-TSV 변환이 실용적 용도를 보여줌. 스크립트에서 구성 파일 편집 시 항상 변경 검증(sed 후 grep)하고 전용 도구(JSON용 jq, YAML용 yq) 고려. sed의 강점은 간단하고 빠른 줄 기반 편집 — grep과 awk와 함께 Unix 텍스트 처리의 필수.

bash
# Clean up output in pipeline
cat messy.txt | sed 's/[ \t]*$//' | sed '/^$/d' | sort

# Extract and transform
echo "name=John;age=30" | sed 's/;/\n/g' | sed 's/=/: /'

# Modify config files in scripts
sed -i "s/PORT=8080/PORT=$NEW_PORT/" config.env

# Escape special chars for safe substitution
ESCAPED=$(printf '%s\n' "$URL" | sed 's/[&\]/\\&/g')
sed "s|URL|$ESCAPED|g" template.txt

# Multiple substitutions
sed -e 's/foo/bar/g' -e 's/baz/qux/g' file.txt

# Read commands from file
sed -f script.sed input.txt

# Convert CSV to TSV
sed 's/,/\t/g' data.csv
12

awk 심층

awk 기본 & 필드

awk는 자동으로 각 줄을 필드($1, $2, ..., $NF)로 분할. -F는 입력 필드 구분자 설정; OFS는 출력 구분자 설정. $0은 전체 줄. NR(레코드 번호)은 줄 번호; NF(필드 번호)는 현재 줄의 필드 개수. awk는 패턴-액션 쌍을 통해 각 줄을 처리: pattern { action }. 패턴이 없으면 액션이 모든 줄에 실행. 액션이 없으면 줄이 인쇄. 이것이 awk를 CSV, TSV, /etc/passwd 및 로그 파일에서 열 기반 데이터 추출에 이상적으로 만듦.

bash
# Print specific fields (default delimiter: whitespace)
awk '{print $1}' file.txt        # first field
awk '{print $1, $3}' file.txt    # first and third
awk '{print $NF}' file.txt       # last field
awk '{print $(NF-1)}' file.txt   # second-to-last

# Custom field separator
awk -F: '{print $1, $7}' /etc/passwd   # colon-separated
awk -F',' '{print $2}' data.csv        # CSV
awk -F'\t' '{print $1}' data.tsv      # TSV

# Print entire line ($0)
awk '{print}' file.txt
awk '{print NR, $0}' file.txt  # with line number

# NR = record (line) number
# NF = number of fields in current record
# NR and NF are built-in variables

# Change output separator
awk 'BEGIN{OFS="|"} {print $1, $2, $3}' file.txt

awk 패턴 & 조건

awk 패턴은 정규식(/pattern/), 비교($3 > 100), 줄 번호(NR == 5) 또는 범위(/start/,/end/)일 수 있음. ~와 !~는 특정 필드에 정규식 적용. 조건은 &&, ||, !로 결합 가능. 이것이 awk를 강력한 필터로 만듦 — 필드 값을 숫자 또는 문자열로 비교할 수 있어 grep보다 더 표현적. 범위 패턴(/start/,/end/)은 두 마커 사이의 모든 줄을 인쇄(포함). awk는 줄당 조건을 평가; 참이면 액션 실행. 액션이 없으면 기본값은 {print}.

bash
# Pattern: only process matching lines
awk '/error/ {print}' log.txt          # like grep
awk '$3 > 100' data.txt                # 3rd field > 100
awk 'NR == 5' file.txt                 # line 5 only
awk 'NR >= 10 && NR <= 20' file.txt    # lines 10-20
awk 'NF == 0' file.txt                 # empty lines
awk '$1 == "ERROR"' log.txt            # first field equals ERROR

# Range pattern: from start to end
awk '/start/,/end/' file.txt

# Combine conditions
awk '$3 > 50 && $3 < 100 {print $1, $3}' data.txt
awk '$1 == "GET" || $1 == "POST" {print}' access.log

# Negation
awk '!/debug/ {print}' log.txt  # lines NOT containing debug

# Field comparison with regex
awk '$2 ~ /^[0-9]+$/' file.txt   # 2nd field is numeric
awk '$2 !~ /pattern/' file.txt   # 2nd field doesn't match

awk BEGIN/END & 변수

BEGIN은 입력 읽기 전 실행(초기화, 헤더, 변수 설정에 이상). END는 모든 입력 처리 후 실행(요약, 합계에 이상). 사용자 변수는 선언 불필요 — 기본값 0(숫자) 또는 빈 값(문자열). -v는 awk에 외부 변수 전달. 이것이 awk를 데이터 처리용 미니 프로그래밍 언어로 만듦: 합, 평균, 최소, 최대, 개수 — 한 번에. 패턴 'NR == 1 {max = $1}'은 첫 번째 줄에서 max를 초기화하고, 후속 줄이 업데이트. 여러 grep/sort/cut 파이프라인보다 훨씬 효율적.

bash
# BEGIN: runs before processing (setup)
# END: runs after processing (summary)
awk 'BEGIN {print "Total:"} {sum += $1} END {print sum}' nums.txt

# Count lines
awk 'END {print NR " lines"}' file.txt

# Sum a column
awk '{sum += $2} END {print "Total:", sum}' sales.csv

# Average
awk '{sum += $2; count++} END {print sum/count}' data.txt

# Find max value
awk 'NR == 1 {max = $1} $1 > max {max = $1} END {print max}' nums.txt

# User-defined variables (no declaration needed)
awk '{total += $1; if ($1 > max) max = $1}
     END {print "Sum:", total, "Max:", max, "Avg:", total/NR}'

# Pass variables with -v
awk -v threshold=50 '$1 > threshold {print}' data.txt
awk -v date="$(date +%Y-%m-%d)" '{print date, $0}' file.txt

awk 제어 흐름

awk에는 전체 제어 흐름이 있음: if/else, for, while 및 연관 배열(키-값). 이것이 awk를 완전한 데이터 처리 언어로 만듦. 단어 개수 예제(words[$1]++)는 고전적 awk 사용 사례 — 열의 각 값 발생 횟수를 계산한 다음 빈도순으로 정렬. awk의 배열은 연관적(사전처럼), 문자열 또는 숫자로 인덱스. for (key in array)는 키를 반복(순서 없음 — sort로 파이핑). awk는 cut, sort, uniq, wc의 전체 파이프라인을 단일하고 효율적인 패스로 대체 가능. 복잡한 데이터 처리의 경우 awk가 체인된 Unix 명령보다 종종 더 명확.

bash
# if-else
awk '{
  if ($3 >= 90) grade = "A"
  else if ($3 >= 80) grade = "B"
  else if ($3 >= 70) grade = "C"
  else grade = "F"
  print $1, grade
}' students.txt

# for loop
awk 'BEGIN {
  for (i = 1; i <= 5; i++) print i, i*i
}'

# for-in (iterate array)
awk '{count[$1]++} END {
  for (key in count) print key, count[key]
}' access.log | sort -rnk2

# while loop
awk '{
  i = 1
  while (i <= NF) {
    print i, $i
    i++
  }
}' file.txt

# Arrays (associative)
awk '{words[$1]++} END {
  for (w in words) print words[w], w
}' file.txt | sort -rn | head

awk 실용적 예제

이 실용적 예제는 awk의 실제 강력함을 보여줌. IP 빈도 계수는 로그 분석에 필수. 파일 크기별 확장자 요약은 split()을 사용하여 확장자 추출. 열 값별 CSV 필터링은 복잡한 grep/sed 파이프라인을 대체. 백분위수 계산은 awk의 수학적 능력을 보여줌(asorti는 배열 인덱스 정렬). 열 재포맷팅(구분자 변경 및 필드 선택)은 일반적 ETL 작업. awk는 구조화된 텍스트 처리용 기본 도구 — 데이터에 열/필드가 있으면 awk가 거의 항상 올바른 선택. JSON에는 jq; CSV에는 awk 또는 csvkit을 사용.

bash
# Top 10 IP addresses from access log
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head
# Or entirely in awk:
awk '{ip[$1]++} END {for (i in ip) print ip[i], i}' access.log | sort -rn | head

# Sum file sizes by extension
ls -l | awk '{split($NF, a, "."); ext=a[2]; size[ext]+=$5}
  END {for (e in size) print e, size[e]}' | sort -rnk2

# Filter CSV by column value
awk -F',' '$3 == "active" {print $1, $2}' users.csv

# Calculate response time percentiles from log
awk '{times[NR] = $2} END {
  n = asorti(times, sorted)
  print "p50:", sorted[int(n*0.5)]
  print "p95:", sorted[int(n*0.95)]
  print "p99:", sorted[int(n*0.99)]
}' response_times.txt

# Reformat columns
awk -F',' 'BEGIN{OFS="|"} {print $1, $2, $4}' data.csv
13

find & xargs

이름 & 유형별 find

find는 다양한 기준으로 파일 시스템을 검색. -name은 파일 이름 매칭(대소문자 구분); -iname은 대소문자 무시. -type f/d/l은 파일 유형 필터. -path는 전체 경로 매칭; -regex는 전체 경로 정규식 사용. -o(또는)는 조건 결합; 그룹화에는 \( \) 사용. -maxdepth는 재귀 깊이 제한(큰 파일 시스템에서 성능에 중요). find는 경로 출력; 결과에 작용하려면 -exec 또는 xargs와 결합. 셸 glob 확장을 방지하려면 항상 패턴 인용. 코드 검색의 경우 ripgrep(rg)이 더 빠르지만 파일 시스템 작업에는 find가 더 유연.

bash
# Find by name (exact)
find . -name "config.txt"

# Case-insensitive name
find . -iname "config.txt"

# Wildcard pattern
find . -name "*.py"
find . -name "test_*"

# Find by type
find . -type f -name "*.js"   # files only
find . -type d -name "node_modules"  # directories
find . -type l                # symlinks

# Find by path (full path match)
find . -path "*/src/*.test.js"

# Find by regex
find . -regex ".*\.\(py\|js\)$"

# Multiple name patterns
find . \( -name "*.py" -o -name "*.js" \)

# Max depth (don't recurse too deep)
find . -maxdepth 2 -name "*.txt"

시간 & 크기별 find

find의 시간 기반 검색은 정리와 감사에 필수. -mtime(수정), -atime(접근), -ctime(메타데이터 변경)은 일 사용; -mmin/-amin/-cmin은 분 사용. - (미만)과 + (초과)가 값을 접두. 크기는 접미사 사용: c(바이트), k(KB), M(MB), G(GB). -empty는 길이가 0인 파일이나 빈 디렉토리 찾기. -perm은 권한 검사: 정확한 매칭(644), 모든 비트 설정(-u+x), 또는 어떤 비트 설정(/4000). SUID 파일 찾기(/4000)는 보안 감사 기술. 이 기준은 -a(그리고, 기본값)와 -o(또는)로 결합 가능.

bash
# Find by modification time
find . -mtime -1    # modified in last 24 hours
find . -mtime +7    # modified more than 7 days ago
find . -mtime 7     # modified exactly 7 days ago

# Access time (-atime) and change time (-ctime)
find . -atime -1    # accessed in last 24h
find . -ctime -1    # status changed in last 24h

# Minutes instead of days
find . -mmin -30    # modified in last 30 minutes

# Find by size
find . -size +100M   # larger than 100MB
find . -size -1k     # smaller than 1KB
find . -size 10M     # exactly 10MB

# Find empty files/directories
find . -empty -type f
find . -empty -type d

# Find by permissions
find . -perm 644     # exactly 644
find . -perm -u+x    # has execute for user
find / -perm /4000   # SUID files (security audit)

find -exec & -delete

-exec는 각 결과에서 명령 실행. {}는 파일 이름의 자리 표시자; \;는 명령 종료(파일당 한 번 실행); +는 파일 일괄 처리(모든 파일로 한 번 실행 — 더 효율적). -delete는 매칭된 파일 제거(-exec rm보다 빠르지만 -print로 먼저 테스트). chmod 패턴(디렉토리 755, 파일 644)은 일반적 웹 서버 설정. 여러 파일을 허용하는 명령(grep, ls, wc)에는 -exec와 +가 선호. 삭제의 경우 항상 -print로 먼저 무엇이 삭제될지 확인한 다음 -delete로 교체. -exec 대신 -ok는 파일당 확인 프롬프트.

bash
# Execute command on each result
find . -name "*.log" -exec ls -lh {} \;

# Execute with + (batch, more efficient)
find . -name "*.log" -exec ls -lh {} +

# Delete files (DANGEROUS - test first!)
find /tmp -name "*.tmp" -type f -delete
# Safer: use -exec rm
find /tmp -name "*.tmp" -type f -exec rm -i {} \;

# Execute a complex command
find . -name "*.py" -exec wc -l {} \; | sort -rn | head

# Change permissions
find . -type d -exec chmod 755 {} \;  # dirs to 755
find . -type f -exec chmod 644 {} \;  # files to 644

# Find and grep
find . -name "*.py" -exec grep "TODO" {} +

# Print before deleting (confirmation)
find . -name "*.bak" -print -delete

xargs 기본

xargs는 stdin을 명령 인수로 변환. find 출력과 stdin을 읽지 않는 명령 사이의 다리. -n은 명령당 인수 제한; -I {}는 사용자 정의 위치를 위한 자리 표시자 정의. -0(find -print0와 함께)은 공백/줄 바꿈이 있는 파일 이름을 올바르게 처리 — 안전을 위해 항상 이 쌍 사용. -P는 병렬 실행 활성화(이미지 변환 같은 CPU 바운드 작업에 좋음). -t(추적)는 실행 전 명령 표시; -p는 확인 프롬프트. -exec가 너무 느린 경우(파일당 한 프로세스) xargs가 필수 — xargs는 인수를 효율적으로 일괄 처리.

bash
# Basic: take stdin and pass as arguments
echo "file1 file2 file3" | xargs rm

# One argument per line
find . -name "*.bak" | xargs rm

# Control arguments per command (-n)
echo "1 2 3 4 5" | xargs -n 2 echo  # echo 1 2; echo 3 4; echo 5

# Show commands before running (-t, verbose)
find . -name "*.log" | xargs -t rm

# Prompt before each command (-p)
find . -name "*.tmp" | xargs -p rm

# Handle filenames with spaces (-0 with find -print0)
find . -name "*.txt" -print0 | xargs -0 grep "pattern"

# Replace string (-I)
find . -name "*.py" | xargs -I {} cp {} /backup/

# Parallel execution (-P)
find . -name "*.png" | xargs -P 4 -I {} convert {} {}.thumb.png

# Limit with -L (lines per command)
cat urls.txt | xargs -L 1 curl -O

find + xargs 패턴

find + xargs는 일괄 파일 작업을 위한 고전적 Unix 패턴. -print0 | xargs -0은 공백이나 특수 문자가 있는 파일 이름을 위한 안전한 조합. 일괄 grep 패턴은 -exec grep보다 훨씬 빠름(많은 grep 프로세스 대신 하나). 병렬 xargs(-P N)는 오디오/비디오 변환 같은 CPU 바운드 작업을 극적으로 가속. 아카이브 패턴(오래된 로그 찾기, tar)은 일반적 로그 순환 기술. 견고성을 위해 항상 -print0/-0 사용 — 없으면 공백, 인용 또는 줄 바꿈이 있는 파일 이름이 파이프라인을 손상. 이 조합은 Unix 시스템 관리의 기초.

bash
# Find and bulk grep (efficient)
find . -name "*.py" -print0 | xargs -0 grep -l "import django"

# Find large files and show sizes
find . -type f -size +100M -print0 | xargs -0 ls -lhS

# Find and archive
find . -name "*.log" -mtime +30 -print0 |
  xargs -0 tar -czf old_logs.tar.gz

# Find and batch rename
find . -name "*.JPG" | xargs -I {} mv {} {}.bak
# Or with rename command:
find . -name "*.JPG" -exec rename 's/\.JPG$/.jpg/' {} +

# Find empty directories and remove
find . -type d -empty -print0 | xargs -0 rmdir

# Find recently modified files and copy
find . -name "*.py" -mtime -1 -print0 |
  xargs -0 -I {} cp {} /tmp/recent/

# Parallel processing
find . -name "*.wav" -print0 |
  xargs -0 -P 8 -I {} ffmpeg -i {} {}.mp3
14

고급 Bash 기술

Here Document & Here String

Here document(<< DELIMITER)는 명령에 stdin으로 여러 줄 텍스트를 공급 — 구성 파일, SQL 쿼리 또는 여러 줄 입력 생성에 유용. 인용되지 않은 구분자는 변수 확장 허용; 인용된('EOF')는 내용을 문자 그대로 취급. Here string(<<<)은 단일 문자열을 stdin으로 공급 — 간단한 경우 echo | 명령보다 깔끔. 구분자는 임의의 단어일 수 있음(EOF, END, DONE); 관례는 대문자. here doc은 파일을 생성하거나 대화형 프로그램(mysql, psql, ssh)과 상호 작용하는 스크립트에 필수. 들여쓰기된 구분자(<<-)는 가독성을 위해 선행 탭을 제거.

bash
# Here document: multi-line input to a command
cat << EOF
Line 1
Line 2 with $VARIABLE expansion
Line 3
EOF

# Quoted delimiter: no variable expansion
cat << 'EOF'
This $won't expand
\n stays literal
EOF

# Write to file
cat > config.txt << EOF
host=localhost
port=8080
EOF

# Here string: single string as stdin
grep "pattern" <<< "search this text"
read -r line <<< "hello world"

# With variables
name="Alice"
greeting=$(cat <<< "Hello, $name!")

# Pipe multi-line to command
mysql << EOF
USE mydb;
SELECT * FROM users;
EOF

매개변수 확장

매개변수 확장은 Bash의 내장 문자열 조작 — 간단한 작업에 sed/awk/cut가 필요 없음. :-는 기본값 제공; #와 ##는 접두사 제거; %와 %%는 접미사 제거. 파일 확장자 예제는 매우 일반적: ${file##*.}는 확장자 획득, ${file%.*}는 확장자 없는 basename 획득. /는 첫 번째 매칭 치환; //는 모두 치환. ^^와 ,,는 대소문자 변환(Bash 4+). 이 작업은 외부 명령을 생성하는 것보다 빠름. 불필요한 서브셸 없이 효율적이고 읽기 쉬운 Bash 스크립트를 작성하려면 매개변수 확장을 마스터하세요.

bash
# Default values
echo ${name:-"default"}    # use "default" if name unset/empty
echo ${name:="default"}    # set name to "default" if unset/empty

# String length
echo ${#var}

# Substring extraction
var="Hello World"
echo ${var:0:5}    # Hello (start:length)
echo ${var:6}      # World (start to end)

# Remove from beginning (# shortest, ## longest)
file="archive.tar.gz"
echo ${file#*.}     # tar.gz (remove up to first .)
echo ${file##*.}    # gz (remove up to last .)

# Remove from end (% shortest, %% longest)
echo ${file%.*}     # archive.tar (remove from last .)
echo ${file%%.*}    # archive (remove from first .)

# Replace
echo ${var/World/Bash}    # Hello Bash (first match)
echo ${var//l/L}          # HeLLo WorLd (all matches)

# Case conversion (Bash 4+)
echo ${var^^}   # HELLO WORLD (uppercase)
echo ${var,,}   # hello world (lowercase)

trap & 시그널 처리

trap은 시그널과 이벤트에 대한 핸들러를 등록. EXIT는 스크립트 종료 시 발생(정상, 오류 또는 종료) — 정리(임시 파일, 잠금)에 이상. INT(Ctrl-C), TERM(kill), HUP(터미널 닫힘)이 일반적 시그널. 임시 파일 패턴(trap 'rm -f' EXIT)은 스크립트가 실패해도 정리를 보장. DEBUG는 모든 명령 전에 발생 — 추적에 유용. 항상 trap 명령을 인용(작은따옴표는 즉시 확장 방지). trap은 견고한 스크립트에 필수 — 없으면 임시 파일이 축적되고 잠금이 실패 시 해제되지 않을 수 있음. 항상 뒤정리하세요.

bash
# Clean up on exit (any reason)
cleanup() {
  echo "Cleaning up..."
  rm -f /tmp/myapp.lock
  exit 0
}
trap cleanup EXIT

# Handle specific signals
trap 'echo "Interrupted!"; cleanup' INT TERM
trap 'echo "Ctrl-C pressed"' INT

# Ignore a signal
trap '' INT  # ignore Ctrl-C

# Reset trap
trap - INT  # restore default behavior

# Common pattern: temp file cleanup
TMPFILE=$(mktemp)
trap 'rm -f "$TMPFILE"' EXIT
echo "data" > "$TMPFILE"
# ... use TMPFILE ...
# File is removed when script exits (even on error)

# Debug trap (trace execution)
trap 'echo "DEBUG: $BASH_COMMAND"' DEBUG

Bash 스크립트 디버깅

set -x는 실행 추적(+ 접두사로 각 명령 인쇄) — 주요 디버깅 도구. set -euo pipefail(엄격 모드)는 오류를 조기에 잡음: -e는 0이 아닌 값에서 종료, -u는 변수 이름의 오타 잡기, pipefail은 파이프를 올바르게 실패하게 만듦(없으면 마지막 명령의 종료 코드만 중요). bash -n은 실행 없이 구문 검사. PS4는 추적 접두사 사용자 정의(file:line 표시가 매우 도움). trap ERR는(-e와 함께) 오류 발생 시 실행, 줄 번호 컨텍스트 제공. 복잡한 스크립트의 경우 전역이 아닌 의심스러운 섹션에 set -x 추가. 프로덕션 스크립트에서 항상 엄격 모드 사용.

bash
# Trace execution: print each command before running
bash -x script.sh
# Or within script:
set -x   # enable tracing
set +x   # disable tracing

# Verbose: print input lines as read
bash -v script.sh

# Strict mode (recommended for all scripts)
set -euo pipefail
# -e: exit on error
# -u: error on undefined variable
# -o pipefail: pipe fails if any command fails
# -o pipefail: pipe fails if any command fails

# Print specific debug info
echo "DEBUG: var=$var" >&2  # to stderr

# Check syntax without executing
bash -n script.sh

# PS4: customize trace prompt
export PS4='+${BASH_SOURCE}:${LINENO}: '
bash -x script.sh

# Trap ERR for error handling
trap 'echo "Error on line $LINENO"' ERR

curl & wget

curl과 wget은 필수 HTTP 클라이언트. curl은 API 테스트용(모든 HTTP 메서드, 헤더, JSON 지원). -d는 POST 데이터 전송; -H는 헤더 설정; -X는 메서드 지정; -L은 리다이렉트 따름; -O는 원격 파일 이름으로 저장; -s는 조용히(스크립팅용). wget은 다운로드에 최적화(-c로 재개, -r로 재귀). REST API 테스트의 경우 curl과 -H 'Content-Type: application/json' 및 JSON 본문용 -d가 표준. -w '%{http_code}'는 스크립팅을 위해 상태 코드만 추출. 복잡한 API 테스트의 경우 httpie(더 간단한 구문) 또는 postman을 고려.

bash
# Basic HTTP request
curl https://api.example.com/data
curl -O https://example.com/file.zip  # save to file

# HTTP methods
curl -X POST https://api.example.com/users
curl -X PUT -d '{"name":"Alice"}' https://api.example.com/users/1
curl -X DELETE https://api.example.com/users/1

# Headers and JSON
curl -H "Content-Type: application/json" \
     -H "Authorization: Bearer TOKEN" \
     -d '{"key":"value"}' \
     https://api.example.com/data

# Download with wget
wget https://example.com/file.zip
wget -q https://example.com/file.zip  # quiet
wget -c https://example.com/file.zip  # continue/resume

# Follow redirects
curl -L https://short.url/abc

# Save response, show headers
curl -s -o response.json -w "%{http_code}" https://api.example.com

# Download with progress bar
curl --progress-bar -O https://example.com/largefile.iso

# POST form data
curl -d "name=Alice&[email protected]" https://api.example.com/form
15

시그널 & 트랩

trap 정리 핸들러

trap은 시그널에 대한 핸들러를 등록. EXIT는 특수 — 어떤 이유로든 셸 종료 시 발생하여 정리에 완벽. 항상 trap에서 잠금 파일, 임시 디렉토리 및 자식 프로세스를 정리. 트랩할 일반적 시그널: INT(Ctrl-C), TERM(기본 kill), HUP(터미널 닫힘), EXIT.

bash
cleanup() {
  echo "Cleaning up..."
  rm -f /tmp/myapp.lock
  exit 0
}

# catch INT (Ctrl-C), TERM, EXIT
trap cleanup INT TERM EXIT

# normal exit also triggers EXIT trap
echo "Working..."
sleep 30
# cleanup runs automatically

# remove trap
trap - EXIT                      # disable
trap - INT TERM EXIT             # disable all

정리가 있는 임시 파일

$$나 $RANDOM으로 임시 파일 이름을 직접 만들지 마세요 — 예측 가능한 경로는 심볼릭 링크 공격을 초래. mktemp는 원자적으로 고유하게 이름이 지정된 파일/디렉토리를 생성. 오류나 중단 시 정리를 보장하기 위해 EXIT에 trap과 짝지으세요. 위치를 제어하려면 TMPDIR 환경 변수를 설정.

bash
TMPDIR=$(mktemp -d)
TMPFILE=$(mktemp)

cleanup() {
  rm -rf "$TMPDIR" "$TMPFILE"
}
trap cleanup EXIT

# use temp files safely
echo "data" > "$TMPFILE"
process_data < "$TMPFILE"

# mktemp is safer than $RANDOM — avoids symlink attacks
# -d creates a directory
# -p /var/tmp specifies parent
# mktemp -t myapp.XXXXXX   # use TMPDIR env

스크립트의 시그널 처리

장기 실행 스크립트의 경우 시그널 핸들러에 의해 뒤집어지는 플래그 변수를 사용하여 메인 루프에서 우아하게 벗어남. 이것은 현재 반복을 완료하고, 상태를 유지하고, 깔끔하게 종료하게 함. trap 핸들러 자체 내에서 무거운 작업을 피하세요 — 플래그만 설정.

bash
#!/bin/bash
# graceful shutdown for a long-running script

RUNNING=1
stop() {
  echo "Stopping..."
  RUNNING=0
}
trap stop INT TERM

while [ $RUNNING -eq 1 ]; do
  echo "Tick $(date)"
  sleep 1
done

echo "Done."

# send signals: kill -TERM <pid> or Ctrl-C

timeout과 watch

timeout은 명령을 실행하고 지속 시간 후에 종료(기본적으로 SIGTERM). 종료 코드 124는 타임아웃을 의미. 프로세스가 SIGTERM을 무시하면 SIGKILL로 에스컬레이션하려면 --kill-after 사용. 루프와 결합하여 건강 검사 구축. watch는 스크립트가 아닌 사람 대면 주기적 표시용.

bash
# timeout kills a command after N seconds
timeout 30 slow_command
timeout 5s curl https://slow.example.com
timeout --signal=KILL 60 build.sh  # escalate to SIGKILL
timeout -k 5 30 command            # kill -9 after 5s grace

# exit status: 124 if timed out
timeout 2 sleep 10
echo $?                            # 124

# watch reruns a command
watch -n 1 date
watch -n 5 'kubectl get pods'

# tlimit-style loops
while true; do
  timeout 5 curl -s https://api.example.com/health || echo "down"
  sleep 60
done

무시하고 재발생

trap '' SIGNAL은 시그널을 무시(중단 불가능한 임계 섹션에 편리). 자식에게 시그널을 전달하려면 $!로 PID를 캡처하고 재전송. set -e가 활성화된 경우 ERR trap은 명령 실패 시 발생 — $LINENO로 실패 줄 번호를 로깅하는 데 유용.

bash
# ignore a signal entirely
trap '' INT                       # ignore Ctrl-C
trap - INT                        # restore default

# re-raise / propagate to children
trap 'kill -TERM $childpid; wait $childpid' TERM

# forward signals to a subprocess
childpid=
run_child() {
  some_server &
  childpid=$!
  wait $childpid
}
trap 'kill -TERM $childpid' TERM INT
run_child

# exit on any error
set -e
trap 'echo "Failed at line $LINENO"' ERR
16

함수 & 라이브러리

함수 정의

함수는 재사용 가능한 명령을 그룹화. 두 가지 구문: name() {...}와 function name {...}. 함수 내 인수는 $1, $2 등 — $0은 여전히 스크립트 이름. $@는 모든 인수로 확장(공백이 있는 인수를 보존하려면 항상 "$@"로 인용). $#는 개수.

bash
# two equivalent syntaxes
greet() {
  echo "Hello, $1!"
}

function greet {                  # also valid
  echo "Hello, $1!"
}

greet Alice                       # Hello, Alice!
greet "Bob Smith"                 # quoted args

# arguments: $1, $2, ... $@, $#, $*
echo_args() {
  echo "Count: $#"
  echo "All: $@"
  echo "First: $1"
  for arg in "$@"; do echo "- $arg"; done
}

반환 값과 지역 변수

return은 종료 상태 설정(0=성공, 1-255=실패) — 값을 반환하지 않음. 값을 얻으려면 echo하고 $()로 캡처. 전역 범위 오염을 피하기 위해 함수 내 변수에 local 사용. local은 재귀 함수에 필수.

bash
# return is exit status (0-255), not a value
is_even() {
  if (( $1 % 2 == 0 )); then return 0; else return 1; fi
}

if is_even 4; then echo "even"; fi

# capture stdout as a value
get_date() { date +%Y-%m-%d; }
today=$(get_date)

# local variables
counter() {
  local count=0                  # scoped to function
  ((count++))
  echo $count
}

라이브러리 소싱

source(또는 .)는 현재 셸에서 파일을 실행 — 함수, 변수 및 별칭이 사용 가능하게. 이것이 재사용 가능한 라이브러리를 구축하는 방법. BASH_SOURCE[0] vs $0는 소싱된 것과 실행된 것을 구분 — 라이브러리이자 실행 가능한 스크립트인 파일에 편리.

bash
# library file: lib/utils.sh
log() {
  echo "[$(date +%H:%M:%S)] $*" >&2
}

CONFIG_PATH="/etc/myapp"

# main script
source lib/utils.sh              # or: . lib/utils.sh
log "Starting..."
echo "Config: $CONFIG_PATH"

# check if sourced vs executed
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  echo "Running directly"
else
  echo "Sourced"
fi

재귀 함수

Bash는 재귀를 지원하지만 느림(각 호출이 $()를 위해 서브셸을 생성)하고 제한된 스택 깊이(~1000개)를 가짐. 계산 집약적 작업의 경우 awk, Python 또는 외부 도구를 선호. 재귀 함수에서 항상 지역 변수 사용 — 그렇지 않으면 호출 간에 서로 덮어씀.

bash
# factorial
factorial() {
  local n=$1
  if (( n <= 1 )); then
    echo 1
  else
    local prev=$(factorial $((n - 1)))
    echo $((n * prev))
  fi
}
factorial 5                      # 120

# fibonacci
fib() {
  if (( $1 < 2 )); then echo $1; return; fi
  echo $(( $(fib $(($1 - 1))) + $(fib $(($1 - 2))) ))
}

# caution: bash recursion is slow and limited depth
# for heavy work, use awk, python, or external tools

기본 및 선택적 인수

${1:-default}는 $1이 설정되지 않거나 비어 있으면 기본값을 치환. ${1:?message}는 누락된 경우 오류로 종료 — 필수 인수에 좋음. shift는 $1을 인수 목록에서 팝. case + shift 결합은 bash 함수에서 플래그와 위치 인수를 파싱하는 관용적 방법.

bash
# default values
greet() {
  local name="${1:-World}"        # default if unset or empty
  local greeting="${2:-Hello}"
  echo "$greeting, $name!"
}
greet                            # Hello, World!
greet Alice                      # Hello, Alice!
greet Alice Hi                   # Hi, Alice!

# required argument
require_arg() {
  : "${1:?Usage: require_arg <name>}"   # exits with message if missing
  echo "Got: $1"
}

# shift through args
process() {
  while (( $# )); do
    case "$1" in
      -v) verbose=1; shift ;;
      *)  files+=("$1"); shift ;;
    esac
  done
}
17

배열 & 연관 배열

인덱스 배열

인덱스 배열은 0 기반 정수 키 사용. 반복 시 공백이 있는 요소를 올바르게 처리하려면 항상 "${arr[@]}" 인용. ${#arr[@]}는 개수. ${!arr[@]}는 인덱스 제공(희소 배열에 유용). unset 'arr[i]'(인용)는 요소 제거.

bash
# declare and assign
fruits=("apple" "banana" "cherry")
fruits+=("date")                 # append
fruits[10]="kiwi"                # sparse

# access
echo "${fruits[0]}"             # first
echo "${fruits[@]}"             # all values
echo "${#fruits[@]}"            # count
echo "${!fruits[@]}"            # indices

# iterate
for f in "${fruits[@]}"; do
  echo "$f"
done

# slice
echo "${fruits[@]:1:2}"         # 2 items starting at index 1

# delete
unset 'fruits[1]'

연관 배열(맵)

연관 배열(declare -A)은 bash 4+ 기능(macOS는 기본적으로 bash 3 제공 — brew install bash 사용). 키는 문자열. -v 테스트는 키 존재 확인. ${!arr[@]}로 키 반복. macOS 기본 /bin/bash는 3.2 — -A를 사용하는 스크립트는 더 새로운 bash를 가리키는 #!/usr/bin/env bash가 필요.

bash
# requires bash 4+
declare -A ages
ages["alice"]=30
ages["bob"]=25
ages["carol"]=42

# access
echo "${ages["alice"]}"
echo "${ages[@]}"               # all values
echo "${!ages[@]}"              # all keys

# iterate keys
for name in "${!ages[@]}"; do
  echo "$name => ${ages[$name]}"
done

# check key exists
if [[ -v ages["alice"] ]]; then echo "yes"; fi

# count
echo "${#ages[@]}"

# delete
unset 'ages[bob]'

배열로 읽기

mapfile(일명 readarray)은 줄을 배열로 가져오는 가장 빠른 방법 — 후행 줄 바꿈을 제거하려면 항상 -t 사용. while-read 루프는 이식 가능하지만 더 느림. 구분된 문자열을 분할하려면 IFS 설정하고 read -ra 사용. < <(cmd) 프로세스 치환은 서브셸 변수 범위 문제를 피함.

bash
# read lines into array
mapfile -t lines < file.txt      # bash 4+
echo "${lines[0]}"

# alternative (portable)
lines=()
while IFS= read -r line; do
  lines+=("$line")
done < file.txt

# split string into array
IFS=',' read -ra csv <<< "a,b,c,d"
echo "${csv[2]}"                # c

# from command
mapfile -t files < <(find . -name "*.js")

# process each
for f in "${files[@]}"; do
  echo "Processing $f"
done

배열 작업

Bash 배열에는 많은 내장 작업이 없음(.indexOf, .reverse, .unique 없음). 이를 위해 루프를 작성하거나 sort/uniq로 파이핑. 고유 배열 트릭은 연관 배열을 집합으로 사용. 복잡한 배열 조작의 경우 awk 또는 실제 프로그래밍 언어 고려 — bash 배열은 간단한 목록에 가장 적합.

bash
# append
arr=(a b c)
arr+=(d e)                       # (a b c d e)

# length
echo "${#arr[@]}"

# slice
echo "${arr[@]:1:2}"            # (b c)

# find index (no built-in; loop)
idx=-1
for i in "${!arr[@]}"; do
  if [[ "${arr[$i]}" == "b" ]]; then idx=$i; break; fi
done

# reverse
n=${#arr[@]}
for ((i=n-1; i>=0; i--)); do
  echo "${arr[$i]}"
done

# unique (preserve order)
declare -A seen
unique=()
for x in "${arr[@]}"; do
  [[ -v seen[$x] ]] && continue
  seen[$x]=1
  unique+=("$x")
done

배열이 있는 중첩 데이터

Bash에는 네이티브 중첩 배열이 없음. 해결 방법: (1) 레코드를 구분자로 결합된 문자열로 저장하고 IFS로 분할, (2) "r,c" 같은 복합 키로 연관 배열 사용, (3) ${!name}으로 변수 간접 참조 사용. 실제 중첩 데이터의 경우 jq + JSON 또는 실제 언어로 전환.

bash
# array of "records" (delimiter-based)
users=("alice:30:admin" "bob:25:user")
for u in "${users[@]}"; do
  IFS=':' read -r name age role <<< "$u"
  echo "$name ($age) - $role"
done

# simulate 2D with name-mangled vars
declare -A grid
grid["0,0"]=1; grid["0,1"]=2
grid["1,0"]=3; grid["1,1"]=4
for key in "${!grid[@]}"; do
  IFS=',' read -r r c <<< "$key"
  echo "[$r,$c] = ${grid[$key]}"
done

# arrays of arrays (not directly supported)
# workaround: use indirection
rows=("row0" "row1")
row0=(a b c)
row1=(d e f)
ref="${rows[0]}[@]"
echo "${!ref}"                   # a b c
18

문자열 조작

매개변수 확장

매개변수 확장은 bash의 가장 강력한 문자열 도구. #와 ##는 앞에서 제거(가장 짧은/가장 긴 매칭); %와 %%는 끝에서 제거. /는 첫 번째 치환, //는 모두 치환. 이것은 서브셸 생성을 피함 — 간단한 작업의 경우 sed로 파이핑하는 것보다 훨씬 빠름.

bash
path="/var/log/app.log"

# length
echo "${#path}"                 # 16

# substring (offset:length)
echo "${path:5}"                # log/app.log
echo "${path:5:3}"              # log

# remove from front (shortest/longest)
echo "${path#*/}"               # var/log/app.log
echo "${path##*/}"              # app.log (basename)

# remove from end
echo "${path%/*}"               # /var/log (dirname)
echo "${path%%.*}"              # /var/log/app

# replace
echo "${path/log/LOG}"          # /var/LOG/app.log (first)
echo "${path//log/LOG}"         # all occurrences

대소문자 변환

Bash 4+는 ,(소문자), ^(대문자), ^(첫 문자 대문자), ,(첫 문자 소문자) 추가. declare -u/-l은 할당을 자동 변환. 이전 bash(macOS 기본)의 경우 tr 또는 awk 사용. 사용자 입력 정규화나 식별자 구축 시 대소문자 변환이 일반적.

bash
s="Hello World"

# bash 4+
echo "${s,,}"                   # hello world (lower)
echo "${s^^}"                   # HELLO WORLD (upper)
echo "${s^}"                    # Hello World (capitalize first)
echo "${s,}"                    # hello World (lowercase first)

# pattern-based
echo "${s^^[aeiou]}"            # hEllO wOrld (vowels upper)

# portable (pre-bash 4)
echo "$s" | tr 'a-z' 'A-Z'
echo "$s" | awk '{print toupper($0)}'

# declare typed variables
declare -u UPPER="hello"         # HELLO
declare -l LOWER="WORLD"         # world

분할과 결합

분할: IFS 설정하고 read -ra 사용. 결합: IFS가 설정된 ${arr[*]}는 작동하지만 까다로움 — join_by 함수가 더 신뢰. printf '%s' "$d%s" 트릭은 나머지 각 인수에 구분자를 앞에 추가하여 깔끔한 결합을 생성. tr은 구분자와 줄 바꿈 사이를 변환 가능.

bash
# split on delimiter
csv="a,b,c,d"
IFS=',' read -ra parts <<< "$csv"
echo "${parts[2]}"              # c
for p in "${parts[@]}"; do echo "$p"; done

# join with delimiter (bash 4+)
arr=(a b c d)
joined=$(IFS=,; echo "${arr[*]}")
echo "$joined"                   # a,b,c,d

# custom joiner
join_by() {
  local d=$1; shift
  local first=$1; shift
  printf '%s' "$first"
  printf '%s' "$d%s" "$@"
}
join_by " | " a b c              # a | b | c

# split into lines
tr ',' '\n' <<< "a,b,c"

검색과 테스트

패턴 매칭: 와일드카드가 있는 ==(* ? [..])는 glob 매칭. =~는 확장 정규식; 캡처 그룹은 BASH_REMATCH에 저장. shopt -s nocasematch는 [[ ]] 비교를 대소문자 무시로 만듦. expr은 레거시 — 새 코드에는 [[ ]]를 선호.

bash
s="Hello World"

# substring search
if [[ "$s" == *"World"* ]]; then echo "contains"; fi
if [[ "$s" == Hello* ]]; then echo "starts with"; fi
if [[ "$s" == *World ]]; then echo "ends with"; fi

# regex match
if [[ "$s" =~ ^H[a-z]+\sW[a-z]+$ ]]; then echo "match"; fi
echo "${BASH_REMATCH[0]}"       # whole match
echo "${BASH_REMATCH[1]}"       # first group

# case-insensitive
shopt -s nocasematch
[[ "$s" == *world* ]] && echo "yes"
shopt -u nocasematch

# index (no built-in; use expr)
expr "$s" : ".*World"            # length up to match

printf 포맷팅

printf는 echo보다 강력 — C 스타일 형식 지정자(%s, %d, %f, %x), 너비/정밀도 및 정렬 지원. %()T는 date 명령을 생성하지 않고 시간 포맷. printf -v는 결과를 변수에 저장. printf '=%.0s' {1..40}는 문자를 N번 반복하는 깔끔한 트릭.

bash
# basic
printf '%s\n' "hello" "world"   # one per line
printf '%s = %d\n' "count" 42

# width and padding
printf '%10s\n' "right"         # right-aligned, width 10
printf '%-10s|\n' "left"        # left-aligned
printf '%05d\n' 42              # 00042 (zero-padded)
printf '%.2f\n' 3.14159         # 3.14

# date formatting
printf 'Today: %(%Y-%m-%d)T\n' -1

# to variable
printf -v today '%(%Y%m%d)T' -1
echo "$today"                    # 20260621

# repeat
printf '=%.0s' {1..40}; echo     # 40 equals signs
19

정규 표현식

=~가 있는 bash 정규식

=~는 ERE(확장 정규식) 사용. 캡처 그룹은 BASH_REMATCH를 채움(인덱스 0 = 전체 매칭, 1+ = 그룹). 문자열을 인용하지만 정규식은 인용하지 마세요(정규식을 인용하면 리터럴이 됨). 대소문자 무시 매칭의 경우 shopt -s nocasematch 사용. 정규식은 POSIX ERE — \d, \w 없음; [0-9], [A-Za-z0-9_] 사용.

bash
s="user_42"

if [[ "$s" =~ ^([a-z]+)_([0-9]+)$ ]]; then
  echo "Match!"
  echo "Name: ${BASH_REMATCH[1]}"   # user
  echo "ID: ${BASH_REMATCH[2]}"     # 42
fi

# email-ish check
email="[email protected]"
if [[ "$email" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
  echo "valid"
fi

# case-insensitive
shopt -s nocasematch
[[ "$s" =~ USER ]] && echo "yes"
shopt -u nocasematch

grep 정규식 패턴

grep은 기본적으로 BRE(기본 정규식)를 사용 여기서 +, ?, |, ()는 백슬래시 필요. -E는 ERE 활성화(더 깔끔한 구문). -P는 \d, \w, \b, 룩어라운드가 있는 PCRE 활성화 — 가장 강력하지만 덜 이식 가능(GNU grep만). macOS/BSD에서 실행해야 하는 스크립트의 경우 -E를 선호.

bash
# basic (BRE) — default
grep 'foo' file
grep 'a\.' file                 # literal dot (escape in BRE)
grep 'a*b' file                  # zero or more a

# extended (ERE) — -E
grep -E 'a+b' file               # one or more
grep -E 'a|b' file               # alternation
grep -E '(ab)+' file             # grouping
grep -E '[0-9]{3}' file          # quantifier

# perl (PCRE) — -P
grep -P '\d{3}' file            # digits
grep -P '\bword\b' file        # word boundary
grep -P '(?<=foo)bar' file       # lookbehind

# anchors
grep -E '^Error' file            # line start
grep -E 'done$' file             # line end
grep -E '^Error.*timeout$' file

sed 정규식 치환

sed는 기본적으로 BRE 사용(캡처 그룹은 \( \) 필요, 역참조 \1). -E는 ERE로 전환(더 깔끔한 ()와 +, ?, |). I 플래그는 치환을 대소문자 무시로 만듦(GNU sed). 패턴에 슬래시가 포함된 경우 다른 구분자(s|...|...|) 사용. [[:space:]]는 공백용으로 이식 가능.

bash
# BRE (default)
sed 's/foo/bar/' file
sed 's/a\.b/x/' file            # literal dot
sed 's/a\(b\)c/\1/' file      # capture with backrefs

# ERE with -E
sed -E 's/a+b/x/' file
sed -E 's/(foo|bar)/X/g' file
sed -E 's/([0-9]{4})/Year: \1/' file

# case-insensitive
sed 's/foo/bar/I' file
sed 's/[fF][oO][oO]/bar/g' file

# common recipes
sed -E 's/^[[:space:]]+//' file  # trim leading whitespace
sed -E 's/[[:space:]]+$//' file  # trim trailing
sed -E 's/ +/ /g' file           # collapse spaces
sed -E 's#https?://[^ ]+##g' file # strip URLs

awk 정규식

awk는 ERE 사용. ~ 연산자는 문자열을 정규식에 대해 테스트. /pat1/, /pat2/는 범위 패턴(pat1에서 pat2까지 포함 매칭). gsub는 제자리 전역 치환. gawk의 세 번째 인수가 있는 match()는 그룹을 배열로 캡처 — 편리하지만 mawk/POSIX awk로 이식 불가.

bash
# match operator ~
awk '$1 ~ /^[0-9]+/' file        # field 1 is numeric
awk '$0 !~ /debug/' file         # line doesn't contain
awk '/^Error/,/^$/' file         # from Error to blank line

# case-insensitive
awk 'BEGIN { IGNORECASE=1 } /error/' file
awk 'tolower($0) ~ /error/' file

# gsub (global substitute)
awk '{ gsub(/foo/, "bar"); print }' file
awk '{ gsub(/[0-9]+/, "N"); print }' file

# match with capture (gawk)
awk 'match($0, /id=([0-9]+)/, m) { print m[1] }' file

# split on regex
awk '{ n = split($0, parts, /[,;\t]/); print n }' file

일반적 정규식 레시피

이 레시피는 일반적 텍스트 추출 작업을 다룸. -o는 매칭된 부분만 인쇄. 정규식은 HTML, JSON 또는 XML 파싱에 이상적이지 않음 — 적절한 파서 사용(JSON용 jq, XML용 xmllint). IPv4의 경우 이 정규식은 형식을 매칭하지만 유효한 범위는 매칭하지 않음(255.255.255.255 vs 999.999.999.999).

bash
# IPv4
grep -E '([0-9]{1,3}\.){3}[0-9]{1,3}' access.log

# URL
grep -Eo 'https?://[^[:space:]]+' file

# email
grep -Eo '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}' file

# date YYYY-MM-DD
grep -E '[0-9]{4}-[0-9]{2}-[0-9]{2}' file

# hex color
grep -Eo '#[0-9A-Fa-f]{6}' file

# strong password (8+, upper, lower, digit)
grep -E '^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).{8,}$' file

# trim whitespace
sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' file

# extract domain from URL
sed -E 's#https?://([^/]+).*#\1#' file
20

디버깅

set 옵션

set -e는 명령 실패 시 종료(스크립트에 필수). set -u는 변수 이름의 오타 잡기. set -o pipefail은 파이프의 어떤 부분이든 실패하면 실패하게 만듦(없으면 마지막 명령의 종료 코드만 중요). set -x는 실행 추적. -euo pipefail로 결합, 이것이 '엄격 모드' — 가장 안전한 기본값.

bash
#!/bin/bash
set -e                           # exit on error
set -u                           # error on unset variable
set -o pipefail                  # pipe fails if any command fails
set -x                           # print commands (trace)
# or combined:
set -euo pipefail

# shorthand
set -Eeuxo pipefail              # -E: ERR trap inherits

# strict mode for robust scripts
set -euo pipefail
IFS=$'\n\t'

# trace specific section
set -x
complex_logic
set +x

# debug a script
bash -x script.sh
bash -xv script.sh               # verbose + trace

-x로 추적

set -x는 실행 전에 각 명령을 인쇄, 기본 접두사는 +(PS4). PS4를 file:line:function을 포함하도록 사용자 정의하면 추적이 디버깅에 훨씬 더 유용. BASH_XTRACEFD는 추적을 다른 FD로 리다이렉트하여 stdout/stderr에서 추적 출력을 분리 가능.

bash
# trace entire script
bash -x script.sh

# trace inside script
set -x
echo "tracing on"
set +x

# customize PS4 (the trace prompt)
export PS4='+ ${BASH_SOURCE:-shell}:${LINENO}: ${FUNCNAME[0]:-main}() '
set -x
my_function
set +x

# trace to file
exec 5> /tmp/trace.log
BASH_XTRACEFD=5
set -x
# ... commands ...
set +x
exec 5>&-

오류 처리와 ERR trap

ERR trap은 명령이 실패할 때 발생(set -e와 함께). trap 내 $?는 실패한 종료 코드 제공. $LINENO는 실패가 발생한 위치 표시. caller 빌트인은 file:line:function 인쇄 — 루프하면 스택 추적 생성. set -E는 ERR trap이 함수 내에서도 발생하게 만듦(그렇지 않으면 함수 컨텍스트에서 비활성화).

bash
set -e
set -E                           # ERR trap inherits functions

on_error() {
  local exit_code=$?
  local line=$1
  echo "ERROR: command failed at line $line with exit code $exit_code" >&2
  echo "Stack:" >&2
  local i=0
  while caller $i >&2; do ((i++)); done
  exit $exit_code
}
trap 'on_error $LINENO' ERR

# now any failing command triggers the handler
false                            # triggers on_error

# caller builtin gives file:line:func
# stack trace via: while caller $i; do ((i++)); done

shellcheck 린터

shellcheck는 셸 스크립트용 사실상의 린터 — 인용 문제, 사용되지 않은 변수, 일반적 함정을 잡고 관용구 제안. 모든 스크립트에서 실행. 가장 일반적 수정은 SC2086: 단어 분할과 glob을 방지하기 위해 변수 인용. 많은 CI 파이프라인이 shellcheck 통과를 요구.

bash
# install: apt install shellcheck (or brew, or use shellcheck.net)
# run:
shellcheck script.sh
shellcheck -x script.sh          # trust includes (no SC1090 warnings)
shellcheck -S warning script.sh  # only warnings+
shellcheck --exclude=SC2086 *.sh # exclude specific rules

# common warnings:
# SC2086: double-quote to prevent globbing (use "$var")
# SC2046: quote $(cmd) to prevent word splitting
# SC2034: unused variable
# SC2155: declare and assign separately (masks return code)
# SC2004: $/${} unnecessary in arithmetic

# in script: disable inline
# shellcheck disable=SC2086
echo $unquoted_var

# disable for next line
# shellcheck disable=SC2086
echo $args

로깅과 진단

타임스탬프와 색상이 있는 수준별 로거(DEBUG/INFO/WARN/ERROR)는 스크립트를 디버깅하기 훨씬 쉽게 만듦. 데이터용 stdout을 깨끗하게 유지하기 위해 항상 stderr에 로그. --verbose 플래그(set -x 활성화)와 --dry-run(실행하지 않고 명령 인쇄) 지원. ANSI 색상 코드: 31=빨강, 33=노랑, 32=초록, 90=회색.

bash
# logging function with levels
log() {
  local level=$1; shift
  local msg="$*"
  local ts=$(date '+%Y-%m-%d %H:%M:%S')
  local color
  case $level in
    DEBUG) color='\033[90m' ;;
    INFO)  color='\033[32m' ;;
    WARN)  color='\033[33m' ;;
    ERROR) color='\033[31m' ;;
  esac
  printf "%b[%s] [%s] %s\033[0m\n" "$color" "$ts" "$level" "$msg" >&2
}

log INFO "Starting process"
log WARN "Disk space low"
log ERROR "Connection failed"

# verbose flag
[[ $VERBOSE ]] && set -x

# dry-run mode
if [[ $DRY_RUN ]]; then
  echo "Would: $cmd"
else
  eval "$cmd"
fi
21

보안

따옴표와 인젝션

가장 중요한 Bash 보안 규칙: 모든 변수 확장에 따옴표를 사용하세요. 따옴표 없는 $var은 단어 분할과 글로빙을 거쳐 버그와 인젝션을 유발합니다. 동적 인수가 있는 명령의 경우 문자열 대신 배열(cmd=(ls "$dir"); "${cmd[@]}")을 사용하세요. 사용자 입력을 셸 문자열로 eval이나 $()에 넣지 마세요.

bash
# ALWAYS quote variables
file="my file.txt"
rm $file                         # BAD: rm my file.txt (3 args!)
rm "$file"                       # GOOD: rm "my file.txt"

# use arrays for command + args
files=("a.txt" "b file.txt" "c.txt")
rm "${files[@]}"                # each file as separate arg, safely

# avoid eval
eval "echo $user_input"          # DANGEROUS
echo "$user_input"               # safe

# avoid command substitution in unsafe places
# BAD: filename from user, executed
cmd="ls $userdir"
$cmd                             # word-splitting + globbing

# GOOD: use array
cmd=(ls "$userdir")
"${cmd[@]}"

안전한 임시 파일과 비밀 정보

mktemp는 예측할 수 없는 이름과 600 권한으로 파일을 생성하여 심볼릭 링크 경쟁으로부터 안전합니다. 항상 trap으로 정리하세요. 비밀 정보는 환경 변수에서 읽고, 절대 하드코딩하지 마세요. 비밀 정보를 명령줄 인수로 전달하지 마세요(ps를 통해 노출됨) — stdin, 환경 변수 또는 600 권한의 설정 파일을 사용하세요.

bash
# safe temp files
tmp=$(mktemp)                    # atomic, unpredictable name
trap 'rm -f "$tmp"' EXIT

# safe temp dir
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT

# secrets: read from env, never hardcode
api_key="${API_KEY:?Set API_KEY env var}"

# don't leak secrets to process list (ps)
# BAD: curl -H "Authorization: Bearer $TOKEN" ...
# GOOD: pass via stdin or config file
curl -H @- <<EOF https://api.example.com
Authorization: Bearer $TOKEN
EOF

# restrict file perms for secrets
umask 077
echo "$api_key" > ~/.secrets/api
chmod 600 ~/.secrets/api

입력 검증

사용하기 전에 모든 사용자 입력을 검증하세요. 정규식은 형식을 검사하고, 산술 연산은 범위를 검사합니다. 파일 경로의 경우 명시적으로 허용하지 않는 한 ..(탐색)과 절대 경로를 거부하세요. tr -cd는 허용된 집합에 없는 문자를 제거하여 식별자를 살균하는 데 유용합니다. 허용된 작업에 대해 블랙리스트보다 화이트리스트(case)가 더 안전합니다.

bash
# validate numeric input
read -p "Enter a number: " n
if ! [[ "$n" =~ ^[0-9]+$ ]]; then
  echo "Not a number" >&2; exit 1
fi

# validate range
if (( n < 1 || n > 100 )); then
  echo "Out of range" >&2; exit 1
fi

# validate file path (no traversal)
path="$1"
case "$path" in
  *..*|/*) echo "Invalid path" >&2; exit 1 ;;
esac

# sanitize filename
safe=$(echo "$name" | tr -cd 'A-Za-z0-9._-')

# whitelist allowed values
case "$action" in
  start|stop|restart) ;;
  *) echo "Invalid action" >&2; exit 1 ;;
esac

권한 삭제와 setuid

setuid 셸 스크립트는 안전하지 않습니다 — 대부분의 시스템은 스크립트의 setuid 비트를 무시합니다. 대신 tight sudoers 항목(특정 명령, 비밀번호 없음)으로 sudo를 사용하세요. exec sudo -u로 가능한 한 빨리 root를 삭제하세요. 권한(예: 포트 80 바인딩)을 위해 root로 실행하는 대신 setcap을 사용하세요. chmod 700은 스크립트를 소유자만 접근 가능하게 합니다.

bash
# drop privileges in a script running as root
if [[ $EUID -eq 0 ]]; then
  exec sudo -u appuser "$0" "$@"
fi

# check for root
if [[ $EUID -ne 0 ]]; then
  echo "Requires root" >&2; exit 1
fi

# setuid scripts are DANGEROUS — avoid them
# instead, use sudoers entries
# /etc/sudoers.d/myapp:
#   appuser ALL=(root) NOPASSWD: /usr/local/bin/myapp.sh

# restrict script permissions
chmod 700 script.sh              # only owner can run
chown appuser:appuser script.sh

# use capsh instead of full root for fine-grained caps
# setcap 'cap_net_bind_service=+ep' /usr/bin/myapp

서명과 무결성

다운로드한 스크립트, 특히 root로 실행하는 설치 프로그램의 체크섬과 서명을 항상 검증하세요. sha256sum -c는 알려진 좋은 해시와 비교합니다. GPG 서명은 무결성과 신뢰성(서명자의 신원)을 모두 증명합니다. curl을 sha256sum으로 파이프하면 실행 전에 해시를 확인할 수 있지만, 더 안전한 패턴은 다운로드 후 검증 후 실행입니다.

bash
# verify checksum
sha256sum -c file.sha256
# file.sha256 contains: <hash>  <filename>

# generate checksums
sha256sum release.tar.gz > release.tar.gz.sha256
sha256sum *.iso > isos.sha256

# GPG sign and verify
gpg --sign file.txt              # creates file.txt.gpg
gpg --verify file.txt.gpg
gpg --detach-sig --armor file.tar.gz
gpg --verify file.tar.gz.asc file.tar.gz

# download + verify in one go
curl -fsSL https://example.com/install.sh | sha256sum
curl -fsSL https://example.com/install.sh -o install.sh
sha256sum -c <(echo "expected_hash  install.sh")
22

네트워킹

curl 고급

curl은 URL을 통해 데이터를 전송합니다. -X는 메서드 설정, -d는 데이터 전송, -H는 헤더 설정. -O는 원격 파일명으로 저장. -I는 헤더만 가져옵니다. -L은 리다이렉트를 따릅니다. -u user:pass는 인증용. -v는 상세 출력용.

bash
# POST with data
curl -X POST -d "name=Alice" https://api.example.com
# JSON POST
curl -H "Content-Type: application/json" \
     -d '{"name":"Alice"}' https://api.example.com
# Download with progress
curl -O https://example.com/file.zip
# Headers only
curl -I https://example.com

wget

wget은 비대화형으로 파일을 다운로드합니다. -m은 사이트를 미러링합니다. -c는 중단된 다운로드를 재개합니다. -r은 재귀, -l은 깊이 제한. -b는 백그라운드 실행. 재귀 다운로드에는 curl보다 wget이 낫습니다.

bash
# Download file
wget https://example.com/file.zip
# Mirror website
wget -m https://example.com
# Resume download
wget -c https://example.com/largefile.zip
# Recursive download
wget -r -l 2 https://example.com
# Background
wget -b https://example.com/file.zip

ssh & scp

ssh는 원격 머신에 연결합니다. -i는 키 파일 지정. -L은 로컬 포트 포워딩 생성. scp는 SSH를 통해 파일 복사. -r은 디렉토리 재귀. ssh-copy-id로 키를 설치하세요. 별칭을 위해 ~/.ssh/config를 설정하세요.

bash
# SSH connect
ssh user@host
# SSH with key
ssh -i ~/.ssh/id_rsa user@host
# Port forwarding
ssh -L 8080:localhost:80 user@host
# Copy file
scp file.txt user@host:/path/
# Copy directory
scp -r dir/ user@host:/path/

netstat & ss

netstat와 ss는 네트워크 연결을 표시합니다. -t TCP, -u UDP, -l 리스닝, -n 숫자, -p 프로세스. ss가 netstat보다 빠르고 상세합니다. 어떤 프로세스가 포트를 사용하는지 찾거나 연결 문제를 진단하는 데 사용하세요.

bash
# List listening ports
netstat -tlnp
ss -tlnp
# Show all connections
netstat -an
ss -tunap
# Show routing table
netstat -r
# Show interface stats
netstat -i

ping & traceroute

ping은 도달 가능성과 지연을 테스트합니다(-c는 횟수 제한). traceroute는 호스트까지의 경로를 표시합니다. dig는 DNS 레코드(A, MX, NS, TXT)를 쿼리합니다. +short는 간결한 출력을 제공합니다. 네트워크 진단과 DNS 문제 해결에 사용하세요.

bash
# Check connectivity
ping -c 4 google.com
# Trace route
traceroute google.com
# DNS lookup
dig example.com
dig +short example.com
# Get DNS records
dig MX example.com
dig NS example.com
23

셸 스크립팅 심화

함수

함수는 명령을 그룹화합니다. local은 함수 범위 변수를 생성합니다. $1, $2는 인수입니다. return은 종료 상태(0-255)를 설정합니다. $()로 출력을 캡처합니다. 함수는 사용 전에 정의되어야 합니다. source로 파일에서 로드하세요.

bash
greet() {
    local name="$1"
    echo "Hello, $name"
    return 0
}
greet "Alice"
# Return value via $?
result=$(greet "Bob")

조건문

[ ]는 test 명령입니다. -f 파일, -d 디렉토리, -z 빈 문자열, -n 비어있지 않음. =는 문자열용, -eq/-ne/-gt/-lt는 숫자용. 공백을 처리하려면 항상 변수에 따옴표를 사용하세요. [[ ]]는 정규식을 지원하는 Bash 확장입니다.

bash
if [ -f "/path/file" ]; then
    echo "File exists"
elif [ -d "/path" ]; then
    echo "Directory exists"
else
    echo "Not found"
fi
# String comparison
if [ "$str" = "hello" ]; then ... fi
# Numeric
if [ "$num" -gt 10 ]; then ... fi

루프

for는 리스트를 반복합니다. C 스타일 for는 (( ))를 사용합니다. while은 조건이 실패할 때까지 읽습니다. while read는 파일을 한 줄씩 안전하게 처리합니다. 항상 변수에 따옴표를 사용하세요. 재귀적 파일 반복에는 find를 사용하세요.

bash
# For loop
for i in 1 2 3; do
    echo $i
done
# C-style
for ((i=0; i<5; i++)); do echo $i; done
# While loop
while read line; do
    echo "$line"
done < file.txt
# Iterate files
for f in *.txt; do process "$f"; done

배열

배열은 괄호를 사용합니다. ${arr[@]}는 모든 요소를 확장합니다. ${#arr[@]}는 길이입니다. 공백을 처리하려면 항상 "${arr[@]}"에 따옴표를 사용하세요. declare -A는 연관 배열(bash 4+)을 생성합니다. read -a로 배열로 분할하세요.

bash
# Declare array
fruits=("apple" "banana" "cherry")
# Access
echo ${fruits[0]}      # apple
echo ${fruits[@]}      # all
echo ${#fruits[@]}     # length
# Iterate
for fruit in "${fruits[@]}"; do
    echo "$fruit"
done
# Associative array
declare -A config
config[key]="value"

에러 처리

set -e는 에러 시 종료, -u는 정의되지 않은 변수 시, pipefail은 파이프 실패를 캡처합니다. trap ERR는 에러를 처리합니다. trap EXIT는 정리를 실행합니다. command -v는 명령이 존재하는지 확인합니다. 항상 스크립트 시작 시 set -euo pipefail을 사용하세요.

bash
set -e  # Exit on error
set -u  # Error on undefined variable
set -o pipefail  # Pipeline fails if any command fails
set -euo pipefail  # All three (recommended)
trap 'echo "Error on line $LINENO"' ERR
trap 'cleanup' EXIT
# Check command result
if ! command -v docker &> /dev/null; then
    echo "docker not installed"
fi
24

파일 작업 심화

find 고급

find는 기준으로 파일을 검색합니다. -type f/d는 파일/디렉토리용. -size +1M은 1MB보다 큰 파일. -mtime -7은 7일 이내 수정. -exec는 각 일치 항목에 명령을 실행. {}는 파일명; \;는 명령 종료.

bash
# By name
find . -name "*.txt"
# By type and size
find . -type f -size +1M
# By modification time
find . -mtime -7  # Modified in last 7 days
# Execute command
find . -name "*.log" -exec gzip {} \;
# Delete
find . -name "*.tmp" -delete

tar & 압축

tar는 파일을 결합; -z는 gzip으로 압축. -c 생성, -x 추출, -t 목록, -v 상세, -f 파일명. .tar.bz2에는 -j 사용. zip/unzip은 ZIP 형식 처리. 특정 디렉토리로 추출하려면 -C 사용.

bash
# Create tar.gz
tar -czvf archive.tar.gz dir/
# Extract
tar -xzvf archive.tar.gz
# List contents
tar -tzvf archive.tar.gz
# zip/unzip
zip -r archive.zip dir/
unzip archive.zip

rsync

rsync는 차이점만 전송하여 효율적으로 파일을 동기화합니다. -a 아카이브 모드(속성 보존), -v 상세, -z 압축. --delete는 소스에 없는 파일 제거. src/의 후행 슬래시가 중요: 있으면 내용을 복사; 없으면 디렉토리를 복사.

bash
# Sync local
rsync -av src/ dest/
# Sync to remote
rsync -avz src/ user@host:/path/
# Delete files not in source
rsync -av --delete src/ dest/
# Dry run
rsync -av --dry-run src/ dest/
# Exclude
rsync -av --exclude='*.log' src/ dest/

파일 권한

권한: r=4, w=2, x=1. 755 = rwxr-xr-x (소유자 전체, 다른 사람 읽기/실행). 644 = rw-r--r-- (파일). u/g/o/a = 사용자/그룹/기타/전체. chmod -R은 재귀. chown은 소유권 변경.

bash
# Symbolic
chmod u+x file      # User execute
chmod g-w file      # Group write off
chmod a=r file      # All read only
# Numeric
chmod 755 file      # rwxr-xr-x
chmod 644 file      # rw-r--r--
# Change owner
chown user:group file
# Recursive
chmod -R 755 dir/

ln & 심볼릭 링크

하드 링크는 동일한 inode를 가리킴(동일 파일 시스템만). 심볼릭 링크(-s)는 경로를 가리킴. 하드 링크는 원본 삭제 후에도 유지. 심볼릭 링크는 대상이 이동하면 깨짐. readlink로 심볼릭 링크 해석.

bash
# Hard link
ln file.txt hardlink.txt
# Symbolic link
ln -s /path/to/target symlink
# Find broken symlinks
find . -type l ! -exec test -e {} \; -print
# Copy symlink target
cp -L symlink dest/
25

일반적인 함정

따옴표 없는 변수

따옴표 없는 변수는 공백과 글로브로 분할됩니다. $(ls)는 공백이 있는 파일명에서 깨집니다. 글로브(*.txt) 또는 find -print0와 read -d를 사용하세요. 항상 변수에 따옴표: "$var". 안전한 줄 읽기를 위해 IFS= read -r 사용.

bash
# BUG: breaks with spaces
for f in $(ls); do echo "$f"; done
# GOOD: handle spaces
for f in *.txt; do echo "$f"; done
# Or with find
find . -name "*.txt" -print0 | while IFS= read -r -d '' f; do
    echo "$f"
done

cp vs mv

cp는 복사, mv는 이동. 둘 다 경고 없이 덮어쓸 수 있습니다. 덮어쓰기 전에 확인하려면 -i 사용. 절대 덮어쓰지 않으려면 -n 사용. mv는 동일 파일 시스템에서 원자적이며, 잠금과 원자적 업데이트에 유용합니다.

bash
# cp: copy (original remains)
cp file.txt backup.txt
# mv: move/rename (original removed)
mv file.txt newname.txt
# Common mistake: overwriting
cp important.txt /tmp/  # If /tmp/important.txt exists, overwritten!
# Use -i for safety
cp -i file.txt /tmp/

rm의 위험

rm -rf는 위험하며, 변수가 있으면 특히 위험합니다. 빈 변수는 rm -rf /를 유발합니다. 항상 변수에 따옴표를 사용하고 확인하세요. 복구 가능한 삭제를 위해 trash-cli를 고려하세요. rm -rf / 또는 sudo로 부주의하게 실행하지 마세요.

bash
# DANGEROUS: recursive force
rm -rf /  # Never do this!
rm -rf $VAR/  # If $VAR is empty, becomes rm -rf /
# Safer: check first
if [ -n "$VAR" ]; then
    rm -rf "$VAR/"
fi
# Use trash-cli instead
trash-put file.txt

셔뱅

셔뱅(#!)은 인터프리터를 지정합니다. #!/usr/bin/env bash가 #!/bin/bash보다 이식성이 더 좋습니다. 최대 이식성을 위해 /bin/sh 사용(bashism 회피). 항상 chmod +x로 스크립트를 실행 가능하게 만드세요.

bash
#!/bin/bash          # bash script
#!/usr/bin/env bash  # Portable bash
#!/usr/bin/env python3  # Python
#!/bin/sh            # POSIX sh (more portable)
# Make executable
chmod +x script.sh
./script.sh

종료 상태

$?는 마지막 명령의 종료 상태를 가집니다. 0 = 성공, 0이 아니면 실패. 다른 명령이 덮어쓰기 전에 즉시 확인하세요. if command를 직접 사용하는 것이 더 좋습니다. 스크립트에서 항상 의미 있는 상태 코드로 종료하세요.

bash
# Check last command
if [ $? -eq 0 ]; then
    echo "Success"
fi
# Better: direct check
if command; then
    echo "Success"
else
    echo "Failed"
fi
# Exit with status
exit 0  # Success
exit 1  # Failure

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.