Resolvendo “mv: lista de argumentos muito longa”?

64

Eu tenho uma pasta com mais de um milhão de arquivos que precisa ser classificada, mas não posso fazer nada porque mvgera essa mensagem o tempo todo

-bash: /bin/mv: Argument list too long

Estou usando este comando para mover arquivos sem extensão:

mv -- !(*.jpg|*.png|*.bmp) targetdir/
Dominique
fonte

Respostas:

82

xargsé a ferramenta para o trabalho. Isso ou findcom -exec … {} +. Essas ferramentas executam um comando várias vezes, com tantos argumentos quanto podem ser passados ​​de uma só vez.

Ambos os métodos são mais fáceis de executar quando a lista de argumentos variáveis ​​está no final, o que não é o caso aqui: o argumento final para mvé o destino. Com os utilitários GNU (ou seja, no Linux ou Cygwin não incorporado), a -topção to mvé útil para passar o destino primeiro.

Se os nomes dos arquivos não tiverem espaço em branco nem nenhum \"', você poderá simplesmente fornecer os nomes dos arquivos como entrada xargs(o echocomando é um bash integrado, portanto, não está sujeito ao limite de comprimento da linha de comando):

echo !(*.jpg|*.png|*.bmp) | xargs mv -t targetdir

Você pode usar a -0opção para xargsusar entrada delimitada por nulo em vez do formato entre aspas padrão.

printf '%s\0' !(*.jpg|*.png|*.bmp) | xargs -0 mv -t targetdir

Como alternativa, você pode gerar a lista de nomes de arquivos com find. Para evitar a recursão em subdiretórios, use -type d -prune. Como nenhuma ação é especificada para os arquivos de imagem listados, apenas os outros arquivos são movidos.

find . -name . -o -type d -prune -o \
       -name '*.jpg' -o -name '*.png' -o -name '*.bmp' -o \
       -exec mv -t targetdir/ {} +

(Isso inclui arquivos de ponto, ao contrário dos métodos curinga do shell.)

Se você não possui utilitários GNU, pode usar um shell intermediário para obter os argumentos na ordem correta. Este método funciona em todos os sistemas POSIX.

find . -name . -o -type d -prune -o \
       -name '*.jpg' -o -name '*.png' -o -name '*.bmp' -o \
       -exec sh -c 'mv "$@" "$0"' targetdir/ {} +

No zsh, você pode carregar o mvbuiltin :

setopt extended_glob
zmodload zsh/files
mv -- ^*.(jpg|png|bmp) targetdir/

ou se você preferir permitir mve outros nomes continuem se referindo aos comandos externos:

setopt extended_glob
zmodload -Fm zsh/files b:zf_\*
zf_mv -- ^*.(jpg|png|bmp) targetdir/

ou com globs no estilo ksh:

setopt ksh_glob
zmodload -Fm zsh/files b:zf_\*
zf_mv -- !(*.jpg|*.png|*.bmp) targetdir/

Como alternativa, usando GNU mve zargs:

autoload -U zargs
setopt extended_glob
zargs -- ./^*.(jpg|png|bmp) -- mv -t targetdir/
Gilles 'SO- parar de ser mau'
fonte
11
Os dois primeiros comandos retornaram "-bash:!: Event not found" e os próximos dois não moveram nenhum arquivo. Estou no CentOS 6.5, se você souber #
Dominique
11
@ Dominique Eu usei a mesma sintaxe globbing que você usou na sua pergunta. Você precisará shopt -s extglobhabilitá-lo. Eu tinha perdido um passo nos findcomandos, eu os consertei.
Gilles 'SO- stop be evil'
Estou recebendo isso com o comando find "find: expressão inválida; você usou um operador binário '-o' sem nada antes dele". Agora vou tentar os outros.
Dominique
@ Dominique Os findcomandos que eu publiquei (agora) funcionam. Você deve ter deixado uma peça ao copiar e colar.
Gilles 'SO- stop be evil'
Gilles, para os comandos find, por que não usar o operador "not" !? É mais explícito e mais fácil de entender do que a trilha ímpar -o. Por exemplo,! -name '*.jpg' -a ! -name '*.png' -a ! -name '*.bmp'
CivFan 23/10/2015
13

Se trabalhar com o kernel Linux é suficiente, você pode simplesmente fazer

ulimit -s 100000

isso funcionará porque o kernel do Linux incluiu um patch há cerca de 10 anos que mudou o limite de argumentos para se basear no tamanho da pilha: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/ confirmar /? id = b6a2fea39318e43fee84fa7b0b90d68bed92d2ba

Atualização: Se você se sente corajoso, pode dizer

ulimit -s unlimited

e você ficará bem com todas as expansões de shell, desde que tenha RAM suficiente.

Mikko Rantalainen
fonte
Isso é um truque. Como você saberia como definir o limite da pilha? Isso também afeta outros processos iniciados na mesma sessão.
Kusalananda
11
Sim, é um truque. Na maioria das vezes, esse tipo de invasão é pontual (com que frequência você move manualmente uma quantidade enorme de arquivos?). Se você tiver certeza de que o processo não consumirá toda a sua memória RAM, é possível configurá ulimit -s unlimited-lo e ele funcionará para arquivos praticamente ilimitados.
Mikko Rantalainen 3/11
Com ulimit -s unlimitedo limite da linha de comando real, é 2 ^ 31 ou 2 GB. ( MAX_ARG_STRLENFonte no kernel.)
Mikko Rantalainen
9

O limite de aprovação de argumentos do sistema operacional não se aplica a expansões que ocorrem dentro do interpretador de shell. Portanto, além de usar xargsor find, podemos simplesmente usar um loop de shell para dividir o processamento em mvcomandos individuais :

for x in *; do case "$x" in *.jpg|*.png|*.bmp) ;; *) mv -- "$x" target ;; esac ; done

Isso usa apenas recursos e utilitários da POSIX Shell Command Language. Essa linha única é mais clara com recuo, com ponto e vírgula desnecessário removido:

for x in *; do
  case "$x" in
    *.jpg|*.png|*.bmp) 
       ;; # nothing
    *) # catch-all case
       mv -- "$x" target
       ;;
  esac
done
Kaz
fonte
Com mais de um milhão de arquivos, isso gerará mais de um milhão de mvprocessos, em vez de apenas os poucos necessários usando a findsolução POSIX @Gilles postada. Em outras palavras, dessa maneira resulta em muitas rotações desnecessárias da CPU.
CivFan
@CivFan Outro problema é convencer-se de que a versão modificada é equivalente à original. É fácil ver que a casedeclaração sobre o resultado da *expansão para filtrar várias extensões é equivalente à !(*.jpg|*.png|*.bmp)expressão original . A findresposta não é de fato equivalente; desce em subdiretórios (não vejo um -maxdepthpredicado).
Kaz
-name . -o -type d -prune -oprotege de descer para subdiretórios. -maxdepthaparentemente não é compatível com POSIX, embora isso não seja mencionado na minha findpágina de manual.
CivFan
Revertida para a revisão 1. A pergunta não diz nada sobre variáveis ​​de origem ou destino, portanto, isso adiciona fragmentos desnecessários à resposta.
Kaz
5

Para uma solução mais agressiva do que as oferecidas anteriormente, acesse a fonte do kernel e edite include/linux/binfmts.h

Aumente o tamanho MAX_ARG_PAGESpara algo maior que 32. Isso aumenta a quantidade de memória que o kernel permitirá para argumentos do programa, permitindo que você especifique seu comando mvou rmpara um milhão de arquivos ou o que estiver fazendo. Recompile, instale, reinicie.

Cuidado! Se você definir isso muito grande para a memória do sistema e executar um comando com muitos argumentos, COISAS MAU ACONTECEREM! Seja extremamente cauteloso ao fazer isso em sistemas multiusuário, pois torna mais fácil para usuários mal-intencionados usar toda a sua memória!

Se você não sabe como recompilar e reinstalar seu kernel manualmente, provavelmente é melhor que você apenas finja que essa resposta não existe por enquanto.

Perkins
fonte
5

Uma solução mais simples usando em "$origin"/!(*.jpg|*.png|*.bmp)vez de um bloco catch:

for file in "$origin"/!(*.jpg|*.png|*.bmp); do mv -- "$file" "$destination" ; done

Obrigado a @Score_Under

Para um script de várias linhas, você pode fazer o seguinte (observe o ;antes do donelançamento):

for file in "$origin"/!(*.jpg|*.png|*.bmp); do        # don't copy types *.jpg|*.png|*.bmp
    mv -- "$file" "$destination" 
done 

Para fazer uma solução mais generalizada que mova todos os arquivos, você pode fazer o seguinte procedimento:

for file in "$origin"/*; do mv -- "$file" "$destination" ; done

Que fica assim se você fizer o recuo:

for file in "$origin"/*; do
    mv -- "$file" "$destination"
done 

Isso pega todos os arquivos na origem e os move um a um para o destino. As aspas $filesão necessárias caso haja espaços ou outros caracteres especiais nos nomes dos arquivos.

Aqui está um exemplo desse método que funcionou perfeitamente

for file in "/Users/william/Pictures/export_folder_111210/"*.jpg; do
    mv -- "$file" "/Users/william/Desktop/southland/landingphotos/";
done
Gato branco
fonte
Você pode usar algo como o glob original no loop for para obter uma solução mais próxima do que está sendo solicitado.
Score_Under
Como assim, glob original?
WhiteCat
Desculpe se isso foi um pouco enigmática, eu estava me referindo ao glob na questão: !(*.jpg|*.png|*.bmp). Você pode adicionar isso ao seu loop for observando o "$origin"/!(*.jpg|*.png|*.bmp)que evitaria a necessidade da chave usada na resposta de Kaz e manteria o corpo simples do loop for.
Score_Under
Pontuação impressionante. Incorporei seu comentário e atualizei minha resposta.
WhiteCat
3

Às vezes é mais fácil escrever um pequeno script, por exemplo, em Python:

import glob, shutil

for i in glob.glob('*.jpg'):
  shutil.move(i, 'new_dir/' + i)
duhaime
fonte
1

Você pode contornar essa restrição enquanto ainda estiver usando, mvse não se importar de executá-la algumas vezes.

Você pode mover partes de cada vez. Digamos, por exemplo, que você tenha uma longa lista de nomes de arquivos alfanuméricos.

mv ./subdir/a* ./

Isso funciona. Então nocauteie outro pedaço grande. Depois de alguns movimentos, você pode voltar a usarmv ./subdir/* ./

Kristian
fonte
0

Aqui estão meus dois centavos, acrescente isso .bash_profile

mv() {
  if [[ -d $1 ]]; then #directory mv
    /bin/mv $1 $2
  elif [[ -f $1 ]]; then #file mv
    /bin/mv $1 $2
  else
    for f in $1
    do
      source_path=$f
      #echo $source_path
      source_file=${source_path##*/}
      #echo $source_file
      destination_path=${2%/} #get rid of trailing forward slash

      echo "Moving $f to $destination_path/$source_file"

      /bin/mv $f $destination_path/$source_file
    done
  fi
}
export -f mv

Uso

mv '*.jpg' ./destination/
mv '/path/*' ./destination/
Ako
fonte