TypeScriptでCLIツールを作りたい人のためのパッケージ6つ 画面表示/入力編
対象読者
- TypeScriptを使い慣れている
- ウェブアプリばかり書いている。コマンドラインツールも書きたい
- CLIツールの表示や入力を今風にしたい
コマンドラインは殺風景
日常のちょっとした雑用や、開発で同じ作業を繰り返す仕事のために、コマンドラインツールを作る。手でやるのとツール作るの、どっちが早いか分からないなと思いつつ、デジタル単純作業は精神に来るし、コードを書くのが楽しいのでそうしている。
シェルプロンプトに向かって、テキストで用意したデータやコマンドの出力をパイプをつなげてアドリブで処理することから始める。が、すぐに限界が来る。シェルスクリプトは書きづらいのだ。
50行を超えないうちにTypeScriptで書き直す。最近はAIがやってくれる。ずいぶんラクになった。
そうやって開発する中でよそのCLIツールを使う。いまどきはCLIツールもずいぶん華やかで親切になったな、と感心する。自分の雑用CLIツールもこうしたいな。と思い始めた。
そんなわけで、CLIのUI関連パッケージをいくつか使ってみた。以下に紹介する。
ログ
ログを華やかに: consola
何か表示したければ全部 console.log() していたが、もうちょっと華やかにしたい。一番使っているのはこれ。
import { consola } from consola
consola.log('ログ出力します')
ワードのシルエットが console.log() とそっくりで、開発体験に戸惑いが少ない。
consolaのログ出力メソッドには、通常のログレベル指定出力と、.info()に意味をのせたバリエーションメソッドがある。
| ログレベル | レベルに従うメソッド | バリエーション |
|---|---|---|
| 0以上 | consola.error() |
consola.fatal() |
| 1以上 | consola.warn() |
|
| 2以上 | consola.log() |
|
| 3以上 | consola.info() |
consola.ready() |
consola.start() |
||
consola.success() |
||
consola.fail() |
||
consola.box() |
||
| 4以上 | consola.debug() |
|
| 5以上 | consola.trace() |
※consola.silent()…レベルに関係なく必ず出力される
import { consola } from 'consola'
import { colorize, colors } from 'consola/utils'
consola.fatal('fatal')
consola.error('error')
consola.warn('warn')
consola.log('log')
consola.log(colorize('blue', 'blue log'))
consola.info('info')
consola.ready('ready')
consola.start('start')
consola.success('success')
consola.fail('fail')
consola.debug('debug')
consola.trace('trace')
consola.silent('silent')
consola.box('Here is a message box.')

バリエーションのメソッドのログ出力には、意味にそった色と絵文字の装飾がつく。使い始めた当初は内心、(ちょっと邪道だな)と思っていたが、使っているうちに「これは良いかもしれない」と見直した。
このようなメソッドがあることによって、いつ、どのようにログを書くか、という基準を守るようになっている。
ログレベルを指定したメソッドしかなければ、ログを書く基準・規則は自分が作り、守らねばならない。しかし、このようなメソッドがあることによって、処理を開始するなら.start()でログを書き、成功したら.success()、失敗したら.fail()、考えることが少なくて済むうえ、出力されるログの見た目もそろう。判断する負荷を減らしてくれる。書く時も見る時もラクなのだ。
consolaはあくまでログのパッケージなので、ログレベルやスコープ(consolaではtag)の概念があり、それらによって出力の有無や出力先、整形などを調整する機能がついている。
装飾をはずしたければ最初のimportを差し替える:
import { consola } from 'consola/basic'

ファイルにも出力するにはこうしてやればよい:
consola.addReporter([
log(logObj) {
const logMessage = `${logObj.date.toISOString()} [${logObj.level}] [${
logObj.type
}] ${logObj.args.join(' ')}\n`
fs.appendFileSync('./logfile.log', logMessage, 'utf8')
},
])
バリエーションは見た目だけが違うわけではなく、発行するlogObjのtypeプロパティにそのメソッドの名が設定されている。
画面表示
現状を表示: log-update
リストを一括で処理する際、進捗を表示したいことがある。console.log()で全部書くと流れて見づらい場合に、画面には現在処理中のものを表示し、流さないでその場で表示更新してもらいたい。そういう時には log-updateを使う。
import logUpdate from 'log-update'
for (const [index, item] of list.entries()) {
logUpdate(`Processing item ${item} [${index + 1} of ${list.length}]`)
await registerItem(item)
}

進捗をバーで表示: cli-progress
件数が多くて全体進捗が見たい場合は progress bar を表示しよう。
import cliProgress from 'cli-progress'
const progressBar = new cliProgress.SingleBar(
{
format: 'Progress |{bar}| {percentage}% || {value}/{total} Items || {item}',
},
cliProgress.Presets.shades_classic,
)
progressBar.start(list.length, 0)
for (const [index, item] of list.entries()) {
progressBar.increment({ item })
await registerItem(item)
}
progressBar.stop()

これは現在の処理内容も表示するバージョンだが、バーだけでよければnewの第一引数は不要、increment()も引数なしでOK。
スピナーをつけたい: ora
import ora from 'ora'
const spinner = ora('Processing events...').start()
for (const [index, item] of list.entries()) {
spinner.text = `Processing: ${item} (${index + 1}/${list.length})`
await new Promise((resolve) => setTimeout(resolve, 400))
}
spinner.succeed('All events processed successfully!')

入力
コマンドライン引数解析: yargs
コマンドライン引数の解析をしてくれる。使いやすいが、説明しようとするとちょっと多くなるため、ここではひとまず紹介だけ。
import yargs from 'yargs'
import { hideBin } from 'yargs/helpers'
const argv = yargs(hideBin(process.argv))
.version('0.1.0')
.help()
.option('delete', {
alias: 'd',
type: 'boolean',
description: 'Delete events from calendar',
default: false,
})
.parseSync()
const willDelete = argv.delete // boolean
console.log(willDelete)
$ yargs-command --help
Options:
--version Show version number [boolean]
--help Show help [boolean]
-d, --delete Delete events from calendar [boolean] [default: false]
$ yargs-command --delete
true
対話式入力: inquirer
対話的に入力を受け付けたい場合はinquirerを使う。あなたもきっとユーザとしては見かけたことがあるはず。開発しているとナンチャラinitやナントカcreateでよく見る。
import inquirer from 'inquirer';
const questions = [
{
type: 'input',
name: 'username',
message: 'あなたの名前を入力してください:',
},
{
type: 'password',
name: 'password',
message: 'パスワードを入力してください:',
mask: '*',
},
{
type: 'list',
name: 'language',
message: '使用する言語を選択してください:',
choices: ['JavaScript', 'TypeScript', 'Python'],
},
{
type: 'checkbox',
name: 'features',
message: '使用したい機能を選択してください:',
choices: [
{ name: 'ログイン', value: 'login' },
{ name: '登録', value: 'register' },
{ name: '削除', value: 'delete' },
{ name: '更新', value: 'update' },
],
},
{
type: 'confirm',
name: 'confirmAction',
message: 'この操作を実行しますか?',
default: false,
},
] as const
const answer = await inquirer.prompt(questions)
console.log('Answer:', answer)

さいごに
これらのパッケージのおかげでCLIツールを作るのが楽しくなった。あなたもぜひ!
🐣 Enjoy command line scripting!
Discussion