명령줄 인터페이스 만들기

명령줄 인터페이스 만들기 (create-cli)

Raku 스크립트에 명령줄 인자를 파싱하고, 사용법 메시지를 만들고, 옵션을 정의하는 기능이 기본으로 내장되어 있다는 걸 아시나요? MAIN 서브루틴만 잘 정의하면 raku 프로그램.raku --옵션 값 같은 처리를 손쉽게 할 수 있어요. 이 문서는 Raku의 명령줄 인터페이스(CLI)를 만드는 방법을 처음부터 끝까지 안내해요.

출처: Raku Docs — Create CLI

본문

명령줄 인터페이스 — 개요

Raku 스크립트의 기본 명령줄 인터페이스는 세 부분으로 나뉘어요.

명령줄 파라미터를 Capture로 파싱하기

@*ARGS의 값들을 살펴보고, 어떤 정책에 따라 해석해서 그걸로 Capture 객체를 만들어요. 파싱의 대안적 방법은 개발자가 제공하거나 모듈로 설치할 수 있어요.

생성된 capture로 제공된 MAIN 서브루틴 호출하기

표준 multi dispatch가 생성된 Capture 객체로 MAIN 서브루틴을 호출해요. 즉 여러분의 MAIN 서브루틴은 multi sub일 수 있고, 각 후보가 주어진 명령줄 인자의 일부를 처리하는 책임을 맡아요.

MAIN 호출 실패 시 사용법 정보 만들기/보여주기

multi dispatch가 실패했다면, 스크립트 사용자에게 왜 실패했는지 최대한 알려줘야 해요. 기본적으로 각 MAIN 후보 sub의 시그니처와 관련 Pod 정보를 검사해서 그렇게 해요. 결과는 사용자에게 STDERR로 (또는 --help가 지정됐으면 STDOUT으로) 보여줘요. 사용법 정보 생성의 대안적 방법은 개발자가 제공하거나 모듈로 설치할 수 있어요.

sub MAIN

특별한 이름 MAIN을 가진 sub은 관련된 모든 진입 페이저(BEGIN, CHECK, INIT, PRE, ENTER)가 실행되고 스크립트의 mainline이 실행된 후에 실행돼요. MAIN sub이 없다고 해서 오류가 나지는 않아요. 그러면 스크립트는 인자 파싱 같은 일을 mainline에서 직접 하면 되죠.

MAIN sub에서 정상적으로 종료하면 종료 코드 0(성공)이 돼요. MAIN sub의 반환값은 무시돼요. MAIN sub 안에서 처리되지 않은 예외가 던져지면 종료 코드는 1이 돼요. MAIN으로의 dispatch가 실패하면 STDERR에 사용법 메시지가 표시되고 종료 코드는 2가 돼요.

명령줄 파라미터는 @*ARGS 동적 변수에 있고, MAIN 유닛이 호출되기 전에 스크립트의 mainline에서 변경할 수 있어요.

(multi의 후보들 중) sub MAIN의 시그니처가 표준 multi dispatch 시맨틱으로 실제로 호출될 후보를 결정해요.

간단한 예:

# inside file 'hello.raku'
sub MAIN($name) {
    say "Hello $name, how are you?"
}

이 스크립트를 파라미터 없이 호출하면 다음 사용법 메시지가 나와요.

$ raku hello.raku
Usage:
hello.raku <name>

하지만 파라미터에 기본값을 주면, 이름을 지정하든 안 하든 스크립트를 항상 실행할 수 있어요.

# inside file 'hello.raku'
sub MAIN($name = 'bashful') {
    say "Hello $name, how are you?"
}
$ raku hello.raku
Hello bashful, how are you?
$ raku hello.raku Liz
Hello Liz, how are you?

이를 달성하는 또 다른 방법은 sub MAINmulti로 만드는 거예요.

# inside file 'hello.raku'
multi MAIN()      { say "Hello bashful, how are you?" }
multi MAIN($name) { say "Hello $name, how are you?"   }

