[Dart] 명령어(CLI) 툴 만들기

[Dart] 명령어(CLI) 툴 만들기

2023-06-05 hit count image

Dart로 명령어(CLI) 툴을 만드는 방법에 대해서 알아보도록 하겠습니다.

개요

Flutter로 앱을 개발하다보면, 명령어(CLI - Command Line Interface)로 무언가를 하는 기능이 필요할 때가 있습니다. 예를 들어 pubspec.yaml 파일의 version을 업데이트할 때, 직접 수정하는 것도 가능하지만, 다음과 같이 명령어로 업데이트를 하도록 만들 수 도 있습니다.

dart run bull:pub_version --version 2.5.7

이번 블로그 포스트에서는 Dart를 사용하여 명령어(CLI) 툴을 개발하는 방법에 대해서 알아보도록 하겠습니다.

패키지 프로젝트 생성

Dart를 사용해서 명령어 툴을 만들어 pub.dev에 배포하기 위해 Dart 패키지 프로젝트를 생성할 필요가 있습니다. pub.dev에 배포할 필요가 없다면, 기존 프로젝트에서 명령어 툴을 제작하면 되므로 이 부분은 건너뛰어도 됩니다.

그럼 다음 명령어를 실행하여 Dart 패키지를 생성합니다.

# dart create --template=package [PACKAGE_NAME]
dart create --template=package cli_example

프로젝트가 생성이 완료되면 다음과 같이 폴더와 파일들이 생성되는 것을 확인할 수 있습니다.

cli_example
├── CHANGELOG.md
├── README.md
├── analysis_options.yaml
├── example
│   └── cli_example_example.dart
├── lib
│   ├── cli_example.dart
│   └── src
│       └── cli_example_base.dart
├── pubspec.lock
├── pubspec.yaml
└── test
    └── cli_example_test.dart

args 패키지 설치

args 패키지는 Dart로 명령어(CLI)를 제작하는 것을 도와주는 패키지입니다.

args 패키지를 사용하기 위해 다음 명령어를 실행하여 args 패키지를 설치합니다.

dart pub add args

Flutter 프로젝트라면 다음 명령어를 실행하여 args를 설치합니다.

flutter pub add args

명령어 구현

Dart로 명령어(CLI)를 만들기 위해서는 args 패키지의 Command 클래스를 사용할 필요가 있습니다. lib/src/cli_example_base.dart 파일을 열고 다음과 같이 수정합니다.

import 'package:args/command_runner.dart';

class Echo extends Command {
  @override
  final name = 'echo';

  @override
  final description = 'Echo option';

  Echo() {
    argParser.addOption('message', help: 'A message to echo');
  }

  @override
  void run() {
    String? message = argResults?['message'];

    // ignore: avoid_print
    print('Message: $message');
  }
}

Command 클래스를 상속받으면 3개의 멤버를 오버라이드(Override)해야 합니다. name은 해당 명령어의 이름이고, description은 명령어의 설명문입니다. 그리고 run() 함수는 해당 명령어를 구현하는 부분입니다.

Command 클래스에서는 기본적으로 argParseargResults 변수를 사용할 수 있습니다. 생성자(Echo())에서 argParseraddOption을 통해 해당 명령어의 필요한 옵션명(message)과 설명문(help)을 추가하였습니다.

그리고 명령어의 구현 부분인 run() 함수에서 명령어 옵션에 설정된 값을 argResults?['message']을 통해 할당받았으며, print를 사용하여 해당 내용을 출력하도록 하였습니다.

실제 명령어를 구현할 때에는 argParser에 다양한 옵션을 추가할 것이고, run() 함수에서 옵션에 따른 다양한 기능을 구현하게 될 것입니다.

명령어 추가

lib 폴더 하위에 구현한 내용은, 패키지를 설치하고 Dart 파일안에서 패키지의 내용을 불러와 사용할 수 있습니다. 즉, 우리가 만든 Echo는 명령어로써 사용이 불가능하고, *.dart 파일안에서만 사용이 가능합니다.

우리가 만든 Echo 명령어를 *.dart 파일이 아닌 명령어로 사용할 수 있게 하려면, bin 폴더를 만들고 Echo 명령어를 실행하는 파일을 만들어야 합니다.

그럼 bin/cli_example.dart 파일을 생성하고 다음과 같이 수정합니다.

import 'package:args/command_runner.dart';
import 'package:cli_example/cli_example.dart';

void main(List<String> arguments) {
  CommandRunner(
    "cli_example",
    "Dart CLI example",
  )
    ..addCommand(Echo())
    ..run(arguments);
}

사용자가 명령어를 실행하면 main 함수가 실행되고 arguments에 사용자가 입력한 옵션과 값이 설정됩니다.

이 파일에는 args 패키지를 사용하여 우리가 만든 Echo 명령어를 등록(addCommand)하고, 사용자가 입력한 값(arguments)와 함께 실행(run)하도록 구성되었습니다.

이렇게 만든 파일은 다음과 같은 명령어로 실행이 가능합니다.

dart run cli_example echo --message="test"

그럼 다음과 같은 결과를 얻을 수 있습니다.

Message: test

우리는 argsCommand 클래스를 사용하였기 때문에, 다음 명령어과 같은 명령어를 사용할 수 있습니다.

dart run cli_example -h

그럼 다음과 같이 우리가 작성한 descriptionhelp 메시지가 잘 표시되는 것을 확인할 수 있습니다.

Dart CLI example

Usage: cli_example <command> [arguments]

Global options:
-h, --help    Print this usage information.

Available commands:
  echo   Echo option

Run "cli_example help <command>" for more information about a command.

물론, 다음과 같이 Echo 명령어에 대한 help 메시지도 출력이 가능합니다.

dart run cli_example echo -h

그럼 다음과 같은 결과를 확인할 수 있습니다.

Echo option

Usage: cli_example echo [arguments]
-h, --help       Print this usage information.
    --message    A message to echo

예제 작성

우리가 만든 명령어 패키지를 pub.dev에 배포하기 위해서는, Echo 명령어를 사용하는 예제를 만들어야 합니다. example/cli_example_example.dart 파일을 열고 다음과 같이 수정합니다.

import 'package:args/command_runner.dart';
import 'package:cli_example/cli_example.dart';

void main() async {
  final cmd = CommandRunner(
    "cli_example",
    "Dart CLI example",
  )..addCommand(Echo());

  await cmd.run(['echo', '--message', 'test message']);
}

우리는 현재 명령어 툴을 만들고 있기 때문에 사실 예제를 작성할 필요는 없습니다. 이 파일은 반대로 사용자에게 혼란을 줄 수 있으므로 제거하는 것을 추천합니다.

테스트 코드 작성

명령어를 실행하였을 때, 잘 동작하는지 확인하기 위한 테스트 코드를 작성할 수 있습니다. 테스트 코드를 작성하기 위해 test/cli_example_test.dart 파일을 만들고 다음과 같이 수정합니다.

import 'package:args/command_runner.dart';
import 'package:cli_example/cli_example.dart';
import 'package:run_with_print/run_with_print.dart';
import 'package:test/test.dart';

void main() {
  final runner = CommandRunner('test', 'test')..addCommand(Echo());

  test('Echo test message', () async {
    await runWithPrint((logs) async {
      await runner.run(['echo', '--message', 'test message']);
      expect(logs, ['Message: test message']);
    });
  });
}

우리가 만든 Echo 명령어는 print를 사용하여 결과를 출력합니다. 이를 확인하기 위해서 다음 명령어를 실행하여 run_with_print 패키지를 설치하였습니다.

dart pub add --dev run_with_print

설치가 완료되었다면, run_with_printrunWithPrint 함수를 통해 print로 출력되는 내용을 체크하였습니다. run_with_print 패키지에 대해서는 다음 링크를 참고하시기 바랍니다.

이제 다음 명령어를 실행하여 테스트 코드를 실행합니다.

dart test test/cli_example_test.dart

그럼 다음과 같이 테스트가 잘 통과하는 것을 확인할 수 있습니다.

00:00 +1: All tests passed!

명령어 테스트

이제 이렇게 만든 명령어가 제대로 동작하는지 확인해 봅시다. 우선 명령어를 구현한 코드를 GitHub에 올립니다. 그리고 다음 명령어를 실행하여 명령어를 활성화 시킵니다.

dart pub global activate --source git https://github.com/dev-yakuza/cli_example

이렇게 GitHub에 올린 코드는 다음과 같이 실행할 수 있습니다.

dart pub global run cli_example echo --message="test messsage"

그럼 다음과 같이 명령어가 잘 실행되는 것을 확인할 수 있습니다.

Message: test messsage

빌드

이렇게 만든 명령어는 독립적인 실행 파일(Standalone executable)로 만들 수 있습니다. 다음 명령어를 실행하여 명령어를 독립적인 실행 파일로 만듭니다.

dart compile exe bin/cli_example.dart

그럼 bin 폴더에 cli_example.exe 파일이 생성된 것을 확인할 수 있습니다. 이렇게 생성된 명령어 파일은 다음과 같이 실행할 수 있습니다.

./bin/cli_example.exe echo --message="test message"

명령어 확장자가 exe이지만, 이 명령어 파일은 윈도우뿐만 아니라, macOS 그리고 Linux를 지원합니다.

pub.dev에 배포

이렇게 만든 명령어 패키지를 pub.dev에 배포하여 사용할 수 있습니다. pub.dev에 패키지를 배포하는 방법에 대해서는 다음 링크를 참고하시기 바랍니다.

공식 문서 참고

다음은 Dart로 명령어를 만들때, 도움이 되는 공식 문서입니다.

완료

이것으로 Dart를 사용하여 명령어(CLI) 툴을 만드는 방법에 대해서 알아보았습니다. Flutter로 앱을 개발하거나 Dart로 프로젝트를 개발할 때, 특정 명령어를 만들어야 할 때가 있습니다. 이때, 이번 포스트를 참고하여 명령어(CLI) 툴을 만들어 보시기 바랍니다.

제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!

앱 홍보

책 홍보

스무디 한 잔 마시며 끝내는 React Native 책을 출판한지 벌써 2년이 다되었네요.
이번에도 좋은 기회가 있어서 스무디 한 잔 마시며 끝내는 리액트 + TDD 책을 출판하게 되었습니다.

아래 링크를 통해 제가 쓴 책을 구매하실 수 있습니다.
많은 분들에게 도움이 되면 좋겠네요.

스무디 한 잔 마시며 끝내는 React Native, 비제이퍼블릭
스무디 한 잔 마시며 끝내는 리액트 + TDD, 비제이퍼블릭
[심통]현장에서 바로 써먹는 리액트 with 타입스크립트 : 리액트와 스토리북으로 배우는 컴포넌트 주도 개발, 심통
Posts