30 KiB
Задания
Масштабируемый подход к управлению системой требует, чтобы административные изменения были структурированы, воспроизводимы и тиражируемы на нескольких компьютерах. В реальном мире это означает, что такие изменения должны быть сделаны программным обеспечением, а не выполняться администраторами по инструкциям или, что еще хуже, по памяти.
Сценарии стандартизируют административную работу и освобождают время администраторов для более важных и интересных задач. Сценарии также служат своеобразной бесплатной документацией, поскольку в них записываются шаги, необходимые для выполнения той или иной задачи. Основной альтернативой сценариям для сисадминов является использование систем управления конфигурацией (Ansible, Puppet, Chef). На практике большинство администраторов используют комбинацию сценариев и управления конфигурациями. Каждый из этих подходов имеет свои преимущества, и они хорошо сочетаются друг с другом.
Сценарии оболочки — это также следующий шаг в сложности комбинирования команд после конвейеров для обработки данных. Большинство оболочек имеют собственный язык сценариев с переменными, потоком управления и собственным синтаксисом. Сценарии оболочки отличаются от других языков программирования сценариев тем, что они оптимизированы для выполнения задач, связанных с оболочкой. Таким образом, создание конвейеров команд, сохранение результатов в файлы и чтение из стандартного ввода являются примитивами в сценариях оболочки, что делает их более простыми в использовании, чем языки сценариев общего назначения.
Далее задания посвящены сценариям bash
, поскольку они наиболее распространены.
При написании скриптов используйте https://www.shellcheck.net/, чтобы проверить их на ошибки до запуска.
0.
Каждый процесс имеет как минимум три канала связи: стандартный ввод STDIN
, стандартный вывод STDOUT
и стандартный вывод ошибок STDERR
. Процессы изначально наследуют эти каналы от своих родителей, поэтому они не обязательно знают, куда они ведут. Они могут подключаться к окну терминала, файлу, сетевому соединению или каналу, принадлежащему компьютеру, или каналу, принадлежащему другому процессу, и т.д.
В UNIX и Linux реализована унифицированная модель ввода-вывода, в которой каждый канал именуется небольшим целым числом, называемым дескриптором файла. Точный номер, присвоенный каналу, обычно не имеет значения, но STDIN
, STDOUT
и STDERR
гарантированно соответствуют файловым дескрипторам 0
, 1
и 2
, поэтому можно смело называть эти каналы по номерам. В контексте интерактивного окна терминала STDIN
обычно считывает данные с клавиатуры, а STDOUT
и STDERR
записывают вывод на экран.
Многие традиционные команды UNIX принимают входные данные из STDIN
и записывают их в STDOUT
. Сообщения об ошибках они записывают в STDERR
. Это соглашение позволяет объединять команды, как строительные блоки, для создания конвейеров.
Оболочка интерпретирует символы <
, >
и >>
как инструкции по перенаправлению входных или выходных данных команды в файл или из файла. Символ <
соединяет STDIN
команды с содержимым существующего файла. Символы >
и >>
перенаправляют STDOUT
. Символ >
заменяет существующее содержимое файла, а >>
добавляет в конец файла.
Чтобы перенаправить и STDOUT
, и STDERR
в одно и то же место, используйте символ >&
или 2>&1
. Чтобы перенаправить только STDERR
, используйте 2>
.
# find / -name core
# find / -name core 2> /dev/null
Следующий синтаксис <( CMD )
выполнит CMD
, поместит вывод во временный файл и заменит <()
именем этого файла. Это полезно, когда команды ожидают, что значения будут переданы через файл, а не STDIN
. Например, diff <(ls foo) <(ls bar)
покажет различия между файлами в каталогах foo и bar.
1.
Кроме результатов в STDOUT
и ошибок в STDERR
сценарии возвращают код возврата. Код возврата или статус выхода — это то, как скрипты/команды должны сообщать о том, как прошло выполнение. Значение 0
обычно означает, что все прошло нормально; все, что отличается от 0
, означает, что произошла ошибка.
Коды выхода можно использовать для условного выполнения команд с использованием операторов &&
(и
) и ||
(или
). Команды также можно разделять в пределах одной строки с помощью точки с запятой ;
, если они должны выполниться независимо от возвращаемого кода. Истинная программа всегда будет иметь код возврата 0
, а ложная команда всегда будет иметь код возврата от 1
до 255
.
Выполните следующие примеры:
false || echo "Oops, fail"
true || echo "Will not be printed"
true && echo "Things went well"
false && echo "Will not be printed"
true ; echo "This will always run"
false ; echo "This will always run"
*
Что произойдёт в данном случае?
(echo red; echo green 1>&2) | echo blue
Ответ в [1-2].
2.
Чтобы назначить переменные в bash, используйте синтаксис foo=bar
. Чтобы получить доступ к значению переменной запишите знак $
перед её именем $foo
. Обратите внимание, что foo = bar
не будет работать, так как интерпретируется как вызов программы foo
с аргументами =
и bar
. Как правило, в сценариях оболочки символ пробела выполняет разделение аргументов. Поначалу такое поведение может сбивать с толку, поэтому всегда проверяйте его.
Стандартных правил именования переменных оболочки не существует, но имена, набранные заглавными буквами, обычно указывают на переменные среды или переменные, считываемые из глобальной конфигурации. Чаще всего локальные переменные обозначаются всеми строчными буквами, а их компоненты отделяются друг от друга разделены символами подчеркивания. Имена переменных чувствительны к регистру.
Строки в bash могут быть определены парными символами '
или "
, но они не эквивалентны. Строки, разделенные '
, являются литеральными строками и не интерпретируют bash
выражения, тогда как строки с "
будут. Для экранирования кавычек используется символ обратного слеша \
.
Напишите .sh
скрипт в котором в переменную date
присваивается значение текущей даты, в переменную claim
- строка "snow is white"
. После вызова скрипт должен выводить строку, используя переменные date
и claim
:
Ср 14 сен 2022 14:50:46 +04: The claim that “snow is white” is true if and only if snow is white.
Сделайте файл исполняемым для владельца и вызовите указанием полного или относительного пути к нему.
3.
В отличие от других языков сценариев, bash
использует множество специальных переменных для ссылки на аргументы, коды ошибок и другие важные переменные. Ниже приведен список некоторых из них. Более полный список можно найти здесь (https://tldp.org/LDP/abs/html/special-chars.html или в переводе на русский здесь https://www.opennet.ru/docs/RUS/bash_scripting_guide/c301.html.
Используйте следующие специальные переменные в скрипте:
$0
- имя скрипта,$1
до$9
- аргументы скрипта,$@
- все аргументы,$#
- количество аргументов,$?
- возвращаемый код предыдущей команды,$$
- идентификатор процесса текущего скрипта,$_
- последний аргумент последней команды (в терминале попробуйте нажатьEsc .
илиAlt+
).
В терминале также доступна команда !!
. Попробуйте sudo !!
, если забыли, что команде требуются права суперпользователя). Команда !!
будет работать в скрипте только, если в начале присутствует set -H
, включающая работу с историей. По умолчанию это возможность включена для интерактивного взаимодействия.
Напишите скрипт, который создает каталог, а затем переходит в него, создаёт в нём файл и записывает в него значения всех специальных переменных. Путь к каталогу должен быть передан в качестве первого аргумента.
4.
Как и в большинстве языков программирования, bash
поддерживает методы управления потоком, включая условные и циклические операторы if
, case
, while
и for
. Точно так же в bash
есть функции, которые принимают аргументы и могут с ними работать. Вот пример определения функции:
echo_arguments () {
echo "First three arguments: $0 $1 $2 $3, Argument count: $#, All arguments: $@"
}
По умолчанию переменная глобальна для скрипта, даже если определена внутри функции. Чтобы сделать переменную локальной, используйте синтаксис local variable
внутри функции.
Напишите аналогичный предыдущему заданию скрипт, но оберните все действия в функцию, а в конце скрипта выполните эту функцию. Проверьте работу скрипта. Доступна ли функция за пределами контекста скрипта?
Заведите внутри функции глобальную a
и локальную переменную local b
. Вызовите функцию в скрипте. Выведите обе переменные на экран внутри функции после присвоения значений переменным и после вызова функции.
5.
Функции выполняются в текущей среде оболочке, тогда как скрипты выполняются в своем собственном процессе.
Проверьте, что функции могут изменять переменные среды, например, изменить текущий каталог, тогда как скрипт не может. Напишите скрипт, в котором инициализируются две переменные A
и B
. Перед одной из них указывается export
. Выполните скрипт и посмотрите на выдачу команды env
. Выполните файл командой source
(документация в source --help
и секции встроенных команд man bash
). Какие переменные экспортировались в оболочку?
6.
Попробуйте условные выражения в скрипте в if
конструкции:
if [[ условное выражение ]]; then
echo "True"
fi
6.5 Пропущенные задания
https://ryanstutorials.net/bash-scripting-tutorial/bash-if-statements.php http://mywiki.wooledge.org/BashFAQ/031
https://ryanstutorials.net/bash-scripting-tutorial/bash-loops.php
https://ryanstutorials.net/bash-scripting-tutorial/bash-functions.php
7.
Напишите скрипт, который проходит по всем файлам, переданным в качестве аргументов, и ищет в них строку hello
. Перенаправьте grep STDOUT
и STDERR
в специальный файл /dev/null
. Для каждого файла в if
создайте файл с содержимым hello
, если grep
завершился с ошибкой.
8.
Установите пакет shellcheck
и проверьте написанный вами ранее скрипт одноименной командой.
9.
Bash отличается от других языков программирования в следующем примере.
#!/usr/bin/env bash
mv ./*.txt /tmppp
echo "success!"
Как правило, если происходит ошибка, то программа останавливается. Но в данном случае скрипт продолжит работу, несмотря на ошибку в mv
.
set -e
был попыткой добавить в оболочку "автоматическое обнаружение ошибок" [6]. Его целью было заставить оболочку прерываться при возникновении ошибки, чтобы не приходилось ставить || exit 1
после каждой важной команды. Однако на практике это всё равно работает не очень хорошо.
Попробуйте выполнить следующие скрипты. Воспользуйтесь shellcheck
, чтобы узнать в чём проблема с этими примерами.
9.0 Почему в первой исправленной версии скрипт останавливается, а во второй нет?
#!/usr/bin/env bash
set -e
mv ./*.txt /tmppp
echo "success!"
#!/usr/bin/env bash
set -e
f() {
mv ./*.txt /tmppp
echo "success!"
}
f || echo "failed!"
Ответ - во втором случае, оператор ||
деактивирует set -e
. Это задокументированное поведение.
9.1. Почему в этом примере не работает печать?
#!/usr/bin/env bash
set -e
i=0
let i++
echo "i is $i"
9.2. Почему этот вариант иногда работает? В каких версиях bash он работает, а в каких - нет?
#!/usr/bin/env bash
set -e
i=0
((i++))
echo "i is $i"
9.3. Почему эти два сценария не идентичны?
#!/usr/bin/env bash
set -e
test -d nosuchdir && echo no dir
echo survived
#!/usr/bin/env bash
set -e
f() { test -d nosuchdir && echo no dir; }
f
echo survived
9.4. Почему эти два сценария не идентичны?
#!/usr/bin/env bash
set -e
f() { test -d nosuchdir && echo no dir; }
f
echo survived
#!/usr/bin/env bash
set -e
f() { if test -d nosuchdir; then echo no dir; fi; }
f
echo survived
9.5. При каких условиях это приведет к отказу?
#!/usr/bin/env bash
set -e
read -r foo < configfile
Рекомендация проста: не используйте set -e
. Вместо этого добавьте собственную проверку ошибок.
10.
Cкрипты не обязательно должны быть написаны на bash
для вызова из терминала. Ядро определяет, что сценарий нужно выполнять с помощью другого интерпретатора вместо командой оболочки, если в начало скрипта включена строка shebang
. Хорошей практикой является написание строк shebang
с помощью команды env
, которая будет разрешаться везде, где эта команда находится в системе, что повышает переносимость ваших сценариев. Например, чтобы определить местоположение python
, env
будет использовать переменную среды PATH
при записи #!/usr/bin/env python
. Если у вас установлено несколько версий Python, то команда /usr/bin/env
обеспечит использование интерпретатора, который стоит первым в $PATH
окружения. Альтернативой может быть жесткая привязка вида #!/usr/bin/python
что тоже нормально, но менее гибко.
Напишите скрипт, который выполняет следующий код:
import sys
for arg in reversed(sys.argv[1:]):
print(arg)
11.
Скажем, у вас есть команда, которая редко дает сбой. Чтобы отладить её, вам нужно зафиксировать её выходные данные, но это может занять много времени, чтобы поймать неудачный запуск. Напишите сценарий bash
, который запускает следующий сценарий до тех пор, пока он не завершится аварийно, записывает стандартный вывод и потоки ошибок в файлы и печатает все в конце. Бонусный балл, за то, что вы можете сообщить, сколько запусков потребовалось для сбоя сценария.
#!/usr/bin/env bash
n=$(( RANDOM % 100 ))
if [[ n -eq 42 ]]; then
echo "Something went wrong"
>&2 echo "The error was using magic numbers"
exit 1
fi
echo "Everything went according to plan"
12.
Выберите любые две машины под вашим контролем, имеющие доступ друг к другу по сети, и создайте на каждой машине папку /data/
с заданными правами и владельцем -rwxr-xr-x root root
. Выполните задание в соответствие с лучшими практиками для написания скриптов. В задачи скрипта входят:
- создание пользователя на двух машинах с домашними директориями и интерпретатором
bash
по умолчанию, - создание для пользователя папки в
/data/
с доступом только её владельцу, - создание ссылки в домашней директории на директорию в
/data/
, - настройки беспарольного перемещения между машинами:
- генерация пользователю ключа
ed25519
, - рассылка по машинам,
- регистрация ключа в списке авторизованных.
- генерация пользователю ключа
Лучшие практики написания скриптов
- При запуске с несоответствующими аргументами скрипты должны выводить сообщение об использовании и завершать работу. Реализуйте таким же образом и
--help
. - Производите валидацию входных данных и проверку правильности полученных значений. Например, перед выполнением команды
rm -rf
по вычисленному пути можно попросить скрипт дважды проверить, что путь соответствует ожидаемому шаблону. - Возвращайте значимый код выхода:
0
в случае успеха и больше нуля1-255
в случае неудачи. Не обязательно давать каждому режиму отказа уникальный код выхода; подумайте, что именно захотят узнать пользователи скрипта. - Использовать соответствующие соглашения об именовании переменных, скриптов и процедур. Они должны соответствовать принятым в языке, остальной кодовой базе и, что особенно важно, другим переменным и функциям, определенным в текущем проекте. Используйте смешанный регистр или подчеркивание, чтобы сделать длинные имена читаемыми.
- Имена переменных должны отражать хранимые в них значения, но старайтесь делать их короткими.
number_of_lines_of_input
- слишком длинно, попробуйтеn_lines
. - Важным является именование самих скриптов. В этом контексте для имитации пробелов чаще используются тире, чем подчеркивание (
system-config-printer
). - Рассмотрите возможность разработки руководства по стилю, чтобы вы и ваши коллеги могли писать код в соответствии с едиными правилами. С помощью руководства вам будет легче читать чужой код, а им - ваш.
- Начинайте каждый скрипт с блока комментариев, в котором указывается, что делает скрипт и какие параметры он принимает. Укажите свое имя и дату. Если сценарий требует установки в системе нестандартных инструментов, библиотек или модулей, укажите и их.
- Комментируйте на том уровне, который будет полезен вам самим, когда вы вернетесь к сценарию через месяц или два. Полезно прокомментировать следующие моменты: выбор алгоритма, используемые веб-ссылки, причины, по которым не удалось сделать что-то более очевидным способом, необычные пути в коде, все, что вызвало затруднения в процессе разработки.
- Не загромождайте код бесполезными комментариями; предполагайте наличие интеллекта и знание языка у читателя.
- Можно запускать скрипты от имени
root
, но не делайте ихsetuid
; очень сложно сделать setuid-скрипты полностью безопасными. Вместо этого используйтеsudo
для реализации соответствующих политик управления доступом. - Не пишите сценарии, которые вы не понимаете. Администраторы часто рассматривают скрипты как авторитетную документацию о том, как должна выполняться та или иная процедура. Не стоит подавать ложный пример, распространяя "половинчатые" сценарии.
- Не стесняйтесь адаптировать код из существующих скриптов для своих нужд. Но не занимайтесь программированием по принципу "копируй, вставляй и молись", если вы не понимаете кода. Потратьте время на то, чтобы разобраться в нем. Это время никогда не бывает потрачено впустую.
- В
bash
используйте опцию-x
для вывода команд на экран перед их выполнением и-n
для проверки команд на синтаксис без их выполнения. - Помните, что в Python вы находитесь в режиме отладки, если явно не отключите его с помощью аргумента
-0
в командной строке. Перед выводом диагностического вывода можно проверить специальную переменную__debug__
. - Сообщения об ошибках должны идти в
STDERR
, а неSTDOUT
. - В сообщениях об ошибках указываете какая функция или операция не выполнилась.
- Используйте имена, набранные заглавными буквами, для указания на переменные среды или переменные, считываемые из глобальной конфигурации. В именах локальных переменных используйте строчные буквы, с разделением слов нижним подчёркиванием
_
.
Ссылки:
- https://utcc.utoronto.ca/~cks/space/blog/unix/ShellPipelineIndeterminate
- https://www.gibney.org/the_output_of_linux_pipes_can_be_indeter
- https://tldp.org/LDP/abs/html/special-chars.html
- https://www.opennet.ru/docs/RUS/bash_scripting_guide/c301.html
- https://www.shellcheck.net/
- http://mywiki.wooledge.org/BashFAQ/105
- http://mywiki.wooledge.org/BashFAQ/031
Релевантные мануалы:
- man bash
- source (man bash, SHELL BUILTIN COMMANDS)
- man shellcheck
- man test
Литература
- Эви Немет и др. — Unix и Linux. Руководство системного администратора. 5-e издание [Глава 7]