Lesson 06: I/O와 리다이렉션(Redirection)

Lesson 06: I/O와 리다이렉션(Redirection)

난이도: ⭐⭐⭐

이전: 05_Functions_and_Libraries.md | 다음: 07_String_Processing.md


1. 파일 디스크립터(File Descriptors)

파일 디스크립터(FD)는 열린 파일이나 I/O 스트림을 참조하는 정수입니다. bash에서 I/O 리다이렉션을 마스터하려면 이들을 이해하는 것이 필수적입니다.

1.1 표준 파일 디스크립터

모든 프로세스는 세 개의 표준 파일 디스크립터를 가집니다:

FD 이름 목적 기본값
0 stdin 표준 입력(Standard input) 키보드
1 stdout 표준 출력(Standard output) 터미널
2 stderr 표준 에러(Standard error) 터미널
#!/bin/bash

# stdin(FD 0)에서 읽기
read -p "Enter your name: " name
echo "Hello, $name"

# stdout(FD 1)에 쓰기
echo "This goes to stdout" >&1  # 명시적 (일반 echo와 동일)

# stderr(FD 2)에 쓰기
echo "This is an error message" >&2

1.2 사용자 정의 파일 디스크립터

3-9번 파일 디스크립터를 사용자 정의로 생성할 수 있습니다:

#!/bin/bash

# FD 3에 읽기용으로 파일 열기
exec 3< input.txt

# FD 3에서 읽기
while read -u 3 line; do
    echo "Line: $line"
done

# FD 3 닫기
exec 3<&-

# FD 4에 쓰기용으로 파일 열기
exec 4> output.txt

# FD 4에 쓰기
echo "First line" >&4
echo "Second line" >&4

# FD 4 닫기
exec 4>&-

1.3 읽기/쓰기용 파일 디스크립터 열기

#!/bin/bash

# FD 5에 읽기와 쓰기 모두 가능하게 파일 열기
exec 5<> datafile.txt

# 현재 내용 읽기
while read -u 5 line; do
    echo "Read: $line"
done

# 새 내용 쓰기 (추가됨)
echo "New data" >&5

# FD 5 닫기
exec 5>&-

1.4 파일 디스크립터 복제

#!/bin/bash

# stdout(FD 1)을 FD 3으로 복제
exec 3>&1

# 이제 stdout을 파일로 리다이렉트
exec 1> output.log

# 이것은 output.log로 감
echo "Logging to file"

# 이것은 여전히 터미널로 감 (FD 3 사용)
echo "Direct to terminal" >&3

# FD 3에서 stdout 복원
exec 1>&3

# FD 3 닫기
exec 3>&-

# 이제 다시 터미널로
echo "Back to normal stdout"

1.5 파일 디스크립터 검사

#!/bin/bash

# 현재 셸의 파일 디스크립터 보기
ls -l /dev/fd/
# 또는
ls -l /proc/self/fd/

# FD가 열려있는지 확인
if [[ -e /dev/fd/3 ]]; then
    echo "FD 3 is open"
else
    echo "FD 3 is closed"
fi

# FD에 대한 정보 얻기
exec 5> myfile.txt
readlink /proc/self/fd/5  # 파일 경로 표시
exec 5>&-

2. 고급 리다이렉션

기본 >< 이외에도, bash는 강력한 리다이렉션 연산자를 제공합니다.

2.1 Stderr 별도로 리다이렉트

#!/bin/bash

# stdout을 file1로, stderr를 file2로 리다이렉트
command > stdout.log 2> stderr.log

# 예제: C 프로그램 컴파일
gcc program.c -o program > compile_output.txt 2> compile_errors.txt

# 컴파일 에러가 있는지 확인
if [[ -s compile_errors.txt ]]; then
    echo "Compilation failed:"
    cat compile_errors.txt
else
    echo "Compilation successful!"
fi

2.2 Stdout과 Stderr 병합

#!/bin/bash

# 방법 1: stderr를 stdout으로 리다이렉트
command > output.log 2>&1

# 방법 2: 단축 표기법 (Bash 4+)
command &> output.log

# 방법 3: 둘 다 추가
command >> output.log 2>&1

