この記事はDart Advent Calendar 2019の20日目の記事です。
Dart2.6から追加されたdart2nativeを使ったcliツール作成の記事がいくつかありますが、私もcliツールを作成したいと思います。
https://medium.com/dartlang/dart2native-a76c815e6baf
今回はDartの練習のためにも、TODOリストを作成していきます。
mattnさんの https://github.com/mattn/todo をインスパイヤしています
こちらのtodo cliはGoで書かれています。
今回作成するdado
今回はDartのtodoリストということで、dado
という名前にしています。
Usage
❯ bin/dado -h
Usage: dado.dar
-a, --add add task to list
-l, --list watch task list
-d, --done done task
-u, --undone undone task
-r, --remove remove task from list
-c, --clean clean done tasks from list
-h, --help prints usage information
使用したpackage
optionやhelpを簡単に作成するためにも先人が作成したpackageを利用していきます。今回使用するのは以下のpackage。
build_cli
Dartのビルドシステムを使って、引数からアノテーションで指定したオプションを解析していい感じにcliを作りやすくしてくれるpackageです。
https://pub.dev/packages/build_cli
shell
dart:ioのラッパーで、Dartのコード内で、シェルスクリプトの実行を書きやすくるpackageです。今回はDartのAPIにファイルを削除するものがなく?シェルスクリプトでrmコマンドを叩くために使用しています。(あったら教えて下さい)
https://pub.dev/packages/shell
shell packageを使わないファイル削除を追記しました。(2019-12-21)
今回はDartのAPIにファイルを削除するものがなく?シェルスクリプトでrmコマンドを叩くために使用しています。(あったら教えて下さい)
dart:ioにdeleteなるものがありました
@mono0926 さん ありがとうございます
shell packageを使用している以下の部分(done
, undone
, remove
, clean
で使用している)
Shell().start('rm', ['file2.txt']);
newFile.rename('file.txt');
をdeleteメソッドを使って書き直すと以下のような感じになります。
newFile.rename('file.txt').then((file){
newFile.delete(recursive: true);
});
雛形を作成する
stagehandを利用して、dart cliの雛形を作成していきます。
$ stagehand dado
$ pub get
$ dart bin/main.dart
Hello world: 42!
Hello world: 42! が出ると一旦雛形は完成です。
build_cliを適用していきます。
pubspec.yamlを以下のうように設定し、pub get
します。
name: dado
description: A sample command-line application.
environment:
sdk: '>=2.5.0 <3.0.0'
dependencies:
# path: ^1.6.0
build_cli_annotations: ^1.0.0
dev_dependencies:
pedantic: ^1.8.0
test: ^1.6.0
build_cli: ^1.0.0
build_runner: ^1.0.0
end-2-end exampleを参考にDartCliをFlagOptionなどに対応させて行きます。
import 'dart:io';
import 'package:build_cli_annotations/build_cli_annotations.dart';
import 'package:dado/dado.dart';
import 'package:io/io.dart';
import 'package:io/ansi.dart';
part 'main.g.dart';
@CliOptions()
class Options {
@CliOption(abbr: 'add', help: 'add task to list')
String add;
@CliOption(abbr: 'list', help: 'watch task list')
bool list;
@CliOption(abbr: 'done', help: 'done task')
String done;
@CliOption(abbr: 'undone', help: 'undone task')
String undone;
@CliOption(abbr: 'remove', help: 'remove task from list')
String remove;
@CliOption(abbr: 'clean', help: 'clean done tasks from list')
bool clean;
@CliOption(abbr: 'h', negatable: false, help: 'Prints usage information.')
bool help;
Options();
}
void main(List<String> args) {
Options options;
try {
options = parseOptions(args);
} on FormatException catch (e) {
print(red.wrap(e.message));
print('');
_printUsage();
exitCode = ExitCode.usage.code;
return;
}
if (options.help) {
_printUsage();
return;
} else if (options.add != null) {
} else if (options.list) {
} else if (options.done != null) {
} else if (options.undone != null) {
} else if (options.remove != null) {
} else if (options.clean) {
}
}
void _printUsage() {
print('Usage: dado.dar');
print(_$parserForOptions.usage);
}
part of 'main.dart';
Options _$parseOptionsResult(ArgResults result) =>
Options()
..add = result['add'] as String
..list = result['list'] as bool
..done = result['done'] as String
..undone = result['undone'] as String
..remove = result['remove'] as String
..clean = result['clean'] as bool
..help = result['help'] as bool;
ArgParser _$populateOptionsParser(ArgParser parser) => parser
..addOption('add', abbr: 'a', help: 'add task to list')
..addFlag('list', abbr: 'l', help: 'watch task list', negatable: false)
..addOption('done', abbr: 'd', help: 'done task')
..addOption('undone', abbr: 'u', help: 'undone task')
..addOption('remove', abbr: 'r', help: 'remove task from list')
..addFlag('clean', abbr: 'c', help: 'clean done tasks from list', negatable: false)
..addFlag('help', abbr: 'h', help: 'prints usage information', negatable: false);
final _$parserForOptions = _$populateOptionsParser(ArgParser());
Options parseOptions(List<String> args) {
final result = _$parserForOptions.parse(args);
return _$parseOptionsResult(result);
}
一旦、helpメッセージだけ表示できるようになりました。
❯ dart bin/main.dart -h
Usage: dado.dar
-a, --add add task to list
-l, --list watch task list
-d, --done done task
-u, --undone undone task
-r, --remove remove task from list
-c, --clean clean done tasks from list
-h, --help prints usage information
他のオプションをしていしても何も起こりません。
❯ dart bin/main.dart -a hoge
❯ dart bin/main.dart -l
各々のオプションを実装
実装するとbin/main.dart
のコメントの部分にその関数を適用するだけです。
if (options.help) {
_printUsage();
return;
} else if (options.add != null) {
// addの処理
} else if (options.list) {
// listの処理
} else if (options.done != null) {
// doneの処理
} else if (options.undone != null) {
// undoneの処理
} else if (options.remove != null) {
// removeの処理
} else if (options.clean) {
// cleanの処理
}
add
addはタスクをリストに追加する処理です。オプションで指定された文字を受け取り、ストレージとして使用しているfile.txt
に書き込みます。
void add(String task) async {
var file = File('file.txt');
var current = await file.readAsString();
var sink = file.openWrite();
sink.write(current);
sink.write(task + '\n');
await sink.close();
}
list
listはタスク一覧とタスクの状態(done,undone)を見ることができる機能です。
一行ずつ読み込み、-
がついていればdoneマーク、付いていなければundoneマークを付けて表示します。
const undoneMark = '\x1b[32m\u2610\x1b[0m';
const doneMark = '\x1b[31m\u2611\x1b[0m';
void list() {
final file = File('file.txt');
Stream<List<int>> inputStream = file.openRead();
inputStream
.transform(utf8.decoder)
.transform(LineSplitter())
.listen((String line) {
if (line.startsWith('-')) {
line = line.replaceFirst('-', '');
print('$doneMark $line');
} else {
print('$undoneMark $line');
}
},
onDone: () { },
onError: (e) { print(e.toString()); });
}
done
doneは-
が付いていないundoneなtaskをdoneにする処理です。
オプション変数からタスク番号を受け取り、その番号にマッチするタスクに-
を付けることでdoneとしています。
void done(int number) {
File('file2.txt').create(recursive: true);
var newFile = File('file2.txt');
var sink = newFile.openWrite();
final file = File('file.txt');
Stream<List<int>> inputStream = file.openRead();
var cnt = 1;
inputStream
.transform(utf8.decoder)
.transform(LineSplitter())
.listen((String line) {
if (cnt == number && !line.startsWith('-')) {
sink.write('-$line\n');
} else {
sink.write('$line\n');
}
cnt += 1;
},
onDone: () { },
onError: (e) { print(e.toString()); });
Shell().start('rm', ['file2.txt']);
newFile.rename('file.txt');
}
新しくfile2.txt
というファイルを作成し、後にfile.txt
に置き換えることで実装しています。
undone,remove,cleanも同様の実装をしています。
Shell().start
でrm
を実行することによって、ファイル削除をしています。
undone
undoneは-
が付いているdoneなtaskをundoneにする処理です。
オプション変数からタスク番号を受け取り、その番号にマッチするタスクに-
を外すことでdoneとしています。
void undone(int number) {
File('file2.txt').create(recursive: true);
var newFile = File('file2.txt');
var sink = newFile.openWrite();
final file = File('file.txt');
Stream<List<int>> inputStream = file.openRead();
var cnt = 1;
inputStream
.transform(utf8.decoder)
.transform(LineSplitter())
.listen((String line) {
if (cnt == number && line.startsWith('-')) {
var undoneLine = line.replaceFirst('-', '');
sink.write('$undoneLine\n');
} else {
sink.write('$line\n');
}
cnt += 1;
},
onDone: () { },
onError: (e) { print(e.toString()); });
Shell().start('rm', ['file2.txt']);
newFile.rename('file.txt');
}
remove
removeは、TODOリスト自体から指定したタスクを削除するオプションです。
一行ずつ読み込み指定された番号のタスクを書き込まないことで削除しています。
void remove(int number) {
File('file2.txt').create(recursive: true);
var newFile = File('file2.txt');
var sink = newFile.openWrite();
final file = File('file.txt');
Stream<List<int>> inputStream = file.openRead();
var cnt = 1;
inputStream
.transform(utf8.decoder)
.transform(LineSplitter())
.listen((String line) {
var match = cnt == number;
if (!match) {
sink.write('$line\n');
}
cnt += 1;
},
onDone: () { },
onError: (e) { print(e.toString()); });
Shell().start('rm', ['file2.txt']);
newFile.rename('file.txt');
}
clean
cleanはdoneなタスクを一括削除します。
一行ずつ読み込み、-
が付いていれば書き込まないことで実現しています。
void clean() {
File('file2.txt').create(recursive: true);
var newFile = File('file2.txt');
var sink = newFile.openWrite();
final file = File('file.txt');
Stream<List<int>> inputStream = file.openRead();
inputStream
.transform(utf8.decoder)
.transform(LineSplitter())
.listen((String line) {
if (!line.startsWith('-')) {
sink.write('$line\n');
}
},
onDone: () { },
onError: (e) { print(e.toString()); });
Shell().start('rm', ['file2.txt']);
newFile.rename('file.txt');
}
コンパイルして、実行ファイルとして出力する
最後に、dart2native
コマンドを使用することで実行ファイルとして出力できます。
❯ dart2native bin/main.dart -o dado
最後に
今回実装したdadoのリポジトリです。
https://github.com/TaigaMikami/dado