mise run 〇〇(タスクランナー)をfzfで実行しやすくする

miseのタスクランナーはmise run 〇〇で実行できます。これをfzfと連携してさくっと実行するzshのスクリプトを紹介します。

2024年12月12日追記!
miseのtomlファイルが複数ある場合やタスクのrunが配列の場合にも対応しました。

動作の例

筆者はmise run 何だったっけな~とタスク名を忘れて、その度にtomlファイルを確認していました。
これをfzfを使って解決します。

実際に使っている例を見てみましょう。

次のようなファイルを対象に実行しました。

.mise.toml
[tasks.hello]
run = "echo 'hello'"
[tasks.echopath]
run = "echo $PATH | tr ':' '\n'"
[tasks.rev]
run = "rev"

mise runが初めての人

まだmise runを使ったこと無い人は、初回で次のようにエラーが出ます(2024年10月現在)。

mise `mise run` is experimental. Enable it with `mise settings set experimental true`
mise Run with --verbose or MISE_VERBOSE=1 for more information

書いてあるとおりmise runは実験的機能であるため、コマンドでその設定をオンにしておきましょう。

Terminal window
mise settings set experimental true

依存

yqとzshを使っていますが、どちらも別のものに置き換え可能です。

yqはtmolファイルからタスクを抽出するために使っています。
zshはキーバインドの割り当てと履歴を残すためにZLEを使っています。

実装の紹介

先にスクリプトの全体を紹介します。

function fzf_mise_tasks() {
# mise settings set experimental true
# 検索対象のファイルリスト
local files=( ".mise.local.toml" "mise.local.toml" ".mise.toml" "mise.toml" "mise/config.toml" )
local found_files=()
for file in "${files[@]}"; do
if [[ -e $file ]]; then
found_files+=("$file")
fi
done
if [[ ${#found_files[@]} -eq 0 ]]; then
echo 'fzf_mise_tasks'
echo 'No target mise file found'
zle send-break
return 1
fi
if ! type yq > /dev/null; then
echo 'fzf_mise_tasks'
echo 'yq command is required'
zle send-break
return 1
fi
local tasks_list=()
for file in "${found_files[@]}"; do
local file_tasks=$(yq -oj '.tasks | to_entries | .[] | .key + " = " + (
select(.value.run| type == "!!str") .value.run,
select(.value.run| type == "!!seq") .value.run | join(" && ")
)' "$file" 2>/dev/null || echo '')
if [[ ${file_tasks:-null} != null ]]; then
tasks_list+=("$file_tasks")
fi
done
local tasks=$(printf "%s\n" "${tasks_list[@]}")
if [[ -z $tasks ]]; then
echo 'fzf_mise_tasks'
echo 'There is no tasks in .mise.toml'
zle send-break
return 1
fi
local selected=`echo "$tasks" | sed 's/^"//;s/"$//' | FZF_DEFAULT_OPTS='' fzf --height=50% --reverse --exit-0 | awk -F ' = ' '{ print $1}'`
zle reset-prompt
if [[ -z $selected ]]; then
return 0
fi
# 再実行したくなるときのために履歴に残して実行
BUFFER="mise run $selected"
zle accept-line
}
zle -N fzf_mise_tasks
bindkey "^Xm" fzf_mise_tasks

次のセクションから小分けにして順に解説します。

tomlファイルがない場合にエラー

miseで扱うtomlファイルが無い場合はエラーにします。

local files=( ".mise.local.toml" "mise.local.toml" ".mise.toml" "mise.toml" "mise/config.toml" )
local found_files=()
for file in "${files[@]}"; do
if [[ -e $file ]]; then
found_files+=("$file")
fi
done
if [[ ${#found_files[@]} -eq 0 ]]; then
echo 'fzf_mise_tasks'
echo 'No target mise file found'
zle send-break
return 1
fi

send-breakを使わないと、エラーになったときに次のプロンプトが表示されません。

send-breakを使わなかった場合

send-breakを使えば、今のプロンプトを終了して新しいプロンプトに切り替わります。

send-breakを使った例

yqがなかったらエラー

上記と同様にyqがなかったらエラーにしています。

if ! type yq > /dev/null; then
echo 'fzf_mise_tasks'
echo 'yq command is required'
zle send-break
return 1
fi

タスクを抽出

yqを使ってtomlファイルからタスクを抜き出しています。

local tasks_list=()
for file in "${found_files[@]}"; do
local file_tasks=$(yq -oj '.tasks | to_entries | .[] | .key + " = " + (
select(.value.run| type == "!!str") .value.run,
select(.value.run| type == "!!seq") .value.run | join(" && ")
)' "$file" 2>/dev/null || echo '')
if [[ ${file_tasks:-null} != null ]]; then
tasks_list+=("$file_tasks")
fi
done
local tasks=$(printf "%s\n" "${tasks_list[@]}")

この段階ではまだ実行していないため、コマンド部分を改行して掲載します。

yq -oj '.tasks | to_entries | .[] | .key + " = " + (
select(.value.run| type == "!!str") .value.run,
select(.value.run| type == "!!seq") .value.run | join(" && ")
)' "$file" 2>/dev/null
|| echo ''

ざっくりとこんなステップです。

  • yqを実行してtomlファイルからtaskを抽出する
    • runが文字列ならそのまま出力
    • runが配列なら&&で文字列として結合して出力
  • yqでエラーがあったら捨てる
  • エラーがあったら空文字にする

yqの実行後は次のように"タスク名 = コマンドの内容"のような文字列に変換されています。

抽出後
"hello = echo 'hello'"

yqのオプション-ojで出力をJSON形式にしています。こうしないとコマンドに改行を示す\nが入っている場合にうまく処理できないためです。

タスクがなければエラー

タスクがなかったらエラーにします。

if [[ -z $tasks ]]; then
echo 'fzf_mise_tasks'
echo 'There is no tasks in .mise.toml'
zle send-break
return 1
fi

文字列を整形とfzfの実行

local selected=`echo "$tasks" | sed 's/^"//;s/"$//' | FZF_DEFAULT_OPTS='' fzf --height=50% --reverse --exit-0 | awk -F ' = ' '{ print $1}'`

また長いコマンドが訪れたので改行したものを見てみましょう。

eval $tasks \
| sed 's/^"//;s/"$//' \
| FZF_DEFAULT_OPTS='' fzf \
--height=50% \
--reverse \
--exit-0 \
| awk -F ' = ' '{ print $1}'

yqの結果は"hello = echo 'hello'"のようにダブルクオートが付いているため、sedで葬ります。

fzf実行後、選択した文字列のうちタスク名の部分だけを抜き出します。

プロンプトを弄ってから実行

zle reset-prompt
if [[ -z $selected ]]; then
return 0
fi
# 再実行したくなるときのために履歴に残して実行
BUFFER="mise run $selected"
zle accept-line

プロンプトをリセットして、未選択だったらそのまま終了します。

最後に、履歴にmise run 〇〇という実行記録を残すためにプロンプトを書き換えてから実行します。

キーバインドのセット

キーバインドの設定も忘れずに。

zle -N fzf_mise_tasks
bindkey "^Xm" fzf_mise_tasks

以上、miseのタスクランナーとfzfの連携でした。

npm run 〇〇pnpmにも対応)についても書いてます。Node.jsを使う人にはおすすめです。
fzfでpackage.jsonのscriptsを選んで実行 | eiji.page