🐟

fish shell で自作コマンド(プラグイン)を作ってみよう

2023/03/06に公開

本記事の内容

ただHello, World!するだけのすごくシンプルなコマンドを作りながら、以下の内容を簡単に説明していきます。

  • オプションのパース
  • tab補完の設定
  • テストの作成
  • GitHub actionsでのテストの実行
  • fisherを使って出来たコマンドをインストールしてみる

自作コマンドを作れるようになると、ターミナル生活が少し豊かになります。また、「こうこうこうしたいんだけど具体的にどうスクリプトを書けば良いか分からない」といった場合には、ChatGPTに聞くと結構な確率で教えてくれるのでおすすめです。面倒な作業はAIにやらせましょう。

実際にコマンドを作ってみよう

リポジトリの作成

まずはコマンドを開発するリポジトリを作ります。

mkdir helloworld.fish
cd helloworld.fish
git init

続いて、以下のディレクトリとファイルを作成します。

ディレクトリ・ファイル 説明
functions/ コマンドを保存するディレクトリ
functions/helloworld.fish コマンドの処理を書くスクリプトファイル
completions/ コマンドの補完の設定を保存するディレクトリ
completions/helloworld.fish コマンドの補完の設定を書くスクリプトファイル
mkdir functions
mkdir completions
touch functions/helloworld.fish
touch completions/helloworld.fish

ついでに、README.mdとLICENSEも作っときます。LICENSEの内容は適宜好きなようにすると良いでしょう。

echo "# A command to print 'Hello, World!'" > README.md
touch LICENSE

スクリプトを実際に書いていく

実際にコマンドを実行した際に行われる処理をスクリプトファイルへ書いていきます。functions/helloworld.fishへ次の内容を記述してみましょう。

functions/helloworld.fish
function helloworld
    echo "Hello, World!"
end

コマンドを実行した際に、helloworld関数が呼び出されます。

出来たコマンドをインストールしてみる

fish shellでは、~/.config/fish/functions下にあるスクリプトファイルをコマンドとして認識します。毎回手動でコピーして動作確認しても良いですが、面倒なので今回はfisherというプラグインマネージャーを使います。

まずはfisherをインストールします。

curl -sL https://git.io/fisher | source && fisher install jorgebucaran/fisher

次に、実際に先ほど書いたコマンドをインストールします。

# プロジェクトのルートディレクトリにいるか確認する
$ pwd
/your/path/to/helloworld.fish

# 実際にインストールする
$ fisher install .
fisher install version 4.3.5
Installing /your/path/to/helloworld.fish
           ~/.config/fish/functions/helloworld.fish
           ~/.config/fish/completions/helloworld.fish
Installed 1 plugin/s

実際にコマンドを実行してみて、動作確認してみる。

$ helloworld
Hello, World!

また、変更を反映させたい場合には、次のコマンドを実行します。

fisher update /your/path/to/helloworld.fish

次のワンライナーでも同様のことを出来るので、適当にエイリアスを登録しておくと便利かも?しれません。

fisher update (git rev-parse --show-toplevel 2>/dev/null)

コマンドに機能追加していく

-h,--helpオプションの実装

helloworld関数の冒頭に、argparseコマンドを記述することで、オプションを解析できるようにします。argparseコマンドは、与えられたコマンドライン引数に指定したオプションがある場合には、それを示す変数を生成します。次のコードでは、-h,--helpが引数に与えられた場合には、_flag_help変数が生成されます。

functions/helloworld.fish
  function helloworld
+     argparse --name=helloworld \
+         h/help \
+         -- $argv or return
  
      echo "Hello, World!"
  end

helpオプションが与えられた場合には、helpメッセージが表示されるようにします。set -qlコマンドで_flag_help変数が存在するかどうかを調べることで、helpメッセージを表示するかどうかを判断します。

functions/helloworld.fish
  function helloworld
      argparse --name=helloworld \
          h/help \
          -- $argv or return
  
+     if set -ql _flag_help
+         echo "Usage: helloworld"
+         echo ""
+         echo "Options:"
+         echo "  -h, --help  Show this help message and exit"
+         return 0
+     else
          echo "Hello, World!"
+         return 0
+     end
+ 
+     return 1
  end

実際に動作確認をしてみる。

# 変更を反映させる
$ fisher update /your/path/to/helloworld.fish

# 実行する
$ helloworld --help
Usage: helloworld

Options:
  -h, --help  Show this help message and exit

補完の設定を書く

completions/helloworld.fishに次の内容を書き込む。

# tabでファイル名が補完されるのを無効化する
complete --command=helloworld --no-files

# -h, --help が補完されるようにする
complete --command=helloworld --short=h --long=help --description="Show help message."

これでtab補完が効くようになっているはずです。

補完の画像

テストしてみる

テスト用のディレクトリとファイルを作る

まずはテスト用にtestディレクトリを作り、その中にhelloworld.fishを作ります。そしてhelloworld.fishにテストを記述していきます。

