Shell Script 정리 (2편) — 에러 처리, 문자열, 실용 패턴

2026. 5. 28. 15:43·블로그, 컴퓨터/Cheatsheets

shellscript cheatsheet

1편에서 변수, 조건문, 반복문, 함수를 다뤘습니다. 2편에서는 스크립트를 실제로 쓸 만하게 만드는 것들을 정리합니다. 에러 처리, 입력 받기, 문자열 처리, 그리고 반복적으로 등장하는 실용 패턴들입니다.


에러 처리

종료 코드

모든 명령어는 실행 후 종료 코드를 남깁니다. 0은 성공, 그 외는 실패입니다. $?로 직전 명령어의 종료 코드를 확인합니다.

ls /nonexistent
echo $?    # 0이 아닌 값 (보통 2)

ls /tmp
echo $?    # 0

에러 시 메시지 출력하고 종료

command || { echo "command 실패"; exit 1; }

# 함수로 만들어두면 편함
die() {
  echo "Error: $1" >&2    # 표준 에러로 출력
  exit "${2:-1}"
}

[ -f "$config" ] || die "설정 파일이 없습니다: $config"

>&2는 표준 에러(stderr)로 출력하는 것입니다. 에러 메시지는 stdout이 아닌 stderr에 쓰는 게 맞고, 파이프로 필터링할 때도 분리됩니다.

trap — 스크립트 종료 시 정리

스크립트가 끝나거나 중단될 때 실행할 코드를 등록합니다.

tmp_file=$(mktemp)

cleanup() {
  rm -f "$tmp_file"
  echo "정리 완료"
}

trap cleanup EXIT          # 정상 종료 시
trap cleanup INT TERM      # Ctrl+C, kill 시
trap 'cleanup; exit 1' ERR # 에러 발생 시

임시 파일을 만들고 나서 스크립트가 어떤 이유로 종료되든 삭제되도록 보장할 때 씁니다.

set 옵션 복습

set -e          # 명령어 실패 시 즉시 종료
set -u          # 미정의 변수 사용 시 에러
set -o pipefail # 파이프 중간 실패도 에러로 처리
set -x          # 실행되는 명령어 출력 (디버깅)
set +x          # 디버그 모드 끄기

set -x는 스크립트 디버깅할 때 유용합니다. 어느 줄에서 어떤 값으로 실행됐는지 보입니다.


입력 받기

스크립트 인수

#!/usr/bin/env bash

# 인수 검증
if [ "$#" -lt 2 ]; then
  echo "사용법: $0 <이름> <나이>"
  exit 1
fi

name="$1"
age="$2"
echo "$name ($age)"

사용자 입력 받기

read -p "이름을 입력하세요: " name
echo "안녕하세요, $name"

read -sp "비밀번호: " password    # -s : 입력 숨기기
echo

read -t 10 -p "10초 안에 입력: " response    # -t : 타임아웃(초)

옵션 파싱 — getopts

-f, -v 같은 플래그 형태의 인수를 처리할 때 씁니다.

verbose=false
output_file=""

while getopts "vo:h" opt; do
  case "$opt" in
    v) verbose=true ;;
    o) output_file="$OPTARG" ;;
    h)
      echo "사용법: $0 [-v] [-o 출력파일]"
      exit 0
      ;;
    ?)
      echo "알 수 없는 옵션"
      exit 1
      ;;
  esac
done

shift $((OPTIND - 1))    # 파싱된 옵션 이후 나머지 인수로 이동

getopts "vo:h"에서 : 뒤에 오는 문자(o:)는 해당 옵션이 값을 받는다는 의미입니다.


문자열 처리

bash에서 ${변수} 안에 연산자를 넣어서 문자열을 조작할 수 있습니다.

길이

str="Hello, World"
echo "${#str}"    # 12

부분 문자열

str="Hello, World"
echo "${str:7}"       # World (7번째부터 끝까지)
echo "${str:7:5}"     # World (7번째부터 5글자)
echo "${str: -5}"     # World (뒤에서 5글자)

치환

str="Hello, World"
echo "${str/World/Bash}"     # Hello, Bash (첫 번째만)
echo "${str//l/L}"           # HeLLo, WorLd (전체)

제거 — 접두사/접미사

file="backup_2024-01-15.tar.gz"

