6

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.

don_crissti
  • 82,805
Ivan
  • 17,708
  • 1
    I am not sure of what files you have. However, my answer to one similar question seems to be fitting to your requirement. http://unix.stackexchange.com/questions/121469/find-and-replace-recursive/121470#121470 – Ramesh Apr 18 '14 at 17:35
  • The answer you have linked to is about replacing, not adding. My files are not in PHP and don't have any reliable uniform token at their beginnings. – Ivan Apr 18 '14 at 17:39
  • I have added another approach in my answer. You should take a look to it. – Ramesh Apr 18 '14 at 17:40
  • Since you have mentioned only to the beginning of file, the second approach that I had suggested as answer will work. – Ramesh Apr 18 '14 at 17:41
  • How about a whole new approach to the problem? You say that what you want to prepend is licensing information for your end users, correct? How about just adding the licensing bit just before you roll-out your script/program; in a Makefile recipe, maybe? – Joseph R. Apr 18 '14 at 23:12

6 Answers6

8

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 {} +
Graeme
  • 34,027
3

On GNU/anything, to insert _after_ the first line:

find -name '*.txt' ! -name '*thispattern*' ! -name '*thatpattern*' \
  -exec sed -si '1r TheLicense.txt' '{}' +

to insert a file before, simplest is somewhat slower and a little bit kludgier:

find -name '*.txt' ! -name '*thispattern*' ! -name '*thatpattern*' \
  -exec sed -si '1{h;s,.*,cat TheLicense.txt,ep;g}' '{}' +
jthill
  • 2,710
3

With zsh:

zmodload zsh/mapfile
setopt extendedglob # best in ~/.zshrc

for f (Dir/**/(^*DontTouch).txt(N.)) 
  mapfile[$f]=$mapfile[theCommonComment.txt]$mapfile[$f]
1

I would create a file as dummy with the contents that you want to be replaced. So dummy file would look like below.

<?php
 /**
  *
  * Copyright (C) MyCompany, Ltd. - All Rights Reserved
  * Unauthorized copying of this file, via any medium is strictly prohibited
  * Proprietary and Confidential
  * 
  *
  */

After that, I would execute the below script.

for f in ./*; do
sed -i '/<?php/{
    s/<?php//g
    r dummy
}' $f
done

I am replacing the <?php with spaces and replacing it with the contents from the dummy file.

Of course, you can modify the above code to suit your requirement.

However, there is a much more sophisticated way to do it.

for f in *; do 
  echo "whatever" > tmpfile
  cat $f >> tmpfile
  mv tmpfile $f
done

The above answer was taken from here. You are basically putting the contents first into a tmpfile and then adding the original contents to the tmpfile. Now after that is done, you are renaming the tmpfile back to the original file name.

Ramesh
  • 39,297
  • I don't have anything to replace, @Ramesh. What I need is to replace every file with something like cat theCommonComment.txt theTargetFile.txt – Ivan Apr 18 '14 at 17:41
  • I doubt the second solution of yours looks into subdirectories. – Ivan Apr 18 '14 at 17:45
  • You can always replace the for loop to look into all files inside sub directory. – Ramesh Apr 18 '14 at 18:09
  • And this is exactly what I have no idea how to do, @Ramesh. I think what I actually need is a fancy find-based spell that would run through subdirectories, include *.txt files and exclude *.DontTouch.txt files. – Ivan Apr 18 '14 at 18:10
  • You need to check if the file is directory and then if it is a directory you need to go one more level inside. Give me some time, I will update the script. – Ramesh Apr 18 '14 at 18:13
0

This solution uses bash, find,tac and sed.

Copy the following script to a file and make the file executable :chmod +x script

Then use as follow :

./script <DIR> <HEADERFILE>

Where

<DIR> The directory containing files ( or directory containing files..)
<HEADERFILE> The file to add on top of each file

This is the script:

#!/bin/bash

# inset a file content at the beginning of each matching files

DIR=$1
HEADER_FILE=$2

#the matching files will be ignored

IGNORE_FILE="*.DontTouch.txt"

insertHeader(){
   local targetfile=$1
   local headerfile=$2
   echo "$targetfile"
   while read line
    do
#          echo $line
           sed -i "$targetfile" -e "1i$line\ "
    done< <(tac "$headerfile")

}

export -f insertHeader
find "$DIR" -type f -name "*.txt" ! -name "$IGNORE_FILE" -exec bash -c "insertHeader {} $HEADER_FILE"  \;
UnX
  • 841
-1

This solution will take advantage of the find and xargs commands:

find . -name "*.txt" -a ! -name "*.DontTouch.txt" -print | xargs -n 1 cat theCommonComment.txt
KB73
  • 1