# 예제: 테스트 스위트 실행
./run_tests.sh &> test_results.log

# 이것은 잘못됨 (순서가 중요):
command 2>&1 > output.log  # stderr는 여전히 터미널로!
# 올바른 방법:
command > output.log 2>&1  # stderr가 stdout을 따라 파일로

2.3 출력 버리기

#!/bin/bash

# stdout 버리기
command > /dev/null

# stderr 버리기
command 2> /dev/null

# 둘 다 버리기
command &> /dev/null

# 예제: 무음 작업
if some_command &> /dev/null; then
    echo "Command succeeded (silently)"
fi

# stderr 유지, stdout 버리기
command > /dev/null

# 예제: 명령어가 존재하는지 확인
if command -v python3 > /dev/null 2>&1; then
    echo "python3 is installed"
fi

2.4 Stdout과 Stderr 바꾸기

#!/bin/bash

# stdout과 stderr 바꾸기
command 3>&1 1>&2 2>&3 3>&-

# 설명:
# 3>&1  - stdout을 FD 3에 저장
# 1>&2  - stdout을 stderr로 리다이렉트
# 2>&3  - stderr를 FD 3으로 리다이렉트 (원래 stdout)
# 3>&-  - FD 3 닫기

# 실전 예제: 에러 메시지를 stdout으로, 일반 출력을 stderr로
swap_outputs() {
    "$@" 3>&1 1>&2 2>&3 3>&-
}

# 이제 에러가 stdout에 나타남 (캡처 가능)
errors=$(swap_outputs some_command)

2.5 파일 디스크립터 저장 및 복원

#!/bin/bash

# 원래의 stdout과 stderr 저장
exec 3>&1 4>&2

# stdout과 stderr를 파일로 리다이렉트
exec 1> output.log 2> error.log

# 여기 있는 명령들은 로그 파일에 씀
echo "This goes to output.log"
echo "This is an error" >&2

# 원래의 stdout과 stderr 복원
exec 1>&3 2>&4

# 백업 FD 닫기
exec 3>&- 4>&-

# 이제 다시 터미널로
echo "Back to terminal"

2.6 추가(Append) vs 덮어쓰기(Truncate)

#!/bin/bash

# 파일 덮어쓰기
echo "New content" > file.txt

# 파일에 추가
echo "Additional content" >> file.txt

# stderr 추가
command 2>> error.log

# stdout과 stderr 모두 추가
command &>> output.log

3. Here Documents와 Here Strings

Here documents는 임시 파일을 만들지 않고 명령에 여러 줄의 입력을 제공합니다.

3.1 기본 Here Document

#!/bin/bash

# 기본 here document
cat <<EOF
This is a multi-line
here document.
It can contain variables: $HOME
And command substitution: $(date)
EOF

# 들여쓰기 포함 (<<-는 선행 탭 제거, 공백은 아님)
cat <<-EOF
    This is indented with tabs
    The tabs will be removed
    But the text stays aligned
EOF

3.2 변수 확장 없는 Here Document

#!/bin/bash

# 구분자를 따옴표로 묶어 확장 방지
cat <<'EOF'
Variables are literal: $HOME
Command substitution is literal: $(date)
This is useful for generating scripts or code.
EOF

# 예제: bash 스크립트 생성
cat <<'SCRIPT' > myscript.sh
#!/bin/bash
echo "Hello from generated script"
echo "Current directory: $PWD"
SCRIPT

chmod +x myscript.sh

3.3 변수에 Here Document 저장

#!/bin/bash

# here document를 변수에 할당
read -r -d '' sql_query <<EOF
SELECT u.name, u.email, o.order_id
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.status = 'pending'
ORDER BY o.created_at DESC
LIMIT 10;
EOF

echo "Executing query:"
echo "$sql_query"

# 대안 방법 (명령 치환 사용)
json_data=$(cat <<EOF
{
    "name": "John Doe",
    "email": "john@example.com",
    "age": 30,
    "roles": ["admin", "user"]
}
EOF
)

echo "$json_data"

3.4 명령 입력과 Here Document

#!/bin/bash

# 명령에 여러 줄 입력 보내기
mysql -u root -p <<SQL
USE mydb;
CREATE TABLE IF NOT EXISTS users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) NOT NULL,
    email VARCHAR(100) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO users (username, email) VALUES ('alice', 'alice@example.com');
