User Tools

Site Tools


bash:чтение_ввода_с_клавиатуры

Чтение ввода с клавиатуры

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

#!/bin/bash
 
# test-integer2: проверка целочисленного значения.
 
INT=-5
 
if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
	if [ $INT -eq 0 ]; then
		echo "INT is zero."
 	else
 		if [ $INT -lt 0 ]; then
 			echo "INT is negative."
		else
 			echo "INT is positive."
 		fi
 		if [ $((INT % 2)) -eq 0 ]; then
 			echo "INT is even."
 		else
 			echo "INT is odd."
 		fi
 	fi
else
 	echo "INT is not an integer." >&2
 	exit 1
fi

Каждый раз, когда потребуется изменить значение INT, вы должны будете изменить сценарий. Пользоваться сценарием было бы удобнее, если бы он предлагал пользователю ввести значение. В этой статье мы посмотрим, как придать интерактивность нашим программам.

read — чтение значений со стандартного ввода

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

read [-параметры] [переменная...]

Где параметры – это один или несколько параметров из перечисленных в таблице далее, а переменная – имя одной или нескольких переменных для сохранения введенного значения. Если имя переменной не указано, строка с данными сохраняется в переменной REPLY.

В простейшем случае read сохраняет значения полей, прочитанные со стандартного ввода, в указанные переменные. Ниже показано, как можно изменить наш сценарий проверки целочисленных значений, задействовав в нем команду read:

#!/bin/bash
 
# test-integer: проверка целочисленного значения.
 
echo -n "Please enter an integer -> "
read INT
 
if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
	if [ $INT -eq 0 ]; then
		echo "INT is zero."
 	else
 		if [ $INT -lt 0 ]; then
 			echo "INT is negative."
		else
 			echo "INT is positive."
 		fi
 		if [ $((INT % 2)) -eq 0 ]; then
 			echo "INT is even."
 		else
 			echo "INT is odd."
 		fi
 	fi
else
 	echo "INT is not an integer." >&2
 	exit 1
fi

Сначала мы использовали команду echo с параметром <html>-n</html> (подавляющим вывод символа перевода строки в конце) для вывода приглашения к вводу, а затем команду read для ввода значения в переменную INT. Запуск этого сценария приводит к следующим результата

Please enter an integer -> -3
INT is negative.
INT is odd.

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

#!/bin/bash
 
# read-multiple: чтение нескольких значений с клавиатуры
 
echo -n "Enter one or more values > "
read var1 var2 var3 var4 var5
 
echo "var1 = '$var1'"
echo "var2 = '$var2'"
echo "var3 = '$var3'"
echo "var4 = '$var4'"
echo "var5 = '$var5'"

Этот сценарий вводит, присваивает переменным и выводит до пяти значений. Обратите внимание, как действует команда read, когда получает разное число значений:

read-multiple
Enter one or more values > a b
var1 = 'a'
var2 = 'b'
var3 = ''
var4 = ''
var5 = ''
 
read-multiple
Enter one or more values > a b c d e f g
var1 = 'a'
var2 = 'b'
var3 = 'c'
var4 = 'd'
var5 = 'e f g'

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

Если не передать переменные команде read, весь ввод будет сохранен в переменной командной оболочки REPLY

#!/bin/bash
 
# read-single: чтение множества значений в переменную по умолчанию
 
echo -n "Enter one or more values > "
read
 
echo "REPLY = '$REPLY'"

Запуск этого сценария приводит к следующим результатам:

read-single
Enter one or more values > 1 2 3
REPLY = '1 2 3'

Параметры

read поддерживает параметры, перечисленные в таблице ниже

Параметр Описание
<html>-a массив</html> Сохранить ввод в указанный массив, начиная с элемента с индексом 0.
<html>-d разделитель</html> Использовать в качестве признака конца ввода первый символ из строки разделитель, а не символ перевода строки
<html>-e</html> Использовать Readline для обработки ввода. Это позволяет редактировать ввод, как в командной строке
<html>-n число</html> Прочитать указанное число символов, а не всю строку
<html>-p приглашение</html> Показывать указанное приглашение к вводу
<html>-r</html> Режим без промежуточной обработки. Не интерпретировать обратные слеши как экранирующие символы
<html>-s</html> Безмолвный режим. Не производить эхо-вывод символов на экран в процессе ввода. Этот режим может пригодиться для организации ввода паролей и другой конфиденциальной информации
<html>-t секунды</html> Предельное время ожидания. Завершить ввод по истечении указанного числа секунд. По истечении указанного интервала read вернет ненулевое значение
<html>-u дескриптор</html> Произвести ввод из файла с указанным дескриптором вместо стандартного ввода

Множество поддерживаемых параметров открывает доступ к довольно интересным способам использования read. Например, параметр <html>-p</html> позволяет определить строку приглашения к вводу:

#!/bin/bash
 
