Como você executa um comando para cada linha de um arquivo?

161

Por exemplo, agora estou usando o seguinte para alterar alguns arquivos cujos caminhos Unix escrevi para um arquivo:

cat file.txt | while read in; do chmod 755 "$in"; done

Existe uma maneira mais elegante e segura?

Falcão
fonte

Respostas:

127

Leia um arquivo linha por linha e execute comandos: 4 respostas

Isso ocorre porque não há apenas 1 resposta ...

  1. shell expansão da linha de comando
  2. xargs ferramenta dedicada
  3. while read com algumas observações
  4. while read -uusando dedicado fd, para processamento interativo (amostra)

Quanto ao pedido OP: correndo chmodem todos os alvos listados no arquivo , xargsé a ferramenta indicada. Mas para algumas outras aplicações, pequena quantidade de arquivos, etc ...

  1. Leia o arquivo inteiro como argumento de linha de comando.

    Se o seu arquivo não for muito grande e todos os arquivos forem bem nomeados (sem espaços ou outros caracteres especiais, como aspas), você poderá usar a shellexpansão da linha de comando . Simplesmente:

    chmod 755 $(<file.txt)

    Para pequena quantidade de arquivos (linhas), este comando é o mais leve.

  2. xargs é a ferramenta certa

    Para uma quantidade maior de arquivos ou quase qualquer número de linhas no seu arquivo de entrada ...

    Para muitos binutils ferramentas, como chown, chmod, rm, cp -t...

    xargs chmod 755 <file.txt

    Se você possui caracteres especiais e / ou várias linhas file.txt.

    xargs -0 chmod 755 < <(tr \\n \\0 <file.txt)

    se seu comando precisar ser executado exatamente 1 vez por entrada:

    xargs -0 -n 1 chmod 755 < <(tr \\n \\0 <file.txt)

    Isso não é necessário para esta amostra, pois chmodaceita vários arquivos como argumento, mas corresponde ao título da pergunta.

    Para algum caso especial, você pode até definir o local do argumento do arquivo nos comandos gerados por xargs:

    xargs -0 -I '{}' -n 1 myWrapper -arg1 -file='{}' wrapCmd < <(tr \\n \\0 <file.txt)

    Teste com seq 1 5como entrada

    Tente o seguinte:

    xargs -n 1 -I{} echo Blah {} blabla {}.. < <(seq 1 5)
    Blah 1 blabla 1..
    Blah 2 blabla 2..
    Blah 3 blabla 3..
    Blah 4 blabla 4..
    Blah 5 blabla 5..

    Onde commande é feito uma vez por linha .

  3. while read e variantes.

    Como o OP sugere, cat file.txt | while read in; do chmod 755 "$in"; doneele funcionará, mas existem 2 problemas:

    • cat |é um garfo inútil e

    • | while ... ;donese tornará um subshell onde o ambiente desaparecerá depois ;done.

    Portanto, isso poderia ser melhor escrito:

    while read in; do chmod 755 "$in"; done < file.txt

    Mas,

    • Você pode ser avisado sobre $IFSe readsinalizadores:

      help read
      read: read [-r] ... [-d delim] ... [name ...]
          ...
          Reads a single line from the standard input... The line is split
          into fields as with word splitting, and the first word is assigned
          to the first NAME, the second word to the second NAME, and so on...
          Only the characters found in $IFS are recognized as word delimiters.
          ...
          Options:
            ...
            -d delim   continue until the first character of DELIM is read, 
                       rather than newline
            ...
            -r do not allow backslashes to escape any characters
          ...
          Exit Status:
          The return code is zero, unless end-of-file is encountered...

      Em alguns casos, pode ser necessário usar

      while IFS= read -r in;do chmod 755 "$in";done <file.txt

      Para evitar problemas com nomes de arquivos estranhos. E talvez se você encontrar problemas comUTF-8 :

      while LANG=C IFS= read -r in ; do chmod 755 "$in";done <file.txt
    • Enquanto você usa STDINpara ler file.txt, seu script não pode ser interativo (você não pode STDINmais usar ).

  4. while read -u, usando dedicado fd .

    Sintaxe: while read ...;done <file.txtredirecionaráSTDIN para file.txt. Isso significa que você não poderá lidar com o processo até que ele termine.

    Se você planeja criar uma ferramenta interativa , evite o uso deSTDIN e usar algum descritor de arquivo alternativo .

    Constantes descritores de arquivos são: 0para STDIN , 1para STDOUT e 2para STDERR . Você poderia vê-los por:

    ls -l /dev/fd/

    ou

    ls -l /proc/self/fd/

    A partir daí, você deve escolher um número não utilizado, entre 0e 63(mais, de fato, dependendo dasysctl ferramenta de superusuário) como descritor de arquivo :

    Para esta demonstração, usarei o fd 7 :

    exec 7<file.txt      # Without spaces between `7` and `<`!
    ls -l /dev/fd/

    Então você pode usar read -u 7desta maneira:

    while read -u 7 filename;do
        ans=;while [ -z "$ans" ];do
            read -p "Process file '$filename' (y/n)? " -sn1 foo
            [ "$foo" ]&& [ -z "${foo/[yn]}" ]&& ans=$foo || echo '??'
        done
        if [ "$ans" = "y" ] ;then
            echo Yes
            echo "Processing '$filename'."
        else
            echo No
        fi
    done 7<file.txt

    done

    Para fechar fd/7:

    exec 7<&-            # This will close file descriptor 7.
    ls -l /dev/fd/

    Nota: Eu deixei a versão marcada, porque essa sintaxe pode ser útil, ao fazer muitas E / S com processo paralelo:

    mkfifo sshfifo
    exec 7> >(ssh -t user@host sh >sshfifo)
    exec 6<sshfifo
