Команда sed -i для редактирования на месте для работы как с GNU sed, так и с BSD/OSX

У меня есть make-файл (разработанный для gmake в Linux), который я пытаюсь портировать на MacOS, но похоже, что sed не хочет сотрудничать. Что я делаю, так это использую GCC для автоматического создания файлов зависимостей, а затем немного настраиваю их с помощью sed. Соответствующая часть makefile:

$(OBJ_DIR)/%.d: $(SRC_DIR)/%.cpp
  $(CPPC) -MM -MD $< -o $@
  sed -i 's|\(.*\)\.o:|$(OBJ_DIR)/\1.o $(OBJ_DIR)/\1.d $(TEST_OBJ_DIR)/\1_utest.o:|' $@

Хотя это работает без проблем в GNU/Linux, я получаю сообщения об ошибках, подобные следующим, при попытке сборки в MacOS:

sed: 1: "test/obj/equipmentConta ...": undefined label 'est/obj/equipmentContainer_utest.d'
sed: 1: "test/obj/dice_utest.d": undefined label 'est/obj/dice_utest.d'
sed: 1: "test/obj/color-string_u ...": undefined label 'est/obj/color-string_utest.d'

Вроде бы sed отрубает персонажа, но решения не вижу.


person Chris Tonkinson    schedule 23.02.2010    source источник
comment
Возможно, проект BashX может помочь вам с подобными проблемами.   -  person Eduardo Cuomo    schedule 23.04.2021


Ответы (7)


OS X sed обрабатывает аргумент -i отличается от версии Linux.

Вы можете сгенерировать команду, которая может «работать» для обоих, добавив -e таким образом:

#      vv
sed -i -e 's|\(.*\)\.o:|$(OBJ_DIR)/\1.o $(OBJ_DIR)/\1.d $(TEST_OBJ_DIR)/\1_utest.o:|' $@

OS X sed -i интерпретирует следующую вещь после -i как расширение файла для резервной копии редактирования на месте. (Версия для Linux делает это только в том случае, если между -i и расширением нет пробела.) Очевидным побочным эффектом использования этого является то, что вы получите файл резервной копии с -e в качестве расширения, что вам может не понадобиться. Пожалуйста, обратитесь к другим ответам на этот вопрос для получения более подробной информации и более чистых подходов, которые можно использовать вместо этого.

Поведение, которое вы видите, связано с тем, что OS X sed использует s||| как расширение (!), а затем интерпретирует аргумент next как команду — в этом случае он начинается с t, который sed распознает как переход к Команда -label ожидает целевую метку в качестве аргумента - отсюда и ошибка, которую вы видите.

Если вы создадите файл test, вы можете воспроизвести ошибку:

$ sed -i 's|x|y|' test
sed: 1: "test": undefined label 'est'
person martin clayton    schedule 23.02.2010
comment
Я думаю, что если в среде определен POSIXLY_CORRECT или (более старый) POSIX_ME_HARDER, можно ожидать такого же поведения. - person Tim Post♦; 24.02.2010
comment
Правильным решением для OSX является наличие пустого аргумента для -i, но опять же, это несовместимо с Linux sed. )-: Может быть, вместо этого использовать Perl? perl -pi -e 's|x|y|g' file - person tripleee; 18.01.2014
comment
@TimPost: -i не является POSIX-совместимым вариантом, поэтому эти переменные среды не применяются. - person mklement0; 03.07.2017

Собственно, делая

sed -i -e "s/blah/blah/" files

не делает то, что вы ожидаете в MacOS. Вместо этого он создает файлы резервных копий с расширением -e.

Правильная команда для MacOS:

sed -i "" -e "s/blah/blah/" files

В Linux удалите пробел между -i и "" (см. связанный ответ)

sed -i"" -e "s/blah/blah/" files
person Urkle    schedule 10.03.2010
comment
Верно, но ваш ответ не работает для Linux, что является основной проблемой: он должен работать для обоих. Используя -i -e, можно удалить файлы с расширением -e. - person Murphy; 29.02.2012
comment
Я полагаю, что для щедрых определений «работы» это так. Это умный хак, но он создает дополнительные файлы. Ответ Уркла - это то, что я искал. - person brandon; 19.08.2012
comment
ВНИМАНИЕ если вы попытаетесь найти все файлы с расширением -e и удалить их с помощью чего-то вроде find . -type f | grep "-e$" | xargs rm, вы удалите все свои файлы. Нужно использовать "\-e" и даже затем запускать его без канала до xargs rm. - person James Robinson; 18.08.2013
comment
@JamesRobinson, просто используй find . -name "*-e" -delete. Всегда сначала запускайте без флага -delete, просто чтобы подтвердить, что вы собираетесь удалить. - person Martin Konecny; 17.09.2013
comment
Похоже, что пробел в -i "" необходимо удалить, чтобы он работал и в Linux: см. stackoverflow.com/a/14813278/15690< /а> - person blueyed; 26.09.2013
comment
Это решение работает в OS X, но не в Linux из-за пробела после -i. - person Jason D; 29.04.2014

Принятый в настоящее время ответ ошибочен по двум очень важным причинам.

  1. В BSD sed (версия для OSX) параметр -e интерпретируется как расширение файла и поэтому создает файл резервной копии с расширением -e.

  2. Предлагаемое тестирование ядра darwin не является надежным подходом к кросс-платформенному решению, поскольку GNU или BSD sed могут присутствовать в любом количестве систем.

Гораздо более надежным тестом было бы просто проверить параметр --version, который можно найти только в версии GNU sed.

sed --version >/dev/null 2>&1

Как только правильная версия sed определена, мы можем выполнить команду в ее правильном синтаксисе.

Синтаксис GNU sed для опции -i:

sed -i -- "$@"

Синтаксис BSD sed для параметра -i:

sed -i "" "$@"

Наконец, объедините все это в кросс-платформенной функции, чтобы выполнить команду редактирования sed на месте:

sedi () {
    sed --version >/dev/null 2>&1 && sed -i -- "$@" || sed -i "" "$@"
}

Пример использования:

sedi 's/old/new/g' 'some_file.txt'

Это решение было протестировано на OSX, Ubuntu, Freebsd, Cygwin, CentOS, Red Hat Enterprise и Msys.

person Arctelix    schedule 26.07.2016
comment
Если вы хотите объединить это с find, см. эту тему - person Alex Gyoshev; 02.03.2018
comment
отличная работа на mac el capitan, а также на рабочем столе и сервере ubuntu 17 - person stackdave; 18.03.2018
comment
Не работает с sed (GNU sed) 4.7 в образе докера Ubuntu 20.04.1. Удаление -- работает. - person ypresto; 07.12.2020

полезный ответ Мартина Клейтона дает хорошее объяснение проблемы[1], но решение, которое - как он утверждает - имеет потенциально нежелательный побочный эффект.

Вот решения без побочных эффектов:

Предостережение. Одного решения проблемы с синтаксисом -i, как показано ниже, может быть недостаточно, поскольку между GNU sed и BSD/macOS sed существует множество других различий (подробное обсуждение см. в этот мой ответ).


Обходной путь с -i: создайте файл резервной копии временно, а затем очистите его:

С непустым суффиксом (расширение имени файла резервной копии) и параметром-аргументом (значение, которое является не пустой строкой), вы можете используйте -i таким образом, который работает как с BSD/macOS sed, так и с GNU sed, непосредственно добавляя суффикс к параметру -i.

Это можно использовать для создания файла резервной копии временно, который можно сразу очистить:

sed -i.bak 's/foo/bar/' file && rm file.bak

Очевидно, что если вы хотите сохранить резервную копию, просто опустите часть && rm file.bak.


Обходной путь, совместимый с POSIX, с использованием временного файла и mv:

Если нужно редактировать только отдельный файл, параметр -i можно обойти во избежание несовместимости.

Если вы ограничиваете свой сценарий sed и другие параметры функциями, совместимыми с POSIX, ниже приведено полностью переносимое решение (обратите внимание, что -i не POSIX-совместимое).

sed 's/foo/bar' file > /tmp/file.$$ && mv /tmp/file.$$ file
  • Эта команда просто записывает изменения во временный файл и, если команда sed выполнена успешно (&&), заменяет исходный файл временным.

    • If you do want to keep the original file as a backup, add another mv command that renames the original first.
  • Предупреждение: по сути, это то же самое, что и -i, за исключением того, что он пытается сохранить разрешения и расширенные атрибуты (macOS) исходного файла; однако, если исходный файл является символической ссылкой, и это решение, и -i заменят символическую ссылку обычным файлом.
    См. нижнюю половину < href="http://stackoverflow.com/a/30066428/45375">этот мой ответ содержит подробную информацию о том, как работает -i.


[1] Для более подробного объяснения см. этот мой ответ.