# read-single: чтение множества значений в переменную по умолчанию
 
read -p "Enter one or more values > "
 
echo "REPLY = '$REPLY'"

Параметры <html>-t</html> и <html>-s</html> позволяют писать сценарии, реализующие ввод «секретных» данных и прерывающие ввод по истечении заданного времени:

#!/bin/bash
 
# read-secret: ввод секретного пароля
 
if read -t 10 -sp "Enter secret passphrase > " secret_pass; then
	echo -e "\nSecret passphrase = '$secret_pass'"
else
 	echo -e "\nInput timed out" >&2
 	exit 1
fi

Сценарий предлагает пользователю ввести секретный пароль и ждет 10 секунд. Если в течение этого времени ввод не был завершен, сценарий завершается с кодом ошибки. Поскольку в команду включен параметр -s, символы пароля не выводятся на экран в процессе ввода.

С помощью параметров <html>-e</html> и <html>-i</html> можно предложить пользователю значение по умолчанию для ввода.

#!/bin/bash
 
# read-default: предложение значения по умолчанию, для ввода 
# которого достаточно нажать клавишу Enter.
 
read -e -p "What is your user name? " -i $USER
 
echo "You answered: '$REPLY'"

Этот сценарий просит ввести имя учетной записи и использует переменную окружения USER, чтобы предложить значение по умолчанию. После запуска сценарий выведет строку со значением по умолчанию, и если пользователь просто нажмет ENTER, команда read присвоит это значение переменной REPLY.

read_default
What is your user name? nevvad
You answered: 'nevvad'

Выделение полей в строке ввода с помощью IFS

Обычно командная оболочка разбивает введенную строку на слова перед передачей команде read. Как мы уже знаем, в результате этого слова в строке, разделенные одним или несколькими пробелами, превращаются в отдельные значения и присваиваются командой read разным переменным. Такое поведение командной оболочки регулируется переменной с именем IFS (от Internal Field Separator — внутренний разделитель полей). По умолчанию переменная IFS хранит символы пробела, табуляции и перевода строки, каждый из которых может служить разделителем полей.

Изменяя значение переменной IFS, можно управлять делением ввода на поля перед передачей команде read. Например, файл <html>/etc/passwd</html> хранит строки данных, в которых поля отделяются друг от друга двоеточием. Присвоив переменной IFS значение, состоящее из единственного двоеточия, можно с помощью read прочитать содержимое <html>/etc/passwd</html> и благополучно разделить строки на поля для присваивания разным переменным. Ниже приводится сценарий, который именно так и действует:

#!/bin/bash
 
# read-ifs: чтение полей из файла
 
FILE=/etc/passwd
 
read -p "Enter a username > " user_name
 
file_info=$(grep "^$user_name:" $FILE)
 
if [ -n "$file_info" ]; then
	IFS=":" read user pw uid gid name home shell <<< "$file_info"
 	echo "User = '$user'"
 	echo "UID = '$uid'"
 	echo "GID = '$gid'"
 	echo "Full Name = '$name'"
 	echo "Home Dir. = '$home'"
 	echo "Shell = '$shell'"
else
 	echo "No such user '$user_name'" >&2
 	exit 1
fi

Этот сценарий предлагает пользователю ввести имя учетной записи в системе и затем выводит разные поля, найденные в соответствующей записи в файле <html>/etc/passwd</html>. В сценарии есть две интересные строки. Первая:

file_info=$(grep "^$user_name:" $FILE)

присваивает результат команды grep переменной file_info. Регулярное выражение гарантирует извлечение из файла <html>/etc/passwd</html> единственной строки, соответствующей введенному имени пользователя.

Вторая интересная строка:

IFS=":" read user pw uid gid name home shell <<< "$file_info"

состоит из трех частей: присваивания значения переменной, команды read со списком имен переменных в виде аргументов и незнакомого нам, нового оператора перенаправления. Рассмотрим сначала присваивание значения переменной.

Командная оболочка позволяет выполнить одну или несколько операций присваивания значений переменным непосредственно перед командой, в той же строке. Они изменяют окружение, в котором выполняется команда. Действие этих операций присваивания носит временный характер, окружение изменяется только на время выполнения команды. В данном случае в переменной IFS сохраняется двоеточие. То же самое можно выразить иначе:

OLD_IFS="$IFS"
IFS=":"
read user pw uid gid name home shell <<< "$file_info"
IFS="$OLD_IFS"

Здесь мы сохранили прежнее значение IFS, присвоили новое значение, выполнили команду read и восстановили прежнее значение IFS. Очевидно, что размещение операции присваивания перед командой позволяет получить более компактный код, действующий точно так же.

Оператор <html>««/html> отмечает встроенную строку. Встроенная строка (here string) подобна встроенному документу, только короче, она простирается лишь до конца текущей строки кода. В данном примере строка с данными из файла <html>/etc/passwd</html> подается на стандартный ввод команды read. У кого-то может возникнуть вопрос, почему был выбран такой, несколько необычный, способ вместо

