【Bash】インタラクティブな選択メニューを作ってみた

はじめに

こんにちは。izurinaです。最近は無事に来年配属される研究室が決まって平穏な生活を取り戻したと思ったらGo言語の勉強しなければならなくなった大学生です。

UEC Advent Calendar 2022

この記事はUEC Advent Calendar 2022の12日目のものになります。UEC Advent Calendarは毎年電通大の有志によっていつの間にか開催されています。

どんな内容?

ここではBashでの矢印キーなどでカーソルを動かすタイプの選択メニューを作ってみたので簡単にどうやったのかを紹介します。とはいっても大部分はFascodeNetworkハヤオさんのソースコードを参考にしています。私はこれにvim風のキーバインドを追加したり、選択中のものをハイライトするなどの変更を加えています。今回はその点を中心に紹介していきます。

Bash

みなさんはシェルスクリプトを書きますか?書けるようになると結構快適なので、書けるようになりましょう!Bashは多くのLinuxディストリビューションでは標準シェルになっていますね。私はよくPythonと組み合わせていろいろなツールを作っています。

主な機能

カーソルの移動

特殊なキーの入力検知

たとえばabなど、普通の文字入力であれば、readで読み取ったものを、目的の文字との一致を判定すれば良いですが、矢印キーやTabキーなどはそうはいきません。こうした特殊なキーの入力を検知するためにはASCIIコードを用いて判定を行う必要があります。例えばRightArrowのASCIIコードは0x1b 0x5b 0x43と3バイトの値で表現されるので、この値を用いて入力検知を行うことになります。

以下にソースコードの例を掲載します。

capture_keys(){
  local PUSHED_KEY buff
  IFS= read -n1 -s PUSHED_KEY

  case "$PUSHED_KEY" in
    $'\x09')
      echo "Tab"
      ;;

    $'\x1b')
      read -r -n2 -s buff
      PUSHED_KEY+="$buff"

      case "$PUSHED_KEY" in
		$'\x1b\x5b\x5a')
	 		echo "ShiftTab"
	  		;;
		$'\x1b\x5b\x41')
	  		echo "Up"
	  		;;
		$'\x1b\x5b\x42')
	  		echo "Down"
	  		;;
		$'\x1b\x5b\x43')
	  		echo "Right"
	  		;;
		$'\x1b\x5b\x44')
	  		echo "Left"
	  		;;
      esac
      ;;

    "")
      echo "Enter"
      ;;
    $'\x7f')
      echo "Backspace"
      ;;
    $'\x20')
      echo "Space"
      ;;
  esac
}

readコマンドの機能の解説は参考にしたハヤオさんの記事がわかりやすいと思います。

showkeyコマンド

入力キーをCLIで確認するものとしてshowkeyがあります。showkey -aを実行した状態でキー入力すると入力キーとそれに対応するASCIIコードが出力されます。ソースコード中では矢印キーなど特殊なキーの入力をASCIIコードで受け付けている部分があります。下の画像はRightArrowを入力した例のものです。最右列がASCIIコードを16進数で表したものです。

ascii

vim風カーソル移動

自分は普段テキストエディタはvim(neovim)を使っています。vimではh j k lの四つのキーでカーソル移動ができます。個人的にこのカーソル移動に慣れているのと、このカーソル移動があればvimmerも喜ぶだろうという勝手な想像もあり実装してみました。

Tab,Shift-Tabでのカーソル移動

上記のshowkeyを用いてTabShift-TabのASCIIコードを取得して、これらのキーによるカーソル移動を実装しました。Tabは次の選択肢に移動、Shift-Tabはその逆にしました。また、これらの移動はループできるようにもしました。

選択肢の表示

選択肢を縦一列に表示するのではスクロールする手間があったり、右側にスペースがあるのに…といったことが気になってしまうのと、せっかく左右の矢印キーやh lキーの検知も実装しているのに左右のカーソル移動が無いのは勿体ないと感じたので、選択肢を縦横に並べてみました。(選択肢の内容にはツッコまないでね…)

grid

選択肢番号の二次元座標への変換

選択肢の番号(key)は一つの連番で管理されているので、これの二次元の座標への変換が必要になります。ここでは、行数を$H$、列数を$W$とします。

選択肢は以下のように配置しました。下の行列ではkeyのみとなっていますが、実際の画面ではその右側に選択肢の内容が表示されます。 $$ \begin{bmatrix} 0 & H & \cdots & (W-1)H \\ 1 & H+1 & \cdots & (W-1)H+1 \\ \vdots & \vdots & \ddots & \vdots \\ H-1 & 2H-1 & \cdots & WH-1 \end{bmatrix} $$

ここで、選択肢のkeyが$N$とすると、$N = (N \mod{H}) + (N/H) \times H$ (ただし、$N/H$は切り捨て除算)と表現できます。なので、$N$番目の選択肢の座標を$(x,y)$ ($x$行$y$列)とすると$x=N \mod H,y=N/H$となります。これで、二次元座標への変換は完了しました。

縦方向の移動は選択中の番号のインクリメント・デクリメントで実装しています。横方向の移動は選択中の番号を$H$だけ加算あるいは減算すると実現できます。

選択肢のハイライト

選択中のメニューを強調する方法の一つとして、それをハイライトするというものがありますね。実際何を選んでいるか一目瞭然です。これも一緒に実装しました。方法はエスケープシエンスを使って背景色と文字色をいじるだけです。以下のようにechoを使うと色が変わります。(ソースコード中ではprintfを使用しています。)

$ echo -e "\033[48;2;r;g;bm これは背景色を変える"
$ echo -e "\033[38;2;r;g;bm これは文字色を変える"

r g bはそれぞれ0から255までの間の値を入れてください。

実行例

動作環境

Arch Linux+Alacritty、ArchWSL+WindowsTerminalでBash 5.X系で問題なく動作したことを確認しています。

動作例

execex

今後の展望と課題

今後の展望としてこのツールを応用してほかのツールを便利にできたらなと思っています。今までに作ったツールや今後作るツールに組み込んでいきたいです。

一方で課題点として挙げられるのはESCのASCIIコードが0x1bなのでArrowキーのASCIIコードの1バイト目と被ってしまっているんですよね…。その関係でESCに機能を割り当てることができなくなっているのでこれを解決できたらなあと思います。多分無理。

ソースコード

公開

ソースコードはGitHubで公開しています。

ライセンス

私が作成したソースコードはWTFPLにて再ライセンスします。プログラムの使用はご自由にどうぞ。

以下ライセンス原文

            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
                    Version 2, December 2004
  
 Copyright (C) 2004 Sam Hocevar <[email protected]>
 
 Everyone is permitted to copy and distribute verbatim or modified
 copies of this license document, and changing it is allowed as long
 as the name is changed.
  
            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
  
  0. You just DO WHAT THE FUCK YOU WANT TO.

参考

[1] 山田ハヤオ「シェルスクリプトで特殊なキーの入力を検出する」
https://blog.fascode.net/2022/11/02/shell-special-keys/

[2] PruneMazui「ANSIエスケープシエンスチートシート」
https://qiita.com/PruneMazui/items/8a023347772620025ad6

Built with Hugo
テーマ StackJimmy によって設計されています。