View on GitHub

Methodics of GNU/Linux application software development

Practical materials for learning application software development on Linux

Данный раздел посвящён обширному классу языков склейки и, в частности, языку 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

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 существуют спецсимволы перенаправления:

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?

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

Цикл

  1. Цикл с условием: \(\textrm{while\ \textbf{команды};\ do\ \textbf{команды};\ done}\)
  2. Цикл по последовательности: \(\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, рассмотрим специализированные программы для работы с ФС

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`: рассмотрим программу, позволяющую создать и гарантированно удалить временный файл

Программа:

#!/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%

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

Комбайны

Комбайны — важный класс утилит, взаимодействующий со входным потоком на основе регулярных выражений.

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:~>

Разобьём эту часть на кусочки и разберём:

При операторе в sed может стоять т. н. «контекстый адрес» — он описывает, к каким строкам входного текста применять этот оператор. Если контестого адреса нет (как в примере), оператор применятся ко всемп строкам.

Домашнее задание:

Написать сценарий на shell randomize.sh, который принимает один необязательный параметр — время задержки в секундах (вещественное) перед выводом очередного символа, затем читает со стандартного ввода небольшой файл с ASCII-артом в кодировке ASCII и без символов табуляции, после чего выводит его посимвольно в случайном порядке в левый верхний угол экрана. Должен получиться с виду тот же ASCII-арт (аккуратнее с пробелами). Что может понадобиться: