Shell script заменить переменные в файле - ошибка с параметром Sed -i для обновления на месте

Вот мой test.env

RABBITMQ_HOST=127.0.0.1
RABBITMQ_PASS=1234

И я хочу использовать test.sh для замены значения в test.env на:

RABBITMQ_HOST=rabbitmq1
RABBITMQ_PASS=12345

вот мой test.sh

#!/bin/bash
echo "hello world"

RABBITMQ_HOST=rabbitmq1
RABBITMQ_PASS=12345
Deploy_path="./config/test.env"

sed -i 's/RABBITMQ_HOST=.*/RABBITMQ_HOST='$RABBITMQ_HOST'/'  $Deploy_path
sed -i 's/RABBITMQ_PASS=.*/RABBITMQ_PASS='$RABBITMQ_HOST'/'  $Deploy_path 

Но у меня есть ошибка

sed: 1: "./config/test.env": invalid command code .
sed: 1: "./config/test.env": invalid command code . 

Как я могу это исправить?

Ответ 1

TL;DR:

С BSD Sed, например, в macOS, вы должны использовать -i '' вместо -i (для не создание файла резервной копии), чтобы ваши команды работали; например:.

sed -i '' 's/RABBITMQ_HOST=.*/RABBITMQ_HOST='"$RABBITMQ_HOST"'/'  "$Deploy_path"

Чтобы ваша команда работала как с GNU, так и с BSD Sed, укажите непустой параметр-аргумент (который создает резервную копию) и прикрепляет его непосредственно к -i

sed -i'.bak' 's/RABBITMQ_HOST=.*/RABBITMQ_HOST='"$RABBITMQ_HOST"'/'  "$Deploy_path" &&
  rm "$Deploy_path.bak" # remove unneeded backup copy

Фоновая информация, (более) переносные решения и уточнение ваших команд можно найти ниже.


Дополнительная справочная информация

Похоже, вы используете BSD/macOS sed, для параметра -i требуется параметр-параметр, который указывает суффикс создаваемого файла резервной копии.
Таким образом, ваш sed script (в соответствии с вашими ожиданиями) интерпретируется как параметр -i option-argument (суффикс резервного копирования), а ваше имя входного файла интерпретируется как script, который явно не работает.

В отличие от вас, ваши команды используют синтаксис GNU sed, где -i может использоваться сам по себе, чтобы указать, что файл резервной копии входного файла не обновляется на месте.

Эквивалентная опция BSD sed - -i '' - обратите внимание на техническую необходимость использования отдельного аргумента для указания аргумента option '', поскольку это пустая строка (если вы использовали -i'', оболочка просто разделил бы '' до того, как sed когда-либо увидит это: -i'' фактически будет таким же, как только -i).

К сожалению, это не будет работать с GNU sed, поскольку он распознает только аргумент option, когда он напрямую привязан к -i, и интерпретирует отдельный '' как отдельный аргумент, а именно как script.

Эта разница в поведении проистекает из принципиально отличающегося дизайнерского решения за реализацией опции -i, и, вероятно, она не исчезнет из соображений обратной совместимости. [1]

Если вы не хотите создавать резервный файл, не существует синтаксиса -i, который работает как для BSD, так и для GNU sed.

Существует четыре основных варианта:

  • (a) Если вы знаете, что используете только GNU или BSD sed, постройте опцию -i соответственно: -i для GNU sed, -i '' для BSD sed.

  • (b) Укажите непустой суффикс как параметр -i option-argument, который, если вы привязываете его непосредственно к опции -i, работает с обеими реализациями; например, -i'.bak'. Хотя это неизменно создает резервный файл с суффиксом .bak, вы можете просто удалить его позже.

  • (c) Определите во время выполнения, с какой реализацией sed вы имеете дело, и постройте опцию -i соответственно.

  • (d) полностью исключить -i (который не совместим с POSIX) и использовать временный файл, который заменяет оригинал на успех: sed '...' "$Deploy_path" > tmp.out && mv tmp.out "$Deploy_path".
    Обратите внимание, что это по существу то, что -i делает за кулисами, что может иметь неожиданные побочные эффекты, особенно входной файл, который является заменой символической ссылки на обычный файл; -i, однако, сохраняет определенные атрибуты исходного файла: см. нижнюю половину этого answer моего.

Здесь a bash реализация (c), которая также упрощает исходный код (одиночный вызов sed с двумя подстановками) и делает его более надежным (переменные двойные кавычки):

#!/bin/bash

RABBITMQ_HOST='rabbitmq1'
RABBITMQ_PASS='12345'
Deploy_path="test.env"

# Construct the Sed-implementation-specific -i option-argument.
# Caveat: The assumption is that if the `sed` is not GNU Sed, it is BSD Sed,
#         but there are Sed implementations that don't support -i at all,
#         because, as Steven Penny points out, -i is not part of POSIX.
suffixArg=()
sed --version 2>/dev/null | grep -q GNU || suffixArg=( '' )

