如何让 Bash 将命令的输出解释为带引号的字符串?

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仅仅是一个例子,我可能要使用的文件的列表tarcpmv或其他任何文件处理的东西。

ilk*_*chu 6

假设您有这个字符串,字面上嵌入了单引号:

'/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


Sté*_*las 6

如果切换到zsh是一个选项¹,您可以使用其zQ专为此设计的参数扩展标志:

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