跳到主要内容

编写命令行应用

本教程教您如何构建命令行应用,并向您展示一些小型命令行应用程序。这些程序使用大多数命令行应用程序需要的资源,包括标准输出、错误和输入流、命令行参数、文件和目录等等。

使用独立的 Dart VM 运行应用

#

要在 Dart VM 中运行命令行应用,请使用 dart rundart 命令包含在 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 addargs 包添加为依赖项。这会将 args 添加到 pubspec.yaml 文件中找到的依赖项列表中。

    $ 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.7.0
8 
9 # Add regular dependencies here.
10 dependencies:
11   args: ^2.5.0
12   # path: ^1.8.0
13 
14 dev_dependencies:
15   lints: ^5.0.0
16   test: ^1.24.0

此命令显示指定文件的每一行。由于您指定了 -n 选项,因此每行前面都会显示行号。

解析命令行参数

#

args 包 提供了用于将命令行参数转换为一组选项、标志和附加值的解析器支持。按如下方式导入包的 args 库

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

args 库包含以下类,以及其他类

描述
ArgParser一个命令行参数解析器。
ArgResults使用 ArgParser 解析命令行参数的结果。

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

您可以通过名称访问标志和选项,将 ArgResults 视为 Map。您可以使用 rest 属性访问其他值。

args 库的 API 参考 提供了详细信息,以帮助您使用 ArgParserArgResults 类。

使用 stdin、stdout 和 stderr 进行读写

#

与其他语言一样,Dart 具有标准输出、标准错误和标准输入流。标准 I/O 流在 dart:io 库的顶层定义

描述
stdout标准输出
stderr标准错误
stdin标准输入

按如下方式导入 dart:io

dart
import 'dart:io';

stdout

#

dcat 应用中的以下代码将行号写入 stdout(如果指定了 -n 选项),然后写入文件中行的内容。

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

write()writeln() 方法接受任何类型的对象,将其转换为字符串并打印出来。writeln() 方法还会打印一个换行符。dcat 应用使用 write() 方法打印行号,以便行号和文本显示在同一行。

您还可以使用 writeAll() 方法打印对象列表,或使用 addStream() 异步打印流中的所有元素。

stdout 提供的功能比 print() 函数更多。例如,您可以使用 stdout 显示流的内容。但是,对于在 web 上运行的应用,您必须使用 print() 而不是 stdout

stderr

#

使用 stderr 将错误消息写入控制台。标准错误流具有与 stdout 相同的方法,并且您以相同的方式使用它。尽管 stdoutstderr 都打印到控制台,但它们的输出是分开的,并且可以在命令行中或以编程方式重定向或管道传输到不同的目标。

dcat 应用中的以下代码在用户尝试输出目录而不是文件的行时打印错误消息。

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

stdin

#

标准输入流通常从键盘同步读取数据,尽管它可以异步读取并从另一个程序的标准输出中获取管道输入。

这是一个从 stdin 读取单行的小程序

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 返回值。

FileSystemEntityFileDirectoryLink 类的超类。

读取文件

#

dcat 应用使用 openRead() 方法打开命令行上列出的每个文件,该方法返回一个 Streamawait 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 块中可用。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 提供了其他有用的属性,可以提供有关机器、操作系统和当前正在运行的应用的信息。例如

设置退出代码

#

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() 函数可以调用 exit(2) 而不是将 exitCode 设置为 2,但 exit() 会退出程序,并且可能无法处理运行命令指定的所有文件。

虽然您可以为退出代码使用任何数字,但按照惯例,下表中的代码具有以下含义

代码含义
0成功
1警告
2错误

总结

#

本教程介绍了 dart:io 库中以下类中的一些基本 API

API描述
IOSink用于从流中消费数据的对象的辅助类
File表示本地文件系统上的文件
Directory表示本地文件系统上的目录
FileSystemEntityFile 和 Directory 的超类
Platform提供有关机器和操作系统的信息
stdout标准输出流
stderr标准错误流
stdin标准输入流
exitCode访问和设置退出代码
exit()设置退出代码并退出

此外,本教程还介绍了 package:args 中的两个类,它们有助于解析和使用命令行参数:ArgParserArgResults

有关更多类、函数和属性,请查阅 dart:iodart:convertpackage:args 的 API 文档。

有关命令行应用的另一个示例,请查看 command_line 示例。

下一步是什么?

#

如果您对服务器端编程感兴趣,请查看 下一个教程