Наши партнеры








Книги по Linux (с отзывами читателей)

Библиотека сайта rus-linux.net

На главную -> MyLDP -> Электронные книги по ОС Linux
Руководство по Bash для начинающих
Назад Глава 8: Пишем интерактивные скрипты Вперед

Получение данных, вводимых пользователем

Использование встроенной команды read

Встроенная команда read является противоположностью командам echo и printf. Синтаксис команды read следующий:

read [options] NAME1 NAME2 ... NAMEN 

Из стандартного входного потока или из дескриптора файла, указываемого в качестве аргумента в параметре -u, считывается одна строка. Первое слово из строки присваивается первому имени NAME1, второе слово — второму имени и так далее, оставшиеся слова и имеющиеся между ними разделители назначаются последнему имени NAMEN. Если слов, считываемых из входного потока меньше, чем имен, оставшимся именам присваиваются пустые значения.

Для разбиения входной строки на слова или лексемы используются символы, хранящиеся в переменной IFS; смотрите раздел "Разбиение на слова". Для отмены особого свойства следующего символа и для продолжения строки можно использовать символ обратного слеша.

Если имена не указываются, то считанная строка назначается переменной REPLY.

Код возврата команды read равен нулю, если не встретился символ конца файла, если не возник таймаут команды read или если дескриптор файла, указанный в качестве аргумента параметра -u, не оказался недопустимым.

Во встроенной команде read поддерживаются следующие параметры:

Таблица 8.2. Параметры встроенной команды read

ПараметрЗначение

-a ANAME

Слова присваиваются подряд идущим элементам массива ANAME, присваивание начинается с элемента с индексом 0. Перед присваиванием все элементы удаляются из ANAME. Другие аргументы NAME игнорируются

-d DELIM

В качестве завершающего элемента входной строки используется символ DELIM, а не символ новой строки.

-e

Для чтения строки используется программа readline.

-n NCHARS

Команда read возвращает управление после чтения символов NCHARS и не ждет завершения ввода строки.

-p PROMPT

Перед попыткой прочитать какие-нибудь входные данные, сначала выдается PROMPT, который не завершается символом перевода строки. Эта строка приглашения отображается только тогда, когда входные данные поступают из терминала.

-r

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

-s

Тихий режим. Если входные данные поступают из терминала, на терминал не выдается эхо-ответ.

-t TIMEOUT

Если в течение TIMEOUT секунд не будет завершено чтение входной строки, то для команды read возникнет состояние таймаута и будет возвращен код неудачного выполнения команды. Этот параметр не действует, если команда read читает входные данные не из терминала и не из конвейера.

-u FD

Чтение входных данных из дескиптора файла FD.

Это простой пример представляет собой улучшенный вариант скрипта leaptest.sh из предыдущей главы:

michel ~/test> cat leaptest.sh
#!/bin/bash
# This script will test if you have given a leap year or not.

echo "Type the year that you want to check (4 digits), followed by [ENTER]:"

read year

if (( ("$year" % 400) == "0" )) || (( ("$year" % 4 == "0") && ("$year" % 100 !=
"0") )); then
  echo "$year is a leap year."
else
  echo "This is not a leap year."
fi

michel ~/test> leaptest.sh
Type the year that you want to check (4 digits), followed by [ENTER]:
2000
2000 is a leap year.

Приглашение пользователю ввести данные

В следующем примере показано, как можно использовать приглашения, объясняющие, что пользователь должен ввести.

michel ~/test> cat friends.sh
#!/bin/bash

# This is a program that keeps your address book up to date.

friends="/var/tmp/michel/friends"

echo "Hello, "$USER".  This script will register you in Michel's friends database."

echo -n "Enter your name and press [ENTER]: "
read name
echo -n "Enter your gender and press [ENTER]: "
read -n 1 gender
echo

grep -i "$name" "$friends"

if  [ $? == 0 ]; then
  echo "You are already registered, quitting."
  exit 1
elif [ "$gender" == "m" ]; then
  echo "You are added to Michel's friends list."
  exit 1
else
  echo -n "How old are you? "
  read age
  if [ $age -lt 25 ]; then
    echo -n "Which colour of hair do you have? "
    read colour
    echo "$name $age $colour" >> "$friends" 
    echo "You are added to Michel's friends list.  Thank you so much!"
  else
    echo "You are added to Michel's friends list."
    exit 1
  fi
