В прошлой лекции мы посмотрели на основы терминала и некоторые команды, которые
можно соединять в цепочки. Тем не менее, не всегда удобно писать всё в одной
строке через && и хочется писать скрипты.
Shell предназначен в основном для задач, связанных с переменными, процессингом файлов (возможно, даже больших), поиском и перенаправлением потоков. Shell очень-очень плох для математических вычислений, объектно-ориентированного программирования. Также синтаксис shell является достаточно сложным и контринтуитивным, когда дело касается достаточно сложных операций. Тем не менее, в мире достаточно много скриптов на shell, и Вам придётся их читать и понимать.
В этой лекции мы расскажем о том, как писать скрипты, какие подъязыки
хранят в себе команды grep, sed и когда стоит уже сдаться и писать
скрипты на Python, который демонстрирует намного лучшую стабильность, если
скрипт начинает сильно разрастаться.
В bash можно объявлять переменные как foo=bar; к сожалению, нельзя
foo = bar, потому что это расценивается как вызов команды foo с аргументами
= и bar. Как уже говорилось в прошлой лекции, аргументы всегда разделяются
пробелом и, чтобы избежать казусов, надо использовать escaping через символ \,
либо использовать кавычки '' или "". К несчастью, кавычки не всегда
равноценны, хоть и позволяют группировать аргументы, а именно:
foo=bar
echo "$foo"
# prints bar
echo '$foo'
# prints $fooКак и во многих других языках программирования, в bash есть функции, например:
mcd () {
mkdir -p "$1"
cd "$1"
}Эта функция берёт первый аргумент, создаёт папку и входит в нее. $1 —
обозначение аргумента в функциях. В функциях можно использовать следующие
обозначения:
$0— имя функции$1до$9— аргументы функции. Для 10 или более аргументов используйте{}скобки, например,${10}. Максимальное количество аргументов — 255$@— все аргументы$#— количество аргументов$?— код возврата предыдущей команды$$— PID данного процесса!!— полное повторение Вашей предыдущей команды, удобно, например, когда Вам нужно sudo, можно просто написатьsudo !!
Старайтесь постоянно оборачивать аргументы в двойные кавычки. Почему так надо, можете почитать здесь.
В прошлой лекции мы уже немного затрагивали коды возврата, давайте повторим и дополним:
false || echo "Fail"
# 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 run anyway"
# This will run anyway
false ; echo "This will run anyway"
# This will run anyway|| — условие справа выполняется только если левое вернуло ненулевой код
возврата или то же самое, что и оператор "или", && — то же самое, что и
оператор "и". ; — просто разделитель.
В bash очень часто используется подстановка команд через $. Вы можете
в любом месте вставить $(cmd) и оно подставит результат cmd уже как данные
переменной. Самый частый способ так делать это, например, for i in $(ls -1)
— итерация по всем сущностям текущей папки.
Любой bash скрипт должен начинаться с так называемого shebang, который указывает на то, с помощью какого интерпретатора нужно исполнять скрипт.
Стандартно это #!, который последуется с помощью пути интерпретатора
(возможно, с аргументами):
#!/bin/bashИли для Python:
#!/usr/local/bin/pythonПосле этого начинается скрипт. В bash Вы можете писать любые команды с новой строки, они выполняются построчно, функции, переменные, вызовы функций и т.д.
Один из стандартных циклов в общем случае выглядит так:
for item in [LIST]
do
[COMMANDS]
doneLIST это любой лист объектов, разделенный проблельным символом
(как минимум \n, \t, ' '), например:
for element in Hydrogen Helium Lithium Beryllium
do
echo "Element: $element"
doneили
for line in $(cat ~/file)
do
echo $line
doneТакже можно итерироваться по числам:
for i in {1..15}
do
echo "Number: $i"
doneМожно ещё с определённым шагом:
for i in {1..15..3}
do
echo "Number: $i"
done
# Number: 1
# Number: 4
# Number: 7
# Number: 10
# Number: 13И в обратном направлении:
for i in {1..15..-3}
do
echo "Number: $i"
done
# Number: 13
# Number: 10
# Number: 7
# Number: 4
# Number: 1Можно итерироваться по листам, например, аргументов (c 1-го):
for file in "$@"
do
echo $file
doneМожно писать обычные циклы, к которым мы привыкли в C/C++:
for ((i = 0 ; i <= 20 ; i += 5)); do
echo "Counter: $i"
done((cmd)) всегда означает математическое вычисление. Вы можете вычислять
стандартные математические выражения c числами и операторами +, -, /, *, %, ^. К
сожалению, если что-то окажется не числом, оно заменяется на ноль, а shell не
выдаёт и не вернёт ошибку:
$ a=5
$ echo $((a^5))
0
$ echo $((a*5))
25
$ a=rfr
$ echo $((a*5))
0
$ echo $((a*5))
0В циклах можно писать break, continue.
Общий синтаксис для if советует придерживаться двойным [[]] скобкам:
if [[ a op b ]]; then
[COMMANDS]
else
[OTHER_COMMANDS]
fiВы можете встретить одинарные скобки, тем не менее, в них можно много сделать ошибок. В таблице представлены какие операции можно делать:
Также можно перед любыми условиями писать ! — отрицание, как мы привыкли
в C/C++.
Оператор else является необязательным.
case чуть-чуть сложнее, выглядит он так:
case [variable] in
[pattern 1])
[commands]
;;
[pattern 2])
[other commands]
;;
esacПосмотрите case_script.sh внимательно. patterns являются
регулярными выражениями, commands обычными командами, двойной ; нужен
обязательно.
У переменных можно брать подстроки примерно как в Python, например:
$ echo ${PATH:0:2}
/u
$ echo ${PATH:0:-1}
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bi
$ echo ${PATH:50:-1}
/sbin:/biЗаменять подстроки:
$ first="HSE is worse than MIPT"
$ second="better"
$ echo "${first/worse/$second}"
HSE is better than MIPTИ по регулярному выражению:
$ message='The secret code is 12345'
$ echo "${message/[0-9]*/X}"
The secret code is XИ даже все вхождения, а не только первое c помощью дополнительного слеша:
$ message='The secret code is 12345'
$ echo "${message//[0-9]/X}"
The secret code is XXXXXВ bash очень удобно раскрывать множество значений одновременно, например:
$ touch problem_{1..5}.cpp
$ ll | grep problem
-rw-r--r-- 1 danilak primarygroup 0 Aug 16 20:41 problem_1.cpp
-rw-r--r-- 1 danilak primarygroup 0 Aug 16 20:41 problem_2.cpp
-rw-r--r-- 1 danilak primarygroup 0 Aug 16 20:41 problem_3.cpp
-rw-r--r-- 1 danilak primarygroup 0 Aug 16 20:41 problem_4.cpp
-rw-r--r-- 1 danilak primarygroup 0 Aug 16 20:41 problem_5.cppМожно делать через запятую, они все раскрываются:
$ touch problem_{1,2,3,4,5}_{1,2,3,4,5,7,10}.cc
$ ll | grep problem_ | wc -l
35
$ rm problem_*Также в bash поддерживаются wildcard аннотации * — (взять всё) и ? — один
символ. Полезно при удалении/поиске/архивировании огромного ряда файлов
по такому простому регулярному выражению.
Придётся часть выучить, см. конец прошлой лекции.
Я обычно пользуюсь правилом: если я начинаю путаться в bash скриптах и надо сделать более нетривиальные операции, чем сплит, сортировка, поиск, то стоит писать на питоне, иначе можно всё ещё на bash.
Также, если я знаю, что кодом кто-то будет дальше пользоваться, то это тоже зелёный флаг для Python. Если код можно выкинуть через пару часов, я могу дать фору bash.
grep (globally search for a regular expression and print matching lines) — одна из самых частых команд, которая используется в shell scripting.
Основное предназначение — это построковый поиск по регулярному выражению в файле:
grep
Matches patterns in input text.
- Search for a pattern within a file:
grep {{search_pattern}} {{path/to/file}}
$ grep "ro\{2\}t" /etc/passwd
root:x:0:0:root:/root:/bin/bashВ регулярных выражениях поддерживаются стандартные ., *, +, ?, {n,m}, \w, \s, [:alpha:] и т.д.
.означает любой символ*означает matching нуля или более элементов, например,.*— это произвольное количество символов (возможно пустое), аa*— произвольное количество буквa+означает один или более символов;[0-9]+означает хотя бы одна цифра из диапазона0-9.{n,m},{n}— количество повторений;(aba){3}матчит 3 раза строкуaba, а(aba){3,5}от 3 до 5 раз, а(aba){,5}не более 5 раз.?— 0 или 1 группа;https?://матчитhttp://иhttps://, а(https)?://матчитhttps://и://.\w— любой словесный символ (word symbol),\s— любой пробельный символ (пробел, новая строка и т.д.).[]— группы, например,[a-z]матчит одну маленькую букву,[a-z_]матчит одну маленькую букву или_,[0-3]{4}матчит 4 раза цифры от 0 до 3. Отрезки, которые поддерживаются, — это латинские буквы (маленькие и большие, цифры). Можно сделать отрицание, поставив^в начало, например,[^a-z&]матчит всё, кроме маленьких латинских букв и символа&.
Регулярные выражения отличаются своей семантикой иногда, но выше предоставлены те, которые поддерживаются везде. Я советую синтаксис RE2. Он лучше из-за того, что разрешает только те операции, по которым поиск будет идти полиномиальное время.
grep может выводить строки файлов с опцией -n, имена файлов с помощью -H(
бывает полезно для поиска и быстрой замены). А также может рекурсивно искать в
папке во всех файлах с помощью опции -r.
grep очень удобен для pipe поиска, например, достаточно часто используется вот так:
$ cmd | grep $search_patternМожно не учитывать регистр с опцией -i и инвертировать поиск с помощью
-v, а показать контекст на ±N строк — -C N. Остальные опции можете
почитать в man, я указал на самые часто используемые.
Я стал для кода больше использовать ripgrep, потому что он лучше и быстрее ищет по коду, минуя всякие .git директории и бинарные файлы по умолчанию.
Одна из самых насыщенных утилит для поиска файлов в директориях. Примеры скажут сами за себя:
# 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 \;Часто используется команда xargs, которая умеет передавать stdout программы как аргументы другой, например:
$ find . -name '*.tmp' | xargs rmСделает тоже самое, более универсально, но менее оптимально.
curl является отличным инструментом для не очень серьёзного скрейпинга каких-то сайтов, а также дебага проблем с браузерами.
- Download the contents of an URL to a file:
curl {{http://example.com}} -o {{filename}}
- Download a file, saving the output under the filename indicated by the URL:
curl -O {{http://example.com/filename}}
- Download a file, following [L]ocation redirects, and automatically [C]ontinuing (resuming) a previous file transfer:
curl -O -L -C - {{http://example.com/filename}}
- Send form-encoded data (POST request of type application/x-www-form-urlencoded). Use -d @file_name or -d @'-' to read from STDIN:
curl -d {{'name=bob'}} {{http://example.com/form}}
- Send a request with an extra header, using a custom HTTP method:
curl -H {{'X-My-Header: 123'}} -X {{PUT}} {{http://example.com}}
Часто включают опцию --silent, чтобы зря не забивать stderr. Для полных
HTTP запросов ещё используют -K опцию для чтения из файла.
В браузерах по F12 в разделе Network можно скопировать запросы как curl запросы, это стало стандартом.
sed (stream editor) — это утилита для запуска скриптов, которые как-то меняют файлы, однако используется в большинстве своём построчными заменами одного регулярного выражения на другие:
# Замена и вывод в stdout
$ sed 's/expr_1/expr_2/' file.txt
# Inplace замена
$ sed -i 's/expr_1/expr_2/' file.txtВ expr_1 можно ставить скобки, а в expr_2 можно использовать их в порядке как
\1, например:
$ cat file.txt
some_thing1
some_thing2
some_thing3
some_thing4
some_thing5
some_thing6
some_thing7
another_string
$ sed 's/some_\(thing[0-9]\)/\1/' file.txt
thing1
thing2
thing3
thing4
thing5
thing6
thing7
another_string
$ sed -E 's/some_(thing[0-9])/\1/' file.txt
thing1
thing2
thing3
thing4
thing5
thing6
thing7
another_stringВ целом, у sed аргумент принимает скрипт. Если он начинается с s, то идёт
поиск по всем строкам; если есть числа перед s, например, 4,17s, то поиск
идёт с 4 до 17 строки; если строка /apple/s то операция произведётся только
со всеми, где есть apple, !s — отрицание, например:
$ sed -E '1,3!s/some_(thing[0-9])/\1/' file.txt
some_thing1
some_thing2
some_thing3
thing4
thing5
thing6
thing7
kekВ целом, s — просто одна команда, за которой идут аргументы. Есть много других
команд, например, d — delete, y — траснлитерация, i — вставка перед
текстом:
$ seq 10 | sed '1,3d'
4
5
6
7
8
9
10
$ seq 10 | sed '1~4!d' # 1 с шагом 4
1
5
9
$ echo "hello world" | sed 'y/abcdefghij/0123456789/'
74llo worl3То есть структура такая: сначала выбор строк (по номерам или по регулярному выражению), потом однобуквенная команда (возможно с отрицанием предыдущего условия), потом её аргументы.
Как пример, sed чрезвычайно полезен в фильтрации тестовых данных и исправлении каких-то опечаток.
Используйте Python. Забудьте про эту команду.
