Table of Contents
Проектирование сверху вниз
С увеличением размеров и сложности программ их становится все труднее проектировать, программировать и сопровождать. Практически к любому сложному проекту с успехом можно применить методологию деления больших и сложных задач на более мелкие и простые.
В настоящий момент наш сценарий генерирует документ HTML, выполняя следующие шаги:
- Открыть страницу.
- Открыть заголовок страницы.
- Установить название страницы.
- Закрыть заголовок страницы.
- Открыть тело страницы.
- Вывести заголовок на странице.
- Вывести текущее время.
- Закрыть тело страницы.
- Закрыть страницу.
На следующем этапе разработки мы добавим несколько задач между шагами 7 и 8:
- Продолжительность непрерывной работы системы и степень ее загруженности – это интервал времени, прошедшего с момента последней загрузки системы, и среднее число задач, выполняемых процессором в настоящее время для нескольких отрезков времени.
- Дисковое пространство – информация об использовании дискового пространства на системных устройствах хранения.
- Объем домашних каталогов – объем дискового пространства, занятого каждым пользователем
Если бы у нас были команды, решающие перечисленные задачи, мы бы просто добавили их в сценарий, воспользовавшись механизмом подстановки результатов команд:
#!/bin/bash # Программа вывода страницы с информацией о системе TITLE="System Information Report For $HOSTNAME" CURRENT_TIME=$(date +"%x %r %Z") TIME_STAMP="Generated $CURRENT_TIME, by $USER" cat << _EOF_ <html> <head> <title>$TITLE</title> </head> <body> <h1>$TITLE</h1> <p>$TIME_STAMP</p> $(report_uptime) $(report_disk_space) $(report_home_space) </body> </html> _EOF_
Создать такие команды можно двумя способами: написать три отдельных сценария и поместить их в каталог, входящий в список PATH, или встроить эти сценарии в программу в виде функций командной оболочки. Как уже отмечалось ранее, функции — это «мини-сценарии», находящиеся внутри другого сценария, которые работают как автономные программы. Функции имеют две синтаксические формы. Первая выглядит так:
function имя { команды return }
Вторая проще (и часто предпочтительнее):
имя () { команды return }
Где имя – это имя функции, а команды – последовательность команд внутри функции. Обе формы эквивалентны и могут использоваться одна вместо другой. Ниже приводится сценарий, демонстрирующий использование функций командной оболочки:
1 #!/bin/bash 2 3 # Демонстрация функций командной оболочки 4 5 function step2 { 6 echo "Step 2" 7 return 8 } 9 10 # Здесь начинается основная программа 11 12 echo "Step 1" 13 step2 14 echo "Step 3"
Когда командная оболочка читает сценарий, она пропускает строки с 1-й по 11-ю, так как они содержат комментарии и определение функции. Выполнение начинается со строки 12 с командой echo. Строка 13 вызывает функцию step2, и командная оболочка выполняет функцию как любую другую команду. Управление передается в строку 6, и выполняется вторая команда echo. Следующей выполняется строка 7. Команда return в этой строке завершает выполнение функции и возвращает управление в строку, следующую за вызовом функции (строка 14). После этого выполняется заключительная команда echo. Обратите внимание: чтобы вызовы функций интерпретировались не как имена внешних программ, а действительно как вызовы функций, эти функции должны быть определены в сценарии до их вызова.
Добавим в наш сценарий минимальные определения функций:
#!/bin/bash # Программа вывода страницы с информацией о системе TITLE="System Information Report For $HOSTNAME" CURRENT_TIME=$(date +"%x %r %Z") TIME_STAMP="Generated $CURRENT_TIME, by $USER" report_uptime () { return } report_disk_space () { return } report_home_space () { return } cat << _EOF_ <html> <head> <title>$TITLE</title> </head> <body> <h1>$TITLE</h1> <p>$TIME_STAMP</p> $(report_uptime) $(report_disk_space) $(report_home_space) </body> </html> _EOF_
Имена функций подчиняются тем же правилам, что и имена переменных. Функция должна содержать хотя бы одну команду. Команда return (которая является необязательной) помогает удовлетворить это требование.
Локальные переменные
В сценариях, что нам доводилось писать до сих пор, все переменные (включая константы) были глобальными. Глобальные переменные существуют и доступны в любой точке программы. В некоторых случаях это безусловно полезное свойство осложняет использование функций. Внутри функций иногда желательно использовать локальные переменные. Локальные переменные доступны только внутри функции, в которой они определены, и прекращают свое существование по завершении выполнения функции.
Поддержка локальных переменных позволяет программисту использовать переменные с именами, которые уже определены в сценарии, глобально или в других функциях, не беспокоясь о возможных конфликтах имен.
Следующий пример сценария демонстрирует, как определяются и используются локальные переменные:
#!/bin/bash # local-vars: сценарий, демонстрирующий локальные переменные foo=0 # глобальная переменная foo funct_1 () { local foo # переменная foo, локальная для funct_1 foo=1 echo "funct_1: foo = $foo" } funct_2 () { local foo # переменная foo, локальная для funct_2 foo=2 echo "funct_2: foo = $foo" } echo "global: foo = $foo" funct_1 echo "global: foo = $foo" funct_2 echo "global: foo = $foo"
Как видите, локальные переменные объявляются добавлением слова local перед именем переменной. В результате создается переменная, локальная по отношению к функции, в которой она определена. Когда выполнение выйдет за пределы функции, переменная перестанет существовать. Если запустить этот сценарий, он выведет следующее:
global: foo = 0 funct_1: foo = 1 global: foo = 0 funct_2: foo = 2 global: foo = 0
Этот пример показывает, что присваивание значений локальной переменной foo внутри обеих функций не оказывает влияния на значение переменной foo, объявленной за пределами функций.
Эта особенность позволяет писать функции, сохраняя их независимость друг от друга и от сценария, в котором они определяются. Это очень ценное качество, оно предотвращает взаимовлияние разных частей программы друг на друга, а кроме того, помогает писать переносимые функции, то есть функции, которые можно скопировать из одного сценария в другой.
Постоянное опробование сценария
В процессе разработки программ необходимо постоянно проверять их работоспособность. Запуская и тестируя программы как можно чаще, мы сможем выявить ошибки на самых ранних этапах разработки. Это существенно упрощает задачу отладки. Например, если после внесения небольших изменений и очередного запуска программы обнаружится ошибка, источник проблемы почти наверняка будет находиться в последних изменениях. Добавив пустые функции, которые на языке программистов называются заглушками, мы смогли проверить работоспособность программы на ранней стадии. Создавая заглушку, неплохо было бы включить в нее что-то, что давало бы обратную связь, позволяющую программисту оценить ход выполнения. Если сейчас взглянуть на вывод нашего сценария, можно заметить несколько пустых строк, следующих за строкой с текущим временем, но мы пока не уверены в причинах их появления.
sys_info_page <html> <head> <title>System Information Report For HostName</title> </head> <body> <h1>System Information Report For HostName</h1> <p>Generated 06/28/21 04:49:05 PM MSK, by nevvad</p> </body> </html>
Изменим функции, добавив в них сообщения для обратной связи:
report_uptime () { echo "Function report_uptime executed" return } report_disk_space () { echo "Function report_disk_space executed" return } report_home_space () { echo "Function report_home_space executed" return }
И запустим сценарий еще раз:
sys_info_page <html> <head> <title>System Information Report For HostName</title> </head> <body> <h1>System Information Report For v0id</h1> <p>Generated 06/28/21 04:55:09 PM MSK, by nevvad</p> Function report_uptime executed Function report_disk_space executed Function report_home_space executed </body> </html>
Теперь можно с уверенностью сказать, что наши три функции выполняются как надо.
Теперь, когда каркас функций готов и работает, самое время добавить в них некий код. Сначала займемся функцией report_uptime:
report_uptime () { cat <<- _EOF_ <h2>SYSTEM UPTIME</h2> <pre>$(uptime)</pre> _EOF_ return }
Она выглядит очень просто. Мы использовали встроенный документ для вывода заголовка раздела и результатов выполнения команды uptime, заключив их в теги <html><pre></html>, чтобы сохранить формат вывода команды. Функция report_disk_space выглядит аналогично:
report_disk_space () { cat <<- _EOF_ <h2>DISK SPASE UTILIZATION</h2> <pre>$(df -h)</pre> _EOF_ return }
Она получает информацию о дисковом пространстве с помощью команды <html>df -h</html>. Наконец, определим функцию report_home_space:
report_home_space () { cat <<- _EOF_ <h2>HOME SPACE UTILIZATION</h2> <pre>$(du -sh /home/*)</pre> _EOF_ return }
Для решения поставленной задачи мы использовали команду <html>du</html> с параметрами <html>-sh</html>. Однако это не полное решение задачи. Даже притом, что его можно использовать в некоторых системах (например, в Ubuntu), кое-где оно работать не будет. Причина в том, что во многих системах для домашних каталогов выбираются разрешения, не позволяющие читать их содержимое другим пользователям, что является вполне разумной мерой предосторожности. В этих системах функция report_home_space в том виде, в каком она написана здесь, будет работать, только если запустить сценарий с правами суперпользователя. Лучшее, что можно сделать в такой ситуации, – корректировать поведение сценария в соответствии с привилегиями пользователя, запустившего его.