mirror of
https://github.com/jung-geun/NFD2NFC.git
synced 2026-06-21 04:15:14 +09:00
src/cli: yargs 기반 CLI (file/dir 서브커맨드 + 디폴트 path)
file/dir 서브커맨드 외에 \$0 <path> 디폴트 커맨드 추가. stat으로 파일/디렉토리 자동 감지 → 기존 v1 스타일 nfd2nfc <path> 형태 지원. --recursive(-r), --dry-run(-n) 옵션. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
120
src/cli/index.ts
Normal file
120
src/cli/index.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'fs/promises';
|
||||
import yargs from 'yargs';
|
||||
import { hideBin } from 'yargs/helpers';
|
||||
import { scan } from '../core/scanner';
|
||||
import { normalizeEntry } from '../core/normalizer';
|
||||
import type { RenameResult } from '../core/types';
|
||||
|
||||
const argv = yargs(hideBin(process.argv))
|
||||
.usage('Usage: $0 <command|path> [options]')
|
||||
.command(
|
||||
'file <path>',
|
||||
'단일 파일의 이름을 NFD→NFC로 변환',
|
||||
(y) => y.positional('path', { describe: '변환할 파일 경로', type: 'string', demandOption: true }),
|
||||
(args) => runFile(args.path as string)
|
||||
)
|
||||
.command(
|
||||
'dir <path>',
|
||||
'디렉토리 내 파일명을 NFD→NFC로 변환',
|
||||
(y) =>
|
||||
y
|
||||
.positional('path', { describe: '변환할 디렉토리 경로', type: 'string', demandOption: true })
|
||||
.option('recursive', { alias: 'r', describe: '하위 디렉토리 포함', type: 'boolean', default: false })
|
||||
.option('dry-run', { alias: 'n', describe: '실제 변환 없이 대상 파일만 출력', type: 'boolean', default: false }),
|
||||
(args) => runDir(args.path as string, args.recursive, args['dry-run'])
|
||||
)
|
||||
.command(
|
||||
'$0 [path]',
|
||||
'파일 또는 디렉토리를 자동 감지하여 NFD→NFC로 변환',
|
||||
(y) =>
|
||||
y
|
||||
.positional('path', { describe: '변환할 경로', type: 'string' })
|
||||
.option('recursive', { alias: 'r', describe: '하위 디렉토리 포함 (디렉토리인 경우)', type: 'boolean', default: false })
|
||||
.option('dry-run', { alias: 'n', describe: '실제 변환 없이 대상만 출력', type: 'boolean', default: false }),
|
||||
async (args) => {
|
||||
if (!args.path) {
|
||||
yargs.showHelp();
|
||||
process.exit(1);
|
||||
}
|
||||
try {
|
||||
const stat = await fs.stat(args.path);
|
||||
if (stat.isDirectory()) {
|
||||
await runDir(args.path, args.recursive, args['dry-run']);
|
||||
} else {
|
||||
await runFile(args.path);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`오류: ${(err as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
)
|
||||
.help()
|
||||
.alias('help', 'h')
|
||||
.version()
|
||||
.alias('version', 'v')
|
||||
.parseAsync();
|
||||
|
||||
async function runFile(filePath: string): Promise<void> {
|
||||
try {
|
||||
console.log(`파일 변환: ${filePath}`);
|
||||
const result = await normalizeEntry(filePath, 'file');
|
||||
printResult(result);
|
||||
} catch (err) {
|
||||
console.error(`오류: ${(err as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function runDir(dirPath: string, recursive: boolean, dryRun: boolean): Promise<void> {
|
||||
try {
|
||||
const entries = await scan(dirPath, recursive);
|
||||
if (entries.length === 0) {
|
||||
console.log('변환 대상 없음.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
console.log(`[미리보기] 변환 대상 ${entries.length}개:`);
|
||||
for (const e of entries) {
|
||||
const nfc = e.path.split('/').pop()!.normalize('NFC');
|
||||
console.log(` ${e.type === 'directory' ? '📁' : '📄'} ${e.path} → .../${nfc}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`디렉토리 변환 시작: ${dirPath}${recursive ? ' (재귀)' : ''}`);
|
||||
const results: RenameResult[] = [];
|
||||
for (const e of entries) {
|
||||
const result = await normalizeEntry(e.path, e.type);
|
||||
results.push(result);
|
||||
}
|
||||
printResults(results);
|
||||
} catch (err) {
|
||||
console.error(`오류: ${(err as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function printResult(r: RenameResult): void {
|
||||
const label = r.type === 'directory' ? '폴더' : '파일';
|
||||
if (r.status === 'renamed') {
|
||||
console.log(`✓ ${label}: ${r.oldPath} → ${r.newPath}`);
|
||||
} else if (r.status === 'skipped') {
|
||||
console.log(`- ${label}: 변환 불필요 (${r.oldPath})`);
|
||||
} else if (r.status === 'noop-same-inode') {
|
||||
console.log(`= ${label}: APFS 동일 inode — 이미 접근 가능 (${r.newPath})`);
|
||||
} else {
|
||||
console.warn(`⚠ ${label}: 충돌 — 대상 파일이 이미 존재합니다 (${r.newPath})`);
|
||||
}
|
||||
}
|
||||
|
||||
function printResults(results: RenameResult[]): void {
|
||||
let renamed = 0;
|
||||
for (const r of results) {
|
||||
printResult(r);
|
||||
if (r.status === 'renamed') renamed++;
|
||||
}
|
||||
console.log(`\n완료: ${renamed}개 변환됨 (총 ${results.length}개 처리)`);
|
||||
}
|
||||
Reference in New Issue
Block a user