Mar*_*kus 4 bash command-substitution
我有一个程序可以获取在图形 IU 中选择的文件(在我的情况下是 macOS 中的 Finder)。输出是这样的
'/tmp/file number one.txt' '/tmp/file number two.txt'
Run Code Online (Sandbox Code Playgroud)
请注意名称中的空格字符,因此文件名包含在 '(单个直勾号)中
当在 bash 中的命令替换中使用该命令的输出时,例如ls -l
命令一切都搞砸了。为了进行测试,我将上述行放入一个简单的单行文本文件中,并将其用作命令行替换:
$ cat /tmp/files.txt
'/tmp/file number one.txt' '/tmp/file number two.txt'
$ ls -l $(</tmp/files.txt)
ls: "'/tmp/file: No such file or directory
ls: '/tmp/file: No such file or directory
ls: number: No such file or directory
ls: number: No such file or directory
ls: one.txt': No such file or directory
ls: two.txt'": No such file or directory
Run Code Online (Sandbox Code Playgroud)
当我将文件名字符串分配给变量并使用它时,也会发生同样的情况
$ xxx="'/tmp/file number one.txt' '/tmp/file number two.txt'"
$ ls -l $xxx
ls: '/tmp/file: No such file or directory
ls: '/tmp/file: No such file or directory
ls: number: No such file or directory
ls: number: No such file or directory
ls: one.txt': No such file or directory
ls: two.txt': No such file or directory
Run Code Online (Sandbox Code Playgroud)
知道如何解决这个问题吗?将转义的文件名直接复制到命令行上按预期工作
$ ls -l '/tmp/file number one.txt' '/tmp/file number two.txt'
-rw-r--r-- 1 tester wheel 0B Jul 17 17:21:11 2021 /tmp/file number one.txt
-rw-r--r-- 1 tester wheel 0B Jul 17 17:21:16 2021 /tmp/file number two.txt
Run Code Online (Sandbox Code Playgroud)
我的最终目标是使用当前的 Finder 选择(我通过编译的 Applescript 获得)可用于 bash。ls
仅仅是一个例子,我可能要使用的文件的列表tar
,cp
,mv
或其他任何文件处理的东西。
假设您有这个字符串,字面上嵌入了单引号:
'/tmp/file number one.txt' '/tmp/file number two.txt'
Run Code Online (Sandbox Code Playgroud)
您注意到当作为命令行的一部分内联时它可以正常工作,但当它来自扩展时则无法正常工作。是变量扩展还是命令替换并不重要,两者的规则是相同的。未加引号的扩展会进行分词,您在此处不希望这样做,因为在空格上进行拆分会在/tmp/file
和之间拆分number
。带引号的扩展不会进行拆分,但您也不希望这样做,因为您可能希望在两个中间单引号之间进行拆分。此外,还有一个事实,即扩展产生的引号不引用任何内容。所以,我们需要做一些不同的事情。
假设输出已知是 shell 语法,并且是安全的,您可以使用eval
让 shell 进行另一轮处理来解释引号:
#!/bin/bash
input="'/tmp/file number one.txt' '/tmp/file number two.txt'"
eval "ls -ld -- $input"
Run Code Online (Sandbox Code Playgroud)
或将它们放在一个数组中以备将来使用:
#!/bin/bash
input="'/tmp/file number one.txt' '/tmp/file number two.txt'"
eval "files=($input)"
for f in "${files[@]}"; do
printf "<%s>\n" "$f"
done
Run Code Online (Sandbox Code Playgroud)
请注意,如果字符串将eval
包含未加引号或双引号的命令替换(例如/dir/$(uname -a)
, 但不是'/dir/$(uname -a)'
),那么您的 shell将在处理eval
. 同样,如果字符串包含未加引号的)
以结束数组赋值。因此,最好确保仅将其用于受您控制的来源。
此外,您确实需要在eval
'd的字符串周围使用双引号,因为您不希望在eval
处理引号之前将其拆分和通配。
可能有一些方法可以使用其他工具来解释引号而不是处理扩展,例如xargs
默认情况下采用带引号的字符串。例如,这将printf
使用每个文件名作为单独的参数运行¹:
printf '%s\n' "$input" | xargs printf ":%s:\n"
Run Code Online (Sandbox Code Playgroud)
或者运行ls
它们:
printf '%s\n' "$input" | xargs ls -ld --
Run Code Online (Sandbox Code Playgroud)
或者您可以xargs
运行一些东西,然后以更简单的格式打印文件名,然后您可以将其加载到 shell 中的数组中。(这有点落后,但我不知道有什么方法可以让 Bash 只进行引用处理而不处理扩展。)
#!/bin/bash
readarray -td '' files < <(
printf '%s\n' "$input" | xargs printf "%s\0")
for f in "${files[@]}"; do
printf "<%s>\n" "$f"
done
Run Code Online (Sandbox Code Playgroud)
(这里,printf
输出以 NUL 字节结尾的字符串,并且readarray -td ''
² 期望以该格式输出。NUL 是唯一不能出现在文件名中的值,这是一种明确且相对简单的格式。)
但请注意,xargs
与 shell 相比,它对确切的引用规则有不同的看法。它不知道$'...'
quoting³的样式,在某些情况下,Bash 使用它来输出包含嵌入换行符的值,它无法识别双引号内的反斜杠4 ...但是如果 Finder 的输出只是单引号(和反斜杠引用任何硬单引号),你可能没问题。
¹ 独立printf
实用程序,而不是printf
您的 shell的内置工具,即使在空输入(某些 BSD 除外)时也至少使用一次,如果列表很大,则可能不止一次
² 需要 bash 4.4 或以上
³ 由 ksh93 在 90 年代推出
4xargs
在 70 年代后期随 PWB Unix 一起出现,引用语法类似于sh
那里的前 Bourne (Mashey shell),而不是 Bourne shell,更不用说 ksh93 或 bash
如果切换到zsh
是一个选项¹,您可以使用其z
和Q
专为此设计的参数扩展标志:
file_content=$(</tmp/files.txt)
quoted_strings=(${(z)file_content})
strings_with_one_layer_of_quotes_removed=("${(Q@)quoted_strings}")
ls -ld -- "$strings_with_one_layer_of_quotes_removed[@]"
Run Code Online (Sandbox Code Playgroud)
或者一次性完成:
ls -ld -- "${(Q@)${(z)$(</tmp/files.txt)}}"
Run Code Online (Sandbox Code Playgroud)
假设文件中引用的语法与zsh
.
另请参阅Z
参数扩展以调整解析的完成方式。例如,如果文件包含#
应该被忽略的注释(with )并且有多于一行,您需要:
ls -ld -- "${(Q@)${(Z[Cn])$(</tmp/files.txt)}}"
Run Code Online (Sandbox Code Playgroud)
详情请参阅info zsh flags
。
¹ 我听说zsh
现在是较新版本 macos 中的默认交互式 shell