Замена символов в файле

Здравствуйте, я пишу сценарий, который занимает 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"

0
04.06.2017, 17:11
2 ответа

Сначала легкая часть: чтение файлов построчно из оболочки происходит медленно, возможно, вместо этого вы захотите использовать 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"
2
28.01.2020, 02:45

Пока игнорируем тот факт, что существуют инструменты, созданные именно для этой цели.

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.

0
28.01.2020, 02:45

Теги

Похожие вопросы