Данный раздел посвящён обширному классу языков склейки и, в частности, языку Unix Shell, как основному универсальному языку сборки проектов
Подробная лекция по данной теме(2020г.)
Универсальность сборки
Сборка — один из основных этапов разработки программного продукта. У каждого языка есть набор библиотек, функций и методов, позволяющих самостоятельно осуществлять сборку проекта на этом языке. Так, например, в рамках курса “Совместная разработка приложений на Python” слушатели изучат python-утилиту doit по автосборке проектов.
Однако если нам требуется универсальность работы с программами (как в рамках сборки проектов, так и для простого взаимодействия с утилитами), нам на помощь приходят языки склейки.
Почти всегда мы работаем с какой-то машиной, или фреймворком, или сетевой средой, заполненной командами и утилитами. Всё это окружение уже умеет решать наши задачи по обработке каких-то входных данных, преобразованию и передаче их. Но при разработке отдельных утилит разработчик далеко не всегда закладывает ориентир на работу со сторонними утилитами и, тем более, пользовательскими программами. Или взаимодействие со сторонним приложением может требовать довольно сложных синтаксических конструкций. Языки склейки, применяющиеся в качестве языков работы основного интерфейса управления нашей системы (в нашем случае, это командная строка), отвечают трём основным требованиям нашей работы:
- С их помощью мы можем использовать команды, встроенные в нашу систему;
- Множество доступных нам команд алгоритмически полно;
- С их помощью настроен удобный интерфейс объединения команд, их суперпозиция
Unix Shell
Unix Shell — это удобный интерпретатор командной строки, поддерживающий работу с переменными, условные операторы, циклы и другие базовые конструкции языков программирования, а, главное, умеющий просто и быстро работать с установленными в систему программами, а также объединять их. Shell преимущественно работает с текстовыми данными, вследствие чего в системах, поддерживающий его, очень много утилит для работы с текстом
В нашем конспекте мы будем обращаться к «классическому» Shell-у, а также к интерпретатору Z Shell
Работа с переменными
Для ускорения работы интерпретатора в нём организован однопроходный синтаксический анализ скрипта, из-за чего для однозначного определения переменных среди текстового потока они выделяются спецсимволом $. При этом для задания переменных этого не нужно, определение произойдёт за счёт однозначной интерпретации операции присваивания
user@localhost:~> zsh
localhost% a=2
localhost% echo a
a
localhost% echo $a
2
localhost% a=4
localhost% echo $a
4
localhost%
Арифметика
Арифметика в Shell устроена через внутреннее $-представление результата выражения или через встроенную утилиту expr
localhost% A=2; B=3
localhost% echo $A
2
localhost% echo $B
3
localhost% echo $((A+B))
5
localhost% echo $((A+B-13))
-8
localhost% expr $A + $B
5
Пространства имён
Так как интерпретатор может взаимодействовать с программами и запускать их на исполнение, при этом сам являясь программой, для пользователей открывается возможность создания вложенных пространств имён, в которых мы можем обрабатывать Shell-скрипты. Для взаимодействия пространств имён мы можем экспортировать переменные из внешнего пространства во внутреннее (обратное невозможно)
user@localhost:~> zsh
localhost% A=1
localhost% echo $A
1
localhost% zsh
localhost% echo $A
localhost% ASD=123
localhost% echo $ASD
123
localhost% exit
localhost% echo $ASD
localhost% echo A
A
localhost% export A
localhost% zsh
localhost% echo $A
1
localhost% exit
localhost% exit
user@localhost:~>
Функции
Shell поддерживает связывание имён на целые скрипты.
user@localhost:~> A() { echo qq; }
user@localhost:~> A
qq
user@localhost:~>
Для использования переменных в коде мы пользуемся логикой передачи параметров во вложенное пространство из внешнего. Для этого внутри отдельных скриптов мы можем задавать локальные переменные при помощи ключевого слова local
- ВНИМАНИЕ!
localне входит в стандарт POSIX
localhost% func1() { local var='func1 local'; func2}
localhost% func2() {echo "In func2, var = $var"}
localhost% var=global
localhost% echo $var
global
localhost% func2
In func2, var = global
localhost% func1
In func2, var = func1 local
localhost% echo $var
global
Текстовая обработка
При работе с данными Shell может перед непосредственным выводом проводить пост-обработку полученных результатов
user@localhost:~> A=123.456.678
user@localhost:~> echo ${A}b # вывод форматированного текста
123.456.678b
user@localhost:~> echo $Ab # считает, что это переменная из двух букв и, очевидно, не находит её в своём словаре
localhost% A=123/456/789
localhost% echo $A
123/456/789
localhost% echo ${A}
123/456/789
localhost% echo ${A%/*} # Отрезание хвоста данных (отрезает последнее совпадение шаблону /*)
123/456
localhost% echo ${A%%/*} # Жадное отрезание хвоста (по первому совпадению)
123
localhost% echo ${A#*/} # отрезание головы (по первому совпадению)
456/789
localhost% echo ${A##*/} # Жадное отрезание головы (по последнему)
789
localhost% A=123.426.678
localhost% echo ${A/2/E} # замена первого совпадения
1E3.426.678
localhost% echo ${A//2/E} # Замена всех совпадений
1E3.4E6.678
Запуск скриптов в командной строке
Мы можем запускать сразу последовательности команд Shell — скрипты. При этом нам доступна возможность запуска их в фоновом режиме для продолжения
localhost% A=Hello
localhost% echo $A
Hello
localhost% {sleep 1; echo 1; sleep 1; echo 2; sleep 1; echo QQ} # запуск скрипта в данном* процессе
1
2
QQ
localhost% {sleep 1; echo 1; sleep 1; echo 2; sleep 1; echo QQ} & # запуск скрипта фоновым процессом — можем продолжать работу
[1] 189666
localhost% echo $A # успеваем вызывать другие команды Shell
Hello
1
localhost% echo $A
Hello
2
localhost% echo $A
Hello
QQ
[1] + 189666 done { sleep 1; echo 1; sleep 1; echo 2; sleep 1; echo QQ; }
localhost%
*Справедливости ради (вспоминаем ОСи) все вызовы программ отрабатывают не в заданном процессе. Для всех них срабатывает fork-exec механизм для создания родственного процесса и подмены его необходимой к исполнению командой, просто в первом случае основной процесс ждёт завершения дочернего, а во втором случае продолжает работу в ожидании сигнала от дочернего процесса и данных о завершении в связующем канале.
Перенаправление ввода-вывода
Для удобства передачи данных на входной и с выходного потоков программ в Shell существуют спецсимволы перенаправления:
<+ перенаправление ввода>+ перенаправление вывода с перезаписыванием итогового места>>+ перенаправление вывода с дозаписыванием итогового места2>/2>>+ перенаправление потока ошибок ```console localhost% ls bin Desktop dwe Music o3 Py.py d Documents file o oqwer pythonprac d1 dotfiles LinuxAppDev o1 Pictures Templates d34 Downloads Methodics_of_LinuxAppDev o2 Public Videos
localhost% ls > o localhost% cat o bin d d1 d34 Desktop Documents dotfiles Downloads dwe file LinuxAppDev Methodics_of_LinuxAppDev Music o o1 o2 o3 oqwer Pictures Public Py.py pythonprac Templates Videos
localhost% wc qwert asdfghj zxcvbnm,lkjhgfdsa 3 3 32 localhost% wc < o 24 24 169
+ `|` — конвейер, вывод левого операнда передаётся на вход правому операнду
```console
user@localhost:~> ls -l /usr/bin | grep ls | wc
49 453 2964
user@localhost:~>
localhost% { ls o?; ls d?;}
o1 o2 o3
d1
localhost% { ls o*; ls d*;}
o o1 o2 o3 oqwer
d d1 d34 dwe
dotfiles:
alacritty bash init_bash_vim_conf.bash lf neofetch nixos nvim vim
localhost% { ls o?; ls q?;} > o 2>&1
localhost% cat o
o1
o2
o3
zsh: no matches found: q?
<<— Ввод до определённых данных с входного потока ```console user@localhost:~> cat > file «EOFHello Im Here
EOF user@localhost:~> cat file Hello Im Here
user@localhost:~>
#### Условный оператор (if-fi)
$$\textrm{if\ \textbf{команды};\ then\ \textbf{команды-True};\ else\ \textbf{команды-False};\ fi}$$
В качестве условия у нас используется статус завершения последней из **команд**
```console
user@localhost:~> if ls o?; then echo QQ; fi
o1 o2 o3
QQ
user@localhost:~> if ls q?; then echo QQ; fi
ls: cannot access 'q?': No such file or directory
user@localhost:~> if ls q?; then echo QQ; else echo "==$?"; fi
ls: cannot access 'q?': No such file or directory
==2
Цикл
- Цикл с условием: \(\textrm{while\ \textbf{команды};\ do\ \textbf{команды};\ done}\)
- Цикл по последовательности: \(\textrm{for\ \textbf{переменная}\ in\ \textbf{слова};\ do\ \textbf{команды};\ done}\)
localhost% while [ $a -lt 6 ]; do
echo "Hello $a"; read a; done
Hello 1
3
Hello 3
2
Hello 2
5
Hello 5
8
localhost%
localhost% for i in o? d?; do echo "Find ${i}"; done
Find o1
Find o2
Find o3
Find d1
localhost%
Более сложный пример, основанный на использовании программ в условиях: автоматически меняющийся ls:
user@localhost:~> a="."
user@localhost:~> while ls $a; do
> read a
> echo "$a"
> done
bin Desktop dwe Music o3 Py.py
d Documents file o oqwer pythonprac
d1 dotfiles LinuxAppDev o1 Pictures Templates
d34 Downloads Methodics_of_LinuxAppDev o2 Public Videos
o*
o*
o o1 o2 o3 oqwer
o1
o1
o1
P*
P*
Py.py
Pictures:
Me.jpg Screenshot_2024-08-10_16-34-03.png vscode.svg
MK_2redaction.jpeg Screenshot_2024-08-10_16-34-25.png Witcher.jpg
PC_cat.jpg Tmp_Videos Zastavka.gif
Public:
bin Desktop dwe Music o3 Py.py
d Documents file o oqwer pythonprac
d1 dotfiles LinuxAppDev o1 Pictures Templates
d34 Downloads Methodics_of_LinuxAppDev o2 Public Videos
^C
user@localhost:~>
Для проверки условия в примерах выше мы использовали утилиту test, а, точнее, её шорт-кат [ (он принимает 4 аргумента: левый операнд, операцию, правый операнд и закрывающую скобку ] )
user@localhost:~> if test 1 = 2; then echo qq; else echo qwer; fi
qwer
user@localhost:~> if test 2 = 2; then echo qq; else echo qwer; fi
qq
user@localhost:~> if [ 2 = 2 ]; then echo qq; else echo qwer; fi
qq
Также нами использовалась утилита read, работающая построчным считывателем данных, разбивающим их разделителями на элементы для связывания с переменными. При этом последнее связывание происходит на весь оставшийся незаписанный поток информации
user@localhost:~> cal | while read a b c; do
> echo $c $b $a; done
2024 October
Tu We Th Fr Sa Mo Su
3 4 5 2 1
8 9 10 11 12 7 6
15 16 17 18 19 14 13
22 23 24 25 26 21 20
29 30 31 28 27
user@localhost:~> cal
October 2024
Su Mo Tu We Th Fr Sa
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
user@localhost:~> cal | while read a b c; do echo $c // $b // $a; done
// 2024 // October
Tu We Th Fr Sa // Mo // Su
3 4 5 // 2 // 1
8 9 10 11 12 // 7 // 6
15 16 17 18 19 // 14 // 13
22 23 24 25 26 // 21 // 20
29 30 31 // 28 // 27
// //
user@localhost:~>
Оператор выбора
Вид выражения:
case выражение in
шаблон 1) команды ;;
...
шаблон N) команды ;;
*) команды ;;
esac
Шаблоны выражений могут содержать спецсимволы; быть множеством паттернов, разделённых символом |
languages.sh
#!/bin/bash
echo -n "Enter the name of a country: "
read COUNTRY
echo -n "The official language of $COUNTRY is "
case $COUNTRY in
Lithuania)
echo -e "Lithuanian\n"
;;
Romania | Moldova)
echo -e "Romanian\n"
;;
Italy | "San Marino" | Switzerland | "Vatican City")
echo -e "Italian\n"
;;
*)
echo -e "unknown\n"
;;
esac
localhost% ./languages.sh
Enter the name of a country: Romania
The official language of Romania is Romanian
localhost% ./languages.sh
Enter the name of a country: Russia
The official language of Russia is unknown
localhost%
Полезные утилиты
Работа с Shell, как мы говорили, непосредственно связана с системными утилитами. Большая их часть — это внешние программы, работающие с потоком данных. Кроме них в системе есть внутренние программы, которые умеют подменять окружение и редактировать его. Бывают внешние программы, называющиеся и реализованные под внутренние (н-р, внешняя echo и внутренняя /bin/echo отличаются тем, что внутренняя поддерживает флаги настройки, текстовое представление которых вывести не представляется возможным)
Не очень корректно пытаться ответить на вопрос “Сколько команд есть в системе?” (Сколько поставили, столько и будет), правильнее задаваться вопросом “Сколько команд в стандарте POSIX?” (с) FrBrGeorge feat. Mandella's effect
Разберём несколько полезных утилит, предустановленных в систему (основная документация по доступным утилитам лежит здесь).
Работа с ФС и процессами
Мы не будем останавливаться на разборах классических утилит типа cp / ls / mv, рассмотрим специализированные программы для работы с ФС
findнаходит файлы по предикатам в указанном дереве ФС. Среди предикатов даже есть исполняющий, передающий найденные данные в заданный скрипт ```console user@localhost:~> find /usr/bin -perm -2000 /usr/bin/chage user@localhost:~>
user@localhost:~> find /usr/bin -perm -2000 -exec ls -l {} \; -rwxr-sr-x 1 root shadow 84944 Jul 8 14:13 /usr/bin/chage user@localhost:~>
+ `xargs` принимает поток данных с разделителями и передаёт их в заданный скрипт в качестве аргументов. Также имеет флаги настройки передачи данных и взаимодействия со скриптом
```console
user@localhost:~> ls | xargs echo "QQ"
QQ bin d d1 d34 Desktop Documents dotfiles Downloads dwe file LinuxAppDev Methodics_of_LinuxAppDev Music o o1 o2 o3 oqwer Pictures Public Py.py pythonprac Templates Videos
user@localhost:~> ls | xargs -n1 echo "QQ"
QQ bin
QQ d
QQ d1
QQ d34
QQ Desktop
QQ Documents
QQ dotfiles
QQ Downloads
QQ dwe
QQ file
QQ LinuxAppDev
QQ Methodics_of_LinuxAppDev
QQ Music
QQ o
QQ o1
QQ o2
QQ o3
QQ oqwer
QQ Pictures
QQ Public
QQ Py.py
QQ pythonprac
QQ Templates
QQ Videos
user@localhost:~>
mktempАТОМАРНО заводит временный файл со случайным именем и возвращает его имя(гарантируется уникальность имени) ```console user@localhost:~> mktemp /tmp/tmp.4qOGaAWzUL
Немножко отступления вглубь `mktemp`: рассмотрим программу, позволяющую создать и гарантированно удалить временный файл
Программа:
#!/bin/sh exit_handler() { trap + EXIT test -r “$TMPFILE” && rm -vf “$TMPFILE” } trap exit_handler EXIT HUP INT QUIT PIPE TERM echo -n “Create a file? “ read YN if [ “$YN” = y ]; then TMPFILE=”$(mktemp)” echo “$TMPFILE” ls -l “$TMPFILE” fi echo -n “Wait…” read echo “Done”
Запустим два терминала и проверим работоспособность программы:
`Term1`
```console
user@localhost:~/LinuxAppDev/03_Text/Lection> sh mktemp_prog
Create a file? y
/tmp/tmp.0HpoFrspx2
-rw------+ 1 user user 0 Oct 1 17:26 /tmp/tmp.0HpoFrspx2
Wait…
Term2
user@localhost:~> cd /tmp
user@localhost:/tmp> ls
f79d601e26a782fd149b3ffb098aae9f-{87A94AB0-E370-4cde-98D3-ACC110C5967D}
<...>
scoped_dir385nwx
tmp.0HpoFrspx2
Term1
# Вводим любой символ
Done
removed '/tmp/tmp.0HpoFrspx2'
user@localhost:~/LinuxAppDev/03_Text/Lection>
Term2
user@localhost:/tmp> ls
f79d601e26a782fd149b3ffb098aae9f-{87A94AB0-E370-4cde-98D3-ACC110C5967D}
<...>
scoped_dir385nwx
user@localhost:/tmp>
Утилиты работы с текстом
Работа с текстом — одна из важнейших врамках управления системой, где почти все программы принимают и возвращают текстовые данные. При этом очень важно учитывать возможность работы с локализованными данными, так как от того, под какую локальориентировалась разработка утилиты, результаты работы могут сильно отличаться.
localhost% ls /etc/ | sort | tail -20 | tee text1
uefi
updatedb.conf
UPower
usb_modeswitch.conf
vconsole.conf
vnc
wgetrc
wofi
wpa_supplicant
X11
xattr.conf
xdg
xfce_defaults.conf
YaST2
zprofile
zsh_command_not_found
zsh_completion.d
zshenv
zshrc
zypp
localhost% ls /etc/ | LC_ALL=C sort | tail -20 | tee text2
tuned
udev
udisks2
uefi
updatedb.conf
usb_modeswitch.conf
vconsole.conf
vnc
wgetrc
wofi
wpa_supplicant
xattr.conf
xdg
xfce_defaults.conf
zprofile
zsh_command_not_found
zsh_completion.d
zshenv
zshrc
zypp
localhost% diff -u text1 text2
--+ text1 2024-10-04 12:43:06.524414464 +0300
+++ text2 2024-10-04 12:43:17.284449326 +0300
@@ -1,17 +1,17 @@
+tuned
+udev
+udisks2
uefi
updatedb.conf
-UPower
usb_modeswitch.conf
vconsole.conf
vnc
wgetrc
wofi
wpa_supplicant
-X11
xattr.conf
xdg
xfce_defaults.conf
-YaST2
zprofile
zsh_command_not_found
zsh_completion.d
localhost%
cutвозвращает срез со входного потока (файла или потока ввода) по заданным параметрамlocalhost% cat > file Hello my dear friend localhost% cut -c 7-15 file my dear f localhost% cut -d ' ' -f 2 file my localhost%trпроводит редактирование входного потока в соответствии с заданными шаблонамиlocalhost% tr -s "ll" "Q" Hello, Guys HeQo, Guys localhost%headиtailвыводят первые и последние соответственно N элементов входного потока ```console localhost% ls /etc | head -10 adjtime alacritty aliases aliases.d aliases.lmdb alsa alternatives apparmor apparmor.d audit
localhost% ls /etc | tail -10 xattr.conf xdg xfce_defaults.conf YaST2 zprofile zsh_command_not_found zsh_completion.d zshenv zshrc zypp localhost%
Далее хочется отдельно упомянуть утилиты преобразования текстового потока в байтовое представление
+ `hexdump` выдаёт 16-ричный байтовый поток, соответсвующий входным данным
+ `xxd` занимается той же работой, основное отличие утилит (кроме внешнего вида) это их исходные библиотеки — [Util-Linux](https://en.wikipedia.org/wiki/Util-linux) для первого и [vim](http://vim.org/) для второго.
+ `od` аналогично первым двум создаёт байтовое представление данных, однако уже в восьмеричном виде
```console
user@localhost:~/LinuxAppDev/03_Text/Lection> date
Tue Oct 1 17:39:00 MSK 2024
user@localhost:~/LinuxAppDev/03_Text/Lection> date | hexdump -C
00000000 54 75 65 20 4f 63 74 20 20 31 20 31 37 3a 33 39 |Tue Oct 1 17:39|
00000010 3a 31 34 20 4d 53 4b 20 32 30 32 34 0a |:14 MSK 2024.|
0000001d
user@localhost:~/LinuxAppDev/03_Text/Lection> date | xxd
00000000: 5475 6520 4f63 7420 2031 2031 373a 3339 Tue Oct 1 17:39
00000010: 3a32 3120 4d53 4b20 3230 3234 0a :21 MSK 2024.
user@localhost:~/LinuxAppDev/03_Text/Lection> date | od
0000000 072524 020145 061517 020164 030440 030440 035067 030064
0000020 032072 020067 051515 020113 030062 032062 000012
0000035
user@localhost:~/LinuxAppDev/03_Text/Lection> rpm -qf /usr/bin/hexdump
util-linux-2.40.1-5.1.x86_64
user@localhost:~/LinuxAppDev/03_Text/Lection> rpm -qf /usr/bin/xxd
xxd-9.1.0718-1.1.x86_64
user@localhost:~/LinuxAppDev/03_Text/Lection> rpm -qf /usr/bin/vim
vim-9.1.0718-1.1.x86_64
gvim-9.1.0718-1.1.x86_64
user@localhost:~/LinuxAppDev/03_Text/Lection> rpm -qf /usr/bin/od
coreutils-9.5-3.1.x86_64
Комбайны
Комбайны — важный класс утилит, взаимодействующий со входным потоком на основе регулярных выражений.
grep— утилита поиска шаблонов во входном потоке. Своё название утилита получила от своего прародителя — редактораed, где выступала одной из управляющих функций. В редакторе команда имела форматg/re/<action>, которая расшифровывалась, как “Global search / Regular Expression / <>” — Найти совпадение шаблону, заданному регулярным выражением, и произвести действие над элементом с этим шаблоном.g/re/p— частный случай для вывода найденных элементовsed— потоковый текстовый редактор.- В основном используется ради одно операции:
s/регулярное выражение/подстановка/модификаторы— замена одного шаблона на другой в указанном месте
localhost% ls -l > file
localhost% cat file | grep D
drwxr-xr-x 1 stephen stephen 0 Dec 8 2023 bin
drwxr-xr-x 1 stephen stephen 152 Sep 27 21:23 Desktop
drwxr-xr-x 1 stephen stephen 78 Sep 15 22:35 Documents
drwxr-xr-x 1 stephen stephen 896 Sep 28 21:21 Downloads
drwxr-xr-x 1 stephen stephen 104 Oct 1 17:23 LinuxAppDev
drwxr-xr-x 1 stephen stephen 98 Sep 29 21:26 Methodics_of_LinuxAppDev
localhost%
user@localhost:~> cal | sed 's/\([01]\)\([2-3]\)/\2:\1/g'
October 22:04
Su Mo Tu We Th Fr Sa
1 2 3 4 5
6 7 8 9 10 11 2:1
3:1 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
user@localhost:~>
Разобьём эту часть на кусочки и разберём:
- “
s” — замена - ”
\([01]\)\([2-3]\)” — … шаблона из двух цифр (где первая — 0 или 1, вторая — 2-3) - ”
\2:\1” — … на шаблон, где сначала стоит вторая цифра, за ней через двоеточие первая - “
g” — заменя всех вхождений в строке
При операторе в sed может стоять т. н. «контекстый адрес» — он описывает, к каким строкам входного текста применять этот оператор. Если контестого адреса нет (как в примере), оператор применятся ко всемп строкам.
Домашнее задание:
Написать сценарий на shell randomize.sh, который принимает один необязательный параметр — время задержки в секундах (вещественное) перед выводом очередного символа, затем читает со стандартного ввода небольшой файл с ASCII-артом в кодировке ASCII и без символов табуляции, после чего выводит его посимвольно в случайном порядке в левый верхний угол экрана. Должен получиться с виду тот же ASCII-арт (аккуратнее с пробелами).
Что может понадобиться: