🕌

エイリアスやシェル関数はYAMLで書こう

2023/11/03に公開

はじめに

YAMLファイルをシェルのコマンド (関数) としてロードするツール、
YACT (YAML as Command Tree) をオープンソースとして公開したので紹介します。


YACTの主要な特長は以下の通りです。

  • コマンドの階層化
    • さらに階層化されたサブコマンドが入力補完される
  • 入力補完のカスタマイズ
    • 引数ごとに補完候補の生成コマンドを指定可能
  • 快適なコマンド実装
    • ファイル編集時にコンパイルやシェルのリロードが不要
    • CLIによる編集機能
    • サブコマンドを利用することで、コマンドの分割・再利用が容易

YACTでコマンドを階層化して定義することで、コマンド名の競合や煩雑な管理の悩みを解消できます。また、(普通に実装するとかなり面倒な)コマンドの入力補完を容易に定義できます。そして何よりも、コマンドの追加や編集作業自体が非常にスムーズです。

YACTの解説 (要約)

YACTでは既定の形式で記述されたYAMLファイル(YACTファイルと呼びます)をシェルのコマンドとしてロードします。YAMLのファイル名がそのままコマンド名となります。

単純なYACTファイルの例

最も単純なYACTファイルは、ただ実行コマンドのみを記述したものです。エイリアスのように特殊文字をエスケープする必要もなく、実行コマンドをそのままの形で記述できます。

hello.yaml
echo "hello world!"

このファイル名をコマンド名としてターミナルで実行できます

$ hello
hello world!

とてもシンプルです。次にYACT独自の機能を活用した例を紹介します。

複雑なYACTファイルの例

以下のYACTファイルは foo というコマンドを定義しています。fooコマンドは階層化されており、YAMLのキーがそのままサブコマンド名を表します。ただし、x のキーは実行コマンド、数値 (N) のキーはN番目の引数に対応する入力補完コマンド[1]を表現するための特殊なキーです。

foo.yaml
check-file: |
  local filepath={0}
  if [ -f {0} ]; then
      echo "File {0} exists."
  else
      echo "File {0} does not exist."
  fi
docker:
  pull:
    x: docker pull {0}
    ubuntu:
      x: docker pull ubuntu:{0}
      0: curl -s https://hub.docker.com/v2/repositories/library/ubuntu/tags/ | jq '.results[].name'
    python:
      x: docker pull python:{0}
      0: curl -s https://hub.docker.com/v2/repositories/library/python/tags/ | jq '.results[].name'
  run:
    x: docker run -it {0} /bin/{1}
    0: docker images | awk 'NR>1 {print $1":"$2}'
    1: echo "bash zsh dash sh"
  prune: docker system prune

ターミナルで各サブコマンドをツリー表示すると以下になります。

実際に

$ foo docker pull ubuntu 23.10

を実行すると、foo docker pull ubuntuコマンドに引数として23.10が渡されて、 docker pull ubuntu:23.10 が実行されます。

コマンド入力時、それぞれのサブコマンド名が自動的に入力補完されます。コマンドの引数に対しては、対応するキーで指定した コマンド補完関数から入力候補を動的に生成します [2]

ファイルの変更内容はホットリロードされ、実行中のシェルのセッションにも直ちに反映されるため、開発のイテレーションが速いのも特徴です。


CLI操作

YACTではCLIでコマンドの追加や編集、確認などができます。一部を紹介します。

YACTコマンドの追加

$ yact new <コマンド名> [<実行コマンド>] でYACTコマンドを追加できます。

YACTコマンドの編集

$ YACTコマンド -e でYACTファイルをテキストエディタで即時編集することができます。

また、$ YACTコマンド -a <新しい実行コマンド> でコマンドラインから直接編集も可能です。

YACTコマンドの確認

YACTコマンドに実行コマンドが無い場合や、引数の数が足りない場合などにはツリー表示等のナビゲーションが表示されます。-p オプションを付けるとYACTコマンドの詳細を表示できます。

インストール方法

前提条件

  • シェル: bashまたはzsh
  • OS: LinuxまたはMacOS
  • 依存コマンド: sed, awk

手順

  1. 以下のリポジトリをYACTをインストールしたい場所にクローンします:
$ git clone https://github.com/Harukaze9/YACT.git
  1. クローンしたリポジトリ直下のinstall.shを実行します。
$ ./YACT/install.sh

あるいは、以下の行を.bashrc等のシェルの設定ファイルに直接追加してからシェルをリフレッシュしてもOKです。

source /path/to/YACT/src/source-yact.sh
  1. ターミナル(タブ)を新しく立ち上げるか、以下のいずれかのコマンドでシェルをリフレッシュすればインストール完了です。
  • bash または zsh
  • source ~/.bashrc または source ~/.zshrc

(使用しているシェルの種類に応じて選択して下さい)


インストールの確認

$ yact new hello 'echo hello world!'
$ hello

上の2つのコマンドをターミナルで実行し、

hello world!

が表示されればインストールが成功しています。

YACT (YAML as Command Tree)

YACTはYAMLファイルをシェルコマンドに変換する仕組みです。
YACTは次の用語を使用します。

YACTファイル: YAML形式でコマンドの定義を記述したファイル
YACTコマンド: YACTファイルからロードされたコマンド

YACTファイル

YACTファイルは、一定のルールで記述されたコマンド名.yamlという命名のYAMLファイルです。
ファイル名がコマンド名を表現し、YAMLファイルの中身はコマンドの定義(実行コマンド、サブコマンド、入力補完など)を表現します。

YACTでは既定のディレクトリ(YACT/Commands/)以下に置かれたYACTファイルが自動的にYACTコマンドとしてロードされます。

実行コマンドを設定する

x というキーに続けて実行コマンドの文字列を記述します。
以下のYACTファイルの例では、helloコマンドを実行するとhello world!が出力されます。

hello.yaml
x: echo "hello world!"

他にキーが無い場合にはx キーを省略することができます。

hello.yaml
echo "hello world!"

{N}の形式のプレースホルダをコマンド文字列に含めることで、コマンドに引数を与えられます。

hello2.yaml
echo "given arguments are {0} and {1}!"

サブコマンドを設定する

YAMLのキーはそのコマンドのサブコマンド名を表現します。YAMLのキーの木構造がそのままコマンドの構造になります。階層の深さの制約などもありません。

greet.yaml
x: echo "hi!"
hello: echo "Hello World!"
good:
  morning: echo "Good morning, sunshine!"
  evening: echo "Good evening, had a good day?"

実際に各サブコマンドを実行すると以下のようになります。

$ greet
hi!

$ greet hello
Hello World!

$ greet good morning
Good morning, sunshine!

$ greet good evening
Good evening, had a good day?

オーバーロードを設定する

実行コマンドの値にリストを与えることで、コマンドのオーバーロードを実現できます。

hello3.yaml
x:
  - echo hello
  - echo hello {0}!
  - echo hello {0} and {1}!

引数の数に応じて対応するコマンドが実行されます。

$ hello3
hello

$ hello3 Alice
hello Alice!

$ hello3 Alice Bob
hello Alice and Bob!

複数行のコマンドを設定する

シェル関数の形式で複数行のコマンドを書くことができます。if文やfor文を使ったりローカル変数を定義したりもできます。

check.yaml
x: |
  if [ -f {0} ]; then
      echo "File {0} exists."
  else
      echo "File {0} does not exist."
  fi

YACTコマンドの基本的なふるまい

  • YACTコマンドは、与えられたパス(サブコマンドの列)に対応するYACTファイル内の実行コマンドを実行します。
  • 与えられたパスに実行コマンドが定義サれていない場合や、引数が足りない場合は適当なメッセージによるナビゲーションが表示されます。
  • YACTコマンドは、入力時に次の階層のサブコマンドが入力候補として自動的に補完されます。
  • 引数部分の入力時は、YACTファイルで定義されている補完コマンドを参照します。
  • 元となるYACTファイルは実行の度に読み込まれるので、ファイル変更時のリロードも不要です。

既定のオプション

YACTコマンドには既定のオプションがあり、コマンドの確認や編集を行えます。

-p: サブコマンドの一覧をツリー表示

形式: <YACTコマンドのパス> -p

冒頭で示したfoo コマンドに対して foo docker pull -p を実行すると、以下の出力が得られます。

 pull
 ├─ [docker pull {0}]
 ├─ ubuntu
 │  └─ [docker pull ubuntu:{0}]
 └─ python
    └─ [docker pull python:{0}]

-a: 指定したパスにコマンドを追加

形式: <YACTコマンドのパス> -a <設定したい実行コマンド>

任意のYACTコマンドのパスをに対して、 -a オプションを使って設定したい実行コマンドを渡せます。

$ foo your another subcommand -a 'echo "you can add any commands here!"'

パスが存在しない場合はパスごと追加し、パスとコマンドがすでに存在する場合は設定されている実行コマンドを上書きします。

-r: 指定したパスを削除

形式: <YACTコマンドのパス> -r

定義済みのYACTコマンドのパスに -r オプションを渡すことでパスを削除できます。

$ foo docker pull -r

パスが存在しない場合はパスごと追加し、パスとコマンドがすでに存在する場合は設定されている実行コマンドを上書きします。

-e: コマンドをテキストエディタで開く

形式: <コマンド名> -e

YACTコマンドに -e オプションを渡して実行すると、設定したテキストエディタでYACTファイルが開きます。

ユーティリティ

補助ツールとして、yact コマンドが用意されています。こちらを使えば多くの操作をCLIで完結させられます。

  • yact list
    • インストール済みのYACTコマンドの一覧を表示する
  • yact new <作成したいYACTコマンド名> [<実行コマンド内容>]
    • 指定した名前のYACTコマンドを新規追加する
  • yact remove <YACTコマンド名>
    • 指定した名前のYACTコマンドを削除する
  • yact rename <YACTコマンド名> <新しいYACTコマンド名>
    • 指定したYACTコマンドの名前を変更する
  • yact register <YAMLファイルへのパス>
    • 指定したYAMLファイルYACTファイルとして読み込む

