Here Documents w powłokach Unix

Here Documents, Tutaj Dokumenty?

Here Documents, ten dziwnie brzmiący w rodzimym języku zlepek słów, jest rodzajem przekierowania, umożliwiającym potraktowanie części kodu źródłowego jak pliku. Kolejne linie tekstu przekazywane są na wejście innych poleceń, np. cat, ssh, ftp. Tak jakby były wpisywane linia po lini. Występuje w wielu powłokach Linux, miedzy innymi: bash, zsh, sh. Podobne konstrukcje można też spotkać w językach programowania wyższego poziomu, np. Perl. Heredocs domyślnie zachowuje białe znaki, dlatego jest używany do zapisywania wieloliniowych literałów.

Konstrukcja

Heredocs może zawierać tekst, polecenia i zmienne. Umieszczone są pomiędzy znacznikami. Znacznikiem może być dowolne słowo, często spotykane są słowa “EOF” i “END”. Dobrze jest dobierać słowa oddające charakter zawartej miedzy nimi treści, np. SQL, HTML. Tak opakowany kod poprzedzony jest poleceniem i znakami przekierowania <<. Opcjonalny parametr - spowoduje zignorowanie białych znaków. Podobieństwo do występującego w wielu powłokach przekierowania strumienia danych nie jest przypadkowe. Heredocs formalnie jest przekierowaniem literałów znakowych, na standardowe wejście innych poleceń 1.

Budowa HereDocs:

POLECENIE <<[-] ZNACZNIK
"linia 1"
"linia 2"
"linia 3"
polecenie 
$zmienna
.
.
.
ZNACZNIK

Przykłady

Na wejście polecenia SSH przekierowana jest lista poleceń, która wykonuje się na maszynie zdalnej:

$ ssh mate@192.168.0.2 bash << EOF
whoami
uptime
cat /etc/os-release
EOF

Wyjście:

mate
 14:41:12 up 4 days,  1:05,  0 users,  load average: 0.44, 0.28, 0.25
PRETTY_NAME="Ubuntu 22.04 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy

Here string domyślnie pozwala na zachowanie białych znaków:

$ cat << "EOF"
foo
        bar
baz
        what
EOF

Wyjście:
foo
        bar
baz
        what

Opcjonalny parametr - pozwala na zignorowanie białych znaków:

cat <<- "EOF"
foo
       bar
baz
       what
EOF
 
Wyjście:
foo
bar
baz
what

W bloku here można umieścić zmienne, zostaną one odpowiednio rozwinięte:

$ HOST="193.168.0.1"
$ ssh 192.168.0.2 bash << EOF
ping $HOST
EOF

PING 192.168.0.1 (192.168.0.1) 56(84) bytes of data.
64 bytes from 192.168.0.1: icmp_seq=1 ttl=64 time=1.53 ms
64 bytes from 192.168.0.1: icmp_seq=2 ttl=64 time=0.743 ms
...

Heredoc można wykorzystać do budowania zapytań SQL:

$ read -r -d '' QUERY << SQL
SELECT *
	FROM AdventureWorks2016.HumanResources.Department
	WHERE GroupName = 'Manufacturing';
SQL
$ sqlcmd -S localhost -U SA -P passw04d -Q $QUERY

Na koniec bardziej praktyczny przykład

Ta wydawało by się, dziwna trochę konstrukcja, pozwala na użycie wieloliniowych literałów znakowych, tam gdzie jest to utrudnione. Okazuje się to przydatne np. przy budowaniu dłuższych zapytań SQL. Zamiast jednej długiej, nieczytelnej lini. Można sformatować za jej pomocą blok poleceń, umieścić go w zmiennej a następnie przesłać do serwera SQL. Pozwala to na zwiększenie czytelności pisanego skryptu i budowanie złożonych zapytań SQL.

Poniżej przykładowy listing skryptu, wykorzystującego opisane powyżej metody. Za pomocą heredocs budowane jest polecenie przywrócenia bazy danych z kopii zapasowej, na serwerze mssql umieszczonym w kontenerze docker.

/restore_db.sh

#!/usr/bin/env bash
source .env

DB_URL="https://github.com/Microsoft/"`
	`"sql-server-samples/releases/download/"`
	`"adventureworks/AdventureWorks2016.bak"

DB_NAME="AdventureWorks2016"
DB_PATH='"/var/opt/mssql/backup/AdventureWorks2016.bak"'
DB_DATA='"AdventureWorks2016_Data"'
DB_DATA_PATH='"/var/opt/mssql/data/AdventureWorks2016_Data.mdf"'
DB_LOG='"AdventureWorks2016_Log"'
DB_LOG_PATH='"/var/opt/mssql/data/AdventureWorks2016_Log.ldf"'

read -r -d '' QUERY<<SQL
RESTORE DATABASE $DB_NAME 
	FROM DISK = $DB_PATH 
	WITH MOVE $DB_DATA 
	TO $DB_DATA_PATH,
	MOVE $DB_LOG 
	TO $DB_LOG_PATH;
SQL

docker exec -it mssql  \
	sh -c "wget --directory-prefix=/var/opt/mssql/backup/ $DB_URL"
docker exec -it mssql /opt/mssql-tools/bin/sqlcmd -S localhost \
	-U SA -P $SA_PASSWORD \
	-Q "$QUERY" 

[1] Chapter 19. Here Documents - Advanced Bash Scripting Guide