echo "$file_info" | IFS=":" read user pw uid gid name home shell

На это есть особые причины…

Проверка ввода

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

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

Отказ от защитных мер ради экономии простителен, только если программа пишется для однократного использования автором с целью решения некоей специальной задачи. Но даже в этом случае, если программа выполняет потенциально опасные операции, такие как удаление файлов, на всякий случай включите в нее проверку данных.
<callout type=“primary” icon=“true” title=“READ Нельзя использовать в конвейере”>Даже притом, что команда read способна принимать данные со стандартного ввода, она не позволяет использовать ее следующим образом:

echo "foo" | read

Можно было бы ожидать, что этот прием сработает, но это не так. Внешне все будет выглядеть так, как будто команда успешно отработала, но при этом переменная REPLY всегда будет оставаться пустой. Почему?

Объясняется это особенностью обработки конвейеров командной оболочкой. В bash (и в других командных оболочках, таких как sh) конвейеры создают подоболочки(subshells). Они являются копиями родительской оболочки и ее окружения и используются для выполнения команд в конвейерах. В предыдущем примере команда read выполняется в подоболочке.

Для подоболочек в Unix-подобных системах создаются копии родительского окружения, которые они и используют в работе. Когда конвейер завершается, копия окружения уничтожается. Это означает, что подоболочка никогда не сможет изменить окружение родительского процесса. Как мы знаем, read присваивает значения переменным, которые становятся частью окружения. В примере выше read присвоит значение foo переменной REPLY в окружении подоболочки, но когда конвейер завершится, подоболочка и ее окружение будут уничтожены, а результат присваивания будет утрачен.

Использование встроенных строк — один из способов обойти эту проблему. </callout> Далее приводится пример программы, проверяющий входные данные разного вида:

#!/bin/bash
 
# read-validate: проверка ввода
 
invalid_input () {
	echo "Invalid input '$REPLY'" >&2
 	exit 1
}
 
read -p "Enter a single item > "
 
# пустой ввод (недопустимо)
[[ -z "$REPLY" ]] && invalid_input
 
# ввод множества элементов (недопустимо)
(( "$(echo "$REPLY" | wc -w)" > 1 )) && invalid_input
 
# введено допустимое имя файла?
if [[ "$REPLY" =~ ^[-[:alnum:]\._]+$ ]]; then
	echo "'$REPLY' is a valid filename."
 	if [[ -e "$REPLY" ]]; then
 		echo "And file '$REPLY' exists."
 	else
 		echo "However, file '$REPLY' does not exist."
 	fi
 
 	# введено вещественное число?
 	if [[ "$REPLY" =~ ^-?[[:digit:]]*\.[[:digit:]]+$ ]]; then
 		echo "'$REPLY' is a floating point number."
 	else
 		echo "'$REPLY' is not a floating point number."
 	fi
 
 	# введено целое число?
 	if [[ "$REPLY" =~ ^-?[[:digit:]]+$ ]]; then
 		echo "'$REPLY' is an integer."
 	else
 		echo "'$REPLY' is not an integer."
 	fi
else
	echo "The string '$REPLY' is not a valid filename."
fi

Этот сценарий предлагает пользователю ввести элемент данных и затем последовательно анализирует его содержимое.

Меню

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

Выберите команду:
1. Вывести информацию о системе
2. Вывести информацию о дисковом пространстве
3. Вывести информацию об объеме домашнего каталога
0. Выйти
Введите номер выбранной команды [0-3] >

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

#!/bin/bash
 
# read-menu: программа вывода системной информации,
# управляемая с помощью меню
 
clear
echo "
Please Select:
 
1. Display System Information
2. Display Disk Space
3. Display Home Space Utilization
0. Quit
"
 
read -p "Enter selection [0-3] > "
 
if [[ "$REPLY" =~ ^[0-3]$ ]]; then
	if [[ "$REPLY" == 0 ]]; then
 		echo "Program terminated."
 		exit
 	fi
 	if [[ "$REPLY" == 1 ]]; then
 		echo "Hostname: $HOSTNAME"
 		uptime
 		exit
 	fi
 	if [[ "$REPLY" == 2 ]]; then
 		df -h
 		exit
 	fi
 	if [[ "$REPLY" == 3 ]]; then
 		if [[ "$(id -u)" -eq 0 ]]; then
 			echo "Home Space Utilization (All Users)"
 			du -sh /home/*
 		else
 			echo "Home Space Utilization ($USER)"
 			du -sh "$HOME"
 		fi
 			exit
 	fi
else
 	echo "Invalid entry." >&2
 	exit 1
fi

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

 
 
 
 

<html>-n</html>

bash/чтение_ввода_с_клавиатуры.txt · Last modified: 2023/04/06 10:18 (external edit)