余談

yactコマンド自体もYACTで実装されているので、入力補完やナビゲーションを利用できます。

発展的な使い方

YACTはエイリアス・コマンドを階層化して整理する手段にも使えますし、入力補完機能を使って既存コマンドの補完をカスタマイズするのにも使えます(本記事冒頭でdocker hub のAPIを使って入力補完するようなケースです)。

さらに、ここではYACTの発展的な使い方をいくつか紹介します。


例: 自己編集を利用したツール

「ディレクトリのパスを好きな名前で登録し、登録した各ディレクトリに移動するツール」をたったの3行で作成できます。

cdd.yaml
_reg:
  - "{self} {0} -a \"cd $(pwd)\""
  - "{self} $(basename $(pwd)) -a \"cd $(pwd)\""

(注: {self} は特殊タグで、そのYACTコマンド名 (ファイル名) を参照します。)

このコマンドは、
$ cdd _reg [<登録名>] でカレントディレクトリのパスを好きな登録名で登録します。(登録名を省略した場合はディレクトリ名を登録名とします)

サブコマンドは自動で入力補完されるので、ターミナルで
$ cdd と入力してタブキーを押すと、登録名の一覧が入力候補としてサジェストされます。
そのまま $ cdd 登録名 で登録したディレクトリに移動することができます。

$ cdd -pで登録済みの内容を確認できます

$ cdd -p
Below are command details for [cdd]:
 cdd
 ├─ dir1
 │  └─ [cd /path/to/dir1]
 ├─ dir2
 │  └─ [cd /another/path/to/dir2]
 └─ favorite-nickname
    └─ [cd /path/to/any/dir]

YACTコマンドなので、 $ cdd 登録名 -r で削除することもできます。


例: サブルーチンの定義

YACTのサブコマンドは同じYACTコマンドや他のYACTコマンドから呼び出すことができるので、サブルーチンを定義するのにも便利です。サブコマンドとしてスコープ化されていますし、サブコマンド名を_から始めることで、補完候補やツリー表示から除外することもできます。

以下は指定したファイルのフルパスをコピーするコマンド util copy_file_path を定義しています。以下の2つのサブコマンドを呼び出すことで、処理を簡潔に記述しています。

  • yact _file_exists: 引数で指定したファイルが存在するかチェックする関数
  • yact _copy_to_clipboard: OS別にクリップボードの操作コマンドをチェックし、引数で指定した文字列のコピーを実行する関数
util.yaml
copy_file_path: |
  if ! {self} _file_exists "{0}"; then return; fi
  local full_path=`readlink -f {0}`
  if {self} _copy_to_clipboard $full_path; then
    echo "Copied to clipboard: [$full_path]"
  fi

_file_exists: |
  if [[ -f {0} ]]; then
    return 0
  else
    echo "$filename does not exist!"
    return 1
  fi

_copy_to_clipboard: |
  local str={0}
  case "$(uname)" in
    Darwin) echo -n "$str" | pbcopy ;;
    Linux)
      if [[ -e "/mnt/c/Windows/System32/clip.exe" ]]; then
        echo -n "$str" | clip.exe
      elif command -v xclip > /dev/null; then
        echo -n "$str" | xclip -selection clipboard
      else
        echo "Error: No clipboard commands found."; return 1
      fi
      ;;
    *) echo "Error: Unsupported OS."; return 1 ;;
  esac

サブコマンドに分割することで、シェル関数のデバッグが容易になり、開発を効率化できます。YACTのサブコマンドはコマンド名がスコープ化されているのと、ファイルの変更内容がコマンドに即時反映される性質から、シェル関数やシェルスクリプトと比べると手軽[3]にコマンドを分割して開発できます。

YACTコマンドのサブルーチンの作成に限らず、他のYACTコマンドやシェル関数、シェルスクリプト向けのライブラリを作成する用途にも使えます。サブコマンドとして各コマンド名をスコープ化できるので、名前の衝突の心配が無く便利です。

その他

YACTの実装について

YACTでは、YACTコマンドをシェル関数としてシェルの開始時やコマンドの追加時にロードしています。シェル関数内で実行されるコアの処理(YAMLを読み込んだり操作する処理)はRustで書かれています。

ソースコードはオープンソースとしてGithubに公開しています。機能の要望やバグ報告なども歓迎しています。

脚注
  1. 入力補完コマンドは、コマンドの入力補完候補を生成するコマンドです。一般的に入力補完をカスタマイズする際は専用の補完関数を自作する必要がありますが、結構面倒です ↩︎

  2. この例だと docker hub のAPIを叩いて 「20.04 22.04 23.04 ...」のような候補を取得します ↩︎

  3. シェル関数はコマンド名がスコープ化されないためサブ関数を作りづらく、設定ファイルをリロードしないと変更内容が反映されません。シェルスクリプトはサブ関数を直接実行することはできませんし、ファイル単位で分割するのも不便です ↩︎

Discussion