person mklement0    schedule 03.07.2017
comment
«Обходной путь» здесь, безусловно, лучшее решение (самое простое/самое легкое/самое удобное). Этот вопрос касается немного другой проблемы, но включает в себя хорошее обсуждение той же проблемы -i. - person Norman Gray; 03.07.2017
comment
Спасибо, @NormanGray. В рамках заданных ограничений (только 1 входной файл, без сохранения разрешений, даты создания, расширенных атрибутов) это правда. - person mklement0; 03.07.2017
comment
Обходной путь 2, безусловно, самый простой. Это также и самое безопасное — редактирование на месте может быть неудачным, и вы потеряете оригинал. - person Jonathan Leffler; 07.07.2017
comment
@JonathanLeffler: я согласен с тем, что решение sed -i.bak ... file && rm file.bak является самым простым и наилучшим образом отвечает на исходный вопрос, поэтому я переместил его, чтобы оно отображалось как 1-й обходной путь (и я удалил нумерацию, чтобы избежать путаницы). Однако с обходным решением, совместимым с POSIX, учитывая использование && для замены оригинала только в том случае, если sed сообщает об успехе, какие проблемы вы видите? Я предполагаю, что отсутствие прав на замену оригинала может привести к сбою mv, что было бы неприятно, но вы не потеряли бы оригинал. - person mklement0; 07.07.2017
comment
Согласен, что POSIX-совместимое решение тоже работает хорошо — и работает на таких платформах, как Solaris, AIX или HP-UX, где локальный sed (вероятно) вообще не поддерживает -i. Таким образом, из решений, фактически использующих -i, решение sed -i.bak …"$file" && rm -f "$file.bak" является лучшим, и решение, вообще не использующее -i, также имеет свои достоинства. (Все решения становятся проблематичными, если исходный файл сам является символической ссылкой или имеет несколько ссылок. И разрешения/владение могут быть проблемой при перемещении/удалении файлов, но это, вероятно, относится и к решениям, использующим -i.) - person Jonathan Leffler; 07.07.2017

Это не совсем ответ на вопрос, но можно получить поведение, эквивалентное Linux, через

brew install gnu-sed

# Add to .bashrc / .zshrc
export PATH="/usr/local/opt/gnu-sed/libexec/gnubin:$PATH"

(ранее была опция --with-default-names для brew install gnu-sed, но недавно она была удалена)

person Anthony Sottile    schedule 22.12.2014
comment
Это определенно самое быстрое решение для меня. - person frhd; 27.12.2014
comment
brew install gnu-sed установит sed как gsed, поэтому вам не нужно думать о предыдущих сценариях, написанных с помощью sed OSX. - person Dorian; 26.01.2016
comment
Я пробовал с обновленным --with-default-names, заметил это только после открытия окна нового термина, а также с sed --version и т. д. - person Rene Marcelo; 08.07.2016
comment
Возможно hash -r после установки поможет :) - person Anthony Sottile; 09.07.2016
comment
Это неработоспособное решение, если оно предназначено для широких масс. Программисты могут быть в порядке с необходимостью устанавливать программное обеспечение, чтобы все заработало, но обычные пользователи этого не сделают. Поскольку контекст вопроса - это программирование и make-файлы (сломанные make-файлы IMO, но это другое обсуждение), это потенциально решение. - person Jonathan Leffler; 07.07.2017
comment
@JonathanLeffler разумная точка зрения, хотя это сайт для программистов, и я сомневаюсь, что непрограммисты будут иметь дело с sed - person Anthony Sottile; 07.07.2017

Я тоже столкнулся с этой проблемой и подумал о следующем решении:

darwin=false;
case "`uname`" in
  Darwin*) darwin=true ;;
esac

if $darwin; then
  sedi="/usr/bin/sed -i ''"
else
  sedi="sed -i"
fi

$sedi 's/foo/bar/' /home/foobar/bar

У меня работает ;-), YMMV

Я работаю в команде с несколькими ОС, где ppl собирается на Windows, Linux и OS X. Некоторые пользователи OS X жаловались, потому что получили другую ошибку — у них был установлен порт GNU sed, поэтому мне пришлось указать полный путь.

person thecarpy    schedule 06.01.2014
comment
Ваше решение действительно кроссплатформенное. Спасибо. - person Jason D; 29.04.2014
comment
Это создает для меня файл резервной копии bar. Но не при написании команды непосредственно в терминале. Не могу понять, почему. - person Arlukin; 04.12.2014
comment
Пришлось использовать eval. eval $sedi 's/foo/bar/' /home/foobar/bar. Это было проверено только на OS X. - person Arlukin; 04.12.2014
comment
Взгляните на мой ответ ниже, я решил проблему с генерацией лучше, чем решение eval. - person niieani; 02.03.2016

Я исправил решение, опубликованное @thecarpy:

Вот правильное кроссплатформенное решение для sed -i:

sedi() {
  case $(uname) in
    Darwin*) sedi=('-i' '') ;;
    *) sedi='-i' ;;
  esac

  LC_ALL=C sed "${sedi[@]}" "$@"
}
person niieani    schedule 02.03.2016
comment
@masimplo почти такой же, как sed -i в Linux, большинство функций поддерживается, вам придется покопаться в руководствах, чтобы увидеть, в чем они отличаются - person niieani; 03.12.2016
comment
Многообещающе, но нет причин использовать LC_ALL=C, если вам явно не нужно игнорировать кодировку символов текущей локали и рассматривать каждый байт как символ. - person mklement0; 03.07.2017
comment
И хотя использование массива bash для передачи динамически сконструированных аргументов, как и вы, обычно более надежно, на самом деле в ответе @thecarpy нет ничего плохого (за исключением того, что было бы лучше проверить, какова реализация двоичного файла sed, а не проверять < i>хост-платформа). - person mklement0; 03.07.2017