위 예제들과 같은 출력을 만들어 내요. 원하는 목표를 위해 두 방법 중 어떤 걸 쓸지는 전적으로 여러분의 선택이에요.

sub MAIN에서 처리할 파라미터 개수를 정할 수 없을 때는 slurpy 파라미터를 쓰면 돼요.

# inside file 'hello-all.raku'
sub MAIN(*@all) { for @all -> $name { say "Hello, " ~ $name } }
$ raku hello-all.raku peter paul mary
Hello, peter
Hello, paul
Hello, mary

여러 개의 named 파라미터와 where 절

하나의 positional과 여러 named 파라미터를 쓰는 더 복잡한 예시예요. where 절을 MAIN 인자에도 적용할 수 있음을 보여줘요.

# inside "frobnicate.raku"
sub MAIN(
    Str   $file where *.IO.f = 'file.dat',
    Int  :$length = 24,
    Bool :$verbose
) {
    say $length if $length.defined;
    say $file   if $file.defined;
    say 'Verbosity ', ($verbose ?? 'on' !! 'off');
}

where *.IO.f 절은 문자열 $file이 실제 존재하는 파일의 이름에 대응하는지 검사해요. = 'file.dat' 부분은 인자가 주어지지 않았을 때 $file의 기본값을 지정해요.

file.dat 파일이 있다면, 인자 없이 호출할 때 이렇게 동작해요.

$ raku frobnicate.raku
24
file.dat
Verbosity off

또는 --verbose로 이렇게 해요.

$ raku frobnicate.raku --verbose
24
file.dat
Verbosity on

file.dat 파일이 없거나, 존재하지 않는 다른 파일명을 지정했다면, MAIN sub의 내부조사(introspection)로 만들어진 표준 사용법 메시지가 나와요.

$ raku frobnicate.raku doesnotexist.dat
Usage:
frobnicate.raku [--length=<Int>] [--verbose] [<file>]

이런 사용법 메시지는 완전히 자동으로 생성돼요. 너무 짧게 느껴진다면 쉽게 정보를 더 추가할 수 있어요.

rakudoc 주석으로 사용법 메시지 개선하기

자동 생성 사용법 메시지를 pod 기능으로 힌트를 제공해 더 좋게 만들 수 있는 쉬운 방법이 있어요.

# inside "frobnicate.raku"
sub MAIN(
    Str   $file where *.IO.f = 'file.dat',  #= an existing file to frobnicate
    Int  :$length = 24,                     #= length needed for frobnication
    Bool :$verbose,                         #= required verbosity
) {
    say $length if $length.defined;
    say $file   if $file.defined;
    say 'Verbosity ', ($verbose ?? 'on' !! 'off');
}

이렇게 하면 사용법 메시지가 이렇게 개선돼요.

$ raku frobnicate.raku doesnotexist.dat
Usage:
frobnicate.raku [--length=<Int>] [--verbose] [<file>]

[<file>]          an existing file to frobnicate
--length=<Int>    length needed for frobnication
--verbose         required verbosity

명령줄과 사용법 메시지: 더 많은 예시

2021.03 릴리스부터, 단일 named 인자의 값은 공백으로도 구분할 수 있어요. 다음 소스를 가진 demo 프로그램을 생각해 봐요.

subset name of Any where Str|True;
subset port of Str;

multi MAIN(
    $file,
    name :$profile,    #= Write profile information to a file
    port :$debug-port, #= Listen for debugger connections on the specified port
    Bool :v($verbose), #= Display verbose output

) {}
multi MAIN("--process-files", *@images) {}

이 프로그램은 다음 사용법 메시지를 생성해요.

Usage:
demo [--profile[=name]] [--debug-port=<port>] [-v] <file>
demo --process-files [<images> ...]

--profile[=name]       Write profile information to a file
--debug-port=<port>    Listen for debugger connections on the specified port
-v                     Display verbose output

다음은 demo를 호출하는 유효한 방법들이에요.

