目次

コマンドラインアプリの作成

このチュートリアルでは、コマンドラインアプリの構築方法を学び、いくつかの小さなコマンドラインアプリケーションを紹介します。これらのプログラムは、標準出力、標準エラー、標準入力ストリーム、コマンドライン引数、ファイルとディレクトリなど、ほとんどのコマンドラインアプリケーションに必要なリソースを使用しています。

スタンドアロンDart VMによるアプリの実行

#

Dart VMでコマンドラインアプリを実行するには、dart runを使用します。dartコマンドはDart SDKに含まれています。

小さなプログラムを実行してみましょう。

  1. このコードを含むhello_world.dartというファイルを作成します

    dart
    void main() {
      print('Hello, World!');
    }
  2. 作成したファイルを含むディレクトリで、プログラムを実行します

    $ dart run hello_world.dart
    Hello, World!

Dartツールは多くのコマンドとオプションをサポートしています。一般的に使用されるコマンドとオプションを表示するには、dart --helpを使用します。すべてのオプションを表示するには、dart --verboseを使用します。

dcatアプリコードの概要

#

このチュートリアルでは、コマンドラインで指定されたファイルの内容を表示するdcatという小さなサンプルアプリの詳細について説明します。このアプリは、コマンドラインアプリで使用可能なさまざまなクラス、関数、プロパティを使用しています。チュートリアルを進めて、アプリの各部分と使用されているさまざまなAPIについて学びましょう。

dart
import 'dart:convert';
import 'dart:io';

import 'package:args/args.dart';

const lineNumber = 'line-number';

void main(List<String> arguments) {
  exitCode = 0; // Presume success
  final parser = ArgParser()..addFlag(lineNumber, negatable: false, abbr: 'n');

  ArgResults argResults = parser.parse(arguments);
  final paths = argResults.rest;

  dcat(paths, showLineNumbers: argResults[lineNumber] as bool);
}

Future<void> dcat(List<String> paths, {bool showLineNumbers = false}) async {
  if (paths.isEmpty) {
    // No files provided as arguments. Read from stdin and print each line.
    await stdin.pipe(stdout);
  } else {
    for (final path in paths) {
      var lineNumber = 1;
      final lines = utf8.decoder
          .bind(File(path).openRead())
          .transform(const LineSplitter());
      try {
        await for (final line in lines) {
          if (showLineNumbers) {
            stdout.write('${lineNumber++} ');
          }
          stdout.writeln(line);
        }
      } catch (_) {
        await _handleError(path);
      }
    }
  }
}

Future<void> _handleError(String path) async {
  if (await FileSystemEntity.isDirectory(path)) {
    stderr.writeln('error: $path is a directory');
  } else {
    exitCode = 2;
  }
}

依存関係の取得

#

dcatはargsというパッケージに依存していることに気付くかもしれません。argsパッケージを取得するには、pubパッケージマネージャーを使用します。

実際のアプリには、テスト、ライセンスファイル、依存関係ファイル、例などが含まれています。しかし、最初のアプリでは、dart createコマンドを使用して必要なものだけを簡単に作成できます。

  1. ディレクトリ内で、dartツールを使用してdcatアプリを作成します。

    $ dart create dcat
  2. 作成したディレクトリに移動します。

    $ cd dcat
  3. dcatディレクトリ内で、dart pub addを使用してargsパッケージを依存関係として追加します。これにより、pubspec.yamlファイルにある依存関係のリストにargsが追加されます。

    $ dart pub add args
  4. bin/dcat.dartファイルを開き、上記のコードをコピーします。

dcatの実行

#

アプリの依存関係が揃ったら、コマンドラインから任意のテキストファイル(例:pubspec.yaml)に対してアプリを実行できます。

$ dart run bin/dcat.dart -n pubspec.yaml
1 name: dcat
2 description: A sample command-line application.
3 version: 1.0.0
4 # repository: https://github.com/my_org/my_repo
5 
6 environment:
7   sdk: ^3.5.3
8 
9 # Add regular dependencies here.
10 dependencies:
11   args: ^2.5.0
12   # path: ^1.8.0
13 
14 dev_dependencies:
15   lints: ^4.0.0
16   test: ^1.24.0

このコマンドは、指定されたファイルの各行を表示します。-nオプションを指定したため、各行の前に行番号が表示されます。

コマンドライン引数の解析

#

argsパッケージは、コマンドライン引数をオプション、フラグ、追加値のセットに変換するためのパーサーサポートを提供します。argsライブラリを次のようにインポートします

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

argsライブラリには、他にもいくつかのクラスが含まれています。

クラス説明
ArgParserコマンドライン引数パーサー。
ArgResultsArgParserを使用してコマンドライン引数を解析した結果。

dcatアプリの次のコードでは、これらのクラスを使用して、指定されたコマンドライン引数を解析して格納します。

dart
void main(List<String> arguments) {
  exitCode = 0; // Presume success
  final parser = ArgParser()..addFlag(lineNumber, negatable: false, abbr: 'n');

  ArgResults argResults = parser.parse(arguments);
  final paths = argResults.rest;

  dcat(paths, showLineNumbers: argResults[lineNumber] as bool);
}

Dartランタイムは、コマンドライン引数を文字列のリストとしてアプリのmain関数に渡します。ArgParser-nオプションを解析するように構成されています。次に、コマンドライン引数を解析した結果がargResultsに格納されます。

次の図は、上記で使用されたdcatコマンドラインがArgResultsオブジェクトにどのように解析されるかを示しています。

Run dcat from the command-line

MapのようにArgResultsを扱うことで、名前でフラグとオプションにアクセスできます。他の値には、restプロパティを使用してアクセスできます。

argsライブラリのAPIリファレンスには、ArgParserクラスとArgResultsクラスの使用に役立つ詳細な情報が記載されています。

stdin、stdout、stderrによる読み書き

#

他の言語と同様に、Dartには標準出力、標準エラー、標準入力ストリームがあります。標準I/Oストリームは、dart:ioライブラリの最上位レベルで定義されています。

ストリーム説明
stdout標準出力
stderr標準エラー
stdin標準入力

dart:ioライブラリを次のようにインポートします。

dart
import 'dart:io';

stdout

#

dcatアプリの以下のコードは、(-nオプションが指定されている場合)行番号をstdoutに出力し、その後に行の内容を出力します。

dart
if (showLineNumbers) {
  stdout.write('${lineNumber++} ');
}
stdout.writeln(line);

write()メソッドとwriteln()メソッドは、任意の型のオブジェクトを受け取り、それを文字列に変換して出力します。writeln()メソッドは改行文字も出力します。dcatアプリは行番号を出力するためにwrite()メソッドを使用しているので、行番号とテキストは同じ行に表示されます。

オブジェクトのリストを出力するにはwriteAll()メソッドを使用することも、ストリームからすべての要素を非同期的に出力するにはaddStream()メソッドを使用することもできます。

stdoutprint()関数よりも多くの機能を提供します。たとえば、stdoutを使用してストリームの内容を表示できます。ただし、ウェブ上で実行されるアプリの場合は、stdoutではなくprint()を使用する必要があります。

stderr

#

エラーメッセージをコンソールに出力するにはstderrを使用します。標準エラーストリームはstdoutと同じメソッドを持ち、同じように使用します。stdoutstderrの両方ともコンソールに出力しますが、それらの出力は別々であり、コマンドラインまたはプログラムで異なる宛先にリダイレクトまたはパイプできます。

dcatアプリの以下のコードは、ユーザーがファイルではなくディレクトリの行を出力しようとすると、エラーメッセージを出力します。

dart
if (await FileSystemEntity.isDirectory(path)) {
  stderr.writeln('error: $path is a directory');
} else {
  exitCode = 2;
}

stdin

#

標準入力ストリームは通常、キーボードからデータを同期的に読み取りますが、非同期的に読み取ったり、別のプログラムの標準出力からパイプされた入力を受け取ったりすることもできます。

