跳到主要内容

fzf

· 阅读需 20 分钟 ·
Castamere
热爱 Coding

fzf (fuzzy finder) 是一个命令行模糊查找工具。这是一个很强大的工具,发挥你的想象力,和我一起探索 fzf

fzf

目前搞定了以下有趣的功能:

提示

若有新功能建议,欢迎在 GitHub 提交 issue

重要

笔者使用的 fzf 的版本为 0.62.0 (d226d841),不同的版本可能有所差异

缘起

起因是在冲浪时看到了这样一个可视化命令行的轻量软件,当时还没意识到 fzf 有多么强大,包含了可视化、高效搜索、预览等功能。一边看发行说明,一边觉得可以有好多有趣的应用,遂研究

读者可以全篇读一遍,再去一键配置进行安装

安装 fzf

建议直接去 github 下载安装,点击链接直达 release

Linux 下的包管理器中的版本都很旧。笔者建议用 uname --all 查看系统信息,去 github 下载对应版本

下载完成后,上传到服务器,解压。将 fzf 复制到 /usr/bin 目录下

Linux

tar -xzf xxx.tar
cp fzf /usr/bin

fzf 入门

安装好 fzf 之后,直接在终端输入 fzf,可以看到,fzf 会自动将该目录下的所有文件及子文件夹下的文件递归列出来,并转为一个可以选择的列表

fzf

这个工具的功能很强大,笔者暂时无法完全进行详解,仅带各位入门。笔者将分为四部分来介绍 fzf 的基本功能: 输入搜索预览输出

fzf 对于初学者来说比较乱的一点是:它有两个输入和两个输出

可能看起来比较复杂,笔者来逐一介绍

输入

这里的"输入",就是被搜索的内容。我们一般通过管道来输入到 fzf,比如这里我们演示,从所有进程中进行搜索

重要

为了方便演示,笔者这里就只用最简单的命令,避免输出过长影响效果

这里可以看到,将 ps 的输出通过管道传递给 fzf,就可以进行选择

ps

但存在以下几个常见问题。首先我们对比一下单独执行 ps(左) 和通过管道传递给 fzf(右) 的效果

ps_fzf_compare

先忽略 ttypid 的不同,不难发现,fzf 的输入输出是类栈的,即先进后出,顺序是反的。这样的设计应该是为了在行数较多时,更好的显示和选择,但在需要关注顺序时,就会有些麻烦

我们可以通过添加 --reverse 参数来反转顺序,注意该参数会将输入行改到最上方

左边为 ps | fzf --reverse,右边为 ps | fzf

fzf_reverse

第二个问题是,回看上面的图片,ps 输出是一个表格,我们明显不想让表头也成为被搜索的一部分。一方面,可能造成歧义,另一方面,当搜索内容列表过长时,在翻到下面的时候,就看不到表头了

我们可以通过添加 --header-lines= 参数来指定表头的行数

可以测试一下 ps -aux | fzf --reverse --header-lines=1 的效果

fzf_header_lines

可以看到,表头是无法被选中的,而且在滚动时也不会消失

--header-lines= 是指定前 n 行作为表头,在某些需求下会有奇效

搜索

搜索,就类似你 grep 的内容,从上面的输入中进行匹配

这里偷个懒,引用一下官方给出的搜索逻辑,类似正则规则

  • ': 精确匹配
  • ^: 开头
  • $: 结尾
  • !: 否
  • |: 或
Token匹配类型描述
sbtrkt模糊匹配包含 sbtrkt 字符,并且字符出现顺序一致
'wild精确匹配包含单词 wild(非单词边界也匹配)
'wild'边界精确匹配包含完整单词 wild,要求单词边界匹配
^music前缀精确匹配以 music 开头的项
.mp3$后缀精确匹配以 .mp3 结尾的项
!fire反向精确匹配不包含 fire 的项
!^music反向前缀精确匹配不以 music 开头的项
!.mp3$反向后缀精确匹配不以 .mp3 结尾的项

比较常用的就是加 ' 进行精确匹配,下图就是一个例子

exact_match

当需要多个匹配条件时,空格隔开即可; 如果是要 or 逻辑的话,就用 | 隔开

syntax_2

预览

fzf 另一个强大的功能就是预览,也正是这个预览,可以让我们玩出很多花样来

这个预览,就是对当前选中的选项,进行某些操作。比如这样的一个需求:我们想要选择一个文件,在预览中查看该文件的内容,就可以用下面的命令

