shell: read: diferencie entre EOF e newline

7

Lendo um único caractere, como posso saber a diferença entre o nulo <EOF> e \n ?

Por exemplo:

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

Com um caractere imprimível:

$ f
Enter a character: x
You entered 'x'

Ao pressionar Enter :

$ f
Enter a character: 

You entered ''

Ao pressionar Ctrl + D :

$ f
Enter a character: ^D
You entered ''
$ 

Por que a saída é a mesma nos dois últimos casos? Como posso distinguir entre eles?

Existe uma maneira diferente de fazer isso no shell POSIX vs bash ?

    
por Tom Hale 01.08.2017 / 16:29

4 respostas

10

Com read -n "$n" (não um recurso POSIX), e se stdin for um dispositivo terminal, read colocará o terminal fora do modo icanon , caso contrário read verá somente linhas completas como retornadas pelo linha terminal Editor de linha interna da disciplina e, em seguida, lê um byte de cada vez até que $n caracteres ou uma nova linha tenham sido lidos (você poderá ver resultados inesperados se forem inseridos caracteres inválidos).

Ele lê o caractere $n de uma linha. Você também precisará esvaziar $IFS para não remover caracteres IFS da entrada.

Como deixamos o modo icanon , ^D não é mais especial. Então, se você pressionar Ctrl + D , o caractere ^D será lido.

Você não veria eof do dispositivo terminal a menos que o terminal seja de alguma forma desconectado. Se stdin for outro tipo de arquivo, você poderá ver eof (como em : | IFS= read -rn 1; echo "$?" , em que stdin é um pipe vazio ou com o redirecionamento de stdin de /dev/null )

read retornará 0 se $n caracteres (bytes não fazendo parte de caracteres válidos sendo contados como 1 caractere) ou uma linha completa tiver sido lida.

Assim, no caso especial de apenas um caractere sendo solicitado:

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

Fazê-lo POSIXly é bastante complicado.

Isso seria algo como (supondo um sistema baseado em ASCII (em oposição ao EBCDIC, por exemplo)):

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"
}

Note que retornamos somente quando um caractere completo foi lido. Se a entrada estiver na codificação incorreta (diferente da codificação da localidade), por exemplo, se o seu terminal enviar é codificado em iso8859-1 (0xe9) quando esperamos UTF-8 (0xc3 0xa9), você pode digitar quantas é como você gosta, a função não retornará. bash read -n1 retornaria no segundo 0xe9 (e armazenaria ambos na variável), o que é um comportamento um pouco melhor.

Se você também quisesse ler um caractere ^C no Ctrl + C (em vez de deixá-lo matar seu script; também para ^Z , ^\ ...), ou ^S / ^Q em Ctrl + S / Q (em vez de controle de fluxo), você pode adicionar um -isig -ixon à linha stty . Observe que bash read -n1 também não o faz (restaura isig se estiver desativado).

Isso não restaurará as configurações tty se o script for eliminado (como se você pressionasse Ctrl + C . Você poderia adicionar um trap , mas isso potencialmente substituiria outros trap s em o script.

Você também pode usar zsh em vez de bash , onde read -k (que é anterior a ksh93 ou bash ' read -n/-N ) lê um caractere do terminal e manipula ^D sozinho ( retorna não-zero se esse caractere for inserido) e não trata a nova linha especialmente.

if read -k k; then
  printf '1 character entered: %q\n' $k
fi
    
por 01.08.2017 / 17:06
2

Em f() , altere a %s para %q :

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

Saída, se o usuário inserir uma nova linha , então ' Ctrl-D ':

Enter a character: 

You entered ''''
Enter a character: ^D
You entered '$'
 %q       ARGUMENT is printed in a format that can be reused as shell input, 
          escaping non-printable characters with the proposed POSIX $'' syntax.
4''

De "man printf:

f() { read -rn 1 -p "Enter a character: " char && \
      printf "\nYou entered '%q'\n" "$char"; }
f;f
    
por 01.08.2017 / 16:43
2

Na verdade, se você executar read -rn1 no Bash e clicar em ^D , ele será tratado como o caractere de controle literal, não como uma condição EOF. O caractere de controle não é visível quando impresso, por isso não aparece com printf "'%s'" . Piping a saída para algo como od -c iria mostrar, como printf "%q" que outras respostas já mencionadas.

Com nada realmente como entrada, o resultado é diferente, aqui vazio mesmo com printf "%q" :

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

A nova linha não é retornada por read aqui por dois motivos. Primeiro, é o delimitador de linha padrão de leitura e, portanto, retornado como saída. Segundo, também faz parte do padrão IFS , e read remove os espaços em branco inicial e final se eles fizerem parte de IFS .

Então, precisamos de read -d para alterar o delimitador do padrão, e fazer IFS empty:

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

read -d "" torna o delimitador efetivamente o byte NUL, o que significa que ele ainda não indica a diferença entre uma entrada de nada e uma entrada de um byte NUL:

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

Embora com nada como entrada, read retorna falso, então poderíamos verificar $? para detectar isso.

    
por 01.08.2017 / 17:05
0
read -r var
status=$?
echo "\$var='$var':\$?=$status"

Os casos de nova linha e Ctrl-D são distinguidos pela variável de status.

No caso de nova linha, o status é verdadeiro (0), enquanto o Ctrl-D é dado, o status é falso (1)

    
por 01.08.2017 / 16:49