Emulando “while IFS = read -r line” no estilo C do bash para loop

6

Um pequeno contexto para a pergunta primeiro. O while IFS= read -r line; do ... done < input.txt é uma estrutura bem conhecida para ler um arquivo linha por linha no script de shell. Mas tendo usado o estilo c para loop (no caso de alguns usuários aqui não saberem, é o tipo for((i=0;i<=$val;i++))do;...done usado em bash e ksh ), eu lembrei que while e for loops em C- linguagens semelhantes são intercambiáveis e podem imitar uma a outra. Então, eu criei o loop de estilo c que emula a estrutura while IFS= read -r line mencionada acima.

for((i=1;;i++))
do 
    IFS= read -r line || break
    # do something with line here
done < input.txt

Eu testei com vários tipos de entrada - linhas normais, linhas com guias / espaços principais, linhas que não terminam em \n (que é o caso em que read não pode capturar a última linha ) - e em todos os casos, isso funciona exatamente da mesma forma que a abordagem while loop. Isso também tecnicamente tem um contador de linha "interno" com a variável i .

Então, a questão é esta: existe alguma razão (além da não-portabilidade para outras camadas além de ksh e bash ) para evitar o uso dessa abordagem? Existem casos em que isso pode falhar? Embora isso funcione bem, quero saber se há algum problema que negligenciei antes de começar a usar essa abordagem ativamente em meus próprios scripts.

    
por Sergiy Kolodyazhnyy 01.11.2017 / 06:28

2 respostas

4

Is there any reason to avoid using this approach?

Clareza ou falta dela.

Um loop while como while sometest ; do ... também pode ser escrito como

while :; do 
    if ! sometest; then
        break
    fi
    ...

Mas nós não fazemos isso (no shell, ou em C) porque ele afasta a condição de loop de onde as pessoas estão acostumadas a procurá-lo. Sua construção é semelhante: você deixou a expressão do meio da construção for (( ; ; )) vazia.

É claro que você teve que fazer isso, já que for (( ; ; )) no shell apenas avalia expressões aritméticas, não comandos como read . Mesmo que for e while possam ser facilmente transformados em C, o mesmo não se aplica realmente ao shell por causa disso: eles fazem coisas diferentes.

(Eu diria que mesmo em C, um loop for por sua estrutura sugere um pouco em um loop de contagem, mas é claro que não é exatamente claro.)

Quanto a isso:

This also technically has a "built-in" line counter with i variable.

Eu não acho que haja nada embutido nisso. Você inicializou o contador de linhas manualmente e o incrementou manualmente. Você poderia fazer o mesmo com o mais tradicional while loop

i=0
while IFS= read -r line; do 
    ... 
    let i++
done < input.txt

(ou i=$((i + 1)) para ser mais portátil)

    
por 14.11.2017 / 13:52
3

Concordo plenamente com @ilkkachu aqui.

Mas o FWIW, para poder usar o comando read como parte da condição for , com ksh93 (de onde vem essa sintaxe for ((...)) ), você poderia usar disciplinas:

function read.get {
  IFS= read -r line
  .sh.value=$(($? == 0))
}

for ((i = 0; read; i++)) {
  printf '%5d: %s\n' "$i" "$line"
}

Definimos a disciplina get da variável $read para que, quando expandida, o comando read seja executado e $read seja expandido para 1 se read for bem-sucedida ou 0 caso contrário.

Ou uma variante usando tipos :

typeset -T read_t=(
  typeset value
  function get {
    IFS= read -r _.value
    ((.sh.value = $? == 0))
  }
)

read_t line
for ((i = 0; line; i++)) {
  printf '%5d: %s\n' "$i" "${line.value}"
}

Em que o tipo read_t é um tipo de objeto que, quando expandido, lê uma linha em theobject.value e expande para 1 se o read tiver êxito ou 0 caso contrário.

Ou a forma ${ ...; } de substituição de comandos:

for ((i = 0; ${ IFS= read -r line; echo "$(($? == 0))";}; i++)) {
  printf '%5d: %s\n' "$i" "$line"
}

Com zsh , sequestrando o diretório nomeado dinâmico recurso :

set -o extendedglob
handle_-read:var()
  case $1:$2 in
    (d:-read:[a-zA-Z_][a-zA-Z0-9_]#)
      IFS= read -r ${2#*:} && reply=('' $#2);;
    (*) false;;
  esac

zsh_directory_name_functions+=(handle_-read:var)

for ((i = 0; ${#${(D):--read:line}} == 3; i++)) {
  printf '%5d: %s\n' "$i" "$line"
}

(não que eu recomendaria fazer isso).

Essa é a única maneira pela qual eu sei onde você pode ter um comando executado como parte de expressões aritméticas não em um subshell.

A substituição normal de comandos ( $(...) ou '...' ) também pode ser usada para executar comandos em uma expressão aritmética, mas é feita em um subshell, então algo como:

for ((i = 0; $(IFS= read -r line; echo "$((!$?))"); i++)) {
  printf '%5d: %s\n' "$i" "$line"
}

Embora seja uma sintaxe válida para bash , não seria possível definir a variável $line fora da subshell.

Com zsh , você pode fazer algo como:

for ((i = 0; ${${line::=$(IFS= read -re)}+$?} == 0; i++)) {
  printf '%5d: %s\n' "$i" "$line"
}

Embora isso seja ineficiente, uma vez que seria forçado um subshell para cada linha e alimentaria a linha através de um tubo extra.

bash não possui o operador de expansão de parâmetro de atribuição ${var::=value} incondicional e sua capacidade de aninhar expansões de parâmetro é muito limitada. Ele tem o operador ${var=value} Bourne (atribua se previamente não configurado), e há alguns operadores que permitem o aninhamento como ${foo#${bar}} , então você poderia fazer algo como:

unset line
for ((i = 0; 0*${?#"x${line='IFS= read -r && printf %s "$REPLY"'}"}+$? == 0; i++)); do
  printf '%5d: %s\n' "$i" "$line"
  unset line
done

(tendo aqui que trabalhar em torno de dois erros de bash ) .

    
por 14.11.2017 / 15:18