內容

撰寫命令列應用程式

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

使用獨立的 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.3.0
8 
9 # Add regular dependencies here.
10 dependencies:
11   args: ^2.4.2
12   # path: ^1.8.0
13 
14 dev_dependencies:
15   lints: ^3.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 顯示串流的內容。不過,對於在網路上執行的應用程式,您必須使用 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() 方法會從標準輸入串流讀取文字,並在使用者輸入文字並按下 Enter 鍵之前進行封鎖。這個簡短程式碼會列印出輸入的文字。

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 區塊會等待檔案非同步讀取和解碼。當資料在串流中可用時,應用程式會將其列印到標準輸出。

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 範例。

接下來呢?

#

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