0

Hello I am writing script which takes 2 symbols and replaces the first with the second and also the script takes as an arguments files .The idea is to replace in the file the first character with the second here is the script:

char_src=$1
char_dest=$2
shift

for file
do
    while read -r line
    do
        line=${line//$char_src/$char_dest}
        echo "$line"
    done < "$file"
done

The problem is that it prints the contents right,but it doesn't save the changes to the file,I try to save them with done < "$file"

ilkkachu
  • 138,973

2 Answers2

2

First the easy part: reading files line by line from the shell is slow, you probably want to use tr instead. See man tr for details.

Second, you need a temporary file for that. You can't read and write a file at the same time (not from the shell anyway). So you need to do something like this:

tr -- "$1" "$2" <"$file" >"$file".tmp
mv -f -- "$file".tmp "$file"

An obvious problem with this is what happens if tr fails, for whatever reason (say because $1 is empty). Then $file.tmp will still get created (it's created when the line is parsed by the shell), then $file gets replaced by it.

So a somewhat safer way to do it would look like this:

tr -- "$1" "$2" <"$file" >"$file".tmp && \
mv -f -- "$file".tmp "$file"

Now $file is replaced only when tr succeeds.

But what if there is another file named $file.tmp around? Well, it will get overwritten. This is where it gets more complicated: the technically correct way to do it is something like this:

tmp="$( mktemp -t "${0##*/}"_"$$"_.XXXXXXXX )" && \
trap 'rm -f "$tmp"' EXIT HUP INT QUIT TERM || exit 1
tr -- "$1" "$2" <"$file" >"$tmp" && \
cat -- "$tmp" >"$file" && \
rm -f -- "$tmp"

Here mktemp creates a temporary file; trap makes sure this file is cleaned up if the script exits (normally or abnormally); and cat -- "$tmp" >"$file" is used instead of mv to make sure permissions of $file are preserved. This can still fail if, say, you don't have write permissions on $file, but the temporary file is removed eventually.

Alternatively, GNU sed has an option to edit files in place, so you could do something like this:

sed -i "s/$1/$2/g" "$file"

However, this is not as simple as it looks either. The above will fail if $1 or $2 have a special meaning for sed. So you need to escape them first:

in=$(  printf '%s\n' "$1" | sed 's:[][\/.^$*]:\\&:g' )
out=$( printf '%s\n' "$2" | sed 's:[\/&]:\\&:g;$!s/$/\\/' )
sed -i "s/$in/$out/" "$file"
Satō Katsura
  • 13,368
  • 2
  • 31
  • 50
0

Ignoring for now the fact that there are tools built for this very purpose.

char_src=$1
char_dest=$2
shift

You probably want shift 2 here, a plain shift would move $2 to $1 and leave you with whatever was there as the first filename.

    while read -r line

You're correctly using read -r, but note that with the default IFS, read will remove leading and trailing whitespace.

    done < "$file"

< only redirects the input, to redirect the output too, you'd need > "$file2", and as noted in numerous places (e.g. here), redirecting to the same file will just truncate it before reading anything.

About the tools made for this, to change single characters to other single characters, use tr. The shell's ${var//pat/repl} construct replaces whole strings, and is more akin to s/pat/repl/g in sed.

ilkkachu
  • 138,973