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



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 <COMMANDS>
elif <COMMANDS> # optional
then <COMMANDS>
else <COMMANDS> # 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 будет нулём.

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


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 <<EOF


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

# Это неправильно:
echo <<EOF
Hello world
How's it going?
EOF

# Вот что вы пытались сделать:
cat <<EOF
Hello world
How's it going?
EOF

# Либо используйте кавычки, которые могут объединять несколько строк (эффективно, echo встроена):
echo "Hello world
How's it going?"

При использовании кавычек это будет прекрасно работать во всех оболочках. Но вы не сможете просто закинуть в скрипт пачку строк. Первая и последняя строки должны иметь синтаксическую разметку. Если вы хотите, чтобы строки не содержали синтаксиса оболочки, и не хотите множить команду 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--))
fi
echo $i # Prints 1

Эта часть также применима к оболочке Bourne:

true && { echo true; false; } || { echo false; true; }

На выходе получается две строки «true» и «false», вместо одной строки «true».

23. echo «Hello World!»


В интерактивной оболочке Bash (до версии 4.3), вы увидите ошибку:

bash: !": event not found

Дело в том, что при настройках по умолчанию для интерактивной оболочки, Bash выполняет подстановку истории (history expansion) в стиле csh, используя восклицательный знак. Это проблема не для скриптов оболочки, а только для интерактивных оболочек. К сожалению, очевидная попытка «исправления» не сработает:

$ echo "hi\!"
 hi\!

Простейшее решение — вернуть в исходное состояние опцию histexpand. Это можно сделать с помощью set +H или set +o histexpand:

Вопрос: Почему лучше использовать histexpand, чем одинарные кавычки?

Я лично столкнулся с этой ситуацией, когда манипулировал файлами песен с помощью команд наподобие

mp3info -t "Don't Let It Show" ...
mp3info -t "Ah! Leah!" ...

Одинарные кавычки неудобны в использовании, потому что все песни имеют в названиях апострофы. Использование двойных кавычек привело к проблеме подстановки истории. А представьте, что у файла в названии и апостроф, и двойные кавычки. Так что от кавычек лучше отказаться. Поскольку я никогда не прибегаю к подстановке истории, то предпочитаю выключить ~/.bashrc. -- GreyCat

Сработает такое решение:

echo 'Hello World!'

Или

set +H
echo "Hello World!"

Или

histchars=

Многие просто кладут set +H или set +o histexpand в свои ~/.bashrc, чтобы навсегда деактивировать подстановку истории. Это дело вкуса, выбирайте, что вам больше подходит.

Другое решение:

exmark='!'
echo "Hello, world$exmark"

В Bash 4.3 и ниже, двойные кавычки после ! больше не запускают подстановку истории. Но с двойными кавычками оно всё же выполняется, и хотя с echo "Hello World!" порядок, у нас ещё есть проблема:

echo "Hello, World!(and the rest of the Universe)"
 echo "foo!'bar'"

24. for arg in $*


Bash (как и все Bourne-оболочки) имеет специальный синтаксис для ссылания на список позиционных параметров, по одному за раз. Это $*, а не $@. Они оба раскладываются на список слов в ваших параметрах скрипта, каждый параметр не является отдельным словом. Правильный синтаксис:

for arg in "$@"

# Или проще:
for arg

Поскольку в скриптах часто прогоняют через циклы позиционные параметры, for arg по умолчанию используется для for arg in "$@". Взятый в двойные кавычки "$@" — это особая магия, благодаря которой каждый параметр используется как отдельное слово (или отдельная итерация цикла). Так вы должны делать в 99% случаев.

Пример:

# Ошибочная версия
for x in $*; do
  echo "parameter: '$x'"
done

$ ./myscript 'arg 1' arg2 arg3
parameter: 'arg'
parameter: '1'
parameter: 'arg2'
parameter: 'arg3'

Надо было написать:

# Правильная версия
for x in "$@"; do
  echo "parameter: '$x'"
done
# Или лучше:
for x; do
  echo "parameter: '$x'"
done

$ ./myscript 'arg 1' arg2 arg3
parameter: 'arg 1'
parameter: 'arg2'
parameter: 'arg3'

25. function foo()


В некоторых оболочках это работает, но не во всех. При определении функции никогда не комбинируйте ключевое слово function с круглыми скобками (). Bash (как минимум некоторые версии) позволяет их смешивать. Но большинство оболочек такой код не примут (например, zsh 4.x и, вероятно, выше). Некоторые оболочки примут function foo, но для максимальной совместимости лучше применять:

foo() {
 ...
}

26. echo "~"


Подстановка с помощью тильды применяется только тогда, когда '~' не взято в кавычки. В этом примере echo пишет в stdout '~', а не путь пользовательской домашней папки. Брать в кавычки параметры, которые выражены относительно пользовательской домашней папки, нужно с помощью $HOME, а не '~'. Возьмите ситуацию, когда $HOME — это "/home/my photos".

"~/dir with spaces" # разворачивается до "~/dir with spaces"
 ~"/dir with spaces" # разворачивается до "~/dir with spaces"
 ~/"dir with spaces" # разворачивается до "/home/my photos/dir with spaces"
 "$HOME/dir with spaces" # разворачивается до "/home/my photos/dir with spaces"

27. local varname=$(command)


При объявлении в функции локальной переменной, local самостоятельно действует как команда. Иногда остальная часть строки может итерироваться странно. Например, если вы хотите получить код завершения ($?) подстановки команды, то у вас ничего не получится. Он будет скрыт кодом завершения локали. Для этого лучше разделять команды:

local varname
 varname=$(command)
 rc=$?

Следующая проблема описывает другую особенность синтаксиса.

28. export foo=~/bar


Когда тильда находится в начале слова, — самостоятельно или через слэш — то гарантированно будет выполнена только подстановка с помощью тильды (с именем пользователя или без него). Также оно будет обязательно выполнено, когда в присваивании тильда идёт сразу после =.

Однако команды export и local не осуществляют присваивание. Так что в некоторых оболочках (вроде Bash) export foo=~/bar подвергнется подстановке с помощью тильды, а в других (вроде dash) — нет.

foo=~/bar; export foo    # Правильно!
 export foo="$HOME/bar"   # Правильно!

29. sed 's/$foo/good bye/'


В одинарных кавычках параметры подстановки вроде $foo не раскладываются. Это предназначение одинарных кавычек — защищать от оболочки символы наподобие $. Применяйте двойные кавычки:

foo="hello"; sed "s/$foo/good bye/"

Но помните: в этом случае вам может понадобиться использовать больше escapes. За подробностями обратитесь к странице «кавычки».

30. tr [A-Z] [a-z]


Здесь как минимум три проблемы. Первая проблема: [A-Z] и [a-z] рассматриваются оболочкой как глобы. Если у вас в текущей папке нет файлов с именами, состоящими из одного символа, то команда получается некорректной. Если есть, то всё пойдет наперекосяк. Вероятно, в 3 ночи в выходные.

Вторая проблема: на самом деле это неправильная нотация для tr. Фактически, здесь '[' переводится в '[', затем что-то из диапазона A-Z в a-z, а затем ']' в ']'. Так что вам даже не нужны эти квадратные скобки, первая проблема исчезнет сама собой.

Третья проблема заключается в том, что в зависимости от локали, A-Z или a-z могут не дать вам ожидаемые 26 ASCII-символов. Фактически в некоторых локалях z находится посреди алфавита! Решение зависит от того, что вам нужно:

# Используйте, если хотите изменить 26 латинских букв
LC_COLLATE=C tr A-Z a-z

# Используйте, если вам нужно преобразование в зависимости от локаля. Это с большей вероятностью нужно пользователям
tr '[:upper:]' '[:lower:]'

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

31. ps ax | grep gedit


Фундаментальная проблема заключается в том, что имя исполняемого процесса по своей природе ненадёжно. Может быть несколько легитимных процессов gedit. Может быть что-то ещё, маскирующееся под gedit (можно тривиально изменить объявляемое имя исполненной команды). Чтобы разобраться подробнее, читайте об управлении процессами. При поиске PID gedit (например), многие начнут с

$ ps ax | grep gedit
10530 ?        S      6:23 gedit
32118 pts/0    R+     0:00 grep gedit

А это, в зависимости от Race Condition, часто выдаёт в качестве результата сам grep. Его можно отфильтровать:

ps ax | grep -v grep | grep gedit # сработает, но выглядит страшненько


Альтернатива:

ps ax | grep '[g]edit' # возьмите в кавычки, чтобы избежать shell GLOB

Grep будет проигнорирован в таблице процессов, потому что он [g]edit, grep будут искать единожды выполненный gedit.

В GNU/Linux параметр –C можно использовать для фильтрования по имени команды:

$ ps -C gedit
  PID TTY          TIME CMD
10530 ?        00:06:23 gedit

Но зачем переживать, если можно использовать pgrep?

$ pgrep gedit
10530

На втором этапе PID зачастую извлекается с помощью awk или cut:

$ ps -C gedit | awk '{print $1}' | tail -n1

Но даже это можно обработать с помощью триллионов параметров для ps:

$ ps -C gedit -opid=
10530

Если вы застряли в 1992 году и не используете pgrep, то можете применить древний, устаревший и осуждаемый pidof (только под GNU/Linux):

$ pidof gedit
10530

А если вам нужен PID, чтобы убить процесс, то вас может заинтересовать pkill. Только обратите внимание, что, например, pgrep/pkill ssh также найдёт процессы под названием sshd, убивать которые вам не захочется.

К сожалению, некоторые программы начинаются не со своего имени. Скажем, Firefox часто стартует как firefox-bin. Этот процесс тоже надо будет найти с помощью, скажем, ps ax | grep firefox. Или можно добавить несколько параметров к pgrep:

$ pgrep -fl firefox
3128 /usr/lib/firefox/firefox
7120 /usr/lib/firefox/plugin-container /usr/lib/flashplugin-installer/libflashplayer.so -greomni /usr/lib/firefox/omni.ja 3128 true plugin

Почитайте про управление процессами. Серьёзно.

32. printf "$foo"


Здесь ошибка связана не с кавычками, а с использованием строки формата. Если $foo не находится под вашим полным контролем, тогда наличие в переменной символов \ или % может привести к нежелательному поведению. Всегда задавайте свою строку формата:

printf %s "$foo"
printf '%s\n' "$foo"

33. for i in {1..$n}


Парсер Bash выполняет раскрытие скобок до всех разложений и подстановок. Так что код раскрытия видит литерал $n, который не является числом, и поэтому не раскрывает фигурные скобки в список чисел. Так что практически невозможно использовать раскрытие скобок для создания списков, размер которых известен только в ходе runtime. Лучше делайте так:

for ((i=1; i<=n; i++)); do
...
done

При простом итерировании целочисленных переменных почти всегда лучше начинать с цикла арифметических вычислений for, а не с раскрытия скобок, потому что в последнем случае каждый аргумент предварительно раскладывается (pre-expands), что может снизить производительность и увеличить потребление памяти.

34. if [[ $foo = $bar ]] (в зависимости от цели)


Если не взять в кавычки то, что расположено справа от = внутри [[, то Bash не будет считать это строкой, а будет сопоставлять с шаблоном. Так что если в приведённом коде bar будет содержать *, то результат будет всегда true. Если вы хотите проверить строковые на эквивалентность друг другу, то возьмите правую часть в кавычки:

if [[ $foo = "$bar" ]]

Если вам нужно сопоставить с шаблоном, то умнее будет выбрать иное имя переменной, обозначающее, что правая часть содержит шаблон. Или использовать комментарии.

Ещё нужно указать, что если взять в кавычки часть справа от =~, тогда будет принудительно выполняться ещё и простое сравнение строковых, а не только сопоставление регулярных выражений. Это приводит нас к следующей проблеме.

35. if [[ $foo =~ 'some RE' ]]


Кавычки вокруг правой части делают их содержимое строкой, а не регулярным выражением. Если вам нужно использовать длинное или сложное регулярное выражение, избегая многочисленных изолирований с помощью обратных слешей, то поместите данные в переменную:

re='some RE'
if [[ $foo =~ $re ]]

Также это помогает обойти различия в работе =~ в разных версиях Bash. Благодаря переменной мы избегаем некоторых неприятных и неочевидных проблем.

Та же проблема возникает при сопоставлении с шаблоном внутри [[:

[[ $foo = "*.glob" ]]      # Wrong! *.glob is treated as a literal string.
[[ $foo = *.glob ]]        # Correct. *.glob is treated as a glob-style pattern.

36. [ -n $foo ] or [ -z $foo ]


Используя команду [, вы должны брать в кавычки все передаваемые ей подстановки. В противном случае $foo может разложиться на 0 слов, или 42 слова, или любое другое количество, кроме 1, что сломает синтаксис.

[ -n "$foo" ]
[ -z "$foo" ]
[ -n "$(внутри какая-то команда с "$file")" ]

# [[ не выполняет разделение на слова или подстановку с помощью глобов, так что можно использовать и это:
[[ -n $foo ]]
[[ -z $foo ]]

37. [[ -e "$broken_symlink" ]] возвращает 1 несмотря на существование $broken_symlink


Test идёт после symlink’ов, следовательно, если symlink сломан, — например, указывает на файл, который не существует или находится в недоступной папке, — тогда test –e возвращает 1, несмотря на существование symlink. Чтобы это обойти (и подготовиться к этому), используйте:

# bash/ksh/zsh
[[ -e "$broken_symlink" || -L "$broken_symlink" ]]

# POSIX sh+test
[ -e "$broken_symlink" ] || [ -L "$broken_symlink" ]

38. Сбой ed file <<<«g/d\{0,3\}/s//e/g»


Проблема в том, что ed не принимает 0 для \{0,3\}. Можете проверить, что такой код работает:

ed file <<<"g/d\{1,3\}/s//e/g"

Обратите внимание, что это происходит несмотря на то что POSIX-состояния, которые BRE (особенность регулярных выражений, используемая ed), должны принимать 0 в качестве минимального количества вхождений (см. главу 5).

39. Сбой подцепочки (sub-string) expr для «match»


Это достаточно хорошо работает… чаще всего

word=abcde
expr "$word" : ".\(.*\)"
bcde

Но со словом «match» произойдёт сбой

word=match
expr "$word" : ".\(.*\)"

Дело в том, что «match» — ключевое слово. В GNU эта проблема решается с помощью префикса '+'

word=match
expr + "$word" : ".\(.*\)"
atch

Или можно просто отказаться от expr. Всё, что она умеет делать, вы можете выполнять с помощью параметрической подстановки (Parameter Expansion). Например, нужно убрать первую букву в слове? В POSIX-оболочках это решается посредством параметрической подстановки или разложения на подцепочки (Substring Expansion):

$ word=match
$ echo "${word#?}"    # PE
atch
$ echo "${word:1}"    # SE
atch

Серьёзно, для использования expr у вас нет оправданий, если только вы не работаете на Solaris с его несовместимым с POSIX /bin/sh. Это внешний процесс, так что он работает куда медленнее, чем внутрипроцессная обработка строки. А поскольку никто его не использует, то никто и не понимает, что он делает, так что ваш код будет запутан и труден в сопровождении.

40. Про UTF-8 и метки последовательности байтов (Byte-Order Marks, BOM)


В целом: в Unix текст в кодировке UTF-8 не использует BOM. Кодирование обычного текста определяется локалью, MIME-типами или другими метаданными. Хотя наличие BOM обычно не вредит документу в кодировке UTF-8, предназначенному только для чтения людьми, но является проблемой (зачастую из-за нелегального синтаксиса) в любых текстовых файлах, предназначенных для автоматизированных процессов в скриптах, исходном коде, конфигурационных файлах и так далее. Файлы, которые начинаются с BOM, нужно считать чужеродными, как и те, что содержат разрывы строк в стиле MS-DOS.

В скриптах оболочек: там, где UTF-8 прозрачно используется в 8-битных средах, применение BOM будет мешать любому протоколу или файловому формату, который ожидает, что в начале будут идти конкретные ASCII-символы, наподобие "#!" в начале скриптов Unix-оболочки».

http://unicode.org/faq/utf_bom.html#bom5

41. content=$(<file)


С этим выражением всё в порядке, но помните, что эти подстановки команд (всех форм: `...`, $(...), $(<file), `<file` и ${ ...; } (ksh)) удаляют все конечные новые строки. Зачастую, это неуместно или даже нежелательно, но если вам нужно сохранить выходные данные литерала, включая все возможные конечные новые строки, то это будет непросто, поскольку совершенно неизвестно, есть ли они вообще и сколько их. Есть способ решения этой проблемы, хотя и корявый: добавляем внутри подстановки команды постфикс и убираем его снаружи.

absolute_dir_path_x=$(readlink -fn -- "$dir_path"; printf x)
absolute_dir_path=${absolute_dir_path_x%x}

Менее портируемое, но более приятное решение: использовать read с пустым разделителем.

# Ksh (or bash 4.2+ with lastpipe enabled)
readlink -fn -- "$dir_path" | IFS= read -rd '' absolute_dir_path

Недостатком этого способа является то, что read всегда будет возвращать false, если команда не выдаёт NUL-байт, что приводит к чтению только части потока. Единственный способ получения кода завершения команды — через PIPESTATUS. Можно также намеренно выводить NUL-байт, чтобы заставить read вернуть true, и использовать pipefail.

set -o pipefail
{ readlink -fn -- "$dir_path"; printf '\0x'; } | IFS= read -rd '' absolute_dir_path

Здесь настоящий хаос портируемости: Bash поддерживает pipefail и PIPESTATUS, ksh93 — только pipefail, лишь последние версии mksh поддерживают pipefail, а более ранние — только PIPESTATUS. Кроме того, требуется самая свежая версия ksh93, чтобы заставить read остановиться на NUL-байте.

42. for file in ./*; do if [[ $file != *.* ]]


Одним из способов не дать программам интерпретировать передаваемые им имена файлов как опции, является использование путей к файлам (см. главу 3). В названиях файлов в текущей папке можно использовать префикс относительного пути ./.

Но могут возникнуть проблемы с шаблоном *.*, потому что он сопоставляет файлы вида ./filename. В простом случае можно просто напрямую использовать глоб для генерирования желаемых соответствий. Однако, если требуется отдельный этап сопоставления с шаблоном (например, результаты были предварительно обработаны и сохранены в массиве, и их нужно отфильтровать), то в шаблоне можно учитывать префикс: [[ $file != ./*.* ]], либо расщепить шаблон.

# Bash
shopt -s nullglob
for path in ./*; do
    [[ ${path##*/} != *.* ]] && rm "$path"
done

# Так ещё лучше
for file in *; do
    [[ $file != *.* ]] && rm "./${file}"
done

# Всё ещё лучше
for file in *.*; do
    rm "./${file}"
done

Другой способ сигнализировать о конце опций — использовать аргумент -- (снова читайте главу 3).

shopt -s nullglob
for file in *; do
    [[ $file != *.* ]] && rm -- "$file"
done

43. somecmd 2>&1 >>logfile


Несомненно, это наиболее распространённая ошибка, связанная с перенаправлениями, обычно совершаемая программистами, которые хотят направить в файл или пайп stdout и stderr. Они пытаются это сделать и не понимают, почему stderr всё ещё отображается в их терминалах. Если вы тоже в недоумении по этому поводу, то вероятно не знаете, как начать работать с перенаправлениями или файловыми дескрипторами. Перенаправления выполняются слева направо, до выполнения команды. Этот семантически неправильный код означает: «сначала перенаправить стандартную ошибку туда, куда сейчас указывает стандартный вывод (tty), а затем перенаправить стандартный вывод в лог-файл». Это в обратном направлении. Стандартная ошибка уже направляется в tty. Правильно так:

somecmd >>logfile 2>&1

Читайте более подробное объяснение, объяснение про дескриптор Copy и BashGuide — redirection.

44. cmd; ((! $? )) || die


$? нужно только тогда, когда вы пытаетесь извлечь конкретный статус предыдущей команды. Если вам всего лишь нужно узнать, была ли она успешной или нет (любой не нулевой статус), то запросите команду напрямую, например:

if cmd; then
    ...
fi

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

cmd
status=$?
case $status in
    0)
        echo success >&2
        ;;
    1)
        echo 'Must supply a parameter, exiting.' >&2
        exit 1
        ;;
    *)
        echo "Unknown error $status, exiting." >&2
        exit "$status"
esac

45. y=$(( array[$x] ))


Поскольку POSIX выражает арифметическую подстановку (arithmetic expansion) словами (разложение подстановок команд вызывается после параметрической подстановки), то подстановка индекса массива (array subscript) внутри арифметической подстановки может привести к внедрению эксплойтов. Да, получается много больших, запутанных слов.

$ x='$(date >&2)'        # перенаправление только для того, чтобы видеть, что происходит
$ y=$((array[$x]))       # массив даже не нужно создавать
Mon Jun  2 10:49:08 EDT 2014

Не поможет и использование кавычек:

$ y=$((array["$x"]))
Mon Jun  2 10:51:03 EDT 2014

Работают два способа:

# 1. Изолирование $x, чтобы он не был разложен раньше времени.
$ y=$((array[\$x]))

# 2. Использование полного синтаксиса ${array[$x]}.
$ y=$((${array[$x]}))

46. read num; echo $((num+1))


Всегда валидируйте входные данные (см. BashFAQ/054), прежде чем использовать num в арифметическом контексте, поскольку он позволяет выполнять внедрение чужеродного кода.

$ echo 'a[$(echo injection >&2)]' | bash -c 'read num; echo $((num+1))'
injection
1

47. IFS=, read -ra fields <<< "$csv_line"


Это может показаться невероятным, но POSIX подходит к IFS как к прерывателю поля (field terminator), а не к разделителю полей. В нашем примере это означает, что если в конце строки ввода есть пустое поле, то оно будет отброшено:

$ IFS=, read -ra fields <<< "a,b,"
$ declare -p fields
declare -a fields='([0]="a" [1]="b")'

Куда делось пустое поле? Оно пропало по историческим причинам («потому что всегда так делалось»). Такое поведение характерно не только для Bash, это делают все совместимые оболочки. Непустое поле сканируется корректно:

$ IFS=, read -ra fields <<< "a,b,c"
$ declare -p fields
declare -a fields='([0]="a" [1]="b" [2]="c")'

Что нам делать с этой ерундой? Судя по всему, добавление IFS-символа в конец входной строки заставит сканирование работать правильно. Если замыкающее поле пустое, то дополнительный IFS-символ «прерывает» его, чтобы оно было просканировано. А если замыкающее поле непустое, то IFS-символ создаёт новое, пустое поле, которое и теряется.

$ input="a,b,"
$ IFS=, read -ra fields <<< "$input,"
$ declare -p fields
declare -a fields='([0]="a" [1]="b" [2]="")'

48. export CDPATH=.:~/myProject


Не экспортируйте CDPATH. Настраивание CDPATH в .bashrc не является проблемой, но экспортирование приведёт к тому, что выполняемый Bash- или sh-скрипт, использующий cd, может повести себя иначе. Есть две проблемы. Скрипт, делающий следующее:

cd some/dir || exit
cmd to be run in some/dir

Может изменить папку с ./some/dir на ~/myProject/some/dir, в зависимости от того, какие папки существуют в данный момент. Так что cd может отправить скрипт в ошибочную папку, что может оказать негативное влияние на следующие команды, которые теперь выполняются не там, где задумано.

Вторая проблема — когда cd выполняется в контексте, где захватываются выходные данные:

output=$(cd some/dir && some command)

Когда CDPATH настроен, то в качестве побочного эффекта cd может выдать stdout нечто вроде /home/user/some/dir, чтобы показать, что папка найдена через CDPATH, который завершится в выходной переменной с ожидаемыми выходными данными some command.

Скрипт может обрести иммунитет к CDPATH, унаследованному от среды, благодаря обязательному использованию ./ для относительных путей. Или можно в начале скрипта запустить unset CDPATH, но не думайте, что каждый автор скриптов учитывает этот подводный камень, так что не экспортируйте CDPATH.
Поделиться с друзьями
-->

Комментарии (61)


  1. izzholtik
    06.10.2016 13:48
    +20

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


  1. NeoCode
    06.10.2016 14:00
    +4

    По моему bash это вообще одна сплошная ошибка. Неужели в линукс сообществе до сих пор нет попыток прикрутить вместо этого ископаемого монстра нормальный современный интерпретируемый язык программирования?
    Просто взять и сделать дистрибутив целиком без bash.


    1. prefrontalCortex
      06.10.2016 14:23
      +8

      Да бросьте, просто у вас стокгольмский синдром по отношению к bash ещё не до конца выработался.


    1. marcor
      06.10.2016 14:38
      +13

      linux свободен. Любой пользователь может снести bash и поставить туда собственный myNewBash. Собсно, как и было с zsh, csh и так далее, семейства их.

      Но не прижилось. Вот тут уже интересно подумать почему.


    1. selivanov_pavel
      06.10.2016 15:09
      +10

      до сих пор нет попыток прикрутить вместо этого ископаемого монстра нормальный современный интерпретируемый язык программирования

      Shell изначально развивался как средство быстрой интеграции программ в командной строке, и в этом плане он до сих пор удобнее всех альтернатив. Задачи типа "взять последние 5000 строчек лога, отобрать с типом POST и http status 500, выбрать IP, отсортировать по частоте встречаемости" или "заменить в конфиге все example.com на example.net, протестировать корректность конфига, если ок — перезапустить сервис" на баше делаются почти не думая, быстро и лаконично.


      Ну и про legacy забывать не стоит, "просто взять и сделать дистрибутив целиком без bash" — это огромное количество человеко-лет на переписывание всех скриптов, используемых всеми программами, и дальнейшее сопровождение.


      З.Ы. Минусуют ИМХО зря, вопрос вполне нормальный.


      1. lvo
        06.10.2016 20:38

        Вряд ли вы в задаче типа «взять последние 5000 строчек лога, отобрать с типом POST и http status 500, выбрать IP, отсортировать по частоте встречаемости» пользуетесь одним только bash. Там ещё появляются tail, grep, awk, sort и uniq, и это их заслуга, а не bash, что задача такого вида решается просто. Более того, никто не запрещает использовать перечисленные утилиты без bash. Перечисленные утилиты полезны в любом языке. Те же grep и sort справляются с гигабайтными файлами быстрее и эффективнее питона, и это вполне нормальная практика — вызывать из питонячьего скрипта подпроцесс с sort. Так что вопрос о том, почему bash как средство автоматизации до сих пор популярен, весьма хорош.


        1. selivanov_pavel
          06.10.2016 21:02
          +9

          Да, и bash прекрасен именнно как максимально простой и лаконичный клей между всеми этими программами: foo | bar | baz > result. Представьте себе то же самое на питоне: import subprocess as sp; p=sp.Popen("foo", stdout=sp.PIPE); sp.communicate(.... Ну это же просто застрелиться.


          (ba)sh работает под любой *nix системой и достаточно хорош, чтобы альтернативы не развивались за ненадобностью. Не идеален, а именно достаточно хорош для 99% задач.


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


          1. SirNickolas00
            07.10.2016 00:01
            +4

            Вы только что очень ёмко описали Xonsh!

            foo | bar | baz > result
            def clear(d):
                pushd @(d)
                rm `\w+\d{,3}\.log`
                popd
                print(d, "is cleared")
            clear($HOME)
            aliases["ll"] = "ls -Al"
            ll src
            sudo -H pip3 install PyYAML
            import yaml
            doc = yaml.load($(foo | bar | baz))
            git commit -m @(doc["message"]) and git push
            


            1. selivanov_pavel
              07.10.2016 18:21

              Хм, интересно, надо пощупать. Спасибо за подсказку


          1. ZyXI
            07.10.2016 10:54
            +2

            Почему вы решили, что альтернативы не развиваются? Сейчас самая популярная тройка — fish, zsh и bash (интерактивные); dash и bash — основные для скриптов. Всё, кроме dash, развивается. Zsh в ряде дистрибутивов вообще стоит по?умолчанию. Ещё в список популярных оболочек хотят, как минимум, powershell и xonsh (xonsh мне в этом отношении нравится больше, но сейчас я использую zsh).


            При этом, хотя все упомянутые оболочки явно испытывают(ли) влияние bash и POSIX shell, POSIX скрипты работают только в dash, zsh и bash, а остальные несовместимы. Сам zsh имеет огромное количество возможностей, отсутствующих (сложных в реализации/требующих много кода) во всех других оболочках.


            1. AVX
              08.10.2016 19:17

              Пробовал zsh лет 7 назад, очень понравилось. Но не прижилось — на многих дистрибутивах, которые попадались (на работе в разных железках, терминалах, киосках и тонких клиентах; и на компах обычных юзеров) zsh просто не было, а скрипты были в том или ином виде и разной степени сложности и критичности. При этом ba(sh) был везде, поэтому именно на нём и привык. Даже при всех плюшках, которые есть в zsh. Другая причина — это лень. Ведь 90-99% задач сравнительно легко решаются с bash, и всякие возможности zsh так и остаются невостребованы, а ведь их ещё нужно изучить, запомнить, и не забыть применить в нужный момент!


    1. Dolbe
      06.10.2016 15:18

      Ну вот Микрософты предлагают PowerShell…


      1. marcor
        07.10.2016 13:38
        +2

        Как было подмечено выше, баш удобен потому что содержит сотню программ от grep до sox и perl, связи между которыми делают язык *sh гибким. «Всегда есть ещё один способ сделать X».

        PowerShell на мой взгляд недотягивает. Через консоль по-быстрому задеплоить солюшн — ок. Попытка сделать клон *sh — явно нет: слишком многого недостаёт.


        1. Dolbe
          07.10.2016 17:45

          Честно Вам скажу, с sh знаком очень поверхностно, не знаю всех тонкостей, но вот PowerShell, как мне кажется, очень хорош, если ты, к примеру, C# программист, ведь PoSh — это чистый .Net. Считаю это одним из преимуществ, как и то, что там можно оперировать объектами.


          PS Для хейтеров — попрошу мой комментарий не воспринимать как негативную критику *sh. Всего лишь считаю, что преимущества есть у всего.


          1. AVX
            08.10.2016 19:22

            Я с точки зрения линуксоида с почти 10-летним стажем люблю bash, но как виндузятник с over9000 стажем люблю power shell, хоть и пользуюсь им всего-то пару лет (ну или 3 с чем-то, не помню). Для каждой задачи нужен свой инструмент, и логично было бы, чтобы в bash появились возможности работать с объектами, как в PS, а в PS были бы все возможности bash (бОльшая часть их там уже есть). Конечно, есть питон и много чего ещё, на чём можно что угодно реализовать, всё же — powershell весьма мощная штука.


    1. lockywolf
      06.10.2016 15:35

      TCSH пробовали?


    1. sshikov
      06.10.2016 20:40

      Дистрибутив-то вам зачем? Я вполне понимаю ваше отношение в целом, но поверьте, что любой интерпретируемый язык прикручивается одной строкой в начале кода.


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


    1. DistortNeo
      06.10.2016 22:05

      Ну почему же нет попыток? Perl и Python вполне успешно используются.


  1. Self_Perfection
    06.10.2016 14:06

    1)

    while IFS= read -r -d '' file; do
        # Arbitrary operations on "$file" here
    done < <(find /some/path -type f -print0)
    

    http://stackoverflow.com/a/8677566/1398863

    А вообще торт!


    1. izzholtik
      06.10.2016 14:11
      -1

      Программы, портящие STDIN, сломают такой цикл, будучи вызваны изнутри него?


      1. Self_Perfection
        06.10.2016 14:15

        Я не понимаю, что такое «программа, портящая stdin»

        Если что, вот такой вариант

        while IFS= read -r -d '' FNAME; do
            echo "$FNAME" |  sed 's/foo/bar/'
        done < <(find /some/path -type f -print0)
        

        отработает правильно. Это отвечает на вопрос?


  1. inkvizitor68sl
    06.10.2016 14:43
    +6

    > Mail.Ru Group
    Ого. А вы всё продолжаете и продолжаете отмываться %)


    1. lolipop
      06.10.2016 19:08
      +4

      хотел тоже удивиться, но обнаружил, что это перевод.


      1. inkvizitor68sl
        06.10.2016 19:09
        +3

        Да даже если так.


  1. Sild
    06.10.2016 14:47
    +10

    Есть очень полезная штука, называется codestyle.
    Есть такой и для баша. Например, гугловский: https://google.github.io/styleguide/shell.xml
    Используя его, большинство проблем исчезает. Язык как язык. Свои плюсы, свои минусы. Есть класс задач, который лучше него так никто и не решает.

    А вообще автор клёвый чувак и у него очень полезные заметки по юниксовым утилитам.


    1. kalterfive
      06.10.2016 14:59

      Неплохо. Ещё есть некий "framework" для bash – думаю, продвинутым пользователям будет очень занятно почитать код =)


    1. netrusov
      06.10.2016 17:00
      +8

      Хотел бы добавить, что существует прекрасная утилита ShellCheck, которая доступна онлайн и из командной строки (не нашел на Хабре упоминаний о ней).
      Дает очень полезные советы после анализа кода. Используя её, узнал все те «best practice», которые описаны в этой статье.

      Статья очень полезная, спасибо за перевод, с удовольствием прочитал её всю до конца. Я не видел русскоязычном сегменте подобных статей, а пару раз очень надо было дать ссылку на руководство по Bash людям, которые плохо знают английский. Теперь у меня такая ссылка есть.


      1. Sild
        06.10.2016 17:08

        Да это же просто потрясающе! Ещё и опенсорсная!


  1. kalterfive
    06.10.2016 14:56
    +1

    Хороший перевод. От себя могу только добавить, что большинство ошибок может предотвратить set -e:


    #!/bin/bash -e
    
    …

    Кроме того, слышал про gdb?like дебаггер. bashdb, не он?


    1. naudhr
      06.10.2016 16:23
      +1

      #!/bin/bash
      set -euo pipefail
      
      ...
      Поможет предотвратить ещё больше ошибок.


  1. Shamov
    06.10.2016 15:07
    -1

    Очень хотел бы встретить когда-нибудь человека, у которого имена файлов содержат перевод строки. Задушил бы сразу. Собственными руками. И с удовольствием смотрел бы на то, как из него медленно уходит жизнь, которой он не заслуживает.


    1. Sild
      06.10.2016 15:15

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


      1. Shamov
        06.10.2016 16:26

        Я не имел в виду ситуацию, когда это явная ошибка. Речь только о том, когда такое делается специально.


    1. sleeply4cat
      06.10.2016 23:22
      +4

      Очень хотел бы встретить когда-нибудь программиста, из-за которого у меня регулярно возникают папки с именами «Program» и «Рабочий» под виндой и «Домашняя» под линуксом.


  1. hdfan2
    06.10.2016 15:32
    +1

    C-программисты уже знакомы с &&. Bash использует такое же упрощённое вычисление.

    Насколько я в курсе, это называется «ленивое вычисление» (когда вторая часть булевского выражения вычисляется, только если нужно; если же из первой уже следует результат, то вторая не вычисляется).


  1. saggid
    06.10.2016 15:33
    +2

    Баш полезен, бесспорно, например для вызова всяких утилит и для работы с файловой системой. Что также удобно: его простые команды прекрасно вызываются через любой интерпретируемый язык более высокого уровня (php/python/ruby). На мой взгляд, правильно писать основную логику скрипта на чём-то другом, помимо баша. А единичные команды можно вызывать уже с помощью bash. Получается очень удобно в итоге.


  1. Cheater
    06.10.2016 15:43
    +1

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

    … И при этом ваша команда выполнится не только для файлов непосредственно в текущей директории, но и рекурсивно для всех файлов внутри неё. Вряд ли человек, написавший (в случае с mp3) изначально команду с "*.mp3", этого хочет.

    find. -type f -maxdepth 1


    1. AVX
      06.10.2016 16:43
      +1

      Ну почему же. Я так неоднократно делал. Например, есть папка с музыкой, и надо, например, конвертировать все mp3 в что-то ещё (или пережать). Конечно, папка содержит вложенные, где тоже музыка. Естественно, maxdepth нужно использовать, если только в этой папке надо. Но, ИМХО, чаще нужно именно массово и во вложенных.

      P.S. Статья интересная, но не осилил, положил в закладки, потом поизучаю. Правда, часть этих граблей уже испытал на себе раньше…


  1. ALexhha
    06.10.2016 17:33
    -2

    А почему в 1м примере не использовать просто

    ls *.mp3 | while read curr_file
    do
       cp "${curr_file}" "./dst/${curr_file}"
    done
    

    Даже при условии не верного отображения имени, команда отрабатывает корректно
    $ ls
    copy.sh  dst  ?????? ????*.txt
    
    $ copy.sh
    $ cd dst/
    $ cat Привет\ Хабр\*.txt
    Hello Habr!
    

    Причем, что интересно, то find вообще не находит файл по маске
    $ find . -type f -name "*.txt" -print
    

    Или есть таки какие то подводные камни?


    1. Self_Perfection
      06.10.2016 17:59
      +2

      Помимо нескольких разжёванных в статье опасностей, которые таит конструкция, которую вы написали, вам действительно кажется, что ваш вариант с вызовом внешней программ ls проще, чем
      for curr_file in *.mp3; do
      :
      done

      ?


      1. throttle
        26.01.2017 18:12
        +8

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

        Так пишете, как будто отсутствие в соцсетях — это что-то плохое.


        1. immaculate
          27.01.2017 01:31
          +3

          Есть аккаунты в нескольких популярных соцсетях (хотя на самом деле, уже нет смысла в нескольких, так как вся активность перетекла в одну единственную, тот же VK стал центром репостов картинок), но практически перестал ими пользоваться. Чем дольше не захожу, тем меньше хочется.


          Причин много. Считаю, что соц. сети отвлекают (другие отвлечения не отнимают столько времени и сил — если отвлечься на FB в начале дня, то такой день будет намного менее продуктивным по сравнению с днями без FB), поощряют отрицательные качества в личности (самолюбование, эгоизм). Кроме того, не хочу ничего читать о политике, причем неважно кто на какой стороне и в какой стране. В FB каждый день попадаются разговоры о новостях и политике, не в постах, так в комментариях.


          Кроме того, есть претензия к самой сути FB. FB переманил аудиторию у которых были страницы и блоги. Но сам не предоставляет нормальных средств структурирования информации, не дает индексировать контент поисковикам извне, не имеет нормального поиска сам. Их задача: чтобы пользователи постоянно постили короткие заметочки с парой фоточек, и постоянно обновляли ленту. Чем сложнее найти уже существующую информацию, тем больше времени люди будут проводить в FB. В итоге, он превращается в черную дыру информации в интернете.


          Если раньше человек вел блог, в котором описывал, например, ремонт автомобилей определенной марки. С подробными фотографиями, текстами, по которым легко было повторить самостоятельный ремонт или чему-то научиться, то сейчас такой человек переходит в FB, где постит пару невнятных фоточек и подпись: «Я крутой, потому что поменял масляный насос». И даже эту бесполезную информацию почти невозможно найти, потому что один раз она появится в ленте, а потом исчезнет бесследно, под валом фоточек-демотиваторов-цитаточек.


          Только за одно это считаю оправданным бойкот facebook.


        1. znsoft
          27.01.2017 10:04
          +1

          а чем github не соц. сеть?


          1. ZyXI
            06.10.2016 19:08

            Не понял, что имелось ввиду под «read $curr_file съест лишние кавычки»? bash -c 'echo ''"abc def"'' | while read t ; do printf "<%s>\n" "$t"; done' выдаёт <"abc def">.


            1. Self_Perfection
              06.10.2016 19:20
              +1

              /tmp/test2$ touch 'two words'
              /tmp/test2$ ls
              two words
              /tmp/test2$ ls --quoting-style=shell
              'two words'


              Если --quoting-style=shell (кажется это умолчание для свежих версий ls), то в $curr_file попадут одинарные кавычки (т.е. read их прочитает/съест наравне с самим именем файла)


          1. ALexhha
            06.10.2016 19:15

            Код, который вы показали в предыдущем комментарии явно отличается от того, что вы пробовали выполнять.

            код одинаковый, просто проверял на txt. Теперь понял. Но проблема с find и неправильной кодировкой остается в силе. Я бы сделал примечание.

            Какой пример?

            вот этот
            $ touch $'two\nlines.mp3'
            $ touch $'myfile\n'
            


    1. develop7
      07.10.2016 10:51

      $ shellcheck myscript
      
      Line 1:
      ls *.mp3 | while read curr_file
      ^-- SC2012: Use find instead of ls to better handle non-alphanumeric filenames.
         ^-- SC2035: Use ./*glob* or -- *glob* so names with dashes won't become options.
                       ^-- SC2162: read without -r will mangle backslashes.


      1. AVX
        08.10.2016 19:36

        Спасибо за наводку на shellcheck!
        Правда, не нашёл в дистрибутиве (mageia 5) такого :(
        Видимо, нужно подождать, или же компилировать самому.


  1. ZyXI
    06.10.2016 19:50
    +2

    К проблемам с именами файлов: попытайтесь использовать CMake, чтобы собрать что?либо в каталоге bu;ld. Узнаете, что *sh даже близко не стоит к CMake по «удобству» использования имён с «нестандартными» символами внутри.


    Собственно, CMake я вспомнил, потому что


    1. Может быть и хуже.
    2. При этом между *sh и ©Make я не видел чего?либо, что было лучше систем сборок, но хуже оболочек (хотя нет, если рассматривать tcsh отдельно от POSIX?совместимых оболочек, то он как раз окажется где?то между).


  1. AVX
    06.10.2016 22:51
    +1

    Возникла мысль, а нет ли примеров злонамеренно созданных имен файлов (набора файлов с произвольным содержимым, но с нужными именами), чтобы ошибочно написанный скрипт выполнил заранее заданное действие?
    Например, хакер получил доступ (неважно как) к какой-либо папке, которую обрабатывает (например, по расписанию или по событию какому-то) какой-то скрипт. Может ли хакер создать такой набор (или даже 1 файл) файлов, чтобы при обработке тем скриптом выполнилась заранее заданная команда?
    Предположим, что хакер не знает содержимое скрипта, но знает что этот скрипт делает (ну, к примеру, копирует содержимое папки куда-то, или наоборот, чистит папку, или конвертирует содержимое файлов в другой формат...) Теоретически, можно ведь предположить, что администратор ресурса допустил одну или несколько ошибок вроде описанных тут и использовать это.


  1. stranger777
    07.10.2016 10:25

    Большое спасибо за труд: это было крайне актуально как раз в последние дни. А вопрос в Advanced Bash Scripting Guide либо освещён плохо, либо я не нашёл, либо не понял. Пока не дочитал, кладу в избранное. Ещё раз огромное спасибо!


  1. stranger777
    07.10.2016 13:24

    И ещё… я думаю, линукс гибок ровно настолько, чтобы вместо беспокойства о проблемах совмести просто взять и собрать нужную оболочку, будь то bash, ksh или прочие. А если речь идёт о корпоративной безопасности (нет доступа в сеть, ограничены возможности установки, компилирования и т.п.), то проще обосновать применение конкретного софта (мне кажется), чем вот так жестоко носится с совместимостью.


  1. POPSuL
    07.10.2016 14:26
    +1

    Везде где только могу использую zsh, но когда пишу скрипты, всегда пишу #!/bin/bash (или иногда #!/bin/sh)...


    Спасибо за отличный перевод! Узнал для себя несколько новых штук)


    1. prefrontalCortex
      07.10.2016 23:45

      Ведущие собаководы рекомендуют

      #!/usr/bin/env sh
      

      ибо в /bin sh может и не быть.


  1. celebrate
    07.10.2016 15:42

    У моего коллеги был печальный пример, как не надо писать скрипт на баше. Был сервак IBM AIX, на который через аналог rc.local была примонтирована в /mnt NFS-шара, в которую писала свои логи приложуха. Т.к. приложуха писала много, то для защиты от переполнения шары был написан скрипт в кроне, который периодически эту папку очищал.

    #!/bin/bash
    cd /mnt/logs
    rm -rf *
    

    Угадайте что произошло, когда упала сеть и сервак ребутнули?


    1. kalterfive
      07.10.2016 16:03
      +1

      1. Сеть не работает => NFS не монтируется;
      2. NFS должна монтироваться в /mnt => директории /mnt/logs не существует;
      3. cd: /mnt/logs: No such file or directory;
      4. … =)

      Этот пример скорее показывает важность правильной настройки инициализации.


      1. celebrate
        07.10.2016 17:04

        Да, верно. В AIX отсутствует защита от rm -rf * из корня, поэтому сервак стал девственно чист.


      1. mikhailidi
        08.10.2016 16:52

        А вот если бы NFS монтировали по верх пустой директории /mnt/logs все были бы живы.


        1. kalterfive
          08.10.2016 18:36

          Разумеется. Но эта мера ничем не обусловлена в условиях нормальной работы сервера.


    1. AVX
      08.10.2016 19:30
      -1

      всего-то можно было написать что-то вроде
      cd /mnt/logs && rm -rf *
      Ну или массу других вариантов можно придумать. Конечно, пока «петух не клюнет» — админ не пошевелится :)


  1. vmspike
    08.10.2016 20:27
    +1

    43. somecmd 2>&1 >>logfile
    Для этого есть удобный башизм:
    somecmd &>>logfile
    или если с перезаписью:
    somecmd &>logfile
    В последней ссылке об этом есть, но, думаю, полезно явно озвучить такую плюшку.