Melhor maneira de renomear arquivos com base em vários padrões

muitos arquivos que eu baixei têm porcaria / spam em seus nomes de arquivos, por exemplo

[ www.crap.com ] file.name.ext

www.crap.com - file.name.ext

Eu criei duas maneiras de lidar com elas, mas as duas parecem bem desajeitadas:

com expansão de parâmetro:

 if [[ ${base_name} != ${base_name//\[+([^\]])\]} ]] then mv -v "${dir_name}/${base_name}" "${dir_name}/${base_name//\[+([^\]])\]}" && base_name="${base_name//\[+([^\]])\]}" fi if [[ ${base_name} != ${base_name//www.*.com - /} ]] then mv -v "${dir_name}/${base_name}" "${dir_name}/${base_name//www.*.com - /}" && base_name="${base_name//www.*.com - /}" fi # more of these type of statements; one for each type of frequently-encountered pattern 

e depois com echo / sed:

 tmp=`echo "${base_name}" | sed -e 's/\[[^][]*\]//g' | sed -e 's/\s-\s//g'` mv "${base_name}" "{tmp}" 

Eu sinto que a expansão de parâmetro é o pior dos dois, mas eu gosto porque eu sou capaz de manter a mesma variável atribuída ao arquivo para processamento posterior após a renomeação (o código acima é usado em um script que é chamado para cada arquivo após o download do arquivo estar completo).

De qualquer forma eu estava esperando que houvesse uma maneira melhor / mais limpa de fazer o acima que alguém mais experiente do que eu poderia me mostrar, de preferência de uma maneira que me permitisse reatribuir facilmente a variável antiga / original ao arquivo novo / renomeado.

obrigado

Dois resposta: usando renomear perl ou usando pura bash

Como há algumas pessoas que não gostam de perl, eu escrevi minha única versão bash

Renomeando arquivos usando o comando rename .

Introdução

Sim, este é um trabalho típico para o comando rename que foi projetado precisamente para:

 man rename | sed -ne '/example/,/^[^ ]/p' For example, to rename all files matching "*.bak" to strip the extension, you might say rename 's/\.bak$//' *.bak To translate uppercase names to lower, you'd use rename 'y/AZ/az/' * 

Amostras mais orientadas

Basta soltar todos os espaços e colchetes :

 rename 's/[ \[\]]*//g;' *.ext 

Renomeie tudo .jpg pela numeração de 1 :

 rename 's/^.*$/sprintf "IMG_%05d.JPG",++$./e' *.jpg 

Demonstração:

 touch {a..e}.jpg ls -ltr total 0 -rw-r--r-- 1 user user 0 sep 6 16:35 e.jpg -rw-r--r-- 1 user user 0 sep 6 16:35 d.jpg -rw-r--r-- 1 user user 0 sep 6 16:35 c.jpg -rw-r--r-- 1 user user 0 sep 6 16:35 b.jpg -rw-r--r-- 1 user user 0 sep 6 16:35 a.jpg rename 's/^.*$/sprintf "IMG_%05d.JPG",++$./e' *.jpg ls -ltr total 0 -rw-r--r-- 1 user user 0 sep 6 16:35 IMG_00005.JPG -rw-r--r-- 1 user user 0 sep 6 16:35 IMG_00004.JPG -rw-r--r-- 1 user user 0 sep 6 16:35 IMG_00003.JPG -rw-r--r-- 1 user user 0 sep 6 16:35 IMG_00002.JPG -rw-r--r-- 1 user user 0 sep 6 16:35 IMG_00001.JPG 

Sintaxe completa para corresponder a pergunta SO, de forma segura

Há uma maneira forte e segura usando o utilitário de rename :

Como esta é a ferramenta comum perl , temos que usar a syntax perl:

 rename 'my $o=$_; s/[ \[\]]+/-/g; s/-+/-/g; s/^-//g; s/-\(\..*\|\)$/$1/g; s/(.*[^\d])(|-(\d+))(\.[a-z0-9]{2,6})$/ my $i=$3; $i=0 unless $i; sprintf("%s-%d%s", $1, $i+1, $4) /eg while $o ne $_ && -f $_; ' * 

Regra de teste:

 touch '[ www.crap.com ] file.name.ext' 'www.crap.com - file.name.ext' ls -1 [ www.crap.com ] file.name.ext www.crap.com - file.name.ext rename 'my $o=$_; ... ... ...' * ls -1 www.crap.com-file.name-1.ext www.crap.com-file.name.ext touch '[ www.crap.com ] file.name.ext' 'www.crap.com - file.name.ext' ls -1 www.crap.com-file.name-1.ext [ www.crap.com ] file.name.ext www.crap.com - file.name.ext www.crap.com-file.name.ext rename 'my $o=$_; ... ... ...' * ls -1 www.crap.com-file.name-1.ext www.crap.com-file.name-2.ext www.crap.com-file.name-3.ext www.crap.com-file.name.ext 

… e assim por diante…

… e é seguro enquanto você não usa -f flag para rename comando: o arquivo não será sobrescrito e você receberá uma mensagem de erro se algo der errado.

Renomeando arquivos usando bash e assim chamados bashisms :

Eu prefiro fazer isso usando o utilitário dedicado, mas isso pode até ser feito usando pura bash (aka sem qualquer fork)

Não há uso de nenhum outro binário além do bash (não sed , awk , tr ou outro):

 #!/bin/bash for file;do newname=${file//[ \]\[]/.} while [ "$newname" != "${newname#.}" ] ;do newname=${newname#.} done while [ "$newname" != "${newname//[.-][.-]/.}" ] ;do newname=${newname//[.-][.-]/-};done if [ "$file" != "$newname" ] ;then if [ -f $newname ] ;then ext=${newname##*.} basename=${newname%.$ext} partname=${basename%%-[0-9]} count=${basename#${partname}-} [ "$partname" = "$count" ] && count=0 while printf -v newname "%s-%d.%s" $partname $[++count] $ext && [ -f "$newname" ] ;do :;done fi mv "$file" $newname fi done 

Para ser executado com arquivos como argumento, por exemplo:

 /path/to/my/script.sh \[* 
  • Substituindo espaços e colchetes por ponto
  • Substituindo seqüências de .- , -. , ou .. por apenas um - .
  • Teste se o nome do arquivo não for diferente, não há nada a fazer.
  • Teste se existe um arquivo com o novo nome …
  • dividir nome do arquivo, contador e extensão, para criação de novo nome indexado
  • loop se um arquivo existir com newname
  • Finalmente, renomeie o arquivo.

Aproveite o seguinte padrão clássico:

  job_select /path/to/directory| job_strategy | job_process 

onde job_select é responsável por selecionar os objects de seu trabalho, job_strategy prepara um plano de processamento para esses objects e job_process eventualmente executa o plano.

Isso pressupõe que os nomes dos arquivos não contenham uma barra vertical | nem um caractere de nova linha.

A function job_select

  # job_select PATH # Produce the list of files to process job_select() { find "$1" -name 'www.*.com - *' -o -name '[*] - *' } 

O comando find pode examinar todas as propriedades do arquivo mantido pelo sistema de arquivos, como tempo de criação, tempo de access, hora da modificação. Também é possível controlar como o sistema de arquivos é explorado, informando para não descer em filesystems montados, quanto níveis de recursion são permitidos. É comum acrescentar canais ao comando find para executar seleções mais complicadas com base no nome do arquivo.

Evite a armadilha comum de include o conteúdo de diretórios ocultos na saída da function job_select . Por exemplo, os diretórios CVS , .svn , .svk e .svk são usados ​​pelas ferramentas de gerenciamento de controle de origem correspondentes e é quase sempre errado include seu conteúdo na saída da function job_select . Por inadvertidamente o processamento em lote desses arquivos, pode-se facilmente inutilizar a cópia de trabalho afetada.

A function job_strategy

 # job_strategy # Prepare a plan for renaming files job_strategy() { sed -e ' h s@/www\..*\.com - *@/@ s@/\[^]]* - *@/@ x G s/\n/|/ ' } 

Este comando lê a saída do job_select e faz um plano para o nosso trabalho de renomeação. O plano é representado por linhas de texto com dois campos separados pelo caractere | , o primeiro campo sendo o nome antigo do arquivo e o segundo sendo o novo arquivo computado do arquivo, parece

 [ www.crap.com ] file.name.1.ext|file.name.1.ext www.crap.com - file.name.2.ext|file.name.2.ext 

O programa específico usado para produzir o plano é essencialmente irrelevante, mas é comum usar sed como no exemplo; awk ou perl para isso. Vamos percorrer o sed -script usado aqui:

 h Replace the contents of the hold space with the contents of the pattern space. … Edit the contents of the pattern space. x Swap the contents of the pattern and hold spaces. G Append a newline character followed by the contents of the hold space to the pattern space. s/\n/|/ Replace the newline character in the pattern space by a vertical bar. 

Pode ser mais fácil usar vários filtros para preparar o plano. Outro caso comum é o uso do comando stat para adicionar tempos de criação aos nomes dos arquivos.

A function job_process

 # job_process # Rename files according to a plan job_process() { local oldname local newname while IFS='|' read oldname newname; do mv "$oldname" "$newname" done } 

O separador de campo de input IFS é ajustado para permitir que a function leia a saída de job_strategy . Declarar oldname e newname como local é útil em programas grandes, mas pode ser omitido em scripts muito simples. A function job_process pode ser ajustada para evitar sobrescrever arquivos existentes e relatar os itens problemáticos.

Sobre estruturas de dados em programas shell Observe o uso de pipes para transferir dados de um estágio para o outro: os aprendizes geralmente dependem de variables ​​para representar tais informações, mas isso acaba sendo uma escolha desajeitada. Em vez disso, é preferível representar dados como arquivos tabulares ou como streams de dados tabulares que se deslocam de um processo para outro. Dessa forma, os dados podem ser facilmente processados ​​por ferramentas poderosas como sed , awk , join , paste e sort – apenas para citar os mais comuns.

Se você estiver usando Ubunntu / Debian, use o comando rename para renomear múltiplos arquivos no momento.

Se você quiser usar algo que não depende do perl, você pode usar o seguinte código (vamos chamá-lo sanitizeNames.sh ). Está mostrando apenas alguns casos, mas é facilmente extensível usando a substituição de strings, tr (e sed também).

  #!/bin/bash ls $1 |while read f; do newfname=$(echo "$f" \ |tr -d '\[ ' \ # Removing opened square bracket |tr ' \]' '-' \ # Translating closing square bracket to dash |tr -s '-' \ # Squeezing multiple dashes |tr -s '.' \ # Squeezing multiple dots ) newfname=${newfname//-./.} if [ -f "$newfname" ]; then # Some string magic... extension=${newfname##*\.} basename=${newfname%\.*} basename=${basename%\-[1-9]*} lastNum=$[ $(ls $basename*|wc -l) ] mv "$f" "$basename-$lastNum.$extension" else mv "$f" "$newfname" fi done 

E use:

  $ touch '[ www.crap.com ] file.name.ext' 'www.crap.com - file.name.ext' '[ www.crap.com ] - file.name.ext' '[www.crap.com ].file.anothername.ext2' '[www.crap.com ].file.name.ext' $ ls -1 *crap* [ www.crap.com ] - file.name.ext [ www.crap.com ] file.name.ext [www.crap.com ].file.anothername.ext2 [www.crap.com ].file.name.ext www.crap.com - file.name.ext $ ./sanitizeNames.sh *crap* $ ls -1 *crap* www.crap.com-file.anothername.ext2 www.crap.com-file.name-1.ext www.crap.com-file.name-2.ext www.crap.com-file.name-3.ext www.crap.com-file.name.ext 

Você pode usar o rnm

 rnm -rs '/\[crap\]|\[spam\]//g' *.ext 

O acima irá remover [crap] ou [spam] do nome do arquivo.

Você pode passar vários padrões de regex encerrando-os com ; ou sobrecarregando a opção -rs .

 rnm -rs '/[\[\]]//g;/\s*\[crap\]//g' -rs '/crap2//' *.ext 

O formato geral desta string de substituição é /search_part/replace_part/modifier

  1. search_part : regex para procurar.
  2. replace_part : string para replace por
  3. modificador : i (insensitivo a maiúsculas), g (replace globalmente)

minúsculas letras maiúsculas:

Uma string de substituição do form /search_part/\c/modifier fará com que a parte selecionada do nome do arquivo (pelo regex search_part ) em minúsculas enquanto \C (capital \ C) na parte de substituição torne-a maiúscula.

 rnm -rs '/[abcd]/\C/g' *.ext ## this will capitalize all a,b,c,d in the filenames 


Se você tiver muitos padrões de regex que precisam ser tratados, coloque esses padrões em um arquivo e passe o arquivo com a opção -rs/f .

 rnm -rs/f /path/to/regex/pattern/file *.ext 

Você pode encontrar alguns outros exemplos aqui .

Nota:

  1. rnm usa o regex PCRE2 (PCRE revisado).
  2. Você pode desfazer uma operação de renomeação indesejada executando rnm -u

PS: Eu sou o autor dessa ferramenta.