DartでCLIツールを作る

dcli

このパッケージでは「ツールとして使用する」「パッケージとして使用する」と言う2つの選択肢がある模様👀
DCli toolsのインストール
dart pub global activate dcli
dcli install
んー、なんかうまくいかんな。。。
dcli toolsの設定がうまくいかないので、単純に
-
dart create
でCLIプロジェクトを作成 - pubspec.yamlにdcliを追加
- プロジェクトのルートで
dart run
を実行することで走らせる

やりたいこと
- ファイルの特定部位の書き換え
- 複数ファイルの特定

Dartの入出力
入出力関連のライブラリはこちら
関連クラス
File, Directory, and Link
Fileクラス:
Directoryクラス:
FileSystemEntityクラス:File, Directoryクラスが継承する親クラス
Standard output, error, and input streams
stdout:
stderr:
stdin: 標準入力用クラス。stdin.readLineSync()
で入力値を取得。
✏️ 標準入力
StringBufferクラス
📄ファイル操作
ファイルの読み込み
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
インタラクションを伴わない処理の実行
var result = await Process.run('mason', ['version']);
print('${result.stdout}');
start
ユーザー入力などインタラクションを伴うプログラムの実行
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);
});

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
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"]
}

CLIのリリース手順
下記コマンドでコンパイル可能
dart compile exe bin/xxxx.dart -o [出力先]
github actionsでコンパイルを自動化
以下記事で紹介されているようなgithub actionsを作成し、アーティファクトを各自ダウンロードしてもらうことでチームに共有。

サブコマンドの設定方法
- サブコマンドを自前で実装すると結構工数かかりそうな予感
- VeryGoodVentures社が提供している
very_good_cli
にてdart_cliのテンプレートを作成するコマンドがあり、それにより生成されたcliがサブコマンドなどの実装を既に含んでおり、手っ取り早そう。
- サブコマンドは
args
パッケージのCommandRunner
クラスを使うのが一般的な様子

dart pub global activate
パッケージ外でdartファイルを実行する 処理を記述したdartファイルやcliを、それらが記述されたディレクトリ外で実行したいことがある。その場合に使うのがdart pub global activate
.
現在登録中の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.yaml
のexecutables
に設定することで初めてコマンドラインから実行可能になる - 例えば
bin/helloworld.dart
があった場合は以下のように記述
name: helloworld
executables:
helloworld:
不要なツールのアンインストール
dart pub global deactivate <package>

参考にしたCLI
- firebase-tools : https://github.com/firebase/firebase-tools
- flutterfire cli: https://github.com/invertase/flutterfire_cli
- grinder: https://github.com/google/grinder.dart
- mason: https://github.com/felangel/mason
- very_good_cli: https://github.com/VeryGoodOpenSource/very_good_cli
- dcli: https://github.com/onepub-dev/dcli

flutterfire cli
flutterfire cliには裏コマンドとして--yes
がある。ありがてぇえ!!!!

very_good_cliを使ったCLIテンプレートでの開発
上記のCLIを使うことでdart cliのテンプレートを使うことができる。そちらが機能が豊富でCLI開発の体験が良いので、そちらを用いた開発方法をここに明記。