shell: read: различать EOF и новую строку

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

set | grep HIST
env | grep HIST
printenv | grep HIST
7
01.08.2017, 17:55
4 ответа

Посколькуread -n "$n"(не является функцией POSIX ), и если stdin является терминальным устройством, readвыводит терминал из режима icanon, иначе readбудет видеть только полные строки, возвращаемые встроенный строковый редактор терминальной строки, а затем считывает по одному байту за раз до тех пор, пока не будет прочитано $nсимволов или новой строки (вы можете увидеть неожиданные результаты, если введены недопустимые символы ).

Читает до $nсимволов из одной строки. Вам также нужно будет очистить $IFS, чтобы он не удалял символы IFS из ввода.

Поскольку мы покидаем режим icanon, ^Dбольше не является особым. Поэтому, если вы нажмете Ctrl+D , будет прочитан символ ^D.

Вы не увидите eof с терминального устройства, если терминал каким-либо образом не отключен. Если stdin — это другой тип файла, вы можете увидеть eof (, как в : | IFS= read -rn 1; echo "$?", где stdin — пустой канал, или с перенаправлением stdin из/dev/null)

readвозвращает 0, если $nсимволов (байтов, не являющихся частью допустимых символов, считаются за 1 символ )или была прочитана целая строка.

Таким образом, в особом случае запрашивается только один символ:

if IFS= read -rn 1 var; then
  if [ "${#var}" -eq 0 ]; then
    echo an empty line was read
  else
    printf %s "${#var} character "
    (export LC_ALL=C; printf '%s\n' "made of ${#var} byte(s) was read")
  fi
else
  echo "EOF found"
fi

Сделать это с помощью POSIX довольно сложно.

Это было бы что-то вроде (, предполагая, что система ASCII -основана (, а не EBCDIC, например, )система):

readk() {
  REPLY= ret=1
  if [ -t 0 ]; then
    saved_settings=$(stty -g)
    stty -icanon min 1 time 0 icrnl
  fi
  while true; do
    code=$(dd bs=1 count=1 2> /dev/null | od -An -vto1 | tr -cd 0-7)
    [ -n "$code" ] || break
    case $code in
      000 | 012) ret=0; break;; # can't store NUL in variable anyway
      (*) REPLY=$REPLY$(printf "\\$code");;
    esac
    if expr " $REPLY" : '.' > /dev/null; then
      ret=0
      break
    fi
  done
  if [ -t 0 ]; then
    stty "$saved_settings"
  fi
  return "$ret"
}

Обратите внимание, что мы возвращаемся только тогда, когда прочитан полный символ. Если ввод в неправильной кодировке (, отличной от кодировки локали ), например, если ваш терминал отправляет éв кодировке iso8859 -1 (0xe9 ), когда мы ожидаем UTF -8 (0xc3 0xa9 ), тогда вы можете ввести сколько угодно é, функция не вернется. bash's read -n1вернется после второго 0xe9 (и сохранит оба значения в переменной ), что немного лучше.

Если вы также хотите прочитать символ ^Cпри Ctrl+C(вместо того, чтобы позволить ему убить ваш скрипт; также для ^Z, ^\... )или ^S/ ^Qпосле Ctrl+S/Q(вместо управления потоком )вы можете добавить -isig -ixonк строке stty. Обратите внимание, что bashread -n1тоже этого не делает (, он даже восстанавливает isig, если он был выключен ).

Это не восстановит настройки tty, если скрипт будет убит (, например, если вы нажмете Ctrl+C . Вы можете добавить trap, но это может переопределить другие trapв скрипте.

Вы также можете использовать zshвместо bash, где read -k(, который предшествует ksh93или bashдля read -n/-N), считывает один символ с терминала и обрабатывает ^Dсам по себе (возвращает не -ноль, если этот символ введен )и не обрабатывает новую строку специально.

if read -k k; then
  printf '1 character entered: %q\n' $k
fi
12
27.01.2020, 20:15

В f()измените %sна %q:

.
f() { read -rn 1 -p "Enter a character: " char && \
      printf "\nYou entered '%q'\n" "$char"; }
f;f

Вывод, если пользователь вводит новую строку , затем ' Ctrl -D ':

Enter a character: 

You entered ''''
Enter a character: ^D
You entered '$'\004''

Из `man printf:

 %q       ARGUMENT is printed in a format that can be reused as shell input, 
          escaping non-printable characters with the proposed POSIX $'' syntax.
2
27.01.2020, 20:15
read -r var
status=$?
echo "\$var='$var':\$?=$status"

Случаи новой строки и Ctrl -D различаются переменной состояния.

В случае перехода на новую строку статус true (0 ), в то время как при задании Ctrl -D, статус ложный (1)

0
27.01.2020, 20:15

На самом деле, если вы запустите read -rn1в Bash и нажмете ^D, он будет рассматриваться как буквальный управляющий символ, а не как условие конца строки. Управляющий символ просто не виден при печати, поэтому он не отображается с printf "'%s'". Направление вывода на что-то вроде od -cпоказало бы это, как и printf "%q", которые уже упоминались в других ответах.

Если на входе ничего нет, результат будет другим, здесь пусто даже сprintf "%q":

$ f()  { read -rn 1  x ; printf "%q\n" "$x"; }
$ printf "" | f
''

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

Итак, нам нужно read -dизменить разделитель по умолчанию,и делают IFSпустым:

$ g() { IFS= read -rn 1 -d '' x ; printf "%q\n" "$x"; }
$ printf "\n" | g
$'\n'

read -d ""делает разделитель эффективным байтом NUL, что означает, что это по-прежнему не указывает на разницу между вводом ничего и вводом байта NUL:

$ printf "" | g
''
$ printf "\000" | g
''

Несмотря на отсутствие входных данных, readвозвращает false, поэтому мы можем проверить $?, чтобы обнаружить это.

2
27.01.2020, 20:15

Теги

Похожие вопросы