3

In an effort to learn the shell better, and without always having to resort to xargs, I have been trying to discover any other ways to do:

find . -name *.tcl | xargs -I{} cat {}

xargs make it feel messy and I would like to know if there are multiple way to accomplish this.

EDIT: I did discover that another solution is to use:

find . -name "*.tcl" | cat `cat /dev/stdin`

I don't understand why I have to cat a filename before cat will see it as a file instead of a string though....

5 Answers5

3

You can use exec in find:

find . -name "*.tcl" -exec cat {} \;

Everything between the -exec and trailing \; is the command to run. Like xargs you replace the find results with {}. For each file that gets found cat is run (much like how xargs iterates over the list of files given via STDIN.

Some could argue this is more efficient since you're not using a pipe or starting another application.

  • 3
    How about using one call to cat- find . -name "*.tcl" -exec cat {} + – fd0 Oct 28 '15 at 18:01
  • That's not the same thing as what the original question that was asked. Your method uses all the resulting filenames as an argument to cat. I don't know offhand the length of a command built in this manner but for a lot of files it might exceed that length. My method runs cat repeatedly, but once per file. For cat the results to the user appear to be the same, but there might be differences for other apps. – mkomarinski Oct 28 '15 at 18:06
3

You could also use find in a sub process and feed the output to cat :)

cat $(find . -name "*.tcl")
Rob
  • 818
  • noting that if you have a file named "my file.tcl", the resulting cat command will be: "cat my file.tcl" and will fail... – Jeff Schaller Oct 28 '15 at 19:33
  • Ah yes! those evil files with spaces in their names :) – Rob Oct 28 '15 at 19:39
  • 1
    I really like that solution, I wish I knew of a way to mark more than one response as an answer, this is very helpful. – Josh Barton Oct 28 '15 at 21:47
  • 1
    Note that it's not only spaces, it's all the characters present in $IFS (space tab and newline by default) and all the globbing characters. Do IFS=$'\n'; set -f; cat $(find . -name '*.tcl') to reduce the problem to newline only. – Stéphane Chazelas Oct 28 '15 at 22:05
  • \n is not a problem, after the return from the sub process all those \n do not matter.. Think about it.. find returns one file per line, yet it gets interpreted by the cat command correctly ;). – Rob Oct 29 '15 at 14:15
2

If you use bash, just:

shopt -s globstar # to match files recursively
cat -- **/*.tcl
chaos
  • 48,171
1

You can run a command for each line of the input with a while loop and the shell built-in command read:

find . -name '*.tcl' | while IFS= read -r filename; do cat "$filename"; done
nwk
  • 1,009
1

For simplicity's sake my preference for this would be the clean and readable solution already posed by Mark Komarinski. However, there are lots of ways to do this in Bash. Another interesting way is to avoid cat altogether and take advantage of redirection:

find . -name '*.tcl' -exec sh -c 'printf "%s\n" "$(< {})' \;;

In fact, if you want to accomplish the whole task purely with Bash built-ins then you can combine the use of redirection with the glob solution posed by chaos:

shopt_globstar_temp="$(shopt -p globstar)";
shopt -s globstar;
for filename in **/*.bat; do
    printf "%s" "$(< "${filename}")";
done;
${shopt_globstar_temp};

These are a bit convoluted, but my point here is to illustrate that Bash can do some powerful things with file descriptors and redirection. There are often many solutions to a given problem.


I don't understand why I have to cat a filename before cat will see it as a file instead of a string though....

The output of cat /dev/fd/stdin will be the same as the output of find in your last example, so it will effectively be replaced by <filename1>.tcl <filename2>.tcl ... and cat use that file list as its list of arguments.

If you're wondering why you have to cat stdin in that same example, the reason is that not all programs treat stdin the same way as they treat arguments. If data is transferred to cat through stdin then cat will simply output that same data instead of interpreting it as a filename to be read.

Mike Hill
  • 121