Open11

DartでCLIツールを作る

heyhey1028heyhey1028

このパッケージでは「ツールとして使用する」「パッケージとして使用する」と言う2つの選択肢がある模様👀
https://pub.dev/packages/dcli/install

DCli toolsのインストール

dart pub global activate dcli
dcli install

んー、なんかうまくいかんな。。。

dcli toolsの設定がうまくいかないので、単純に

  • dart createでCLIプロジェクトを作成
  • pubspec.yamlにdcliを追加
  • プロジェクトのルートでdart runを実行することで走らせる
heyhey1028heyhey1028

やりたいこと

  1. ファイルの特定部位の書き換え
  2. 複数ファイルの特定
heyhey1028heyhey1028

Dartの入出力

入出力関連のライブラリはこちら
https://api.dart.dev/stable/3.1.0/dart-io/dart-io-library.html

関連クラス

Fileクラス:
Directoryクラス:
FileSystemEntityクラス:File, Directoryクラスが継承する親クラス

Standard output, error, and input streams

stdout:
stderr:
stdin: 標準入力用クラス。stdin.readLineSync()で入力値を取得。

✏️ 標準入力

StringBufferクラス

https://runebook.dev/ja/docs/dart/dart-core/stringbuffer-class

📄ファイル操作

https://api.dart.dev/stable/3.1.0/dart-io/File-class.html

ファイルの読み込み

File.readAsString()を実行。小さいファイルならこれで対応可能。

void main() {
  File('file.txt').readAsString().then((String contents) {
    print(contents);
  });
}

より大きなファイルを読み込む場合は、

  • File.openReadでStreamを取得し、transform()で変換して取得する
  • openRead()ではバイトのStreamとなる為、transform(utf8.decoder)で文字列に変換する
void main() async {
  final file = File('file.txt');
  Stream<String> lines = file.openRead()
    .transform(utf8.decoder)       // Decode bytes to UTF-8.
    .transform(LineSplitter());    // Convert stream to individual lines.
  try {
    await for (var line in lines) {
      print('$line: ${line.length} characters');
    }
    print('File is now closed.');
  } catch (e) {
    print('Error: $e');
  }
}

ファイルの書き込み

  • writeAsStringもしくはfile.openWrite()で書き込み用Streamを受け取り、書き込み続ける。最後はsink.close()する事で書き込みStreamを終了する。
  • どちらもデフォルトでは上書き。modeパラメータにFileMode.appendを指定することで追記モードになる。
  • 同期的に書き込みを行うwriteAsStringSyncもある
// writeAsString
  var file = await File(filename).writeAsString('some content');

// writeAsString FileMode.append
newFile.writeAsString('mode: stub\n', mode: FileMode.append);

// openWrite
  var sink = file.openWrite();
  sink.write('FILE ACCESSED ${DateTime.now()}\n');

  // Close the IOSink to free system resources.
  sink.close();

// openWrite FileMode.append
final sink = newFile.openWrite(mode: FileMode.append);

ファイルの作成、削除

  • File.create()で新規ファイルの作成
  • File.delete()でファイルの削除

ファイルの一部を更新する際の戦略

  • 新規ファイルを作成 (await File.create())
  • 更新元ファイルの中身を新規ファイルに一行一行書き込み
  • 更新したい行を分岐処理で書き換え
  • 終了したら更新元ファイルを削除 (await File.delete())
  • 新規ファイルを更新元ファイルの名前に変更 (await File.rename())

🗂️ディレクトリ操作

ディレクトリの作成

Directory.create([ディレクトリ名])

var directory = await Directory('dir/subdir').create(recursive: true)

ディレクトリ内のフォルダを列挙

  • Directory.listもしくはDirectory.listSyncで取得
  • list()はStreamを返す
  • recursiveをtrueで、サブディレクトリも読み込む
  // List directory contents, recursing into sub-directories,
  // but not following symbolic links.
  await for (var entity in
      someDir.list(recursive: true, followLinks: false)) {
    print(entity.path);
  }

🛠️ 他CLIの実行

  • dart:ioに実装されているProcessクラスのrunメソッドもしくはstartで実行可能

run

インタラクションを伴わない処理の実行
https://api.dart.dev/stable/3.1.1/dart-io/Process/run.html

var result = await Process.run('mason', ['version']);
print('${result.stdout}');

start

ユーザー入力などインタラクションを伴うプログラムの実行
https://api.dart.dev/stable/3.1.1/dart-io/Process/start.html

var process = await Process.start('mason',['create','some_brick']);
// プログラムからの出力をそのまま出力
stdout.addStream(process.stdout);
stderr.addStream(process.stderr);

上記によりプログラム(Process)のstdout→呼び出してるプログラムのstdoutとして橋渡ししている。

