Last active
May 2, 2025 05:17
-
-
Save m-fire/b060914991ccde16dbeb5b093c210c77 to your computer and use it in GitHub Desktop.
지정된 디렉토리 하위의 파일들을 검사하여 유니코드 이스케이프 시퀀스(표준: \uXXXX, 비표준: uXXXX)에 해당하는 자연어 문자로 변환합니다.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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