Por que os caracteres de nova linha se perdem ao usar a substituição de comandos? [duplicado]

18

Eu tenho um arquivo de texto chamado links.txt que se parece com isso

link1
link2
link3

Eu quero percorrer este arquivo linha por linha e executar uma operação em cada linha. Eu sei que posso fazer isso usando loop while, mas desde que eu estou aprendendo, pensei em usar um loop for. Eu realmente usei a substituição de comandos assim

a=$(cat links.txt)

Em seguida, usou o loop como este

for i in $a; do ###something###;done

Também posso fazer algo assim

for i in $(cat links.txt); do ###something###; done

Agora, minha pergunta é quando eu substituí a saída do comando cat em uma variável a, os novos caracteres de linha entre link1 link2 e link3 são removidos e são substituídos por espaços

echo $a

saídas

link1 link2 link3

e depois usei o loop for. É sempre que uma nova linha é substituída por espaço quando fazemos uma substituição de comando ??

Atenciosamente

    
por user3138373 27.10.2014 / 21:31

5 respostas

11

As novas linhas foram perdidas porque o shell realizou divisão de campo após a substituição do comando .

Na seção POSIX Substituição de comando :

The shell shall expand the command substitution by executing command in a subshell environment (see Shell Execution Environment) and replacing the command substitution (the text of command plus the enclosing "$()" or backquotes) with the standard output of the command, removing sequences of one or more characters at the end of the substitution. Embedded characters before the end of the output shall not be removed; however, they may be treated as field delimiters and eliminated during field splitting, depending on the value of IFS and quoting that is in effect. If the output contains any null bytes, the behavior is unspecified.

Valor padrão IFS (pelo menos em bash ):

$ printf '%q\n' "$IFS"
$' \t\n'

No seu caso, você não define IFS ou usa aspas duplas, então o caractere de nova linha será eliminado durante a divisão de campo.

Você pode preservar novas linhas, por exemplo, definindo IFS como vazio:

$ IFS=
$ a=$(cat links.txt)
$ echo "$a"
link1
link2
link3
    
por 28.10.2014 / 04:15
25

As novas linhas são trocadas em alguns pontos porque são caracteres especiais. Para mantê-los, você precisa se certificar de que eles sejam sempre interpretados, usando aspas:

$ a="$(cat links.txt)"
$ echo "$a"
link1
link2
link3

Agora, como usei aspas sempre que estava manipulando os dados, os caracteres de nova linha ( \n ) sempre eram interpretados pelo shell e, portanto, permaneciam. Se você esquecer de usá-los em algum momento, esses caracteres especiais serão perdidos.

O mesmo comportamento ocorrerá se você usar o loop em linhas contendo espaços. Por exemplo, dado o seguinte arquivo ...

mypath1/file with spaces.txt
mypath2/filewithoutspaces.txt

A saída dependerá do uso ou não de citações:

$ for i in $(cat links.txt); do echo $i; done
mypath1/file
with
spaces.txt
mypath2/filewithoutspaces.txt

$ for i in "$(cat links.txt)"; do echo "$i"; done
mypath1/file with spaces.txt
mypath2/filewithoutspaces.txt

Agora, se você não quiser usar aspas, existe uma variável de shell especial que pode ser usada para alterar o separador de campo do shell ( IFS ). Se você definir este separador para o caractere de nova linha, você se livrará da maioria dos problemas.

$ IFS=$'\n'; for i in $(cat links.txt); do echo $i; done
mypath1/file with spaces.txt
mypath2/filewithoutspaces.txt

Por questão de integridade, aqui está outro exemplo, que não depende da substituição de saída do comando. Depois de algum tempo, descobri que esse método era considerado mais confiável pela maioria dos usuários devido ao próprio comportamento do utilitário read .

$ cat links.txt | while read i; do echo $i; done

Aqui está um trecho da página de manual de read :

The read utility shall read a single line from standard input.

