Como posso usar xargs para copiar arquivos com espaços e aspas em seus nomes?

232

Estou tentando copiar um monte de arquivos abaixo de um diretório e vários arquivos têm espaços e aspas simples em seus nomes. Quando tento encadear finde grepcom xargs, recebo o seguinte erro:

find .|grep "FooBar"|xargs -I{} cp "{}" ~/foo/bar
xargs: unterminated quote

Alguma sugestão para um uso mais robusto do xargs?

Isso ocorre no Mac OS X 10.5.3 (Leopard) com BSD xargs.

Drew Stephens
fonte
2
A mensagem de erro GNU xargs para isso com um nome de arquivo contendo uma aspas simples é bastante mais útil: "xargs: aspas simples sem correspondência; por padrão, aspas são especiais para xargs, a menos que você use a opção -0".
Steve Jessop
3
O GNU xargs também tem a --delimiteropção ( -d). Experimente \ncomo delimitador, Isso impede a xargsseparação de linhas com espaços em várias palavras / argumentos.
27417 MattBianco

Respostas:

199

Você pode combinar tudo isso em um único findcomando:

find . -iname "*foobar*" -exec cp -- "{}" ~/foo/bar \;

Isso manipulará nomes de arquivos e diretórios com espaços neles. Você pode usar -namepara obter resultados que diferenciam maiúsculas de minúsculas.

Nota: O --sinalizador transmitido para cpimpede o processamento de arquivos começando com -como opções.

godbyk
fonte
70
As pessoas usam xargs porque normalmente é mais rápido chamar um executável 5 vezes com 200 argumentos de cada vez do que chamá-lo 1000 vezes com um argumento de cada vez.
tzot 14/10/08
12
A resposta de Chris Jester-Young deve ser a "boa resposta" lá ... BTW, esta solução não funcionará se um nome de arquivo começar com "-". Pelo menos, ele precisa "-" depois do cp.
Keltia 23/01/09
11
Exemplo de velocidade - em 829 arquivos, o método "find -exec" levou 26 segundos, enquanto a ferramenta "find -print0 | xargs --null" 0,7 segundos. Diferença significante.
Peter Porter
7
@tzot Um comentário tardio, mas de qualquer maneira, xargsnão é necessário para solucionar o problema que você está descrevendo, findjá o suporta com a -exec +pontuação.
Jlliagre
3
não responde à questão de como lidar com espaços
Ben Glasser
117

find . -print0 | grep --null 'FooBar' | xargs -0 ...

Não sei se grepsuporta --null, nem se xargssuporta -0, no Leopard, mas no GNU é tudo de bom.

Chris Jester-Young
fonte
1
O Leopard suporta "-Z" (é GNU grep) e, é claro, o find (1) e o xargs (1) suportam "-0".
Keltia 23/01/09
1
No OS X 10.9, grep -{z|Z}significa "comporte-se como zgrep" (descompacte) e não o pretendido "imprima um byte zero após cada nome de arquivo". Use grep --nullpara alcançar o último.
Bassim
4
O que há de errado find . -name 'FooBar' -print0 | xargs -0 ...?
Quentin Pradet
1
@QuentinPradet Obviamente, para uma sequência fixa como "FooBar", -nameou -pathfuncione bem. O OP especificou o uso de grep, presumivelmente porque eles querem filtrar a lista usando expressões regulares.
Chris Jester-Young
1
@ Hi-Angel É exatamente por isso que uso xargs -0 em conjunto find -print0 . O último imprime nomes de arquivos com um terminador NUL e o primeiro recebe arquivos dessa maneira. Por quê? Os nomes de arquivos no Unix podem conter caracteres de nova linha. Mas eles não podem conter caracteres NUL.
Chris Jester-Young
92

A maneira mais fácil de fazer o que o pôster original deseja é alterar o delimitador de qualquer espaço em branco para apenas o caractere de fim de linha como este:

find whatever ... | xargs -d "\n" cp -t /var/tmp
user87601
fonte
4
Essa resposta é simples, eficaz e direta ao ponto: o delimitador padrão definido para xargs é muito amplo e precisa ser reduzido para o que o OP deseja fazer. Eu sei disso em primeira mão porque me deparei com essa mesma questão hoje fazendo algo semelhante, exceto no cygwin. Se eu tivesse lido a ajuda do comando xargs, poderia ter evitado algumas dores de cabeça, mas sua solução o corrigiu para mim. Obrigado ! (Sim, o OP estava no MacOS usando BSD xargs, que eu não uso, mas espero que o parâmetro xargs "-d" exista em todas as versões).
Etienne Delavennat
7
Boa resposta, mas não está funcionando no Mac. Em vez disso, pode canalizar o achado para sed -e 's_\(.*\)_"\1"_g'a citações de força em torno do nome do arquivo
ishahak
10
Essa deve ser a resposta aceita. A questão era sobre o uso xargs.
Mohammad Alhashash 6/11/2016
2
Eu receboxargs: illegal option -- d
nehem 11/11
1
Vale ressaltar que os nomes de arquivos podem conter um caractere de nova linha em muitos sistemas * nix. É improvável que você se depare com isso na natureza, mas se estiver executando comandos do shell em entradas não confiáveis, isso pode ser uma preocupação.
Soren Bjornstad 19/01/19
71

Isso é mais eficiente, pois não executa o "cp" várias vezes:

find -name '*FooBar*' -print0 | xargs -0 cp -t ~/foo/bar
Tometzky
fonte
1
Isso não funcionou para mim. Ele tentou cp ~ / foo / bar em tudo o que você encontrar, mas não o contrário
Shervin Asgari
13
O sinalizador -t para cp é uma extensão GNU, AFAIK, e não está disponível no OS X. Mas, se fosse, funcionaria como mostrado nesta resposta.
Metamatt 18/05/12
2
Estou usando o Linux. Obrigado pela opção '-t'. Isso é o que estava faltando :-)
Vahid Pazirandeh
59

Eu tive o mesmo problema. Aqui está como eu resolvi:

find . -name '*FoooBar*' | sed 's/.*/"&"/' | xargs cp ~/foo/bar

Eu costumava sedsubstituir cada linha de entrada pela mesma linha, mas cercada por aspas duplas. Na sedpágina de manual, " ... Um e comercial (` `& '') que aparece na substituição é substituído pela string correspondente ao RE ... " - nesse caso .*, a linha inteira.

Isso resolve o xargs: unterminated quoteerro.

oyouareatubeo
fonte
3
Estou no windows e estou usando o gnuwin32, então tive que usar sed s/.*/\"&\"/para fazê-lo funcionar.
Pat
Sim, mas presumivelmente isso não trataria nomes de arquivos com "in - a menos que sed também cite aspas?
Artfulrobot 27/05
Usar sedé genial e, por enquanto, a solução correta sem reescrever o problema!
entonio 3/06/2015
53

Este método funciona no Mac OS X 10.7.5 (Lion):

find . | grep FooBar | xargs -I{} cp {} ~/foo/bar

Também testei a sintaxe exata que você postou. Isso também funcionou bem em 10.7.5.

the_minted
fonte
4
Isso funciona, mas -Iimplica -L 1(assim diz o manual), o que significa que o comando cp está sendo executado uma vez por arquivo = v lento.
Artfulrobot 27/05
xargs -J% cp% <dir de destino> Possivelmente é mais eficiente no OSX.
Walker D
3
Desculpe, mas isso está errado. Primeiro, produz exatamente o erro que o TO queria evitar. Você deve usar find ... -print0e xargs -0trabalhar em torno de xargs "por padrão, as cotações são especiais". Segundo, geralmente '{}'não use {}comandos passados ​​para xargs, para proteger contra espaços e caracteres especiais.
Andreas Spindler
3
Desculpe Andreas Spindler, eu não estou familiarizado com xargs e encontrei essa linha após algumas experiências. Parece funcionar para a maioria das pessoas que comentaram e votaram positivamente. Você se importaria em detalhar um pouco mais que tipo de erro ele produz? Além disso, você se importaria de postar a entrada exata que acha mais correta? Obrigado.
the_minted
12

Só não use xargs. É um programa interessante, mas não combina bem com findcasos não triviais.

Aqui está uma solução portátil (POSIX), ou seja, que não requer find, xargsou cpextensões específicas do GNU:

find . -name "*FooBar*" -exec sh -c 'cp -- "$@" ~/foo/bar' sh {} +

Observe o final em +vez do mais usual ;.

Esta solução:

  • lida corretamente com arquivos e diretórios com espaços incorporados, novas linhas ou quaisquer caracteres exóticos.

  • funciona em qualquer sistema Unix e Linux, mesmo aqueles que não fornecem o kit de ferramentas GNU.

  • não usa o xargsque é um programa agradável e útil, mas requer muitos recursos de ajuste e fora do padrão para lidar adequadamente com a findsaída.

  • também é mais eficiente (leia mais rápido ) que o aceito e a maioria, senão todas as outras respostas.

Observe também que, apesar do que é indicado em outras respostas ou comentários, a citação {}é inútil (a menos que você esteja usando o fishshell exótico ).

jlliagre
fonte
1
@PeterMortensen Você provavelmente ignora o final positivo. findpode fazer o que xargsfaz sem sobrecarga.
Jlliagre
8

Procure usar a opção --null commandline para xargs com a opção -print0 em find.

Shannon Nelson
fonte
8

Para quem confia em comandos, além de encontrar, por exemplo ls:

find . | grep "FooBar" | tr \\n \\0 | xargs -0 -I{} cp "{}" ~/foo/bar
Aleksandr Guidrevitch
fonte
1
Funciona, mas porque lenta -Iimplica-L 1
artfulrobot
6
find | perl -lne 'print quotemeta' | xargs ls -d

Acredito que isso funcione de maneira confiável para qualquer personagem, exceto o feed de linha (e eu suspeito que, se você tiver feeds de linha em seus nomes de arquivo, você tem problemas piores do que isso). Não requer o GNU findutils, apenas o Perl, por isso deve funcionar praticamente em qualquer lugar.

mavit
fonte
É possível ter um feed de linha em um nome de arquivo? Nunca ouvi falar disso.
Mtk 16/05/2019
2
De fato é. Tente, por exemplo,mkdir test && cd test && perl -e 'open $fh, ">", "this-file-contains-a-\n-here"' && ls | od -tx1
mavit 17/05
1
|perl -lne 'print quotemeta'é exatamente o que eu tenho procurado. Outras postagens aqui não me ajudaram porque, em vez de findprecisar usar, grep -rlreduzi bastante o número de arquivos PHP apenas para os infectados por malware.
Marcos
perl e quotemeta são muito mais geral do que print0 / -0 - graças para a solução geral para pipelining arquivos com espaços
bmike
5

Eu descobri que a seguinte sintaxe funciona bem para mim.

find /usr/pcapps/ -mount -type f -size +1000000c | perl -lpe ' s{ }{\\ }g ' | xargs ls -l | sort +4nr | head -200

Neste exemplo, estou procurando os maiores 200 arquivos com mais de 1.000.000 de bytes no sistema de arquivos montado em "/ usr / pcapps".

O liner de linha Perl entre "find" e "xargs" escapa / cita cada espaço em branco para que "xargs" passe qualquer nome de arquivo com espaços em branco incorporados para "ls" como um único argumento.

Peter Mortensen
fonte
3

Desafio de quadros - você está perguntando como usar o xargs. A resposta é: você não usa xargs, porque não precisa.

O comentáriouser80168 descreve uma maneira de fazer isso diretamente com o cp, sem chamar o cp para cada arquivo:

find . -name '*FooBar*' -exec cp -t /tmp -- {} +

Isso funciona porque:

  • o cp -tsinalizador permite fornecer o diretório de destino próximo ao início de cp, e não perto do final. De man cp:
   -t, --target-directory=DIRECTORY
         copy all SOURCE arguments into DIRECTORY
  • O --sinalizador indica cppara interpretar tudo depois como um nome de arquivo, não um sinalizador; portanto, os arquivos que começam com -ou --não confundem cp; você ainda precisa disso porque os caracteres -/ --são interpretados por cp, enquanto outros caracteres especiais são interpretados pelo shell.

  • A find -exec command {} +variante essencialmente faz o mesmo que xargs. De man find:

   -exec command {} +                                                     
         This  variant  of the -exec action runs the specified command on
         the selected files, but the command line is built  by  appending
         each  selected file name at the end; the total number of invoca‐
         matched  files.   The command line is built in much the same way
         that xargs builds its command lines.  Only one instance of  `{}'
         is  allowed  within the command, and (when find is being invoked
         from a shell) it should be quoted (for example, '{}') to protect
         it  from  interpretation  by shells.  The command is executed in
         the starting directory.  If any invocation  returns  a  non-zero
         value  as exit status, then find returns a non-zero exit status.
         If find encounters an error, this can sometimes cause an immedi‐
         ate  exit, so some pending commands may not be run at all.  This
         variant of -exec always returns true.

Ao usar isso em localizar diretamente, isso evita a necessidade de um pipe ou uma invocação de shell, para que você não precise se preocupar com caracteres desagradáveis ​​nos nomes de arquivos.

gerrit
fonte
Descoberta incrível, eu não tinha ideia !!! "utilitário -exec [argumento ...] {} + O mesmo que -exec, exceto que` `{} '' é substituído pelo maior número de caminhos possível para cada chamada do utilitário. Esse comportamento é semelhante ao de xargs (1 ). " na implementação do BSD.
conny
2

Esteja ciente de que a maioria das opções discutidas em outras respostas não é padrão em plataformas que não usam os utilitários GNU (Solaris, AIX, HP-UX, por exemplo). Consulte a especificação POSIX para obter o comportamento xargs 'padrão'.

Também acho o comportamento de xargs pelo qual ele executa o comando pelo menos uma vez, mesmo sem entrada, ser um incômodo.

Eu escrevi minha própria versão privada do xargs (xargl) para lidar com os problemas de espaços nos nomes (apenas as novas linhas se separam - embora a combinação 'find ... -print0' e 'xargs -0' seja bastante interessante, pois os nomes dos arquivos não podem contém caracteres ASCII NUL '\ 0'. Meu xargl não é tão completo quanto deveria valer a pena ser publicado - especialmente porque o GNU tem instalações que são pelo menos tão boas.

Jonathan Leffler
fonte
2
GitHub ou isso não aconteceu
Corey Goldberg
@ CoreyGoldberg: Acho que não aconteceu então.
27616 Jonathan Leffler
O POSIX findnão precisa xargsem primeiro lugar (e isso já era verdade há 11 anos).
Jlliagre
2

Com o Bash (não POSIX), você pode usar a substituição de processo para obter a linha atual dentro de uma variável. Isso permite que você use aspas para escapar de caracteres especiais:

while read line ; do cp "$line" ~/bar ; done < <(find . | grep foo)
StackedCrooked
fonte
2

Para mim, eu estava tentando fazer algo um pouco diferente. Eu queria copiar meus arquivos .txt na minha pasta tmp. Os nomes de arquivo .txt contêm espaços e caracteres de apóstrofo. Isso funcionou no meu Mac.

$ find . -type f -name '*.txt' | sed 's/'"'"'/\'"'"'/g' | sed 's/.*/"&"/'  | xargs -I{} cp -v {} ./tmp/
Moises
fonte
1

Se as versões find e xarg em seu sistema não suportarem -print0e -0alternar (por exemplo, AIX find e xargs), você poderá usar este código de aparência terrível:

 find . -name "*foo*" | sed -e "s/'/\\\'/g" -e 's/"/\\"/g' -e 's/ /\\ /g' | xargs cp /your/dest

Aqui o sed cuidará de escapar dos espaços e das aspas para xargs.

Testado no AIX 5.3

Jan Ptáčník
fonte
1

Criei um pequeno script de invólucro portátil chamado "xargsL" em torno de "xargs", que soluciona a maioria dos problemas.

Ao contrário de xargs, xargsL aceita um nome de caminho por linha. Os nomes de caminho podem conter qualquer caractere, exceto (obviamente) nova linha ou bytes NUL.

Nenhuma citação é permitida ou suportada na lista de arquivos - seus nomes de arquivos podem conter todos os tipos de espaços em branco, barras invertidas, barras de reticulação, caracteres curinga do shell e similares - o xargsL os processará como caracteres literais, sem causar danos.

Como um recurso adicional de bônus, o xargsL não executará o comando uma vez se não houver entrada!

Observe a diferença:

$ true | xargs echo no data
no data

$ true | xargsL echo no data # No output

Quaisquer argumentos fornecidos ao xargsL serão passados ​​para o xargs.

Aqui está o script de shell POSIX "xargsL":

#! /bin/sh
# Line-based version of "xargs" (one pathname per line which may contain any
# amount of whitespace except for newlines) with the added bonus feature that
# it will not execute the command if the input file is empty.
#
# Version 2018.76.3
#
# Copyright (c) 2018 Guenther Brunthaler. All rights reserved.
#
# This script is free software.
# Distribution is permitted under the terms of the GPLv3.

set -e
trap 'test $? = 0 || echo "$0 failed!" >& 2' 0

if IFS= read -r first
then
        {
                printf '%s\n' "$first"
                cat
        } | sed 's/./\\&/g' | xargs ${1+"$@"}
fi

Coloque o script em algum diretório no seu $ PATH e não se esqueça de

$ chmod +x xargsL

o script lá para torná-lo executável.

Guenther Brunthaler
fonte
1

A versão Perl do bill_starr não funcionará bem para novas linhas incorporadas (apenas lida com espaços). Para aqueles que, por exemplo, Solaris, onde você não possui as ferramentas GNU, uma versão mais completa pode ser (usando sed) ...

find -type f | sed 's/./\\&/g' | xargs grep string_to_find

ajuste os argumentos find e grep ou outros comandos conforme necessário, mas o sed corrigirá suas novas linhas / espaços / guias.

Peter Mortensen
fonte
1

Usei a resposta de Bill Star ligeiramente modificada no Solaris:

find . -mtime +2 | perl -pe 's{^}{\"};s{$}{\"}' > ~/output.file

Isso colocará aspas em cada linha. Eu não usei a opção '-l', embora provavelmente ajude.

A lista de arquivos que eu estava indo embora poderia ter '-', mas não novas linhas. Eu não usei o arquivo de saída com outros comandos, pois quero revisar o que foi encontrado antes de começar a excluí-los massivamente via xargs.

Carl Yamamoto-Furst
fonte
1

Eu brinquei um pouco com isso, comecei a contemplar a modificação de xargs e percebi que, para o tipo de caso de uso que estamos falando aqui, uma simples reimplementação no Python é uma idéia melhor.

Por um lado, ter ~ 80 linhas de código para a coisa toda significa que é fácil descobrir o que está acontecendo, e se um comportamento diferente for necessário, você pode simplesmente invadir um novo script em menos tempo do que o necessário para obter uma resposta em algum lugar como Stack Overflow.

Consulte https://github.com/johnallsup/jda-misc-scripts/blob/master/yargs e https://github.com/johnallsup/jda-misc-scripts/blob/master/zargs.py .

Com yargs como está escrito (e o Python 3 instalado), você pode digitar:

find .|grep "FooBar"|yargs -l 203 cp --after ~/foo/bar

para copiar 203 arquivos de cada vez. (Aqui, 203 é apenas um espaço reservado, é claro, e usar um número estranho como 203 deixa claro que esse número não tem outro significado.)

Se você realmente deseja algo mais rápido e sem a necessidade de Python, tome zargs e yargs como protótipos e reescreva em C ++ ou C.

John Allsup
fonte
0

Você pode precisar grep o diretório Foobar como:

find . -name "file.ext"| grep "FooBar" | xargs -i cp -p "{}" .
fred
fonte
1
De acordo com a página do manual, -iestá obsoleto e -Ideve ser usado.
Acumenos
-1

Se você estiver usando o Bash, poderá converter stdout em uma matriz de linhas mapfile:

find . | grep "FooBar" | (mapfile -t; cp "${MAPFILE[@]}" ~/foobar)

Os benefícios são:

  • É embutido, então é mais rápido.
  • Execute o comando com todos os nomes de arquivo de uma só vez, para que seja mais rápido.
  • Você pode acrescentar outros argumentos aos nomes dos arquivos. Para cp, você também pode:

    find . -name '*FooBar*' -exec cp -t ~/foobar -- {} +
    

    no entanto, alguns comandos não possuem esse recurso.

As desvantagens:

  • Talvez não seja dimensionado bem se houver muitos nomes de arquivos. (O limite? Eu não sei, mas eu testei com um arquivo de lista de 10 MB, que inclui mais de 10000 nomes de arquivos sem problemas, no Debian)

Bem ... quem sabe se o Bash está disponível no OS X?

Xiè Jìléi
fonte