日報作業でちょっと楽するCUIツールの作り方

# はじめに
アドカレに乗っかって初Qiitaを書きます。
私の所属先ではSlackに勤怠チャンネルがあり、業務終了時にその日の業務内容について投稿するようにしています。（ミニ日報みたいな感じです。）
投稿する内容はこんな感じです。
```
【勤務時間】10:00 ~ 19:00
【稼働時間の合計】8時間00分（休憩：1時間00分）
【本日の作業】○○○_初回リリース, △△△_コンテンツ追加
【明日の作業】△△△_コンテンツ追加, □□□_リファクタリング
【共有・相談事項】
```
特に大変というわけではないのですが、毎日「よし！業務終了だ！」と思ってから、このミニ日報を書くのは少し面倒でした。
そこで、その少し面倒くさい作業で楽するためにCUI上で簡単に日報を書いて投稿できるツールを作成したので、今回はそのツールの作成に使った技術について共有します。（以降作成したツールを退勤ツールと呼びます。）

面倒くさい作業で楽するために退勤ツールで実装したいと思ったことは以下の4つです。
1\. 時間を記入するときに対話型で入力する
2\. 作業内容はチケット管理ツールからデータを取得して選択式で入力する
3\. 日報はCUIから直接Slackのチャンネルに送信する
4\. ツールは短いコマンド一つで起動する

今回はこれらの目的を達成するために使った実装方法についてお話しします。
言語はJavaScriptで、nodeのバージョンは16.13.0です。

# 実装方法
実装方法は4つの目的別にセクション分けして記載しています。

## 1\. 時間を記入するときに対話型で入力する
「対話型」というのはこんな感じです。

![対話型のイメージ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982501/f7438db5-c040-49e8-02a2-1dc18d1dfd6e.png)

これをCUIで再現できるように実装します。

退勤ツールを実行したら、質問が出力され、それに応じて答えを入力したいです。
そこで質問の出力、答えの入力を実装するためにNode.jsのreadline.Interfaceの[question](https://nodejs.org/api/readline.html#rlquestionquery-options-callback "rlquestionquery-options-callback")メソッドを使いました。
```javascript:readline.cjs
const readline = require("readline");
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
// rl.question()のように使います
```
上記のコードではimportしたreadlineのcreateInterfaceメソッドで入出力の設定を行なっています。
inputにprocess.stdinを、outputにprocess.stdoutを指定することで、questionメソッドの「入力」に標準入力（キーボードで入力）を設定でき、出力に標準出力（ターミナルで出力）を設定できます。

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982501/b6f78522-e1b0-ef83-2f30-528b4a93b2ed.png">

questionメソッドは第一引数に「質問」を第二引数にコールバック関数で「回答」の処理経路を書いていきます。
```javascript:readline.cjs
const readline = require("readline");
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });

rl.question('好きな食べ物はなんですか？', (answer) => {
    console.log(`なるほど、${answer}が好きなんですね。`)
    rl.close() // closeメソッドでインターフェイスを閉じます。
})
```
上記のコードを実行すると以下のように対話型で入出力を行うことができます。
![question_method.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982501/e38d60a1-d04a-5d43-a59e-e19c53503c67.gif)

対話型が実装できたので、あとは「開始時間は何時ですか？」のように稼働時間等を埋めるための質問を出力させて時間を入力すればいいですね。
コールバック関数の中で、条件分岐させれば、「何も入力せずエンターを押した時（answerが空文字の時）は現在時刻を入力する」のような実装もすることができます。

これで【勤務時間】と【稼働時間の合計】の箇所は、Node.jsのreadline.Interfaceのquestionメソッドを使って実装できました。（ついでに日報の【共有・相談事項】の部分も同じくquestionメソッドで実装しました。）

## 2\. 作業内容はチケット管理ツールからデータを取得して選択式で入力する

次は【本日の作業】と【明日の作業】の箇所です。
私の場合、日報の作業内容の欄は、基本的に自分の持ちタスク（担当チケット）で埋めていたので、作業内容を管理しているチケット管理ツールから自分の持ちタスクを一覧で取得して、それらタスク一覧から選択形式で入力できるように実装することにしました。

CUI上で選択形式の入力を行うためにnpmの[enquirer](https://www.npmjs.com/package/enquirer 'enquire')をインストールして使いました。
```
npm i enquirer
```
enquirerはCUI上で直感的な入力方法を簡単に実装できるパッケージです。今回は中でも[MultiSelect Prompt](https://www.npmjs.com/package/enquirer#multiselect-prompt 'MultiSelect Prompt')を使いました。
MultiSelect Promptは１つの質問と、複数の回答を選択形式で入力できるプロンプトを表示することができます。

使い方は簡単で、importしたenquirerでMultiSelectインスタンスを作成して、質問と回答（複数）を流し込むだけです。
```javascript:multiSelect.cjs
const { MultiSelect } = require("enquirer");

const prompt = new MultiSelect({
  message: '好きな食べ物はなんですか？',
  choices: [
    { name: '寿司' },
    { name: '焼肉' },
    { name: 'カレー' },
    { name: '唐揚げ' },
    { name: 'ハンバーグ' },
    { name: 'ピザ' }
  ]
});

prompt.run()
  .then(answer => console.log('Answer:', answer))
  .catch(console.error);

// Answer: ['寿司', 'カレー', 'ピザ']
```
上記のコードを実行すると、以下のように質問と選択肢が表示されます。スペースキーで選択してエンターキーで選択内容を決定することができます。
![multiselect.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982501/5db624cc-2b81-f92b-2923-2edf260c2ee6.gif)


退勤ツールの方では、チケット管理ツールから取得した自分の持ちタスクの一覧を`choices`プロパティに流し込んで、回答して返ってきた値は、`"寿司, カレー, ピザ"`のようにカンマ区切りの１つの文字列に変換して利用しました。これで【本日の作業】と【明日の作業】は簡単に入力することができるようになりました。

## 3\. 日報はCUIから直接Slackのチャンネルに送信する

次にCUIからそのままSlackのチャンネルに送信する実装方法です。やはりCUIで作成した投稿内容はコピペしてSlackに投稿するのではなく、エンターキーで気持ち良くSlackに送信したいです。

<img width="550" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982501/1f980c75-fe34-8faa-fefc-c2c68c34cee3.png">


そこで[Slack API](https://api.slack.com/ "Slack API")を使って作成した退勤ツールとSlackチャンネル間の橋渡しをするイメージで実装しました。
このSlack APIを使うためには受信側（Slackのワークスペース)と送信側（退勤ツール）のそれぞれに設定が必要です。
#### 受信側の設定
送信する投稿内容をワークスペース側で受け取るためには、ワークスペースに退勤ツール専用の投稿内容を受け取るアプリを設置する必要があります。

このアプリを作成するにはSlack APIのブラウザページにアクセスし、create new appに進んでGUI上でアプリを作成していきます。アプリを作成するときに様々な機能をつけることができますが、ここでは「退勤ツールから送信された投稿内容を受け取って、自分のアカウントで投稿する」機能をつけました。

アプリを作成するとそのアプリ専用のトークンとシークレットIDが発行されるので、それらを用いて外部からのアクセスを安全に行います。
<img width="550" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982501/79d6dd4b-4237-2b8d-83c4-a95c72bb7bb1.png">

これで受信側の設定が完了しましたので続いて送信側の設定を行います。
#### 送信側の設定
送信側の退勤ツールには[@slack/bolt](https://www.npmjs.com/package/@slack/bolt '@slack/bolt')というnpmパッケージをインストールしました。
```
npm i @slack/bolt
```
importしたパッケージを使って、Appインスタンスを作成し、インスタンスには受信側のアプリで発行されたトークンとシークレットIDを設定しています。今回は対象のSlackチャンネルにテキストを送信したかったので`client.chat.postMessage`メソッドを使いました。

```javascript:slackBolt.cjs
const pkg = require("@slack/bolt");
const { App } = pkg;

const app = new App({
  token: 'ワークスペースに設置したアプリのトークン',
  signingSecret: 'ワークスペースに設置したアプリのシークレットID',
});

const channelId = "投稿するチャンネルのID";

module.exports = () => {
  try {
    app.client.chat.postMessage({
      channel: channelId,
      text: '投稿内容',
    });
  } catch (error) {
    console.error(error);
  }
};
```
これで受信側の設定も完了して退勤ツールとワークスペースの橋渡しが完了しました。
<img width="550" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982501/c33bbf39-cbc2-9c64-e377-e8e4515ebfae.png">

加えて送信する前に「Slackに送信していいですか？yes/no」の確認を先ほど紹介したreadline.Interfaceのquestionメソッドを使って実装すれば誤送信を防げますね。
以上がSlack APIを使ったCUIから直接Slackのチャンネルに送信する実装方法になります。

## 4\. ツールは短いコマンド一つで起動する

退勤ツールは毎日使うものなので、いくらCUIで日報を作成できるようになっても毎回ツールのディレクトリまで階層を移動し、コマンド（npm start）を実行するのは手間です。そこで**ツールのディレクトリまでパスを通して、短いコマンドでどこからでも起動できる**ように実装しました。まずは**短いコマンド**で起動できるように実装します。

短いコマンドで起動するには、**退勤ツールを起動するまでに打つシェルスクリプト**が書かれたコマンド名のファイルを用意して、そのファイルをシェルで実行する方法があります。
今回は「`report`」というコマンドだけで退勤ツールを起動させたかったのでファイル名を「report（拡張子なし）」としました。

そしてそのreportファイルは退勤ツールのルートディレクトリ直下に作成したbinディレクトリ内に格納しました。というのも退勤ツールの実行コマンドが`npm start`なので`package.json`のあるディレクトリで実行する必要があったからです。
まずreportファイルにはこのように書きました。
```bash
#!/bin/bash
npm start
```
`#!/bin/bash`はreportファイルのシェルスクリプトをbashで実行するために書いています。
これで退勤ツールのディレクトリまで移動すれば`npm start`ではなく、`report`だけで実行できるようになりました。
<details><summary>作成したファイルで実行できない時</summary>

作成したファイルで実行した時に`Permission denied`のエラーが出るかもしれません。
これはファイル別に付与されたアクセス権などを決定している**許可属性**が関係しています。デフォルトなのかは分かりませんが、作成したファイルには許可属性の「実行可能」の許可がない状態になっていたので、「実行可能」を許可することで実行できるようになりました。
詳細は[こちら](https://atmarkit.itmedia.co.jp/ait/articles/1605/18/news015.html "「ls -l」コマンドの表示からファイルの属性を理解しよう")を参考にしてください。
</details>

でもこれだとほんのちょっとコマンドが短くなっただけで、結局`cd`でツールのディレクトリ階層まで移動しなければ実行できません。そこでシェルスクリプトに`cd`でツールまでディレクトリを移動する部分を書き加えました。
```bash
#!/bin/bash
cd $HOME/..../..../ツールのディレクトリ/
npm start
```
これでどのディレクトリにいても`$HOME/..../..../ツールのディレクトリ/bin/report`というコマンドを実行するだけで、シェルスクリプトが実行され、**ツールのディレクトリまで移動**し、**npm startで実行する**ことができるようになりました。

でもこれでは、どこからでも実行できるとはいえ、コマンドが長くなってしまいました。そこでコマンドを`$HOME/..../..../ツールのディレクトリ/bin/report`ではなく`report`だけで実行できるように**環境変数$PATH**にreportファイルまでのパスを登録してあげます。

<details><summary>$PATHにパスを登録するとは？</summary>

ここでの環境変数`$PATH`は**コマンド検索パス**のことを指します。$PATHに登録されたパス（ディレクトリの場所を示す情報）は**コマンドを実行するときに省略することができる**ようになります。

例えば、`cd`のコマンドを使うときに、わざわざ`/usr/bin/cd`とは打たないですよね。`cd`を使うときには対象のファイルまでのパスを打たずに実行できますが、**それは`/usr/bin`が`$PATH`に登録されているから**です。コマンドを実行した時、シェル側が`$PATH`に登録されたパスの一覧から対象のコマンドのファイルがあるかを検索します。
`cd`と打った時に、シェルが$PATHを参照して`/usr/bin`内の`cd`ファイルを検索できたから`/usr/bin/cd`を実行できるわけです。
このように$PATHにパスを登録することで、実行するファイルまでのパスを省略することができます。

</details>

以下のように`.bash_profile`に追記して、$PATHに`report`コマンドのファイルのパスを登録しました。
```bash
export PATH=$PATH:$HOME/..../..../ツールのディレクトリ/bin
```
これでどこからでも`report`だけで退勤ツールを起動できるようになりました。

以上で日々の日報作業でちょっと楽するための退勤ツールは完成です。

# おわりに

ちょっと日報作業が楽になるかなと思って作成した今回のツールですが、実際に使ってみると日報を書く小さなストレスがなくなり、毎日ポチポチコマンドを押しながら楽しく日報を埋めることができるようになりました。
そしてツールを作成して色々と勉強になったので（ツールをメンバーに見せて沸いてたのも嬉しかったので）また何か思いついたら作ってみようかなと思います。