さらにこのプログラムの入力(stdin)を呼び出し中のプログラムの入力(stdin)にする為には、以下のようにこのプログラムの入力を受け取るストリームを定義→listenし、その値をprocess.stdin.writelnで呼び出し中のプログラムに渡す必要がある

    // Capture lines from user input
    var userStdinStream = stdin.transform(utf8.decoder).transform(const LineSplitter());

    // Listen to Dart's stdin and write to process's stdin
    var subscription = userStdinStream.listen((String line) {
      // Forward the user input to the spawned process's stdin
      process.stdin.writeln(line);
    });
heyhey1028heyhey1028

Masonを組み合わせる

https://www.youtube.com/watch?v=o8B1EfcUisw
https://pub.dev/packages/mason

Mason

プロジェクトに閉じたbrickの運用

  • プロジェクトにbrickディレクトリを作成し、必要なbrickを定義
  • 以下コマンドでmason.yamlに追加
mason add [brick名] --path [brickのパス]

mason.yaml

Brick

brickの作成

  • mason newでbrickを生成
  • -oオプションで出力先を指定可能
mason new example -o bricks

brick.yaml

brick.yaml
name: test_brick
description: A new brick created with the Mason CLI.

version: 0.1.0+1

environment:
  mason: ">=0.1.0-dev.47 <0.1.0"

vars:
  name:
    type: string
    description: Your name
    default: Dash
    prompt: What is your name?

brickを登録

  • mason addで登録
  • 指定したbrickがbrick.yamlに記述される
  • ローカルのbrickを使うなら--pathオプションを使って、パスを指定
mason add [brick名] --path [brickのパス]

brickを使ったファイル生成

  • mason makeでbrickを使ってファイル生成
  • -oオプションで出力先を指定可能
mason make example -o lib

Brickの記法

if文

true: {{#変数名}}〇〇〇〇{{/変数名}}
false: {{^変数名}}✖️✖️✖️✖️{{/変数名}}

{{^publish}}
publish_to: none
{{/publish}}
dependencies:
  flutter:
    sdk: flutter
  {{#useGoogleFonts}}
  google_fonts: latest
  {{/useGoogleFonts}}

ファイル内だけでなく、ファイルやディレクトリ自体も上記記法で囲うことで条件分岐が可能

├── __brick__
│   └── {{#createChangelog}}CHANGELOG.md{{/createChangelog}}

for文

先ほどのif文のtrueの間に{{.}}を挿入することで、その変数の配列の値を繰り返し処理を行う

{{#platforms}}
{{.}}
{{/platforms}}
入力値
{
  "platforms": ["iOS", "Android", "Web"]
}
heyhey1028heyhey1028

CLIのリリース手順

下記コマンドでコンパイル可能

dart compile exe bin/xxxx.dart -o [出力先]

https://dev.to/stack-labs/cli-applications-made-easy-with-dart-dcli-8af

github actionsでコンパイルを自動化

以下記事で紹介されているようなgithub actionsを作成し、アーティファクトを各自ダウンロードしてもらうことでチームに共有。
https://tech.jxpress.net/entry/2019/12/19/150048
https://zenn.dev/yusukeiwaki/articles/dc168f5aba605d

heyhey1028heyhey1028

サブコマンドの設定方法

  • サブコマンドを自前で実装すると結構工数かかりそうな予感
  • VeryGoodVentures社が提供しているvery_good_cliにてdart_cliのテンプレートを作成するコマンドがあり、それにより生成されたcliがサブコマンドなどの実装を既に含んでおり、手っ取り早そう。

https://github.com/VeryGoodOpenSource/very_good_cli/tree/main

  • サブコマンドはargsパッケージのCommandRunnerクラスを使うのが一般的な様子

https://pub.dev/documentation/args/latest/command_runner/CommandRunner-class.html

heyhey1028heyhey1028

パッケージ外でdartファイルを実行する dart pub global activate

処理を記述したdartファイルやcliを、それらが記述されたディレクトリ外で実行したいことがある。その場合に使うのがdart pub global activate.
https://dart.dev/tools/pub/cmd/pub-global

現在登録中のpub globalコマンドの確認

dart pub global list

gitからCLIツールをグローバル化

  • これはhttpsのURLの模様
dart pub global activate --source git <Git URL>

ローカルのCLIツールをグローバル化

  • これはプロジェクトのパス?
dart pub global activate --source path <path>

binディレクトリ内のファイルを実行する設定

  • binディレクトリ内のファイルはpubspec.yamlexecutablesに設定することで初めてコマンドラインから実行可能になる
  • 例えばbin/helloworld.dartがあった場合は以下のように記述
pubspec.yaml
name: helloworld

executables:
  helloworld:

不要なツールのアンインストール

dart pub global deactivate <package>