Оболочка и скрипты
В этой лекции мы покажем некоторые основные варианты использования bash в качестве языка скриптов, а также инструменты оболочки, которые решают несколько наиболее распространенных задач.
Скрипты
До этого мы видели, как выполнять команды в оболочке и объединять их в конвейер (это серия команд, соединенных операторами конвейера).
Но есть много задач, в которых нужно выполнить серию команд и управлять серией выражений, таких как условные операторы или циклы.
Скрипты оболочки – следующий по сложности шаг. У большинства оболочек есть свой язык сценариев с переменными и собственным синтаксисом.
Что отличает сценарии оболочки от других сценарных языков? Они оптимизированы для выполнения задач, связанных с оболочкой. Таким образом, создание конвейеров, сохранение результатов в файлы и чтение из стандартного ввода – это примитивы в сценариях оболочки, которые упрощают работу в сравнении со скриптовыми языками общего назначения. В этом разделе мы сосредоточимся на bash-скриптах, как на наиболее распространенных.
Строки в bash
можно определять с помощью кавычек – одинарных '
или двойных "
, но они не равнозначны. Строки с '
читаются буквально и не заменяют значение переменной, в то время как "
– заменяют.
Для присвоения переменной в bash используется синтаксис foo=bar
, для получения доступа к значению переменной - $foo
. А foo = bar
интерпретируется как вызов программы foo
с аргументами =
и bar
. Обратите внимание, что в скриптах пробел служит разделителем между аргументами.
foo=bar
echo "$foo"
# prints bar
echo '$foo'
# prints $foo
Как и большинство языков программирования, bash поддерживает порядок вычислений, включая if
, case
, while
и for
. Точно так же в bash есть функции, которые получают аргументы и позволяют выполнять операции. Вот пример функции, которая создает папку (mkdir
) и заходит (cd
) в нее
mcd () {
mkdir -p "$1"
cd "$1"
}
$1
— обозначение аргумента в функции/скрипте. В отличие от других сценарных языков, bash использует специальные переменные для ссылки на аргументы и для кодов ошибок. Ниже перечислены несколько из обозначений. Полный список можно найти здесь.
$0
- имя скрипта$1
до$9
- аргументы скрипта$@
- все аргументы, переданные скрипту (параметры выводятся отдельными строками)$#
- количество аргументов$?
- код возврата предыдущей команды$$
- pid текущего shell (самого процесса-сценария)!!
- полное повторение предыдущей команды. Распространенное применение – когда команда не была выполнена из-за отсутствия прав, то можно повторить ее, просто вызвавsudo !!
$_
- последний аргумент предыдущей команды
Команды часто возвращают выходные данные с STDOUT
, ошибки с STDERR
и кодом возврата – это более удобный для скриптов способ. Код возврата – это способ, которым сценарий/команды должны сообщать о завершении процесса. Значение 0 обычно означает, что все прошло хорошо; любое значение, кроме 0, означает ошибку.
Код возврата может использоваться для условного выполнения команды с помощью &&
(оператор И) и ||
(оператор ИЛИ), где оба являются операторами оценки короткого замыкания. Команды также могут быть разделены в одной строке точкой с запятой ;
. True
программа всегда будет иметь код возврата 0
, а команда False
всегда будет иметь код возврата 1
. Посмотрим на несколько примеров.
false || echo "Oops, fail"
# Oops, fail
true || echo "Will not be printed"
#
true && echo "Things went well"
# Things went well
false && echo "Will not be printed"
#
true ; echo "This will always run"
# This will always run
false ; echo "This will always run"
# This will always run
Другой распространенный шаблон – подстановка результата выполнения команды в виде переменной. Делается это через $
. Если ввести в командную строку $(cmd)
, то консоль подставит результат cmd
как данные переменной. Например, for file in $(ls)
— итерация по всем сущностям текущей папки.
Похожая, но менее известная команда — подстановка процесса (process substitution). Например, результат выполнения diff <(ls foo) <(ls bar)
покажет разницу между файлами в директориях foo
и bar
.
Давайте разберем на конкретном примере. Допустим, с помощью команды grep
(это построчный поиск по регулярному выражению) попытаемся найти строку foobar
в файле.
#!/bin/bash
echo "Starting program at $(date)" # Date will be substituted
echo "Running program $0 with $# arguments with pid $$"
for file in "$@"; do
grep foobar "$file" > /dev/null 2> /dev/null
# When pattern is not found, grep has exit status 1
# We redirect STDOUT and STDERR to a null register since we do not care about them
if [[ $? -ne 0 ]]; then
echo "File $file does not have any foobar, adding one"
echo "# foobar" >> "$file"
fi
done
Выше мы также проверили, действительно ли $?
не равен 0.
Bash проводит много сравнений подобного рода - вы можете найти подробный список на странице руководства test
.
Выполняя сравнение в bash, попробуйте использовать двойные квадратные скобки [[ ]]
вместо обычных [ ]
. Это уменьшит вероятность ошибок. Подробное объяснение можно найти здесь.
В скриптах часто встречается ситуация, когда нужно выполнить операцию над несколькими объектами файловой системы. В bash можно выполнить подстановку имен файлов - «globbing» (по историческим причинам; в русском также известно как «универсализация файловых имен»).
- Шаблоны (wildcards) - для сопоставления и последующего удаления, поиска или архивирования большого количества файлов можно пользоваться
?
(для одного символа) и*
(все символы). Например, даны файлыfoo
,foo1
,foo2
,foo10
иbar
, коммандаrm foo?
удалитfoo1
иfoo2
, тогда как командаrm foo*
удалит все, кромеbar
. - Фигурные скобки
{}
- удобно использовать в случае, когда к файлу применяется серия команд. Например, при перемещении или конвертации.
convert image.{png,jpg}
# Will expand to
convert image.png image.jpg
cp /path/to/project/{foo,bar,baz}.sh /newpath
# Will expand to
cp /path/to/project/foo.sh /path/to/project/bar.sh /path/to/project/baz.sh /newpath
# Globbing techniques can also be combined
mv *{.py,.sh} folder
# Will move all *.py and *.sh files
mkdir foo bar
# This creates files foo/a, foo/b, ... foo/h, bar/a, bar/b, ... bar/h
touch {foo,bar}/{a..h}
touch foo/x bar/y
# Show differences between files in foo and bar
diff <(ls foo) <(ls bar)
# Outputs
# < x
# ---
# > y
Инструмент shellcheck помогает отследить и исправить ошибки в скриптах. Обратите внимание, что сценарии не обязательно должны быть написаны на bash для вызова из терминала. Например, это простой скрипт Python, который печатает аргументы в обратном порядке:
#!/usr/local/bin/python
import sys
for arg in reversed(sys.argv[1:]):
print(arg)
Ядро понимает, что нужно выполнить этот скрипт с помощью интерпретатора Python, а не с помощью оболочки, потому что мы включили shebang в самый верх скрипта. Хорошей практикой является написание shebang с помощью команды env
. В нашем примере shebang выглядит так: #!/usr/bin/env python
.
Разница между функциями и скриптами:
- Функции задаются на языке оболочки, тогда как скрипты могут быть написаны на разных языках программирования. Для этого используется shebang.
- Скрипты загружаются каждый раз при выполнении, а не единожды, как функции.
- Функции выполняются в текущей среде оболочки, а скрипты - в специальном процессе. Кроме того, функции могут изменять переменные, например текущий каталог.
- Функции хороши для достижения модульности, повторного использования кода и его чистоты. Обычно сценарии оболочки включают собственное определение функции.
Инструменты оболочки
Как использовать команды
На этом этапе вы, вероятно, задаетесь вопросом, как найти флаги команд, например, ls -l
, mv -i
и mkdir -p
. И в общем, как узнавать, какая команда отвечает за конкретное действие? Всегда можно воспользоваться гуглом, но поскольку UNIX старше StackOverflow, в нем уже есть встроенные способы получения этой информации.
Как мы помним из лекции про оболочку, у нас есть -h
или --help
. Более детальная информация находится по команде man
(это сокращение от мануала, manual или man
).
Например, man rm
выведет полную информацию о команде rm
, включая ее флаги (ранее мы уже сталкивались с флагом -i
). В man
внесены абсолютно все команды, даже созданные сторонними разработчиками.
В других программах, например, написанных с использованием ncurses (это библиотека, предназначенная для управления вводом-выводом на терминал), информацию о командах можно найти в :help
или флаге ?
.
Иногда страницы руководства могут содержать слишком подробное описание команд. Проект TLDR - сайт альтернативных справочных страниц. Авторы проекта позиционируют его как «коллекцию упрощённых и создаваемых сообществом man-страниц».
Так, авторы этого курса чаще всего пользуются страницами о tar
и ffmpeg
.
Поиск файлов
Одна из часто повторяемых задач - поиск файлов и/или директорий. Для этого во всех UNIX-подобных системах есть утилита find
. Пример:
# Find all directories named src
find . -name src -type d
# Find all python files that have a folder named test in their path
find . -path '*/test/*.py' -type f
# Find all files modified in the last day
find . -mtime -1
# Find all zip files with size in range 500k to 10M
find . -size +500k -size -10M -name '*.tar.gz'
Помимо простого поиска, find
также может изменять найденные файлы:
# Delete all files with .tmp extension
find . -name '*.tmp' -exec rm {} \;
# Find all PNG files and convert them to JPG
find . -name '*.png' -exec convert {} {}.jpg \;
Ниже примеры синтаксиса для разных задач.
Чтобы просто найти файлы, которые удовлетворяют заданным критериям (назовем их PATTERN
), необходимо выполнить find -name '*PATTERN*'
(или -iname
если сопоставление должно быть нечувствительным к регистру).
Можете создавать псевдонимы для таких скриптов, но важно рассмотреть и альтернативные варианты решения.
Помните, что одна из главных особенностей оболочки - это то, что вы вызываете программы, а значит, можно найти (или даже написать самостоятельно) замену некоторым из них. Например, fd
- это простая, быстрая и удобная альтернатива find
. Она предлагает цветной вывод, сопоставление регулярных выражений по умолчанию и поддержку Unicode. Плюс легче запомнить синтаксис - для примера выше: fd PATTERN
.
Но насколько вообще эффективно использовать find
и fd
для поиска по всей иерархии директорий? Не лучше ли применить команду locate
, которая ведет поиск по собственной базе данных. locate
использует базу данных, которая обновляется с помощью updatedb
. В большинстве систем updatedb
обновляется по cron
.
Но find
ищет файлы не только по названию, а также по другим атрибутам (размер, время изменения, разрешения) поиска и аналогичные инструменты также могут находить файлы с помощью атрибутов, таких как размер файла, время изменения, права доступа к файлу, в то время как locate ищет только по имени файла только имя файла. Больше информации здесь.
Поиск кода
Поиск файла по имени полезен, но часто вам нужно будет искать файлы по их содержимому.
Обычный сценарий - поиск всех файлов, содержащих определенные строки/значения. Для таких случаев в UNIX-подобных системах есть grep
. Более подробно grep
мы рассмотрим в лекции по управлению данными.
У grep
есть много флагов, что делает его по-настоящему универсальным.
-C
используется для получения числа строк контекста, -v
для вывода строк, которые не совпадают с тем, что ищем. -R
используется для быстрого поиска.
У grep
есть альтернавы, такие как ack, ag и rg. Они имеют схожий функционал.
Рассмотрим другой аналог ripgrep (rg
). Он быстрее ищет по коду, так как по умолчанию не проходит .git директории и бинарные файлы.
# Find all python files where I used the requests library
rg -t py 'import requests'
# Find all files (including hidden files) without a shebang line
rg -u --files-without-match "^#!"
# Find all matches of foo and print the following 5 lines
rg foo -A 5
# Print statistics of matches (# of matched lines and files )
rg --stats PATTERN
Поиск команд
Выше мы рассматривали способы поиска файлов, директорий и кода. При долгой работе в оболочке обязательно возникнет необходимость в поиске ранее использованной команды. Во-первых, нажатие стрелки вверх вернет последнюю вызванную команду. Если продолжить нажимать ее, она медленно пройдет через историю работы.
Команда history
позволит получить доступ к истории. Все действия будут выведены на экран. Для последующего поиска по истории воспользуйтесь grep
. Так, history | grep find
выведет подстроки, содержащие “find”.
В большинстве оболочек вы можете использовать Ctrl+R
для поиска по истории оболочки. После выполнения этой команды можно ввести подстроку, которую необходимо найти.
По такому же принципу стрелки вверх-вниз работают в командной оболочке zsh.
Хороший поиск предлагает утилита fzf bindings.
fzf
предоставляет возможность нечеткого поиска с использованием множества команд. Результат поиска выводится в визуально приятном стиле.
Еще один удобный прием - автодополнение (history-based autosuggestions). Впервые появился в оболочке fish. При вводе команды плагин читает историю и дозаполняет последнюю команду из истории, начинающуюся с тех же символов. Функция доступна и в zsh является значимым аргументом оболочки, улучшающим удобство использования.
И последнее, о чем стоит помнить: если в начале команды стоит пробел, она не будет добавлена в историю. Это удобно при вводе пароля и другой конфиденциальной информации.
Если вы ошиблись и не добавили начальный пробел, вы всегда можете вручную удалить запись, отредактировав .bash_history
или .zhistory
.
Навигация по директории
До сих пор мы рассматривали примеры, когда вы выполняете команды в нужном месте каталога. Но что насчет быстрой навигации по директориям? Есть много простых способов переходить между каталогами, например, написать псевдоним оболочки или создать символические ссылки с помощью ln -s, но есть варианты легче и быстрее. Нужно научиться оптимизировать повторяющиеся задачи.
Найти часто используемые или недавно открытые файлы и директории можно с помощью утилит fasd
и autojump
.
fasd
ранжирует файлы и каталоги по особому весовому коэффициенту frecency (от frequency и recency).
По умолчанию, fasd
добавляет команду z
для перехода между директориями (тогда как обычно для этого используется cd
). Например, если часто посещаемая директория /home/user/files/cool_project
, вы можете перейти в нее, выполнив z cool
. А используя перепрыгивание (autojump), перейти в эту же директорию можно, выполнив j cool
.
Для просмотра структуры каталогов существуют более сложные утилиты: tree
, broot
; файловые менеджеры nnn
или ranger
.
Упражнения
-
Прочтите
man ls
и напишите командуls
которая выводит список файлов в следующем виде:- Отображает все файлы, включая скрытые;
- Размер файлов представлен в удобном для человека формате (пример: 454M вместо 454279954);
- Порядок файлов - по дате изменения (от более новых - к старым);
- Вывод раскрашен.
Образец вывода:
-rw-r--r-- 1 user group 1.1M Jan 14 09:53 baz drwxr-xr-x 5 user group 160 Jan 14 09:53 . -rw-r--r-- 1 user group 514 Jan 14 06:42 bar -rw-r--r-- 1 user group 106M Jan 13 12:12 foo drwx------+ 47 user group 1.5K Jan 12 18:08 ..
-
Создайте команды bash
marco
иpolo
которые делают следующее. При выполнении командыmarco
- текущая рабочая директория должна быть сохранена таким образом, чтоб при выполнении командыpolo
, вне зависимости в какой директории вы находитесь сейчас,polo
должноcd
вернуть вас в ту директорию, где была выполнена командаmarco
. Для простоты дебагинга вы можете записать код команды в файлmarco.sh
и загрузить (перезагрузить) определения в вашей оболочке, выполнивsource marco.sh
. -
Скажем, вы имеете скрипт, который иногда выполняется с ошибкой. С целью дебагинга, вам необходимо записать выводимую информацию данного скрипта, но на это будет затрачено значительное количество времени, из-за продолжительности выполнения и редкости воспроизведения ошибки. Напишите скрипт 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"
-
Как было рассказано в лекции, команды
find
и-exec
будут крайне полезны при выполнении операций над найденным файлом. Однако, что если мы хотим сделать что-то со всеми файлами, к примеру - заархивировать их? Как вам известно, команды могут принимать ввод из аргументов или из STDIN. При создании конвейеров, мы перенаправляем вывод одной команды в ввод другой (STDOUT -> STDIN), однако некоторые команды, такие какtar
, принимают ввод из аргументов. Для возможности объединения данных подходов, используется командаxargs
, которая выполняет указанную команду используя STDIN как аргумент. К примеруls | xargs rm
удалит все файлы в директории.Ваша задача - написать команду, которая рекурсивно находит все HTML файлы в директории и объединяет их в zip архив. Обратите внимание, что ваша команда должна работать даже если файлы содержат пробел в названии. (подсказка: ознакомьтесь с
-d
флагом командыxargs
) -
(Продвинутое) Напишите команду или скрипт для рекурсивного поиска файла, который выводит самый последний измененный файл в директории, т.е. файл, который был изменен позже всех.
Лицензия CC BY-NC-SA.