Здравствуйте, я пишу сценарий, который занимает 2 символы и заменяет первый вторым, а также сценарий принимает в качестве аргументов файлы. Идея состоит в том, чтобы заменить в файле первый символ вторым, вот сценарий:
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
Проблема в том, что он печатает содержимое правильно, но он не сохраняет изменения в файл, я пытаюсь сохранить их с помощью done <"$ file"
Сначала легкая часть: чтение файлов построчно из оболочки происходит медленно, возможно, вместо этого вы захотите использовать tr
. Подробнее см. man tr
.
Во-вторых, для этого вам понадобится временный файл. Вы не можете читать и записывать файл одновременно (во всяком случае, не из оболочки). Таким образом, вам нужно сделать что-то вроде этого:
tr -- "$1" "$2" <"$file" >"$file".tmp
mv -f -- "$file".tmp "$file"
Очевидная проблема заключается в том, что происходит, если tr
терпит неудачу по какой-либо причине (например, потому что $1
пуст). Затем $file.tmp
все равно будет создан (он создается, когда строка анализируется оболочкой), затем $file
заменяется им.
Таким образом, несколько более безопасный способ сделать это будет выглядеть следующим образом:
tr -- "$1" "$2" <"$file" >"$file".tmp && \
mv -f -- "$file".tmp "$file"
Теперь $file
заменяется только тогда, когда tr
завершается успешно.
Но что, если есть другой файл с именем $file.tmp
? Ну, это будет перезаписано. Здесь все становится сложнее: технически правильный способ сделать это примерно так:
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"
Здесь mktemp
создает временный файл; trap
гарантирует, что этот файл будет очищен, если скрипт выйдет (нормально или аварийно); и cat -- "$tmp" >"$file"
используется вместо mv
, чтобы убедиться, что разрешения $file
сохранены. Это все равно может завершиться ошибкой, если, скажем, у вас нет прав на запись в $file
, но временный файл в конечном итоге будет удален.
Кроме того, GNU sed
имеет возможность редактировать файлы на месте, поэтому вы можете сделать что-то вроде этого:
sed -i "s/$1/$2/g" "$file"
Однако это тоже не так просто, как кажется. Вышеприведенное не удастся, если $1
или $2
имеют особое значение для sed
. Поэтому вам нужно сначала сбежать от них:
in=$( printf '%s\n' "$1" | sed 's:[][\/.^$*]:\\&:g' )
out=$( printf '%s\n' "$2" | sed 's:[\/&]:\\&:g;$!s/$/\\/' )
sed -i "s/$in/$out/" "$file"
Пока игнорируем тот факт, что существуют инструменты, созданные именно для этой цели.
char_src=$1
char_dest=$2
shift
Вы, вероятно, хотите shift 2
здесь, простой shift
переместит $2
в $1
и оставит вас с тем, что там было первое имя файла.
while read -r line
Вы правильно используете read -r
, но обратите внимание, что по умолчанию IFS
read
удалит начальные и конечные пробелы.
done < "$file"
<
перенаправляет только вход, чтобы перенаправить и вывод, вам потребуется > "$file2"
, и, как отмечалось во многих местах ( напримерздесь ), перенаправление на тот же файл просто усекает его перед чтением чего-либо.
Об инструментах, созданных для этого, чтобы изменить одиночные символы на другие одиночные символы, используйте tr
. Конструкция оболочки ${var//pat/repl}
заменяет целые строки и больше похожа на s/pat/repl/g
в sed
.