当模式和解析的路径包含空格时如何进行通配符/全局扩展?

当模式和解析的路径包含空格时如何进行通配符/全局扩展?

我需要使用 POSIX sh 或 Bash 扩展一些路径:

以下是两个示例模式(我故意选择了过于复杂的模式):

$ npm pkg get workspaces | jq -r '.[]'
apps/app*
lib/{be,fe *} lib/*lib

假设我的目录树如下所示:

$ mkdir -p "lib/be lib/fantastic lib" "lib/fantastic" "lib/fe 1 lib/other lib" "apps/app1" "apps/app2" "be" "1"

$ tree
.
├── 1
├── apps
│   ├── app1
│   └── app2
├── be
└── lib
    ├── be lib
    │   └── fantastic lib
    ├── fantastic
    └── fe 1 lib
        └── other lib

12 directories, 0 files

如何获得一个简单列表,其中所有路径与模式匹配,每行一个路径?

看起来基本的 shell 扩展只是解析路径并用空格分隔它们,而不引用各个路径:

例如,这个偶匹配是什么?

$ echo "lib/"{"be","fe "*}" lib/"*"lib"
lib/be lib/fantastic lib lib/fe 1 lib/other lib

它可能是:lib/be lib/fantasticliblib/fe 1Or lib/other lib
lib/be lib/fantastic lib哎呀lib/fe 1 lib/other lib
,它甚至可能只是一条很长的路径:lib/be lib/fantastic lib lib/fe 1 lib/other lib

如果您不知道哪个空格是分隔符以及哪个空格是路径的一部分,则似乎无法判断。

但同样具有挑战性的是,您必须引用包含空格的所有内容,但同时不得引用通配符等。

我的意思是,我设法将一些东西组合在一起,但我非常怀疑这实际上能解决所有可能的情况:

echo 'lib/{be,fe *} lib/*lib' | sed -e 's/\([*,{}]\)/"\1"/g' -e 's/.*/"&"/' -e 's/""//g'

在我的两种模式上运行它似乎确实有效:

$ echo -e 'lib/{be,fe *} lib/*lib\napps/app*' | sed -e 's/\([*,{}]\)/"\1"/g' -e 's/.*/"&"/' -e 's/""//g' | while IFS= read -r line; do bash -c "echo $line"; done
lib/be lib/fantastic lib lib/fe 1 lib/other lib
apps/app1 apps/app2

但话又说回来,路径从哪里开始,又在哪里结束?

最后,我不知道如何使用 eval 或bash -c.这看起来有点危险,因为恶意制作的模式基本上可以擦除您的系统。例如,类似的文件模式bye && rm -rf ~可以删除您的主目录。

答案1

看起来基本的 shell 扩展只是解析路径并用空格分隔它们,

这并不愚蠢,而且根本行不通。这里的关键是,当处理命令行时,它的处理更像是一组不同的字符串(“单词”或“字段”),而不是单个长字符串。大括号扩展和文件名全局生成多个不同的字段。这些字段最终作为您运行的任何命令的命令行参数(最终作为数组的元素,argv[]正如 C 程序中通常所说的那样)。

你的问题,也是一个常见的陷阱,是echo用空格连接它得到的所有参数,产生你看到的一长串。

例如,Bash 的 interachivehelp echo明确表示这正是它的作用:

$ help echo
echo: echo [-neE] [arg ...]
    Write arguments to the standard output.

    Display the ARGs, separated by a single space character and followed by a
    newline, on the standard output.

这意味着即使参数明显不同,它们也会给出相同的输出:

$ echo foo bar doo
foo bar doo
$ echo "foo bar" doo
foo bar doo

但是使用像这样简单的东西ls,你会看到它的工作原理:

$ touch "foo bar" doo
$ ls -l *oo*
-rw-r----- 1 ilkkachu ilkkachu 0 Sep  6 12:58 doo
-rw-r----- 1 ilkkachu ilkkachu 0 Sep  6 12:58 foo bar

如果将 glob 的输出逐字复制echo回 shell 时得到的结果,您将得到以下结果之一:

$ ls -l foo bar doo
ls: cannot access 'foo': No such file or directory
ls: cannot access 'bar': No such file or directory
-rw-r----- 1 ilkkachu ilkkachu 0 Sep  6 12:58 doo

或者

$ ls -l "foo bar doo"
ls: cannot access 'foo bar doo': No such file or directory

(取决于我们是否要进一步用空格分割该字符串)

这里的解决方案是停止使用echo调试。相反,使用例如printf适当的选项。这会根据需要多次重复使用格式字符串这一事实来打印<和using之间的每个不同参数:>printf

$ printf "<%s>\n" *oo*
<doo>
<foo bar>

或者创建一个像这样的脚本:

#!/bin/sh
printf "%d args\n" "$#"
if [ "$#" -gt 0 ]; then
    printf "<%s>\n" "$@"
fi

并称其为例如args.sh。然后尝试使用您的支架扩展。

但同样具有挑战性的是,您必须引用包含空格的所有内容,但同时不得引用通配符等。

