ANSI 转义序列在被脚本调用时中断
ANSI escape sequences breaking when called by a script
一段时间以来,我一直在使用名为 fzf 的程序在脚本中选择内容,但有几次我发现自己试图 运行 在未安装此程序的系统上使用这些脚本。
为了解决这个问题,我写了一个迷你克隆(但在大型数据集上显然慢得多)我可以嵌入我的脚本中以完全避免依赖。
它使用 ANSI 转义序列来“召唤第二个终端”(我不记得它叫什么)并避免用垃圾填满常规终端。如果我手动调用该脚本,它可以正常工作,但如果我在子 shell 或函数中调用它,它就会完全中断。如果我将输出或 运行 输出到子 shell 中,它还会将转义序列吐到下一个程序中。
此时我已经花了很多时间尝试调试它,但我不确定如何继续。如果在脚本中调用,是否有其他方法可以使用这些序列。
代码如下:
#!/bin/bash
# FZY: Command Line Fuzzy Finder
# Created: 29/10/2020
# Author: Nan0Scho1ar (Christopher Mackinga)
# License: MIT License
input="$(< /dev/stdin)"; height="$(tput lines)"; inum=$(echo "$input" | wc -l)
echo -e "\e[?1049h" 1>&2; row=1; col=1; str=""; cur=1; fnum=$inum; regex=""; regex2=""
while true; do
range="$row,$((row+height-3))p;$((row+height-3))q"
filtered=$(echo "$input" | grep ".*$regex" | sed -n $range 2>/dev/null | sed -e 's/.*/ &/');
frange="$(echo "$filtered" | wc -l)"
curpos=$((frange-cur+1))
echo "$filtered" | cut -c$col- | grep -E --color=always "$regex2" | tac | sed $curpos's/^ \(.*\)/> /'
echo " $fnum/$inum" && read -r -p "> $str" -n 1 -s < /dev/tty && read -r -sn3 -t 0.001 k1 < /dev/tty; REPLY+=$k1;
case "$REPLY" in
'') echo -e "\e[?1049l" 1>&2; echo "$filtered" | sed -n "${cur}s/ //p;${cur}q;" && exit;;
$'\e[C'|$'\e0C') col=$((col+1));;
$'\e[D'|$'\e0D') [[ $col -gt 1 ]] && col=$((col-1));;
$'\e[B'|$'\e0B') [[ $cur -ge 1 ]] && cur=$((cur-1));;
$'\e[A'|$'\e0A') [[ $cur -le $fnum ]] && cur=$((cur+1));;
$'\e[1~'|$'\e0H'|$'\e[H') row=1;;
$'\e[4~'|$'\e0F'|$'\e[F') row=$fnum;;
*)
char=$(echo "$REPLY" | hexdump -c | awk '{ print }');
if [[ $char = "033" ]]; then echo -e "\e[?1049l" 1>&2 && break;
elif [[ $char = "177" ]] && [[ ${#str} -gt 0 ]]; then str="${str::-1}";
else str="$str$REPLY" && row=1; fi
regex=$(echo "$str" | sed "s/\(.\)/.*/g");
regex2=$(echo "$str" | sed "s/\(.\)/|/g");
fnum=$(echo "$input" | grep -c ".*$regex")
;;
esac
[[ $((frange-cur+1)) -lt 1 ]] && row=$((row+1)) && cur=$((cur-1));
[[ $cur -lt 1 ]] && row=$((row-1)) && cur=$((cur+1));
[[ $cur -gt $fnum ]] && cur=$fnum;
[[ $((row-fnum+frange)) -gt 1 ]] && row=$((row-1))
[[ $row -lt 1 ]] && row=1;
yes '' | sed "${height}q";
done
一个有效的用法示例:
echo 'test 1
test 2
test 3' | fzy
一个无效的用法示例:
output=$(echo 'test 1
test 2
test 3' | fzy)
编辑:如评论中所述,不捕获子 shell 中的输出并不代表我的实际用例。更新了示例失败案例。
EDIT2:将控制序列重定向到 stderr 似乎有所改进。更新代码以反映此更改。
我的问题的解决方案是将所有控制序列和 menu/UI 相关行发送到 stderr,如评论中建议的 Charles Duffey。感谢他指出这一点。
这是应用了更改的最终代码。 (还有一些小错误,但它们与这个问题完全无关。)
请注意 echo -ne "\e[?1049h"
等一些内容已更改为 echo -ne "\e[?1049h" 1>&2
#!/bin/bash
# FZY: Command Line Fuzzy Finder
# Created: 29/10/2020
# Author: Nan0Scho1ar (Christopher Mackinga)
# License: MIT License
input="$(< /dev/stdin)"; height="$(tput lines)"; inum=$(echo "$input" | wc -l)
echo -ne "\e[?1049h\r" 1>&2; row=1; col=1; str=""; cur=1; fnum=$inum; regex=""; regex2=""
while true; do
range="$row,$((row+height-3))p;$((row+height-3))q"
filtered=$(echo "$input" | grep ".*$regex" | sed -n $range 2>/dev/null | sed -e 's/^.*/ &/');
frange="$(echo "$filtered" | wc -l)"
curpos=$((frange-cur+1))
echo "$filtered" | cut -c$col- | grep -E --color=always "$regex2" | tac | sed $curpos's/^ \(.*\)/> /' 1>&2
echo " $fnum/$inum" 1>&2 && read -r -p "> $str" -n 1 -s < /dev/tty && read -r -sn3 -t 0.001 k1 < /dev/tty; REPLY+=$k1;
case "$REPLY" in
'') echo -ne "\e[?1049l" 1>&2; echo "$filtered" | sed -n "${cur}s/ //p;${cur}q;" && exit;;
$'\e[C'|$'\e0C') col=$((col+1));;
$'\e[D'|$'\e0D') [[ $col -gt 1 ]] && col=$((col-1));;
$'\e[B'|$'\e0B') [[ $cur -ge 1 ]] && cur=$((cur-1));;
$'\e[A'|$'\e0A') [[ $cur -le $fnum ]] && cur=$((cur+1));;
$'\e[1~'|$'\e0H'|$'\e[H') row=1;;
$'\e[4~'|$'\e0F'|$'\e[F') row=$fnum;;
*)
char=$(echo "$REPLY" | hexdump -c | awk '{ print }');
if [[ $char = "033" ]]; then echo -e "\e[?1049l" 1>&2 && exit 1;
elif [[ $char = "177" ]] && [[ ${#str} -gt 0 ]]; then str="${str::-1}";
else str="$str$REPLY" && row=1; fi
regex=$(echo "$str" | sed "s/\(.\)/.*/g");
regex2=$(echo "$str" | sed "s/\(.\)/|/g");
fnum=$(echo "$input" | grep -c ".*$regex")
;;
esac
[[ $((frange-cur+1)) -lt 1 ]] && row=$((row+1)) && cur=$((cur-1));
[[ $cur -lt 1 ]] && row=$((row-1)) && cur=$((cur+1));
[[ $cur -gt $fnum ]] && cur=$fnum;
[[ $((row-fnum+frange)) -gt 1 ]] && row=$((row-1))
[[ $row -lt 1 ]] && row=1;
yes '' | sed "${height}q" 1>&2;
done
一段时间以来,我一直在使用名为 fzf 的程序在脚本中选择内容,但有几次我发现自己试图 运行 在未安装此程序的系统上使用这些脚本。
为了解决这个问题,我写了一个迷你克隆(但在大型数据集上显然慢得多)我可以嵌入我的脚本中以完全避免依赖。
它使用 ANSI 转义序列来“召唤第二个终端”(我不记得它叫什么)并避免用垃圾填满常规终端。如果我手动调用该脚本,它可以正常工作,但如果我在子 shell 或函数中调用它,它就会完全中断。如果我将输出或 运行 输出到子 shell 中,它还会将转义序列吐到下一个程序中。
此时我已经花了很多时间尝试调试它,但我不确定如何继续。如果在脚本中调用,是否有其他方法可以使用这些序列。
代码如下:
#!/bin/bash
# FZY: Command Line Fuzzy Finder
# Created: 29/10/2020
# Author: Nan0Scho1ar (Christopher Mackinga)
# License: MIT License
input="$(< /dev/stdin)"; height="$(tput lines)"; inum=$(echo "$input" | wc -l)
echo -e "\e[?1049h" 1>&2; row=1; col=1; str=""; cur=1; fnum=$inum; regex=""; regex2=""
while true; do
range="$row,$((row+height-3))p;$((row+height-3))q"
filtered=$(echo "$input" | grep ".*$regex" | sed -n $range 2>/dev/null | sed -e 's/.*/ &/');
frange="$(echo "$filtered" | wc -l)"
curpos=$((frange-cur+1))
echo "$filtered" | cut -c$col- | grep -E --color=always "$regex2" | tac | sed $curpos's/^ \(.*\)/> /'
echo " $fnum/$inum" && read -r -p "> $str" -n 1 -s < /dev/tty && read -r -sn3 -t 0.001 k1 < /dev/tty; REPLY+=$k1;
case "$REPLY" in
'') echo -e "\e[?1049l" 1>&2; echo "$filtered" | sed -n "${cur}s/ //p;${cur}q;" && exit;;
$'\e[C'|$'\e0C') col=$((col+1));;
$'\e[D'|$'\e0D') [[ $col -gt 1 ]] && col=$((col-1));;
$'\e[B'|$'\e0B') [[ $cur -ge 1 ]] && cur=$((cur-1));;
$'\e[A'|$'\e0A') [[ $cur -le $fnum ]] && cur=$((cur+1));;
$'\e[1~'|$'\e0H'|$'\e[H') row=1;;
$'\e[4~'|$'\e0F'|$'\e[F') row=$fnum;;
*)
char=$(echo "$REPLY" | hexdump -c | awk '{ print }');
if [[ $char = "033" ]]; then echo -e "\e[?1049l" 1>&2 && break;
elif [[ $char = "177" ]] && [[ ${#str} -gt 0 ]]; then str="${str::-1}";
else str="$str$REPLY" && row=1; fi
regex=$(echo "$str" | sed "s/\(.\)/.*/g");
regex2=$(echo "$str" | sed "s/\(.\)/|/g");
fnum=$(echo "$input" | grep -c ".*$regex")
;;
esac
[[ $((frange-cur+1)) -lt 1 ]] && row=$((row+1)) && cur=$((cur-1));
[[ $cur -lt 1 ]] && row=$((row-1)) && cur=$((cur+1));
[[ $cur -gt $fnum ]] && cur=$fnum;
[[ $((row-fnum+frange)) -gt 1 ]] && row=$((row-1))
[[ $row -lt 1 ]] && row=1;
yes '' | sed "${height}q";
done
一个有效的用法示例:
echo 'test 1
test 2
test 3' | fzy
一个无效的用法示例:
output=$(echo 'test 1
test 2
test 3' | fzy)
编辑:如评论中所述,不捕获子 shell 中的输出并不代表我的实际用例。更新了示例失败案例。
EDIT2:将控制序列重定向到 stderr 似乎有所改进。更新代码以反映此更改。
我的问题的解决方案是将所有控制序列和 menu/UI 相关行发送到 stderr,如评论中建议的 Charles Duffey。感谢他指出这一点。
这是应用了更改的最终代码。 (还有一些小错误,但它们与这个问题完全无关。)
请注意 echo -ne "\e[?1049h"
等一些内容已更改为 echo -ne "\e[?1049h" 1>&2
#!/bin/bash
# FZY: Command Line Fuzzy Finder
# Created: 29/10/2020
# Author: Nan0Scho1ar (Christopher Mackinga)
# License: MIT License
input="$(< /dev/stdin)"; height="$(tput lines)"; inum=$(echo "$input" | wc -l)
echo -ne "\e[?1049h\r" 1>&2; row=1; col=1; str=""; cur=1; fnum=$inum; regex=""; regex2=""
while true; do
range="$row,$((row+height-3))p;$((row+height-3))q"
filtered=$(echo "$input" | grep ".*$regex" | sed -n $range 2>/dev/null | sed -e 's/^.*/ &/');
frange="$(echo "$filtered" | wc -l)"
curpos=$((frange-cur+1))
echo "$filtered" | cut -c$col- | grep -E --color=always "$regex2" | tac | sed $curpos's/^ \(.*\)/> /' 1>&2
echo " $fnum/$inum" 1>&2 && read -r -p "> $str" -n 1 -s < /dev/tty && read -r -sn3 -t 0.001 k1 < /dev/tty; REPLY+=$k1;
case "$REPLY" in
'') echo -ne "\e[?1049l" 1>&2; echo "$filtered" | sed -n "${cur}s/ //p;${cur}q;" && exit;;
$'\e[C'|$'\e0C') col=$((col+1));;
$'\e[D'|$'\e0D') [[ $col -gt 1 ]] && col=$((col-1));;
$'\e[B'|$'\e0B') [[ $cur -ge 1 ]] && cur=$((cur-1));;
$'\e[A'|$'\e0A') [[ $cur -le $fnum ]] && cur=$((cur+1));;
$'\e[1~'|$'\e0H'|$'\e[H') row=1;;
$'\e[4~'|$'\e0F'|$'\e[F') row=$fnum;;
*)
char=$(echo "$REPLY" | hexdump -c | awk '{ print }');
if [[ $char = "033" ]]; then echo -e "\e[?1049l" 1>&2 && exit 1;
elif [[ $char = "177" ]] && [[ ${#str} -gt 0 ]]; then str="${str::-1}";
else str="$str$REPLY" && row=1; fi
regex=$(echo "$str" | sed "s/\(.\)/.*/g");
regex2=$(echo "$str" | sed "s/\(.\)/|/g");
fnum=$(echo "$input" | grep -c ".*$regex")
;;
esac
[[ $((frange-cur+1)) -lt 1 ]] && row=$((row+1)) && cur=$((cur-1));
[[ $cur -lt 1 ]] && row=$((row-1)) && cur=$((cur+1));
[[ $cur -gt $fnum ]] && cur=$fnum;
[[ $((row-fnum+frange)) -gt 1 ]] && row=$((row-1))
[[ $row -lt 1 ]] && row=1;
yes '' | sed "${height}q" 1>&2;
done