⌨️

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.')

alt text

バリエーションのメソッドのログ出力には、意味にそった色と絵文字の装飾がつく。使い始めた当初は内心、(ちょっと邪道だな)と思っていたが、使っているうちに「これは良いかもしれない」と見直した。

このようなメソッドがあることによって、いつ、どのようにログを書くか、という基準を守るようになっている。

ログレベルを指定したメソッドしかなければ、ログを書く基準・規則は自分が作り、守らねばならない。しかし、このようなメソッドがあることによって、処理を開始するなら.start()でログを書き、成功したら.success()、失敗したら.fail()、考えることが少なくて済むうえ、出力されるログの見た目もそろう。判断する負荷を減らしてくれる。書く時も見る時もラクなのだ。

consolaはあくまでログのパッケージなので、ログレベルやスコープ(consolaではtag)の概念があり、それらによって出力の有無や出力先、整形などを調整する機能がついている。

装飾をはずしたければ最初のimportを差し替える:

import { consola } from 'consola/basic'

alt text

ファイルにも出力するにはこうしてやればよい:

consola.addReporter([
  log(logObj) {
    const logMessage = `${logObj.date.toISOString()} [${logObj.level}] [${
      logObj.type
    }] ${logObj.args.join(' ')}\n`
    fs.appendFileSync('./logfile.log', logMessage, 'utf8')
  },
])

バリエーションは見た目だけが違うわけではなく、発行するlogObjtypeプロパティにそのメソッドの名が設定されている。

画面表示

現状を表示: 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