SQL

# Python 스크립트 실행
python3 <<PYTHON
import sys
import json

data = {
    'message': 'Hello from Python',
    'version': sys.version
}

print(json.dumps(data, indent=2))
PYTHON

3.5 Here Strings

#!/bin/bash

# Here string: 한 줄 입력
grep "pattern" <<< "This is a test pattern string"

# 변수 파이핑에 유용
while read -r word; do
    echo "Word: $word"
done <<< "one two three four five"

# 예제: CSV 줄 파싱
IFS=',' read -r name age city <<< "John,30,NYC"
echo "Name: $name, Age: $age, City: $city"

# 문자열을 Base64 인코딩
encoded=$(base64 <<< "Secret message")
echo "Encoded: $encoded"

# 다시 디코딩
decoded=$(base64 -d <<< "$encoded")
echo "Decoded: $decoded"

3.6 실전 템플릿 생성

#!/bin/bash

generate_html() {
    local title=$1
    local content=$2

    cat <<HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>$title</title>
</head>
<body>
    <h1>$title</h1>
    <p>$content</p>
    <footer>Generated on $(date)</footer>
</body>
</html>
HTML
}

# HTML 페이지 생성
generate_html "My Page" "Welcome to my website!" > index.html

# 설정 파일 생성
generate_config() {
    local host=$1
    local port=$2

    cat <<CONFIG > app.conf
# Application Configuration
# Generated: $(date)

[server]
host = $host
port = $port
workers = 4

[database]
host = localhost
port = 5432
name = myapp

[logging]
level = INFO
file = /var/log/myapp.log
CONFIG
}

generate_config "0.0.0.0" "8080"

4. 프로세스 치환(Process Substitution)

프로세스 치환은 명령 출력에 대한 임시 명명된 파이프를 생성하여, 파일이 필요한 곳에 명령을 사용할 수 있게 합니다.

4.1 입력 프로세스 치환

#!/bin/bash

# 두 명령의 출력 비교
diff <(ls dir1) <(ls dir2)

# 더 복잡한 예제: 정렬된 리스트 비교
diff <(sort file1.txt) <(sort file2.txt)

# 두 시스템의 실행 중인 프로세스 비교
diff <(ssh server1 ps aux | sort) <(ssh server2 ps aux | sort)

# 예제: 두 명령 출력의 공통 줄 찾기
comm -12 <(sort list1.txt) <(sort list2.txt)

4.2 출력 프로세스 치환

#!/bin/bash

# 여러 파일에 동시에 쓰기
tee >(grep "ERROR" > errors.log) \
    >(grep "WARN" > warnings.log) \
    >(grep "INFO" > info.log) \
    < application.log > /dev/null

# 예제: 로그를 심각도로 분할
process_logs() {
    local logfile=$1

    cat "$logfile" | tee \
        >(grep "ERROR" > errors.log) \
        >(grep "WARN" > warnings.log) \
        > all.log
}

4.3 서브셸 변수 스코프 문제 회피

#!/bin/bash

# 문제: 파이프라인의 변수는 서브셸에 있음
count=0
cat file.txt | while read line; do
    ((count++))
done
echo "Lines: $count"  # 출력: 0 (변수가 수정되지 않음!)

# 해결책 1: 프로세스 치환
count=0
while read line; do
    ((count++))
done < <(cat file.txt)
echo "Lines: $count"  # 올바른 카운트

# 해결책 2: 명령 치환과 here string 사용 (작은 파일용)
count=0
while read line; do
    ((count++))
done <<< "$(cat file.txt)"
echo "Lines: $count"  # 올바른 카운트

4.4 다중 입력 스트림

#!/bin/bash

# 여러 파일에서 병렬로 읽기
paste <(cut -d',' -f1 file1.csv) \
      <(cut -d',' -f2 file2.csv) \
      <(cut -d',' -f3 file3.csv)

# 예제: 여러 소스의 데이터 병합
while read -u 3 name && read -u 4 age && read -u 5 city; do
    echo "$name is $age years old and lives in $city"
done 3< <(cut -d',' -f1 data.csv) \
     4< <(cut -d',' -f2 data.csv) \
     5< <(cut -d',' -f3 data.csv)

4.5 실전 예제

#!/bin/bash

# 예제 1: 최근 24시간 내 수정된 파일 중 패턴이 포함된 파일 찾기
grep "TODO" <(find . -type f -mtime -1 -exec cat {} \;)

# 예제 2: 로그 파일 모니터링 및 알림 전송
while read line; do
    if [[ $line == *"CRITICAL"* ]]; then
        echo "Alert: $line" | mail -s "Critical Error" admin@example.com
    fi
done < <(tail -f /var/log/app.log)

# 예제 3: 압축 파일을 추출하지 않고 처리
while read line; do
    echo "Processing: $line"
done < <(gunzip -c data.txt.gz)

# 예제 4: 처리를 위한 임시 파일 리스트 생성
tar czf backup.tar.gz -T <(find /data -type f -mtime -7)

5. 명명된 파이프(Named Pipes - FIFOs)

명명된 파이프는 파일시스템을 통한 프로세스 간 통신을 가능하게 합니다.

5.1 FIFO 생성 및 사용

#!/bin/bash

# 명명된 파이프 생성
mkfifo mypipe

# 생산자 (백그라운드 프로세스)
{
    for i in {1..10}; do
        echo "Message $i"
        sleep 1
    done > mypipe
} &

# 소비자
while read line; do
    echo "Received: $line"
done < mypipe

# 정리
rm mypipe

5.2 생산자-소비자 패턴

#!/bin/bash

PIPE="/tmp/data_pipe_$$"

# 파이프 생성 및 정리용 trap 설정
mkfifo "$PIPE"
trap "rm -f '$PIPE'" EXIT

# 생산자: 데이터 생성
producer() {
    local pipe=$1
    echo "Producer starting..."

    for i in {1..100}; do
        echo "Data item $i: $(date +%s)"
        sleep 0.1
    done > "$pipe"

    echo "Producer finished"
}

# 소비자: 데이터 처리
consumer() {
    local pipe=$1
    echo "Consumer starting..."

    local count=0
    while read line; do
        ((count++))
        # 데이터 처리 (작업 시뮬레이션)
        [[ $((count % 10)) -eq 0 ]] && echo "Processed $count items"
    done < "$pipe"

    echo "Consumer finished: $count items processed"
}

# 백그라운드에서 생산자 실행
producer "$PIPE" &
producer_pid=$!

# 포그라운드에서 소비자 실행
consumer "$PIPE"

# 생산자가 끝날 때까지 대기
wait $producer_pid

5.3 양방향 통신

#!/bin/bash

REQUEST_PIPE="/tmp/request_$$"
RESPONSE_PIPE="/tmp/response_$$"

# 파이프 생성
mkfifo "$REQUEST_PIPE" "$RESPONSE_PIPE"
trap "rm -f '$REQUEST_PIPE' '$RESPONSE_PIPE'" EXIT

# 서버 프로세스
server() {
    echo "Server started"

    while true; do
        # 요청 읽기
        read request < "$REQUEST_PIPE"

        # 요청 처리
        case $request in
            "PING")
                echo "PONG" > "$RESPONSE_PIPE"
                ;;
            "TIME")
                date > "$RESPONSE_PIPE"
                ;;
            "QUIT")
                echo "BYE" > "$RESPONSE_PIPE"
                break
                ;;
            *)
                echo "ERROR: Unknown command" > "$RESPONSE_PIPE"
                ;;
        esac
    done

    echo "Server stopped"
}

# 클라이언트 함수
client() {
    local command=$1

    # 요청 전송
    echo "$command" > "$REQUEST_PIPE"

    # 응답 읽기
    read response < "$RESPONSE_PIPE"
    echo "Response: $response"
}

# 백그라운드에서 서버 시작
server &
server_pid=$!

sleep 1  # 서버가 시작할 시간 줌

# 요청 전송
client "PING"
client "TIME"
client "QUIT"

# 서버 대기
wait $server_pid

5.4 FIFO vs 프로세스 치환 사용 시기

특징 FIFO 프로세스 치환(Process Substitution)
지속성 예 (삭제될 때까지) 아니오 (자동 정리)
다중 reader/writer 아니오
명시적 동기화 아니오
백그라운드 사용 쉬움 복잡함
정리 필요 수동 자동
최적 용도 장기 실행 IPC 일회성 작업
#!/bin/bash

# 일회성 비교에는 프로세스 치환 사용
diff <(command1) <(command2)

# 지속적 통신에는 FIFO 사용
mkfifo /tmp/logpipe
tail -f /var/log/app.log > /tmp/logpipe &
while read line; do
    process_log_line "$line"
done < /tmp/logpipe

6. 파이프 함정과 해결책

6.1 서브셸 변수 스코프 손실

#!/bin/bash

# 문제: 파이프라인의 마지막 명령이 서브셸에서 실행됨
total=0
cat numbers.txt | while read num; do
    ((total += num))
done
echo "Total: $total"  # 출력: 0 (수정되지 않음!)

# 해결책 1: 프로세스 치환
total=0
while read num; do
    ((total += num))
done < <(cat numbers.txt)
echo "Total: $total"  # 올바름

# 해결책 2: lastpipe 사용 (Bash 4.2+, 스크립트에서만)
shopt -s lastpipe
total=0
cat numbers.txt | while read num; do
    ((total += num))
done
echo "Total: $total"  # 올바름

# 해결책 3: 임시 파일
tmpfile=$(mktemp)
cat numbers.txt > "$tmpfile"
total=0
while read num; do
    ((total += num))
done < "$tmpfile"
rm "$tmpfile"
echo "Total: $total"  # 올바름

6.2 PIPESTATUS 배열

#!/bin/bash

# 파이프라인의 모든 명령 종료 상태 확인
command1 | command2 | command3

# PIPESTATUS는 모든 명령의 종료 코드를 포함
echo "Exit codes: ${PIPESTATUS[@]}"

# 예제: 파이프라인 어디서든 실패 감지
false | true | true
if [[ ${PIPESTATUS[0]} -ne 0 ]]; then
    echo "First command failed"
fi

# 실전 예제: 데이터베이스 파이프라인
{
    mysql -u root -p mydb -e "SELECT * FROM users" | \
    grep "active" | \
    sort -k2
} 2>/dev/null

pipeline_status=("${PIPESTATUS[@]}")
if [[ ${pipeline_status[0]} -ne 0 ]]; then
    echo "Database query failed"
elif [[ ${pipeline_status[1]} -ne 0 ]]; then
    echo "Grep failed"
elif [[ ${pipeline_status[2]} -ne 0 ]]; then
    echo "Sort failed"
else
    echo "Pipeline succeeded"
fi

6.3 파이프라인 에러 처리

#!/bin/bash

# pipefail 활성화: 어떤 명령이든 실패하면 파이프라인 실패
set -o pipefail

# 이제 파이프라인은 어떤 명령이든 실패하면 0이 아닌 값을 반환
if command1 | command2 | command3; then
    echo "Pipeline succeeded"
else
    echo "Pipeline failed"
fi

# 실전 예제: 안전한 데이터 처리
set -euo pipefail  # 에러, 미정의 변수, 파이프라인 실패 시 종료

process_data() {
    local input=$1
    local output=$2

    cat "$input" | \
        grep -v "^#" | \
        sort -u | \
        sed 's/foo/bar/g' \
        > "$output"

    # 어떤 명령이든 실패하면 스크립트 종료
}

# pipefail과 함께 에러 처리
set -o pipefail
if ! tar czf backup.tar.gz --exclude="*.tmp" -T <(find /data -type f); then
    echo "Backup failed" >&2
    exit 1
fi

6.4 명명된 파이프 교착 상태 방지

#!/bin/bash

# 문제: reader/writer가 조율되지 않으면 교착 상태
mkfifo mypipe
echo "data" > mypipe  # 영원히 블록! (reader 없음)

# 해결책 1: 읽기와 쓰기 모두로 파이프 열기
mkfifo mypipe
exec 3<> mypipe  # 읽기/쓰기로 열기

echo "data" >&3  # 쓰기
read line <&3    # 읽기
exec 3>&-        # 닫기

rm mypipe

# 해결책 2: 적절한 동기화와 함께 백그라운드 프로세스
mkfifo mypipe
trap "rm -f mypipe" EXIT

# 백그라운드의 reader
cat < mypipe &
reader_pid=$!

# Writer
echo "data" > mypipe

# reader 대기
wait $reader_pid

7. 실전 I/O 패턴

7.1 여러 목적지로 Tee

#!/bin/bash

# 기본 tee: 파일과 stdout에 쓰기
echo "Important message" | tee log.txt

# 여러 파일
echo "Message" | tee file1.txt file2.txt file3.txt

# 추가 모드
echo "New entry" | tee -a logfile.txt

# 복잡한 예제: 분할 처리
cat data.txt | tee \
    >(grep "ERROR" > errors.log) \
    >(grep "WARN" > warnings.log) \
    >(wc -l > linecount.txt) \
    | grep "INFO" > info.log

7.2 콘솔과 파일에 로깅

#!/bin/bash

# 로깅 설정
LOGFILE="application.log"

log() {
    local level=$1
    shift
    local message="$@"
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')

    # 콘솔과 파일 모두에 로그
    echo "[$timestamp] [$level] $message" | tee -a "$LOGFILE"
}

# 사용
log "INFO" "Application started"
log "WARN" "Configuration file not found, using defaults"
log "ERROR" "Failed to connect to database"

# 대안: 모든 출력 리다이렉트
exec > >(tee -a "$LOGFILE")
exec 2>&1

# 이제 모든 출력이 콘솔과 파일 모두로 감
echo "This appears in both places"
ls /nonexistent  # 에러도 로깅됨

7.3 동일 파일 안전하게 읽고 쓰기

#!/bin/bash

# 잘못된 방법: 읽기 전에 파일을 덮어씀!
sort file.txt > file.txt  # file.txt가 비워짐!

# 해결책 1: sponge 사용 (moreutils에서)
sort file.txt | sponge file.txt

# 해결책 2: 임시 파일 사용
sort file.txt > file.txt.tmp && mv file.txt.tmp file.txt

# 해결책 3: -i 플래그로 제자리 편집 (지원되는 경우)
sed -i 's/foo/bar/g' file.txt

# 원자적 파일 교체
update_config() {
    local config_file=$1
    local tmpfile=$(mktemp)

    # 파일 처리
    process_config < "$config_file" > "$tmpfile"

    # 원자적 교체
    mv "$tmpfile" "$config_file"
}

7.4 원자적 파일 쓰기

#!/bin/bash

# 원자적 쓰기 패턴: 임시에 쓰고 그 다음 이동
atomic_write() {
    local target_file=$1
    local content=$2

    local tmpfile=$(mktemp "${target_file}.XXXXXX")

    # 임시 파일에 쓰기
    echo "$content" > "$tmpfile"

    # 쓰기가 성공했는지 확인
    if [[ $? -eq 0 ]]; then
        # 원자적 이동 (같은 파일시스템에서)
        mv "$tmpfile" "$target_file"
    else
        rm -f "$tmpfile"
        return 1
    fi
}

# 사용
atomic_write "config.json" '{"setting": "value"}'

# 복잡한 예제: 중요한 파일 업데이트
update_critical_file() {
    local file=$1
    local tmpfile=$(mktemp)

    # 정리용 trap 설정
    trap "rm -f '$tmpfile'" RETURN

    # 새 콘텐츠 생성
    if ! generate_content > "$tmpfile"; then
        echo "Error: Failed to generate content" >&2
        return 1
    fi

    # 새 콘텐츠 검증
    if ! validate_content "$tmpfile"; then
        echo "Error: Content validation failed" >&2
        return 1
    fi

    # 원본과 동일한 권한 설정
    chmod --reference="$file" "$tmpfile" 2>/dev/null

    # 원자적 교체
    mv "$tmpfile" "$file"
}

7.5 안전한 동시 접근을 위한 파일 잠금

#!/bin/bash

# 파일 잠금에 flock 사용
update_counter() {
    local counter_file="counter.txt"
    local lockfile="counter.lock"

    # 배타적 잠금 획득 (FD 200)
    {
        flock -x 200

        # 현재 값 읽기
        local count=0
        [[ -f $counter_file ]] && count=$(cat "$counter_file")

        # 증가
        ((count++))

        # 다시 쓰기
        echo "$count" > "$counter_file"

        echo "Counter updated to: $count"

    } 200>"$lockfile"
}

# 여러 프로세스가 안전하게 이것을 호출 가능
for i in {1..10}; do
    update_counter &
done
wait

# 최종 값
echo "Final count: $(cat counter.txt)"

# 대안: 인라인 잠금
{
    flock -x 200

    # 임계 영역
    echo "Exclusive access to resource"
    sleep 2

} 200>/tmp/mylock

7.6 FIFO를 사용한 진행 상황 표시

#!/bin/bash

# 진행 상황 파이프 생성
mkfifo /tmp/progress_$$
trap "rm -f /tmp/progress_$$" EXIT

# 진행 상황 모니터 (백그라운드)
{
    while read percent message; do
        printf "\r[%-50s] %d%% %s" \
            "$(printf '#%.0s' $(seq 1 $((percent / 2))))" \
            "$percent" \
            "$message"
    done < /tmp/progress_$$
    echo
} &
monitor_pid=$!

# 작업 프로세스
{
    total=100
    for i in $(seq 1 $total); do
        # 작업 시뮬레이션
        sleep 0.05

        # 진행 상황 보고
        percent=$((i * 100 / total))
        echo "$percent Processing item $i" > /tmp/progress_$$
    done
} &
worker_pid=$!

# 완료 대기
wait $worker_pid
wait $monitor_pid

연습 문제

문제 1: 다중 대상 로거

다음 기능을 가진 로깅 시스템 생성: - 로그 레벨 (DEBUG, INFO, WARN, ERROR)과 메시지 허용 - 모든 로그를 all.log에 쓰기 - ERROR 로그를 error.log에 쓰기 - WARN과 ERROR를 important.log에 쓰기 - ERROR와 WARN을 stderr에, 나머지는 stdout에 표시 - 각 로그 항목에 타임스탬프와 호스트명 추가 - 파일이 10MB를 초과하면 로그 로테이션 구현

문제 2: 파이프라인 모니터

다음 기능을 가진 스크립트 작성: - 다단계 파이프라인 실행 (예: download | decompress | process | upload) - PIPESTATUS를 사용하여 각 단계의 종료 상태 모니터링 - 프로세스 치환을 사용하여 파일에 진행 상황 로깅 - 실패한 단계에 대한 재시도 로직 구현 - 어느 단계가 실패했는지와 그 이유 보고 - 총 시간과 처리량 계산

문제 3: FIFO 기반 큐 시스템

명명된 파이프를 사용한 간단한 작업 큐 구현: - 큐에 작업을 보내는 job_submit 명령 생성 - 큐에서 작업을 처리하는 job_worker 생성 - 여러 동시 워커 지원 - 작업 상태 추적 구현 (pending, running, completed, failed) - 워커 충돌을 우아하게 처리 - 큐 상태를 확인하는 job_status 명령 제공

문제 4: 설정 검증기

다음 기능을 가진 도구 구축: - stdin 또는 파일 인자에서 설정 파일 읽기 - 검증 명령을 사용하여 구문 검증 - 유효하면, 원자적으로 이전 config 교체 - 유효하지 않으면, stderr에 에러 표시하고 이전 config 유지 - 교체 전 백업 생성 (마지막 5개 백업 유지) - 타임스탬프와 함께 모든 변경 사항 로깅 - dry-run 모드 지원 (교체하지 않고 검증만)

문제 5: FD를 사용한 스트림 프로세서

스트림 처리 프레임워크 생성: - FD 3, 4, 5에 3개 입력 스트림 열기 - 타임스탬프와 함께 스트림 병합 - 정규식 패턴에 기반한 필터링 - 내용에 따라 다른 파일로 출력 분할 - 통계 유지 (스트림당 처리된 줄, 일치, 에러) - 실시간 모니터링을 위해 프로세스 치환 사용 - 스트림 종료를 우아하게 처리

이전: 05_Functions_and_Libraries.md | 다음: 07_String_Processing.md

to navigate between lessons