LoginSignup
14
10

More than 1 year has passed since last update.

誰も知らない xargs の仕様と入力形式とPOSIXの罠 「ある環境では _ がきたらそこで処理終了ですよ」

Last updated at Posted at 2021-09-09

はじめに

xargs の仕様はよくわからないし罠もあります。みんな気軽に使ってばかりで、詳細を誰もまとめてくれてないのでまとめました。もちろん誰も知らないってことはないのでしょうが、よく使われるコマンドの割にこれらの話を書いている人が少なすぎるような。

予備知識

printf "[%s]\n を使うと引数ごとに [ ] で括られて表示されます。いくつの引数を渡されたの明確にするのに便利です。

$ LF="
"
$ printf "[%s]\n" "foo1" "bar 2" "b${LF}a${LF}z"
[foo1]
[bar 2]
[b
a
z]

入力フォーマット

入力フォーマットには、-0 オプションを指定したときと指定しない時に大きく2つのパターンがあります。なお POSIX では -0 オプションは規定されていません。

補足 xargs のクォートのルールはシェルのクォートルールとは異なります(参照

Note that the quoting rules used by xargs are not the same as in the shell.

-0 オプションを指定しない場合

-0 が POSIX で規定されていないため、POSIX に準拠したシェルスクリプトを書く場合、こちらの入力フォーマットを使用するしかありません。

1. 「スペース・タブ・改行」は引数の区切り文字

連続した「スペース・タブ・改行」は一つの引数として扱われます。

$ printf 'foo  \n  bar \t  baz\n' | xargs printf "[%s]\n"
[foo]
[bar]
[baz]

一般論として最後の行は改行で終わることを推奨します。末尾の改行を省略した場合、正しく処理できない実装があるようです(未確認 → 2021-09-12 追記 AIXであるとの情報をいただきました。下記コメント参照)。もしデータが改行で終わらない可能性がある場合、単に改行を追加するだけで良いです(末尾に改行が複数あっても一つとみなされる)。

$ { printf 'foo  \n  bar \t  baz'; echo; } | xargs printf "[%s]\n"
[foo]
[bar]
[baz]

2. クォートすると「スペース・タブ」の特別な意味はなくなる

クォートにはシングルクォートとダブルクォートの両方が使えます。シングルクォートとダブルクォートに機能的な違いはありません。

$ printf "\"f o o\" 'b a r' 'b \t a \t z'\n" | xargs printf "[%s]\n"
[f o o]
[b a r]
[b 	 a 	 z]

3. クォートの中に同じクォート文字は入れられない

バックスラッシュでエスケープしても入れることはできません。

$ printf "'f 'o o'\n" | xargs printf "[%s]\n"
xargs: シングルクオートが一致しません。デフォルトでは -O オプションを指定しない限り
xargs でクォートは特別な意味を持ちます
[f o]

4. クォートしても改行の特別な意味はなくならない

クォートの中に改行文字を入れられないということを意味します。

$ printf "'f \n o o\n" | xargs printf "[%s]\n"
xargs: シングルクオートが一致しません。デフォルトでは -O オプションを指定しない限り 
xargs でクォートは特別な意味を持ちます

以下のようにみなされるので、クォートが閉じてないと判断されます。

'f
o o'

5. クォートしない場合にバックスラッシュで「スペース・タブ・改行」をエスケープできる

改行を含めたい場合はこの方法しかなさそうです。

$ printf 'f\\ o\\ o b\\\ta\\\tr b\\\na\\\nz\n' 
f\ o\ o b\	a\	r b\
a\
z

$ printf 'f\\ o\\ o b\\\ta\\\tr b\\\na\\\nz\n' | xargs printf "[%s]\n"
[f o o]
[b	a	r]
[b
a
z]

6. 空文字をデータとして渡す場合はクォートを使えば良い

$ printf 'foo\n""\nbar\nbaz' | xargs printf "[%s]\n"
[foo]
[]
[bar]
[baz]

7. 前後の「スペース・タブ・改行」は無視される

$ printf '\n\n\nfoo\nbar\nbaz\n\n\n' | xargs printf "[%s]\n"
[foo]
[bar]
[baz]

8. \0 を読み取るとそこで読み取りは終了

GNU 版では警告が出力されます。細かく調べていませんが Coreutils 6.0 (2006) ぐらいからのようです。

$ printf 'foo\0bar\0baz' | xargs printf "[%s]\n"
xargs: 警告: NUL 文字が入力にあります。これは引数のリストとして渡すことができません。 
--null オプションを使おうとしているのですか?
[foo]

-0 オプションを指定した場合

クォートとかエスケープという概念がないのでこちらは簡単です。

1. \0 区切り

$ printf 'f o o\0b\ta\tr\0b\na\nz\0' | xargs -0 printf "[%s]\n"
[f o o]
[b	a	r]
[b
a
z]

2. 最後の \0 は無視される

$ printf 'foo\0bar\0baz\0' | xargs -0 printf "[%s]\n"
[foo]
[bar]
[baz]

-0 を付けない場合に、一行の最後は改行で終わるのと同じ扱いです。通常は末尾に \0 をつけておくべきでしょう。

3. 空の項目は GNU だと無視されず、BSD だと無視されます

GNU

$ printf '\0foo\0\0bar\0baz\0\0' | xargs -0 printf "[%s]\n"
[]
[foo]
[]
[bar]
[baz]
[]

BSD

$ printf '\0foo\0\0bar\0baz\0\0' | xargs -0 printf "[%s]\n"
[foo]
[bar]
[baz]

つまり BSD 版だと -0 を使った場合に空文字をデータとして渡すことはできないように見えます。

入力終了文字

-E オプションを使うと指定した文字で読み取り終了できる

一体どういう場面でこの機能を使うのでしょうか?あると便利であるような気もしますが、具体的なユースケースを思いつきません。

$ printf 'foo \n bar \n _ \n baz' | xargs -E _ printf "[%s]\n"
[foo]
[bar]

【罠】 一部の環境は -E _ がデフォルト

POSIX では -E オプションを指定しないときのデフォルトの動作は「未定義」であると定義されています(注意 POSIX が未定義と決めたから各実装がバラバラに実装したのではなく、POSIX より前から存在した実装がバラバラだったから POSIX では「未定義」と定義しました)。つまり確実に移植性を持たせたいなら xargs を使う時には必ず -E "" を指定する必要があります。

該当環境 Solaris 10、11 (おそらく歴史的な Unix の動きであると思われるため、未検証ですが他の商用 Unix でも同じだと思われます。)

$ printf 'foo \n bar \n _ \n baz' | xargs printf "[%s]\n"
[foo]
[bar]

-E "" を指定すれば _ を読み取っても終了することはありません。

$ printf 'foo \n bar \n _ \n baz' | xargs -E "" printf "[%s]\n"
[foo]
[bar]
[_]
[baz]

空文字を読み取っても終了することはないようです。

printf 'foo\n""\nbar\n\nbaz' | xargs -E "" printf "[%s]\n"
[foo]
[]
[bar]
[baz]

現実への影響

xargs とよく組み合わせて使うと思われる find ではおそらくパスとして _ が出力されることはないと思います。./ で始まる相対パス、もしくは / で始まる絶対パスのどちらかになると思います。ただし find 以外では問題が発生する場合があるでしょう。

例えば「覚えると便利、xargsコマンドの使い方12選」の例の一つ「lsコマンドの出力を1行にまとめる方法」を Solaris 10 で実行すると

$ ls -1 files/ # 以下のファイルが files ディレクトリにあるとする
_
a
b
c

$ ls -1 files/ | xargs # 最初の _ で打ち切られるので、なにも出力されません

他の例では改行を取り除くのに xargs が使われたりしますが、この場合もデータの中に _ が含まれているとそこからデータが途切れてしまいます。

$ cat list.txt
foo
bar
_
baz

$ cat list.txt | xargs
foo bar

Solaris 10 での注意点

Solaris 10 の /usr/bin/xargs (おそらく POSIX 以前の歴史的な実装)は -E で空文字を指定することができません(Solaris 11 では問題ありません)。この場合 -e オプションで代用することができるようです。

$ printf 'foo\n""\nbar\n_\nbaz' | xargs -e printf "[%s]\n"
[foo]
[]
[bar]
[_]
[baz]

-e オプションは 1997 年の POSIX Issue 5では規定されていましたが、2004 年の POSIX Issue 6 で削除されました。経緯も書かれているのですが、これまた気になることが書かれていて、歴史的な(POSIX 以前の) xargs にはまた別の罠があるようですが、検証環境がないので引用にとどめます。

The -e option was omitted from the ISO POSIX-2:1993 standard in the belief that the eofstr option-argument was recognized only when it was on a line by itself and before quote and escape processing were performed, and that the logical end-of-file processing was only enabled if a -e option was specified. In that case, a simple sed script could be used to duplicate the -e functionality. Further investigation revealed that:

  • The logical end-of-file string was checked for after quote and escape processing, making a sed script that provided equivalent functionality much more difficult to write.
  • The default was to perform logical end-of-file processing with an as the logical end-of-file string.

To correct this misunderstanding, the -E eofstr option was adopted from the X/Open Portability Guide. Users should note that the description of the -E option matches historical documentation of the -e option (which was not adopted because it did not support the Utility Syntax Guidelines), by saying that if eofstr is the null string, logical end-of-file processing is disabled. Historical implementations of xargs actually did not disable logical end-of-file processing; they treated a null argument found in the input as a logical end-of-file string. (A null string argument could be generated using single or double-quotes ( '' or "" ). Since this behavior was not documented historically, it is considered to be a bug.

お手軽なワークアラウンド

xargs を使う時に、必ず -E "" をつけるのは面倒ですよね? この関数をシェルスクリプトの上の方に書いておくだけで簡単にデフォルトで -E "" の動作となります。

xargs() { command xargs -E "" "$@"; } 

【罠】 macOS (FreeBSD) では -E オプションの挙動がおかしい

空文字以外を指定すると 必ず一行目 空白で始まる行で止まる。バグ?

該当環境 macOS 11.4 Big Sur、FreeBSD 13.0 など

$ printf 'foo \n bar \n _ \n baz' | xargs -E "_" printf "[%s]\n"
[foo]

空入力時の処理

Linux (GNU)、OpenBSD、Solaris の場合

$ printf '' | xargs echo "run" # コマンドが実行される
run

macOS、FreeBSD、NetBSD の場合

$ printf '' | xargs echo "run" # コマンドが実行されない

データが空の場合にコマンドが実行されるかどうかの挙動が異なります。FreeBSD の挙動の方が便利そうに見えますが POSIX の仕様では「1 回以上実行される」とあるので、GNU や Solaris の動作の方が POSIX に準拠した動作であるようです。

The utility named by utility shall be executed one or more times until the end-of-file is reached or the logical end-of file string is found.

どうやら歴史的な Unix の xargs ではデータがなくてもコマンドが実行されるのに対して FreeBSD では実行しないことを選んだようです。GNU では POSIX (及び歴史的な動作)をデフォルトする代わりに FreeBSD と同じ挙動に合わせる -r (--no-run-if-empty) オプションを追加しました(いつ追加されたかまでは調べていませんが Debian 2.2 時点ですでに追加されていました。)。そのため -r オプションに対応していない Solaris、AIX、HP-UP を無視すれば FreeBSD の挙動に統一することができます。例えば、以下のコードを冒頭にいれるだけで簡単に GNU と FreeBSD の挙動の違いを吸収することができます。

# もしデータがないのに false コマンドが実行されてしまったら
if ! xargs false < /dev/null; then
  # 常に -r オプションをつけて呼び出すように回避用のシェル関数を定義する
  xargs() { command xargs -r "$@"; } 
fi

また比較的最近の環境に限定するのであれば上記の回避策は必要ありません。FreeBSD 7.1 以降の xargs は GNU コマンドとの互換性のために -r オプション(何の効果もないダミーオプション)をサポートしてるので、単に xargs -r を使うだけで解決します。macOS の xargs は -r オプションをサポートしていないという情報が多いですが、少なくとも macOS 11.4 Big Sur では対応していました。おそらく比較的最近の macOS のバージョンで更新されたのだと思います。

  • -r オプションをサポートしている
    • Debian 2.2.7
    • OpenBSD 3.9 (2006-05)
    • FreeBSD 7.1 (2009-01) (ダミーオプション)
    • NetBSD 6.0 (2012-10) (ダミーオプション)
    • macOS 11.4 Big Sur (ダミーオプション)
  • -r オプションをサポートしていない
    • Solaris 10, 11
    • AIX 7.2
    • HP-UX 11.22

もし -r オプションをサポートしてない Solaris などの環境で FreeBSD の動作に統一したい場合は、引数を見てコマンドを実行するかどうかの簡単なラッパーを作ればよいでしょう。それほど難しいものでもないので参考として作っておきました。もし必要な方がいればご自由にどうぞ。一応 Solaris 10 の Bourne シェル (/bin/sh) でも動くコードにして軽く動作確認していますが十分なテストはしてないので注意してください。

念の為ですが以下のスクリプトは Solaris などの環境に対応する場合にのみ必要です。また GNU Coreutils をインストールすることが可能であれば、その方法で解決した方が良いです。その他の多くの問題も解決します。

# ライセンス: CC0 (パブリックドメイン)、ご自由にどうぞ
# xargs で呼び出すコマンドの前に ./no-run-if-empty 引数の数 を追加する。
# xargs というシェル関数を定義して挙動を修正しています
xargs() (
  wrapper="./no-run-if-empty"
  OPTIND=1
  # その他のオプションを使えるようにするにはここに定義を追加してください。
  while getopts E:I:L:n:ps:tx OPT; do :; done
  len=$#
  # 古い環境の可能性があるため Bourne シェルで動くように expr を使っています。
  pos=`expr $# + $OPTIND - 1`
  args=`expr $# - $OPTIND + 1`
  for i in "$@"; do
    [ $# -eq "$pos" ] && set -- "$@" "$wrapper" "$args"
    set -- "$@" "$i"
  done
  [ "$args" -eq 0 ] && set -- "$@" "$wrapper" "$args"
  shift "$len"
  exec xargs "$@"
)

# 上記のシェル関数を定義するだけで利用者側のコードを変更する必要はありません
printf '' | xargs echo "run"
printf 'foo' | xargs echo "run"

# このように直接 ./no-run-if-empty を使用するなら上記の xargs 関数は不要
printf '' | xargs ./no-run-if-empty 1 echo
no-run-if-empty
#!/bin/sh
# ライセンス: CC0 (パブリックドメイン)、ご自由にどうぞ
# 1 番目の引数は固定の引数の数。この数より多くの引数が渡された場合にのみコマンドを実行する
set -eu
num=$1
shift
[ $# -gt "$num" ] || exit 0
[ "$num" -eq 0 ] && set echo "$@"
exec "$@"

中断機能がある

実は exit 255 すると途中で処理を中断することができます。使ったことはありませんが便利だとは思います。ただし xargs で実行するコマンドで exit 255 を別の意味に使っていたりすると問題が発生する可能性があるので注意が必要です。

printf 'foo\n bar\n baz\n' | xargs -n 1  sh -c 'echo "$1"; exit 255' --
foo
xargs: sh: 終了ステータス 255。中止しています

さいごに

この記事の内容は xargs の入力形式の調査と仕様や罠の2つに大きく別れています。xargs の入力形式の調査は私が POSIX に準拠した範囲でスペースや改行等を正しく扱うことは可能なのか?ということを知りたかったため調査しました。結果としては(クォートするのではなく)バックスラッシュでエスケープすることで可能であると思われます。つまり標準入出力経由で xargs にわたすデータを正しくエスケープすれば本来は -0 オプションは必要ないわけです。ただし実際には xargs で読み取るのに適した形式で出力するコマンドは(私が知る限り)存在せず、代わりに単純な \0 区切りというデータ形式が広く使われています。

もう一つの罠の話ですが、_ が来たら停止するのは Solaris (商用 Unix?)だけで、-r オプションはもう広く使えます。現実にはほとんど環境で問題がないのでさほど気にする必要はないでしょう。この問題で困るのはどうしても(POSIX に準拠した)すべての環境で動作するようにしたいという人だけです。そのためにはこの記事で書いたような細かい罠にまで対応しなければしなければいけないということです。すべての環境で動くようにしたいというのであれば、そうするのは自由ですが本当にすべての環境で動くようにしなければならないのでしょうか? それを実現するには POSIX に準拠するだけでは不十分です。環境の数だけ違いがあるわけで、本当にそれらの環境に対応する価値があるのかを考える必要があります。もしそれらの環境で動かす予定がないなら対応する価値はまったくありません。対応するというならば、十分なテストをしなければいけません。POSIX に準拠した所で動く保証はまったくないわけでテストしてないならまず動きません。POSIX に準拠してコードを書くことは、動くかもしれないなーという希望的観測が得られる程度の意味しかありません。

そして多くの環境に対応するならば、それなりのテクニックが必要です。本文ではさらっと流しましたが、この記事で紹介した -E-r の問題への解決策は xargs コマンドの問題を xargs シェル関数で修正する手法を使っており、xargs を利用する側のコードを全く変えずに問題を解決しています。いろんなコマンドを組み合わせて試してみて動くパターンを探すなどという、その場しのぎの対処療法に振り回されている限りシェルスクリプトで高い生産性も移植性も実現することはできません。それで動いても、たまたまその環境でうまく動いたというだけのことです。本当の「Write once, run anywhere(一度書けばどこでも動く)」というのはこの記事で書いたようなくだらない罠をなくして初めて実現できることです。他の言語では移植性の問題を気にせずコードを書いてもほとんど同じように動きます。それに比べてシェルスクリプトの世界の移植性は圧倒的に遅れています。「バッドノウハウを知ってどの環境でも動くように気をつけてコードを書いて、それでやっとどうにかどの環境でも動くようになる」という現状は「POSIX に準拠するだけでシェルスクリプトはどこでも動く」という理想を全く実現できていません。バッドノウハウにいくら詳しくなっても時間を無駄に費やすだけで技術力にはなりません。もしバッドノウハウが必要なくなった時ムダ知識だけあっても意味がないでしょう?バッドノウハウは無くさなければいけません。いずれはシェルスクリプト版 jQuery とでも言えるような互換性を吸収するライブラリを使うだけで、面倒なことを何も考えずにどこでも動くシェルスクリプトが書けるようにしたいものですね。

14
10
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
10