База из командне линије

[верзија 1.0, 26. април 2023.]

Страхиња Радић

Садржај

Увод

Ако бисмо просечног корисника Униксоликог1 система питали шта препоручује за базе података, вероватно бисмо добили различите одговоре: преко неизбежног, полувласничког, MySQL‑а (ако саговорник не прати трендове), његовог слободног наследника MariaDB‑а (ако прати), па до власничких система као што је IBM‑ов Db2, итд.

За све ове системе и програме је карактеристично да користе модел сервер-клијент, да складиште податке у бинарном формату, и да је за приступање подацима потребно имати инсталиране одређене програме, филтере или додатке. Мало ко би поменуо могућност да се подаци складиште у најобичнијој текстуалној датотеци.

Формат табеле

У систему текстуалних база табела је представљена једном TSV (Tab Separated Values) датотеком. За оне који до сада нису чули за овај назив, ради се о текстуалном формату у коме су подаци у колонама раздвојени ASCII знаком TAB (Horizontal Tab), који има хексадекадну вредност 0x09=9. Врсте се раздвајају знаком за нови ред, који је у Униксоликим системима знак LF (Line Feed), са вредношћу 0x0A=10. У Microsoft‑овим системима заснованим на DOS‑у, знаку LF претходи знак CR (Carriage Return), са вредношћу 0x0D=13. Пошто овде не разматрамо DOS, подразумева се да ћемо за крај реда користити само LF.

Креирање табеле

Зато је креирање нове табеле сасвим просто:

$ cat >kupci.tsv
Р.Бр.	Име	Презиме	Мејл
1	Петар	Петровић	ppetrovic@gmail.com
2	Милан	Милић	mmilic@gmail.com
3	Жика	Жикић	zzikic@gmail.com
<C-D>

Поља раздвајамо тастером Tab, а слогове тастером Enter. Крај уноса задајемо комбинацијом Ctrl+D.

Алтернативно, можемо користити наредбу сличну овој:

$ cat <<! >kupci.tsv
Р.Бр.	Име	Презиме	Мејл
1	Петар	Петровић	ppetrovic@gmail.com
2	Милан	Милић	mmilic@gmail.com
3	Жика	Жикић	zzikic@gmail.com
!

где унос завршавамо текстом који следи оператору << а претходи белини, у засебном реду, али је тада у већини љуски, које користе ГНУ‑ов Readline или њему сличан систем за унос текста, за унос сваког знака TAB потребно подразумевано притиснути Ctrl+V, а затим Tab. Оцену о томе шта је лакше препуштам читаоцу.

Испис и филтрирање „слогова“ у табели

По уносу, ова табела се одмах може и форматирати, рецимо програмом table:

$ tsvtable -n kupci.tsv
╔══════════════════╤══════════════════╤══════════════════╤══════════════════╗
║Р.Бр.             │Име               │Презиме           │Мејл              ║
║1                 │Петар             │Петровић          │ppetrovic@gmail.co║
║2                 │Милан             │Милић             │mmilic@gmail.com  ║
║3                 │Жика              │Жикић             │zzikic@gmail.com  ║
╚══════════════════╧══════════════════╧══════════════════╧══════════════════╝

Оно што је карактеристично за систем текстуалних база је флексибилност: TSV се може отворити из читавог низа најразличитијих програма, укључујући и LibreOffice Calc, Microsoft Excel, итд. По потреби, разумљив је чак и најобичнији испис наредбом cat(1) или преглед наредбом less(1):

$ cat kupci.tsv
Р.Бр.	Име	Презиме	Мејл
1	Петар	Петровић	ppetrovic@gmail.com
2	Милан	Милић	mmilic@gmail.com
3	Жика	Жикић	zzikic@gmail.com

Пошто је ова наредба гарантовано део сваког Униксоликог система, за ово нису потребни никакви додатни програми, а резултат је буквално тренутан!

Сада се поставља питање: шта ако желимо да испишемо само одређене слогове? Уколико нам није битно коју колону претражујемо, можемо просто користити grep(1):

$ grep Милан kupci.tsv
2       Милан   Милић   mmilic@gmail.com

Уз ГНУ‑ов grep, који подржава PCRE‑ове (Perl Compatible Regular Expressions), можемо задати и напредније претраге. Рецимо, да бисмо тражили текст „Петровић“ у трећој колони, можемо извршити:

$ grep -P '^([^\t]*\t){2}.*Петровић.*' kupci.tsv
1       Петар   Петровић        ppetrovic@gmail.com

Ипак, увек можемо користити програм awk(1):