fzf --preview="cat {}"

fzf_preview

这里的 batcat 是一个类似 cat 的命令,对代码高亮有更好的支持

接下来重点讲解一下预览的语法

--preview 后面是一个命令,其中的 {} 是一个占位符,会替换为当前选中的选项。比如上面的演示中,选中的是文件名,就会被替换为类似 cat /path/to/file 的命令。然后将这个输出显示在预览框里

需要注意的是,这个命令可以是一个很完整的 sh 命令或脚本,这就给了我们很高的可玩性,下面我用一个例子抛个砖

fzf preview
docker ps | fzf \
--header-lines=1 \
--preview="echo {} | awk '{print \$1}' | xargs docker logs -n 100"
提示

这里还有一种更简单的方法,--preview="docker logs {1}" 在后面会继续讲

fzf_preview_2

这个命令的作用是,列出所有的 docker 容器,并在预览中显示该容器的日志。这里的 {} 会被替换为选中的容器信息,然后通过 awk 提取出容器 ID,再通过 xargs 将其传递给 docker logs 命令,最后显示最近 100 行日志

这只是一个很简陋的例子,还有很多可以优化的点,但也不难看出,可玩性真的很高,下面笔者列几个很常见的点

  • 预览样式: 在这里可以找到有关预览的边框,间隔,颜色等内容
  • 跳转预览行数: 在预览时,我们有时会想初始直接跳转到某一行,比如在集成 grep 时,自动跳转到 grep 到的那行;或者在显示日志时,直接跳转到末尾
--preview-window=follow # 跳转到末尾
--preview-window=+100 # 跳转到第 100 行
  • 修改预览窗口位置与比例
--preview-window=right:60% # 预览窗口在右面,占整个界面 60%
--preview-window=down:50% # 预览窗口在下面,占整个界面 50%

# preview window help 中的其他选项
PREVIEW WINDOW
--preview=COMMAND Command to preview highlighted line ({})
--preview-window=OPT Preview window layout (default: right:50%)
[up|down|left|right][,SIZE[%]]
[,[no]wrap][,[no]cycle][,[no]follow][,[no]info]
[,[no]hidden][,border-STYLE]
[,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
[,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]
--preview-border[=STYLE] Short for --preview-window=border-STYLE
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
top|bottom|left|right|line|none] (default: rounded)
--preview-label=LABEL
--preview-label-pos=N Same as --border-label and --border-label-pos,
but for preview window
重要

使用 fzf --help 查看更多参数

输出

最后就来到了我们的输出,默认的输出就是你选中的内容,比如选中 docker ps 的结果:

但我们可能只想要某一项,用于后续操作。当然,我们可以用管道 + awk 来实现,但 fzf 给了我们一个更简单的解决办法 --accept-nth <n> ,可以让我们选择输出第几项,默认用空格隔开,比如:

只输出了容器的 ID,我们就可以进行后续的操作,比如: 选择容器 -> 删除容器

docker ps | fzf --accept-nth 1 | xargs docker rm 

上文我们有讲到,在 preview 中,可以通过 {}{n} 来获取选择的整行,或者某项。同样也是默认用空格隔开,比如

fzf preview
docker ps | fzf \
--header-lines=1 \
--preview="docker logs -n 100 {1}"

我们把刚刚的两步结合起来,就可以实现: 选择容器(选择时预览容器日志) -> 删除容器

docker ps | fzf \
--accept-nth 1 \
--header-lines=1 \
--preview="docker logs -n 100 {1}" \
| xargs docker rm

但有时候可能不是空格隔开的内容

可以看到,grep 的输出是由 : 分隔的,每一项分别是: 文件名、行号、内容。其中的每一项我们都可以用到,具体在下面的grep 并预览结果会讲。这里主要关注如何使用 : 作为分隔符

我们可以添加 --delimiter ':' 参数来指定分隔符。注意这样过后,不论是在 --preview 中使用 {1} 还是在 --accept-nth 2 中,都是按照 : 作为分隔符,有时可能造成混淆

Docker

目前 Docker 实现了如下功能

重要

可以跳转到一键配置进行安装,下面内容只做演示,不需要一个一个复制

从所有容器中选择

从所有容器中选择,并可以预览最后 100 行日志,输出为容器 ID

Container All

