跳到主要內容

編寫命令列應用

本教程教您如何構建命令列應用,並展示了一些小型命令列應用。這些程式使用了大多數命令列應用所需的資源,包括標準輸出、錯誤和輸入流,命令列引數,檔案和目錄等。

使用獨立 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 示例。

下一步?

#

如果您對伺服器端程式設計感興趣,請檢視下一教程