Skip to content

Instantly share code, notes, and snippets.

@m-fire
Last active May 2, 2025 05:17
Show Gist options
  • Save m-fire/b060914991ccde16dbeb5b093c210c77 to your computer and use it in GitHub Desktop.
Save m-fire/b060914991ccde16dbeb5b093c210c77 to your computer and use it in GitHub Desktop.
지정된 디렉토리 하위의 파일들을 검사하여 유니코드 이스케이프 시퀀스(표준: \uXXXX, 비표준: uXXXX)에 해당하는 자연어 문자로 변환합니다.
#!/usr/bin/env bash
# shellcheck disable=SC2016,SC2086,SC2034,SC2207
# ==============================================================================
# 유니코드 이스케이프 시퀀스 변환 스크립트
#
# 설명:
# 지정된 디렉토리 하위의 파일들을 검사하여 유니코드 이스케이프 시퀀스(표준: \uXXXX, 비표준: uXXXX)에
# 해당하는 자연어 문자로 변환합니다.
#
# 사용법:
# 명령어 인자로 실행: $(basename "$0") [옵션] <대상경로>
# 인터랙티브 메뉴로 실행: $(basename "$0")
#
# 의존성:
# - GNU getopt: 긴 옵션 및 옵션 인자 처리를 위해 필요합니다.
# - perl: 유니코드 변환 로직을 위해 필요합니다.
# - find, xargs, mv, cmp, mktemp, mkdir, basename, dirname 등 표준 유틸리티
# ==============================================================================
# --- 스크립트 안전 설정 ---
# -E: ERR 트랩 상속 활성화
# -e: 명령 실패 시 즉시 종료
# -u: 정의되지 않은 변수 사용 시 오류 발생 및 종료
# -o pipefail: 파이프라인에서 명령 실패 시 마지막 실패 명령의 종료 코드를 반환
set -Eeuo pipefail
# --- 기본값 설정 ---
DRY_RUN=false # 미리보기 실행 여부 (기본값: false)
VERBOSE=false # 상세 로그 출력 여부 (기본값: false)
EXTENSIONS="" # 처리할 확장자 목록 (쉼표 구분, 기본값: 모든 파일)
RECURSIVE=true # 하위 디렉토리 재귀 탐색 활성화 여부 (기본값: true)
EXCLUDE_PATTERNS="" # 제외할 파일/디렉토리 패턴 목록 (쉼표 구분, 기본값: 없음)
BACKUP_SUFFIX=".bak" # 백업 파일 접미사 (기본값:.bak)
BACKUP_DIR="" # 백업 파일 저장 디렉토리 (기본값: 원본 파일 옆)
# 보고서 파일 경로는 제거됨. 보고서는 터미널에 직접 출력.
# REPORT_FILE=""
# 스크립트 실행 시작 시 설정되는 시스템 임시 폴더 하위의 스크립트 전용 임시 디렉토리
SCRIPT_TEMP_BASE_DIR=""
MAX_JOBS=4 # 🚀 병렬 처리할 최대 작업 수 (기본값: 4)
# --- 임시 파일 및 보고서 내용 관리 ---
# 개별 파일 변환 시 생성되는 임시 파일 목록을 저장할 배열
declare -a TEMP_FILES=()
# 보고서 내용을 임시로 저장할 파일 경로 (작업 완료 후 터미널 출력용)
REPORT_TEMP_FILE=""
# 스크립트 종료 시 정리 함수
cleanup() {
# 개별 파일 변환 시 생성된 임시 파일 정리
if [[ ${#TEMP_FILES[@]} -gt 0 ]]; then
log_verbose "[INFO] 개별 파일 임시 파일 정리 중: ${TEMP_FILES[*]}"
# rm -f 명령에 -f 옵션을 사용하여 존재하지 않는 파일에 대한 오류를 무시합니다.
rm -f "${TEMP_FILES[@]}"
fi
# 보고서 임시 파일 정리
if [[ -n "$REPORT_TEMP_FILE" && -f "$REPORT_TEMP_FILE" ]]; then
log_verbose "[INFO] 보고서 임시 파일 정리 중: $REPORT_TEMP_FILE"
rm -f "$REPORT_TEMP_FILE"
fi
# 스크립트 전용 임시 디렉토리 정리
if [[ -n "$SCRIPT_TEMP_BASE_DIR" && -d "$SCRIPT_TEMP_BASE_DIR" ]]; then
log_verbose "[INFO] 스크립트 임시 디렉토리 정리 중: $SCRIPT_TEMP_BASE_DIR"
# -r: 재귀적으로 삭제, -f: 강제 삭제 (확인 없이)
rm -rf "$SCRIPT_TEMP_BASE_DIR"
fi
}
# 종료, 인터럽트, 에러 시 cleanup 함수 호출하도록 트랩 설정
# cleanup 함수 내에서 trap을 해제하면, cleanup 실행 중 발생하는 오류에 대해서는 다시 cleanup이 호출되지 않습니다.
# 따라서 트랩 해제는 cleanup 함수 시작 시점에 두는 것이 일반적입니다.
trap cleanup SIGINT SIGTERM ERR EXIT
# --- 로깅 함수 ---
# 상세 로그 출력 함수 (--verbose 플래그 활성화 시 작동)
log_verbose() {
local message="$1"
if [[ "$VERBOSE" == true ]]; then
# 파란색으로 출력 (터미널 지원 시)
printf '\033[0;34m%s\033[0m\n' "$message" >&2
fi
}
# 처리 결과 로그 출력 함수 (--verbose 플래그 활성화 시 작동)
# 인수: $1 - 상태 ("변경", "변경 없음", "실패")
# 인수: $2 - 파일 경로
# 인수: $3 - 현재 처리 번호
# 인수: $4 - 총 파일 수
log_result() {
local status="$1"
local file="$2"
local current="$3"
local total="$4"
local log_message=""
local color_code="\033[0m" # 기본색
local emoji="" # 이모티콘 변수 추가
case "$status" in
"변경")
color_code="\033[0;32m" # 초록색
emoji="" # 변경 이모티콘
log_message="[INFO] 처리결과(%s): %s (%d/%d)"
;;
"변경 없음")
# 변경 없음은 verbose 모드에서도 기본적으로 출력하지 않음 (요구사항 반영)
# log_verbose "[INFO] 처리결과(변경 없음): $file ($current/$total)"
return # 상세 로그에서 변경 없음은 출력하지 않음
;;
"실패")
color_code="\033[0;31m" # 빨간색
emoji="" # 실패 이모티콘
log_message="[INFO] 처리결과(%s): %s (%d/%d)"
;;
*)
# 알 수 없는 상태
emoji="" # 알 수 없음 이모티콘
log_message="[INFO] 처리결과(알 수 없음 - %s): %s (%d/%d)"
;;
esac
if [[ "$VERBOSE" == true ]]; then
# 이모티콘과 함께 상태 출력
printf "${color_code}${emoji} ${log_message}\033[0m\n" "$status" "$file" "$current" "$total" >&2
fi
}
# 오류 메시지 출력 및 종료 함수
die() {
local message="$1"
# 빨간색으로 출력 (터미널 지원 시)
printf '\033[0;31m오류: %s\033[0m\n' "$message" >&2
exit 1
}
# --- 도움말 함수 ---
usage() {
cat <<EOF
사용법:
1: $(basename "$0")
2: $(basename "$0") [옵션] <대상경로>
지정된 디렉토리 하위의 파일들에서 유니코드 이스케이프 시퀀스(\\uXXXX)를 변환합니다.
대상경로 없이 실행: 인터랙티브 메뉴 모드로 실행.
옵션:
-d, --dry-run 💨 미리보기만 실행 (실제 파일 변경 없음)
-v, --verbose 🔊 상세 로그 출력 (변경/실패 파일만 출력)
-e, --ext <ext,...> 🏷️처리할 파일 확장자 지정(기본: *.* | 옵션: 쉼표 구분, 예: txt,json,yml)
-r, --recursive ⤵️ 하위 디렉토리 재귀 탐색 (기본값)
--no-recursive ⤴️ 재귀 탐색 비활성
--exclude <pat,...> 🚫 제외 디렉토리/파일 패턴(예: */dir/*,*.log)
--backup-suffix <sfx> 💾 백업 파일 확장자 지정 (기본값:.bak)
--backup-dir <dir> 📁 백업 파일을 특정 디렉토리 저장(기본: 원본의 경로)
-j, --jobs <num> 🚀 병렬 처리할 최대 작업 수 (기본값: ${MAX_JOBS})
-h, --help ❓ 이 도움말 메시지 표시
EOF
exit 0
}
# --- 파일 변환 함수 ---
# 개별 파일을 변환하는 함수
# 인수: $1 - 변환할 파일 경로
# 인수: $2 - 현재 처리 번호
# 인수: $3 - 총 파일 수
# 반환: 0 성공 (변경/변경 없음), 1 실패
convert_file() {
local target_file="$1"
local current_idx="$2"
local total_count="$3"
local tmp_file # 개별 파일 변환을 위한 임시 파일
local perl_exit_code
local backup_path=""
local backup_dir_path
local backup_filename
local result_status="변경 없음" # 기본 상태는 변경 없음
# [핵심 로직] 임시 파일 생성: 스크립트 전용 임시 디렉토리 하위에 생성
# mktemp는 기본적으로 umask를 따르며, 디렉토리가 존재하지 않으면 오류 발생
# SCRIPT_TEMP_BASE_DIR는 스크립트 시작 시점에 생성되었음을 가정
tmp_file=$(mktemp -t "$(basename "$target_file").XXXXXX" --tmpdir="$SCRIPT_TEMP_BASE_DIR")
# 임시 파일 생성 실패 시 즉시 종료 (심각한 오류)
if [[ -z "$tmp_file" || ! -e "$tmp_file" ]]; then
die "임시 파일 생성 실패: $SCRIPT_TEMP_BASE_DIR/$(basename "$target_file").XXXXXX"
fi
TEMP_FILES+=("$tmp_file") # 정리 목록에 추가
# [핵심 로직] Perl을 사용하여 유니코드 이스케이프 시퀀스 변환
# 결과를 임시 파일에 저장
# -CSDA: 표준 입출력 및 스크립트 내부 인코딩을 UTF-8로 설정
# -pe: 각 줄을 읽고 스크립트 실행 후 출력
# s/.../.../ge: 대체 (g: 모든 일치, e: 대체 부분을 Perl 코드로 실행)
# (?:u|\\u)([a-f0-9]{4}): 'u' 또는 '\u' 뒤에 4개의 16진수 문자가 오는 패턴 비캡처 그룹
# chr(hex($1)): 캡처된 16진수($1)를 10진수로 변환하여 해당 유니코드 문자 반환
if ! perl -CSDA -pe 's/(?:u|\\u)([a-f0-9]{4})/chr(hex($1))/ge;' "$target_file" > "$tmp_file"; then
# Perl 실행 실패 시: 상태 변경, 오류 메시지 출력, 임시 파일 정리, 보고서 기록 후 오류 반환 (Early Return)
result_status="실패"
echo "오류: '$target_file' 처리 중 Perl 오류 발생. 건너뜁니다." >&2
rm -f "$tmp_file" # 임시 파일 즉시 제거
# 보고서 임시 파일에 결과 기록 (실패)
if [[ -n "$REPORT_TEMP_FILE" ]]; then
echo "실패: $target_file" >> "$REPORT_TEMP_FILE"
fi
log_result "$result_status" "$target_file" "$current_idx" "$total_count"
return 1
fi
# [핵심 로직] 원본 파일 내용과 임시 파일 내용 비교
if cmp -s "$target_file" "$tmp_file"; then
# 내용 변경 없음: 상태 변경, 상세 로그 출력 안 함, 임시 파일 정리, 보고서 기록 후 성공 반환 (Early Return)
result_status="변경 없음"
log_verbose "[INFO] 내용 변경 없음, 건너뜁니다: $target_file"
rm -f "$tmp_file"
# 보고서 임시 파일에 결과 기록 (변경 없음)
if [[ -n "$REPORT_TEMP_FILE" ]]; then
echo "변경 없음: $target_file" >> "$REPORT_TEMP_FILE"
fi
return 0
fi
# 내용 변경 감지됨
result_status="변경"
# [핵심 로직] Dry run 모드인 경우: 변경 예정 메시지 출력, 상세 로그 출력, 임시 파일 정리, 보고서 기록 후 성공 반환 (Early Return)
if [[ "$DRY_RUN" == true ]]; then
echo "변환 예정: $target_file"
log_result "$result_status" "$target_file" "$current_idx" "$total_count"
rm -f "$tmp_file"
# 보고서 임시 파일에 결과 기록 (Dry Run 변경 예정)
if [[ -n "$REPORT_TEMP_FILE" ]]; then
echo "Dry Run 변경 예정: $target_file" >> "$REPORT_TEMP_FILE"
fi
return 0
fi
# [핵심 로직] Dry run 모드가 아닌 경우: 실제 파일 변경 수행
# 백업 경로 결정
if [[ -n "$BACKUP_DIR" ]]; then
backup_dir_path="$BACKUP_DIR"
if ! mkdir -p "$backup_dir_path"; then
echo "경고: 백업 디렉토리 '$backup_dir_path' 생성 실패. 백업 없이 진행합니다." >&2
backup_path="" # 백업 경로 초기화
else
backup_filename=$(basename "$target_file")
backup_path="$backup_dir_path/$backup_filename$BACKUP_SUFFIX"
fi
else
backup_path="$target_file$BACKUP_SUFFIX"
fi
# 백업 수행 (백업 경로가 유효한 경우)
if [[ -n "$backup_path" ]]; then
log_verbose "[INFO] 원본 백업 중: $target_file -> $backup_path"
if ! mv -f "$target_file" "$backup_path"; then
# 백업 실패 시: 상태 변경, 오류 메시지 출력, 임시 파일 정리, 보고서 기록 후 오류 반환 (Early Return)
echo "오류: '$target_file' 백업 실패. 파일 변경을 건너뜁니다." >&2
rm -f "$tmp_file"
result_status="실패" # 백업 또는 mv 실패 시 상태 변경
if [[ -n "$REPORT_TEMP_FILE" ]]; then
echo "실패: $target_file" >> "$REPORT_TEMP_FILE"
fi
log_result "$result_status" "$target_file" "$current_idx" "$total_count"
return 1
fi
fi # 백업 경로가 없으면 백업 없이 진행
# [핵심 로직] 임시 파일을 원본 파일 위치로 이동 (파일 교체)
log_verbose "[INFO] 파일 교체 중: $tmp_file -> $target_file"
if ! mv -f "$tmp_file" "$target_file"; then
# 파일 교체 실패 시: 오류 메시지 출력, 백업 복구 시도, 상태 변경, 보고서 기록 후 오류 반환 (Early Return)
echo "오류: 임시 파일 '$tmp_file'을(를) 원본 '$target_file'(으)로 이동 실패." >&2
# 백업 복구 시도 (선택적)
if [[ -n "$backup_path" && -f "$backup_path" ]]; then
echo "[INFO] 백업 파일 '$backup_path'에서 원본 복구 시도..." >&2
if ! mv -f "$backup_path" "$target_file"; then
echo "오류: 백업 복구 실패." >&2
fi
fi
result_status="실패"
if [[ -n "$REPORT_TEMP_FILE" ]]; then
echo "실패: $target_file" >> "$REPORT_TEMP_FILE"
fi
log_result "$result_status" "$target_file" "$current_idx" "$total_count"
return 1
fi
# 성공적으로 mv 완료 시, 임시 파일은 더 이상 존재하지 않음 (trap에서 최종 정리)
# 변경 성공 시: 보고서 기록, 상세 로그 출력 후 성공 반환 (Early Return)
if [[ -n "$REPORT_TEMP_FILE" ]]; then
echo "변경: $target_file" >> "$REPORT_TEMP_FILE"
fi
log_result "$result_status" "$target_file" "$current_idx" "$total_count"
return 0 # 성공 반환 (변경 여부와 관계없이 처리 성공)
}
# --- 인자 파싱 (GNU getopt 사용) ---
# getopt 가용성 확인 (긴 옵션 지원 확인)
if ! getopt --test > /dev/null 2>&1; then
if command -v getopt > /dev/null; then
GETOPT_CMD="getopt"
else
die "GNU getopt 를 찾을 수 없습니다. 긴 옵션 처리를 위해 필요합니다.\nMac OS에서는 'brew install gnu-getopt' 로 설치할 수 있습니다."
fi
else
GETOPT_CMD="getopt"
fi
# 옵션 정의
# report 옵션 제거
SHORT_OPTS="dve:rhj:" # j: 옵션 추가
LONG_OPTS="dry-run,verbose,ext:,recursive,no-recursive,exclude:,backup-suffix:,backup-dir:,jobs:,help" # jobs: 옵션 추가
# getopt 실행 및 인자 재구성
PARSED_ARGS=$($GETOPT_CMD --options "$SHORT_OPTS" --longoptions "$LONG_OPTS" --name "$(basename "$0")" -- "$@" 2>/dev/null)
# getopt 오류 처리
if [[ $? -ne 0 ]]; then
echo "오류: 옵션 파싱 오류. 도움말을 보려면 '-h' 또는 '--help'를 사용하세요." >&2
exit 1
fi
# 파싱된 인자를 스크립트의 위치 매개변수로 설정
eval set -- "$PARSED_ARGS"
# 옵션 처리 루프
while true; do
case "$1" in
-d | --dry-run)
DRY_RUN=true
shift
;;
-v | --verbose)
VERBOSE=true
shift
;;
-e | --ext)
EXTENSIONS="$2"
shift 2
;;
-r | --recursive)
RECURSIVE=true
shift
;;
--no-recursive)
RECURSIVE=false
shift
;;
--exclude)
EXCLUDE_PATTERNS="$2"
shift 2
;;
--backup-suffix)
BACKUP_SUFFIX="$2"
BACKUP_SUFFIX="${BACKUP_SUFFIX%/}" # 마지막 '/' 제거
shift 2
;;
--backup-dir)
BACKUP_DIR="$2"
BACKUP_DIR="${BACKUP_DIR%/}" # 마지막 '/' 제거
shift 2
;;
# report 옵션 처리 제거
-h | --help)
usage
;;
-j | --jobs) # jobs 옵션 처리
if [[ "$2" =~ ^[0-9]+$ ]] && (( "$2" > 0 )); then
MAX_JOBS="$2"
else
die "유효하지 않은 --jobs 값: '$2'. 1 이상의 정수를 입력하세요."
fi
shift 2
;;
--)
shift
break
;;
*)
die "내부 오류: 알 수 없는 옵션 '$1'"
;;
esac
done
# --- 대상 경로 처리 및 인터랙티브 모드 ---
# 위치 매개변수($1)에 대상 경로가 있는지 확인
if [[ $# -eq 1 ]]; then
TARGET_DIR="$1"
TARGET_DIR="${TARGET_DIR%/}" # 마지막 '/' 제거
elif [[ $# -eq 0 ]]; then
# 대상 경로가 없으면 인터랙티브 모드 진입
INTERACTIVE_MODE=true
echo "=== 유니코드 변환 스크립트 (인터랙티브 모드) ==="
echo ""
# 1단계: 대상 디렉토리 입력
while true; do
read -rp "1단계: 변환할 대상 디렉토리 경로를 입력하세요: " input_dir
if [[ -z "$input_dir" ]]; then
echo "오류: 대상 경로는 비워둘 수 없습니다."
elif [[ ! -d "$input_dir" ]]; then
echo "오류: '$input_dir'는 유효한 디렉토리가 아닙니다."
else
TARGET_DIR="${input_dir%/}"
break
fi
done
echo ""
# 2단계: 미리보기(Dry Run) 여부
read -rp "2단계: 미리보기(Dry Run)만 실행하시겠습니까? (y/N, 기본값: N): " yn_dry_run
case "$yn_dry_run" in
[Yy]*) DRY_RUN=true ;;
*) DRY_RUN=false ;;
esac
echo ""
# 3단계: 상세 로그 출력 여부
read -rp "3단계: 상세 로그를 출력하시겠습니까? (y/N, 기본값: N): " yn_verbose
case "$yn_verbose" in
[Yy]*) VERBOSE=true ;;
*) VERBOSE=false ;;
esac
echo ""
# 4단계: 확장자 지정
read -rp "4단계: 처리할 파일 확장자를 입력하세요 (쉼표 구분, 비워두면 모든 파일, 예: txt,json,md): " input_ext
EXTENSIONS="$input_ext"
echo ""
# 5단계: 재귀 탐색 여부
read -rp "5단계: 하위 디렉토리를 포함하여 탐색하시겠습니까? (Y/n, 기본값: Y): " yn_recursive
case "$yn_recursive" in
[Nn]*) RECURSIVE=false ;;
*) RECURSIVE=true ;;
esac
echo ""
# 6단계: 제외 패턴 지정
read -rp "6단계: 제외할 파일/디렉토리 패턴을 입력하세요 (쉼표 구분, 비워두면 없음, 예: */temp/*,*.log): " input_exclude
EXCLUDE_PATTERNS="$input_exclude"
echo ""
# 7단계: 백업 접미사
read -rp "7단계: 백업 파일 접미사를 입력하세요 (기본값: $BACKUP_SUFFIX): " input_suffix
BACKUP_SUFFIX="${input_suffix:-$BACKUP_SUFFIX}"
echo ""
# 8단계: 백업 디렉토리
read -rp "8단계: 백업 파일을 저장할 디렉토리를 입력하세요 (비워두면 원본 옆): " input_backup_dir
if [[ -n "$input_backup_dir" ]]; then
BACKUP_DIR="${input_backup_dir%/}"
else
BACKUP_DIR=""
fi
echo ""
# 9단계: 병렬 처리 작업 수 설정
input_jobs=""
while true; do
read -rp "9단계: 병렬 처리할 최대 작업 수를 입력하세요 (기본값: $MAX_JOBS): " input_jobs
if [[ -z "$input_jobs" ]]; then
# 입력 없으면 기본값 사용
break
elif [[ "$input_jobs" =~ ^[0-9]+$ ]] && (( "$input_jobs" > 0 )); then
MAX_JOBS="$input_jobs"
break # 유효한 입력 시 루프 종료
else
echo "오류: 유효하지 않은 값입니다. 1 이상의 정수를 입력하세요."
fi
done
echo ""
echo "---------------------------------------------"
echo "설정이 완료되었습니다. 다음 설정으로 실행합니다:"
printf " 대상 경로: %s\n" "$TARGET_DIR"
printf " Dry Run: %s\n" "$( [[ "$DRY_RUN" == true ]] && echo "" || echo "아니오" )"
printf " Verbose: %s\n" "$( [[ "$VERBOSE" == true ]] && echo "" || echo "아니오" )"
printf " 확장자: %s\n" "${EXTENSIONS:-모든 파일}"
printf " 재귀 탐색: %s\n" "$( [[ "$RECURSIVE" == true ]] && echo "" || echo "아니오" )"
printf " 제외 패턴: %s\n" "${EXCLUDE_PATTERNS:-없음}"
printf " 백업 접미사: %s\n" "$BACKUP_SUFFIX"
printf " 백업 디렉토리: %s\n" "${BACKUP_DIR:-원본 파일 옆}"
printf " 병렬 작업 수: %s\n" "$MAX_JOBS"
echo "---------------------------------------------"
read -rp "위 설정대로 변환 작업을 시작하시겠습니까? (Y/n): " confirm
case "$confirm" in
[Nn]*) echo "작업을 취소했습니다."; exit 0 ;;
*) echo "변환 작업을 시작합니다..." ;;
esac
elif [[ $# -gt 1 ]]; then
die "인자가 너무 많습니다. 대상 경로는 하나만 지정해야 합니다."
fi
# --- 최종 유효성 검사 ---
if [[ -z "$TARGET_DIR" ]]; then
die "대상 경로가 지정되지 않았습니다."
fi
if [[ ! -d "$TARGET_DIR" ]]; then
die "대상 경로 '$TARGET_DIR'는 유효한 디렉토리가 아닙니다."
fi
# [핵심 로직] 스크립트 전용 임시 디렉토리 생성
# 시스템 임시 폴더 하위의 script_temp/스크립트명_초단위날짜 디렉토리 생성
SCRIPT_TEMP_BASE_DIR=$(mktemp -d -t "script_temp_$(basename "$0")_$(date +%s).XXXXXX")
if [[ -z "$SCRIPT_TEMP_BASE_DIR" || ! -d "$SCRIPT_TEMP_BASE_DIR" ]]; then
die "스크립트 임시 디렉토리 생성 실패."
fi
log_verbose "[INFO] 스크립트 임시 디렉토리: $SCRIPT_TEMP_BASE_DIR"
# [핵심 로직] 보고서 임시 파일 생성 (보고서는 이제 무조건 생성되어 터미널 출력됨)
REPORT_TEMP_FILE=$(mktemp -t "report_temp.XXXXXX" --tmpdir="$SCRIPT_TEMP_BASE_DIR")
if [[ -z "$REPORT_TEMP_FILE" || ! -e "$REPORT_TEMP_FILE" ]]; then
# 보고서 임시 파일 생성 실패는 경고만 하고 보고서 출력 없이 진행
echo "경고: 보고서 임시 파일 생성 실패. 작업 완료 후 보고서가 출력되지 않습니다." >&2
REPORT_TEMP_FILE="" # 임시 파일 경로 초기화
fi
# 보고서 임시 파일이 성공적으로 생성되었으면 시작 메시지 기록
if [[ -n "$REPORT_TEMP_FILE" ]]; then
echo "=== 유니코드 변환 보고서 ===" >> "$REPORT_TEMP_FILE"
echo "작업 시작 시간: $(date '+%Y-%m-%d %H:%M:%S')" >> "$REPORT_TEMP_FILE"
echo "---------------------------------------------" >> "$REPORT_TEMP_FILE"
log_verbose "[INFO] 보고서 임시 파일: $REPORT_TEMP_FILE"
fi
# --- 파일 검색 ---
log_verbose "[INFO] 대상 디렉토리 스캔 시작: $TARGET_DIR"
# find 명령어 옵션 및 액션을 저장할 배열
declare -a find_cmd=("find" "$TARGET_DIR")
# 재귀 옵션 처리
if [[ "$RECURSIVE" == false ]]; then
find_cmd+=("-maxdepth" "1")
fi
# 제외 패턴 처리 (-path... -prune -o...) - 다른 조건보다 먼저 적용
if [[ -n "$EXCLUDE_PATTERNS" ]]; then
IFS=',' read -ra patterns <<< "$EXCLUDE_PATTERNS"
first_exclude=true
find_cmd+=("(") # 제외 패턴 그룹 시작
for pattern in "${patterns[@]}"; do
cleaned_pattern=$(echo "$pattern" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
if [[ -n "$cleaned_pattern" ]]; then
if [[ "$first_exclude" == false ]]; then
find_cmd+=("-o") # OR 연산자
fi
# -path 패턴은 find의 시작 경로(TARGET_DIR)를 기준으로 매칭됩니다.
find_cmd+=("-path" "$TARGET_DIR/$cleaned_pattern" "-prune")
first_exclude=false
fi
done
find_cmd+=(")") # 제외 패턴 그룹 닫기
find_cmd+=("-o") # 마지막 제외 패턴 뒤에 OR 추가하여 다음 조건과 연결
fi
# 확장자 필터링 처리 (-name '*.ext1' -o -name '*.ext2'...)
if [[ -n "$EXTENSIONS" ]]; then
IFS=',' read -ra exts <<< "$EXTENSIONS"
find_cmd+=("(") # 확장자 그룹 시작 괄호
first_ext=true
for ext in "${exts[@]}"; do
cleaned_ext=$(echo "$ext" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e 's/^\.//') # 앞 '.' 제거
if [[ -n "$cleaned_ext" ]]; then
if [[ "$first_ext" == false ]]; then
find_cmd+=("-o") # OR 연산자
fi
find_cmd+=("-name" "*.$cleaned_ext")
first_ext=false
fi
done
find_cmd+=(")") # 확장자 그룹 닫는 괄호
fi
# 기본 대상: 일반 파일만 (-type f)
find_cmd+=("-type" "f")
# 최종 액션: null 문자로 구분하여 출력 (-print0)
find_cmd+=("-print0")
# [핵심 로직] 파일 목록 수집: find 명령 실행 결과를 배열에 저장
declare -a target_files=()
# find의 출력을 안전하게 배열로 읽어옴 (null 문자로 구분)
while IFS= read -r -d $'\0'; do
target_files+=("$REPLY")
done < <("${find_cmd[@]}") # find 명령어를 서브쉘에서 실행하고 파이프 연결
total_files=${#target_files[@]}
log_verbose "[INFO] 총 ${total_files}개 파일 발견."
# --- 파일 변환 실행 (수집된 목록 기반, 병렬 처리) ---
running_jobs=0 # 실행 중인 백그라운드 작업 수 카운터
log_verbose "[INFO] 파일 목록 기반 병렬 처리 시작 (최대 ${MAX_JOBS}개 작업)."
# [핵심 로직] 수집된 파일 목록 순회하며 개별 파일 병렬 처리
for (( i=0; i<total_files; i++ )); do
file="${target_files[i]}"
current_idx=$((i + 1))
# convert_file 함수를 백그라운드로 실행
# 현재 인덱스와 총 파일 수를 전달하여 log_result에서 사용
convert_file "$file" "$current_idx" "$total_files" &
# 백그라운드 작업 수 증가
running_jobs=$((running_jobs + 1))
# 최대 작업 수에 도달하면 대기
if [[ "$running_jobs" -ge "$MAX_JOBS" ]]; then
log_verbose "[INFO] 최대 병렬 작업 수(${MAX_JOBS}) 도달, 작업 완료 대기 중..."
wait -n # 임의의 백그라운드 작업 하나가 완료될 때까지 대기
running_jobs=$((running_jobs - 1)) # 완료된 작업 수 감소
log_verbose "[INFO] 작업 하나 완료됨, 계속 진행."
fi
done
# 모든 백그라운드 작업 완료 대기
if [[ "$running_jobs" -gt 0 ]]; then
log_verbose "[INFO] 남은 모든 병렬 작업 완료 대기 중..."
wait # 모든 백그라운드 작업이 완료될 때까지 대기
log_verbose "[INFO] 모든 병렬 작업 완료."
fi
# [CHANGE] error_count 변수 제거됨. 최종 결과 요약에서 보고서 파일을 파싱하여 카운트.
# local error_count=0
# [핵심 로직] 보고서 내용 터미널 출력
if [[ -n "$REPORT_TEMP_FILE" && -f "$REPORT_TEMP_FILE" ]]; then
echo "" # 결과 요약 전에 빈 줄 추가
echo "---------------------------------------------"
echo "=== 변환 결과 보고서 ==="
# 임시 보고서 파일 내용을 터미널에 출력
cat "$REPORT_TEMP_FILE"
echo "---------------------------------------------"
# 임시 보고서 파일은 trap에서 정리됨
fi
# --- 최종 결과 요약 ---
echo "========================================="
echo "유니코드 변환 작업 완료"
echo "========================================="
# [CHANGE] 보고서 파일을 파싱하여 결과 카운트
changed_count=0
unchanged_count=0
failed_count=0
dry_run_changed_count=0
if [[ -n "$REPORT_TEMP_FILE" && -f "$REPORT_TEMP_FILE" ]]; then
# 보고서 임시 파일 내용을 읽어서 각 상태별 라인 수를 카운트
changed_count=$(grep -c "^변경: " "$REPORT_TEMP_FILE" || true) # grep 실패 시 오류 방지
unchanged_count=$(grep -c "^변경 없음: " "$REPORT_TEMP_FILE" || true)
failed_count=$(grep -c "^실패: " "$REPORT_TEMP_FILE" || true)
dry_run_changed_count=$(grep -c "^Dry Run 변경 예정: " "$REPORT_TEMP_FILE" || true)
# Dry Run 모드인 경우 '변경 예정' 카운트를 '변경' 카운트에 합산하여 보여주는 것이 더 직관적일 수 있음
if [[ "$DRY_RUN" == true ]]; then
changed_count=$dry_run_changed_count
# Dry Run 모드에서는 실제 '변경' 또는 '실패'는 발생하지 않으므로 해당 카운트는 0
failed_count=0
fi
fi
echo "검색된 총 파일 수: $total_files" # 변수명 변경 (processed_count -> total_files)
echo "실제 변경된 파일 수: $changed_count" # 변경된 파일 수 출력
echo "내용 변경 없는 파일 수: $unchanged_count" # 변경 없는 파일 수 출력
echo "처리 중 오류 발생 파일 수: $failed_count" # 실패 파일 수 출력
if [[ "$DRY_RUN" == true ]]; then
echo "💨 Dry Run 모드로 실행되어 실제 파일 변경은 이루어지지 않았습니다."
fi
echo "========================================="
# 오류 발생 시 0이 아닌 종료 코드 반환 (실패한 파일이 있는 경우)
if [[ "$failed_count" -gt 0 ]]; then
exit 1
else
exit 0
fi
#!/usr/bin/env bash
# test_convert_all_unicode.sh (v14 - 예상 결과 수정 및 비교 로직 개선)
#
# convert_all_unicode.sh 스크립트의 기능 및 옵션을 검증합니다.
#
# --- 스크립트 안전 설정 ---
set -Eeuo pipefail # -E: ERR 트랩 상속, -e: 명령 실패 시 즉시 종료, -u: 정의되지 않은 변수 사용 시 오류, -o pipefail: 파이프라인 오류 전파
# 실패 세부정보 및 카운터
declare -a failures=()
fail_count=0
pass_count=0
# 임시 테스트 디렉토리 생성 및 정리 함수
# 스크립트 실행 위치와 관계없이 임시 디렉토리 생성 보장
# mktemp -d는 디렉토리를 생성하고 이름을 반환합니다.
TEST_DIR=$(mktemp -d -t convert_unicode_test.XXXXXX)
if [[ ! -d "$TEST_DIR" ]]; then
echo "오류: 임시 테스트 디렉토리 생성 실패." >&2
exit 1
fi
echo "테스트용 임시 디렉토리: $TEST_DIR"
# 스크립트 종료 시 임시 디렉토리 자동 삭제 설정
# shellcheck disable=SC2064 # TEST_DIR 변수는 trap 정의 시점에 확장되어야 함
trap "rm -rf '$TEST_DIR'" EXIT
# 변환 스크립트 경로 (테스트 스크립트 기준 상대 경로)
# 스크립트가 있는 디렉토리를 기준으로 경로 설정
# dirname -- "${BASH_SOURCE[0]}" 은 스크립트 파일 자체의 디렉토리를 정확히 찾습니다.
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
CONVERT_SCRIPT="$SCRIPT_DIR/convert_all_unicode.sh"
# 변환 스크립트 존재 확인
if [[ ! -f "$CONVERT_SCRIPT" ]]; then
echo "오류: $CONVERT_SCRIPT 파일을 찾을 수 없습니다." >&2
exit 1
fi
# 스크립트에 실행 권한 부여 (필요시)
chmod +x "$CONVERT_SCRIPT"
# --- Helper Functions ---
# 테스트 결과 기록 함수
record_result() {
local test_name="$1"
local condition_result="$2" # true 또는 false 문자열
local message="$3"
# bash의 조건식 사용
if [[ "$condition_result" == true ]]; then
echo "✅ PASS: $test_name"
pass_count=$((pass_count + 1))
else
local error_msg="❌ FAIL: $test_name"
echo "$error_msg"
if [[ -n "$message" ]]; then
echo " $message"
fi
failures+=("$error_msg${message:+\n $message}") # 메시지가 있으면 실패 목록에 추가
fail_count=$((fail_count + 1))
fi
}
# 파일 내용 검증 함수 (줄바꿈 문자 정규화 추가)
check_file_content() {
local file="$1"
local expected="$2"
local test_name="파일 내용 검증 ($file)"
# 줄바꿈 문자 정규화 함수 (CRLF -> LF, CR -> LF)
normalize_newlines() {
# sed를 사용하여 CRLF(\r\n)를 LF(\n)로, CR(\r)을 LF(\n)로 변환
# 마지막 줄에 개행 문자가 없을 경우 추가될 수 있으므로 주의 필요 시 다른 방식 고려
# gnu sed와 bsd sed 호환성을 위해 -e 옵션 분리
sed -e 's/\r$//g' | sed -e 's/\r/\n/g'
}
# 파일 존재 여부 먼저 확인
if [[ ! -e "$file" ]]; then
# 파일이 존재하지 않을 때, 기대값도 비어있으면 PASS
if [[ -z "$expected" ]]; then
record_result "$test_name" true "파일이 예상대로 존재하지 않습니다."
else
record_result "$test_name" false "파일이 존재하지 않습니다. 예상 내용: [$expected]"
fi
return
fi
# 파일이 존재할 때, 일반 파일인지 확인
if [[ -f "$file" ]]; then
local content
# 파일 내용을 읽고 줄바꿈 정규화
# cat "$file" 대신 < "$file" 리다이렉션을 사용하여 불필요한 파이프 생성 방지
content=$(<"$file" normalize_newlines)
# 예상 내용도 줄바꿈 정규화 (printf는 LF를 사용하므로 큰 변화 없을 수 있음)
local normalized_expected
normalized_expected=$(printf '%s' "$expected" | normalize_newlines)
local condition=false
# 정규화된 내용 비교
if [[ "$content" = "$normalized_expected" ]]; then
condition=true
fi
# 실패 시 상세 메시지 보강
if [[ "$condition" == false ]]; then
# 정규화된 값으로 길이 비교 및 출력
local expected_len=${#normalized_expected}
local actual_len=${#content}
local diff_info="길이(예상:$expected_len, 실제:$actual_len)"
# 실패 시 원본(정규화 전) 값과 예상/실제(정규화 후) 값 함께 표시
local original_content
original_content=$(<"$file") # 원본 내용 다시 읽기
record_result "$test_name" "$condition" "예상(정규화): [$normalized_expected]\n실제(정규화): [$content] ($diff_info)\n--- 원본 내용 ---\n[$original_content]\n--- 예상 내용 ---\n[$expected]"
else
record_result "$test_name" "$condition" "" # 성공 시 메시지 없음
fi
else
record_result "$test_name" false "경로가 일반 파일이 아닙니다 ($file)."
fi
}
# 파일 존재 여부 검증 함수
check_file_exists() {
local file="$1"
local should_exist="$2" # true or false 문자열
local test_name="파일 존재 검증 ($file, 존재해야 함: $should_exist)"
local condition_met=false
if [[ "$should_exist" == true ]]; then
# 파일이 존재해야 하는 경우
if [[ -e "$file" ]]; then
condition_met=true
fi
else
# 파일이 존재하지 않아야 하는 경우
if [[ ! -e "$file" ]]; then
condition_met=true
fi
fi
record_result "$test_name" "$condition_met" "파일 존재 상태가 예상과 다릅니다."
}
# --- 테스트 환경 설정 ---
setup_test_files() {
echo "테스트 파일 설정..."
# 임시 디렉토리 내용 비우기 (안전하게)
# -mindepth 1: TEST_DIR 자체는 삭제하지 않음
# -delete: 찾은 파일/디렉토리 삭제
find "$TEST_DIR" -mindepth 1 -delete
# 기본 테스트 파일들 (printf '%s' 사용)
# printf는 이스케이프 시퀀스를 해석하지 않고 문자열 그대로 출력합니다.
# \uXXXX 패턴이 파일 내용에 그대로 들어가야 하므로 printf가 적합합니다.
printf '%s' 'This is a test: \u0041 is A, \u0062 is b.' > "$TEST_DIR/file1.txt"
printf '%s' "This file has no unicode pattern." > "$TEST_DIR/file2.txt"
printf '%s' "This contains A already: A should remain A." > "$TEST_DIR/file3.md"
touch "$TEST_DIR/file4.log"
# Perl 패턴 's/(?:u|\\u)([a-f0-9]{4})/chr(hex($1))/ge;' 에 따라 u0068과 \u0068 모두 변환됨
printf '%s' 'Mix: u0068ello, world! And already: \u0068 ello.' > "$TEST_DIR/file5.txt"
# 재귀 및 제외 테스트용
mkdir -p "$TEST_DIR/subdir"
printf '%s' 'Subdir test: \u0043 is C.' > "$TEST_DIR/subdir/subfile1.txt"
mkdir -p "$TEST_DIR/exclude_dir"
printf '%s' 'Exclude this: \u0044 is D.' > "$TEST_DIR/exclude_dir/excludefile.txt"
printf '%s' 'Keep this: \u0045 is E.' > "$TEST_DIR/keep.dat"
# 백업 디렉토리 테스트용
mkdir -p "$TEST_DIR/backup_test_area"
printf '%s' 'Backup test: \u0046 is F.' > "$TEST_DIR/backup_test_area/backup_target.txt"
# 특수 문자 파일 이름 테스트용 (printf 사용)
local special_file="$TEST_DIR/file with spaces 'and' \"quotes\" \$var *(chars).txt"
local content_special='Special chars test: \u0053 is S.'
printf '%s' "$content_special" > "$special_file"
# Non-BMP 테스트용 (printf 사용)
local target_file_emoji="$TEST_DIR/emoji_test.txt"
# 😀 (U+1F600) - Non-BMP 문자. \u{1F600} 또는 다른 형식일 수 있으나, 현재 스크립트는 \uXXXX 만 처리.
# u1F600 형태는 변환되지 않아야 함.
local content_emoji='Emoji test: u1F600 is 😀. Normal: \u0041 is A.'
printf '%s' "$content_emoji" > "$target_file_emoji"
# uc 패턴 테스트용 (printf 사용 - LF 사용)
local target_file_uc="$TEST_DIR/uc_pattern_test.yml"
# 스크립트가 uc 패턴을 변환하지 않도록 수정되었으므로, 이 내용은 그대로 유지되어야 함.
local line1='# PR ucf54uba58ud2b8 uae30ub2a5uc744 uc704ud55c uad8cud55c ucd94uac00'
local line2='# Normal: \u0041, uc41. uc4142 -> AB. uc414 -> A(4는 무시). uc414243 -> ABC.'
local line3='# Invalid: uc123, ucXYZ. Not hex: ucXXXX.'
printf '%s\n%s\n%s' "$line1" "$line2" "$line3" > "$target_file_uc"
echo "테스트 파일 설정 완료."
}
# --- 테스트 실행 ---
# 1. 기본 변환 테스트 (인자 전달 방식)
run_basic_conversion_test() {
echo "--- 기본 변환 테스트 시작 ---"
setup_test_files
# convert_all_unicode.sh 실행 (대상 디렉토리 인자로 전달)
"$CONVERT_SCRIPT" "$TEST_DIR"
# 파일 내용 및 백업 파일 존재 여부 확인
check_file_content "$TEST_DIR/file1.txt" "This is a test: A is A, b is b."
check_file_exists "$TEST_DIR/file1.txt.bak" true # 변경 발생했으므로 백업 존재
check_file_content "$TEST_DIR/file2.txt" "This file has no unicode pattern."
check_file_exists "$TEST_DIR/file2.txt.bak" false # 변경 없으므로 백업 없음
# file3.md는 \u 패턴이 없으므로 변경되지 않아야 함
check_file_content "$TEST_DIR/file3.md" "This contains A already: A should remain A."
check_file_exists "$TEST_DIR/file3.md.bak" false # 변경 없으므로 백업 없음
check_file_content "$TEST_DIR/file4.log" "" # 빈 파일
check_file_exists "$TEST_DIR/file4.log.bak" false # 변경 없으므로 백업 없음
# file5.txt 예상 결과 수정됨: u0068ello 는 변환 안됨, \u0068 는 h로 변환됨
check_file_content "$TEST_DIR/file5.txt" "Mix: hello, world! And already: h ello."
check_file_exists "$TEST_DIR/file5.txt.bak" true # \u0068 변환으로 인해 변경 발생
# 하위 디렉토리 파일 변환 확인 (재귀 기본값 true)
check_file_content "$TEST_DIR/subdir/subfile1.txt" "Subdir test: C is C."
check_file_exists "$TEST_DIR/subdir/subfile1.txt.bak" true # 변경 발생했으므로 백업 존재
# 제외 디렉토리 내 파일 변환 확인 (제외 옵션 사용 안 했으므로 변환됨)
check_file_content "$TEST_DIR/exclude_dir/excludefile.txt" "Exclude this: D is D."
check_file_exists "$TEST_DIR/exclude_dir/excludefile.txt.bak" true # 변경 발생했으므로 백업 존재
# .dat 파일 변환 확인 (확장자 필터링 사용 안 했으므로 변환됨)
check_file_content "$TEST_DIR/keep.dat" "Keep this: E is E."
check_file_exists "$TEST_DIR/keep.dat.bak" true # 변경 발생했으므로 백업 존재
echo "--- 기본 변환 테스트 종료 ---"
}
# 2. Dry-run 테스트
run_dry_run_test() {
echo "--- Dry-run 테스트 시작 ---"
setup_test_files
# 원본 파일 내용 미리 읽어두기 (Dry-run 이므로 변경 없어야 함)
local original_content_f1
original_content_f1=$(cat "$TEST_DIR/file1.txt")
local original_content_f3
original_content_f3=$(cat "$TEST_DIR/file3.md")
local original_content_sub1
original_content_sub1=$(cat "$TEST_DIR/subdir/subfile1.txt")
# 표준 출력과 표준 에러 모두 캡처
# 2>&1: 표준 에러를 표준 출력으로 리다이렉션
output=$("$CONVERT_SCRIPT" --dry-run "$TEST_DIR" 2>&1)
local exit_code=$?
# Dry-run 시 종료 코드는 0이어야 함 (오류가 발생하지 않았다는 가정 하에)
record_result "Dry-run 종료 코드 확인" "$([[ $exit_code -eq 0 ]] && echo true || echo false)" "Dry-run 시 종료 코드가 0이 아닙니다 (코드: $exit_code)."
# 원본 파일 내용 변경 없는지 확인 (Dry-run 이므로)
check_file_content "$TEST_DIR/file1.txt" "$original_content_f1" # 변경 없어야 함
check_file_content "$TEST_DIR/subdir/subfile1.txt" "$original_content_sub1" # 변경 없어야 함
check_file_content "$TEST_DIR/file3.md" "$original_content_f3" # 변경 없어야 함
# 백업 파일 생성 안 됐는지 확인
check_file_exists "$TEST_DIR/file1.txt.bak" false
check_file_exists "$TEST_DIR/subdir/subfile1.txt.bak" false
# file1.txt는 변경 대상이므로 Dry-run 메시지 출력 확인 (절대 경로 확인)
local condition1=false
# Dry-run 메시지 형식 확인 (스크립트 v38 기준) - "변환 예정: /path/to/file" 형태
local expected_msg1="변환 예정: ${TEST_DIR}/file1.txt"
if [[ "$output" == *"$expected_msg1"* ]]; then condition1=true; fi
record_result "Dry-run 출력 확인 (file1.txt)" "$condition1" "Dry-run 메시지 '$expected_msg1'가 출력되지 않았습니다."
# subfile1.txt도 변경 대상이므로 Dry-run 메시지 출력 확인 (절대 경로 확인)
local condition2=false
local expected_msg2="변환 예정: ${TEST_DIR}/subdir/subfile1.txt"
if [[ "$output" == *"$expected_msg2"* ]]; then condition2=true; fi
record_result "Dry-run 출력 확인 (subfile1.txt)" "$condition2" "Dry-run 메시지 '$expected_msg2'가 출력되지 않았습니다."
# file3.md는 변경 대상이 아니므로 Dry-run 메시지가 출력되지 않아야 함
local condition3=true
local unexpected_msg3="변환 예정: ${TEST_DIR}/file3.md"
if [[ "$output" == *"$unexpected_msg3"* ]]; then
condition3=false
fi
record_result "Dry-run 출력 미출력 확인 (file3.md)" "$condition3" "변경 없는 파일(file3.md)에 대한 Dry-run 메시지 '$unexpected_msg3'가 출력되었습니다."
echo "--- Dry-run 테스트 종료 ---"
}
# 3. 확장자 필터링 테스트 (--ext)
run_extension_filter_test() {
echo "--- 확장자 필터링 테스트 시작 ---"
setup_test_files
local original_md_content=$(cat "$TEST_DIR/file3.md")
local original_log_content="" # 빈 파일 내용
# .txt 확장자만 처리하도록 실행
"$CONVERT_SCRIPT" --ext txt "$TEST_DIR"
# .txt 파일 변환 확인
check_file_content "$TEST_DIR/file1.txt" "This is a test: A is A, b is b."
check_file_exists "$TEST_DIR/file1.txt.bak" true
# .md 파일은 처리 안 됨 (확장자 필터링)
check_file_content "$TEST_DIR/file3.md" "$original_md_content"
check_file_exists "$TEST_DIR/file3.md.bak" false
# .log 파일은 처리 안 됨 (확장자 필터링)
check_file_content "$TEST_DIR/file4.log" "$original_log_content"
check_file_exists "$TEST_DIR/file4.log.bak" false
# 하위 디렉토리의 .txt 파일은 처리됨 (재귀 기본값 true)
check_file_content "$TEST_DIR/subdir/subfile1.txt" "Subdir test: C is C."
check_file_exists "$TEST_DIR/subdir/subfile1.txt.bak" true
echo "--- 확장자 필터링 테스트 종료 ---"
}
# 4. 백업 옵션 테스트 (--backup-suffix, --backup-dir)
run_backup_options_test() {
echo "--- 백업 옵션 테스트 시작 ---"
setup_test_files
local backup_subdir="$TEST_DIR/my_backups"
# 백업 디렉토리 미리 생성하지 않음 (스크립트가 생성해야 함)
# backup_test_area 디렉토리 내의 파일만 대상으로 지정하고 백업 옵션 적용
"$CONVERT_SCRIPT" --backup-suffix.myorig --backup-dir "$backup_subdir" "$TEST_DIR/backup_test_area"
# 백업 디렉토리 생성 확인
check_file_exists "$backup_subdir" true
# 변환된 파일 내용 확인
check_file_content "$TEST_DIR/backup_test_area/backup_target.txt" "Backup test: F is F."
# 원본 위치에 기본 백업(.bak) 없음 확인
check_file_exists "$TEST_DIR/backup_test_area/backup_target.txt.bak" false
# 지정된 백업 디렉토리에 지정된 접미사로 백업 파일 존재 확인
# basename 으로 파일 이름만 추출하여 백업 경로 조합
local backup_file_name=$(basename "$TEST_DIR/backup_test_area/backup_target.txt")
check_file_exists "$backup_subdir/${backup_file_name}.myorig" true
echo "--- 백업 옵션 테스트 종료 ---"
}
# 5. 재귀 비활성화 테스트 (--no-recursive)
run_no_recursive_test() {
echo "--- 재귀 비활성화 테스트 시작 ---"
setup_test_files
local original_subfile_content=$(cat "$TEST_DIR/subdir/subfile1.txt")
# 재귀 비활성화 옵션 적용
"$CONVERT_SCRIPT" --no-recursive "$TEST_DIR"
# 최상위 파일들은 변환됨
check_file_content "$TEST_DIR/file1.txt" "This is a test: A is A, b is b."
check_file_exists "$TEST_DIR/file1.txt.bak" true
# 하위 디렉토리 파일은 변환 안 됨 (재귀 비활성화)
check_file_content "$TEST_DIR/subdir/subfile1.txt" "$original_subfile_content"
check_file_exists "$TEST_DIR/subdir/subfile1.txt.bak" false
echo "--- 재귀 비활성화 테스트 종료 ---"
}
# 6. 제외 옵션 테스트 (--exclude)
run_exclude_test() {
echo "--- 제외 옵션 테스트 시작 ---"
setup_test_files
# 원본 파일 내용 미리 읽어두기 (제외되었으므로 변경 없어야 함)
local original_exclude_content='Exclude this: \u0044 is D.' # printf 로 생성됨
local original_keep_content='Keep this: \u0045 is E.' # printf 로 생성됨
# 제외 패턴은 대상 디렉토리 기준 상대 경로로 전달
# exclude_dir/* 디렉토리 전체와 keep.dat 파일 제외
"$CONVERT_SCRIPT" --exclude "exclude_dir/*,keep.dat" "$TEST_DIR"
# 제외되지 않은 파일 변환 확인
check_file_content "$TEST_DIR/file1.txt" "This is a test: A is A, b is b."
check_file_exists "$TEST_DIR/file1.txt.bak" true
# 제외된 디렉토리 내 파일 미변환 확인
check_file_content "$TEST_DIR/exclude_dir/excludefile.txt" "$original_exclude_content"
# 제외되었으므로 백업 없어야 함
check_file_exists "$TEST_DIR/exclude_dir/excludefile.txt.bak" false
# 제외된 파일 미변환 확인
check_file_content "$TEST_DIR/keep.dat" "$original_keep_content"
# 제외되었으므로 백업 없어야 함
check_file_exists "$TEST_DIR/keep.dat.bak" false
echo "--- 제외 옵션 테스트 종료 ---"
}
# 7. 심볼릭 링크 테스트
run_symlink_test() {
echo "--- 심볼릭 링크 테스트 시작 ---"
setup_test_files
local target_file="$TEST_DIR/file1.txt"
local link_file="$TEST_DIR/link_to_file1.txt"
local target_dir="$TEST_DIR/subdir"
local link_dir="$TEST_DIR/link_to_subdir"
local expected_content="This is a test: A is A, b is b."
# 원본 파일의 실제 초기 내용 (\u 포함)
local original_target_content='This is a test: \u0041 is A, \u0062 is b.'
# 심볼릭 링크 생성
# ln -s target linkname
ln -s "file1.txt" "$link_file" # 상대 경로 링크
ln -s "subdir" "$link_dir" # 상대 경로 링크
"$CONVERT_SCRIPT" "$TEST_DIR"
# 원본 파일 변환 확인
check_file_content "$target_file" "$expected_content"
check_file_exists "$target_file.bak" true
# 링크 파일 자체는 처리되지 않아야 함 (find -type f는 심볼릭 링크를 따르지 않음)
# 링크 파일의 내용은 원본 파일과 동일하게 변환된 상태로 보임 (readlink 아님)
check_file_content "$link_file" "$expected_content" # 링크는 원본을 가리키므로 변환된 내용이 보임
# 링크 자체에 대한 백업은 생성되지 않음
check_file_exists "$link_file.bak" false # 심볼릭 링크는 처리 안되므로 백업 없음
# 링크된 디렉토리 내 파일 변환 확인 (find는 디렉토리 링크를 따라 들어갑니다)
check_file_content "$TEST_DIR/subdir/subfile1.txt" "Subdir test: C is C."
check_file_exists "$TEST_DIR/subdir/subfile1.txt.bak" true
# 링크 디렉토리 자체 백업 없음 확인
check_file_exists "$link_dir.bak" false
echo "--- 심볼릭 링크 테스트 종료 ---"
}
# 8. 특수 문자 포함 파일 이름 테스트
run_special_chars_test() {
echo "--- 특수 문자 파일 이름 테스트 시작 ---"
setup_test_files
local special_file="$TEST_DIR/file with spaces 'and' \"quotes\" \$var *(chars).txt"
local content_special='Special chars test: \u0053 is S.'
local expected_content="Special chars test: S is S."
# 파일 생성은 setup_test_files 에서 이미 처리됨
"$CONVERT_SCRIPT" "$TEST_DIR"
check_file_content "$special_file" "$expected_content"
check_file_exists "$special_file.bak" true
echo "--- 특수 문자 파일 이름 테스트 종료 ---"
}
# 9. 일치하는 파일 없음 테스트
run_no_match_test() {
echo "--- 일치 파일 없음 테스트 시작 ---"
setup_test_files
# 모든 잠재적 대상 파일 삭제
# find 명령어를 사용하여 안전하게 삭제
find "$TEST_DIR" -type f \( -name '*.txt' -o -name '*.md' -o -name '*.dat' -o -name '*.yml' \) -delete
printf '%s' "No match content" > "$TEST_DIR/no_match.other" # .other 확장자는 처리 대상이 아님
# 존재하지 않는 확장자로 실행
# 2>&1: 표준 에러를 표준 출력으로 리다이렉션하여 캡처
output=$("$CONVERT_SCRIPT" --ext nonexisting "$TEST_DIR" 2>&1)
local exit_code=$?
# 오류 없이 정상 종료 확인 (종료 코드 0 예상)
record_result "일치 파일 없음 (종료 코드 확인)" "$([[ $exit_code -eq 0 ]] && echo true || echo false)" "종료 코드가 0이 아닙니다 (코드: $exit_code)."
# 결과 요약 출력은 정상 동작으로 간주 (v3 이후 수정됨)
local condition=true
# 명시적인 '오류' 또는 'Error' 메시지가 출력되지 않았는지 확인
if [[ "$output" == *"오류"* || "$output" == *"Error"* ]]; then
condition=false
fi
record_result "일치 파일 없음 (오류 메시지 미출력 확인)" "$condition" "명시적인 오류 메시지가 출력되었습니다: $output"
# 관련 없는 파일의 백업이 생성되지 않았는지 확인
check_file_exists "$TEST_DIR/no_match.other.bak" false
echo "--- 일치 파일 없음 테스트 종료 ---"
}
# 10. 백업 파일 충돌 테스트
run_backup_collision_test() {
echo "--- 백업 파일 충돌 테스트 시작 ---"
setup_test_files
local target_file="$TEST_DIR/file1.txt"
local backup_file="$target_file.bak"
# 원본 파일의 실제 초기 내용 (\u 포함)
local original_target_content='This is a test: \u0041 is A, \u0062 is b.'
local pre_existing_backup_content="This is a pre-existing backup."
# 변환 후 target_file의 예상 내용
local expected_converted_content="This is a test: A is A, b is b."
# 백업 파일(backup_file)에 저장될 것으로 예상되는 내용 (변환 전 원본)
local expected_backup_content='This is a test: \u0041 is A, \u0062 is b.' # \ 포함되도록 수정
# 미리 백업 파일 생성
printf '%s' "$pre_existing_backup_content" > "$backup_file"
# 스크립트 실행
"$CONVERT_SCRIPT" "$TEST_DIR"
# 원본 파일 변환 확인
check_file_content "$target_file" "$expected_converted_content"
# 백업 파일 내용 확인 (변환 전 원본 내용이어야 함)
check_file_content "$backup_file" "$expected_backup_content"
echo "--- 백업 파일 충돌 테스트 종료 ---"
}
# 11. Non-BMP 유니코드 테스트
run_non_bmp_test() {
echo "--- Non-BMP 유니코드 테스트 시작 ---"
setup_test_files
local target_file="$TEST_DIR/emoji_test.txt"
# 😀 (U+1F600) - Non-BMP 문자
local content_emoji='Emoji test: u1F600 is 😀. Normal: \u0041 is A.'
# 현재 Perl 로직은 Non-BMP (uXXXXX 또는 \u{XXXXX}) 를 처리하지 못함.
# u1F600 은 변환되지 않고, \u0041 만 변환되어야 함.
local expected_content="Emoji test: u1F600 is 😀. Normal: A is A."
# 파일 생성은 setup_test_files 에서 이미 처리됨
"$CONVERT_SCRIPT" "$TEST_DIR"
check_file_content "$target_file" "$expected_content"
# 변경 사항(\u0041 -> A)이 있었으므로 백업 파일 존재해야 함
check_file_exists "$target_file.bak" true
echo "--- Non-BMP 유니코드 테스트 종료 ---"
}
# 12. 대상 경로가 파일일 경우 테스트
run_target_is_file_test() {
echo "--- 대상 경로가 파일일 경우 테스트 시작 ---"
setup_test_files
local target_as_file="$TEST_DIR/file_as_target.txt"
printf '%s' "This is a file, not a directory." > "$target_as_file"
# 스크립트가 오류를 발생시키는지 확인하기 위해 set -e 를 일시적으로 비활성화
set +e
# 2>&1: 표준 에러를 표준 출력으로 리다이렉션하여 캡처
output=$("$CONVERT_SCRIPT" "$target_as_file" 2>&1)
local exit_code=$?
# set -e 복구
set -e
# 종료 코드가 0이 아닌지 확인 (오류 발생 예상)
local condition_exit_code=false
if [[ "$exit_code" -ne 0 ]]; then condition_exit_code=true; fi
record_result "대상 경로가 파일일 때 (종료 코드 확인)" "$condition_exit_code" "스크립트가 오류 없이 종료되었습니다 (예상: 오류, 실제 종료 코드: $exit_code)."
# 오류 메시지에 '유효한 디렉토리가 아니' 또는 'is not a directory' 포함 여부 확인
local condition_msg=false
if [[ "$output" == *"오류"* || "$output" == *"Error"* ]] && [[ "$output" == *"유효한 디렉토리가 아니"* || "$output" == *"is not a directory"* || "$output" == *"not a directory"* ]]; then
condition_msg=true
fi
record_result "대상 경로가 파일일 때 (오류 메시지 확인)" "$condition_msg" "예상된 오류 메시지가 출력되지 않았습니다: $output"
echo "--- 대상 경로가 파일일 경우 테스트 종료 ---"
}
# 13. uc + 연속 16진수 패턴 테스트
run_uc_pattern_test() {
echo "--- uc + 연속 16진수 패턴 테스트 시작 ---"
setup_test_files # 기본 파일들을 다시 설정
local target_file="$TEST_DIR/uc_pattern_test.yml"
# 수정된 convert_all_unicode.sh 실행
"$CONVERT_SCRIPT" "$TEST_DIR"
# uc 로직이 비활성화되었으므로, \u0041만 변환되고 나머지는 그대로 있어야 함
# check_file_content 함수에서 줄바꿈 정규화 후 비교
local expected_content="# PR ucf54uba58ud2b8 uae30ub2a5uc744 uc704ud55c uad8cud55c ucd94uac00\n# Normal: A, uc41. uc4142 -> AB. uc414 -> A(4는 무시). uc414243 -> ABC.\n# Invalid: uc123, ucXYZ. Not hex: ucXXXX."
check_file_content "$target_file" "$expected_content"
# \u0041 변환으로 인해 내용이 변경되었으므로 백업 파일 존재 확인
check_file_exists "$target_file.bak" true
echo "--- uc + 연속 16진수 패턴 테스트 종료 ---"
}
# --- 테스트 실행 순서 ---
run_basic_conversion_test
run_dry_run_test
run_extension_filter_test
run_backup_options_test
run_no_recursive_test
run_exclude_test
run_symlink_test
run_special_chars_test
run_no_match_test
run_backup_collision_test
run_non_bmp_test
run_target_is_file_test
run_uc_pattern_test # 새로운 테스트 함수 호출 추가
# 인터랙티브 테스트는 자동화 어려움으로 제거됨
# --- 최종 결과 출력 ---
echo
echo "========================================="
echo "테스트 결과 요약"
echo "========================================="
echo "성공: $pass_count"
echo "실패: $fail_count"
echo
if [[ "$fail_count" -gt 0 ]]; then
echo "실패한 테스트 상세 내용:"
# printf '%s\n' "${failures[@]}" # 실패 상세 메시지 출력
for failure in "${failures[@]}"; do
echo -e "$failure" # -e 옵션을 사용하여 \n 해석
done
echo "========================================="
exit 1
else
echo "🎉 모든 자동 테스트 통과!"
echo "========================================="
exit 0
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment