You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

29 KiB

Задания

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

Сценарии стандартизируют административную работу и освобождают время администраторов для более важных и интересных задач. Сценарии также служат своеобразной бесплатной документацией, поскольку в них записываются шаги, необходимые для выполнения той или иной задачи. Основной альтернативой сценариям для сисадминов является использование систем управления конфигурацией (Ansible, Puppet, Chef). На практике большинство администраторов используют комбинацию сценариев и управления конфигурациями. Каждый из этих подходов имеет свои преимущества, и они хорошо сочетаются друг с другом.

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

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

При написании скриптов используйте https://www.shellcheck.net/, чтобы проверить их на ошибки до запуска.

0.

Каждый процесс имеет как минимум три канала связи: стандартный ввод STDIN, стандартный вывод STDOUT и стандартный вывод ошибок STDERR. Процессы изначально наследуют эти каналы от своих родителей, поэтому они не обязательно знают, куда они ведут. Они могут подключаться к окну терминала, файлу, сетевому соединению или каналу, принадлежащему компьютеру, или каналу, принадлежащему другому процессу, и т.д.

В UNIX и Linux реализована унифицированная модель ввода-вывода, в которой каждый канал именуется небольшим целым числом, называемым дескриптором файла. Точный номер, присвоенный каналу, обычно не имеет значения, но STDIN, STDOUT и STDERR гарантированно соответствуют файловым дескрипторам 0, 1 и 2. C файловыми дескрипторами 0, 1 и 2, поэтому можно смело называть эти каналы по номерам. В контексте интерактивного окна терминала STDIN обычно считывает данные с клавиатуры, а STDOUT и STDERR клавиатуры, а 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.

*Что произойдёт в данном случае?

(echo red; echo green 1>&2) | echo blue

Ответ в [1-2].

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"

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 - аргументы скрипта,
  • $@ - все аргументы,
  • $# - количество аргументов,
  • $? - возвращаемый код предыдущей команды,
  • $$ - идентификатор процесса текущего скрипта,
  • !! - вся команда, включая аргументы (попробуйте sudo !!, если забыли, что команде требуются права суперпользователя),
  • $_ - последний аргумент последней команды (в терминале попробуйте нажать Esc . или Alt+).

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

4.

Как и в большинстве языков программирования, bash поддерживает методы управления потоком, включая условные и циклические операторы if, case, while и for. Точно так же в bash есть функции, которые принимают аргументы и могут с ними работать. Вот пример определения функции:

echo_arguments () {
    echo "First three arguments: $0 $1 $2 $3, Argument count: $#, All arguments: $@"
}

Напишите аналогичный предыдущему заданию скрипт, но оберните все действия в функцию, а в конце скрипта выполните эту функцию. Проверьте работу скрипта. Работает ли функция за пределами контекста скрипта?

5.

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

Проверьте, что функции могут изменять переменные среды, например, изменить текущий каталог, тогда как скрипт не может. Напишите скрипт, в котором инициализируются две переменные A и B. Перед одной из них указывается export . Выполните скрипт и посмотрите на выдачу команды env. Выполните файл командой source (документация в source --help и секции встроенных комманд man bash). Какие переменные экспортировались в оболочку?

6.

Попробуйте условные выражения в скрипте в if конструкции:

if [[ условное выражение ]]; then
    echo "True"
fi

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. Почему эти два сценария не идентичны?

set -e
f() { test -d nosuchdir && echo no dir; }
f
echo survived
set -e
f() { if test -d nosuchdir; then echo no dir; fi; }
f
echo survived

9.5. При каких условиях это приведет к отказу?

set -e
read -r foo < configfile

Рекомендация проста: не используйте set -e. Вместо этого добавьте собственную проверку ошибок.

10.

рипты не обязательно должны быть написаны на 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/,
  • настройки беспарольного перемещения между машинами:
    1. генерирация пользователю ключа ed25519,
    2. рассылка по машинам,
    3. регистрирация ключа в списке авторизованных.

Лучшие практики написания скриптов

  • При запуске с несоответствующими аргументами скрипты должны выводить сообщение об использовании и завершать работу. Реализуйте таким же образом и --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.
  • В сообщениях об ошибка указываете какая функция или операция не выполнилась.
  • Если произошла ошибка в системном вызове включайте в сообщение об ошибке perror строку.
  • Используйте имена, набранные заглавными буквами, для указания на переменные среды или переменные, считываемые из глобальной конфигурации. В именах локальных переменных используйте строчные буквы, с разделением слов нижним подчёркиванием _.

Ссылки:

  1. https://utcc.utoronto.ca/~cks/space/blog/unix/ShellPipelineIndeterminate
  2. https://www.gibney.org/the_output_of_linux_pipes_can_be_indeter
  3. https://tldp.org/LDP/abs/html/special-chars.html
  4. https://www.opennet.ru/docs/RUS/bash_scripting_guide/c301.html
  5. https://www.shellcheck.net/
  6. http://mywiki.wooledge.org/BashFAQ/105

Релевантные мануалы:

  • man bash
  • source (man bash, SHELL BUILTIN COMMANDS)
  • man shellcheck

Литература

  • Эви Немет и др. — Unix и Linux. Руководство системного администратора. 5-e издание [Глава 7]