echo "${file#backup_}"        # 2024-01-15.tar.gz (짧은 접두사 제거)
echo "${file##*.}"            # gz (가장 긴 접두사 제거, 확장자만)
echo "${file%.tar.gz}"        # backup_2024-01-15 (접미사 제거)
echo "${file%%.*}"            # backup_2024-01-15 (가장 긴 접미사 제거)

#은 앞에서, %는 뒤에서 제거합니다. 두 개 쓰면 최대한 많이 제거합니다.

대소문자 변환

str="Hello World"
echo "${str,,}"    # hello world (전체 소문자)
echo "${str^^}"    # HELLO WORLD (전체 대문자)
echo "${str,}"     # hello World (첫 글자만 소문자)
echo "${str^}"     # Hello World (첫 글자만 대문자)

파일 처리

파일 읽기

# 한 줄씩 처리
while IFS= read -r line; do
  echo "줄: $line"
done < input.txt

# 빈 줄, 주석 제외
while IFS= read -r line; do
  [[ -z "$line" || "$line" == \#* ]] && continue
  echo "$line"
done < config.txt

파일 쓰기

echo "내용" > file.txt          # 덮어쓰기
echo "추가 내용" >> file.txt    # 이어쓰기

# 여러 줄 쓰기 (heredoc)
cat > file.txt << 'EOF'
첫 번째 줄
두 번째 줄
EOF

임시 파일

tmp=$(mktemp)               # 임시 파일 생성
tmp_dir=$(mktemp -d)        # 임시 디렉토리 생성
trap 'rm -rf "$tmp" "$tmp_dir"' EXIT

mktemp는 유일한 이름의 임시 파일을 만들어줍니다. /tmp/tmp.XXXXXXXX 형태입니다.


자주 쓰는 실용 패턴

스크립트 위치 기준 경로

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="$SCRIPT_DIR/config.yaml"

$0는 스크립트를 어디서 호출했냐에 따라 달라질 수 있습니다. BASH_SOURCE[0]을 쓰면 스크립트 자신의 위치를 정확히 가리킵니다.

로그 함수

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

log_info()  { echo "[INFO]  $(date '+%H:%M:%S') $*"; }
log_warn()  { echo "[WARN]  $(date '+%H:%M:%S') $*" >&2; }
log_error() { echo "[ERROR] $(date '+%H:%M:%S') $*" >&2; }

log_info "시작합니다"
log_error "연결 실패"

명령어 존재 여부 확인

command -v docker &>/dev/null || { echo "docker가 설치되지 않았습니다"; exit 1; }

# 함수로
require_command() {
  command -v "$1" &>/dev/null || die "'$1' 명령어가 필요합니다"
}

require_command docker
require_command git
require_command jq

재시도 로직

retry() {
  local max_attempts=3
  local delay=5
  local attempt=1

  while [ $attempt -le $max_attempts ]; do
    "$@" && return 0
    echo "실패 ($attempt/$max_attempts), ${delay}초 후 재시도..."
    sleep "$delay"
    ((attempt++))
  done

  echo "최대 재시도 횟수 초과"
  return 1
}

retry curl -f https://example.com/api

진행 상황 표시

spinner() {
  local pid=$1
  local delay=0.1
  local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
  local i=0

  while kill -0 "$pid" 2>/dev/null; do
    printf "\r${frames[$((i % ${#frames[@]}))]} 처리 중..."
    sleep "$delay"
    ((i++))
  done
  printf "\r✓ 완료          \n"
}

long_running_command &
spinner $!

색상 출력

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'    # No Color

echo -e "${GREEN}성공${NC}"
echo -e "${RED}에러${NC}"
echo -e "${YELLOW}경고${NC}"

확인 프롬프트

confirm() {
  read -rp "${1:-계속하시겠습니까?} [y/N] " response
  case "$response" in
    [yY][eE][sS]|[yY]) return 0 ;;
    *) return 1 ;;
  esac
}

confirm "데이터베이스를 초기화하시겠습니까?" || exit 0
echo "초기화 진행..."

실전 스크립트 예시

위에서 다룬 것들을 조합한 간단한 배포 스크립트입니다.

#!/usr/bin/env bash
set -euo pipefail

# ── 설정 ────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APP_NAME="myapp"
DEPLOY_DIR="/var/www/${APP_NAME}"
BACKUP_DIR="/var/backup/${APP_NAME}"

# ── 함수 ────────────────────────────────────────
log()   { echo "[$(date '+%H:%M:%S')] $*"; }
die()   { echo "Error: $1" >&2; exit "${2:-1}"; }

confirm() {
  read -rp "$1 [y/N] " r
  [[ "$r" =~ ^[yY] ]]
}

require_command() {
  command -v "$1" &>/dev/null || die "'$1'이 필요합니다"
}

# ── 사전 확인 ───────────────────────────────────
require_command git
require_command rsync

[ -d "$DEPLOY_DIR" ] || die "배포 경로가 없습니다: $DEPLOY_DIR"

# ── 백업 ────────────────────────────────────────
log "현재 버전 백업 중..."
timestamp=$(date +%Y%m%d_%H%M%S)
mkdir -p "$BACKUP_DIR"
rsync -a "$DEPLOY_DIR/" "$BACKUP_DIR/${timestamp}/"

# ── 배포 ────────────────────────────────────────
confirm "배포를 진행하시겠습니까?" || { log "취소됨"; exit 0; }

log "배포 시작..."
git -C "$DEPLOY_DIR" pull origin main

log "완료: $(git -C "$DEPLOY_DIR" rev-parse --short HEAD)"

정리 표

분류 문법 용도
에러 처리 cmd || die "메시지" 실패 시 종료
에러 처리 trap cleanup EXIT 종료 시 정리
에러 처리 set -x 디버그 모드
입력 read -p "프롬프트" var 사용자 입력
입력 getopts "vo:" 옵션 파싱
문자열 ${#str} 길이
문자열 ${str:offset:length} 부분 문자열
문자열 ${str/old/new} 치환
문자열 ${str#prefix} ${str%suffix} 접두사/접미사 제거
문자열 ${str,,} ${str^^} 소문자 / 대문자
파일 while IFS= read -r line 줄 단위 읽기
파일 mktemp 임시 파일 생성
패턴 BASH_SOURCE[0] 스크립트 위치
패턴 command -v cmd 명령어 존재 확인
반응형
저작자표시 비영리 변경금지 (새창열림)

'블로그, 컴퓨터 > Cheatsheets' 카테고리의 다른 글

tmux 정리 (2편) — 설정, 플러그인, 실용 커스터마이징  (0) 2026.05.29
tmux 정리 (1편) — 세션, 윈도우, 패널  (0) 2026.05.29
Shell Script 정리 (1편) — 변수, 조건문, 반복문, 함수  (0) 2026.05.28
Docker 명령어 정리 (2편) — Dockerfile과 Docker Compose  (0) 2026.05.27
Docker 명령어 정리 (1편) — 개념, 이미지, 컨테이너  (0) 2026.05.27
'블로그, 컴퓨터/Cheatsheets' 카테고리의 다른 글
  • tmux 정리 (2편) — 설정, 플러그인, 실용 커스터마이징
  • tmux 정리 (1편) — 세션, 윈도우, 패널
  • Shell Script 정리 (1편) — 변수, 조건문, 반복문, 함수
  • Docker 명령어 정리 (2편) — Dockerfile과 Docker Compose
생각사람
생각사람
지극히 사적인 연구실
  • 생각사람
    생각사람의 별장
    생각사람
  • 전체
    오늘
    어제
    • 분류 전체보기 (207)
      • 금융 (57)
        • 주식 공부 (11)
        • 파생상품 입문 (17)
        • 파생상품 기초 (15)
        • 파생상품 실전 (14)
      • 블로그, 컴퓨터 (83)
        • 프로그래밍 (16)
        • DevOps (8)
        • AI, RL, ML, ... (5)
        • 애드센스, SEO (23)
        • 임베디드 (3)
        • 컴퓨터 관련 (7)
        • Cheatsheets (21)
      • 다른 공부들 (67)
        • 읽고 쓰기 (18)
        • 수학 (15)
        • 물리 (9)
        • 사진 공부 (25)
  • 인기 글

  • 최근 글

  • 최근 댓글

  • 태그

    github
    웹크롤러
    양자역학
    스트랭글
    오펜하이머
    코딩
    AI
    소니 a6000
    GIT
    옵션
    파생상품
    옵션 투자
    구글 애드센스
    슈뢰딩거 방정식
    독후감
    c++
    프로그래밍
    선물 옵션
    cmake
    공업수학
    c
    깃
    스트래들
    version control
    Kreyszig
    CheatSheet
    행렬
    벡터
    깃허브
    선형대수학
  • hELLO· Designed By정상우.v4.10.6
생각사람
Shell Script 정리 (2편) — 에러 처리, 문자열, 실용 패턴
상단으로

티스토리툴바