В прошлой лекции мы посмотрели на основы терминала и некоторые команды, которые
можно соединять в цепочки. Тем не менее, не всегда удобно писать всё в одной
строке через &&
и хочется писать скрипты.
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]
done
LIST
это любой лист объектов, разделенный пробельным символом
(как минимум \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
-у после -exec
передавать команды для исполнения над найденными файлами:
# 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
— транcлитерация, 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. Забудьте про эту команду.(¬_¬)