你真的无法摆脱这个。有些字符以某种方式是特殊的(空格分割单词),有些字符以另一种方式是特殊的(glob 字符扩展为文件名),有些你想像这样保留(glob 字符),有些你不想保留(空格)。

最后,我不知道如何解决使用 eval 或 bash -c 的问题。这看起来有点危险,因为恶意制作的模式基本上可以擦除您的系统。

是的,这很危险,所以你不应该这样做。将数据保留为数据,将代码保留为代码,不要混合它们。文件名扩展实际上确实保持了分隔,您可以使用通配符安全地处理具有任意字符的文件名。当您尝试将多个文件名打印到单个字符串或单个输出流(例如stdoutof )时,就会出现问题echo。如果不需要,请尽量避免这样做,并且当您这样做时,请将文件名打印为 NUL 终止(C 样式)字符串,因为,这就是它们的本质。

您的问题并不完全是关于分词(未加引号的参数扩展),但这可能仍然有用: https://mywiki.wooledge.org/WordSplitting

答案2

当通配符如*?被引用时,它们的特殊含义被禁用。但是,您需要引用或转义来保护空格。解决方案是仅引用或转义模式中需要它的部分,避免使用通配符运算符。例如:

当前目录中至少包含一个空格(并且不以句点开头)的所有对象:

  *" "*

另一种方法是转义空格而不是引用它:

  *\ *

Bash 大括号扩展不是通配符:它是一种生成文本的理解符号。a{b,c}d意思是 { "a$x$d" | x ϵ { "b", "c" } }:$x$ 的所有字符串 a$x$d 是元素“b”和“c”。

Bash 首先执行大括号扩展来生成字段,然后对这些字段进行路径名扩展。

引用抑制大括号扩展;大括号必须不加引号。

给定类似 的模式*.{jpg,gif},首先应用大括号扩展来生成字段*.jpg*.gif。然后,这些文件将受到文件名扩展的影响,就像它们以这种方式输入命令行一样。

引用和转义可以应用于大括号的内部,以便{\*,"?"}产生\*and"?"变成未展开的字段*and ?

答案3

感谢评论@ilkkatchu,我现在明白我只需要使用 echo 之外的其他东西,所以我想出了一个简单的内联 bash 脚本,它将每个收到的参数作为一行打印到标准输出: printf "%s\n" "$0" "$@"然后我“简单地”将扩展模式传递给它。

# Set up test directory structure
mkdir -p "lib/be lib/fantastic lib" "lib/fantastic" "lib/fe 1 lib/other lib" "apps/app1" "apps/app2" "be" "1"

# Define path patterns
export PATH_PATTERNS='lib/{be,fe *} lib/*lib
apps/app*'

# Print path patterns
echo -e "$PATH_PATTERNS"
# Output is:
# lib/{be,fe *} lib/*lib
# apps/app*

# Put double quotes around everything that is not `*`, `,`, `{` and `}`
export SANITIZED_PATH_PATTERNS="$(echo -e "$PATH_PATTERNS" | sed -e 's/\([*,{}]\)/"\1"/g' -e 's/.*/"&"/' -e 's/""//g')"
echo -e "$SANITIZED_PATH_PATTERNS"
# Output is:
# "lib/"{"be","fe "*}" lib/"*"lib"
# "apps/app"*

# Iterate over every sanitized expression and expand it by evaluating it with bash -c "... $line",
# And inside that new bash put another bash -c "..." right before the $line, so that the expanded $line is passed as multiple parameters to the next bash. # In that next bash we simply print all passed arguments to stdout (on per line), by using `printf "%s\n" "$0" "$@"`:
echo -e "$SANITIZED_PATH_PATTERNS" | while IFS= read -r line; do 
    bash -c "bash -c 'printf \"%s\n\" \"\$0\" \"\$@\"' $line";
done
# Output is:
# lib/be lib/fantastic lib
# lib/fe 1 lib/other lib
# apps/app1
# apps/app2

或者作为一句单行:

$ echo "$PATH_PATTERNS" | sed -e 's/\([*,{}]\)/"\1"/g' -e 's/.*/"&"/' -e 's/""//g' | while IFS= read -r line; do bash -c "bash -c 'printf \"%s\n\" \"\$0\" \"\$@\"' $line"; done

不幸的是,问题中提到的有关恶意制作模式的安全隐患仍然适用,而且这也不符合 POSIX 标准,并且仅针对上述两种模式进行了测试。我想到的可能会导致我的方法出现问题的事情:

  • 包含新行字符的模式
  • 包含换行符的要匹配的路径
  • 在大括号定义之外包含逗号的模式
  • 包含转义通配符的模式\*
  • 双通配符**
  • 包含问号的图案

我希望有一个简单的方法来解决所有这些问题,但似乎没有。如果您有 python 或其他可用的现代脚本引擎,那么您最好编写该语言的脚本来处理模式解析。

或者只是使用现有的 cli 实用程序,例如全局可以像这样安装npm i -g glob和使用:

glob "apps/app*" "/{bin,usr/bin}/" "test/**"

使用--cmd标志 the 您甚至可以将扩展模式作为参数传递给另一个命令。

相关内容