fi

michel ~/test> cp friends.sh /var/tmp; cd /var/tmp

michel ~/test> touch friends; chmod a+w friends

michel ~/test> friends.sh
Hello, michel.  This script will register you in Michel's friends database.
Enter your name and press [ENTER]: michel
Enter your gender and press [ENTER] :m
You are added to Michel's friends list.

michel ~/test> cat friends

Обратите внимание, что здесь не показаны строки, выдаваемые скриптом. Скрипт только запоминает информацию о людях, которыми интересуется Мишель, но скрипт всегда сообщит вам, что вы добавлены в список, если вас в нем не было.

Теперь другие могут запустить скрипт:

[anny@octarine tmp]$ friends.sh
Hello, anny.  This script will register you in Michel's friends database.
Enter your name and press [ENTER]: anny
Enter your gender and press [ENTER] :f
How old are you? 22
Which colour of hair do you have? black
You are added to Michel's friends list.

Через некоторое время список friends будет выглядеть следующим образом:

tille 24 black
anny 22 black
katya 22 blonde
maria 21 black
--output omitted--

Конечно, эта ситуация не идеальна, поскольку каждый может редактировать (но не удалять) файлы Мишеля. Вы можете решить эту проблему с помощью специальных режимов доступа к файлу скрипта; смотрите описание SUID и SGID в руководстве "Введение в Linux".

Перенаправление и дескрипторы файлов

Общие положения

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

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

Ввод из файла и вывод в файл сопровождается обработкой целых чисел, с помощью которых отслеживаются все файлы, открываемые в данном процессе. Эти числовые значения называются дескрипторами файлов. Самыми известными являются дескрипторы файлов stdin, stdout и stderr, номера дескрипторов файлов для которых равны 0, 1 и 2, соответственно. Под этими номерами зарезервированы соответствующие им устройства. В Bash в качестве дескрипторов файлов можно также использовать порты TCP и UDP сетевых хостов.

В приведенных ниже выходных данных показывается, как зарезервированные дескрипторы файлов указывают на фактически имеющиеся устройства:

michel ~> ls -l /dev/std*
lrwxrwxrwx  1 root    root     17 Oct  2 07:46 /dev/stderr -> ../proc/self/fd/2
lrwxrwxrwx  1 root    root     17 Oct  2 07:46 /dev/stdin -> ../proc/self/fd/0
lrwxrwxrwx  1 root    root     17 Oct  2 07:46 /dev/stdout -> ../proc/self/fd/1

michel ~> ls -l /proc/self/fd/[0-2]
lrwx------  1 michel  michel   64 Jan 23 12:11 /proc/self/fd/0 -> /dev/pts/6
lrwx------  1 michel  michel   64 Jan 23 12:11 /proc/self/fd/1 -> /dev/pts/6
lrwx------  1 michel  michel   64 Jan 23 12:11 /proc/self/fd/2 -> /dev/pts/6

Обратите внимание, что каждый процесс имеет в /proc/self свое собственное представление, т.к. фактически это символическая ссылка на /proc/<идентификатор процесса>.

Вы можете с помощью info MAKEDEV и info proc посмотреть дополнительную информацию о директории /proc и о том, что в вашей системе в каждом запущенном процессе делается с дескрипторами стандартных файлов.

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

  • Если стандартный вывод предыдущей команды подключен через конвейер к стандартному входу текущей команды, то /proc/<идентификатор текущего процесса>/fd/0 будет заменен на тот же самый анонимный конвейер, который использовался для /proc/<идентификатор предыдущего процесса>/fd/1.
  • Если стандартный вывод текущей команды подключен через конвейер к стандартному входу следующей команды, /proc/<идентификатор текущего процесса>/fd/1 будет заменен на еще один анонимный конвейер.
  • Перенаправление для текущей команды обрабатываются слева направо
  • Перенаправление "N>&M" или "N<&M", указываемое после команды, представляет собой создание или изменении символической ссылки /proc/self/fd/N, так чтобы она ссылалась туда, куда и /proc/self/fd/M.
  • Перенаправление "N>файл" и "N<файл" представляет собой создание или изменение символической ссылки /proc/self/fd/N, так чтобы она указывала на целевой файл.
  • Если указывается "N>&-", то это вызывает удаление символической ссылки /proc/self/fd/N.
  • Только после этого происходит выполнение текущей команды.

