Substituição de comando: divisão na nova linha, mas não no espaço

28

Eu sei que posso resolver este problema de várias maneiras, mas estou querendo saber se existe uma maneira de fazer isso usando apenas os built-ins do bash, e se não, qual é a maneira mais eficiente de fazer isso.

Eu tenho um arquivo com conteúdo como

AAA
B C DDD
FOO BAR

pelo qual eu só quero dizer que tem várias linhas e cada linha pode ou não ter espaços. Eu quero executar um comando como

cmd AAA "B C DDD" "FOO BAR"

Se eu usar cmd $(< file) , obtenho

cmd AAA B C DDD FOO BAR

e se eu usar cmd "$(< file)" , obtenho

cmd "AAA B C DDD FOO BAR"

Como faço para que cada linha trate exatamente um parâmetro?

    
por Old Pro 27.05.2012 / 23:22

6 respostas

8

Parece que a maneira canônica de fazer isso em bash é algo como

unset args
while IFS= read -r line; do 
    args+=("$line") 
done < file

cmd "${args[@]}"

ou, se sua versão do bash tiver mapfile :

mapfile -t args < filename
cmd "${args[@]}"

A única diferença que eu posso encontrar entre o mapfile e o loop while-read versus o one-liner

(set -f; IFS=$'\n'; cmd $(<file))

é que o primeiro converterá uma linha em branco em um argumento vazio, enquanto o one-liner ignorará uma linha em branco. Neste caso, o comportamento de uma linha é o que eu prefiro de qualquer maneira, então bônus duplo em ser compacto.

Eu usaria IFS=$'\n' cmd $(<file) , mas não funciona, porque $(<file) é interpretado para formar a linha de comando antes de IFS=$'\n' entrar em vigor.

Apesar de não funcionar no meu caso, agora aprendi que muitas ferramentas suportam o encerramento de linhas com null (newline (\n)0) em vez de %code% , o que facilita muito isso quando se lida com, digamos, arquivo nomes, que são fontes comuns dessas situações:

find / -name '*.config' -print0 | xargs -0 md5

alimenta uma lista de nomes de arquivos completos como argumentos para md5 sem qualquer globbing ou interpolação ou qualquer outra coisa. Isso leva à solução não integrada

tr "\n" "
unset args
while IFS= read -r line; do 
    args+=("$line") 
done < file

cmd "${args[@]}"
0" <file | xargs -0 cmd

embora isso, também, ignore linhas vazias, embora capture linhas que possuem apenas espaços em branco.

    
por 28.05.2012 / 02:32
25

Portável:

set -f              # turn off globbing
IFS='
'                   # split at newlines only
cmd $(cat <file)
unset IFS
set +f

Ou usando um subshell para tornar o IFS e a opção alterada local:

( set -f; IFS='
'; exec cmd $(cat <file) )

O shell executa a divisão de campo e geração de nome de arquivo no resultado de uma variável ou substituição de comando que não está entre aspas duplas. Portanto, você precisa desativar a geração de nome de arquivo com set -f e configurar a divisão de campo com IFS para tornar apenas novas linhas separadas.

Não há muito a ganhar com construções bash ou ksh. Você pode tornar IFS local para uma função, mas não set -f .

No bash ou ksh93, você pode armazenar os campos em uma matriz, se precisar passá-los para vários comandos. Você precisa controlar a expansão no momento em que cria a matriz. Então "${a[@]}" se expande para os elementos da matriz, um por palavra.

set -f; IFS=$'\n'
a=($(cat <file))
set +f; unset IFS
cmd "${a[@]}"
    
por 28.05.2012 / 02:15
10

Você pode fazer isso com uma matriz temporária.

Configuração:

$ cat input
AAA
A B C
DE F
$ cat t.sh
#! /bin/bash
echo "$1"
echo "$2"
echo "$3"

Preencha o array:

$ IFS=$'\n'; set -f; foo=($(<input))

Use o array:

$ for a in "${foo[@]}" ; do echo "--" "$a" "--" ; done
-- AAA --
-- A B C --
-- DE F --

$ ./t.sh "${foo[@]}"
AAA
A B C
DE F

Não é possível descobrir uma maneira de fazer isso sem essa variável temporária, a menos que a alteração de IFS não seja importante para cmd . Nesse caso:

$ IFS=$'\n'; set -f; cmd $(<input) 

deve fazer isso.

    
por 27.05.2012 / 23:35
3

Você pode usar o bash mapfile embutido para ler o arquivo em uma matriz

mapfile -t foo < filename
cmd "${foo[@]}"

ou, não testado, xargs pode fazer isso

xargs cmd < filename
    
por 28.05.2012 / 00:03
0
old=$IFS
IFS='  #newline
'
array='cat Submissions' #input the text in this variable
for ...  #use parts of variable in the for loop
... 
done
IFS=$old

Melhor maneira que encontrei. Apenas funciona.

    
por 23.10.2018 / 08:40
0

Arquivo

O loop mais básico (portátil) para dividir um arquivo em novas linhas é:

#!/bin/sh
while read -r line; do            # get one line (\n) at a time.
    set -- "$@" "$line"           # store in the list of positional arguments.
done <infile                      # read from a file called infile.
printf '<%s>' "$@" ; echo         # print the results.

Que imprimirá:

$ ./script
<AAA><A B C><DE F>

Sim, com o padrão IFS = espaço guia nova linha .

Por que funciona

  • O IFS será usado pelo shell para dividir a entrada em várias variáveis. Como há apenas uma variável one , nenhuma divisão é executada pelo shell. Portanto, nenhuma mudança de IFS foi necessária.
  • Sim, os espaços / divisórias iniciais e finais estão sendo removidos, mas isso não parece ser um problema neste caso.
  • Não, nenhuma globbing é feita, pois nenhuma expansão é sem aspas . Portanto, não é necessário set -f .
  • O único array usado (ou necessário) é o dos parâmetros posicionais do tipo array.
  • A opção -r (raw) é evitar a remoção da maioria das barras invertidas.

Isso não funcionará se for necessário dividir e / ou globbing. Em tais casos, é necessária uma estrutura mais complexa.

Se você precisar (ainda portável) para:

  • Evite a remoção de espaços / divisórias iniciais e finais, use: IFS= read -r line
  • Dividir linha para vars em algum caractere, use: IFS=':' read -r a b c .

Divida o arquivo em outro caractere (não portátil, funciona com ksh, bash, zsh):

IFS=':' read -d '+' -r a b c

Expansão

É claro que o título da sua pergunta é sobre a divisão de uma execução de comando em novas linhas, evitando a divisão em espaços.

A única maneira de obter divisão do shell é deixar uma expansão sem aspas:

echo $(< file)

Isso é controlado pelo valor do IFS e, em expansões sem aspas, o globbing também é aplicado. Para realizar esse trabalho, você precisa:

  • Defina o IFS para a nova linha apenas , para obter a divisão apenas na nova linha.
  • Desmarque a opção de shell globbing set +f :

    set + f IFS = ' ' cmd $ (< file)

Claro, isso altera o valor do IFS e da globbing para o restante do script.

    
por 24.10.2018 / 01:01