다음을 통해 공유


System.CommandLine의 처리기에 인수를 바인딩하는 방법

Important

System.CommandLine은 현재 미리 보기로 제공되며 이 설명서는 버전 2.0 베타 4용입니다. 일부 정보는 릴리스되기 전에 상당 부분 수정될 수 있는 시험판 제품과 관련이 있습니다. Microsoft는 여기에 제공된 정보에 대해 어떠한 명시적이거나 묵시적인 보증도 하지 않습니다.

인수를 구문 분석하고 이를 명령 처리기 코드에 제공하는 프로세스를 매개 변수 바인딩이라고 합니다. System.CommandLine에는 기본 제공된 많은 인수 형식을 바인딩하는 기능이 있습니다. 예를 들어 정수, 열거형, 파일 시스템 개체(예: FileInfoDirectoryInfo)를 바인딩할 수 있습니다. 여러 System.CommandLine 형식도 바인딩할 수 있습니다.

기본 제공 인수 유효성 검사

인수에는 예상되는 형식과 인자가 포함됩니다. System.CommandLine은 이러한 예상과 일치하지 않는 인수를 거부합니다.

예를 들어 정수 옵션의 인수가 정수가 아닌 경우 구문 분석 오류가 표시됩니다.

myapp --delay not-an-int
Cannot parse argument 'not-an-int' as System.Int32.

여러 인수가 다음 중 하나의 최대 인자를 갖는 옵션으로 전달되면 다음과 같은 인자 오류가 표시됩니다.

myapp --delay-option 1 --delay-option 2
Option '--delay' expects a single argument but 2 were provided.

이 동작은 Option.AllowMultipleArgumentsPerTokentrue로 설정하여 재정의할 수 있습니다. 이 경우 최대 인자가 1인 옵션을 반복할 수 있지만 줄의 마지막 값만 허용됩니다. 다음 예제에서는 three 값이 앱에 전달됩니다.

myapp --item one --item two --item three

최대 8개의 옵션 및 인수를 바인딩하는 매개 변수

다음 예제에서는 SetHandler를 호출하여 명령 처리기 매개 변수에 옵션을 바인딩하는 방법을 보여 줍니다.

var delayOption = new Option<int>
    ("--delay", "An option whose argument is parsed as an int.");
var messageOption = new Option<string>
    ("--message", "An option whose argument is parsed as a string.");

var rootCommand = new RootCommand("Parameter binding example");
rootCommand.Add(delayOption);
rootCommand.Add(messageOption);

rootCommand.SetHandler(
    (delayOptionValue, messageOptionValue) =>
    {
        DisplayIntAndString(delayOptionValue, messageOptionValue);
    },
    delayOption, messageOption);

await rootCommand.InvokeAsync(args);
public static void DisplayIntAndString(int delayOptionValue, string messageOptionValue)
{
    Console.WriteLine($"--delay = {delayOptionValue}");
    Console.WriteLine($"--message = {messageOptionValue}");
}

람다 매개 변수는 옵션 및 인수의 값을 나타내는 변수입니다.

(delayOptionValue, messageOptionValue) =>
{
    DisplayIntAndString(delayOptionValue, messageOptionValue);
},

람다 뒤에 오는 변수는 옵션 및 인수 값의 소스인 옵션과 인수 개체를 나타냅니다.

delayOption, messageOption);

옵션 및 인수는 람다와 람다 뒤에 있는 매개 변수에서 동일한 순서로 선언되어야 합니다. 순서가 일치하지 않으면 다음 시나리오 중 하나가 발생합니다.

  • 순서가 잘못된 옵션이나 또는 인수의 형식이 다른 경우 런타임 예외가 throw됩니다. 예를 들어 소스 목록에서 string이 있어야 하는 위치에 int가 나타날 수 있습니다.
  • 순서가 잘못된 옵션이나 인수가 동일한 형식인 경우 처리기에 제공된 매개 변수에서 잘못된 값을 자동으로 가져옵니다. 예를 들어 소스 목록에서 string 옵션 y가 있어야 하는 위치에 string 옵션 x가 나타날 수 있습니다. 이 경우 옵션 y 값의 변수는 옵션 x 값을 가져옵니다.

동기 서명과 비동기 서명을 모두 사용하여 최대 8개의 매개 변수를 지원하는 SetHandler의 오버로드가 있습니다.

9개 이상의 옵션 및 인수를 바인딩하는 매개 변수

9개 이상의 옵션을 처리하거나 여러 옵션에서 사용자 지정 형식을 구성하려면 InvocationContext 또는 사용자 지정 바인더를 사용할 수 있습니다.

InvocationContext 사용

SetHandler 오버로드는 InvocationContext 개체에 대한 액세스를 제공하며 InvocationContext를 사용하여 다양한 옵션 및 인수 값을 가져올 수 있습니다. 예로 종료 코드 설정종료 처리를 참조하세요.

사용자 지정 바인더 사용

사용자 지정 바인더를 사용하면 여러 옵션 또는 인수 값을 복합 형식으로 결합하여 이를 단일 처리기 매개 변수에 전달할 수 있습니다. Person 형식이 있다고 가정합니다.

public class Person
{
    public string? FirstName { get; set; }
    public string? LastName { get; set; }
}

BinderBase<T>에서 파생된 클래스를 만듭니다. 여기서 T는 명령줄 입력을 기반으로 구성할 형식입니다.

public class PersonBinder : BinderBase<Person>
{
    private readonly Option<string> _firstNameOption;
    private readonly Option<string> _lastNameOption;

    public PersonBinder(Option<string> firstNameOption, Option<string> lastNameOption)
    {
        _firstNameOption = firstNameOption;
        _lastNameOption = lastNameOption;
    }

    protected override Person GetBoundValue(BindingContext bindingContext) =>
        new Person
        {
            FirstName = bindingContext.ParseResult.GetValueForOption(_firstNameOption),
            LastName = bindingContext.ParseResult.GetValueForOption(_lastNameOption)
        };
}

사용자 지정 바인더를 사용하면 옵션 및 인수 값을 가져오는 것과 동일한 방식으로 사용자 지정 형식을 처리기에 전달할 수 있습니다.

rootCommand.SetHandler((fileOptionValue, person) =>
    {
        DoRootCommand(fileOptionValue, person);
    },
    fileOption, new PersonBinder(firstNameOption, lastNameOption));

다음은 이전 예제에서 가져온 전체 프로그램입니다.

using System.CommandLine;
using System.CommandLine.Binding;

public class Program
{
    internal static async Task Main(string[] args)
    {
        var fileOption = new Option<FileInfo?>(
              name: "--file",
              description: "An option whose argument is parsed as a FileInfo",
              getDefaultValue: () => new FileInfo("scl.runtimeconfig.json"));

        var firstNameOption = new Option<string>(
              name: "--first-name",
              description: "Person.FirstName");

        var lastNameOption = new Option<string>(
              name: "--last-name",
              description: "Person.LastName");

        var rootCommand = new RootCommand();
        rootCommand.Add(fileOption);
        rootCommand.Add(firstNameOption);
        rootCommand.Add(lastNameOption);

        rootCommand.SetHandler((fileOptionValue, person) =>
            {
                DoRootCommand(fileOptionValue, person);
            },
            fileOption, new PersonBinder(firstNameOption, lastNameOption));

        await rootCommand.InvokeAsync(args);
    }

    public static void DoRootCommand(FileInfo? aFile, Person aPerson)
    {
        Console.WriteLine($"File = {aFile?.FullName}");
        Console.WriteLine($"Person = {aPerson?.FirstName} {aPerson?.LastName}");
    }

    public class Person
    {
        public string? FirstName { get; set; }
        public string? LastName { get; set; }
    }

    public class PersonBinder : BinderBase<Person>
    {
        private readonly Option<string> _firstNameOption;
        private readonly Option<string> _lastNameOption;

        public PersonBinder(Option<string> firstNameOption, Option<string> lastNameOption)
        {
            _firstNameOption = firstNameOption;
            _lastNameOption = lastNameOption;
        }

        protected override Person GetBoundValue(BindingContext bindingContext) =>
            new Person
            {
                FirstName = bindingContext.ParseResult.GetValueForOption(_firstNameOption),
                LastName = bindingContext.ParseResult.GetValueForOption(_lastNameOption)
            };
    }
}

종료 코드 설정

SetHandlerTask-returning Func 오버로드가 있습니다. 처리기가 비동기 코드에서 호출되는 경우 다음 예제와 같이 이들 중 하나를 사용하는 처리기에서 Task<int>를 반환하고 int 값을 사용하여 프로세스 종료 코드를 설정할 수 있습니다.

static async Task<int> Main(string[] args)
{
    var delayOption = new Option<int>("--delay");
    var messageOption = new Option<string>("--message");

    var rootCommand = new RootCommand("Parameter binding example");
    rootCommand.Add(delayOption);
    rootCommand.Add(messageOption);

    rootCommand.SetHandler((delayOptionValue, messageOptionValue) =>
        {
            Console.WriteLine($"--delay = {delayOptionValue}");
            Console.WriteLine($"--message = {messageOptionValue}");
            return Task.FromResult(100);
        },
        delayOption, messageOption);

    return await rootCommand.InvokeAsync(args);
}

그러나 람다 자체가 비동기여야 하는 경우에는 Task<int>를 반환할 수 없습니다. 이 경우에는 InvocationContext.ExitCode를 사용하세요. InvocationContext를 단독 매개 변수로 지정하는 SetHandler 오버로드를 사용하여 람다에 삽입된 InvocationContext 인스턴스를 가져올 수 있습니다. 이 SetHandler 오버로드에서는 IValueDescriptor<T> 개체를 지정할 수는 없지만 다음 예제와 같이 InvocationContextParseResult 속성에서 옵션 및 인수 값을 가져올 수 있습니다.

static async Task<int> Main(string[] args)
{
    var delayOption = new Option<int>("--delay");
    var messageOption = new Option<string>("--message");

    var rootCommand = new RootCommand("Parameter binding example");
    rootCommand.Add(delayOption);
    rootCommand.Add(messageOption);

    rootCommand.SetHandler(async (context) =>
        {
            int delayOptionValue = context.ParseResult.GetValueForOption(delayOption);
            string? messageOptionValue = context.ParseResult.GetValueForOption(messageOption);
        
            Console.WriteLine($"--delay = {delayOptionValue}");
            await Task.Delay(delayOptionValue);
            Console.WriteLine($"--message = {messageOptionValue}");
            context.ExitCode = 100;
        });

    return await rootCommand.InvokeAsync(args);
}

수행할 비동기 작업이 없는 경우 Action 오버로드를 사용할 수 있습니다. 이 경우 비동기 람다와 동일한 방식으로 InvocationContext.ExitCode를 사용하여 종료 코드를 설정합니다.

종료 코드의 기본값은 1입니다. 명시적으로 설정하지 않으면 처리기가 정상적으로 종료될 때 해당 값이 0으로 설정됩니다. 예외가 throw되면 기본값이 유지됩니다.

지원되는 유형

다음 예제에서는 일반적으로 사용되는 몇 가지 형식을 바인딩하는 코드를 보여 줍니다.

열거형

enum 형식의 값은 이름으로 바인딩되며 바인딩은 대/소문자를 구분하지 않습니다.

var colorOption = new Option<ConsoleColor>("--color");

var rootCommand = new RootCommand("Enum binding example");
rootCommand.Add(colorOption);

rootCommand.SetHandler((colorOptionValue) =>
    { Console.WriteLine(colorOptionValue); },
    colorOption);

await rootCommand.InvokeAsync(args);

다음은 이전 예제의 샘플 명령줄 입력 및 결과 출력입니다.

myapp --color red
myapp --color RED
Red
Red

배열 및 목록

IEnumerable을 구현하는 많은 일반적인 형식이 지원됩니다. 예시:

var itemsOption = new Option<IEnumerable<string>>("--items")
    { AllowMultipleArgumentsPerToken = true };

var command = new RootCommand("IEnumerable binding example");
command.Add(itemsOption);

command.SetHandler((items) =>
    {
        Console.WriteLine(items.GetType());

        foreach (string item in items)
        {
            Console.WriteLine(item);
        }
    },
    itemsOption);

await command.InvokeAsync(args);

다음은 이전 예제의 샘플 명령줄 입력 및 결과 출력입니다.

--items one --items two --items three
System.Collections.Generic.List`1[System.String]
one
two
three

AllowMultipleArgumentsPerTokentrue로 설정되므로 다음 입력에서 동일한 출력이 발생합니다.

--items one two three

파일 시스템 형식

파일 시스템에서 작동하는 명령줄 애플리케이션은 FileSystemInfo, FileInfo, DirectoryInfo 형식을 사용할 수 있습니다. 다음 예에서는 FileSystemInfo의 사용법을 보여 줍니다.

var fileOrDirectoryOption = new Option<FileSystemInfo>("--file-or-directory");

var command = new RootCommand();
command.Add(fileOrDirectoryOption);

command.SetHandler((fileSystemInfo) =>
    {
        switch (fileSystemInfo)
        {
            case FileInfo file                    :
                Console.WriteLine($"File name: {file.FullName}");
                break;
            case DirectoryInfo directory:
                Console.WriteLine($"Directory name: {directory.FullName}");
                break;
            default:
                Console.WriteLine("Not a valid file or directory name.");
                break;
        }
    },
    fileOrDirectoryOption);

await command.InvokeAsync(args);

FileInfoDirectoryInfo를 사용하면 패턴 일치 코드는 필요하지 않습니다.

var fileOption = new Option<FileInfo>("--file");

var command = new RootCommand();
command.Add(fileOption);

command.SetHandler((file) =>
    {
        if (file is not null)
        {
            Console.WriteLine($"File name: {file?.FullName}");
        }
        else
        {
            Console.WriteLine("Not a valid file name.");
        }
    },
    fileOption);

await command.InvokeAsync(args);

지원되는 기타 형식

단일 문자열 매개 변수를 사용하는 생성자가 있는 많은 형식은 이러한 방식으로 바인딩될 수 있습니다. 예를 들어 FileInfo에서 작동하는 코드는 대신 Uri에서 작동합니다.

var endpointOption = new Option<Uri>("--endpoint");

var command = new RootCommand();
command.Add(endpointOption);

command.SetHandler((uri) =>
    {
        Console.WriteLine($"URL: {uri?.ToString()}");
    },
    endpointOption);

await command.InvokeAsync(args);

파일 시스템 형식 및 Uri 외에도 다음 형식이 지원됩니다.

  • bool
  • byte
  • DateTime
  • DateTimeOffset
  • decimal
  • double
  • float
  • Guid
  • int
  • long
  • sbyte
  • short
  • uint
  • ulong
  • ushort

System.CommandLine 개체 사용

InvocationContext 개체에 대한 액세스를 제공하는 SetHandler 오버로드가 있습니다. 그런 다음 해당 개체를 사용하여 다른 System.CommandLine 개체에 액세스할 수 있습니다. 예를 들어 다음의 개체에 액세스할 수 있습니다.

InvocationContext

예로 종료 코드 설정종료 처리를 참조하세요.

CancellationToken

CancellationToken을 사용하는 방법에 대한 자세한 내용은 종료 처리 방법을 참조하세요.

IConsole

IConsole을 사용하면 System.Console을 사용하는 것보다 테스트뿐만 아니라 많은 확장성 시나리오가 보다 쉬워집니다. InvocationContext.Console 속성에서 사용할 수 있습니다.

ParseResult

ParseResult 개체는 InvocationContext.ParseResult 속성에서 사용할 수 있습니다. 명령줄 입력을 구문 분석한 결과를 나타내는 싱글톤 구조체입니다. 이를 사용하여 명령줄에 옵션 또는 인수가 있는지 확인하거나 ParseResult.UnmatchedTokens 속성을 가져올 수 있습니다. 이 속성은 구문 분석되었지만 구성된 명령, 옵션 또는 인수와 일치하지 않은 토큰 목록을 포함합니다.

일치하지 않은 토큰 목록은 래퍼처럼 동작하는 명령에 유용합니다. 래퍼 명령은 토큰 집합을 가져와서 다른 명령 또는 앱으로 전달합니다. Linux의 sudo 명령은 예시입니다. 사용자의 이름을 가장하고 실행할 명령을 수행합니다. 예시:

sudo -u admin apt update

이 명령줄에서는 apt update 명령을 사용자 admin으로 실행합니다.

이와 같은 래퍼 명령을 구현하려면 명령 속성 TreatUnmatchedTokensAsErrorsfalse로 설정합니다. 그러면 ParseResult.UnmatchedTokens 속성에는 명령에 명시적으로 속하지 않는 모든 인수가 포함됩니다. 앞의 예에서 ParseResult.UnmatchedTokens에는 apt 토큰과 update 토큰이 포함됩니다. 그런 다음, 명령 처리기는 UnmatchedTokens를 새 셸 호출로 전달할 수 있게 됩니다.

사용자 지정 유효성 검사 및 바인딩

사용자 지정 유효성 검사 코드를 제공하려면 다음 예에 표시된 대로 명령, 옵션 또는 인수에 대해 AddValidator를 호출합니다.

var delayOption = new Option<int>("--delay");
delayOption.AddValidator(result =>
{
    if (result.GetValueForOption(delayOption) < 1)
    {
        result.ErrorMessage = "Must be greater than 0";
    }
});

입력을 구문 분석하고 유효성을 검사하려면 다음 예와 같이 ParseArgument<T> 대리자를 사용합니다.

var delayOption = new Option<int>(
      name: "--delay",
      description: "An option whose argument is parsed as an int.",
      isDefault: true,
      parseArgument: result =>
      {
          if (!result.Tokens.Any())
          {
              return 42;
          }

          if (int.TryParse(result.Tokens.Single().Value, out var delay))
          {
              if (delay < 1)
              {
                  result.ErrorMessage = "Must be greater than 0";
              }
              return delay;
          }
          else
          {
              result.ErrorMessage = "Not an int.";
              return 0; // Ignored.
          }
      });

앞의 코드에서는 isDefaulttrue로 설정하여 사용자가 이 옵션에 대한 값을 입력하지 않은 경우에도 parseArgument 대리자가 호출되도록 합니다.

다음은 AddValidator에서 수행할 수 없지만 ParseArgument<T>에서 수행할 수 있는 작업의 몇 가지 예입니다.

  • 다음 예제의 Person 클래스와 같은 사용자 지정 형식의 구문 분석:

    public class Person
    {
        public string? FirstName { get; set; }
        public string? LastName { get; set; }
    }
    
    var personOption = new Option<Person?>(
          name: "--person",
          description: "An option whose argument is parsed as a Person",
          parseArgument: result =>
          {
              if (result.Tokens.Count != 2)
              {
                  result.ErrorMessage = "--person requires two arguments";
                  return null;
              }
              return new Person
              {
                  FirstName = result.Tokens.First().Value,
                  LastName = result.Tokens.Last().Value
              };
          })
    {
        Arity = ArgumentArity.OneOrMore,
        AllowMultipleArgumentsPerToken = true
    };
    
  • 다른 종류의 입력 문자열 구문 분석(예: "1,2,3"을 int[]로 구문 분석)

  • 동적 인자 예를 들어 문자열 배열로 정의된 두 개의 인수가 있고 명령줄 입력에서 문자열 시퀀스를 처리해야 합니다. ArgumentResult.OnlyTake 메서드를 사용하면 입력 문자열을 인수 간에 동적으로 나눌 수 있습니다.

참고 항목

System.CommandLine 개요