我需要使用 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/fantastic
、lib
和lib/fe 1
Or 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 的问题。这看起来有点危险,因为恶意制作的模式基本上可以擦除您的系统。
是的,这很危险,所以你不应该这样做。将数据保留为数据,将代码保留为代码,不要混合它们。文件名扩展实际上确实保持了分隔,您可以使用通配符安全地处理具有任意字符的文件名。当您尝试将多个文件名打印到单个字符串或单个输出流(例如stdout
of )时,就会出现问题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 您甚至可以将扩展模式作为参数传递给另一个命令。