CLIのプログレスバーを作ってみた

## はじめに

皆さん、こんにちは。株式会社BTMの風間と申します。

今回はCLIで動くプログレスバーを作成してみました。
実際に作ったのはこちら。

![progressbar_animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3826118/6caf04ef-c5b2-864b-2d4f-bad5ed6f6801.gif)

コンソール（ターミナル）で、yum、apt、brewなどを利用したパッケージのインストールをされたことがある方は、こういった進行状況の表示をよく見ていると思います。

この進行状況の表示方法に以前から興味があったので、実際に作ってみて、使った方法をまとめてみました。

:::note info
本記事で紹介する実装・実行は以下の環境にて行いました。
・Windows11
・WSL2（Ubuntu 22.04.3 LTS）
・Node.js 20.11.1
・Git Bash（推奨ウインドウサイズ Columns:80 x Rows:20）
:::

## 実際のソース

```javascript
// 実際の処理をsleepで代用
const sleep = async (ms) => new Promise(resolve => setTimeout(() => resolve(), ms));

// プログレスバーの処理
// 複数あるファイルの処理の進捗状況を表示する
const progressBar = async () => {

    const maxFileNum = 10; // 処理するファイルの最大数

    for (let i = 0; i <= maxFileNum; i++) {

        // 1行目に[処理済みのファイル数/全ファイル数]を表示する
        process.stdout.write(`[${i}/${maxFileNum}]\n`); 

        // 2行目に処理しているファイル名を表示する。今回は適当な名前を表示。
        process.stdout.write(`ファイル${i}\n`);

        // 3行目に処理しているファイルの進捗状況を表示する（ここでは処理開始前の状態）
        process.stdout.write('[--------------------]');

        // キャリッジリターンで3行目の先頭に戻り、処理の進行に合わせて#を表示する
        process.stdout.write('\r[');
        for (let j = 0; j < 20; j++) {
            process.stdout.write('#');
            await sleep(100); // 処理
        }

        // ファイルが残っている場合は行先頭に戻り、ANSIエスケープシーケンスを利用して2行上に移動する
        if (i !== maxFileNum) {
            process.stdout.write("\r\x1b[2A");
        }
    }

    process.stdout.write("\ncomplete\n");
};

progressBar();

```

### 実行方法

git bash上で、コマンド「node 上記ソースのファイル」を実行します。
例）上記のソースが~/index.jsに保存してある場合、git bash上で以下のコマンドを実行

```shell
node ~/index.js
```

## 今回やったこと

### その１ 基本

今回作ったプログレスバーは、コンソール（ターミナル）に文字を出力して表現しています。
1行目を表示、2行目を表示、3行目を表示して、1行目からまた表示ということを繰り返し行っています。
3行目の表示は最初に全体の長さを表示し、処理の進行に合わせてそれを#で上書きし、プログレスバーのように見せています。
3行目の文字の上書きや1行目に戻るといった文字の表示位置の制御を次の方法で行っています。

### その２ 行の先頭から表示をやり直す（キャリッジリターンを利用する）

行の先頭から表示をやり直すのに、制御コードを利用しました。
制御コードは、画面上は見えませんが、ある処理を行うための特別な文字となります。
よく知られているのは"\n"の改行かと思います。

今回利用した制御コードは、キャリッジリターン"\r"です。
行の先頭にカーソルを移動させてくれます。

キャリッジリターンを使うと次のようになります。

```javascript
process.stdout.write("AAAAA"); // 画面上はAAAAAと表示される
process.stdout.write("\r");    // AAAAAの先頭にカーソルが移動する
process.stdout.write("BB");    // BBを出力し、画面上はBBAAAと表示される
```

### その３ ３行書いたら１行目に戻る（ANSIエスケープシーケンスを利用する）

1行目に戻るのに、ANSIエスケープシーケンスを利用しました。
ANSIエスケープシーケンスは、コンソール（ターミナル）上のカーソル移動やテキストフォントの種類の指定や色の指定などをすることができます。

ANSIエスケープシーケンスは文字の組み合わせで表現され、エスケープコード（文字コード0x1b）から記述します。
カーソルを上下左右に動かすANSIエスケープシーケンスは、下記の通りです。
|記法|内容|
|--|--|
|ESC[nA|カーソルを上にn移動。※今回利用| 
|ESC[nB|カーソルを下にn移動|
|ESC[nC|カーソルを右にn移動|
|ESC[nD|カーソルを左にn移動|

※ESCは、エスケープコード0x1bを表します。

ANSIエスケープシーケンスを使うと次のようになります。
```javascript
process.stdout.write('AAAAA');   // 画面上はAAAAAと表示される
process.stdout.write('\n');      // 改行
process.stdout.write('BBB');     // BBBを出力し、画面上はAAAAA（改行）BBBと表示される
process.stdout.write('\x1b[1A'); // カーソルがBBBの後ろにある状態から、1つ上の行に移動する
process.stdout.write('CCCCC');   // CCCCCを出力し、画面上はAAACCCCC（改行）BBBと表示される
process.stdout.write('\x1b[1B'); // カーソルが1つ下の行に移動する
```

## 最後に

今回はCLIで動くシンプルなプログレスバーを作ってみました。
ANSIエスケープシーケンスを利用すれば、文字色を変えたり、もっと凝ったものが作れると思います。
気になった方はぜひ試してみてください。

:::note info
株式会社BTMではエンジニアの採用をしております。
ご興味がある方はぜひ[コチラ](https://www.b-tm.co.jp/recruit/mid-career.html)をご覧ください
:::