stdinから1行を読み取る小さなプログラムを次に示します。

dart
import 'dart:io';

void main() {
  stdout.writeln('Type something');
  final input = stdin.readLineSync();
  stdout.writeln('You typed: $input');
}

readLineSync()メソッドは標準入力ストリームからテキストを読み取り、ユーザーがテキストを入力してリターンキーを押すまでブロックします。この小さなプログラムは入力されたテキストを出力します。

dcatアプリでは、ユーザーがコマンドラインにファイル名を指定しない場合、プログラムは代わりにpipe()メソッドを使用してstdinから読み取ります。pipe()は非同期であるため(このコードはその戻り値を使用しませんが、Futureを返します)、それを呼び出すコードはawaitを使用します。

dart
await stdin.pipe(stdout);

この場合、ユーザーはテキストの行を入力し、アプリはその行をstdoutにコピーします。Control+D(またはWindowsではControl+Z)を押すと、入力の終了を知らせます。

$ dart run bin/dcat.dart
The quick brown fox jumps over the lazy dog.
The quick brown fox jumps over the lazy dog.

ファイルに関する情報の取得

#

dart:ioライブラリのFileSystemEntityクラスは、ファイルシステムの検査と操作に役立つプロパティと静的メソッドを提供します。

たとえば、パスがある場合、FileSystemEntityクラスのtype()メソッドを使用して、そのパスがファイル、ディレクトリ、リンク、または見つからないものかどうかを判断できます。type()メソッドはファイルシステムにアクセスするため、非同期的にチェックを実行します。

dcatアプリの以下のコードは、FileSystemEntityを使用して、コマンドラインで指定されたパスがディレクトリかどうかを判断します。返されたFutureは、そのパスがディレクトリかどうかを示すブール値で完了します。チェックが非同期であるため、コードはawaitを使用してisDirectory()を呼び出します。

dart
if (await FileSystemEntity.isDirectory(path)) {
  stderr.writeln('error: $path is a directory');
} else {
  exitCode = 2;
}

FileSystemEntityクラスの他の興味深いメソッドには、isFile()exists()stat()delete()rename()などがあり、これらもすべてFutureを使用して値を返します。

FileSystemEntityは、FileDirectoryLinkクラスのスーパークラスです。

ファイルの読み込み

#

dcatアプリは、openRead()メソッドを使用してコマンドラインにリストされている各ファイルを開き、これはStreamを返します。await forブロックは、ファイルが非同期的に読み取られデコードされるのを待ちます。データがストリームで利用可能になると、アプリはそれをstdoutに出力します。

dart
for (final path in paths) {
  var lineNumber = 1;
  final lines = utf8.decoder
      .bind(File(path).openRead())
      .transform(const LineSplitter());
  try {
    await for (final line in lines) {
      if (showLineNumbers) {
        stdout.write('${lineNumber++} ');
      }
      stdout.writeln(line);
    }
  } catch (_) {
    await _handleError(path);
  }
}

以下は、コードの残りの部分を強調したもので、await forブロックで利用可能になる前にデータを変換する2つのデコーダーを使用しています。UTF8デコーダーはデータをDart文字列に変換します。LineSplitterは改行でデータを分割します。

dart
for (final path in paths) {
  var lineNumber = 1;
  final lines = utf8.decoder
      .bind(File(path).openRead())
      .transform(const LineSplitter());
  try {
    await for (final line in lines) {
      if (showLineNumbers) {
        stdout.write('${lineNumber++} ');
      }
      stdout.writeln(line);
    }
  } catch (_) {
    await _handleError(path);
  }
}

dart:convertライブラリはこれらと他のデータコンバーター(JSON用のコンバーターを含む)を提供します。これらのコンバーターを使用するには、dart:convertライブラリをインポートする必要があります。

dart
import 'dart:convert';

ファイルへの書き込み

#

テキストをファイルに書き込む最も簡単な方法は、Fileオブジェクトを作成してwriteAsString()メソッドを使用することです。

dart
final quotes = File('quotes.txt');
const stronger = 'That which does not kill us makes us stronger. -Nietzsche';

