명령줄 인자 파싱 — args 패키지
지난 챕터에서 만든 switch문은 조잡합니다. --help도 안 되고, --output /tmp 같은 옵션도 파싱하지 못합니다. args 패키지가 이 문제를 해결합니다.
args는 Dart 팀이 직접 관리하는 공식 패키지입니다. dart 커맨드 자체도 내부적으로 args를 사용합니다. 안정적이고 기능이 충분합니다.
ArgParser 기초
ArgParser는 허용하는 옵션과 플래그를 미리 선언하고, 실제 인자 문자열을 파싱합니다.
import 'package:args/args.dart';
void main(List<String> args) {
final parser = ArgParser()
..addFlag('verbose', abbr: 'v', help: '상세 출력을 활성화합니다.')
..addOption('output', abbr: 'o', help: '출력 디렉토리를 지정합니다.')
..addOption('ext', allowed: ['jpg', 'png', 'gif'], help: '처리할 확장자.');
final results = parser.parse(args);
if (results['verbose'] as bool) {
print('상세 모드 활성화');
}
final output = results['output'] as String?;
if (output != null) {
print('출력 디렉토리: $output');
}
}
addFlag:true/false값을 갖는 플래그 (--verbose,--no-verbose)addOption: 값을 갖는 옵션 (--output /tmp)abbr: 단축 플래그 (-v,-o)
서브커맨드 구조 — CommandRunner
args 패키지에는 서브커맨드 패턴을 위한 CommandRunner와 Command 추상 클래스가 있습니다. git commit, git push처럼 서브커맨드를 가진 도구를 만들 때 사용합니다.
이제 FileOrganizerRunner를 CommandRunner 기반으로 교체합니다.
// 수정: lib/src/runner.dart
import 'package:args/command_runner.dart';
import 'commands/organize_command.dart';
import 'commands/rename_command.dart';
import 'commands/stats_command.dart';
/// CLI 애플리케이션의 진입점 역할을 하는 러너.
class FileOrganizerRunner {
FileOrganizerRunner() {
_runner = CommandRunner<void>(
'file_organizer',
'파일 정리 및 변환 CLI 도구.',
)
..addCommand(OrganizeCommand())
..addCommand(StatsCommand())
..addCommand(RenameCommand());
// 전역 옵션 추가
_runner.argParser.addFlag(
'verbose',
abbr: 'v',
negatable: false,
help: '상세 출력을 활성화합니다.',
);
}
late final CommandRunner<void> _runner;
Future<void> run(List<String> arguments) async {
await _runner.run(arguments);
}
}
// 수정: lib/file_organizer.dart
library file_organizer;
export 'src/runner.dart';
OrganizeCommand 작성
각 커맨드는 Command<void>를 상속합니다. name, description을 오버라이드하고 run()을 구현합니다.
// 새 파일: lib/src/commands/organize_command.dart
import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:path/path.dart' as p;
class OrganizeCommand extends Command<void> {
OrganizeCommand() {
argParser
..addOption(
'source',
abbr: 's',
help: '정리할 디렉토리 경로.',
defaultsTo: '.',
)
..addOption(
'dest',
abbr: 'd',
help: '파일을 이동할 대상 디렉토리.',
)
..addFlag(
'dry-run',
help: '실제로 이동하지 않고 결과만 출력합니다.',
negatable: false,
)
..addFlag(
'recursive',
abbr: 'r',
help: '하위 디렉토리까지 탐색합니다.',
negatable: false,
);
}
@override
String get name => 'organize';
@override
String get description => '파일을 확장자별로 분류합니다.';
@override
Future<void> run() async {
final source = argResults!['source'] as String;
final dest = argResults!['dest'] as String? ?? source;
final dryRun = argResults!['dry-run'] as bool;
final recursive = argResults!['recursive'] as bool;
final sourceDir = Directory(p.absolute(source));
if (!sourceDir.existsSync()) {
usageException('소스 디렉토리가 존재하지 않습니다: $source');
}
print('정리 시작: ${sourceDir.path}');
if (dryRun) print('[dry-run 모드] 실제 파일을 이동하지 않습니다.');
// 실제 로직은 Ch 03에서 구현합니다.
print('organize 실행 — source: $source, dest: $dest, recursive: $recursive');
}
}
usageException()은 Command가 제공하는 헬퍼 메서드입니다. 호출하면 해당 커맨드의 사용법과 함께 에러 메시지를 출력합니다.
StatsCommand 작성
// 새 파일: lib/src/commands/stats_command.dart
import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:path/path.dart' as p;
class StatsCommand extends Command<void> {
StatsCommand() {
argParser
..addOption(
'path',
abbr: 'p',
help: '통계를 볼 디렉토리 경로.',
defaultsTo: '.',
)
..addFlag(
'recursive',
abbr: 'r',
help: '하위 디렉토리까지 포함합니다.',
negatable: false,
)
..addOption(
'format',
help: '출력 형식.',
allowed: ['table', 'json', 'csv'],
defaultsTo: 'table',
allowedHelp: {
'table': '테이블 형식으로 출력합니다.',
'json': 'JSON 형식으로 출력합니다.',
'csv': 'CSV 형식으로 출력합니다.',
},
);
}
@override
String get name => 'stats';
@override
String get description => '디렉토리 파일 통계를 출력합니다.';
@override
Future<void> run() async {
final targetPath = argResults!['path'] as String;
final recursive = argResults!['recursive'] as bool;
final format = argResults!['format'] as String;
final dir = Directory(p.absolute(targetPath));
if (!dir.existsSync()) {
usageException('디렉토리가 존재하지 않습니다: $targetPath');
}
print('통계 분석: ${dir.path} (recursive: $recursive, format: $format)');
// 실제 로직은 Ch 03에서 구현합니다.
}
}
RenameCommand 작성
// 새 파일: lib/src/commands/rename_command.dart
import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:path/path.dart' as p;
class RenameCommand extends Command<void> {
RenameCommand() {
argParser
..addOption(
'path',
abbr: 'p',
help: '대상 디렉토리 경로.',
defaultsTo: '.',
)
..addOption(
'pattern',
help: '변경 전 파일명 패턴 (정규식).',
mandatory: true,
)
..addOption(
'replacement',
help: '변경 후 파일명.',
mandatory: true,
)
..addFlag(
'dry-run',
help: '실제로 변경하지 않고 결과만 출력합니다.',
negatable: false,
);
}
@override
String get name => 'rename';
@override
String get description => '정규식 패턴으로 파일을 일괄 이름 변경합니다.';
@override
Future<void> run() async {
final targetPath = argResults!['path'] as String;
final pattern = argResults!['pattern'] as String;
final replacement = argResults!['replacement'] as String;
final dryRun = argResults!['dry-run'] as bool;
final dir = Directory(p.absolute(targetPath));
if (!dir.existsSync()) {
usageException('디렉토리가 존재하지 않습니다: $targetPath');
}
print('이름 변경: $pattern → $replacement (dry-run: $dryRun)');
// 실제 로직은 Ch 03에서 구현합니다.
}
}
도움말 자동 생성
CommandRunner는 도움말을 자동으로 생성합니다. 별도 코드를 작성할 필요가 없습니다.
dart run bin/file_organizer.dart --help
파일 정리 및 변환 CLI 도구.
Usage: file_organizer <command> [arguments]
Global options:
-h, --help Print this usage information.
-v, --verbose 상세 출력을 활성화합니다.
Available commands:
organize 파일을 확장자별로 분류합니다.
rename 정규식 패턴으로 파일을 일괄 이름 변경합니다.
stats 디렉토리 파일 통계를 출력합니다.
Run "file_organizer help <command>" for more information about a command.
서브커맨드 도움말도 자동입니다.
dart run bin/file_organizer.dart organize --help
파일을 확장자별로 분류합니다.
Usage: file_organizer organize [arguments]
-h, --help Print this usage information.
-s, --source 정리할 디렉토리 경로.
(defaults to ".")
-d, --dest 파일을 이동할 대상 디렉토리.
--dry-run 실제로 이동하지 않고 결과만 출력합니다.
-r, --recursive 하위 디렉토리까지 탐색합니다.
실행 확인
각 커맨드가 올바르게 라우팅되는지 확인합니다.
# organize 실행dart run bin/file_organizer.dart organize -s /tmp# stats 실행 (json 형식)dart run bin/file_organizer.dart stats --format json# rename 실행 (필수 옵션 누락 시 에러)dart run bin/file_organizer.dart rename# → Missing mandatory option: pattern
bin/file_organizer.dart 업데이트
args의 UsageException을 처리하도록 엔트리포인트를 수정합니다.
// 수정: bin/file_organizer.dart
import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:file_organizer/file_organizer.dart';
Future<void> main(List<String> arguments) async {
final runner = FileOrganizerRunner();
try {
await runner.run(arguments);
} on UsageException catch (e) {
stderr.writeln(e.message);
stderr.writeln();
stderr.writeln(e.usage);
exit(64);
} catch (e, st) {
stderr.writeln('오류: $e');
if (Platform.environment['DEBUG'] == '1') {
stderr.writeln(st);
}
exit(1);
}
}
DEBUG=1 환경 변수가 설정된 경우에만 스택 트레이스를 출력합니다. 개발 중에는 유용하고, 일반 사용자에게는 불필요한 출력을 숨깁니다.
정리
이번 챕터에서는 args 패키지의 CommandRunner와 Command를 사용해 서브커맨드 구조를 완성했습니다.
CommandRunner는 도움말, 에러 처리, 라우팅을 자동으로 처리합니다.Command.argParser에 옵션과 플래그를 선언합니다.mandatory: true로 필수 옵션을 강제할 수 있습니다.usageException()으로 사용법 오류를 우아하게 처리합니다.
다음 챕터에서는 dart:io와 path 패키지를 사용해 실제 파일 시스템 작업을 구현합니다.