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


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

  1. COMMAND <<InputComesFromHERE
  2. ...
  3. InputComesFromHERE


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

  1. command #1
  2. command #2
  3. ...


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

  1. #!/bin/bash
  2. interactive-program <<LimitString
  3. command #1
  4. command #2
  5. ...
  6. LimitString


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

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

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

  1. #!/bin/bash
  2. # Неинтерактивное редактирование файла с помощью 'vi'.
  3. # Эмуляция 'sed'.
  4. E_BADARGS=65
  5. if [ -z "$1" ]
  6. then
  7.   echo "Порядок использования: `basename $0` filename"
  8.   exit $E_BADARGS
  9. fi
  10. TARGETFILE=$1
  11. # Вставить 2 строки в файл и сохранить.
  12. #--------Начало встроенного документа-----------#
  13. vi $TARGETFILE <<x23LimitStringx23
  14. i
  15. Это строка 1.
  16. Это строка 2.
  17. ^[
  18. ZZ
  19. x23LimitStringx23
  20. #----------Конец встроенного документа-----------#
  21. #  Обратите внимание: ^[, выше — это escape-символ
  22. #+ Control-V <Esc>.
  23. #  Bram Moolenaar указывает, что этот скрипт может не работать с 'vim',
  24. #+ из-за возможных проблем взаимодействия с терминалом.
  25. exit 0

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

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

  1. #!/bin/bash
  2. wall <<zzz23EndOfMessagezzz23
  3. Пошлите, по электронной почте, ваш заказ на пиццу, системному администратору.
  4.     (Добавьте дополнительный доллар, если вы желаете положить на пиццу анчоусы или грибы.)
  5. # Внимание: строки комментария тоже будут переданы команде 'wall' как часть текста.
  6. zzz23EndOfMessagezzz23
  7. # Возможно, более эффективно это может быть сделано так:
  8. #         wall <message-file
  9. # Однако, встроенный документ помогает сэкономить ваши силы и время.
  10. exit 0

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

  1. #!/bin/bash
  2. # Команда 'echo' прекрасно справляется с выводом однострочных сообщений,
  3. # но иногда необходимо вывести несколько строк.
  4. # Команда 'cat' и встроенный документ помогут вам в этом.
  5. cat <<End-of-message
  6. -------------------------------------
  7. Это первая строка сообщения.
  8. Это вторая строка сообщения.
  9. Это третья строка сообщения.
  10. Это четвертая строка сообщения.
  11. Это последняя строка сообщения.
  12. -------------------------------------
  13. End-of-message
  14. exit 0
  15. #--------------------------------------------
  16. # Команда "exit 0", выше, не позволить исполнить нижележащие строки.
  17. # S.C. отмечает, что следующий код работает точно так же.
  18. echo "-------------------------------------
  19. Это первая строка сообщения.
  20. Это вторая строка сообщения.
  21. Это третья строка сообщения.
  22. Это четвертая строка сообщения.
  23. Это последняя строка сообщения.
  24. -------------------------------------"
  25. # Однако, в этом случае, двойные кавычки в теле сообщения, должны экранироваться.

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

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

  1. #!/bin/bash
  2. # То же, что и предыдущий сценарий, но...
  3. #  Символ "-", начинающий строку-ограничитель встроенного документа: <<-
  4. #  подавляет вывод символов табуляции, которые могут встречаться в теле документа,
  5. #  но не пробелов.
  6. cat <<-ENDOFMESSAGE
  7.         Это первая строка сообщения.
  8.         Это вторая строка сообщения.
  9.         Это третья строка сообщения.
  10.         Это четвертая строка сообщения.
  11.         Это последняя строка сообщения.
  12. ENDOFMESSAGE
  13. # Текст, выводимый сценарием, будет смещен влево.
  14. # Ведущие символы табуляции не будут выводиться.
  15. # Вышеприведенные 5 строк текста "сообщения" начинаются с табуляции, а не с пробелов.
  16. exit 0

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

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

  1. #!/bin/bash
  2. # Вывод встроенного документа командой 'cat', с использованием подстановки параметров.
  3. # Попробуйте запустить сценарий без аргументов,   ./scriptname
  4. # Попробуйте запустить сценарий с одним аргументом,   ./scriptname Mortimer
  5. # Попробуйте запустить сценарий с одним аргументом, из двух слов, в кавычках,
  6. #                           ./scriptname "Mortimer Jones"
  7. CMDLINEPARAM=1     # Минимальное число аргументов командной строки.
  8. if [ $# -ge $CMDLINEPARAM ]
  9. then
  10.   NAME=$1          # Если аргументов больше одного,
  11.                    # то рассматривается только первый.
  12. else
  13.   NAME="John Doe"  # По-умолчанию, если сценарий запущен без аргументов.
  14. fi
  15. RESPONDENT="автора этого сценария"
  16. cat <<Endofmessage
  17. Привет, $NAME!
  18. Примите поздравления от $RESPONDENT.
  19. # Этот комментарий тоже выводится (почему?).
  20. Endofmessage
  21. # Обратите внимание на то, что пустые строки тоже выводятся.
  22. exit 0

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

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

  1. #!/bin/bash
  2. # upload.sh
  3. # Передача пары файлов (Filename.lsm, Filename.tar.gz)
  4. # на Sunsite (ibiblio.org).
  5. E_ARGERROR=65
  6. if [ -z "$1" ]
  7. then
  8.   echo "Порядок использования: `basename $0` filename"
  9.   exit $E_ARGERROR
  10. fi
  11. Filename=`basename $1`           # Отсечь имя файла от пути к нему.
  12. Server="ibiblio.org"
  13. Directory="/incoming/Linux"
  14. # Вообще, эти строки должны бы не "зашиваться" жестко в сценарий,
  15. # а приниматься в виде аргумента из командной строки.
  16. Password="your.e-mail.address"   # Измените на свой.
  17. ftp -n $Server <<End-Of-Session
  18. # Ключ -n запрещает автоматическую регистрацию (auto-logon)
  19. user anonymous "$Password"
  20. binary
  21. bell                # "Звякнуть" после передачи каждого файла
  22. cd $Directory
  23. put "$Filename.lsm"
  24. put "$Filename.tar.gz"
  25. bye
  26. End-Of-Session
  27. exit 0

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

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

  1. #!/bin/bash
  2. # Вывод встроенного документа командой 'cat', с запретом подстановки параметров.
  3. NAME="John Doe"
  4. RESPONDENT="автора этого сценария"
  5. cat <<'Endofmessage'
  6. Привет, $NAME.
  7. Примите поздравления от $RESPONDENT.
  8. Endofmessage
  9. #  Подстановка параметров не производится, если строка ограничитель
  10. #  заключена в кавычки или экранирована.
  11. #  Тот же эффект дают:
  12. #  cat <<"Endofmessage"
  13. #  cat <<\Endofmessage
  14. exit 0

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

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

  1. #!/bin/bash
  2. # generate-script.sh
  3. # Автор идеи: Albert Reiner.
  4. OUTFILE=generated.sh         # Имя нового сценария.
  5. # -----------------------------------------------------------
  6. # 'Встроенный документ' содержит тело создаваемого сценария.
  7. (
  8. cat <<'EOF'
  9. #!/bin/bash
  10. echo "Этот сценарий сгенерирован автоматически."
  11. #  Обратите внимание: поскольку действия происходят в подоболочке,
  12. #+ мы не можем получить доступ к переменным родительской оболочки.
  13. #  Удостоверимся в этом...
  14. echo "Файл сценария был назван: $OUTFILE"  # Не работает.
  15. a=7
  16. b=3
  17. let "c = $a * $b"
  18. echo "c = $c"
  19. exit 0
  20. EOF
  21. ) > $OUTFILE
  22. # -----------------------------------------------------------
  23. #  Заключение 'строки-ограничителя' предотвращает подстановку значений переменных
  24. #+ в теле 'встроенного документа.'
  25. #  Что позволяет записать все строки в выходной файл "один к одному".
  26. if [ -f "$OUTFILE" ]
  27. then
  28.   chmod 755 $OUTFILE
  29.   # Дать право на исполнение.
  30. else
  31.   echo "Не могу создать файл: \"$OUTFILE\""
  32. fi
  33. #  Этот метод можно использовать для создания
  34. #+ Makefile-ов, программ на языках C, Perl, Python
  35. #+ и т.п..
  36. exit 0

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

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


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

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

  1. #!/bin/bash
  2. # here-function.sh
  3. GetPersonalData ()
  4. {
  5.   read firstname
  6.   read lastname
  7.   read address
  8.   read city
  9.   read state
  10.   read zipcode
  11. } # Это немного напоминает интерактивную функцию, но...
  12. # Передать ввод в функцию.
  13. GetPersonalData <<RECORD001
  14. Bozo
  15. Bozeman
  16. 2726 Nondescript Dr.
  17. Baltimore
  18. MD
  19. 21226
  20. RECORD001
  21. echo
  22. echo "$firstname $lastname"
  23. echo "$address"
  24. echo "$city, $state $zipcode"
  25. echo
  26. exit 0

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

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

  1. #!/bin/bash
  2. : <<TESTVARIABLES
  3. ${HOSTNAME?}${USER?}${MAIL?}  # Если одна из переменных не определена, то выводится сообщение об ошибке.
  4. TESTVARIABLES
  5. exit 0

Tip

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

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

  1. #!/bin/bash
  2. # commentblock.sh
  3. : << COMMENTBLOCK
  4. echo "Эта строка не будет выведена."
  5. Эта строка комментария не начинается с символа "#".
  6. Это еще одна строка комментария, которая начинается не с символа "#".
  7. &*@!!++=
  8. Эта строка не вызовет ошибки,
  9. поскольку Bash проигнорирует ее.
  10. COMMENTBLOCK
  11. echo "Код завершения  \"COMMENTBLOCK\" = $?."   # 0
  12. # Показывает, что ошибок не возникало.
  13. #  Такая методика создания блочных комментариев
  14. #+ может использоваться для комментирования блоков кода во время отладки.
  15. #  Это экономит силы и время, т.к. не нужно втавлять символ "#" в начале каждой строки,
  16. #+ а затем удалять их.
  17. : << DEBUGXXX
  18. for file in *
  19. do
  20.  cat "$file"
  21. done
  22. DEBUGXXX
  23. exit 0
Tip

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

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

  1. #!/bin/bash
  2. # self-document.sh: сценарий со встроенной справкой
  3. # Модификация сценария "colm.sh".
  4. DOC_REQUEST=70
  5. if [ "$1" = "-h"  -o "$1" = "--help" ]     # Request help.
  6. then
  7.   echo; echo "Порядок использования: $0 [directory-name]"; echo
  8.   sed --silent -e '/DOCUMENTATIONXX$/,/^DOCUMENTATION/p' "$0" |
  9.   sed -e '/DOCUMENTATIONXX/d'; exit $DOC_REQUEST; fi
  10. : << DOCUMENTATIONXX
  11. Сценарий выводит сведения о заданном каталоге в виде таблице.
  12. -------------------------------------------------------------
  13. Сценарию необходимо передать имя каталога. Если каталог не
  14. указан или он недоступен для чтения, то выводятся сведения
  15. о текущем каталоге.
  16. DOCUMENTATIONXX
  17. if [ -z "$1" -o ! -r "$1" ]
  18. then
  19.   directory=.
  20. else
  21.   directory="$1"
  22. fi
  23. echo "Сведения о каталоге "$directory":"; echo
  24. (printf "PERMISSIONS LINKS OWNER GROUP SIZE MONTH DAY HH:MM PROG-NAME\n" \
  25. ; ls -l "$directory" | sed 1d) | column -t
  26. exit 0
Note

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

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


Caution

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

Warning

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

  1. #!/bin/bash
  2. echo "----------------------------------------------------------------------"
  3. cat <<LimitString
  4. echo "Это первая строка сообщения во встроенном документе."
  5. echo "Это вторая строка сообщения во встроенном документе."
  6. echo "Это последняя строка сообщения во встроенном документе."
  7.      LimitString
  8. #^^^^Отступ перед строкой-ограничителем. Ошибка!
  9. #    Этот сценарий будет вести себя не так как вы ожидаете.
  10. echo "----------------------------------------------------------------------"
  11. #  "Этот комментарий находится за пределами 'встроенного документа',
  12. #+ и не должен выводиться.
  13. echo "За пределами встроенного документа."
  14. exit 0
  15. echo "Держу пари, что эта строка не будет выведена."  # Стоит после команды 'exit'.


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