完整代码
从所有容器中选择
ContainerAll () {
# choose from all containers
local header=$'NAME\tCONTAINER ID\tIMAGE\tSTATUS'
local data combined selected preview_lines
data=$(docker ps -a --format '{{.Names}}|{{.ID}}|{{.Image}}|{{.Status}}' | \
awk -F'|' '{
name = length($1) > 20 ? substr($1, 1, 17) "..." : $1;
printf "%-20s\t%s\t%s\t%s\n", name, $2, $3, $4
}')
combined="$header"$'\n'"$data"
formatted=$(echo "$combined" | column -t -s $'\t')

preview_lines=100

selected=$(echo "$formatted"| fzf \
--reverse \
--height 80% \
--header-lines="1" \
--preview-label="🐳 Preview" \
--preview="docker logs -n $preview_lines {1}" \
--preview-window=follow\
--accept-nth=2
)

echo $selected
}

从所有运行容器中选择

从所有运行容器中选择,并可以预览最后 100 行日志,输出为容器 ID,可以用于衔接进入容器,或 inspect 容器

Container UP

完整代码
从所有运行容器中选择
ContainerUP () {
# choose from all up containers
local header=$'NAME\tCONTAINER ID\tIMAGE\tSTATUS'
local data exited running combined selected
data=$(docker ps -a --format '{{.Names}}|{{.ID}}|{{.Image}}|{{.Status}}' | \
awk -F'|' '{
name = length($1) > 20 ? substr($1, 1, 17) "..." : $1;
printf "%-20s\t%s\t%s\t%s\n", name, $2, $3, $4
}')
exited=$(echo "$data" | awk -F'\t' '$4 ~ /^Exited/ { print }')
running=$(echo "$data" | awk -F'\t' '$4 !~ /^Exited/ { print }')
combined="$header"$'\n'"$exited"$'\n'"$running"
formatted=$(echo "$combined" | column -t -s $'\t')

preview_lines=100
exited_count=$(echo "$exited" | grep -c '^')

selected=$(echo "$formatted"| fzf \
--reverse \
--height 80% \
--header-lines=$((1 + exited_count)) \
--preview-label="🐳 Preview" \
--preview="docker logs -n $preview_lines {1}" \
--preview-window=follow\
--accept-nth=2
)

echo $selected
}

从所有停止容器中选择

从所有停止容器中选择,并可以预览最后 100 行日志,输出为容器 ID,可以用于删除容器等

Container UP

完整代码
从所有停止容器中选择
ContainerDown () {
# choose from all down containers
local header=$'NAME\tCONTAINER ID\tIMAGE\tSTATUS'
local data exited running combined selected
data=$(docker ps -a --format '{{.Names}}|{{.ID}}|{{.Image}}|{{.Status}}' | \
awk -F'|' '{
name = length($1) > 20 ? substr($1, 1, 17) "..." : $1;
printf "%-20s\t%s\t%s\t%s\n", name, $2, $3, $4
}')
exited=$(echo "$data" | awk -F'\t' '$4 ~ /^Exited/ { print }')
running=$(echo "$data" | awk -F'\t' '$4 !~ /^Exited/ { print }')
combined="$header"$'\n'"$running"$'\n'"$exited"
formatted=$(echo "$combined" | column -t -s $'\t')

preview_lines=100
running_count=$(echo "$running" | grep -c '^')

selected=$(echo "$formatted"| fzf \
--reverse \
--height 80% \
--header-lines=$((1 + running_count)) \
--preview-label="🐳 Preview" \
--preview="docker logs -n $preview_lines {1}" \
--preview-window=follow\
--accept-nth=2
)

echo $selected
}

选择并进入容器

效果如下,搭配了前面的 ContainerUP。使用 docker exec -it [CONTAINERID] bash 连接容器

Docker Enter

选择并进入容器
enter() {
local selected=$(ContainerUP)
if [ -z "$selected" ]; then
echo "Canceled"
return 1
fi
docker exec -it $selected bash
}

选择并删除容器

删除就不演示了,搭配了前面的 ContainerDown

选择并删除容器
ddel() {
local selected=$(ContainerDown)
if [ -z "$selected" ]; then
echo "Canceled"
return 1
fi
docker rm $selected
}

# 强制删除(可以删运行中的)
dfdel() {
local selected=$(ContainerAll)
if [ -z "$selected" ]; then
echo "Canceled"
return 1
fi
docker rm -f $selected
}

grep

重要