mkdir test
touch test/helloworld.fish

まずはテストが成功しているかどうかの状態を保持するis_success変数を宣言します。

test/helloworld.fish
#!/usr/bin/env fish
set is_success true

そして、得られた結果と期待される結果が一致するかを確認し、一致しない場合にはis_success変数をfalseにするassert関数を作ります。

test/helloworld.fish
  #!/usr/bin/env fish
  set is_success true
  
+ function assert
+     set expected $argv[1]
+     set actual $argv[2]
+     if [ "$expected" != "$actual" ]
+         echo "ERROR: expected '$expected', got '$actual'"
+         set is_success false
+     else
+         echo "SUCCESS: expected '$expected', got '$actual'"
+     end
+ end

実際にコマンドを実行して、期待される結果を得られるかテストするコードを書く。

test/helloworld.fish
  #!/usr/bin/env fish
  set is_success true
  
  function assert
  # 省略...
  end
  
+ echo "=== START TESTS ==="
+ 
+ # * test no args
+ set expected "Hello, World!"
+ set actual (helloworld)
+ assert "$expected" "$actual"
+ 
+ # * test -h,--help
+ set expected "Usage: helloworld\n\nOptions:\n  -h, --help  Show this help message and + exit"
+ set actual (helloworld --help | string join "\n")
+ assert "$expected" "$actual"
+ 
+ set actual (helloworld -h | string join "\n")
+ assert "$expected" "$actual"
+ 
+ echo "===  END TESTS  ==="

いくつかのテストケースで失敗した場合には、exit 1するようにする。

test/helloworld.fish
  #!/usr/bin/env fish
  set is_success true
  
  function assert
  # 省略...
  end
  
  echo "=== START TESTS ==="
  # 省略...
  echo "===  END TESTS  ==="
  
+ if $is_success
+     echo ""
+     echo "All tests passed!"
+     exit 0
+ else
+     echo ""
+     echo "Some tests failed!"
+     exit 1
+ end

これで、実際にテストをするためのスクリプトが完成した。

# テスト用スクリプトに実行権限を付与する
$ chmod +x test/helloworld.fish

# 実際にテストを実行する
$ ./test/helloworld.fish
=== START TESTS ===
SUCCESS: expected 'Hello, World!', got 'Hello, World!'
SUCCESS: expected 'Usage: helloworld\n\nOptions:\n  -h, --help  Show this help message and exit', got 'Usage: helloworld\n\nOptions:\n  -h, --help  Show this help message and exit'
SUCCESS: expected 'Usage: helloworld\n\nOptions:\n  -h, --help  Show this help message and exit', got 'Usage: helloworld\n\nOptions:\n  -h, --help  Show this help message and exit'
===  END TESTS  ===

All tests passed!

GitHub Actionsでテストが実行されるようにする

.gihub/workflowsディレクトリを作り、その中にテストを実行するtest.ymlを作成する。このアクションでは、先ほど作ったテスト用のシェルスクリプトが自動実行され、テストに失敗した場合にはアクションが失敗します。

.github/workflows/test.yml
name: test
run-name: test
on: [push]
jobs:
  run-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Install fish shell
        run: |
          sudo apt-add-repository --yes --no-update ppa:fish-shell/release-3
          sudo apt-get update -qq
          sudo apt-get install --yes fish
        shell: bash
      - name: Install fisher and install `helloworld` plugin
        run: |
          curl -sL https://git.io/fisher | source && fisher install jorgebucaran/fisher
          fisher install ./
        shell: fish {0}
      - name: Run tests
        run: |
          chmod +x ./test/test.fish
          ./test/test.fish
        shell: fish {0}

また余談ですが、ローカルでgithub actionsを実行したい場合には、actが便利なのでおすすめです。

fisher でインストールしてみる

今まではローカルディレクトリから作ったコマンド(プラグイン)をインストールしていましたが、これでは他のPCからインストールする際などには不便です。なので、この章ではgithubのリポジトリからインストールしてみましょう。

まずは、リモートリポジトリに今までの変更をpushしましょう(もうすでにしている方は必要ありません)。

gh repo create --public helloworld.fish
git add .
git commit -m 'first'
git remote add origin https://github.com/repos/YOURNAME/helloworld.fish
git push origin main

これで準備は整ったので、今までローカルディレクりからインストールしていたhelloworldコマンドを削除しましょう。

fisher remove /your/path/to/helloworld.fish

そして、GitHubのリポジトリからhelloworldコマンドをインストールしましょう。fisher install <GitHubの名前>/<リポジトリ名>でインストールできます。

fisher install YOURNAME/helloworld.fish

おわりに

今回は、生産性がなく単に「Hello, World!」を表示するだけのコマンドを作成しました。しかし、この記事によって、fish shellでコマンド(プラグイン)を作成する手順を掴めていただけたら幸いです。

https://github.com/r4ai/helloworld.fish

Discussion