sed -i "${suffixArg[@]}" '
 s/^\(RABBITMQ_HOST\)=.*/\1='"$RABBITMQ_HOST"'/
 s/^\(RABBITMQ_PASS\)=.*/\1='"$RABBITMQ_PASS"'/
' "$Deploy_path"

Обратите внимание, что с определенными значениями, указанными выше для $RABBITMQ_HOST и $RABBITMQ_PASS, можно безопасно связать их непосредственно в sed script, но если значения содержат экземпляры &, /, \ или новые строки, требуется предварительное экранирование, чтобы не нарушить команду sed.
См. этот ответ о том, как выполнить общее предварительное экранирование, но вы можете также рассмотреть другие инструменты в этой точке, такие как awk и perl.


[1] GNU Sed рассматривает параметр-аргумент -i optional, тогда как BSD Sed считает его обязательным, что также отражается в спецификациях синтаксиса. на соответствующих страницах man: GNU Sed: -i[SUFFIX] и BSD Sed -i extension.

Ответ 2

ex -sc '%!awk "\
\$1 == \"RABBITMQ_HOST\" && \$2 = \"rabbitmq1\"\
\$1 == \"RABBITMQ_PASS\" && \$2 = 12345\
" FS== OFS==' -cx file
  • POSIX Sed не поддерживает параметр -i. Однако ex может редактировать файлы на месте

  • Awk - лучший инструмент для этого, поскольку данные разделяются на записи и поля

  • В любом случае Sed или Awk вы можете использовать новую строку или ;, чтобы сделать все в одном вызове

  • У вас есть строки с двойными кавычками без переменных внутри, может также использоваться одиночные кавычки

  • Вы процитировали свое имя файла, когда у него нет символов, требующих экранирования

  • У вас есть несколько бесполезных применений переменных, почти никогда не хорошая идея

Ответ 3

Простой случай

Если test.env содержит только две переменные, вы можете просто создать новый файл или перезаписать существующий:

printf "RABBITMQ_HOST=%s\nRABBITMQ_PASS=%s\n" \
  "${RABBITMQ_HOST}" "${RABBITMQ_PASS}" > "$Deploy_path"

Фиксирование неупорядоченных переменных и оптимизация команд SED

Попробуйте исправить вашу команду следующим образом:

sed -i -e 's/\(RABBITMQ_HOST=\).*/\1'"$RABBITMQ_HOST"'/' \
  -e 's/\(RABBITMQ_PASS=\).*/\1'"$RABBITMQ_PASS"'/' \
  "$Deploy_path"

Вы должны заключить переменные в двойные кавычки, так как в противном случае оболочка будет интерпретировать содержимое. В содержимом в двойных кавычках оболочка будет интерпретировать только $ (заменяя переменную содержимым), backquote и \ (escape). Также обратите внимание на использование нескольких опций -e.

Почему SED плохо для этой задачи (в моем мнении)?

Но, как сказано в ответе @mklement0, -i может не работать в этой форме в BSD-системах. Кроме того, команда изменяет только две переменные, если они определены в файле $Deploy_path, если файл существует. Он не добавит новые переменные в файл. Будьте осторожны, переменные встроены непосредственно в замену, и их значения, как правило, должны быть экранированы в соответствии с правилами SED!

Alternative

Если файл test.env доверен, я рекомендую загрузить переменные, изменить их и распечатать в выходной файл:

(
  # Load variables from test.env
  source test.env

  # Override some variables
  RABBITMQ_HOST=rabbitmq1
  RABBITMQ_PASS=12345

  # Print all variables prefixed with "RABBITMQ_".
  # In POSIX mode, `set` will not output defines and functions
  set -o posix
  set | grep ^RABBITMQ_
) > "$Deploy_path"

Рассмотрите возможность настройки разрешений файловой системы для test.env. Я полагаю, исходный файл является надежным шаблоном.

Решение без SED лучше, на мой взгляд, потому, что реализация SED может отличаться, и опция на месте может работать не так, как ожидалось, на разных платформах.

Но не рискован ли source?

В то время как синтаксический анализ назначений переменных оболочки обычно является легкой задачей, он более рискован, чем просто поиск готового к использованию "script" (test.env). Например, рассмотрите следующую строку в test.env:

declare RABBITMQ_HOST=${MYVAR:=rabbitmq1}

или

export RABBITMQ_HOST=host

Все предлагаемые в настоящее время решения, за исключением кода с использованием source, предположим, что вы назначаете переменную как RABBITMQ_HOST=.... Некоторые из решений даже предполагают, что RABBIT_HOST помещается в начало строки. Ах, вы могли бы исправить регулярное выражение, верно? Только для этого случая...

Таким образом, source является рискованным, поскольку файл, находящийся в источнике, не доверяется. Подумайте о #include <file> в C, или include "file.php" в PHP. Эти инструкции включают источник в текущий источник. Поэтому не слепо рассматривайте поиск файла как анти-шаблон. Все зависит от конкретных обстоятельств. Если ваш test.env входит в состав вашего репозитория, он безопасно вызывает source test.env. Это мое мнение, однако.