$ awk -F"$(printf '\t')" '{if ($3 ~ ".*Петровић.*") print}' kupci.tsv
1       Петар   Петровић        ppetrovic@gmail.com

Ако желимо да тражимо слогове који у четвртој колони садрже текст „gmail“, а истовремено у другој текст „Жика“, извршићемо:

$ awk -F"$(printf '\t')" \
'{if ($4 ~ ".*gmail.*" && $2 ~ ".*Жика.*") print}' kupci.tsv
3       Жика    Жикић   zzikic@gmail.com

Да бисмо у претходном примеру исписали и наслове колона, можемо извршити низ наредби:

$ sed 1q kupci.tsv; awk -F"$(printf '\t')" \
'{if ($4 ~ ".*gmail.*" && $2 ~ ".*Жика.*") print}' kupci.tsv
Р.Бр.   Име     Презиме Мејл
3       Жика    Жикић   zzikic@gmail.com

Да све ово и форматирамо, просто цео излаз проследимо програму table:

$ { sed 1q kupci.tsv; awk -F"$(printf '\t')" \
'{if ($4 ~ ".*gmail.*" && $2 ~ ".*Жика.*") print}' kupci.tsv; } \
| tsvtable -n
╔══════════════════╤══════════════════╤══════════════════╤══════════════════╗
║Р.Бр.             │Име               │Презиме           │Мејл              ║
║3                 │Жика              │Жикић             │zzikic@gmail.com  ║
╚══════════════════╧══════════════════╧══════════════════╧══════════════════╝

Спајање табела

Нека сада имамо две табеле: narudzbine.tsv и kupci.tsv. Табела narudzbine.tsv је следеће садржине:

$ tsvtable -n narudzbine.tsv
╔══════════════════╤══════════════════╤══════════════════╤══════════════════╗
║Р.Бр.             │Купац             │Производ          │Датум             ║
║1                 │3                 │1                 │23.04.2023        ║
║1                 │3                 │3                 │23.04.2023        ║
║2                 │1                 │2                 │26.04.2023        ║
╚══════════════════╧══════════════════╧══════════════════╧══════════════════╝

За исписивање свих слогова из табеле narudzbine.tsv и слогова из табеле kupci.tsv који им одговарају (поље „Купац“ односно „Р.Бр.“ је „кључ“), можемо искористити програм join(1). Пошто је за његову примену неопходно да улазни подаци буду сортирани по пољу које је еквивалент кључа из релационих база података, морамо да примењујемо и програм sort(1), а то значи да ћемо морати да податке прослеђујемо „у лету“, жонглирањем стандардним улазом/излазом и цевима (енгл. pipes). Ако користимо ГНУ‑ову љуску Bash, ово се може постићи оператором супституције процеса (енгл. process substitution), „<()“. У љускама које немају тај оператор, морају се користити именоване цеви (енгл. named pipes, FIFO).

bash-5.1.16$ join -t $'\t' -1 2 -a 1 <(sed 1d narudzbine.tsv | sort -k2) \
<(sed 1d kupci.tsv | sort) | sort -k2 | tsvtable -n
╔══════════╤══════════╤══════════╤══════════╤══════════╤══════════╤══════════╗
║3         │1         │1         │23.04.2023│Жика      │Жикић     │zzikic@gma║
║3         │1         │3         │23.04.2023│Жика      │Жикић     │zzikic@gma║
║1         │2         │2         │26.04.2023│Петар     │Петровић  │ppetrovic@║
╚══════════╧══════════╧══════════╧══════════╧══════════╧══════════╧══════════╝

Исто то у POSIX љусци, преко именованих цеви:

$ mkfifo narudzbine kupci
$ sed 1d narudzbine.tsv | sort -k2 >narudzbine & \
	sed 1d kupci.tsv | sort >kupci &
[2] 11734 11735
[3] 11736 11737
$ join -t "$(printf 't')" -1 2 -a 1 narudzbine kupci \
	| sort -k 2 | tsvtable -n
╔══════════╤══════════╤══════════╤══════════╤══════════╤══════════╤══════════╗
║3         │1         │1         │23.04.2023│Жика      │Жикић     │zzikic@gma║
║3         │1         │3         │23.04.2023│Жика      │Жикић     │zzikic@gma║
║1         │2         │2         │26.04.2023│Петар     │Петровић  │ppetrovic@║
╚══════════╧══════════╧══════════╧══════════╧══════════╧══════════╧══════════╝
[3] - Done                 \sed 1d kupci.tsv | \sort >kupci
[2]   Done                 \sed 1d narudzbine.tsv | \sort -k2 >narudzbine
$ rm narudzbine kupci

