【问题标题】:bash: Expand list of arguments and pass it to find - escaping/white spaces hellbash:扩展参数列表并将其传递给 find - 转义/空格地狱
【发布时间】:2026-02-06 01:20:10
【问题描述】:

我想检查文件夹中的文件并删除其中一些。一个条件是保留某种类型的所有文件(例如 .txt),并保留所有具有第一次搜索名称但扩展名不同的文件([第一次搜索的名称].)。应该删除目录中的所有其他文件。

这可以通过find . -type f -not -name xxx 命令轻松实现。但是,我想为自动找到的每个 [第一次搜索的名称] 填充 find 命令。

为此,我编写了这个小脚本

#!/bin/bash

while read filename; do
     filename=$(echo $filename | sed 's/\ /\\\ /g')
     filename=\'$filename*\'
     file_list=$file_list" -not -name $filename"
done <<<"$(ls *.txt | sed 's/.txt//g')"

find . -type f $file_list -print0| while read -d $'\0' FILE
     do
     rm -f "$FILE"
done

$file_list 很好地填充了相应的数据,但是 find 失败说:

查找:未知谓词`-\'

如果我使用 sed 命令 (' ' -> '\ ') 或

find:路径必须在表达式之前:- 用法:find [-H] [-L] [-P] [-Olevel] [-D [help|tree|search|stat|rates|opt|exec] [path...] [表达]

如果我评论 sed 行。

bash -x 显示以下执行的命令:

没有 sed 命令:

找到 . -type f -not -name ''\''Text' - 这里 - 或 - 那里*'\'''

使用 sed 命令:

找到 . -type f -not -name ''\''文本\' '-\' '这里\' '-\' '或\' '那里*'\'''

这甚至可以通过 find 实现吗?我还尝试在 find 命令中转义 $find_list,但没有成功。

【问题讨论】:

  • 您无法将引号添加到work that way 的字符串参数中。但你不需要。引用变量扩展,它将是正在运行的命令的一个参数。也 don't parse the output from ls 只需使用 glob(在这种情况下使用 echo 或者不要打扰 read 循环,只需在 glob for file in *.txt 上使用 for 循环)。
  • 另外,$'\0' 只是在 bash 中编写 '' 的一种更复杂的方式,因为 bash 将内容存储在 C 字符串中,而 NUL 字节终止 C 字符串。 (-d '' 作为 read 的参数正确指示了 NUL 终止符,因为 0 字节字符串的第一个字节是它的 NUL 终止符)。

标签: linux bash shell find escaping


【解决方案1】:

使用数组,而不是字符串。

#!/bin/bash
# ^-- must be /bin/bash, not /bin/sh, for this to work

excludes=( )
for filename in *.txt; do
  excludes+=( -not -name "${filename%.txt}" )
done

find . -type f -not -name '*.txt' "${excludes[@]}" -exec rm -f '{}' +

要了解为什么会这样,请参阅BashFAQ #50


现在,如果你想兼容/bin/sh,而不仅仅是bash,那么将它封装在一个函数中,这样你就可以覆盖参数列表(这是唯一可用的数组),而不会丢弃脚本的全局参数:

delete_except_textfiles() {
  local filename 2>/dev/null ||: "local keyword not in POSIX, ignore if not present"
  set --
  for filename in *.txt; do
    set -- "$@" -not -name "${filename%.txt}"
  done
  find . -type f -not -name '*.txt' "$@" -exec rm -f '{}' +
}
delete_except_textfiles

【讨论】:

  • 感谢您的热烈回答,伙计们。由于我将仅将脚本用于 bash,因此我将使用 Charles Duffy 的数组解决方案。但是,有很多链接可供阅读。再次感谢!
【解决方案2】:

试试这个

#!/bin/bash

remove_except()
{
    local extension=$( printf "%q" "$1" )
    local dir=$( printf "%q" "$2" )
    local start_dir=$(pwd)

    [ -z "$extension" ] && return 1
    [ -z "$dir" ] || [ ! -d "$dir" ] && dir="."
    cd "$dir"

    local this="$0"
    this="${this##*/}"

    # exclude myself and extension
    local excludes=" -name \"$this\" -o -name \"*.$extension\" "

    for f in *."$extension";
    do
        filename="${f%.*}"
        excludes="$excludes -o -name \"$filename.*\""
    done

    eval "find . -maxdepth 1 -type f -not \( $excludes \) -print0" | xargs -0 -I {} rm -v {}

    cd "$start_dir"
}

remove_except "txt" "/your/dir"

放入脚本,例如remove_except.sh 并像这样运行它:

remove_except.sh "txt" "/your/dir"

第二个参数是可选的,如果未指定,将假定为.

【讨论】:

  • 使用eval 而不进行完全安全的转义(例如printf %q),从而为代码提供一个被视为数据的路径是危险的——考虑被称为remove_except 'text$(rm -rf .)foo'
  • 好的,我同意并讨厌自己使用 eval,但如果不使用数组,$excludes 将无法正确扩展,即您在调试输出中会得到额外的引号,例如 find . -maxdepth 1 -type f -not '(' -name '"*.txt"' -o -name '"aaa.*"' -o -name '"bbb.*"' ')' 而不是 find . -maxdepth 1 -type f -not '(' -name '*.txt' -o -name 'aaa.*' -o -name 'bbb.*' ')'
  • 备份和重读,我明白你在说什么,是的——如果不使用数组,你会得到字符串拆分和全局扩展,但没有引号处理,这是正确的。然而......好吧,坦率地说,在这个用例中,不使用数组是错误的,在实践-原因-安全-漏洞的意义上。
  • 我已经修改了我的答案,以涵盖一种避免 bashism 的机制(我能想到的不使用数组的唯一原因),同时仍以正确/安全的方式执行此操作。
  • (实际上,使用恶意字符串调用是不太令人担忧的情况;以.txt 结尾的恶意文件名更有可能是真正的漏洞,例如,如果我们是清理上传目录)。