BEMAロゴ

エンジニアの
成長を支援する
技術メディア

【Dartスクリプト入門】たった30行で作るツリービューCLI ─ 手を動かして学ぶ・セットアップ不要

はじめに

こんにちは、株式会社メンバーズ Cross ApplicationOpen in new tab カンパニーの田原です。

Dartと聞くと、多くの人はFlutterを使ったスマホアプリやWebアプリの開発を思い浮かべると思います。確かに、そのパワフルなUIフレームワークはDartの大きな魅力の一つです。しかし、Dartの真の力はそれだけにとどまりません。実は、ちょっとした作業を自動化したり、日々の開発を効率化したりするためのスクリプトをサクッと作るのにも最適な言語です。

この記事では、Dartを使ってツリービュー作成スクリプトを自作します。ファイルやディレクトリの階層構造を美しく色付けして表示します。

スクリプトで出力されたツリービュー

「難しそう」と感じるかもしれませんが、ご安心ください。このスクリプトは、たったの30行ほどのコードで完成します。記事を読み進めながら、実際に手を動かしてみてください。わずかなコードで、ターミナルが華やかに彩られる瞬間に「おお!」となること間違いなしです。

さあ、Dartを使ったスクリプト作成の第一歩を、プロジェクトのセットアップなしで踏み出しましょう。

本記事はmacOS(Macユーザー)を主な対象としています。Windowsなど他のOSをご利用の場合は、一部コマンドやパスの表記で手順の変更や注意が必要となります。

準備

Dart環境の準備

まだDartをインストールしていない方は、Dartの公式サイトから、ご自身のOSに合ったSDKをダウンロードしてください。ここで

スクリプトファイルの作成

ターミナルを開き、スクリプトを作成したいディレクトリに移動します。そこで以下のコマンドを実行して、tree_viewer.dartという新しいファイルを作成してください。

touch tree_viewer.dart

touchコマンドは、指定した名前のファイルがない場合に空のファイルを新規作成するコマンドです。

※WindowsのコマンドプロンプトやPowerShellで実行する場合は、以下の代替コマンドを利用してください。

  • コマンドプロンプトの場合: type nul > tree_viewer.dart
  • PowerShellの場合: New-Item tree_viewer.dart -ItemType File

スクリプトの実装

作成したtree_viewer.dartファイルをVS Codeなどのエディタで開いてください。ここから、一行ずつコードを書いていきます。

必要なライブラリのインポート

まずは、ファイルやディレクトリを操作するために、Dartの標準ライブラリである dart:io をインポートします。

import 'dart:io';

この一行で、ディレクトリの読み込みや、ターミナルへの出力といったCLIツール開発に必要な機能が使えるようになります。

ディレクトリを再帰的に走査する関数を定義する

次に、ツリー構造を再帰的に表示するためのコアとなる関数 printDirectoryTree を定義します。

