Classifique e mv arquivos com base no nome do arquivo (com espaços), recursivamente

Classifique e mv arquivos com base no nome do arquivo (com espaços), recursivamente

Cometi um erro e coloquei os arquivos juntos no mesmo diretório. Felizmente posso classificá-los com base no nome do arquivo: ''' 2019-02-19 20.18.58.ndpi_2_2688_2240.jpg ''' Onde o#pouco ou2neste caso específico são as informações de localização, por assim dizer. O intervalo é de 0 a 9 e os nomes dos arquivos têm comprimento idêntico, de modo que o número estará sempre na mesma posição do nome do arquivo (26º caractere incluindo espaços, flanqueado por sublinhados). Encontrei este ótimo link: comando para localizar arquivos pesquisando apenas parte de seus nomes?

No entanto, não consigo canalizar a saída para o comando move. Tentei fazer um loop da saída em uma variável, mas também não funcionou:

for f in find . -name '*_0_*' ; do  mv "$f" /destination/directory ; done

Com base neste link, posso ter colocado * ou algumas aspas no lugar errado:mv: não é possível stat Arquivo ou diretório inexistente no shell script.

Dito isto, tenho muitos diretórios e gostaria de classificá-los em uma estrutura de diretórios idêntica em outro lugar:

-Flowers (to be sorted)          -Flowers-buds               -Flowers-stems
     -Roses                          -Roses                      -Roses
        buds.jpg       ===>             buds.jpg     ===>           stems.jpg
        stems.jpg
        petals.jpg
     -Daisies                       -Daisies                    -Daisies
        buds.jpgs                       buds.jpg                   stems.jpg
        stems.jpg
        petals.jpg
     -Tulips                        -Tulips                     -Tulip
        buds.jpgs                       buds.jpg                  stems.jpg
        stems.jpg
        petals.jpg

