🔥 파싱 전략 정하기

770자
8분

ArgumentParser는 커맨드 라인 입력을 파싱할 때 대시(-)로 시작하는 키와 대시(-)로 시작하지 않는 그냥 값을 구분해요. 보통은 키에 해당하는 값을 찾을 때 대시(-)가 없는 값만 선택합니다.

예를 들어보면, 아래 명령은 --verbose 플래그, --name 옵션, 그리고 file 인자를 정의하고 있어요:

struct Example: ParsableCommand {
    @Flag var verbose = false
    @Option var name: String
    @Argument var file: String?
 
    mutating func run() throws {
        print("Verbose: \(verbose), name: \(name), file: \(file ?? "none")")
    }
}
 
swift

이 명령을 실행할 때는 --name 옵션 값을 바로 뒤에 써 줘야 해요. 만약 --verbose 플래그가 그 사이에 있으면 파싱이 실패하고 오류가 나버립니다:

% example --verbose --name Tomás
Verbose: true, name: Tomás, file: none

% example --name --verbose Tomás
Error: Missing value for '--name <name>'
Usage: example [--verbose] --name <name> [<file>]
  See 'example --help' for more information.
text

배열 옵션도 비슷해요. 기본적으로는 바로 옆에 있는 키-값 쌍만 인식합니다.

다른 방식의 단일 값 파싱

@Option을 만들 때 다른 파싱 전략을 주면 이 동작을 바꿀 수 있어요. 다른 파싱 전략을 고를 때는 조심해야 합니다! 사용자가 예상치 못한 동작을 겪을 수 있거든요.

.unconditional 전략은 대시(-)로 시작하더라도 바로 다음 입력을 값으로 사용합니다. 만약 name@Option(parsing: .unconditional) var name: String으로 정의했다면, 두 번째 시도에서 "--verbose"name의 값이 됩니다:

% example --name --verbose Tomás
Verbose: false, name: --verbose, file: Tomás
text

반면 .scanningForValue 전략은 커맨드 라인 입력을 앞으로 훑어보면서 대시(-)가 없는 첫 번째 값을 사용해요. 다른 플래그나 옵션은 그냥 건너뛰기도 합니다. 만약 name@Option(parsing: .scanningForValue) var name: String으로 정의했다면, 파서는 Tomás를 찾기 위해 앞으로 살펴봅니다. Tomásname의 값으로 사용한 후에는 Tomás 바로 다음 인자부터 다시 파싱을 시작하여 --verbose 플래그를 받아들입니다:

% example --name --verbose Tomás
Verbose: true, name: Tomás, file: none
text

다른 방식의 배열 파싱

배열 옵션의 기본 파싱 방식은 각 값을 키-값 쌍에서 읽어오는 거예요. 예를 들어, 다음 명령은 입력 파일 이름을 여러 개 받을 수 있습니다:

struct Example: ParsableCommand {
    @Option var file: [String] = []
    @Flag var verbose = false
 
    mutating func run() throws {
        print("Verbose: \(verbose), files: \(file)")
    }
}
 
swift

단일 값처럼 사용자가 --file 키를 쓸 때마다 값도 함께 제공해야 해요:

% example --verbose --file file1.swift --file file2.swift
Verbose: true, files: ["file1.swift", "file2.swift"]

% example --file --verbose file1.swift --file file2.swift
Error: Missing value for '--file <file>'
Usage: example [--file <file> ...] [--verbose]
  See 'example --help' for more information.
text

.unconditionalSingleValue 전략은 대시(-)로 시작하더라도 키 다음에 나오는 모든 입력을 값으로 받아들입니다. 만약 file@Option(parsing: .unconditionalSingleValue) var file: [String]으로 정의했다면, 결과 배열에 옵션처럼 생긴 문자열도 들어갈 수 있어요:

% example --file file1.swift --file --verbose
Verbose: false, files: ["file1.swift", "--verbose"]
text

.upToNextOption 전략은 대시(-)로 시작하는 입력이 나올 때까지 옵션 키 뒤에 오는 모든 값을 사용합니다. 만약 file@Option(parsing: .upToNextOption) var file: [String]으로 정의했다면, 사용자는 --file을 반복하지 않고도 여러 파일을 지정할 수 있어요:

% example --file file1.swift file2.swift
Verbose: false, files: ["file1.swift", "file2.swift"]

% example --file file1.swift file2.swift --verbose
Verbose: true, files: ["file1.swift", "file2.swift"]
text

마지막으로 .remaining 전략은 대시(-) 여부와 상관없이 옵션 키 뒤에 오는 모든 입력을 다 받아들입니다. file@Option(parsing: .remaining) var file: [String]으로 정의할 경우, --verbose 플래그를 인식하려면 반드시 --file 키 앞에 써 줘야 합니다:

% example --verbose --file file1.swift file2.swift
Verbose: true, files: ["file1.swift", "file2.swift"]

% example --file file1.swift file2.swift --verbose
Verbose: false, files: ["file1.swift", "file2.swift", "--verbose"]
text

다른 방식의 위치 인자 파싱

위치 인자 배열을 파싱하는 기본 방식은 대시(-)로 시작하는 모든 커맨드 라인 입력을 무시하는 거예요. 예를 들어, 다음 명령은 --verbose 플래그와 위치 인자로 파일 이름을 여러 개 받습니다:

struct Example: ParsableCommand {
    @Flag var verbose = false
    @Argument var files: [String] = []
 
    mutating func run() throws {
        print("Verbose: \(verbose), files: \(files)")
    }
}
 
swift

files 인자 배열은 기본 .remaining 전략을 사용하므로 대시(-)가 없는 값만 가져와요:

% example --verbose file1.swift file2.swift
Verbose: true, files: ["file1.swift", "file2.swift"]

% example --verbose file1.swift file2.swift --other
Error: Unexpected argument '--other'
Usage: example [--verbose] [<files> ...]
  See 'example --help' for more information.
text
  • - 종결자 뒤에 오는 입력은 자동으로 위치 인자로 취급되니까, 사용자는 기본 설정에서도 이렇게 대시()로 시작하는 값을 넘겨줄 수 있습니다:
% example --verbose -- file1.swift file2.swift --other
Verbose: true, files: ["file1.swift", "file2.swift", "--other"]
text

.unconditionalRemaining 전략은 알려진 옵션과 플래그를 파싱하고 남은 입력을 전부 가져갑니다. 대시(-)로 시작하는 입력이나 -- 종결자까지도 포함됩니다. 예를 들어, files@Argument(parsing: .unconditionalRemaining) var files: [String]으로 정의하면, 결과 배열에 옵션 같은 문자열도 포함됩니다:

% example --verbose file1.swift file2.swift --other
Verbose: true, files: ["file1.swift", "file2.swift", "--other"]

% example -- --verbose file1.swift file2.swift --other
Verbose: false, files: ["--", "--verbose", "file1.swift", "file2.swift", "--other"]
text

이렇게 ArgumentParser에서 여러 가지 파싱 전략을 정하는 방법을 살펴봤어요. 각 전략마다 장단점이 있으니 상황에 맞게 알맞은 걸 고르는 게 중요하겠죠? 보통은 기본 전략으로도 잘 작동하지만, 필요하다면 다른 전략을 써서 사용자에게 좀 더 융통성 있는 커맨드 라인 인터페이스를 제공할 수 있답니다.