await quotes.writeAsString(stronger, mode: FileMode.append);

writeAsString()メソッドはデータを非同期的に書き込みます。書き込み前にファイルを開き、完了時にファイルを閉じます。既存のファイルにデータを追加するには、オプションの命名パラメーターmodeを使用し、その値をFileMode.appendに設定できます。それ以外の場合は、モードはFileMode.writeにデフォルトになり、ファイルの以前の内容(存在する場合)は上書きされます。

より多くのデータを書きたい場合は、書き込みのためにファイルを開くことができます。openWrite()メソッドはIOSinkを返し、これはstdinとstderrと同じ型です。openWrite()から返されたIOSinkを使用すると、完了するまでファイルに書き込み続け、その時点でファイルを閉じなければなりません。close()メソッドは非同期であり、Futureを返します。

dart
final quotes = File('quotes.txt').openWrite(mode: FileMode.append);

quotes.write("Don't cry because it's over, ");
quotes.writeln('smile because it happened. -Dr. Seuss');
await quotes.close();

環境情報の取得

#

Platformクラスを使用して、アプリが実行されているマシンとオペレーティングシステムに関する情報を取得します。

静的Platform.environmentプロパティは、環境変数の複製を不変のマップで提供します。変更可能なマップ(変更可能なコピー)が必要な場合は、Map.of(Platform.environment)を使用できます。

dart
final envVarMap = Platform.environment;

print('PWD = ${envVarMap['PWD']}');
print('LOGNAME = ${envVarMap['LOGNAME']}');
print('PATH = ${envVarMap['PATH']}');

Platformは、マシン、OS、現在実行中のアプリに関する情報を提供する他の便利なプロパティを提供します。たとえば

終了コードの設定

#

dart:ioライブラリは、トップレベルのプロパティexitCodeを定義しており、これを使用してDart VMの現在の呼び出しの終了コードを設定できます。終了コードとは、Dartアプリから親プロセスに渡される数値で、アプリの実行の成功、失敗、またはその他の状態を示します。

dcatアプリは、_handleError()関数で終了コードを設定して、実行中にエラーが発生したことを示します。

dart
Future<void> _handleError(String path) async {
  if (await FileSystemEntity.isDirectory(path)) {
    stderr.writeln('error: $path is a directory');
  } else {
    exitCode = 2;
  }
}

終了コード2は、アプリでエラーが発生したことを示します。

exitCodeを使用する代わりに、トップレベルのexit()関数を使用できます。これは終了コードを設定し、アプリをすぐに終了します。たとえば、_handleError()関数は、exitCodeを2に設定する代わりにexit(2)を呼び出すことができますが、exit()はプログラムを終了し、実行中のコマンドで指定されたすべてのファイルを処理できない可能性があります。

終了コードには任意の数値を使用できますが、慣例により、以下の表のコードには次の意味があります。

コード意味
0成功
1警告
2エラー

まとめ

#

このチュートリアルでは、dart:ioライブラリの以下のクラスにある基本的なAPIについて説明しました。

API説明
IOSinkストリームからデータを読み取るオブジェクトのヘルパークラス
Fileネイティブファイルシステム上のファイルを表現します
Directoryネイティブファイルシステム上のディレクトリを表します
FileSystemEntityFileとDirectoryのスーパークラス
Platformマシンとオペレーティングシステムに関する情報を提供します
stdout標準出力ストリーム
stderr標準エラーストリーム
stdin標準入力ストリーム
exitCode終了コードへのアクセスと設定
exit()終了コードを設定して終了します

さらに、このチュートリアルでは、コマンドライン引数の解析と使用に役立つpackage:argsの2つのクラス、ArgParserArgResultsについて説明しました。

クラス、関数、プロパティの詳細については、dart:iodart:convertpackage:argsのAPIドキュメントを参照してください。

コマンドラインアプリの別の例については、command_lineサンプルを確認してください。

次のステップ

#

サーバーサイドプログラミングに興味がある場合は、次のチュートリアルを確認してください。