🔥 완전한 유틸리티 만들기
642자
6분
ArgumentParser 프레임워크를 사용해 커맨드 라인 도구(command line tool)를 만드는 방법을 살펴봅시다. 여기서는 count
명령어를 구현해 볼 텐데요, 이 명령어는 단어 개수를 세는 기능을 제공합니다.
먼저 필요한 모듈을 가져와야(import) 합니다.
import ArgumentParser import Foundation
swift
그리고 ParsableCommand
프로토콜을 채택하는 Count
구조체를 정의합니다. @main
속성을 사용하면 이 구조체가 프로그램 시작점(entry point)이 됩니다.
@main struct Count: ParsableCommand { static let configuration = CommandConfiguration(abstract: "Word counter.") // 옵션과 플래그 속성을 추가할 위치 mutating func run() throws { // 명령어 실행 로직을 구현할 위치 } }
swift
configuration
속성으로 명령어에 대한 간단한 설명을 제공할 수 있습니다.
이제 옵션과 플래그 속성을 추가해 봅시다.
@Option(name: [.short, .customLong("input")], help: "A file to read.") var inputFile: String @Option(name: [.short, .customLong("output")], help: "A file to save word counts to.") var outputFile: String @Flag(name: .shortAndLong, help: "Print status updates while counting.") var verbose = false
swift
inputFile
과outputFile
옵션으로 입력 파일 경로와 출력 파일 경로를 받습니다.verbose
플래그로 명령어 실행 중 상태 메시지 출력 여부를 제어합니다.
이제 run()
메서드 안에 명령어 실행 로직을 구현해 보겠습니다.
mutating func run() throws { if verbose { print(""" Counting words in '\(inputFile)' \\ and writing the result into '\(outputFile)'. """) } guard let input = try? String(contentsOfFile: inputFile) else { throw RuntimeError("Couldn't read from '\(inputFile)'!") } let words = input.components(separatedBy: .whitespacesAndNewlines) .map { word in word.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) .lowercased() } .compactMap { word in word.isEmpty ? nil : word } let counts = Dictionary(grouping: words, by: { $0 }) .mapValues { $0.count } .sorted(by: { $0.value > $1.value }) if verbose { print("Found \(counts.count) words.") } let output = counts.map { word, count in "\(word): \(count)" } .joined(separator: "\n") guard let _ = try? output.write(toFile: outputFile, atomically: true, encoding: .utf8) else { throw RuntimeError("Couldn't write to '\(outputFile)'!") } }
swift
코드를 단계별로 살펴볼까요?
verbose
플래그를 설정했다면, 어떤 파일에서 단어를 세고 어느 파일에 결과를 쓸지 출력합니다.inputFile
에서 문자열을 읽어옵니다. 파일 읽기에 실패하면RuntimeError
를 던집니다.- 입력 문자열을 단어 단위로 쪼개고, 특수 문자를 제거하며, 소문자로 변환합니다. 빈 문자열은 제거합니다.
- 단어별 등장 횟수를 세고, 등장 횟수에 따라 내림차순으로 정렬합니다.
verbose
플래그가 설정되었다면, 총 단어 수를 출력합니다.- 결과를 "
단어: 등장횟수
" 형식 문자열로 변환합니다. - 결과 문자열을
outputFile
에 씁니다. 파일 쓰기에 실패하면RuntimeError
를 던집니다.
마지막으로 RuntimeError
타입을 정의합니다. 이 타입은 Error
와 CustomStringConvertible
프로토콜을 채택해 에러 메시지를 제공합니다.
struct RuntimeError: Error, CustomStringConvertible { var description: String init(_ description: String) { self.description = description } }
swift
자, 이제 count
명령어 구현을 완성했습니다! 터미널에서 다음과 같이 실행할 수 있습니다.
$ swift run count -i input.txt -o output.txt -v Counting words in 'input.txt' and writing the result into 'output.txt'. Found 42 words.
shell
ArgumentParser 프레임워크를 활용하니 명령어 인터페이스를 정의하고 파싱하는 코드를 깔끔하게 작성할 수 있었습니다. 아래는 전체 소스 코드입니다.
import ArgumentParser import Foundation @main struct Count: ParsableCommand { static let configuration = CommandConfiguration(abstract: "Word counter.") @Option(name: [.short, .customLong("input")], help: "A file to read.") var inputFile: String @Option(name: [.short, .customLong("output")], help: "A file to save word counts to.") var outputFile: String @Flag(name: .shortAndLong, help: "Print status updates while counting.") var verbose = false mutating func run() throws { if verbose { print(""" Counting words in '\(inputFile)' \\ and writing the result into '\(outputFile)'. """) } guard let input = try? String(contentsOfFile: inputFile) else { throw RuntimeError("Couldn't read from '\(inputFile)'!") } let words = input.components(separatedBy: .whitespacesAndNewlines) .map { word in word.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) .lowercased() } .compactMap { word in word.isEmpty ? nil : word } let counts = Dictionary(grouping: words, by: { $0 }) .mapValues { $0.count } .sorted(by: { $0.value > $1.value }) if verbose { print("Found \(counts.count) words.") } let output = counts.map { word, count in "\(word): \(count)" } .joined(separator: "\n") guard let _ = try? output.write(toFile: outputFile, atomically: true, encoding: .utf8) else { throw RuntimeError("Couldn't write to '\(outputFile)'!") } } } struct RuntimeError: Error, CustomStringConvertible { var description: String init(_ description: String) { self.description = description } }
swift