F. Hauri
fonte
3
Como xargsfoi criado inicialmente para responder a esse tipo de necessidade, alguns recursos, como criar o comando o maior tempo possível no ambiente atual, para invocar chmodneste caso o menos possível, reduzir os garfos garante a eficiência. while ;do..done <$fileimplie executando 1 fork para 1 arquivo. xargspoderia executar 1 fork para milhares de arquivos ... de maneira confiável.
F. Hauri
1
por que o terceiro comando não funciona em um makefile? Estou recebendo "erro de sintaxe próximo ao token inesperado` <'", mas a execução direta da linha de comando funciona.
Woodrow Barlow
2
Isso parece vinculado à sintaxe específica do Makefile. Você pode tentar reverter a linha de comando:cat file.txt | tr \\n \\0 | xargs -0 -n1 chmod 755
F. Hauri 28/09
@ F.Hauri, por algum motivo, tr \\n \\0 <file.txt |xargs -0 [command]é cerca de 50% mais rápido que o método que você descreveu.
precisa saber é
Outubro de 2019, nova edição, adicione amostra de processador de arquivo interativo .
F. Hauri
150

Sim.

while read in; do chmod 755 "$in"; done < file.txt

Dessa forma, você pode evitar um catprocesso.

catquase sempre é ruim para um propósito como esse. Você pode ler mais sobre o uso inútil do gato.

PP
fonte
Evitar que uma cat seja uma boa ideia, mas, neste caso, o comando indicado éxargs
F. Hauri
Esse link não parece ser relevante, talvez o conteúdo da página da Web tenha mudado? O resto da resposta é incrível embora :)
starbeamrainbowlabs
@starbeamrainbowlabs Sim. Parece que a página foi movida. Voltei a ligar e deve estar ok agora. Obrigado :)
PP
1
Obrigado! Isso foi útil, especialmente quando você precisa fazer outra coisa além de chamar chmod(ou seja, realmente executar um comando para cada linha do arquivo).
Per Lundberg
tenha cuidado com as barras invertidas! de unix.stackexchange.com/a/7561/28160 - " read -rlê uma única linha da entrada padrão ( readsem -rinterpretar barras invertidas, você não deseja isso)."
Aquele cara brasileiro
16

se você tiver um bom seletor (por exemplo, todos os arquivos .txt em um diretório), poderá:

for i in *.txt; do chmod 755 "$i"; done

bash para loop

ou uma variante sua:

while read line; do chmod 755 "$line"; done <file.txt
d.raev
fonte
O que não funciona é que, se houver espaços na linha, a entrada é dividida por espaços, não por linha.
Michael Fox
@ Michael Fox: Linhas com espaços podem ser suportadas alterando o separador. Para alterá-lo para novas linhas, defina a variável de ambiente 'IFS' antes do script / comando. Ex: exportar IFS = '$ \ n'
codesniffer 14/09/18
Erro de digitação no meu último comentário. Deve ser: exportar IFS = $ '\ n'
codesniffer
14

Se você sabe que não possui nenhum espaço em branco na entrada:

xargs chmod 755 < file.txt

Se houver espaço em branco nos caminhos, e se você tiver GNU xargs:

tr '\n' '\0' < file.txt | xargs -0 chmod 755
Glenn Jackman
fonte
Eu sei sobre xargs, mas (infelizmente) parece menos uma solução confiável do que os recursos internos do bash, como while e read. Além disso, eu não tenho o GNU xargs, mas estou usando o OS X e o xargs também tem uma opção -0 aqui. Obrigado pela resposta.
falcão
1
@hawk Não: xargsé robusto. Essa ferramenta é muito antiga e seu código é fortemente revisitado . Seu objetivo era inicialmente criar linhas em relação às limitações do shell (64kchar / line ou algo assim). Agora, essa ferramenta pode funcionar com arquivos muito grandes e pode reduzir muito o número de bifurcações para o comando final. Vejo minha resposta e / ou man xargs.
F. Hauri
@hawk Menos de uma solução confiável de que maneira? Se ele funciona no Linux, Mac / BSD e Windows (sim, o MSYSGIT comporta o GNU xargs), então é o mais confiável possível.
Camilo Martin
1
Para aqueles que ainda encontrarem isso nos resultados da pesquisa ... você pode instalar o GNU xargs no macOS usando o Homebrew (brew install findutils ) e, em seguida, invocar o GNU xargs com gxargs, por exemplo, por exemplo:gxargs chmod 755 < file.txt
Jase
13

Se você deseja executar seu comando em paralelo para cada linha, você pode usar o GNU Parallel

parallel -a <your file> <program>

Cada linha do seu arquivo será passada para o programa como argumento. Por padrão, parallelexecuta tantos threads quanto suas CPUs contam. Mas você pode especificá-lo com-j

janisz
fonte
3

Vejo que você marcou o bash, mas o Perl também seria uma boa maneira de fazer isso:

perl -p -e '`chmod 755 $_`' file.txt

Você também pode aplicar uma regex para garantir que está obtendo os arquivos corretos, por exemplo, para processar apenas os arquivos .txt:

perl -p -e 'if(/\.txt$/) `chmod 755 $_`' file.txt

Para "pré-visualizar" o que está acontecendo, substitua os backticks por aspas duplas e preencha print:

perl -p -e 'if(/\.txt$/) print "chmod 755 $_"' file.txt
1.618
fonte
2
Por que usar backticks? Perl tem uma chmodfunção
glenn jackman
1
Você gostaria perl -lpe 'chmod 0755, $_' file.txt- use -lpara o recurso "auto-chomp"
glenn jackman 18/12/12
1

Você também pode usar o AWK, que oferece mais flexibilidade para lidar com o arquivo

awk '{ print "chmod 755 "$0"" | "/bin/sh"}' file.txt

se o seu arquivo tiver um separador de campos como:

campo1, campo2, campo3

Para obter apenas o primeiro campo que você faz

awk -F, '{ print "chmod 755 "$1"" | "/bin/sh"}' file.txt

Você pode verificar mais detalhes na documentação do GNU https://www.gnu.org/software/gawk/manual/html_node/Very-Simple.html#Very-Simple

brunocrt
fonte
0

A lógica se aplica a muitos outros objetivos. E como ler .sh_history de cada usuário em / home / filesystem? E se houver milhares deles?

#!/bin/ksh
last |head -10|awk '{print $1}'|
 while IFS= read -r line
 do
su - "$line" -c 'tail .sh_history'
 done

Aqui está o script https://github.com/imvieira/SysAdmin_DevOps_Scripts/blob/master/get_and_run.sh

BladeMight
fonte
0

Eu sei que é tarde, mas ainda

Se por acaso você encontrar o arquivo de texto salvo no Windows em \r\nvez de \n, poderá ficar confuso com a saída se o seu comando sth após a linha de leitura como argumento. Então remova \r, por exemplo:

cat file | tr -d '\r' | xargs -L 1 -i echo do_sth_with_{}_as_line
EP
fonte