我有一个程序可以获取在图形 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
的文件列表。tar
cp
mv
答案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
实用程序,而不是printf
shell 的内置程序,即使在空输入时也至少一次(某些 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
(或hd
或hexdump
或类似的十六进制转储程序),您可以看到它已变为 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