7

I am trying to loop through a directory of text files and combine them into one document. This works great, but the text files contain code snippets, and all of my formatting is getting collapsed to the left. All leading whitespace on a line is stripped.

#!/bin/sh
OUTPUT="../best_practices.textile"
FILES="../best-practices/*.textile"
for f in "$FILES"
do
  echo "Processing $f file..."
  echo "">$OUTPUT

  cat $f | while read line; do 
      echo "$line">>$OUTPUT
  done
  echo >>$OUTPUT
  echo >>$OUTPUT
done

I am admittedly a bash noob, but after searching high and low I couldn't find a proper solution. Apparently BASH hates the leading white space in general.

Joel Hooks
  • 6,465
  • 4
  • 33
  • 39

5 Answers5

41

As others have pointed out, using cat or awk instead of a read-echo loop is a much better way to do this -- avoids the whitespace-trimming problem (and a couple of others you haven't stumbled upon), runs faster, and at least with cat, is simply cleaner code. Nonetheless, I'd like to take a stab at getting the read-echo loop to work right.

First, the whitespace-trimming problem: the read command automatically trims leading and trailing whitespace; this can be fixed by changing its definition of whitespace by setting the IFS variable to blank. Also, read assumes that a backslash at the end of the line means the next line is a continuation, and should be spliced together with this one; to fix this, use its -r (raw) flag. The third problem here is that many implementations of echo interpret escape sequences in the string (e.g. they may turn \n into an actual newline); to fix this, use printf instead. Finally, just as a general scripting hygiene rule, you shouldn't use cat when you don't actually need to; use input redirection instead. With those changes, the inner loop looks like this:

while IFS='' read -r line; do 
  printf "%s\n" "$line">>$OUTPUT
done <$f

...there are also a couple of other problems with the surrounding script: the line that tries to define FILES as the list of available .textile files has quotes around it, meaning it never gets expanded into an actual list of files. The best way to do this is to use an array:

FILES=(../best-practices/*.textile)
...
for f in "${FILES[@]}"

(and all occurrences of $f should be in double-quotes in case any of the filenames have spaces or other funny characters in them -- should really do this with $OUTPUT as well, though since that's defined in the script it's actually safe to leave off.)

Finally, there's a echo "">$OUTPUT near the top of the loop-over-files that's going to erase the output file every time through (i.e. at the end, it only contains the last .textile file); this needs to be moved to before the loop. I'm not sure if the intent here was to put a single blank line at the beginning of the file, or three blank lines between files (and one at the beginning and two at the end), so I'm not sure exactly what the appropriate replacement is. Anyway, here's what I can up with after fixing all of these problems:

#!/bin/sh
OUTPUT="../best_practices.textile"
FILES=(../best-practices/*.textile)

: >"$OUTPUT"
for f in "${FILES[@]}"
do
  echo "Processing $f file..."
  echo >>"$OUTPUT"

  while IFS='' read -r line; do 
    printf "%s\n" "$line">>"$OUTPUT"
  done <"$f"

  echo >>"$OUTPUT"
  echo >>"$OUTPUT"
done
Gordon Davisson
  • 118,432
  • 16
  • 123
  • 151
  • Thank you for putting the time into this Gordon, it was extremely informative. – Joel Hooks Oct 30 '09 at 23:23
  • Best answer here ! Thanks a lot for having taken the time to explain so clearly. :) – Arenzel Jan 31 '14 at 14:19
  • Am I right in thinking that `IFS='' read -r line` only affects the internal field separator used by the `read` command and not anything inside or after the while loop? I've seen `ORIG_IFS="$IFS"; ...; IFS="$ORIG_IFS"`, but this isn't necessary in this case, is it? – davidchambers Mar 21 '14 at 16:47
  • @davidchambers Correct, since the assignment is used as a prefix to the `read` command, it only applies to that one command. – Gordon Davisson Mar 21 '14 at 18:42
4

Instead of:

cat $f | while read line; do 
    echo "$line">>$OUTPUT
done

Do this:

cat $f >>$OUTPUT

(If there's a reason you need to do things line by line it'd be good to include that in the question.)

Willi Mentzel
  • 27,862
  • 20
  • 113
  • 121
Laurence Gonsalves
  • 137,896
  • 35
  • 246
  • 299
  • That kills the whitespace too. I switched to line by line to see if maybe it would give me more options for saving the leading space. – Joel Hooks Oct 30 '09 at 05:01
  • 1
    Interesting. This answer has been downvoted twice with no explanation. If you're going to downvote, say why. (And if it's because you're thinking "this could be just one cat command": 1. not really, note the extra blank lines inserted between files and 2. I'm assuming (perhaps incorrectly) that this was a pared down script for the sake of simplicity and the real version may have some extra per-file logic.) – Laurence Gonsalves Oct 30 '09 at 06:55
  • 1
    I am sure there's some way for us to view who down-voted. If not, i don't see why there can't be something to make it compulsory to put a comment when down-voting. – ghostdog74 Oct 30 '09 at 07:13
  • 2
    @ehime please elaborate. – Laurence Gonsalves Mar 12 '15 at 03:20
4

that's an overly expensive way of combining files.

cat ../best-practices/*.textile >  ../best_practices.textile

if you want to add a blank( newline) to each file as you concatenate, use awk

awk 'FNR==1{print "">"out.txt"}{print > "out.txt" }' *.textile

OR

awk 'FNR==1{print ""}{print}' file* > out.txt
ghostdog74
  • 327,991
  • 56
  • 259
  • 343
1

This allows you to intersperse newlines between each input file as you have done in your original script:

for f in $FILES; do echo -ne '\n\n' | cat "$f" -; done > $OUTPUT

Note that $FILES is unquoted for this to work (otherwise the extra newlines appear only once at the end of all the output), but $f must be quoted to protect spaces in filenames, if they exist.

Dennis Williamson
  • 346,391
  • 90
  • 374
  • 439
0

The correct answer, imo, is this, reproduced below:

while IFS= read line; do
    check=${line:0:1}
done < file.txt

Note that it'll take care of situations where the input is piped from another command, and not just from an actual file.

Note that you can also simplify the redirection as shown below.

#!/bin/bash
OUTPUT="../best_practices.textile"
FILES="../best-practices/*.textile"
for f in "$FILES"
do
  echo "Processing $f file..."
  {
  echo

  while IFS= read line; do 
      echo "$line"
  done < $f
  echo
  echo;
  } > $OUTPUT
done
Community
  • 1
  • 1
akhan
  • 2,952
  • 2
  • 22
  • 13