正确转义 xargs 中管道的输出

正确转义 xargs 中管道的输出

例子:

% touch -- safe-name -name-with-dash-prefix "name with space" \
    'name-with-double-quote"' "name-with-single-quote'" \
    'name-with-backslash\'

xargs似乎无法处理双引号:

% ls | xargs ls -l 
xargs: unmatched double quote; by default quotes are special to xargs unless you use the -0 option
ls: invalid option -- 'e'
Try 'ls --help' for more information.

如果我们使用该-0选项,则带有破折号前缀的名称会出现问题:

% ls -- * | xargs -0 -- ls -l --
ls: invalid option -- 'e'
Try 'ls --help' for more information.

这是在使用其他可能有问题的字符(例如换行符、控制字符等)之前的情况。

答案1

POSIX规范确实给了你一个例子:

ls | sed -e 's/"/"\\""/g' -e 's/.*/"&"/' | xargs -E '' printf '<%s>\n'

(文件名是任意序列字节(除了/NULL)和sed/xargs期待文本,您还需要将语言环境修复为 C(其中所有非 NUL 字节都将生成有效字符)以使其可靠(除了对xargs参数最大长度具有非常低限制的实现之外))

-E ''对于某些xargs实现来说,如果没有它,则需要它来理解表示_输入结束的参数(例如仅echo a _ b | xargs输出)。a

使用 GNU xargs,您可以使用:

ls | xargs -rd '\n' printf '<%s>\n'

(还添加-r(也是 GNU 扩展)如果输入为空则不会运行该命令)。

GNUxargs还有一个-0已被其他一些实现复制的,因此:

ls | tr '\n' '\0' | xargs -0 printf '<%s>\n'

稍微更便携一些。

所有这些都假设文件名不包含换行符。如果文件名可能带有换行符,则 的输出ls根本无法进行后处理。如果你得到:

a
b

这可以是 aab文件,也可以是一个名为 的文件a<newline>b,没有办法分辨。

GNUls有一个--quoting-style=shell-always使得它的输出明确并且可以进行后处理,但是引用与 期望的引用不兼容xargsxargs识别"..."和引用的形式\x'...'但 和"..."都是'...'强引号,不能包含换行符(只能\转义 换行符xargs),因此与 sh 引用不兼容,其中只有'...'强引号(并且可以包含换行符),但是\<newline>行延续(已删除) ) 而不是转义换行符。

您可以使用 shell 解析该输出,然后以预期的格式输出xargs

eval "files=($(ls --quoting-style=shell-always))"
[ "${#files[@]}" -eq 0 ] || printf '%s\0' "${files[@]}" |
  xargs -0 printf '<%s>\n'

或者您可以让 shell 获取文件列表并将其以 NUL 分隔传递给xargs.例如:

  • zsh

    print -rNC1 -- *(N) | xargs -r0 printf '<%s>\n'
    
  • ksh93

    (set -- ~(N)*; (($# == 0)) || printf '%s\0' "$@") |
      xargs -r0 printf '<%s>\n'
    
  • fish

    begin set -l files *; string join0 -- $files; end |
      xargs -r0 printf '<%s>\n'
    
  • bash

    (
      shopt -s nullglob
      set -- *
      (($# == 0)) || printf '%s\0' "$@"
    ) | xargs -r0 printf '<%s>\n'
    

2023 编辑。自 GNU coreutils 9.0 版本(2021 年 9 月)以来,GNUls现在有一个--zero可以与以下命令结合使用的选项xargs -r0

ls --zero | xargs -r0 printf '<%s>\n'

答案2

为了xargs理解-0空分隔输入选项,发送方还必须将空分隔符应用于他们发送的数据。

否则两者之间就没有同步。

一种选项是GNU find可以放置此类分隔符的命令:

find . -maxdepth 1 ! -name . -print0 | xargs -0 ls -ld

答案3

正如你所说,xargs不喜欢不匹配的双引号,除非你使用,-0-0只有当你向它提供空终止数据时才有意义。所以,这失败了:

$ echo * | xargs
xargs: unmatched double quote; by default quotes are special to xargs unless you use the -0 option
name-with-backslash -name-with-dash-prefix

但这有效:

$ printf '%s\0' -- * | xargs -0
-- name-with-backslash\ -name-with-dash-prefix name-with-double-quote" name-with-single-quote' name with space safe-name

无论如何,您的基本方法并不是真正做到这一点的最佳方法。不用摆弄xargsandls之类的东西,只需使用 shell glob 即可:

$ for f in *; do ls -l -- "$f"; done
-rw-r--r-- 1 terdon terdon 4142 Aug 11 16:03 a
-rw-r--r-- 1 terdon terdon 0 Aug 11 15:34 'name-with-backslash\'
-rw-r--r-- 1 terdon terdon 0 Aug 11 15:34 -name-with-dash-prefix
-rw-r--r-- 1 terdon terdon 0 Aug 11 15:34 'name-with-double-quote"'
-rw-r--r-- 1 terdon terdon 0 Aug 11 15:34 "name-with-single-quote'"
-rw-r--r-- 1 terdon terdon 0 Aug 11 15:34 'name with space'
-rw-r--r-- 1 terdon terdon 0 Aug 11 15:34 safe-name

答案4

尝试这样做是极其愚蠢的解析命令的输出ls那是不是为解析而设计的提供一个命令不适合处理多个字符(例如:new lines{})当 shell 自行执行此操作时:

set -- *; for f; do echo "<$f>"; done

set    -- *
for    f
do     ls "$f"
done

或者,在一个命令行中:

$ set -- *; for f; do echo "<$f>"; done
<name-with-backslash\>
<-name-with-dash-prefix>
<name-with-double-quote">
<name-with-single-quote'>
<name with space>
<safe-name>
<with_a
newline>

请注意,输出处理(并且有 n 个示例作为最后一个文件名)与换行符完全没有问题。

或者,如果文件数量使 shell 变慢,请使用 find:

$ find ./ -type f -exec echo '<{}>' \;
<./safe-name>
<./with_a
newline>
<./name-with-double-quote">
<./-name-with-dash-prefix>
<./name with space>
<./name-with-single-quote'>
<./name-with-backslash\>

请注意,find 处理所有点文件和所有子目录的方式与 shell 不同。

相关内容