void printDirectoryTree(Directory directory, String prefix, bool isLast) {
  final list = directory.listSync(followLinks: false);

この関数は、引数として受け取ったディレクトリの中身を listSync メソッドで取得しています。listSync は、ファイルやディレクトリのリストを同期的に(すぐに)返してくれます。

この関数は3つの引数を受け取ります。

  • directory: 走査の対象となるディレクトリです。
  • prefix: 現在の階層のインデントを表現する文字列です。ツリーの「幹」を描画するために使われます。
  • isLast: 親ディレクトリのリストにおいて、このdirectoryが最後の要素であるかどうかを示す真偽値です。ツリーの「枝」の形 (├── または └──) を決定するために使われます。

listSync(followLinks: false) とシンボリックリンクとは?

listSyncの引数であるfollowLinks: false は、シンボリックリンクをたどらないようにする設定です。

シンボリックリンクは、特定のファイルやディレクトリを指し示すショートカットのようなものです。Windowsの「ショートカット」やmacOSの「エイリアス」に近い概念ですが、より深くOSのファイルシステムと結びついています。

例えば、doc/report.pdfというファイルに対して、link_to_report.pdfというシンボリックリンクを作成すると、link_to_report.pdfを操作した際に、実際にはreport.pdfが操作されます。

シンボリックリンクを辿るとどうなるか?

私たちが作成しているツリービューツールは、ディレクトリの中身を再帰的に(つまり、その中にあるディレクトリもすべて)走査します。もし、この走査中にシンボリックリンクを辿る設定にしていた場合、以下のような無限ループが発生する可能性があります。

例えば、dir_aというディレクトリ内に、dir_a自身を指すシンボリックリンクdir_a/linkを作成したとします。

dir_a
└─ link -> dir_a

この状態で再帰的な走査を行うと、プログラムはdir_a -> link -> dir_a -> link…というように、永遠に同じ場所を辿り続ける無限ループに陥り、プログラムが停止しなくなってしまいます。

listSync(followLinks: false)は、このような無限ループや意図しない挙動を防ぐために非常に重要な設定です。この設定により、シンボリックリンクを見つけても、そのリンク先を辿らず、単なるファイルとして扱うため、安全にファイルシステムを探索できます。

ツリーの描画ロジックを実装する

取得したリストをループ処理で一つずつ見ていき、ツリーの枝を描画するロジックを実装します。

for (int i = 0; i < list.length; i++) {
    final entity = list[i];
    final isLastItem = i == list.length - 1;
    final branch = isLastItem ? '└── ' : '├── ';
    final nextPrefix = isLastItem ? '$prefix    ' : '$prefix│   ';

このループでは、listSyncで取得したファイルやディレクトリのリストをiというインデックスを使って一つずつ処理していきます。entity には、リストの各要素が順番に代入されます。

prefixnextPrefix の役割

このコードにおける prefix は、現在の階層のツリーの「幹」を表現するための文字列です。そして nextPrefix は、その prefix を基に、次の階層の「幹」を生成するための変数です。

再帰関数である printDirectoryTree は、階層を進むたびに自分自身を繰り返し呼び出します。このとき、次の呼び出しに「現在の階層の情報」を正しく渡す必要があります。その役割を担うのが prefixnextPrefix なのです。

ツリーの「幹」と「枝」の仕組み

ツリーの構造は、├──└── といった「枝」と、 という「幹」の組み合わせでできています。このコードでは、すべての描画文字を統一するために、「半角4文字」のルールを使っています。

  • ├── (半角4文字): 枝の途中にあるファイルやディレクトリを示します。
  • └── (半角4文字): 枝の最後にあるファイルやディレクトリを示します。
  • (半角4文字): ツリーの幹(縦の線)を示します。
  •     (半角4文字): ツリーの幹が途切れた後のスペースを埋めます。

nextPrefix は、この半角4文字のルールに従って、次の階層の「幹」を生成します。

final nextPrefix = isLastItem ? '$prefix    ' : '$prefix│   ';

isLastItemtrue(現在の要素が最後)の場合、ツリーの幹はそこで途切れるため、prefix に半角4文字のスペース ' ' を加えて、次の階層のインデントを調整しています。一方、isLastItemfalse(最後の要素ではない)場合は、prefix に半角4文字の'│ ' を加えて、ツリーの幹を伸ばします。

このように、prefixnextPrefix は、半角4文字のルールで統一された文字列を使って、再帰的にツリーのインデントを正確に表現しているのです。

ファイルとディレクトリを区別して出力する

次に、リスト内の要素がファイルなのか、それともディレクトリなのかを判別し、それぞれに応じた処理を行います。

if (entity is Directory) {
      stdout.write('$prefix$branch\x1b[34m${entity.path.split('/').last}\x1b[0m\n');
      printDirectoryTree(entity, nextPrefix, isLastItem);
    } else {
      stdout.write('$prefix$branch\x1b[32m${entity.path.split('/').last}\x1b[0m\n');
    }
  }
}
  1. if (entity is Directory) でディレクトリかどうかを判定します。
  2. ディレクトリの場合は、ファイル名の色を青色 (\x1b[34m) にして出力し、printDirectoryTree 関数を再帰的に呼び出して次の階層に進みます。
  3. ファイルの場合は、ファイル名の色を緑色 (\x1b[32m) にして出力します。

上記コードのentity.path.split('/')の部分では、macOS/Linuxで使われるパスの区切り文字/を前提としています。Windowsではパスの区切り文字は通常\です。

\x1b[...mANSIエスケープシーケンスと呼ばれ、ターミナルに色を付けるための特殊な文字列です。

プログラムのエントリーポイントを作成する

最後に、プログラムの実行開始地点となる main 関数を作成します。

void main(List<String> arguments) {
  final path = arguments.isNotEmpty ? arguments[0] : '.';
  final directory = Directory(path);
  if (!directory.existsSync()) {
    print('Directory not found: $path');
    return;
  }
  print('\x1b[34m$path\x1b[0m');
  printDirectoryTree(directory, '', true);
}
  • main 関数は、コマンドライン引数 (arguments) を受け取ります。
  • arguments.isNotEmpty で引数が存在するかをチェックし、もしあればそのパスを、なければ現在のディレクトリ (.) を対象とします。
  • 最後に、printDirectoryTree 関数を呼び出してツリーの描画を開始します。

これで、30行のコードが完成しました。以下にtree_viewer.dartのコード全体を示します。

import 'dart:io';
void printDirectoryTree(Directory directory, String prefix, bool isLast) {
  final list = directory.listSync(followLinks: false);
  for (int i = 0; i < list.length; i++) {
    final entity = list[i];
    final isLastItem = i == list.length - 1;
    final branch = isLastItem ? '└── ' : '├── ';
    final nextPrefix = isLastItem ? '$prefix    ' : '$prefix│   ';
    if (entity is Directory) {
      stdout.write(
        '$prefix$branch\x1b[34m${entity.path.split('/').last}\x1b[0m\n',
      );
      printDirectoryTree(entity, nextPrefix, isLastItem);
    } else {
      stdout.write(
        '$prefix$branch\x1b[32m${entity.path.split('/').last}\x1b[0m\n',
      );
    }
  }
}
void main(List<String> arguments) {
  final path = arguments.isNotEmpty ? arguments[0] : '.';
  final directory = Directory(path);
  if (!directory.existsSync()) {
    print('Directory not found: $path');
    return;
  }
  print('\x1b[34m$path\x1b[0m');
  printDirectoryTree(directory, '', true);
}

スクリプトの実行

それでは、作成したスクリプトを実行してみます。tree_viewer.dartを保存したディレクトリに移動して、ターミナルで以下のコマンドを入力して実行します。

dart run tree_viewer.dart
ターミナルのスクリーンショット(カレントディレクトリ)

このコマンドは引数を指定していないため、カレントディレクトリ(./)のツリー構造が出力されます。

引数を指定した実行

次に、スクリプト実行時に引数..を指定して、親ディレクトリを起点としたディレクトリ構造を出力してみます。

dart run tree_viewer.dart ..
ターミナルのスクリーンショット(親ディレクトリ指定)

このコマンドは、現在のディレクトリの親ディレクトリ(../)を対象とします。ネストした構造が綺麗にツリー状に出力されているのが確認できるはずです。また、ANSIエスケープシーケンスで指定したファイル名とディレクトリ名の表示色も反映されています。

まとめ

今回のスクリプトは非常にシンプルですが、これを応用することで、さらに多機能なツールへと発展させることができます。

DartはモバイルやWebだけでなく、日々の開発を効率化するスクリプト作成においても非常にパワフルな選択肢です。ぜひ、あなただけの便利なツール作りに挑戦してみてください。

最後までお読みいただき、ありがとうございました!

この記事が役に立ったと思ったら、
ぜひ「いいね」とシェアをお願いします!

リンクをコピーXでシェアするfacebookでシェアする

この記事を書いた人

田原 葉
田原 葉
2024年にメンバーズに中途で入社。前職はiOSエンジニア。現在はCross ApplicationカンパニーでFlutter技術をメインにモバイルアプリ開発支援を担当。
詳しく見る
ページトップへ戻る