可以跳转到一键配置进行安装,下面内容只做演示,不需要一个一个复制

效果如下,右边的窗口可以进行预览,并会直接跳转到对应的行数附近

注意

需要安装 batcat 实现高亮

ffgrep

完整代码
grep 并预览结果
ffgrep() {
local query="$*"
local ans
local cmd_height=$(awk "BEGIN { printf \"%d\", $(tput lines) * 0.8 - 6 }")
local offset=$(awk "BEGIN { printf \"%d\", $cmd_height * 0.5 }")

ans=$(grep -rnI --color=always -E "$query" . 2>/dev/null | \
fzf --ansi \
--delimiter ':' \
--height=80% --reverse \
--preview='batcat --color=always --paging=never {1} --highlight-line={2} --wrap=character' \
--preview-window=right:60%,wrap,+{2}-$offset \
)

if [[ -n "$ans" ]]; then
echo $ans | head -n1 | awk -F: '{print $1":"$2}'
fi
}

进程

重要

可以跳转到一键配置进行安装,下面内容只做演示,不需要一个一个复制

查找并杀死进程
fkill() {
local pid
pid=$(ps aux | fzf --accept-nth 2)
if [ -n "$pid" ]; then
kill -9 "$pid"
fi
}

Conda

重要

可以跳转到一键配置进行安装,下面内容只做演示,不需要一个一个复制

进入 Conda 环境

效果如下,在选择环境时,可以预览该环境有哪些 pip

Conda activate

完整代码
进入 Conda 环境
conda_activate() {
local env envs
envs=$(conda env list | awk 'NF && $0 !~ /^#/')
env=$(echo "$envs" | fzf \
--preview='
pippath={-1}/bin/pip
"$pippath" list
' \
--prompt="Activate Conda Env > " \
--height=80% \
--reverse \
--accept-nth 1 \
)

if [[ -n "$env" ]]; then
echo "🔄 Activating Conda environment: $env"
conda activate $env
else
echo "❌ Cancelled."
fi
}

搜索 Conda 环境

效果如下,在所有 Conda 环境中搜索 pip 包,并预览 pip show。在 Conda 环境多起来之后,十分好用

Conda search

完整代码
搜索 Conda 环境
conda_search() {
local rows=""
local envs
envs=$(conda env list | awk 'NF && $0 !~ /^#/' | awk '{print $1}')

while read -r env; do
while IFS=$'\t' read -r name version; do
[[ -n "$name" ]] && rows+="$env\t$name\t$version"$'\n'
done < <(conda run -n "$env" pip list --format=columns 2>/dev/null | awk 'NR > 2 {print $1 "\t" $2}')
done <<< "$envs"

if [[ -z "$rows" ]]; then
echo "⚠️ Nothing Here"
return 1
fi

{
echo -e "ENV\tPACKAGE\tVERSION"
echo -e "$rows"
} | column -t -s $'\t' | \
fzf \
--prompt="🔎 Search pip packages > " \
--header-lines=1 \
--reverse \
--nth 2 \
--accept-nth 2 \
--color nth:regular,fg:dim \
--height=90% \
--preview='
env=$(echo {} | awk "{print \$1}")
pkg=$(echo {} | awk "{print \$2}")
conda run -n $env pip show $pkg 2>/dev/null || echo "📦 Nothing Here"
'
}

一键配置

脚本本身需要 fzf 以及一些其他依赖, 请确保在使用这些脚本前安装了这些依赖

本项目在 GitHub 上开源,地址为 Casta-mere/fzf_scripts,可访问并下载

这里提供两种安装方法

命令安装

该方式需要设备能连接到 github, 若无法连接或下载超时请使用手动安装

curl -fsSL https://github.com/Casta-mere/fzf_scripts/releases/download/V0.1.0/install.sh -o ./install.sh
chmod +x ./install.sh
./install.sh --install

手动安装

  1. 点击 fzf_scripts 下载 install_pack.tar.gz
  2. 上传文件到设备并切换到该目录
  3. 使用 tar -xzvf install_pack.tar.gz 解压
  4. 使用 chmod +x ./install.sh && ./install.sh --install 安装

后记

fzf 确实是一个很好玩的工具,笔者也是根据自己的实际需求写了以上小工具,读者若有新功能建议,欢迎提交 issue 到 GitHub

请作者喝可乐🥤:
本文遵循 CC 4.0 BY-SA 版权协议,转载请标明出处