... e mais com base nesse número (#). Isso é algo prático no bash? Estou executando o MacOS no terminal com coreutils instalados para que as ferramentas se comportem como GNU Linux, em vez de Darwin (BSD).

Responder1

Você pode fazer isso com finde mv, mas não é a abordagem mais simples aqui. Você está no macOS, entãozshestá pré-instalado. Zsh vem com uma ferramenta bacana de renomeação em massa de arquivos chamadazmv. Execute zshem um terminal e, em zsh, execute algo como:

autoload zmv
zmv -n '[^_]#_([0-9])_*' '/elsewhere/${1}/${f}'

Explicações:

  • -ndiz zmvpara exibir o que faria, mas na verdade não faz nada. Quando estiver satisfeito com o que ele mostra, execute o comando novamente sem -n.
  • O primeiro argumento de não opção é umpadrão curinga. zmvatuará nos arquivos correspondentes (e ignorará os arquivos não correspondentes).
  • [^_]#significa zero ou mais caracteres diferentes de _. Zsh dá acesso aos mesmos curingas do bash e muito mais. #é um recurso somente zsh (disponível no bash com uma sintaxe diferente) que significa “qualquer número do caractere anterior”. O padrão [^_]#_[0-9]_*corresponde a qualquer nome de arquivo que contenha um único dígito entre dois sublinhados e nenhum outro sublinhado antes desse dígito.
  • Os parênteses [0-9]disponibilizam o dígito como $1no texto de substituição.
  • O texto de substituição pode usar ${1}, ${2}, etc. para se referir a fragmentos entre parênteses no padrão de origem e ${f}para se referir ao nome original completo.

Por exemplo, o trecho acima é movido 2019-02-19 20.18.58.ndpi_2_2688_2240.jpgpara /elsewhere/2/2019-02-19 20.18.58.ndpi_2_2688_2240.jpg. Sua pergunta não descreve o que precisa ser movido e para onde precisa ser movido com muita precisão, mas você deve conseguir o que deseja ajustando o comando acima. Se você não conseguir descobrir, edite sua pergunta para adicionar exemplos específicos do que deve ou não ser correspondido.

Se você tiver arquivos em subdiretórios, poderá usar */no início do padrão. Se você precisar combinar o diretório para se referir a ele , será necessário colocar parênteses em torno de : você não pode colocar parênteses em torno de uma barra. Se você deseja percorrer o diretório recursivamente e combinar arquivos com qualquer débito, use para${NUM}***/globulação recursiva. Como exceção, você precisa usar (**/)para acessar o caminho do diretório no texto de substituição como . Em casos como este, muitas vezes é mais fácil não usar parênteses e, em vez disso, usar${NUM}modificadorespara ${f}extrair partes do caminho original como um todo. Por exemplo, faça a mesma renomeação acima, mas movendo os arquivos do diretório atual para uma estrutura paralela em /elsewhere, mas com um nível extra 0, 1, etc., logo antes do nome do arquivo:

autoload zmv
zmv -n '**/[^_]#_([0-9])_*' '/elsewhere/${f:h}/${1}/${f:t}'

Se for difícil combinar os arquivos com base no nome, uma abordagem diferente seria combiná-los com base no horário de alteração¹. Se você acabou de copiar vários arquivos para um diretório que não era alterado há algum tempo, os novos arquivos são aqueles que têm um horário de alteração recente. Você pode combinar arquivos por hora de alteração em zsh com oqualificador globalc. Por exemplo, para listar os arquivos alterados pela última vez na última hora:

ls -lctr *(ch-1)

Em vez de encontrar um tempo limite, você pode consultar oNarquivos alterados mais recentemente com os qualificadores glob oc(para classificar aumentando ctime) e (para manter apenas o primeiro[1,N]Npartidas). Por exemplo, se você sabe que acabou de mover 42 arquivos para esse diretório e não alterou nada desde então:

ls -lctr *(oc[1,42])

Se quiser usar qualificadores glob com zmv, você precisa passar a -Qopção. Por exemplo, para mover arquivos para um diretório com base em seus nomes, como acima, mas ignore todos os arquivos que não foram alterados na última hora:

zmv -n -Q '[^_]#_([0-9])_*(ch-1)' '/elsewhere/${1}/${f}'

zmvpossui algumas medidas de segurança para verificar se não substituirá arquivos e se não haverá colisões (dois arquivos de origem com o mesmo nome de destino). Se você tiver um grande número de arquivos, essas medidas de segurança podem levar algum tempo. Se zmvfor muito lento e a estrutura da sua renomeação garantir que não haverá colisões, você poderá codificar a renomeação específica que está fazendo em um loop. Zsh tende a vencer o bash mesmo se você não o usa, zmvgraças ao seu melhorglobulandoeexpansão de parâmetrosmecanismos. Para desempenho, você podecarregar dinamicamenteum módulo que contémbuiltins para manipulação de arquivos.

Por exemplo, para mover arquivos regulares do diretório atual para uma hierarquia totalmente nova se o nome deles contiver _0_:

zmodload -m -F zsh/files 'b:zf_*'
for x in **/*_0_*(.); do
  zf_mkdir -p /destination/directory/$x:h
  zf_mv ./$x /destination/directory/$x
done

( :hé mais um recurso do zsh: ummodificador de histórico.)

Para pegar o primeiro número que está entre dois sublinhados no nome do arquivo e colocar os arquivos em um diretório com o nome desse número, você precisará extrair o número. zmvfaz isso automaticamente para grupos entre parênteses, aqui precisamos fazer isso manualmente.

zmodload -m -F zsh/files 'b:zf_*'
for source in **/*_<->_*(.); do
  suffix=${${source:t}#*_<->_}
  n=${${source%$suffix}##*_}
  destination=${source:h}/$n/${source:t}
  zf_mkdir -p /destination/directory/$destination:h
  zf_mv ./$source /destination/directory/$destination
done

Se quiser aprender sobre outros métodos para renomear arquivos em massa, você pode navegarperguntas marcadas renameneste site.

¹ O horário de alteração do inode de um arquivo, denominado “tempo de alteração” ou “ctime”, abreviadamente, é o horário em que o arquivo foi movido pela última vez ou teve seus atributos alterados pela última vez, como permissões. É diferente do horário de modificação (mtime).

Responder2

O shell está dividindo a entrada em espaços em branco. Você pode usar bash globbing com o recursivo **para dividir os nomes dos arquivos corretamente:

shopt -s globstar
for f in **/*_0_*; do mv "$f" /dest/; done

Se todos estiverem indo para o mesmo lugar, o loop não será necessário:

shopt -s globstar
mv **/*_0_*  /dest/

... ou use find -exec, que passa nomes de arquivos diretamente de find para uma exec()chamada, sem o envolvimento do shell:

find . -name '*_0_*' -exec mv {} /dest/ \;

Ou use readmais find -print0para dividir em nulo. É um exagero para este problema, útil ao globbing e find -execnão está à altura da tarefa:

while IFS=  read -r -d ''; do mv "$REPLY" /dest/; done < <( find . -name '*_0_*' -print0 )

Para alterar o destino de nível superior com base no nome do arquivo, como no seu exemplo, corte partes do nome do arquivo usando ${var#remove_from_start}e ${var%remove_from_end}. Os curingas nos padrões de remoção removerão o mínimo possível; para remover o máximo possível, dobre o caractere da operação, como em ${…##…}ou ${…%%…}:

    shopt -s globstar
    for f in **/*_0_*; do            # Flowers/Roses/stems.jpg 
      file=${f##*/}                  # stems.jpg
      base=${file%.*}                # stems
      path=${f%/*}                   # Flowers/Roses
      toppath=${path%%/*}            # Flowers
      subpath=${path#*/}             # Roses
      dest=$toppath-$base/$subpath/  # Flowers-stems/Roses/
      [[ -d $dest ]] || mkdir -p "$dest"
      mv "$f" "$dest"
    done

informação relacionada