[Перевод] Подводные камни Bash

e9b20363c99b43628a2913f59071b5bd.jpg

В этой статье мы поговорим об ошибках, совершаемых программистами на Bash. Во всех приведённых примерах есть какие-то изъяны. Вам удастся избежать многих из нижеописанных ошибок, если вы всегда будете использовать кавычки и никогда не будете использовать разбиение на слова (wordsplitting)! Разбиение на слова — это ущербная легаси-практика, унаследованная из оболочки Bourne. Она применяется по умолчанию, если вы не заключаете подстановки (expansions) в кавычки. В общем, подавляющее большинство подводных камней так или иначе связаны с подстановкой без кавычек, что приводит к разбиению на слова и глоббингу (globbing) получившегося результата.

Содержание
  1. for i in $(ls *.mp3)
  2. cp $file $target
  3. Имена файлов с предшествующими дефисами
  4. [ $foo = «bar» ]
  5. cd $(dirname »$f»)
  6. [ »$foo» = bar && »$bar» = foo ]
  7. [[ $foo > 7 ]]
  8. grep foo bar | while read -r; do ((count++)); done
  9. if [grep foo myfile]
  10. if [bar=»$foo»]; then …
  11. if [ [ a = b ] && [ c = d ] ]; then …
  12. read $foo
  13. cat file | sed s/foo/bar/ > file
  14. echo $foo
  15. $foo=bar
  16. foo = bar
  17. echo <
  18. su -c 'some command'
  19. cd /foo; bar
  20. [ bar == »$foo» ]
  21. for i in {1…10}; do ./something &; done
  22. cmd1 && cmd2 || cmd3
  23. echo «Hello World!»
  24. for arg in $*
  25. function foo ()
  26. echo »~»
  27. local varname=$(command)
  28. export foo=~/bar
  29. sed 's/$foo/good bye/'
  30. tr [A-Z] [a-z]
  31. ps ax | grep gedit
  32. printf »$foo»
  33. for i in {1…$n}
  34. if [[ $foo = $bar ]] (в зависимости от цели)
  35. if [[ $foo =~ 'some RE' ]]
  36. [ -n $foo ] or [ -z $foo ]
  37. [[ -e »$broken_symlink» ]] возвращает 1, несмотря на существование $broken_symlink
  38. Сбой ed file <<<«g/d\{0,3\}/s//e/g»
  39. Сбой подцепочки (sub-string) expr для «match»
  40. Про UTF-8 и отметках последовательности байтов (Byte-Order Marks, BOM)
  41. content=$(
  42. for file in ./*; do if [[ $file!= *.* ]]
  43. somecmd 2>&1 >>logfile
  44. cmd; ((! $?)) || die
  45. y=$((array[$x]))
  46. read num; echo $((num+1))
  47. IFS=, read -ra fields <<< "$csv_line"
  48. export CDPATH=.:~/myProject


1. for i in $(ls *.mp3)
Одна из самых распространённых ошибок, совершаемых BASH-программистами. Выражается она в написании подобных циклов:
for i in $(ls *.mp3); do    # Неправильно!
    some command $i         # Неправильно!
done

for i in $(ls)              # Неправильно!
for i in `ls`               # Неправильно!

for i in $(find . -type f)  # Неправильно!
for i in `find . -type f`   # Неправильно!

files=($(find . -type f))   # Неправильно!
for i in ${files[@]}        # Неправильно!

Да, было бы замечательно, если бы вы могли обрабатывать выходные данные ls или find в виде списка имён файлов и итерировать его. Но вы не можете. Этот подход целиком ошибочен, и этого никак не исправить. Нужно подходить к этому совершенно иначе.

Тут есть как минимум пять проблем:

  1. Если имя файла содержит пробелы, то оно подвергается WordSplitting. Допустим, в текущей папке у нас есть файл с именем 01 - Don't Eat the Yellow Snow.mp3. Цикл for итерирует каждое слово и выдаст результат: 01, -, Don’t, Eat, etc.
  2. Если имя файла содержит символы glob, то оно подвергается глоббингу («globbing»). Если выходные данные ls содержат символ *, то слово, в которое он входит, будет расценено как шаблон и заменено списком всех имён файлов, которые ему соответствуют. Путь к файлу может содержать любые символы, за исключением NUL. Да, в том числе и символы перевода строки.
  3. Утилита ls может искромсать имена файлов. В зависимости от платформы, на которой выработаете, от используемых вами аргументов (или не используемых), а также в зависимости от того, указывают ли на терминал стандартные выходные данные, ls может внезапно заменить какие-то символы в имени файла на »?». Или вообще их не выводить. Никогда не пытайтесь парсить выходные данные ls.
  4. CommandSubstitution обрезает из выходных данных все конечные символы переноса строки. На первый взгляд, это хорошо, потому что ls добавляет новую строку. Но если последнее имя файла в списке заканчивается новой строкой, то `…` или $() уберут и его в придачу.

Также нельзя заключать подстановку в двойные кавычки:
for i in "$(ls *.mp3)"; do # Неправильно!

Это приведёт к тому, что выходные данные ls целиком будут считаться одним словом. Вместо итерирования каждого имени файла, цикл будет выполнен один раз, присвоив i строковое значение из объединённых имён файлов. И вы не можете просто изменить IFS на новую строку. Имена файлов тоже могут содержать новые строки.

Другая вариация на эту тему заключается в злоупотреблении разбиением на слова и циклами for для (неправильного) чтения строк файла. Например:

IFS=$'\n'
for line in $(cat file); do ...     # Неправильно!

Это не работает! Особенно если строки являются именами файлов. Bash (как и любая другая оболочка семейства Bourne) просто не работает таким образом. В добавление ко всему сказанном, совершенно не нужно использовать саму ls. Это внешняя команда, выходные данные которой специально предназначены для чтения человеком, а не для парсинга скриптом.

Так как же делать правильно?

Используете find, например, в совокупности с -exec:

find . -type f -exec some command {} \;

Вместо ls можно рассмотреть такой вариант:
for i in *.mp3; do    # Уже лучше! и...
    some command "$i" # ...всегда заключайте в двойные кавычки!
done

Оболочки POSIX, как и Bash, специально для этого имеют свойство globbing — это позволяет им применять шаблоны к списку сопоставляемых имён файлов. Не нужно интерпретировать результаты работы внешней утилиты. Поскольку globbing — последний этап процедуры подстановки, то шаблон *.mp3 корректно применяется к отдельным словам, на которые не оказывает эффекта подстановка без кавычек. Если вам нужно рекурсивно обработать файлы, то воспользуйтесь UsingFind или присмотритесь к shopt -s globstar в Bash 4 и выше.

Вопрос: Что случится, если в текущей папке нет файлов, удовлетворяющих шаблону *.mp3? Цикл for будет выполнен один раз с i="*.mp3", что не является ожидаемым поведением! В качестве решения этой проблемы можно применять проверку на наличие подходящего файла:

# POSIX
for i in *.mp3; do
    [ -e "$i" ] || continue
    some command "$i"
done

Другое решение — использовать свойство Bash’а shopt -s nullglob. Хотя так можно делать только после прочтения документации и внимательной оценки эффекта этой настройки на все остальные glob«ы в этом скрипте. Обратите внимание на кавычки вокруг $i в теле цикла. Это приводит нас ко второй проблеме:.2. cp $file $target
Что плохого в этой команде? В принципе, ничего, если вы заранее знаете, что $file и $target не содержат пробелов или подстановочных символов (wildcards). Однако результаты подстановки всё равно подвергаются WordSplitting и подстановке пути к файлу. Поэтому всегда заключайте параметрические подстановки (parameter expansions) в двойные кавычки.
cp -- "$file" "$target"

В противном случае вы получите такую команду cp 01 - Don't Eat the Yellow Snow.mp3 /mnt/usb, что приведёт к ошибкам наподобие cp: cannot stat `01': No such file or directory. Если $file содержит символы подстановки (* или ? или [), то они будут разложены, только если есть удовлетворяющие условиям файлы. С двойными кавычками всё будет хорошо, пока в начале »$file» не окажется символа »-». В этом случае cp решит, что вы пытаетесь скормить ему опции командной строки (см. следующую главу).

Даже в несколько необычных обстоятельствах, когда вы можете гарантировать содержимое переменной, заключать в кавычки подстановки параметров — это хорошая и общепринятая практика, особенно если в них содержатся имена файлов. Опытные авторы скриптов всегда будут использовать кавычки, за исключением редких случаев, когда из контекста кода абсолютно очевидно, что параметр содержит гарантированно безопасное значение. Эксперты наверняка решат, что использование в заголовке команды cp является ошибкой. Вам тоже следует так считать.

3. Имена файлов с предшествующими дефисами
Имена файлов с предшествующими дефисами могут доставить немало проблем. Glob«ы наподобие *.mp3 отсортированы в расширенный список (expanded list) (согласно вашей текущей локали), а в большинстве локалей сначала сортируется дефис, а потом буквы. Затем список передаётся какой-то команде, которая может некорректно интерпретировать -filename в качестве опции. У этой ситуации есть два основных решения.

Первое — вставить два дефиса (--) между командой (например cp) и её аргументами. Это будет сигналом прекращения поиска опций, и всё будет хорошо:

cp -- "$file" "$target"

Но у этого подхода есть свои проблемы. Вы должны быть уверены, что вставляете -- при каждом использовании параметра в контексте, когда он может быть интерпретирован в качестве опции. А это подразумевает большую избыточность, и можно легко что-то упустить.

Большинство из хорошо написанных библиотек для парсинга опций это понимают, и корректно использующие их программы должны бесплатно наследовать эту особенность. Однако имейте в виду, что ответственность за распознавание окончаний опций лежит исключительно на приложении. Некоторые программы, которые парсят опции вручную, или делают это некорректно, или используют сторонние библиотеки могут окончания не распознавать. Стандартные утилиты должны это делать, не считая нескольких исключений, описанных в POSIX. Например, echo.

Другой вариант — удостовериться, что ваши имена файлов всегда начинаются с папки. Для этого используются относительные или абсолютные пути к файлам.

for i in ./*.mp3; do
    cp "$i" /target
    ...
done

В таком случае, даже если у нас есть файл, имя которого начинается с дефиса, благодаря glob мы можем быть уверены, что переменная всегда содержит что-то вроде ./-foo.mp3. А это совершенно безопасно, если говорить о cp.

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

for i in *.mp3; do
    cp "./$i" /target
    ...
done

4. [ $foo = «bar» ]
Эта ситуация очень похожа на проблему, описанную во второй главе. Но всё же я повторю её, поскольку она очень важна. В приведённой в заголовке строке кавычки находятся не там, где нужно. В Bash вам не нужно заключать в кавычки строковые литералы (если они не содержат метасимволы или символы шаблонов). Но вы должны заключать в кавычки свои переменные, если они могут содержать пробелы или символы подстановки.

Приведённый пример может сломаться по нескольким причинам:

  • Если переменная, на которую ссылаются в [, не существует или пуста, тогда команда [ в конечном итоге будет выглядеть так:
    [ = "bar" ] # Неправильно!
    

    … и выкинет ошибку: unary operator expected. (Оператор = является двоичным, а не унарным, поэтому команда [ будет шокирована от встречи с ним).
  • Если переменная содержит внутренние пробелы, то она будет разделена на слова до того, как её увидит команда [. Следовательно, получим:
    [ multiple words here = "bar" ]
    

    Возможно, вы не видите проблем, но благодаря использованию [ тут присутствует синтаксическая ошибка. Правильный способ написания:
    # POSIX
    [ "$foo" = bar ] # Правильно!
    

    Это будет прекрасно работать в совместимых с POSIX реализациях, даже если перед $foo будет идти дефис, потому что в POSIX-команда [ определяет свои действия в зависимости от количества переданных ей аргументов. Только совсем древние оболочки будут испытывать с этим проблемы, можете о них не переживать при написании кода (см. далее уловку с x»$foo»).

В Bash и многих других ksh-подобных оболочках есть превосходная альтернатива, использующая ключевое слово [[.
# Bash / Ksh
[[ $foo == bar ]] # Правильно!

Вам не нужно брать в кавычки ссылки на переменные, расположенные слева от = внутри [[ ]], потому что они не подвергаются разделению на слова или глоббингу. И даже пустые переменные будут корректно обработаны. С другой стороны, использование кавычек никак не повредит. В отличие от [ и test, вы также можете использовать идентичный набор ==. Только обратите внимание, что при сравнениях с использованием [[ поиск по шаблону выполняется для строк в правой части, а не простое сравнение строк. Чтобы сделать правую строку литералом, вы должны поместить её в кавычки, при использовании любых символов, имеющих особое значение в контексте поиска по шаблону.
# Bash / Ksh
match=b*r
[[ $foo == "$match" ]] # Хорошо! Если кавычек не будет, то также будет сопоставлено по шаблону b*r.

Вероятно, вы видели подобный код:
# POSIX / Bourne
[ x"$foo" = xbar ] # Можно, но обычно не нужно.

Для кода, работающего на совсем древних оболочках, потребует хак x»$foo». Здесь вместо [[ используется более примитивное [. Если $foo начинается с дефиса, то возникает путаница. На старых системах [ не заботится о том, начинается ли с дефиса токен справа от =. Она использует его буквально. Так что нужно быть более внимательными с левой частью.

Обратите внимание, что оболочки, для которых нужен такой обходной путь, не совместимы с POSIX. Даже Heirloom Bourne этого не требует (вероятно, это неPOSIX клон Bourne-оболочки, который до сих пор является одной из самых распространённых системных оболочек). Такая экстремальная портируемость востребована редко, она делает ваш код менее читабельным и красивым.

5. cd $(dirname »$f»)
Ещё одна ошибка, связанная с кавычками. Как и в случае с подстановкой переменной (variable expansion), результат подстановки команды подвергается разбиению на слова и подстановке пути к файлу. Поэтому заключайте в кавычки:
cd -P -- "$(dirname -- "$f")"

Здесь не совсем очевидна логика вложенности кавычек. Программист на С будет ожидать, что первые и вторые двойные кавычки будут сгруппированы вместе, а затем будут идти третьи и четвёртые. Но в Bash всё иначе. Bash обрабатывает двойные кавычки внутри подстановки команды как одну пару, а двойные кавычки снаружи подстановки — как другую пару.

Можно написать и по-другому: парсер обрабатывает подстановку команды как «уровень вложенности», и кавычки внутри идут отдельно от кавычек снаружи.

6. [ »$foo» = bar && »$bar» = foo ]
Нельзя использовать && внутри старой команды test (или [). Парсер Bash видит && снаружи [[ ]] или (( )), и в результате разбивает вашу команду на две команды — до и после &&. Вместо этого используйте один из двух вариантов:
[ bar = "$foo" ] && [ foo = "$bar" ] # Правильно! (POSIX)
[[ $foo = bar && $bar = foo ]]       # Тоже правильно! (Bash / Ksh)

(Обратите внимание, что по причине легаси, упомянутого в главе 4, мы поменяли местами константу и переменную внутри [. Можно было бы поменять и [[, но для предотвращения интерпретирования в качестве шаблона пришлось бы брать подстановки в кавычки).

То же самое относится и к ||. Вместо них используйте [[ или две команды [.

Избегайте такого:

[ bar = "$foo" -a foo = "$bar" ] # Не портируемо.

Двоичные операторы -a, -o и (/) (группирование) — это XSI-расширения стандарта POSIX. В POSIX-2008 все они помечены как устаревшие. Использовать их в новом коде не стоит. Одной из практических проблем, связанных с [ A = B -a C = D ] (или -o), является то, что POSIX не определяет результаты команд test или [ с более чем четырьмя аргументами. Вероятно, в большинстве оболочек это будет работать, но рассчитывать на это нельзя. Если вам нужно писать для POSIX-оболочки, то используйте две команды test или [, разделённые оператором &&.7. [[ $foo > 7 ]]
Здесь есть несколько моментов. Во-первых, команду [[ не следует использовать исключительно для вычисления арифметических выражений. Её нужно применять для test-выражений, включающих один из поддерживаемых test-операторов. Хотя технически вы можете выполнять вычисления с помощью операторов [[, но делать это имеет смысл только в сочетании с одним из нематематических test-операторов, который присутствует где-то в выражении. Если вы хотите всего лишь сравнить числовые данные (или выполнить любое другое арифметическое действие), то гораздо лучше применить (( )):
# Bash / Ksh
((foo > 7))     # Правильно!
[[ foo -gt 7 ]] # Работает, но бессмысленно. Многие сочтут ошибкой. Лучше используйте ((...)) или let.

Если внутри [[ ]] вы используете оператор >, то система обработает это как сравнение строковых данных (проверка порядка сортировки по локали), а не числовых. Иногда это может сработать, но подведёт вас именно тогда, когда вы этого меньше всего ожидаете. Ещё хуже использовать > внутри [ ]: это перенаправление вывода. В вашей папке появится файл с названием 7, и тест будет успешно выполняться до тех пор, пока в $foo что-то есть.

Если требуется строгая совместимость с POSIX и не доступна команда ((, тогда правильной альтернативой будет использование старомодной [:

# POSIX
[ "$foo" -gt 7 ]       # Тоже правильно!
[ $((foo > 7)) -ne 0 ] # Совместимый с POSIX эквивалент (( для более общих математических операций.

Обратите внимание, что если $foo не является целочисленным, то команда test ... -gt завершится неудачно. Поэтому заключать в кавычки имеет смысл только ради производительности и разделения аргументов на одиночные слова, чтобы снизить вероятность возникновения побочных эффектов в некоторых оболочках.

Если вы не можете гарантировать входные данные для любого арифметического контекста (включая (( или let), либо тестового выражения [, подразумевающего числовые сравнения, тогда вы должны всегда валидировать входные данные, прежде чем выполнять вычисление.

# POSIX
case $foo in
    *[![:digit:]]*)
        printf '$foo expanded to a non-digit: %s\n' "$foo" >&2
        exit 1
        ;;
    *)
        [ $foo -gt 7 ]
esac

8. grep foo bar | while read -r; do ((count++)); done
Этот код выглядит нормально? Конечно, это всего лишь посредственная реализация grep -c, но так сделано для простоты примера. Изменения в count не будут распространяться за границы цикла while, потому что каждая команда конвейера исполняется в отдельной подоболочке (SubShell). В какой-то момент это удивляет любого новичка в Bash.

POSIX не определяет, должен ли вычисляться в подоболочке последний элемент конвейера. Одни оболочки, вроде ksh93 и Bash >= 4.2 с включённым shopt -s lastpipe, запустят приведённый в примере цикл while в исходном shell-процессе, что может привести к любым побочным эффектам. Следовательно, портируемые скрипты должны писаться так, чтобы не зависеть от подобного поведения.

Способы решения этой и подобных проблем вы можете почерпнуть из Bash FAQ #24. Здесь их слишком долго описывать.

9. if [grep foo myfile]
У многих новичков возникает ошибочное представление о выражениях if, обусловленное тем, что очень часто за этим ключевым словом сразу идёт [ или [[. Люди считают, что [ каким-то образом является частью синтаксиса выражения if, как и простые скобки, используемые в выражении if в языке С. Это не так! if получает команду. Ею является [, это не синтаксический маркер для if. Эта команда эквивалентна test, за исключением того, что последним аргументом должен быть ]. Например:
# POSIX
if [ false ]; then echo "HELP"; fi
if test false; then echo "HELP"; fi

Эти строки эквивалентны: обе проверяют, чтобы аргумент «false» не был пустым. В обоих случаях будет выводиться HELP, к удивлению программистов, пришедших из других языков и пытающихся разобраться с синтаксисом оболочки.

У выражения if такой синтаксис:

if COMMANDS
then 
elif  # optional
then 
else  # optional
fi # required

Ещё раз — [ является командой. Она получает аргументы, как и любая другая обычная команда. if — это составная команда, содержащая другие команды. И в её синтаксисе нет [!

Хотя в Bash есть встроенная команда [, и таким образом он знает о [, в ] нет ничего особенного. Bash всего лишь передаёт ] в качестве аргумента команде [, которой нужно, чтобы именно ] был последним аргументом, иначе скрипт будет выглядеть некрасиво.

Там может быть ноль и более опциональных секций elif, а также одна опциональная секция else.

Составная команда if содержит две и более секций, в которых находятся списки команд. Каждая секция начинается с ключевого слова then, elif или else, а заканчивается ключевым словом fi. Код завершения последней команды первой секции и каждая последующая секция elif определяют вычисление каждой соответствующей секции then. Другая секция elif вычисляется до того, как будет выполнена одна из then. Если не вычислено ни одной секции then, то происходит переключение на ветку else. Если нет else, то блок if завершается, а результирующая команда if возвращает 0 (true).

Если вы хотите принять решение в зависимости от выходных данных команды grep, то не нужно заключать её в круглые или квадратные скобки, backticks или любой другой синтаксис! Просто используйте grep как команду после if:

if grep -q fooregex myfile; then
...
fi

Если grep находит совпадение в строке из myfile, тогда код завершения будет 0 (true), и выполнится часть then. Если совпадений найдено не будет, grep вернёт значение, отличное от 0, а результирующая команда if будет нулём.

Читайте также:

  • BashGuide/TestsAndConditionals
  • http://wiki.bash-hackers.org/syntax/ccmd/if_clause

10. if [bar=»$foo»]; then …
[bar="$foo"]   # Неправильно!
[ bar="$foo" ] # Все еще неправильно!

Как объяснялось в предыдущей главе, [ — это команда (это можно доказать с помощью type -t [ или whence -v [). Как и в случае с любой другой простой командой, Bash ожидает, что после неё будет идти пробел, затем первый аргумент, снова пробел, и так далее. Вы просто не можете пренебрегать пробелами! Вот правильное написание:
if [ bar = "$foo" ]; then ...

Каждый из компонентов — bar, =, подстановка »$foo» и ] — являются отдельными аргументами команды [. Каждая пара аргументов должна быть разделена пробелом, чтобы оболочка знала, где начинается и кончается каждый из них. 11. if [ [ a = b ] && [ c = d ] ]; then …
Повторюсь в который раз. [ является командой. Это не синтаксический маркер, расположенный между if и каким-нибудь «состоянием», наподобие как в С. Не используется [ и для группирования. Вы не можете взять С-команды if и транслировать их в Bash-команды, просто заменив круглые скобки на квадратные!

Если вы хотите выразить составные условные конструкции, делайте так:

if [ a = b ] && [ c = d ]; then ...

Обратите внимание, что здесь у нас две команды после if, объединённые оператором && (логическое AND, сокращённое вычисление). Это то же самое, что и:
if test a = b && test c = d; then ...

Если первая команда test возвращает false, то вход в тело выражения if не выполняется. Если возвращает true, тогда запускается вторая команда test; если и она возвращает true, тогда выполняется вход в тело выражения if. (C-программисты уже знакомы с &&. Bash использует такое же упрощённое вычисление. Подобно тому, как || выполняет упрощённое вычисление для операции OR.)

Ключевое слово [[ разрешает использование &&, так что можно написать и так:

if [[ a = b && c = d ]]; then ...

В главе 6 описана проблема, связанная с комбинированием test с условными операторами.12. read $foo
Не используйте $ перед именем переменной в команде read. Если вы хотите поместить данные в переменную с именем foo, делайте так:
read foo

Или ещё безопаснее:
IFS= read -r foo

read $foo считает строку входных данных и поместит её в переменную/ые с именем $foo. Это может быть полезным, если вы действительно хотели сделать foo ссылкой на другую переменную;, но в большинстве случаев это баг. 13. cat file | sed s/foo/bar/ > file
Вы не можете читать из файла и писать в него в рамках одного конвейера. В зависимости от того, что делает ваш конвейер, файл:
  • может оказаться затёрт (clobbered) (до 0 байт, или до размера, эквивалентного размеру буфера конвейера вашей ОС),
  • может разрастись и заполнить всё доступное дисковое пространство,
  • может достичь предельного размера, заданного ОС или вами, и так далее.

Если вы хотите безопасно изменить файл, не просто добавляя информацию в конец, то воспользуйтесь текстовых редактором.
printf %s\\n ',s/foo/bar/g' w q | ed -s file

Если он не может помочь вам в решении вашей задачи, то в определённый момент (*) необходимо создавать временный файл.

Этот пример можно портировать без ограничений:

sed 's/foo/bar/g' file > tmpfile && mv tmpfile file

Этот пример будет работать только под GNU sed 4.x:
sed -i 's/foo/bar/g' file(s)

Обратите внимание, что здесь создаётся временный файл и применяется такой же трюк с переименованием — обработка выполняется прозрачно.

А следующая команда-аналог требует наличия Perl 5.x (который, вероятно, более распространён, чем GNU sed 4.x):

perl -pi -e 's/foo/bar/g' file(s)

За подробностями о замене контента файлов обратитесь к Bash FAQ #21.

(*) в мануале к sponge из moreutils приводится такой пример:

sed '...' file | grep '...' | sponge file

Вместо использования временного файла и атомарного mv, эта версия «впитывает» (цитата из мануала!) все данные, прежде чем открыть и записать в file. Правда, если программа или система рухнет в ходе операции, данные будут потеряны, потому что в этот момент на диске нет копии исходной информации.

Использование временного файла + mv всё ещё подвергает нас небольшому риску потери данных при падении системы / отключении питания. Чтобы старый или новый файл сохранился, нужно перед mv использовать sync.

14. echo $foo
Эта относительно безобидно выглядящая команда вносит сильную путаницу. Поскольку $foo не взята в кавычки, она не только подвергнется разбиению на слова, но и глоббингу. Из-за этого Bash-программисты думают, что их переменные содержат неправильные значения, хотя на самом деле с ними всё в порядке. Смуту вносит разбиение на слова или подстановка пути к файлу.
msg="Пожалуйста, введите название формы *.zip"
echo $msg

Это сообщение разбито на слова, а все глобы (glob) (вроде *.zip) разложены. Что подумают пользователи, когда увидят сообщение: Пожалуйста, введите название формы freenfss.zip lw35nfss.zip. Иллюстрация:
var="*.zip"   # var содержит звёздочку, точку и слово "zip"
echo "$var"   # пишет *.zip
echo $var     # пишет список файлов, заканчивающихся на .zip

По сути, здесь команда echo не может быть использована безопасно. Если переменная содержит, например, -n, то echo решит, что это опция, а не данные для вывода на экран. Единственный гарантированный способ вывода значения переменной — использование printf:
printf "%s\n" "$foo"

15. $foo=bar
Нет, помещая $ перед именем переменной, вы не присваиваете ей значение. Это не Perl. 16. foo = bar
Нет, вы не можете вставить пробелы вокруг =, когда присваиваете переменной значение. Это не С. Когда вы пишете foo = bar, оболочка разбивает это на три слова. Первое — foo — берётся в качестве имени команды. Второе и третье — в качестве аргументов команды.
  • foo= bar # Неправильно!
  • foo =bar # Неправильно!
  • $foo = bar; # СОВСЕМ НЕПРАВИЛЬНО!
  • foo=bar # Правильно.
  • foo="bar" # Ещё правильнее.

17. echo < Here-док — это полезный инструмент для встраивания в скрипт больших блоков текстовых данных. Это приводит к перенаправлению строк текста в скрипте на стандартный ввод команды. К сожалению, команда echo не читает из stdin.
# Это неправильно:
echo <

При использовании кавычек это будет прекрасно работать во всех оболочках. Но вы не сможете просто закинуть в скрипт пачку строк. Первая и последняя строки должны иметь синтаксическую разметку. Если вы хотите, чтобы строки не содержали синтаксиса оболочки, и не хотите множить команду cat, то воспользуйтесь альтернативой:
# Или примените printf (тоже эффективно, printf встроена):
printf %s "\
Hello world
How's it going?
"

В примере с printf, знак \ в первой строке предотвращает появление дополнительной новой строки в начале текстового блока. Новая строка есть в конце блока (потому что последняя кавычка находится в новой строке). Отсутствие \n в аргументе printf предотвращает добавление в конце новой строки. Только трюк с \ не сработает при использовании одинарных кавычек. Если вы хотите включить в них блок текста, то у вас есть два варианта, и оба они подразумевают «загрязнение» ваших данных синтаксисом оболочки:
printf %s \
  'Hello world
  '

  printf %s 'Hello world
  '

18. su -c 'some command'
Этот синтаксис почти корректен. Проблема в том, что на многих платформах su берёт аргумент -c, но не тот, который вам нужен. Вот пример с OpenBSD:
$ su -c 'echo hello'
su: only the superuser may specify a login class

Вы хотите передать оболочке -c 'some command', то есть перед -c вам нужно имя пользователя.
su root -c 'some command' # Now it's right.

su подразумевает имя root-пользователя, когда вы его опускаете. Но он сталкивается с этим, когда вы позднее пытаетесь передать команду оболочке. Так что в этом случае вы должны явно указать имя пользователя. 19. cd /foo; bar
Если вы не проверяете на наличие ошибок после команды cd, то можете выполнить bar в неверном месте. А это попахивает катастрофой, если, к примеру, bar окажется rm -f *. Всегда проверяйте на наличие ошибок после команды cd. Простейший способ:
cd /foo && bar

Если после cd идёт больше одной команды, то можно сделать так:
cd /foo || exit 1
bar
baz
bat ... # Lots of commands.

cd сообщит о невозможности изменения папки, выдав stderr-сообщение наподобие «bash: cd: /foo: No such file or directory». Если вы хотите добавить в stdout собственное сообщение, то можете применить группирование команд:
cd /net || { echo >&2 "Can't read /net. Make sure you've logged in to the Samba network, and try again."; xit 1; }
do_stuff
more_stuff

Обратите внимание, что между { и echo нужен пробел. Также перед закрывающей } нужен ;.

Некоторые любят включать set -e, чтобы их скрипты прерывались на любой команде, возвращающей значение, отличное от нуля. Но это не так просто использовать правильно (потому что многие обычные команды могут возвращать не нулевое значение ради предупреждения, что вы можете не считать фатальным).

Кстати, если вы много раз меняете папки в Bash-скрипте, то почитайте инструкцию по пользованию pushd, popd и dirs. Вероятно, весь код, который вы писали для управления cd и pwd, совершенно не нужен. Сравните это:

find ... -type d -print0 | while IFS= read -r -d '' subdir; do
  here=$PWD
  cd "$subdir" && whatever && ...
  cd "$here"
done

C этим:
find ... -type d -print0 | while IFS= read -r -d '' subdir; do
  (cd "$subdir" || exit; whatever; ...)
done

Принудительное использование подоболочки заставляет cd выполняться только в ней. Для следующей итерации цикла мы возвращаемся в нормальное место, вне зависимости от того, успешно ли выполнилась cd. Нам не нужно менять папку вручную, и мы не застреваем в бесконечной строке с логикой ... && ..., предотвращающей использование других условных конструкций. Версия с подоболочкой проще и чище (хотя и немножко медленнее).20. [ bar == »$foo» ]
Оператор == не является валидным для POSIX-команды [. Используйте = ключевое слово [[.
[ bar = "$foo" ] && echo yes
[[ bar == $foo ]] && echo yes

В Bash [ "$x" == y ] принимается как подстановка, поэтому многие программисты считают синтаксис правильным. Но это не так — это «башизм» (Bashism). Если вы собрались использовать башизмы, то можете вместо этого использовать и [[. 21. for i in {1…10}; do ./something &; done
Нельзя помещать ; сразу после &. Просто удалите лишнюю ;.
 for i in {1..10}; do ./something & done
Или: 
 for i in {1..10}; do
   ./something &
 done

& уже работает как прерыватель команды (command terminator), как и ;. Нельзя их смешивать.

В целом, ; можно заменить новой строкой, но не все новые строки можно заменить на ;.

22. cmd1 && cmd2 || cmd3
Кто-то любит использовать && и || в качестве сокращённого синтаксиса для if ... then ... else ... fi. Во многих случаях это безопасно:
[[ -s $errorlog ]] && echo "Uh oh, there were some errors." || echo "Successful."

Однако в целом эта конструкция не полностью эквивалентна if ... fi. Команда, идущая после &&, также генерирует код завершения. И если этот код не «true» (0), тогда будет вызвана и команда, идущая после ||. Например:
i=0
 true && ((i++)) || ((i--))
 echo $i # Prints 0

Что здесь происходит? Похоже, что i должно равняться 1, но получается 0. Почему? Потому что были выполнены и i++, и i--. Команда ((i++)) имеет код завершения, который унаследован от вычисления выражения внутри круглых скобок по примеру языка С. Значение выражения равно 0 (начальное значение i), а в С выражение целочисленным, равным 0, считается false. Так что команда ((i++)) (когда i равно 0) имеет код завершения 1 (false), и значит также выполняется команда ((i--)).

Это не происходит, если мы используем оператор предварительного инкрементирования, поскольку код завершения ++i равен true:

i=0
 true && (( ++i )) || (( --i ))
 echo $i # Prints 1

Но это работает благодаря случайности. Вы не можете полагаться на x && y || z, если y имеет малейший шанс сбоя! Этот пример не будет работать, если начальное значение i будет -1 вместо 0.

Если вас волнует безопасность или если вы просто не уверены, как это работает, или если вы хоть что-то недопоняли из предыдущих параграфов, пожалуйста, используйте простой синтаксис if ... fi.

i=0
if true; then
  ((i++))
else
  ((i--))

    
            

© Habrahabr.ru