Skip to content

Latest commit

 

History

History
 
 

shell_scripting

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

Shell scripting

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

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

Один из стандартных циклов в общем случае выглядит так:

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, case statements

Общий синтаксис для if советует придерживаться двойным [[]] скобкам:

if [[ a op b ]]; then
  [COMMANDS]
else
  [OTHER_COMMANDS]
fi

Вы можете встретить одинарные скобки, тем не менее, в них можно много сделать ошибок. В таблице представлены какие операции можно делать:

All ifs

Также можно перед любыми условиями писать ! — отрицание, как мы привыкли в 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 аннотации * — (взять всё) и ? — один символ. Полезно при удалении/поиске/архивировании огромного ряда файлов по такому простому регулярному выражению.

Как находить все эти команды

Придётся часть выучить, см. конец прошлой лекции.

Python vs Bash

Я обычно пользуюсь правилом: если я начинаю путаться в bash скриптах и надо сделать более нетривиальные операции, чем сплит, сортировка, поиск, то стоит писать на питоне, иначе можно всё ещё на bash.

Также, если я знаю, что кодом кто-то будет дальше пользоваться, то это тоже зелёный флаг для Python. Если код можно выкинуть через пару часов, я могу дать фору bash.

grep

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

Одна из самых насыщенных утилит для поиска файлов в директориях. Примеры скажут сами за себя:

# 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

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

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 чрезвычайно полезен в фильтрации тестовых данных и исправлении каких-то опечаток.

awk

Используйте Python. Забудьте про эту команду.(¬_¬)