Нека је још и табела proizvodi.tsv следеће садржине:

$ tsvtable -n proizvodi.tsv
╔═════════════════════════╤═════════════════════════╤═════════════════════════╗
║Р.Бр.                    │Назив                    │Јед.мере                 ║
║1                        │шећер                    │kg                       ║
║2                        │лаптоп                   │ком                      ║
║3                        │уље                      │l                        ║
╚═════════════════════════╧═════════════════════════╧═════════════════════════╝

Сада применом наредби добијамо комплетан извештај, комбинован од података из све три табеле:

$ mkfifo narudzbine proizvodi narpro kupci
$ sed 1d narudzbine.tsv | sort -k3 >narudzbine &
[2] 12258 12259
$ sed 1d proizvodi.tsv | sort >proizvodi &
[3] 12260 12261
$ join -t "$(printf '\t')" -1 3 -a 1 narudzbine proizvodi | sort -k3 >narpro &
[4] 12262 12263
$ sed 1d kupci.tsv | sort >kupci &
[5] 12265 12266
[3]   Done                 \sed 1d proizvodi.tsv | \sort >proizvodi
[2]   Done                 \sed 1d narudzbine.tsv | \sort -k3 >narudzbine
$ { printf "Р.Бр.\tДатум\tПроизвод\tЈед.мере\tИме\tПрезиме\tМејл\n"; \
	  { join -t "$(printf '\t')" -1 3 -a 1 narpro kupci | sort -k3 \
		| cut -f3-; }; } | tsvtable -n
╔══════════╤══════════╤══════════╤══════════╤══════════╤══════════╤══════════╗
║Р.Бр.     │Датум     │Производ  │Јед.мере  │Име       │Презиме   │Мејл      ║
║1         │23.04.2023│шећер     │kg        │Жика      │Жикић     │zzikic@gma║
║1         │23.04.2023│уље       │l         │Жика      │Жикић     │zzikic@gma║
║2         │26.04.2023│лаптоп    │ком       │Петар     │Петровић  │ppetrovic@║
╚══════════╧══════════╧══════════╧══════════╧══════════╧══════════╧══════════╝
[5] - Done                 \sed 1d kupci.tsv | \sort >kupci
[4]   Done                 \join -t "$(\printf "\\t" )" -1 3 -a 1 narudzbine
proizvodi | sort -k3 >narpro
$ rm narudzbine proizvodi narpro kupci

Наравно, све ово постаје гломазно за куцање, па се зато може направити скрипт за потребу генерисања табеле која комбинује податке из ове три табеле, као што је рецимо narudzbine.sh. Њега можемо користити на следећи начин:

$ ./narudzbine.sh | tsvtable -n
╔══════════╤══════════╤══════════╤══════════╤══════════╤══════════╤══════════╗
║Р.Бр.     │Датум     │Производ  │Јед.мере  │Име       │Презиме   │Мејл      ║
║1         │23.04.2023│уље       │l         │Жика      │Жикић     │zzikic@gma║
║1         │23.04.2023│шећер     │kg        │Жика      │Жикић     │zzikic@gma║
║2         │26.04.2023│лаптоп    │ком       │Петар     │Петровић  │ppetrovic@║
╚══════════╧══════════╧══════════╧══════════╧══════════╧══════════╧══════════╝

Или, ако желимо PDF (txt2pdf је функција из мојих дотфајлова):

$ ./narudzbine.sh | tsvtable -c 150 -n \
	| txt2pdf -l -m 1 -p a4 -x 15 -y 8 >narudzbine.pdf

Резултат је датотека narudzbine.pdf.

Закључак

За једноставне примене, као што је већина ситуација у којима је потребна база података (ако искључимо базе података са стотинама хиљада и више слогова), довољно је ослонити се на стандардне Уникс алате из командне линије. Ако посматрамо само време потребно за покретање гломазних GUI програма, оно вишеструко надмашује време потребно за генерисање извештаја програма из командне линије. Зато и за потребе мог посла у школи за евиденцију користим систем сличан описаном, који сам сам развио.

Фусноте


1. У овом тексту и другде, под „Униксолики“ (енгл. Unix‑like) подразумевам оперативне системе који за узор имају оперативни систем Уникс (енгл. Unix). То су у данашње време разне варијанте ГНУ‑а са Линуксом (енгл. GNU/Linux), које се углавном погрешно називају Линуксом, али и друге врсте сличних оперативних система, рецимо Alpine Linux, који је суштински Busybox/musl/Linux, NetBSD, OpenBSD, па чак и Apple‑ов MacOS, итд.


Generated by slweb © 2020-2023 Strahinya Radich.