7

The challenge of my task is that the file.txt can be in subfolder or in sub-sub folders. The general structure looks like this:

   folder
     |___subfolder
             |_____file.txt
     |
     |___subfolder
             |____subfolder
                      |_______file.txt
     etc

My previous code does not deal with the sub-sub folder and I am not sure how to add in this part (since some sub folder does not have any subsequent folders)

 for dir in ./*/ ; do
        if [ -d "$dir" ]; then 
           cd $d;
           for subdir in ./*/; do
               cd $subdir && $(sed command file.txt) && cd ..;
           done
           cd ..
        fi
done

I have also learned there's a one line method of using find, something like:

find . -name 'file.txt' | sed command file.txt

Apparently this one doesn't work as find only returns the dir to my file.txt

1

3 Answers 3

18

Try:

find . -name 'file.txt' -exec sed command {} +

This finds all files named file.txt that in subdirectories of . and runs sed command against those files.

If you want sed to modify those files in place, then add the -i option.

Althought -exec ... + is now required by POSIX (hat tip: jordanm), some people may be using old versions of BSD find that do not support +. If you have one of those, then use (hat tip: unxnut):

find . -name 'file.txt' -exec sed command {} \;

More Secure Alternative

If rapid changes are made to the directory structure, -exec can be subject to a race-condition. For greater security, use -execdir (hat tip: unxnut):

find . -name 'file.txt' -execdir sed command {} +

Note that, if you have the current directory in your PATH, then -execdir will refuse to run.

11
  • @unxnut What was the reason for replacing -exec sed command {} + with the much less efficient -exec sed command {} \;?
    – John1024
    Commented Jul 25, 2016 at 3:10
  • Why is + more efficient than \;? Isn't \; more portable?
    – unxnut
    Commented Jul 25, 2016 at 3:13
  • 1
    I checked again and you are right. However, a better option for exec is execdir, preventing some race conditions.
    – unxnut
    Commented Jul 25, 2016 at 3:34
  • 1
    @unxnut + is part of the POSIX standard
    – jordanm
    Commented Jul 25, 2016 at 3:54
  • 1
    I would suggest to use find -print0|xargs -0 sed 'some sed stuff' -i, as find -exec spawns one process per file, while xargs uses one sed for all files, which is much faster if you have a lot of files.
    – allo
    Commented Jul 25, 2016 at 14:22
3

Unless you know what that sed command is doing you want to run one sed per input file.
You can do this with either a for loop (if your shell supports recursive globbing) e.g. zsh, ksh93, yash, bash (tcsh and fish as well, but the loop syntax is different there).

shopt -s globstar        # bash
#set -o globstar         # ksh93
#set -o extended-glob    # yash

for f in **/file.txt; do [ -f "$f" ] && sed 'cmd' "$f"; done

or (as pointed out by other people) with find:

find . -type f -name 'file.txt' -exec sed 'cmd' {} \;

Now, one may wonder - why not "optimize" this and use finds -exec with + or a zsh-ism like:

sed 'cmd' **/file.txt(.)

Sure, these would be more efficient as they invoke sed with multiple file operands but that may well the problem:
for the sake of simplicity, imagine you're trying to print only the 1st line of each file using the sed command 1q1 (without editing the file in-place2, maybe to redirect the combined output to another file or just to inspect it) so you would run either

find . -name 'file.txt' -exec sed '1q' {} +

or

sed '1q' **/file.txt(.)

however they will both fail to deliver because
if multiple file operands are specified, the named files shall be read in the order specified and the concatenation shall be edited
and as a result sed will only print the 1st line of input not the 1st line of each file3.
Now, gnu sed has the -s switch:

-s, --separate
        consider files as separate rather than as a single continuous long stream

that might come in handy sometimes but in this particular case (1q), it won't help.


1: if you don't like 1q try $d which should delete the last line from each file
2: you might get away with gnu sed sometimes as -i implies -s, but definitely not when one of the sed commands is q
3: you might argue that this is because q quits - but the same would happen if you used sed -n '1p' - I only used q to emphasize 2:

2
  • 1
    Note a difference between **/file and find: the former will not descend into hidden directories. Commented Jul 25, 2016 at 12:38
  • 1
    Note a difference between **/file; [[ -f ]] and find -type f/**/file(.): the former will include symlinks to directories. Also note that with bash prior to 4.3, ** followed symlinks when descending the directories. Commented Jul 25, 2016 at 12:39
-1

Assuming your file paths don't contain blanks, newlines, single quote, double quote or backslash characters:

find . -name 'file.txt' -type f | xargs sed command
3
  • @Rahul why the downvote?
    – Oberluz
    Commented Jul 25, 2016 at 10:21
  • I didn't downvote, but a couple of reasons the downvoter might have thought: 1. no need to use pipe-xargs, as find has the -exec or -execdir actions and 2. if you use pipe-xargs, at least do it in a way that won't fail with files with spaces or newlines in its name: find ... -print0 | xargs -0 sed ... (I don't know about the portability of this). Commented Jul 25, 2016 at 10:42
  • Given my reputation is too low to comment on the solution proposed by @John1024 I'll do it here, shouldn't that solution include the -type f to avoid processing any directories with the name 'file.txt', which was the main reason for my answer? the use of xargs was just to show another way...
    – Oberluz
    Commented Jul 25, 2016 at 10:54

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .