Встроенные документы

Глава 17. Встроенные документы

Встроенный документ (here document) является специальной формой перенаправления ввода/вывода, которая позволяет передать список команд интерактивной программе или команде, например ftp, telnet или ex.

COMMAND <<InputComesFromHERE
...
InputComesFromHERE


Конец встроенного документа выделяется "строкой-ограничителем", которая задается с помощью специальной последовательности символов <<. Эта последовательность -- есть перенаправление вывода из файла на stdin программы или команды, что напоминает конструкцию interactive-program < command-file, где command-file содержит строки:

command #1
command #2
...


Сценарий, использующий "встроенный документ" для тех же целей, может выглядеть примерно так:

#!/bin/bash
interactive-program <<LimitString
command #1
command #2
...
LimitString


В качестве строки-ограничителя должна выбираться такая последовательность символов, которая не будет встречаться в теле "встроенного документа".

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

Пример 17-1. dummyfile: Создание 2-х строчного файла-заготовки

#!/bin/bash

# Неинтерактивное редактирование файла с помощью 'vi'.
# Эмуляция 'sed'.

E_BADARGS=65

if [ -z "$1" ]
then
  echo "Порядок использования: `basename $0` filename"
  exit $E_BADARGS
fi

TARGETFILE=$1

# Вставить 2 строки в файл и сохранить.
#--------Начало встроенного документа-----------#
vi $TARGETFILE <<x23LimitStringx23
i
Это строка 1.
Это строка 2.
^[
ZZ
x23LimitStringx23
#----------Конец встроенного документа-----------#

#  Обратите внимание: ^[, выше -- это escape-символ
#+ Control-V <Esc>.

#  Bram Moolenaar указывает, что этот скрипт может не работать с 'vim',
#+ из-за возможных проблем взаимодействия с терминалом.

exit 0

Этот сценарий, с тем же эффектом, мог бы быть реализован, основываясь не на vi, а на ex. Встроенные документы, содержащие команды для ex, стали настолько обычным делом, что их уже смело можно вынести в отдельную категорию -- ex-сценарии.

Пример 17-2. broadcast: Передача сообщения всем, работающим в системе, пользователям

#!/bin/bash

wall <<zzz23EndOfMessagezzz23
Пошлите, по электронной почте, ваш заказ на пиццу, системному администратору.
    (Добавьте дополнительный доллар, если вы желаете положить на пиццу анчоусы или грибы.)
# Внимание: строки комментария тоже будут переданы команде 'wall' как часть текста.
zzz23EndOfMessagezzz23

# Возможно, более эффективно это может быть сделано так:
#         wall <message-file
# Однако, встроенный документ помогает сэкономить ваши силы и время.

exit 0

Пример 17-3. Вывод многострочных сообщений с помощью cat

#!/bin/bash

# Команда 'echo' прекрасно справляется с выводом однострочных сообщений,
# но иногда необходимо вывести несколько строк.
# Команда 'cat' и встроенный документ помогут вам в этом.

cat <<End-of-message
-------------------------------------
Это первая строка сообщения.
Это вторая строка сообщения.
Это третья строка сообщения.
Это четвертая строка сообщения.
Это последняя строка сообщения.
-------------------------------------
End-of-message

exit 0


#--------------------------------------------
# Команда "exit 0", выше, не позволить исполнить нижележащие строки.

# S.C. отмечает, что следующий код работает точно так же.
echo "-------------------------------------
Это первая строка сообщения.
Это вторая строка сообщения.
Это третья строка сообщения.
Это четвертая строка сообщения.
Это последняя строка сообщения.
-------------------------------------"
# Однако, в этом случае, двойные кавычки в теле сообщения, должны экранироваться.

Если строка-ограничитель встроенного документа начинается с символа - (<<-LimitString), то это приводит к подавлению вывода ведущих (начальных) символов табуляции (но не пробелов). Это может оказаться полезным при форматировании текста сценария для большей удобочитаемости.

Пример 17-4. Вывод многострочных сообщений с подавлением символов табуляции

#!/bin/bash
# То же, что и предыдущий сценарий, но...

#  Символ "-", начинающий строку-ограничитель встроенного документа: <<-
#  подавляет вывод символов табуляции, которые могут встречаться в теле документа,
#  но не пробелов.

cat <<-ENDOFMESSAGE
        Это первая строка сообщения.
        Это вторая строка сообщения.
        Это третья строка сообщения.
        Это четвертая строка сообщения.
        Это последняя строка сообщения.
ENDOFMESSAGE
# Текст, выводимый сценарием, будет смещен влево.
# Ведущие символы табуляции не будут выводиться.

# Вышеприведенные 5 строк текста "сообщения" начинаются с табуляции, а не с пробелов.


exit 0

Встроенные документы поддерживают подстановку команд и параметров. Что позволяет передавать различные параметры в тело встроенного документа.

Пример 17-5. Встроенные документы и подстановка параметров

#!/bin/bash
# Вывод встроенного документа командой 'cat', с использованием подстановки параметров.

# Попробуйте запустить сценарий без аргументов,   ./scriptname
# Попробуйте запустить сценарий с одним аргументом,   ./scriptname Mortimer
# Попробуйте запустить сценарий с одним аргументом, из двух слов, в кавычках,
#                           ./scriptname "Mortimer Jones"

CMDLINEPARAM=1     # Минимальное число аргументов командной строки.

if [ $# -ge $CMDLINEPARAM ]
then
  NAME=$1          # Если аргументов больше одного,
                   # то рассматривается только первый.
else
  NAME="John Doe"  # По-умолчанию, если сценарий запущен без аргументов.
fi

RESPONDENT="автора этого сценария"


cat <<Endofmessage

Привет, $NAME!
Примите поздравления от $RESPONDENT.

# Этот комментарий тоже выводится (почему?).

Endofmessage

# Обратите внимание на то, что пустые строки тоже выводятся.

exit 0

Еще один пример сценария, содержащего встроенный документ и подстановку параметров в его теле.

Пример 17-6. Передача пары файлов во входящий каталог на "Sunsite"

#!/bin/bash
# upload.sh

# Передача пары файлов (Filename.lsm, Filename.tar.gz)
# на Sunsite (ibiblio.org).

E_ARGERROR=65

if [ -z "$1" ]
then
  echo "Порядок использования: `basename $0` filename"
  exit $E_ARGERROR
fi


Filename=`basename $1`           # Отсечь имя файла от пути к нему.

Server="ibiblio.org"
Directory="/incoming/Linux"
# Вообще, эти строки должны бы не "зашиваться" жестко в сценарий,
# а приниматься в виде аргумента из командной строки.

Password="your.e-mail.address"   # Измените на свой.

ftp -n $Server <<End-Of-Session
# Ключ -n запрещает автоматическую регистрацию (auto-logon)

user anonymous "$Password"
binary
bell                # "Звякнуть" после передачи каждого файла
cd $Directory
put "$Filename.lsm"
put "$Filename.tar.gz"
bye
End-Of-Session

exit 0

Заключая строку-ограничитель в кавычки или экранируя ее, можно запретить подстановку параметров в теле встроенного документа.

Пример 17-7. Отключение подстановки параметров

#!/bin/bash
# Вывод встроенного документа командой 'cat', с запретом подстановки параметров.

NAME="John Doe"
RESPONDENT="автора этого сценария"

cat <<'Endofmessage'

Привет, $NAME.
Примите поздравления от $RESPONDENT.

Endofmessage

#  Подстановка параметров не производится, если строка ограничитель
#  заключена в кавычки или экранирована.
#  Тот же эффект дают:
#  cat <<"Endofmessage"
#  cat <<\Endofmessage

exit 0

Запрет на подстановку параметров позволяет выводить текст, как говорится, "один к одному". Это обстоятельство может использоваться, например, для автоматической генерации сценариев или даже текстов программ на других языках программирования.

Пример 17-8. Сценарий, который создает другой сценарий

#!/bin/bash
# generate-script.sh
# Автор идеи: Albert Reiner.

OUTFILE=generated.sh         # Имя нового сценария.


# -----------------------------------------------------------
# 'Встроенный документ' содержит тело создаваемого сценария.
(
cat <<'EOF'
#!/bin/bash

echo "Этот сценарий сгенерирован автоматически."
#  Обратите внимание: поскольку действия происходят в подоболочке,
#+ мы не можем получить доступ к переменным родительской оболочки.
#  Удостоверимся в этом...
echo "Файл сценария был назван: $OUTFILE"  # Не работает.

a=7
b=3

let "c = $a * $b"
echo "c = $c"

exit 0
EOF
) > $OUTFILE
# -----------------------------------------------------------

#  Заключение 'строки-ограничителя' предотвращает подстановку значений переменных
#+ в теле 'встроенного документа.'
#  Что позволяет записать все строки в выходной файл "один к одному".

if [ -f "$OUTFILE" ]
then
  chmod 755 $OUTFILE
  # Дать право на исполнение.
else
  echo "Не могу создать файл: \"$OUTFILE\""
fi

#  Этот метод можно использовать для создания
#+ Makefile-ов, программ на языках C, Perl, Python
#+ и т.п..

exit 0

Допускается запись тела встроенного документа в переменную.

variable=$(cat <<SETVAR
Это многострочная
переменная.
SETVAR)
  
echo "$variable"


Встроенные документы могут передаваться на вход функции, находящейся в том же сценарии.

Пример 17-9. Встроенные документы и функции

#!/bin/bash
# here-function.sh

GetPersonalData ()
{
  read firstname
  read lastname
  read address
  read city
  read state
  read zipcode
} # Это немного напоминает интерактивную функцию, но...


# Передать ввод в функцию.
GetPersonalData <<RECORD001
Bozo
Bozeman
2726 Nondescript Dr.
Baltimore
MD
21226
RECORD001


echo
echo "$firstname $lastname"
echo "$address"
echo "$city, $state $zipcode"
echo

exit 0

Встроенный документ можно передать "пустой команде" :. Такая конструкция, фактически, создает "анонимный" встроенный документ.

Пример 17-10. "Анонимный" Встроенный Документ

#!/bin/bash

: <<TESTVARIABLES
${HOSTNAME?}${USER?}${MAIL?}  # Если одна из переменных не определена, то выводится сообщение об ошибке.
TESTVARIABLES

exit 0

Tip

Подобную технику можно использовать для создания "блочных комментариев".

Пример 17-11. Блочный комментарий

#!/bin/bash
# commentblock.sh

: << COMMENTBLOCK
echo "Эта строка не будет выведена."
Эта строка комментария не начинается с символа "#".
Это еще одна строка комментария, которая начинается не с символа "#".

&*@!!++=
Эта строка не вызовет ошибки,
поскольку Bash проигнорирует ее.
COMMENTBLOCK

echo "Код завершения  \"COMMENTBLOCK\" = $?."   # 0
# Показывает, что ошибок не возникало.


#  Такая методика создания блочных комментариев
#+ может использоваться для комментирования блоков кода во время отладки.
#  Это экономит силы и время, т.к. не нужно втавлять символ "#" в начале каждой строки,
#+ а затем удалять их.

: << DEBUGXXX
for file in *
do
 cat "$file"
done
DEBUGXXX

exit 0
Tip

Еще одно остроумное применение встроенных документов -- встроенная справка к сценарию.

Пример 17-12. Встроенная справка к сценарию

#!/bin/bash
# self-document.sh: сценарий со встроенной справкой
# Модификация сценария "colm.sh".

DOC_REQUEST=70

if [ "$1" = "-h"  -o "$1" = "--help" ]     # Request help.
then
  echo; echo "Порядок использования: $0 [directory-name]"; echo
  sed --silent -e '/DOCUMENTATIONXX$/,/^DOCUMENTATION/p' "$0" |
  sed -e '/DOCUMENTATIONXX/d'; exit $DOC_REQUEST; fi

: << DOCUMENTATIONXX
Сценарий выводит сведения о заданном каталоге в виде таблице.
-------------------------------------------------------------
Сценарию необходимо передать имя каталога. Если каталог не
указан или он недоступен для чтения, то выводятся сведения
о текущем каталоге.

DOCUMENTATIONXX

if [ -z "$1" -o ! -r "$1" ]
then
  directory=.
else
  directory="$1"
fi

echo "Сведения о каталоге "$directory":"; echo
(printf "PERMISSIONS LINKS OWNER GROUP SIZE MONTH DAY HH:MM PROG-NAME\n" \
; ls -l "$directory" | sed 1d) | column -t

exit 0
Note

Для встроенных документов, во время исполнения, создаются временные файлы, но эти файлы удаляются после открытия и недоступны для других процессов.

bash$ bash -c 'lsof -a -p $$ -d0' << EOF
> EOF
lsof    1213 bozo    0r   REG    3,5    0 30386 /tmp/t1213-0-sh (deleted)
        


Caution

Некоторые утилиты не могут работать внутри встроенных документов.

Warning

Строка-ограничитель, закрывающая встроенный документ, должна начинаться с первого символа в строке. Перед ней не должно быть пробельных символов. Аналогично, пробельные символы, стоящие за строкой-ограничителем, могут дать нежелательные побочные эффекты.

#!/bin/bash

echo "----------------------------------------------------------------------"

cat <<LimitString
echo "Это первая строка сообщения во встроенном документе."
echo "Это вторая строка сообщения во встроенном документе."
echo "Это последняя строка сообщения во встроенном документе."
     LimitString
#^^^^Отступ перед строкой-ограничителем. Ошибка!
#    Этот сценарий будет вести себя не так как вы ожидаете.

echo "----------------------------------------------------------------------"

#  "Этот комментарий находится за пределами 'встроенного документа',
#+ и не должен выводиться.

echo "За пределами встроенного документа."

exit 0

echo "Держу пари, что эта строка не будет выведена."  # Стоит после команды 'exit'.


Если какая либо задача не может быть решена с помощью "встроенного документа", то вам следует попробовать язык сценариев expect, который приспособлен для передачи параметров на вход интерактивных программ.