demo --profile ~/foo
demo --profile=/tmp/bar ~/foo
demo --debug-port 4242 ~/foo
demo --debug-port=4242 ~/foo
demo -v ~/foo
demo --process-files *.jpg

그러나 다음은 유효하지 않아요.

demo --profile /tmp/bar ~/foo
demo --debug-port ~/foo

첫 번째가 유효하지 않은 이유는 /tmp/bar~/foo 둘 다 positional 인자로 파싱돼서, demo가 positional 인자를 너무 많이 받게 되기 때문이에요. 두 번째가 유효하지 않은 이유는 ~/foo--debug-port의 인자로 파싱되어 demo에 필요한 positional 인자가 없어지기 때문이에요.

이렇게 동작해요. Raku는 세 가지 유형의 옵션을 구분해요.

  • Boolean 옵션(-v처럼): 인자를 절대 받지 않아요. 있거나 없거나죠.
  • 필수 인자를 가진 옵션(--debug-port처럼): 항상 인자를 받아요. =로 인자를 주면 그걸 쓰고, 아니면 다음 인자를 취해요.
  • 선택적 인자를 가진 옵션(--profile처럼): 인자가 있든 없든 유효해요. 이런 옵션엔 = 문법으로만 인자를 줄 수 있어요. 옵션 뒤에 공백이 있으면 인자 없이 호출된 거예요.

그리고 각 유형의 인자를 만들어내는 시그니처는 이래요.

  • Boolean 옵션: Bool 타입 제약.
  • 필수 인자를 가진 옵션: Bool.ACCEPT하지 않는 타입.
  • 선택적 인자를 가진 옵션: True.ACCEPTS하는 타입 (인자 없이 옵션을 넘기는 건 True를 넘기는 것과 같으니까요).

named 파라미터의 별칭 (Aliases)

다른 서브루틴처럼 MAIN도 named 파라미터에 별칭을 정의할 수 있어요. 특히 단일 문자 대체 이름을 정의하는 데 쓸 수 있어요.

sub MAIN(
    Str   $file where *.IO.f = 'file.dat',  #= an existing file to frobnicate
    Int  :l(:$length) = 24,                 #= length needed for frobnication
    Bool :v(:$verbose),                     #= required verbosity
) {
    say $length if $length.defined;
    say $file   if $file.defined;
    say 'Verbosity ', ($verbose ?? 'on' !! 'off');
}

이 경우 이 별칭들도 --help로 대안으로 나열돼요.

Usage:
frobnicate.raku [--size|--length=<Int>] [--verbose] [<file>]

[<file>]                 an existing file to frobnicate
-l|--length=<Int>        length needed for frobnication
-v|--verbose             required verbosity

Named 배열 (Named arrays)

MAIN 서브루틴은 다른 종류의 named 파라미터, named 배열도 쓸 수 있어요. 이러면 명령줄에서 두 개의 서로 다른 짧은 배열을 인자로 제공하는 게 가능해져요.

sub MAIN 시그니처에 :@로 named-array 파라미터를 선언해요.

# inside file 'named-array.raku'
sub MAIN(:@n) {
    .raku.say for @n
}

명령줄에서 named-array 파라미터를 매번 다른 값으로 반복해서 사용할 수 있어요.

$ raku named-array.raku --n=foo --n=23 --n=6.3 --n=3e8 --n=2+2i --n=bar
"foo"
IntStr.new(23, "23")
RatStr.new(6.3, "6.3")
NumStr.new(300000000e0, "3e8")
ComplexStr.new(<2+2i>, "2+2i")
"bar"

열거형 (Enumerations)

Enumeration은 시그니처에서 사용할 수 있고, 인자는 자동으로 대응하는 enum 심볼로 변환돼요.

enum Flag  (
    FLAG_FOO => 0b001,
    FLAG_BAR => 0b010,
    FLAG_BAZ => 0b100,
);

sub MAIN(Flag $flag = FLAG_FOO) {
    say "Flagging $flag with value $flag.value()";
}

이것은 이렇게 호출하면 올바르게 동작해요.

raku MAIN-enum.raku FLAG_BAZ
# OUTPUT: «Flagging FLAG_BAZ with value 4␤»

하지만 Flag가 아닌 것으로 호출하면 죽어요.

%*SUB-MAIN-OPTS

인자가 sub MAIN {}에 전달되기 전에 처리되는 방식을 %*SUB-MAIN-OPTS 해시의 옵션으로 바꿀 수 있어요. 동적 변수의 특성상 %*SUB-MAIN-OPTS 해시를 설정하고 적절한 설정으로 채워야 해요. 예를 들어:

my %*SUB-MAIN-OPTS =
    :named-anywhere,             # allow named variables at any location
    :bundling,                   # allow bundling of named arguments
    :coerce-allomorphs-to(Int),  # coerce allomorphic arguments to given type
    :allow-no,                   # allow --no-foo as alternative to --/foo
    :numeric-suffix-as-value,    # allow -j2 as alternative to --j=2
;
sub MAIN ($a, $b, :$c, :$d) {
    say "Accepted!"
}

사용 가능한 옵션들:

named-anywhere

기본적으로 프로그램(MAIN)에 전달되는 named 인자는 positional 인자 뒤에 올 수 없어요. 하지만 %*SUB-MAIN-OPTS<named-anywhere>가 참값이면 named 인자는 어디에든, 심지어 positional 파라미터 뒤에도 올 수 있어요. 예를 들어 위 프로그램은 이렇게 호출할 수 있어요.

$ raku example.raku 1 --c=2 3 --d=4

bundling

%*SUB-MAIN-OPTS<bundling>이 참값이면, 단일 문자 named 인자를 한 개의 대시로 묶을 수 있어요. 다음 두 명령은 동등해요.

$ raku example.raku -a -b -c
$ raku example.raku -abc

단, 묶인 인자는 플래그로 이해되며, 부정되거나 값을 할당받을 수 없어요.

$ raku example.raku -/a       # OK
$ raku example.raku -a=asdf   # OK
$ raku example.raku -abc=asdf # Error
$ raku example.raku -/abc     # Error

이 옵션은 Rakudo 컴파일러 2020.10 릴리스부터 사용할 수 있어요.

coerce-allomorphs-to

%*SUB-MAIN-OPTS<coerce-allomorphs-to>가 특정 타입으로 설정되면, allomorphic 값들이 그 타입으로 강제 변환돼요. MAIN으로의 dispatch 문제에서 도움이 될 수 있어요.

이 옵션은 Rakudo 컴파일러 2020.12 릴리스부터 사용할 수 있어요.

allow-no

%*SUB-MAIN-OPTS<allow-no>가 참값이면, 명령줄에서 인자의 부정을 / 대신 no-로도 나타낼 수 있어요.

$ raku example.raku --/foo    # named argument "foo" is False
$ raku example.raku --no-foo  # same

이 옵션은 Rakudo 컴파일러 2022.12 릴리스부터 사용할 수 있어요.

numeric-suffix-as-value

%*SUB-MAIN-OPTS<numeric-suffix-as-value>가 참값이면, 단일 문자 인자가 숫자 값을 접미사로 가질 수 있어요.

$ raku example.raku --j=2  # named argument "j" is 2
$ raku example.raku -j2    # same

이 옵션은 Rakudo 컴파일러 2022.12 릴리스부터 사용할 수 있어요.

is hidden-from-USAGE

때로는 MAIN 후보 중 하나를 자동 생성 사용법 메시지에서 제외하고 싶을 수 있어요. 이는 보여주고 싶지 않은 MAIN 후보의 시그니처에 hidden-from-USAGE 트레이트를 추가하면 돼요. 앞선 예를 확장해서:

# inside file 'hello.raku'
multi MAIN() is hidden-from-USAGE {
    say "Hello bashful, how are you?"
}
multi MAIN($name) {  #= the name by which you would like to be called
    say "Hello $name, how are you?"
}

그래서 이 스크립트를 named 변수만으로 호출하면 다음 사용법이 나와요.

$ raku hello.raku --verbose
Usage:
hello.raku <name> -- the name by which you would like to be called

첫 후보에 hidden-from-USAGE 트레이트가 없었다면 이렇게 보였을 거예요.

$ raku hello.raku --verbose
Usage:
hello.raku
hello.raku <name> -- the name by which you would like to be called

기술적으로는 맞지만 읽기에 그렇게 좋진 않죠.

MAIN의 unit 스코프 정의

프로그램 본문 전체가 MAIN 안에 있다면, unit 선언자를 이렇게 쓸 수 있어요 (앞선 예를 변형).

unit sub MAIN(
    Str   $file where *.IO.f = 'file.dat',
    Int  :$length = 24,
    Bool :$verbose,
);  # <- note semicolon here

say $length if $length.defined;
say $file   if $file.defined;
say 'Verbosity ', ($verbose ?? 'on' !! 'off');
# rest of script is part of MAIN

이는 단 하나(유일한)의 sub MAIN으로 충분할 때만 적절하다는 점에 주의하세요.

sub USAGE

주어진 명령줄 파라미터에 대한 MAIN의 multi 후보를 찾지 못하면 sub USAGE가 호출돼요. 그런 메서드가 없으면 컴파일러가 기본 사용법 메시지를 출력해요.

다음 예에서는 Q 문자열로 사용자 정의 사용법 메시지를 출력해요. (:c 플래그는 중괄호의 보간을, :to 플래그는 모두 heredoc으로 표현하는 것을 활성화해요).

#|(is it the answer)
multi MAIN(Int $i) { say $i == 42 ?? 'answer' !! 'dunno' }
#|(divide two numbers)
multi MAIN($a, $b){ say $a/$b }

sub USAGE() {
    print Q:c:to/EOH/;
    Usage: {$*PROGRAM-NAME} [number]

    Prints the answer or 'dunno'.
    EOH
}

기본 사용법 메시지는 sub USAGE 안에서 읽기 전용 $*USAGE 변수로 접근할 수 있어요. 사용 가능한 sub MAIN 후보와 그 파라미터를 기반으로 생성돼요. 앞서 봤듯이 각 후보에 #|(...) Pod 블록으로 추가 확장 설명을 지정해서 WHY를 설정할 수 있어요.

사용법 메시지 생성 가로채기 (2018.10, v6.d 이후)

GENERATE-USAGE 서브루틴을 직접 제공하거나 ecosystem의 Getopt 모듈에서 가져와서, (MAIN으로의 dispatch 실패 후) 사용법 메시지 생성의 기본 방식을 대체·보강할 수 있어요.

sub GENERATE-USAGE

GENERATE-USAGE 서브루틴은 dispatch 실패로 실행되지 않은 MAIN 서브루틴을 나타내는 Callable을 받아야 해요. 이는 내부조사에 사용할 수 있어요. 다른 모든 파라미터는 MAIN에 보내도록 설정된 파라미터들이에요. 이 서브루틴은 사용자에게 보여주고 싶은 사용법 정보의 문자열을 반환해야 해요. 인자 처리로부터 만들어진 Capture를 그대로 재현하는 예:

sub GENERATE-USAGE(&main, |capture) {
    capture<foo>:exists
    ?? "You're not allowed to specify a --foo"
    !! &*GENERATE-USAGE(&main, |capture)
}

multi 서브루틴으로도 같은 효과를 만들 수 있어요.

multi GENERATE-USAGE(&main, :$foo!) {
    "You're not allowed to specify a --foo"
}
multi GENERATE-USAGE(&main, |capture) {
    &*GENERATE-USAGE(&main, |capture)
}

동적 변수 &*GENERATE-USAGE가 기본 사용법 메시지 생성을 수행한다는 점에 주의하세요. 원하지 않으면 수레바퀴를 다시 발명할 필요가 없어요.

CLI 인자 파싱 가로채기 (2018.10, v6.d 이후)

ARGS-TO-CAPTURE 서브루틴을 직접 제공하거나 ecosystem의 Getopt 모듈에서 가져와서, 인자 파싱의 기본 방식을 대체·보강할 수 있어요.

sub ARGS-TO-CAPTURE

ARGS-TO-CAPTURE 서브루틴은 두 파라미터를 받아야 해요. 실행할 MAIN 유닛을 나타내는 Callable(필요하면 내부조사 가능)과 명령줄의 인자 배열이에요. MAIN 유닛을 dispatch하는 데 쓰일 Capture 객체를 반환해야 해요. 다음은 입력된 어떤 키워드에 따라 Capture를 만드는 아주 인위적인 예예요 (스크립트의 CLI를 테스트할 때 유용할 수 있어요).

sub ARGS-TO-CAPTURE(&main, @args --> Capture) {
    # if we only specified "frobnicate" as an argument
    @args == 1 && @args[0] eq 'frobnicate'
    # then dispatch as MAIN("foo","bar",verbose => 2)
    ?? Capture.new( list => <foo bar>, hash => { verbose => 2 } )
    # otherwise, use default processing of args
    !! &*ARGS-TO-CAPTURE(&main, @args)
}

동적 변수 &*ARGS-TO-CAPTURE가 기본 명령줄 인자에서 Capture로의 처리를 수행한다는 점에 주의하세요. 원하지 않으면 수레바퀴를 다시 발명할 필요가 없어요.

sub RUN-MAIN

sub RUN-MAIN(&main, $mainline, :$in-as-argsfiles)

이 루틴은 MAIN 처리를 완전히 제어할 수 있게 해줘요. 실행해야 할 MAINCallable, mainline 실행의 반환값, 그리고 추가 named 변수 :in-as-argsfiles를 받아요. 후자는 STDIN을 $*ARGFILES로 취급해야 하면 True가 돼요.

RUN-MAIN이 제공되지 않으면, MAIN_HELPER·USAGE 같은 옛 인터페이스의 서브루틴을 찾는 기본값이 실행돼요. 찾으면 "옛" 시맨틱을 따라 실행돼요.

class Hero {
    has @!inventory;
    has Str $.name;
    submethod BUILD( :$name, :@inventory ) {
        $!name = $name;
        @!inventory = @inventory
    }
}

sub new-main($name, *@stuff ) {
    Hero.new(:name($name), :inventory(@stuff) ).raku.say
}

RUN-MAIN( &new-main, Nil );

이것은 생성된 객체의 이름(첫 인자)을 출력해요.

MAIN 호출 가로채기 (2018.10 이전, v6.e)

옛 인터페이스는 MAIN 호출을 완전히 가로챌 수 있게 해줬어요. 이는 프로그램의 mainline에서 MAIN 서브루틴을 찾으면 호출되는 MAIN_HELPER 서브루틴의 존재에 의존했어요.

이 인터페이스는 문서화된 적이 없어요. 하지만 이 문서화되지 않은 인터페이스를 쓰는 프로그램은 v6.e까지 계속 동작해요. v6.d부터는 문서화되지 않은 API 사용 시 DEPRECATED 메시지가 나와요.

ecosystem 모듈은 Perl 6·Raku의 옛 버전과의 호환성을 위해 새 인터페이스와 옛 인터페이스를 둘 다 제공할 수 있어요. 더 새로운 Raku가 새(문서화된) 인터페이스를 인식하면 그것을 사용해요. 새 인터페이스 서브루틴이 없는데 옛 MAIN_HELPER 인터페이스가 있으면 옛 인터페이스를 사용해요. 모듈 개발자가 v6.d 이상만 위한 모듈을 제공하기로 결정했다면, 모듈에서 옛 인터페이스 지원을 제거할 수 있어요.

더 알아보기