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

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

我有一个程序可以获取在图形 IU 中选择的文件(在我的例子中是 macOS 中的 Finder)。输出是这样的

'/tmp/file number one.txt' '/tmp/file number two.txt'

请注意名称中的空格字符,因此文件名包含在 ' (单直勾号)中

当在 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

当我将文件名字符串分配给变量并使用它时,也会发生同样的情况

$ 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

知道如何解决这个问题吗?将转义文件名直接复制到命令行上可以按预期工作

$ 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

我的最终目标是使用当前的 Finder 选择(我通过编译的 Applescript 获得)可在 bash 中使用。只是一个例子,我可能想使用、或任何其他文件处理内容ls的文件列表。tarcpmv

答案1

如果切换到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[@]"

或者一口气完成:

ls -ld -- "${(Q@)${(z)$(</tmp/files.txt)}}"

假设文件中引用的语法与zsh.

另请参阅Z参数扩展以调整解析的完成方式。例如,如果文件包含#应被忽略的注释(带有 )并且有多于一行,则您需要:

ls -ld -- "${(Q@)${(Z[Cn])$(</tmp/files.txt)}}"

info zsh flags详情请参阅。


¹ 我听说zsh现在是较新版本的 macOS 中的默认交互式 shell

答案2

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

'/tmp/file number one.txt' '/tmp/file number two.txt'

您注意到当作为命令行的一部分内联给出时它可以正常工作,但当它来自扩展时则不能。无论是变量扩展还是命令替换,这并不重要,两者的规则都是相同的。未加引号的扩展会经历分词,您在这里不希望这样做,因为空格上的拆分会在/tmp/file和之间进行拆分number之间进行拆分。带引号的扩展不会进行拆分,但您也不希望这样做,因为您可能希望在两个中间单引号之间进行拆分。另外,还有一个事实是扩展产生的引号不引用任何内容。所以,我们需要做一些不同的事情。

假设输出已知为 shell 语法,并且是安全的,您可以使用eval让 shell 进行另一轮处理来解释引号:

#!/bin/bash
input="'/tmp/file number one.txt' '/tmp/file number two.txt'"
eval "ls -ld -- $input"

或者将它们放入数组中以供将来使用:

#!/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

请注意,如果要执行的字符串eval包含不带引号或双引号的命令替换(例如/dir/$(uname -a),但不是'/dir/$(uname -a)'),那么您的 shell将要执行处理时涉及的命令eval。同样,如果字符串包含不带引号的字符串,)则结束数组赋值。因此,最好确保仅将其与您控制下的源一起使用。

另外,您确实需要在字符串周围使用双引号eval'd,因为您不希望在eval处理引号之前将其拆分和通配。


可能有一些方法使用其他工具来解释引号但不处理扩展,例如xargs默认情况下采用带引号的字符串。例如,这将以printf每个文件名作为单独的参数运行1:

printf '%s\n' "$input" | xargs printf ":%s:\n"

或者运行ls它们:

printf '%s\n' "$input" | xargs ls -ld --

或者您可以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

(这里,printf输出以 NUL 字节结尾的字符串,并且readarray -td ''² 期望以该格式输出。NUL 是唯一不能出现在文件名中的值,这是一种明确且相对简单的格式。)

但请注意,它对xargs精确引用规则的理解与 shell 不同。它不知道$'...'引用的风格,Bash 在某些情况下使用这种风格来输出包含嵌入换行符的值,它无法识别双引号内的反斜杠4 ...但是如果 Finder 的输出只是单引号 (和反斜杠来引用任何硬单引号),你可能没问题。


1 独立printf实用程序,而不是printfshell 的内置程序,即使在空输入时也至少一次(某些 BSD 除外),如果列表很大,则可能多次

² 需要 bash 4.4 或更高版本

³ 由 ksh93 于 90 年代推出

70 年代末,PWB Unix 中的 PWB 4.4xargs出现,引用语法与 Bourne 之前的版本 (Mashey shell) 类似sh,而不是 Bourne shell 的语法,更不用说 ksh93 或 bash

答案3

你最好的选择是修复生成此类无用文件列表的任何内容,以便生成 NUL 分隔的输出(因为 NUL 是仅有的不能出现在路径/文件名中的字符,它是保证处理具有任何有效字符的任何文件名的唯一分隔符)。如果这是不可能的,您可以通过尝试将其转换为 NUL 分隔格式来拼凑“修复”。

以下 perl 单行代码将(大部分)将文件转换为 NUL 分隔的文件名,不带引号:

perl -0 -pe "s/'\s+'/\0/sg; s/^'|'\$//sg; s/\x0d?\x0a\$//" file.txt

第一个正则表达式用 NUL 字符替换序列single-quote, one-or-more whitespace chars, single-quote(其中的逗号和空格不是模式的一部分,它们只是语法上的英语列表分隔符)。第二个正则表达式删除输入开头和结尾的引号,第三个正则表达式删除“行”末尾的 LF 或 CRLF。

这是离完美还很远- 某些输入是无法修复的,因为无法 100% 确定是否应该在文件名中嵌入单引号或 LF(这就是为什么从 NUL 分隔的文件开始是正确的解决方案,不要试图在事后拼凑它)。

例如,如果任何文件名在文件名的开头或结尾处具有嵌入的单引号,或者如果它们具有嵌入的单引号,后跟一个或多个空白字符,后跟另一个单引号,则它将失败(例如' ')-所有这些也将被替换为 NUL,因为/g第一个正则表达式的全局修饰符(这是匹配输入中的所有文件名而不仅仅是第一个文件名所必需的)。可能还有一些我还没有想到的其他极端情况。

您可以将输出重定向到另一个文件,将其输入到xargs -0r,或将其与 bash 内置readarray和进程替换一起使用来填充数组:

readarray -d '' files < <(perl -0 -pe "s/'\s+'/\0/sg;
                                       s/^'|'\$//sg;
                                       s/\x0d?\x0a\$//" file.txt)

如果将输出通过管道传输到xxd(或hdhexdump或类似的十六进制转储程序),您可以看到它已变为 NUL 分隔:

00000000: 2f74 6d70 2f66 696c 6520 6e75 6d62 6572  /tmp/file number
00000010: 206f 6e65 2e74 7874 002f 746d 702f 6669   one.txt./tmp/fi
00000020: 6c65 206e 756d 6265 7220 7477 6f2e 7478  le number two.tx
00000030: 74                                       t

相关内容