Shell 脚本中的拜占庭行为

Shell 脚本中的拜占庭行为

我编写了以下脚本,以交互方式和递归方式删除孤立的备份文件,即删除每个file.txt~没有相应file.txt.

#!/bin/sh -x

set -o errexit
unalias -a

backups=$(find . -name "*~")

orphans=""
while read -r file
do
    [ ! -e "${file%~}" ] && orphans=$(echo "$file\n$orphans");
done << EOF
$backups
EOF

if [ -z "$orphans" ]; then
    echo "No orphans."
else
    echo "orphans:\n$orphans"
    echo "$orphans" | xargs --interactive -d '\n' rm
fi

这个脚本会随机做出一些非常奇怪的事情。有时它表现正常,有时它会忽略传递给 sh 的 -x 选项,有时它会执行注释代码,有时测试会给出错误的结果。

问题似乎与此处脚本有关,因为通过将 find 的输出重定向到临时文件,所有问题似乎都消失了。但为什么,错误在哪里?


解决方案(谢谢丹尼斯·威廉森): 转义字符。参数扩展中的~未转义字符以某种方式创建了所有不可预测的行为。~${file%~}


一个更可读、更确定的解决方案,通过一些删减,可能是(感谢米克尔):

#!/bin/sh
IFS='
'
for backup in $(find . -type f -name "*~"); do
    if [ ! -e "${backup%\~}" ]; then
        rm -i "$backup"
    fi
done

如果你是while read循环的粉丝,事情就没那么优雅了,因为交互命令rm -i无法使用(它会与read命令冲突)。无论如何,解决方案可能是:

#!/bin/sh
orphans=""
while read -r backup; do
    if [ ! -e "${backup%\~}" ]; then
        orphans=$(echo "$backup\n$orphans");
fi
done << EOF
$(find . -type f -name "*~")
EOF

if [ ! -z "$orphans" ]; then
    echo "$orphans" | xargs --interactive -d '\n' rm
fi

或者,也可以采用更复杂的方式丹尼斯·威廉森

答案1

您可能需要转义括号扩展中的波浪号,否则它将扩展到您的主目录。

为什么不将 放入循环find中,while而是创建变量来保存它们?在循环中,只需执行rm -i "$file"

#!/bin/sh -x

set -o errexit
unalias -a

exec 3<&0    # open a duplicate of stdin
flag=false
find . -name "*~" | while IFS=$'\n' read -r file
do
    if [ ! -e "${file%\~}" ]
    then
        orphans="$file"$'\n'"$orphans"

        # use an alternate file descriptor so read and rm -i get along
        rm -i "$file" <&3
        flag=true
    fi
done
exec 3<&-    # close the file descriptor

if ! $flag
then
    echo "No orphans."
else
    echo "orphans:\n$orphans"
fi

如果您想使用 Bash,则需要将其移动find到循环末尾,这样就不会创建子 shell。

#!/bin/bash

...

# use an alternate file descriptor so read and rm -i get along
while read -u 3 -r ...

    rm -i ...
    ...

done 3< <(find ...)

...

答案2

“有时”是什么意思?有时在同一台机器上?还是在不同系统上有不同的行为?

“执行注释代码”是什么意思?您的示例没有任何注释。

一些想法:

  • 尝试set -x代替/bin/sh -x
  • set -e比使用更好set -o errexit
  • 如果你使用 bash,则调用/bin/bash,而不是/bin/sh,这可能是其他东西
  • echo 是不必要的,只需=与文字换行符一起使用即可
  • 如果你必须使用echo,你应该使用echo -e或确保xpg_echo设置
  • 您的孤儿线将它们按相反的顺序排列,是故意的吗?
  • 如果文件名包含空格,你的读取循环将失败,你应该IFS先设置

更简单的版本:

#!/bin/bash

set -x
set -e

IFS=$'\n'
orphans=false
for backup in $(find . -type f -name "*~"); do
    original=${backup%\~}
    if [ ! -e "$original" ]; then
        orphans=true
        rm -i "$backup"
    fi  
done

if ! $orphans; then
    echo "No orphans."
fi

或者如果你想要使用以下命令/bin/sh

#!/bin/sh

set -x
set -e

IFS='
'
orphans=false
for backup in $(find . -type f -name "*~"); do
    original=${backup%\~}
    if [ ! -e "$original" ]; then
        orphans=true
        rm -i "$backup"
    fi  
done

if ! $orphans; then
    echo "No orphans."
fi

相关内容