跳到主要內容

撰寫命令列應用程式

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

使用獨立 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` 套件新增為相依性。這會將 `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 參考提供詳細資訊,以協助您使用 `ArgParser` 和 `ArgResults` 類別。

使用 stdin、stdout 和 stderr 讀取和寫入

#

與其他語言一樣,Dart 具有標準輸出、標準錯誤和標準輸入 streams。標準 I/O streams 在 `dart:io` 函式庫的最上層定義

Stream描述
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()` 非同步印出 stream 中的所有元素。

`stdout` 提供比 `print()` 函數更多的功能。例如,您可以使用 `stdout` 顯示 stream 的內容。但是,對於在網路上執行的應用程式,您必須使用 `print()` 而不是 `stdout`。

stderr

#

使用 `stderr` 將錯誤訊息寫入主控台。標準錯誤 stream 具有與 `stdout` 相同的方法,並且您以相同的方式使用它。雖然 `stdout` 和 `stderr` 都會印出到主控台,但它們的輸出是分開的,並且可以在命令列或透過程式以不同方式重新導向或管線化。

`dcat` 應用程式中的以下程式碼會在使用者嘗試輸出目錄的行而不是檔案時印出錯誤訊息。

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

stdin

#

標準輸入 stream 通常從鍵盤同步讀取資料,儘管它可以非同步讀取,並從另一個程式的標準輸出管線輸入。

這是一個從 `stdin` 讀取單行的小程式

dart
import 'dart:io';

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

`readLineSync()` 方法從標準輸入 stream 讀取文字,並封鎖直到使用者輸入文字並按下 Return 鍵。這個小程式會印出輸入的文字。

在 `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` 是 `File`、`Directory` 和 `Link` 類別的父類別。

讀取檔案

#

`dcat` 應用程式使用 `openRead()` 方法開啟命令列上列出的每個檔案,該方法會傳回 `Stream`。`await 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`。否則,模式預設為 `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協助程式類別,適用於從 streams 取用資料的物件
File代表原生檔案系統上的檔案
Directory代表原生檔案系統上的目錄
FileSystemEntityFile 和 Directory 的父類別
Platform提供關於機器和作業系統的資訊
stdout標準輸出 stream
stderr標準錯誤 stream
stdin標準輸入 stream
exitCode存取和設定結束代碼
exit()設定結束代碼並退出

此外,本教學課程涵蓋了 `package:args` 中的兩個類別,可協助剖析和使用命令列引數:`ArgParser``ArgResults`

如需更多類別、函數和屬性,請查閱 `dart:io``dart:convert``package:args` 的 API 文件。

如需命令列應用程式的另一個範例,請查看 `command_line` 範例。

接下來呢?

#

如果您對伺服器端程式設計感興趣,請查看下一個教學課程