How to prepend a particular text file contents to every text file in a directory and its subdirectories

command linefilesfindtext processing

I hate to clutter my source code files with licensing/attribution comments but some times it is a requirement. So having dozens of source code files (organized in a tree of subdirectories) written I need to add the same multiline comment to the beginning of each of them.

I suspect this is a very easy task to do with standard GNU/Linux command line tools though I am hardly proficient in making any serious use of them, so I beg your pardon and ask for your help.

What I need is to replace every theTargetFile.txt in ./*.txt (including in subdirectories recursively) with something like cat theCommonComment.txt theTargetFile.txt.

I would also prefer to exclude files fitting a particular more specific mask like consider all *.txt but leave *.DontTouch.txt intact.

I think the hardest part of what I actually need is a fancy find-based spell that would run through subdirectories, include *.txt files and exclude *.DontTouch.txt files.

Best Answer

The most straight forward way I can see to do this is with GNU find, bash and the sponge utility from moreutils:

find dir/with/files -name '*.txt' ! -name '*.DontTouch.txt' -print0 |
  while IFS= read -rd '' file; do
    echo 'cat path/to/theCommonComment.txt "$file" | sponge "$file"'
  done

As it stands this will just print the cat/sponge commands without actually doing anything. Once you are sure you have what you want, you can remove the echo and the single quotes surrounding the command.

Without using sponge or the -print0 option for find which may not be available on all systems:

find dir/with/files -name '*.txt' ! -name '*.DontTouch.txt' -exec sh '
  for file; do
    tempfile=$(mktemp)
    cat path/to/theCommonComment.txt "$file" >"$tempfile"
    mv "$tempfile" "$file"
  done
  ' sh {} +

There is no easy way to stop this one simply print what it will do, so be careful. One thing to watch out for - make sure your theCommonComment.txt file is not in the directory you are doing the recursive operation in (or at least make sure that it is excluded from the find), or else you will end up with two of the headers in some files.

Update

A final thought is that you may want to check if the header has already been added to the file. This may be useful if you add new files and have to run the command again. It also gets around the problem of having the theCommonComment.txt file in the search path. The two solutions would become:

comment_file=path/to/theCommonComment.txt
size=$(wc -c "$comment_file")

find dir/with/files -name '*.txt' ! -name '*.DontTouch.txt' -print0 |
  while IFS= read -rd '' file; do
    if [ cmp -n "$size" $comment_file" "$file" ]; do
      echo 'cat "$comment_file" "$file" | sponge "$file"'
    fi
  done
export comment_file=path/to/theCommonComment.txt
export size=$(wc -c "$comment_file")

find dir/with/files -name '*.txt' ! -name '*.DontTouch.txt' -exec sh '
  for file; do
    if [ cmp -n "$size" $comment_file" "$file" ]; do
      tempfile=$(mktemp)
      cat "$comment_file" "$file" >"$tempfile"
      mv "$tempfile" "$file"
    fi
  done
  ' sh {} +
Related Question