Комментарии
НЛО прилетело и опубликовало эту надпись здесь
По моему bash это вообще одна сплошная ошибка. Неужели в линукс сообществе до сих пор нет попыток прикрутить вместо этого ископаемого монстра нормальный современный интерпретируемый язык программирования?
Просто взять и сделать дистрибутив целиком без bash.
Да бросьте, просто у вас стокгольмский синдром по отношению к bash ещё не до конца выработался.
linux свободен. Любой пользователь может снести bash и поставить туда собственный myNewBash. Собсно, как и было с zsh, csh и так далее, семейства их.

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

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


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


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

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

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


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


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

Вы только что очень ёмко описали 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

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


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

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

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

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


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

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

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


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

Ну почему же нет попыток? Perl и Python вполне успешно используются.
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

А вообще торт!
НЛО прилетело и опубликовало эту надпись здесь
Я не понимаю, что такое «программа, портящая stdin»

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

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

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

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

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

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

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

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


#!/bin/bash -e

…

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

#!/bin/bash
set -euo pipefail

...
Поможет предотвратить ещё больше ошибок.
Очень хотел бы встретить когда-нибудь человека, у которого имена файлов содержат перевод строки. Задушил бы сразу. Собственными руками. И с удовольствием смотрел бы на то, как из него медленно уходит жизнь, которой он не заслуживает.
Файл может генерироваться программно в зависимости от ввода пользователя, экранирование спец символов забыли провернуть, вот и перенос строки.
Конечно, можно гнать на тех, кто обрабатывает данные. А можно просто хорошо делать свою работу, и не беспокоиться об опасных форматах.
Я не имел в виду ситуацию, когда это явная ошибка. Речь только о том, когда такое делается специально.
НЛО прилетело и опубликовало эту надпись здесь
C-программисты уже знакомы с &&. Bash использует такое же упрощённое вычисление.

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

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

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

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

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

P.S. Статья интересная, но не осилил, положил в закладки, потом поизучаю. Правда, часть этих граблей уже испытал на себе раньше…
А почему в 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

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

?
Скорее это был вопрос относительно "Используете find, например, в совокупности с -exec", а так же 3го пункта — "Утилита ls может искромсать имена файлов". Как видно в примере выше, find не корректно работает. Первым же 3м пунктам такая конструкция не подвержена.

Но если последнее имя файла в списке заканчивается новой строкой, то `...` или $() уберут и его в придачу.

вот здесь не очень понял, что значит имя файла заканчивается новой строкой. Можно привести пример?

Какой пример? В качестве имени файла может фигурировать любая строка, не содержащая нулевой байт и не являющаяся пустой (в зависимости от ФС, может быть ещё ограничение на длину имени файла). В том числе, строка вида $'foo\nbar\n'. Конечно, используемые программы это несколько ограничивают (те же имена вида -t лучше записывать как ./-t, а file:///tmp/test может значить как ./file:/tmp/test, так и URL), но open() в libc сожрёт практически всё, поэтому после open("foo\nbar\n", O_CREAT) вы вполне можете наблюдать имя foo$'\n'bar$'\n'. Сама оболочка и большинство используемых из неё программ ограничивают максимум возможные начала имени файла, а не его конец, поэтому вызывать open самому не нужно: touch $'foo\nbar\n' не хуже.

Код, который вы показали в предыдущем комментарии явно отличается от того, что вы пробовали выполнять. Хотя бы потому что в цикле глоббинг по *mp3, а в качестве доказательства успешного срабатывания вы используете *txt файл. Вот вам тупое демо, выполните в своём шелле 1 в 1 и просветлитесь

mkdir -v test
cd test
touch $'two\nlines.mp3' #demo for problem 1. bash specific
ls *.mp3 | while read curr_file; do echo "= $curr_file ="; done


Ещё обратите внимание на опцию --quoting-style= у ls. В каких-то версиях ls по-умолчанию окружает кавычками имена файлов с пробелами, так что read $curr_file съест лишние кавычки.

Сделать файл с именем, заканчивающимся new line:
touch $'myfile\n'

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

/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 их прочитает/съест наравне с самим именем файла)
Код, который вы показали в предыдущем комментарии явно отличается от того, что вы пробовали выполнять.

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

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

вот этот
$ touch $'two\nlines.mp3'
$ touch $'myfile\n'
$ 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.
Спасибо за наводку на shellcheck!
Правда, не нашёл в дистрибутиве (mageia 5) такого :(
Видимо, нужно подождать, или же компилировать самому.

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


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


  1. Может быть и хуже.
  2. При этом между *sh и ©Make я не видел чего‐либо, что было лучше систем сборок, но хуже оболочек (хотя нет, если рассматривать tcsh отдельно от POSIX‐совместимых оболочек, то он как раз окажется где‐то между).
Возникла мысль, а нет ли примеров злонамеренно созданных имен файлов (набора файлов с произвольным содержимым, но с нужными именами), чтобы ошибочно написанный скрипт выполнил заранее заданное действие?
Например, хакер получил доступ (неважно как) к какой-либо папке, которую обрабатывает (например, по расписанию или по событию какому-то) какой-то скрипт. Может ли хакер создать такой набор (или даже 1 файл) файлов, чтобы при обработке тем скриптом выполнилась заранее заданная команда?
Предположим, что хакер не знает содержимое скрипта, но знает что этот скрипт делает (ну, к примеру, копирует содержимое папки куда-то, или наоборот, чистит папку, или конвертирует содержимое файлов в другой формат...) Теоретически, можно ведь предположить, что администратор ресурса допустил одну или несколько ошибок вроде описанных тут и использовать это.
Я так через tar -czvf * в бэкапном скрипте как-то на досуге восстанавливал рутовый доступ доступ после свалившего в закат предыдущего админа на сервере, который было крайне нежелательно ребутать.
Создал файл ;chpasswd что-то там, подождал ночи, готово.
Большое спасибо за труд: это было крайне актуально как раз в последние дни. А вопрос в Advanced Bash Scripting Guide либо освещён плохо, либо я не нашёл, либо не понял. Пока не дочитал, кладу в избранное. Ещё раз огромное спасибо!
И ещё… я думаю, линукс гибок ровно настолько, чтобы вместо беспокойства о проблемах совмести просто взять и собрать нужную оболочку, будь то bash, ksh или прочие. А если речь идёт о корпоративной безопасности (нет доступа в сеть, ограничены возможности установки, компилирования и т.п.), то проще обосновать применение конкретного софта (мне кажется), чем вот так жестоко носится с совместимостью.

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


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

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

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

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

Да, верно. В AIX отсутствует защита от rm -rf * из корня, поэтому сервак стал девственно чист.
Можно было и безо всяких уловок, и короче, и безопасно:
rm -rf /mnt/logs/*
А вот если бы NFS монтировали по верх пустой директории /mnt/logs все были бы живы.

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

всего-то можно было написать что-то вроде
cd /mnt/logs && rm -rf *
Ну или массу других вариантов можно придумать. Конечно, пока «петух не клюнет» — админ не пошевелится :)
43. somecmd 2>&1 >>logfile
Для этого есть удобный башизм:
somecmd &>>logfile
или если с перезаписью:
somecmd &>logfile
В последней ссылке об этом есть, но, думаю, полезно явно озвучить такую плюшку.
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.