まえがき
コマンドラインで大量のファイルやディレクトリを扱う際に「argument list too long」というエラーに直面することがあります。
このエラーは、システムが扱える引数の上限を超えた場合に発生します。
その原因と対処法について解説します。
エラーの原因
コマンドラインでコマンドに引数を渡す際、例えばls dir1のように記述します。
ここでdir1が引数となりますが、この引数の数がシステムの許容上限を超えると「argument list too long」エラーが発生します。
この上限値はgetconf ARG_MAXコマンドで確認でき、多くの環境では約 2MB と設定されています。
この値はシステムのコンパイル時に定められるため、動的に変更することはできません。
試しに確認してみると、
$ getconf ARG_MAX
2097152
手元の環境では2MBと表示されました。
この問題が発生する時は大体がワイルドカードを使用したコマンドを実行した時だと思います。
$ ls dir*
上記のようにワイルドカードを使用すると、シェルが展開を行いls dir*コマンドは下記のように置き換えられます。
$ ls dir1 dir2 dir3 ...
もしdirディレクトリが100 万個あったとすると、先ほどの上限は軽く超えてしまうためargument list too longエラーとなってしまいます。
対処方法その 1 xargs コマンドの使用
大量のファイルやディレクトリを扱う場合、xargsコマンドが非常に有効です。xargsは標準入力から受け取ったデータを元に、指定したコマンドの引数として渡し、実行することができます。
例えば、以下のように使用します。
$ find . -maxdepth 1 -type d -name 'dir*' | xargs ls
xargsの動作は簡単に以下の流れになります。
- 標準入力から読み込んだ文字列を指定したコマンド(例だと
ls)の引数として与え作成する - 作成したコマンドを実行する
上記例の場合は下記のコマンドを作成してくれます。
$ ls dir1 dir2 dir3 ...
ですが、このままではまたargument list too longが起きるのでは?と思ってしまいますが、xargs はシステムが定めているコマンドラインの長さの限界に達するまで長いものが作成される仕様となっているため、argument list too longエラーは発生しません。
つまり限界が来たら下記のように複数回のコマンド実行に置き換えられます。
$ ls dir1 dir2 dir3 ... dir1000 dir1001 dir1002
$ ls dir1003 dir1004 ... dir2001 dir2002 dir2003
ちなみに xargs のコマンドラインの限界の長さは--show-limitsオプションで確認できます。
$ xargs --show-limits < /dev/null
※< /dev/nullは入力待ちを防ぐために指定しています
Your environment variables take up 6320 bytes
POSIX upper limit on argument length (this system): 2088784
POSIX smallest allowable upper limit on argument length (all systems): 4096
Maximum length of command we could actually use: 2082464
Size of command buffer we are actually using: 131072
Maximum parallelism (--max-procs must be no greater): 2147483647
5 行目のSize of command buffer we are actually usingの項目がそれで手元の環境では約 130kとなっていました。
システムの限界が2MBだったのに130kとなっているのはs オプションを指定しない場合のデフォルト値が130kになっているからです。
試しにs オプションで2MBを指定してみると変更されることが分かります。
$ xargs -s 2000000 --show-limits < /dev/null
...省略
Size of command buffer we are actually using: 2000000
...省略
ところで、getconf ARG_MAXコマンドで取得した上限は2MBでした。
ではこの上限を超えた値、例えば3MBをSize of command buffer we are actually usingの項目に指定するとどうなるのでしょうか。
答えは上限に補正される、です。
試しに指定してみると2MBとなり、上限に補正されていることが分かります。
$ xargs -s 3000000 --show-limits < /dev/null
...省略
Size of command buffer we are actually using: 2088784
...省略
xargs の動作を実際に確認してみよう
xargsの利便性は分かってもテストしないと怖くて実行できないと思います。
ここではテスト環境を作成して実際に動作を確認してみましょう。
テスト環境を作成する
ではテスト環境を作成して実際にxargsコマンドの動作を確認してみましょう。
ここではディレクトリが 1000 個、各ディレクトリにファイルが 100 個あるケースで実際にやってみます。testディレクトリを作成し移動します。
$ mkdir test
$ cd test
次にディレクトリを 1000 個作成し、各ディレクトリに 100 個ずつファイルを作成します。
$ mkdir dir{0001..1000}
$ find . -maxdepth 1 -type d -name 'dir*' | xargs -I {} touch {}/file{001..100}.txt
findコマンドの所でxargsを使っていますが、この例では既にargument list too longが発生する状態のため使用せざるを得ません。
ここで使用しているIオプションは標準入力から渡される文字列を任意の場所に埋め込みたい場合に使用します。xargs lsのように、何も指定しないとlsの後ろにしか追加されないため、コマンドの指定の位置に埋め込みたい場合はIオプションを使用します。
この場合、{}はdir0001に置き換えられることになり実際は、
touch dir0001/file{001..100}.txt dir0002/file{001..100}.txt ...省略
という形になります。
置き換える文字列に{}を使っていますがaaaやbbbといった任意の文字列が使えます。ただし基本的に{}を使うことになっており、混乱を避けるためにも特に理由がなければ{}を使うことをお勧めします。
argument list too long を発生させてみる
少し話がそれましたが、テスト環境でargument list too longが発生するか確認してみましょう。
$ ls **/*.txt
bash: /usr/bin/ls: Argument list too long
全ての*.txtファイルを取得しようとするとargument list too longが発生しました。
ちゃんと上限を超えているようですね。
.txt ファイルを一つずつ取得してみる
では次に各ディレクトリの.txtファイルを一つずつ取得してfilelist.txtに結果を出力します。
$ find . -type f -name '*.txt' | xargs ls -l > filelist.txt
wcコマンドでfilelist.txtに出力された行数をカウントします。
$ wc -l filelist.txt
100001 filelist.txt
行数を数えるとちゃんと 10 万行になっている事が分かります。(100001 となっているのは最後の改行によるもの)
これで大量のファイルを1つずつ処理できることが確認できましたが、実際に実行されるコマンドを事前に確認したいことがあると思います。
その場合はp オプションを使用します。
$ find . -type f -name '*.txt' | xargs -p ls -l
ls -l ./dir0586/file022.txt ...省略... ./dir0533/file071.txt ?...
実行するととてつもなく長いコマンドが表示された後に?...と表示され入力待ちになります。
ここでyまたはYを入力すると表示されたコマンドが実行され、そのまま enter を押下するとスキップされて次のコマンドが表示されるようになっています。
特に削除するようなコマンドを実行する時はp オプションを指定して、想定した引数になっているか確認してから実行するようにしましょう。
実際に削除してみる
ここまで確認できれば大量ファイルの削除は簡単です。
各ディレクトリのfile001.txt~file080.txtを削除してみましょう。
まずは念のため、まとめて削除できないことを確認してみます。
$ rm **/file{000..080}.txt
bash: /usr/bin/rm: Argument list too long
やはりエラーが出て削除できませんね。
引数が多すぎるようです。
では1つずつファイルを取得してxargsに削除コマンドを作成してもらい実行する形にしてみます。
今回はテストのためpオプションなしで確認せずに一気に削除します。
$ find . -maxdepth 1 -type d -name 'dir*' | xargs -I {} rm {}/file{001..080}.txt
削除後、現在のファイルリストを再度作成します。
$ find . -type f -name '*.txt' | xargs ls -l > filelist.txt
ファイル数を出力します。
$ wc -l filelist.txt
20001 filelist.txt
ちゃんと 8 万個のファイルが削除されてますね。
実際にfile001~file080まで削除されているか確認してみましょう。
$ ls -l dir0001
total 0
-rw-r--r-- 1 root root 0 Dec 23 08:36 file081.txt
-rw-r--r-- 1 root root 0 Dec 23 08:36 file082.txt
-rw-r--r-- 1 root root 0 Dec 23 08:36 file083.txt
-rw-r--r-- 1 root root 0 Dec 23 08:36 file084.txt
-rw-r--r-- 1 root root 0 Dec 23 08:36 file085.txt
-rw-r--r-- 1 root root 0 Dec 23 08:36 file086.txt
-rw-r--r-- 1 root root 0 Dec 23 08:36 file087.txt
-rw-r--r-- 1 root root 0 Dec 23 08:36 file088.txt
-rw-r--r-- 1 root root 0 Dec 23 08:36 file089.txt
-rw-r--r-- 1 root root 0 Dec 23 08:36 file090.txt
-rw-r--r-- 1 root root 0 Dec 23 08:36 file091.txt
-rw-r--r-- 1 root root 0 Dec 23 08:36 file092.txt
-rw-r--r-- 1 root root 0 Dec 23 08:36 file093.txt
-rw-r--r-- 1 root root 0 Dec 23 08:36 file094.txt
-rw-r--r-- 1 root root 0 Dec 23 08:36 file095.txt
-rw-r--r-- 1 root root 0 Dec 23 08:36 file096.txt
-rw-r--r-- 1 root root 0 Dec 23 08:36 file097.txt
-rw-r--r-- 1 root root 0 Dec 23 08:36 file098.txt
-rw-r--r-- 1 root root 0 Dec 23 08:36 file099.txt
-rw-r--r-- 1 root root 0 Dec 23 08:36 file100.txt
削除後に確認してみるとfile001.txt~file080.txtがちゃんと削除されていることが分かります。
対処方法その 2. findコマンドの -exec オプションの使用
xargsの代わりに、findコマンドの-execオプションを使用してもargument list too longの問題を解決できます。-execオプションを利用すると、findが見つけた各ファイルに対して直接コマンドを実行することができます。
$ find . -type f -name '*.txt' -exec ls -l {} \;
上記は下記のようなイメージになります。
$ ls -l 1.txt
$ ls -l 2.txt
$ ls -l 3.txt
...
こちらでもargument list too longのエラーは回避できますが、一つ一つのファイルに対してコマンドを実行することになりxargsと比べるとかなり非効率です。
ですが、まとめてファイルを処理する必要がないケースではxargsと比べてfindと-execオプションを使用した方が簡潔に記載できるメリットがあります。
例えば下記のようなケースではfindと-execオプションの組み合わせが有効です。
特定のファイルを見つけて削除する
特定の拡張子を持つファイルを探して削除する必要がある場合、-execオプションを使用すると、見つかった各ファイルに対して直接rmコマンドを実行できます。
$ find /path/to/directory -type f -name "*.tmp" -exec rm {} \;
このコマンドは/path/to/directory内の.tmp拡張子を持つすべてのファイルを検索し、それぞれを削除します。xargsを使用するよりも直接的で、シンプルなタスクに対してはより読みやすい解決策です。
ファイルに対して特定のコマンドを実行する
見つけたファイルに対してコマンドを実行する必要がある場合、例えばファイルのパーミッションを変更する場合、-execオプションを使うことで、各ファイルに対して直接chmodコマンドを実行できます。
$ find /path/to/directory -type f -name "*.sh" -exec chmod +x {} \;
複数のコマンドを連鎖させる
-execオプションを使用すると、見つけたファイルに対して複数のコマンドを連鎖させて実行することも可能です。
例えば、特定のファイルを見つけて内容を表示した後に削除する場合、以下のように記述できます。
$ find /path/to/directory -type f -name "*.log" -exec cat {} \; -exec rm {} \;
このコマンドは、.logファイルを検索し、それぞれの内容を表示(cat)した後に削除(rm)します。
この方法は、複数のステップを一つのコマンドライン操作で実行したい場合に有効です。
まとめ
argument list too longエラーは、コマンドラインで大量のファイルを扱う際によく遭遇する問題です。
しかし、xargsやfindの-execオプションを適切に使用することで、この問題を回避しつつ作業を効率化することが可能です。
さらに詳しい情報やオプションについては、各コマンドのマニュアルを参照してください。
JM Project find
JM Project xargs
関連記事
xargs の「command line cannot be assembled, too long」エラーについては下記をお読みください。
「xargs 引数行が長すぎます」エラーの調査と解決策
実行環境
$ cat /etc/os-release
NAME="Ubuntu"
VERSION="20.04.3 LTS (Focal Fossa)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 20.04.3 LTS"
VERSION_ID="20.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=focal
UBUNTU_CODENAME=focal
$ xargs --version
xargs (GNU findutils) 4.7.0
Copyright (C) 2019 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Written by Eric B. Decker, James Youngman, and Kevin Dalley.