https://www.oilshell.org/blog/2021/08/xargs.html blog | oilshell.org An Opinionated Guide to xargs 2021-08-21 This post has everything you need to know about xargs, an essential tool for shell programming. It's based on my #comments on this "wrong" post: xargs considered harmful (codefaster.substack.com via lobste.rs) 24 points, 31 comments on 2021-07-16 Apologies to the author for the criticism, but it generated a great discussion! Here is what lies ahead: * An introduction to xargs, and a discussion of alternatives. * Tips on using it, accompanied by #sample-code in the blog-code/ xargs directory. * High-level thoughts on shell and the Oil language. Table of Contents Preliminaries What Is xargs? Which Flags Should I Know About? When Globs Are Enough Prefer xargs Over Shell's Word Splitting Usage Tips Choose One of 3 Ways of Splitting stdin xargs Can Invoke Shell Functions With the $0 Dispatch Pattern Preview Tasks With an echo Prefix xargs -P Automatically Parallelizes Tasks Use -n To Batch Args, not -L Benefits Of This Style Start One rm process, Not 10,000 Recap Conclusions Slogan: Shell-Centric Shell Programming Oil Language: each Builtin Appendices Alternative Shell Challenge #2 More Comments Preliminaries What Is xargs? It's an adapter between text streams and argv arrays, two essential concepts in shell. You pass it flags that specify how to split stdin. Then it generates arguments and invokes processes. Example: $ echo 'alice bob' | xargs -n 1 -- echo hi hi alice hi bob What's happening here? 1. xargs splits the input stream on whitespace, producing 2 arguments, alice and bob. 2. We passed -n 1, so xargs then passes each argument to a separate echo hi $ARG command. By default, it passes as many args to a command as possible, like echo hi alice bob. It may help to mentally replace xargs with the word each. As in, for each word, line, or token, invoke this process with these args. In fact, I propose an each builtin for the Oil language below. (This explanation was derived from this comment on the same thread.) Which Flags Should I Know About? You should know how to control: 1. The algorithm for splitting text into arguments (-d, -0). Discussed below. 2. How many arguments are passed to each process (-n). This determines the total number of processes started. 3. Whether processes are run in sequence or in parallel (-P). When Globs Are Enough The blog post suggests rm $(ls | grep foo) as an alternative to xargs. In this case, it's better to just use a glob, which is built into the shell: rm *foo* Prefer xargs Over Shell's Word Splitting Besides the extra ls, the suggestion is bad because it relies on shell's word splitting. This is due to the unquoted $(). It's better to rely on the splitting algorithms in xargs, because they're simpler and more powerful. (Related: Oil Doesn't Require Quoting Everywhere.) For example, if you want the power of regexes to filter names, you can pipe to egrep, then explicitly split its output by newlines: # Remove Python and C++ unit tests ls | egrep '.*_test\.(py|cc)' | xargs -d $'\n' -- rm Usage Tips Now that we've introduced xargs and discussed alternatives, here's some advice for using it. Choose One of 3 Ways of Splitting stdin In the comment, I suggest using only these three styles of splitting: 1. xargs (the default): when you want "words" without spaces. For example, you can produce two args from the string 'alice bob'. 2. xargs -d $'\n': When you want the args to be lines, as in the egrep example above. (Note that $'\n' is bash syntax for a newline character, and Oil uses this syntax too.) 3. xargs -0: When you want to handle untrusted data. Someone could put a newline in a filename, but this is safe with NUL-delimited tokens. Most of my scripts use the second style, and occasionally the third. Unix tools generally work better on streams of lines than streams of "words" or NUL-delimited tokens. Those formats make it harder to filter the list of tasks/items. That is, grep doesn't support an analogous -0 flag. (This is one motivation for Oil's QSN serialization format. It's line-based, so regular grep still works. It can also represent every string, including those with NULs.) xargs Can Invoke Shell Functions With the $0 Dispatch Pattern The original post discusses xargs -I {}, which allows you to control where each argument is substituted in the argv array. I occasionally use -I, but more often I use xargs with what I call the $0 Dispatch Pattern. I outlined this shell programming pattern last month, but I still need to elaborate on it. The basic idea is to avoid the mini language of -I {} and just use shell -- by recursively invoking shell functions. I use this all over Oil's own shell scripts, and elsewhere. Example: do_one() { # Rather than xargs -I {}, it's more flexible to # use a function with $1 echo "Do something with $1" cp --verbose "$1" /tmp } do_all() { # Call the do_one function for each item. # Also add -P to make it parallel cat tasks.txt | xargs -n 1 -d $'\n' -- $0 do_one } "$@" # dispatch on $0; or use 'runproc' in Oil Now run this script with either: * my_script.sh do_one $ARG to test the work that's done on each item. You want to make this correct first. * myscript.sh do_all to do work on all items. This breaks the problem down nicely: make it work on one item, and then figure out which items to run it on. When you combine them, they will work, unlike the "sed into bash" solution given in the original post. In other words: Use the Shell Language, Not Mini-Languages Like xargs -I {}. This reduces language cacophony. Preview Tasks With an echo Prefix Before running a command like: $ cat tasks.txt | xargs -n 1 -- $0 do_one It's often useful to preview it with echo: $ cat tasks.txt | xargs -n 1 -- echo $0 do_one demo.sh do_one filename demo.sh do_one with demo.sh do_one spaces.txt # Oops! We split the input the wrong way. # We wanted xargs -d $'\n'. xargs -P Automatically Parallelizes Tasks In the do_all example above, you can add -P 8 to the xargs invocation to automatically parallelize it! For example, if you have 1000 indepdendent tasks, xargs will use 8 CPUs to run them as quickly as possible. I've used -P 32 to make day-long jobs take an hour! You can't do that with a for loop. This is one of my favorite tricks, and 3 years ago I gave a 5 minute presentation ago at #recurse-center about it: * What is xargs -P? When Is it Useful? Try xargs -P Before GNU Parallel Some shell users use GNU parallel to parallelize processes. I avoid it because it has yet another mini-language with {} and :::. I don't think there are any problems GNU parallel can solve that xargs -P combined with shell functions can't solve. Let me know if you have a counterexample. Use -n To Batch Args, not -L The original article talks a lot about xargs -L, which I never use. It looks like a mini data language to be avoided: e.g. trailing blanks meaning something special. I asserted that -n was always better than -L in the comments, and nobody found a counterexample. Benefits Of This Style Start One rm process, Not 10,000 A lobste.rs user asked why you would use find | xargs rather than find -exec. The answer is that it can be much faster. If you're trying to rm 10,000 files, you can start one process instead of 10,000 processes! It's basically rm one two three vs. rm one rm two rm three Other commenters pointed out that you can use find -exec + instead of find -exec \;, but I'd say that's another mini-language to be avoided. Links: * A comparison showing that find -exec is slower: https:// www.reddit.com/r/ProgrammingLanguages/comments/frhplj/ some_syntax_ideas_for_a_shell_please_provide/fm07izj/ * Another comparison: https://old.reddit.com/r/commandline/comments /45xxv1/why_find_stat_is_much_slower_than_ls/ Recap To repeat, here are the benefits of the style I advocate: 1. A Clean Problem Separation: Figure out what to do on each item (what's a task?), then figure out what items to do it on (what tasks should I run?) 2. Easy Testing by previewing tasks with echo. This avoids running long batch jobs on the wrong input! 3. Better Performance. + xargs lets you start as few processes as possible. + It also lets you start those processes in parallel. You can't do this with a for loop. 4. Fewer Languages to Remember. We use plain shell and a few flags to xargs. Conclusions This post explained xargs, gave advice on using it, and justified the advice. The most important takeaway is that you can invoke and parallelize shell functions with xargs, via the $0 Dispatch Pattern. I use this pattern all the time, but I've rarely seen it used in the wild. Try it out and let me know what you think! You can start from the sample code in the blog-code/xargs directory. Slogan: Shell-Centric Shell Programming You may have noticed a high level pattern to this advice: We avoid "mini-languages" in various tools, and use the shell language instead: * Shell functions and $1, instead of xargs -I {} * xargs and xargs -n 1 instead of find -exec + and find -exec \; * -n instead of -L (to avoid an ad hoc data language) * xargs with simple flags instead of GNU parallel * Bash syntax $'\n' instead of tool syntax '\n'. Using the shell is simpler than relying on every tool to understand the 2 character escape sequence \n. (Remember that I'm working on Slogans, Fallacies, and Concepts for shell programming.) Oil Language: each Builtin I sketched an idea for each and every builtins in Oil in this comment on the same thread. On second thought, I think we should only have each, and it should look something like this: # Items are lines by default. # Start as few processes as possible, like xargs # and 'find -exec +' find . | each { rm --verbose @_items # remove many files } # Start one process for each item, like 'xargs -n 1' # and 'find -exec \;' find . | each --one { echo "Do something with $_item" } # Parallelize it find . | each --one -P 8 { echo "Do something with $_item" sleep 0.1 } So the separate do_one and do_all functions are avoided with Ruby-like blocks in the Oil language. Just like the cd builtin accepts a block, the each builtin can as well. Let me know what you think! Appendices Alternative Shell Challenge #2 I issued an "alternative shell challenge" last year: Can you redirect stdout of a shell function that invokes both builtins and external processes? * Four More Posts in "Shell: The Good Parts" (February 2020) Here's another challenge for alternative shells: can you parallelize your shell's notion of functions with xargs -P 8? As shown above, it's done in Bourne shell with xargs -P 8 -- $0 myfunc. Both of these challenges relate to shell functions and the Perlis-Thompson Principle, an important idea in # software-architecture. They explain why the Oil language is designed around procs rather than Python- or JavaScript-like functions. More Comments * Comment on a shell injection problem in the original post. I referenced it in a later comment on shell injections. + sed | bash is a bad dangerous pattern. You can pipe data rather than code. + String hygiene was one of the concepts in last month's Summer Blog Backlog: Understanding and Using Shell. * Tip on GNU xargs --show-limits. If your input stream produces too many arguments, xargs will invoke multiple processes, even when -n isn't specified. + However, I've never encountered a case where this matters. That is, it's OK if you need 1 or 2 rm processes, but not 10,000! (for performance) * Discuss This Post on Reddit * Get notified about new posts via @oilshellblog on Twitter * Read Posts Tagged: shell-the-good-parts usage-tips comments string-hygiene oil-language sample-code * Back to the Blog Index