跳到主要内容

编写命令行应用

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

使用独立 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.8.0
8 
9 # Add regular dependencies here.
10 dependencies:
11   args: ^2.7.0
12   # path: ^1.8.0
13 
14 dev_dependencies:
15   lints: ^6.0.0
16   test: ^1.25.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 库的顶层定义

Stream描述
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() 异步打印 stream 中的所有元素。

stdout 提供的功能比 print() 函数更多。例如,您可以使用 stdout 显示 stream 的内容。但是,对于在 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 块等待文件被异步读取和解码。当数据在 stream 上可用时,应用将其打印到 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。否则,mode 默认为 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 的副本。如果您需要一个可变 Map(可修改副本),可以使用 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消耗 stream 中数据的对象的辅助类
File表示本地文件系统中的文件
Directory表示本地文件系统中的目录
FileSystemEntityFile 和 Directory 的超类
Platform提供有关机器和操作系统的信息
stdout标准输出流
stderr标准错误流
stdin标准输入流
exitCode访问和设置退出码
exit()设置退出码并退出

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

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

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

下一步?

#

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