Como read obtém sua linha de entrada por linha, você tem certeza de que ela não será interrompida sempre que um espaço aparecer. Basta passar a saída de cat através de um pipe, e iterará sobre suas linhas muito bem.

Editar: Eu posso ver de outras respostas e comentários que as pessoas estão bastante relutantes quando se trata do uso de cat . Como jasonwryan disse em seu comentário, uma maneira mais correta de ler um arquivo em shell é usar o redirecionamento de fluxo ( < ), como você pode ver na resposta val0x00ff aqui . No entanto, como a pergunta não é " como ler / processar um arquivo na programação de shell ", minha resposta se concentra mais no comportamento das citações, e não no resto.

    
por 27.10.2014 / 21:36
4

Para adicionar minha ênfase, for faz a iteração de palavras . Se o seu arquivo é:

one two
three four

Em seguida, isso emitirá quatro linhas:

for word in $(cat file); do echo "$word"; done

Para iterar as linhas de um arquivo, faça o seguinte:

while IFS= read -r line; do
    # do something with "$line" <-- quoted almost always
done < file
    
por 28.10.2014 / 00:10
3

Você pode usar read do bash. Procure também o mapfile

while read -r link
  do
   printf '%s\n' "$link"
  done < links.txt

Ou usando o arquivo de mapa

mapfile -t myarray < links.txt
for link in "${myarray[@]}"; do printf '%s\n' "$link"; done
    
por 27.10.2014 / 21:52
-2

As novas linhas são substituídas por espaços porque é assim que echo funciona - concatena seus argumentos em espaços. echo substitui os delimitadores de argumentos por um espaço. Na verdade, você pode iterar com for sobre o que quiser, mas é necessário especificar primeiro o delimitador de campo:

string=abababababababababababa IFS=a        
for c in $string
do printf %s "$c"
done

OUTPUT

bbbbbbbbbbb

Mas isso não é um comportamento exclusivo de um for loop - isso acontece para qualquer expansão de divisão de campo:

printf %s $string
bbbbbbbbbbb

Por exemplo, se você deseja imprimir apenas os primeiros 10 bytes de qualquer linha não vazia em um arquivo ...

###original:
first "line"
<second>"line"
<second>"line"
<second>line and so on%
(IFS='                                                       
'; printf %.10s\n $(cat file))
###output
first "lin
<second>"l
<second>"l
<second>li

Existe uma razão pela qual eu especifico não-em branco acima - o \n ewline é um dos três bytes especiais em $IFS . Enquanto todo o resto lhe dará um argumento vazio quando 2 ou mais ocorrerem em sucessão, qualquer sequência de espaços, tabulações ou novas linhas só poderá ser avaliada para um único campo.

E por exemplo:

(IFS=0;printf 'ten lines!%s\n' $(printf "%010d"))

ten lines!
ten lines!
ten lines!
ten lines!
ten lines!
ten lines!
ten lines!
ten lines!
ten lines!
ten lines!

Mas ...

(IFS=\ ;printf 'one line%s\n' $(printf "%010s"))
one line

Em ambos os casos printf imprime 10 caracteres de preenchimento - no primeiro caso, imprime 10 zeros e nos 10 segundos espaços. No primeiro caso, cada 0 gera um campo nulo e o segundo printf obtém 10 argumentos vazios para cada um dos quais ele grava sua cadeia de formatação, mas todos os espaços impressos no segundo caso totalizam nada.

Você deve notar que este não é o tipo de geração de campo que o shell fará com expansões sem aspas - por padrão ele também irá glob . Fazendo coisas como:

for line in $(cat file)

Pode levar a resultados muito inesperados, pois há uma chance muito real de que algumas dessas linhas contenham shell globs que correspondam a arquivos reais - e, de repente, $line não se refere a uma linha de entrada, mas sim -disk nome do arquivo.

Se você planeja usar $IFS para qualquer divisão, sempre é uma boa ideia:

set -f

... primeiro, que instruirá o shell a não fazer glob enquanto você faz isso. Quando você terminar, poderá reativá-lo com set +f .

    
por 28.10.2014 / 03:52