Когда скрипт запускается из командной строки, то ничего меняться не будет, поскольку процесс в дочерней командной оболочке будет использовать те же самые дескрипторы файлов, что и в родительской оболочке. Когда родительская оболочка отсутствует, например, когда вы запускаете скрипт с помощью cron, дескрипторами стандартных файлов будут конвейеры или другие (временные) файлы, если не используется какой-нибудь вариант перенаправления. Это демонстрируется в приведенном ниже примере, в котором показан выход простого скрипта at:

michel ~> date
Fri Jan 24 11:05:50 CET 2003

michel ~> at 1107
warning: commands will be executed using (in order) 
a) $SHELL b) login shell c)/bin/sh
at> ls -l /proc/self/fd/ > /var/tmp/fdtest.at
at> <EOT>
job 10 at 2003-01-24 11:07

michel ~> cat /var/tmp/fdtest.at
total 0
lr-x------    1 michel michel  64 Jan 24 11:07 0 -> /var/spool/at/!0000c010959eb (deleted)
l-wx------    1 michel michel  64 Jan 24 11:07 1 -> /var/tmp/fdtest.at
l-wx------    1 michel michel  64 Jan 24 11:07 2 -> /var/spool/at/spool/a0000c010959eb
lr-x------    1 michel michel  64 Jan 24 11:07 3 -> /proc/21949/fd

И с cron:

michel ~> crontab -l
# DO NOT EDIT THIS FILE - edit the master and reinstall.
# (/tmp/crontab.21968 installed on Fri Jan 24 11:30:41 2003)
# (Cron version -- $Id: chap8.xml,v 1.9 2006/09/28 09:42:45 tille Exp $)
32 11 * * * ls -l /proc/self/fd/ > /var/tmp/fdtest.cron

michel ~> cat /var/tmp/fdtest.cron
total 0
lr-x------    1 michel michel  64 Jan 24 11:32 0 -> pipe:[124440]
l-wx------    1 michel michel  64 Jan 24 11:32 1 -> /var/tmp/fdtest.cron
l-wx------    1 michel michel  64 Jan 24 11:32 2 -> pipe:[124441]
lr-x------    1 michel michel  64 Jan 24 11:32 3 -> /proc/21974/fd

Перенаправление ошибок

Из предыдущих примеров понятно, что вы можете для скрипта указать входной и выходной файлы (подробности смотрите в разделе "Ввод из файла и вывод в файл"), но некоторые могут забыть о перенаправлении ошибок - выход, который может позже потребоваться. К тому же, если повезет, ошибки будут отсылаться вам по почте и, возможно, удастся выявить возможные причины ошибок. Если вы не настолько удачный, ошибки могут привести к отказу в работе скрипта, а обнаружить и отослать ошибку не удастся, из-за чего вы не сможете выполнить какую-либо отладку.

Обратите внимание, что при перенаправлении ошибок важен порядок выполнения действий. Например, следующая команда, которая есть в /var/spool

ls -l * 2> /var/tmp/unaccessible-in-spool

будет перенаправлять стандартный выходной поток команды ls в файл unaccessible-in-spool, находящийся в /var/tmp. Команда

ls -l * > /var/tmp/spoollist 2>&1

будет перенаправлять стандартный ввод и стандартный поток ошибок в spoollist. Команда

ls -l * 2 >& 1 > /var/tmp/spoollist

перенаправляет в файл только стандартный вывод, поскольку перед тем, как стандартный вывод будет перенаправлен, стандартный поток ошибок копируется в стандартный вывод.

Для удобства, ошибки, если есть уверенность в том, что они не нужны, часто перенаправляются в /dev/null. Среди скриптов запуска, имеющихся в вашей системе, можно найти сотни примеров.

Bash позволяет перенаправить в файл как стандартный вывод, так и стандартный вывод ошибок; имя файла FILE будет подставлено в следующую конструкцию:

&> FILE 

Это эквивалентно конструкции > FILE 2>&1, которая использовалась в предыдущих примерах. Кроме того, она объединяется с перенаправлением в /dev/null, например, когда вы просто хотите выполнять команду, независимо от того, будут ошибки или нет.

Ввод из файла и вывод в файл

Использование /dev/fd

В директории /dev/fd находятся записи с именами 0, 1, 2 и так далее. Открытие файла /dev/fd/N эквивалентно дублированию дескриптора файла N. Если в вашей системе есть /dev/stdin, /dev/stdout и /dev/stderr, вы сможете увидеть, что они, соответственно, эквивалентны /dev/fd/0, /dev/fd/1 и /dev/fd/2.

Файлы директория /dev/fd используются, главным образом, из командной оболочки. Этот механизм позволяет программам, которые используют пути к файлам в качестве аргументов, обрабатывать стандартный вход и стандартный выход точно также, как и пути к другим файлам. Если в системе нет директория /dev/fd, вам потребуется искать способ обойти проблему. Это можно сделать, например, при помощи дефиса (-), указывающего, что программа должна читать из конвейера. Например:

michel ~> filter body.txt.gz | cat header.txt - footer.txt
This text is printed at the beginning of each print job and thanks the sysadmin
for setting us up such a great printing infrastructure.

Text to be filtered.

This text is printed at the end of each print job.

Команда cat сначала читает файл header.txt, затем свой собственный стандартный ввод, который является выходом команды filter, и, затем, файл footer.txt. Во многих программах не учитывается особое свойство дефиса, который, когда он используется в качестве аргумента командной строки, является ссылкой на стандартный ввод или стандартный вывод. Проблемы также могут возникнуть, если дефис используется в качестве первого аргумента, т. к. он может рассматриваться как параметр предыдущей команды. Использование /dev/fd ведет к единообразию и предотвращает путаницу:

michel ~> filter body.txt | cat header.txt /dev/fd/0 footer.txt | lp

В этом примере все выходные данные дополнительно через конвейер направляются в команду lp, которая отправляет их на используемый по умолчанию принтер.

Read и exec

Назначение файлам дескрипторов

Другой взгляд на файловые дескрипторы - рассматривать их как способ назначить файлу числовое значение. Вместо того, чтобы пользоваться именем файла, вы можете пользоваться номером дескриптора файла. Для замены текущего процесса или для изменения дескриптора файла можно пользоваться встроенной командой exec. Например, ею можно пользоваться при назначении файлу дескриптора. Используйте команду

exec fdN> file

для назначения дескриптора N выходному файлу file и

exec fdN< file 

для назначения дескриптора N входному файлу file. После назначения файлу дескриптора, дескриптор можно использовать с командами перенаправления так, как это показано в следующем примере:

michel ~> exec 4> result.txt

michel ~> filter body.txt | cat header.txt /dev/fd/0 footer.txt >& 4

michel ~> cat result.txt
This text is printed at the beginning of each print job and thanks the sysadmin
for setting us up such a great printing infrastructure.

Text to be filtered.

This text is printed at the end of each print job.
Дескриптор файла 5

Использование этого дескриптора файла может быть причиной проблем, смотрите главу 16 в книге "Advanced Bash-Scripting Guide" ("Искусство программирования на языке сценариев командной оболочки"). Мы настоятельно рекомендуем его не использовать.

Команда read в скриптах

Ниже приведен пример, в котором показано, каким образом можно чередовать ввод данных из файла и из командной строки:

michel ~/testdir> cat sysnotes.sh
#!/bin/bash

# This script makes an index of important config files, puts them together in
# a backup file and allows for adding comment for each file.

CONFIG=/var/tmp/sysconfig.out
rm "$CONFIG" 2>/dev/null

echo "Output will be saved in $CONFIG."

# create fd 7 with same target as fd 0 (save stdin "value")
exec 7<&0

# update fd 0 to target file /etc/passwd
exec < /etc/passwd

# Read the first line of /etc/passwd
read rootpasswd

echo "Saving root account info..."
echo "Your root account info:" >> "$CONFIG"
echo $rootpasswd >> "$CONFIG"

# update fd 0 to target fd 7 target (old fd 0 target); delete fd 7
exec 0<&7 7<&-

echo -n "Enter comment or [ENTER] for no comment: "
read comment; echo $comment >> "$CONFIG"

echo "Saving hosts information..."

# first prepare a hosts file not containing any comments
TEMP="/var/tmp/hosts.tmp"
cat /etc/hosts | grep -v "^#" > "$TEMP"

exec 7<&0
exec < "$TEMP"

read ip1 name1 alias1
read ip2 name2 alias2

echo "Your local host configuration:" >> "$CONFIG"

echo "$ip1 $name1 $alias1" >> "$CONFIG"
echo "$ip2 $name2 $alias2" >> "$CONFIG"

exec 0<&7 7<&-

echo -n "Enter comment or [ENTER] for no comment: "
read comment; echo $comment >> "$CONFIG"
rm "$TEMP"

michel ~/testdir> sysnotes.sh
Output will be saved in /var/tmp/sysconfig.out.
Saving root account info...
Enter comment or [ENTER] for no comment: hint for password: blue lagoon
Saving hosts information...
Enter comment or [ENTER] for no comment: in central DNS

michel ~/testdir> cat /var/tmp/sysconfig.out
Your root account info:
root:x:0:0:root:/root:/bin/bash
hint for password: blue lagoon
Your local host configuration:
127.0.0.1 localhost.localdomain localhost
192.168.42.1 tintagel.kingarthur.com tintagel
in central DNS

Закрытие дескрипторов файла

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

exec fd<&- 

В приведенном выше примере, дескриптор файла 7, который был назначен стандартному вводу, закрывается каждый раз, когда пользователю необходим доступ к реальному устройству стандартного ввода, как правило, к клавиатуре.

Ниже приводится простой пример перенаправления в конвейер только стандартного потока ошибок:

michel ~> cat listdirs.sh
#!/bin/bash

# This script prints standard output unchanged, while standard error is 
# redirected for processing by awk.

INPUTDIR="$1"

# fd 6 targets fd 1 target (console out) in current shell
exec 6>&1

# fd 1 targets pipe, fd 2 targets fd 1 target (pipe),
# fd 1 targets fd 6 target (console out), fd 6 closed, execute ls
ls "$INPUTDIR"/* 2>&1 >&6 6>&- \
                                # Closes fd 6 for awk, but not for ls.

| awk 'BEGIN { FS=":" } { print "YOU HAVE NO ACCESS TO" $2 }' 6>&-

# fd 6 closed for current shell
exec 6>&-

Встраиваемые документы (Here documents)

Часто ваш скрипт может вызывать другую программу или скрипт, в которых нужно вводит данные. С помощью встраиваемых документов (here documents) можно указать командной оболочке считывать входные данные из некоторого источника до тех пор, пока в нем не будет найдена строка, содержащая определенную подстроку (ею не могут быть завершающие пробелы). Затем все строки, считанные до этого места, будут использованы в вызванной программе или скрипте в качестве стандартного входного потока.

В результате вам не потребуется вызывать файлы по отдельности; вы можете пользоваться специальными символами командной оболочки и это будет выглядеть гораздо лучше, чем куча команд echo:

michel ~> cat startsurf.sh
#!/bin/bash

# This script provides an easy way for users to choose between browsers.

echo "These are the web browsers on this system:"
 
# Start here document
cat < BROWSERS
mozilla
links
lynx
konqueror
opera
netscape
BROWSERS
# End here document

echo -n "Which is your favorite? "
read browser

echo "Starting $browser, please wait..."
$browser &

michel ~> startsurf.sh
These are the web browsers on this system:
mozilla
links
lynx
konqueror
opera
netscape
Which is your favorite? opera
Starting opera, please wait...

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

#!/bin/bash
 
# This script installs packages automatically, using yum.
 
if [ $# -lt 1 ]; then
        echo "Usage: $0 package."
        exit 1
fi
 
yum install $1 < CONFIRM
y
CONFIRM

И о том, как работает скрипт: когда выдается строка "Is this ok [y/N]" ("Это правильно [y/N]"), скрипт автоматически отвечает "y":

[root@picon bin]# ./install.sh tuxracer
Gathering header information file(s) from server(s)
Server: Fedora Linux 2 - i386 - core
Server: Fedora Linux 2 - i386 - freshrpms
Server: JPackage 1.5 for Fedora Core 2
Server: JPackage 1.5, generic
Server: Fedora Linux 2 - i386 - updates
Finding updated packages
Downloading needed headers
Resolving dependencies
Dependencies resolved
I will do the following:
[install: tuxracer 0.61-26.i386]
Is this ok [y/N]: EnterDownloading Packages
Running test transaction:
Test transaction complete, Success!
tuxracer 100 % done 1/1
Installed:  tuxracer 0.61-26.i386
Transaction(s) Complete

Предыдущий раздел: Оглавление Следующий раздел:
Отображение пользовательских сообщений   Подводим итоги главы 8