Renomear arquivos em lote com Bash

101

Como o Bash pode renomear uma série de pacotes para remover seus números de versão? Tenho brincado com os dois expre %%, sem sucesso.

Exemplos:

Xft2-2.1.13.pkg torna-se Xft2.pkg

jasper-1.900.1.pkg torna-se jasper.pkg

xorg-libXrandr-1.2.3.pkg torna-se xorg-libXrandr.pkg

Jeremy L
fonte
1
Pretendo usar isso regularmente, como um script write-once use-a-lot. Qualquer sistema que eu esteja usando terá bash, então não tenho medo dos bashismos que são muito úteis.
Jeremy L
De forma mais geral, consulte também stackoverflow.com/questions/28725333/…
tripleee

Respostas:

190

Você pode usar o recurso de expansão de parâmetros do bash

for i in ./*.pkg ; do mv "$i" "${i/-[0-9.]*.pkg/.pkg}" ; done

As citações são necessárias para nomes de arquivos com espaços.

richq
fonte
1
Eu também gosto, mas usa recursos específicos do bash ... Eu normalmente não os uso em favor do sh "padrão".
Diego Sevilla,
2
Para coisas interativas descartáveis ​​de uma linha, sempre uso isso em vez do sed-fu. Se fosse um script, sim, evite bashisms.
richq,
Um problema que encontrei com o código: o que acontecerá se os arquivos já tiverem sido renomeados e eu executar novamente? Recebo avisos por tentar mover para um subdiretório dele mesmo.
Jeremy L
1
Para evitar erros de nova execução, você pode usar o mesmo padrão na parte " .pkg", ou seja, "for i em * - [0-9.] .Pkg; do mv $ i $ {i / - [0-9 .] *. pkg / .pkg}; concluído ". Mas os erros são inócuos o suficiente (mover para o mesmo arquivo).
richq
3
Não é uma solução bash, mas se você tiver o perl instalado, ele fornece o prático comando rename para renomeação em lote, simplesmente façarename "s/-[0-9.]*//" *.pkg
Rufus
33

Se todos os arquivos estiverem no mesmo diretório, a sequência

ls | 
sed -n 's/\(.*\)\(-[0-9.]*\.pkg\)/mv "\1\2" "\1.pkg"/p' | 
sh

fará o seu trabalho. O comando sed criará uma sequência de comandos mv , que você pode canalizar para o shell. É melhor primeiro executar o pipeline sem o trailing | shpara verificar se o comando faz o que você deseja.

Para percorrer vários diretórios, use algo como

find . -type f |
sed -n 's/\(.*\)\(-[0-9.]*\.pkg\)/mv "\1\2" "\1.pkg"/p' |
sh

Observe que no sed a sequência de agrupamento da expressão regular são colchetes precedidos por uma barra invertida \(e \), em vez de colchetes simples (e ).

Diomidis Spinellis
fonte
Perdoe minha ingenuidade, mas escapar dos parênteses com barras invertidas não os faria serem tratados como caracteres literais?
devios1
2
No sed, a sequência de agrupamento da expressão regular é (, em vez de um único colchete.
Diomidis Spinellis
não funcionou para mim, ficaria feliz em obter sua ajuda .... O sufixo do arquivo FROM é anexado ao arquivo TO: $ find. -tipo d | sed -n 's /(.*)\/(.*) anysoftkeyboard (. *) / mv "\ 1 \ / \ 2anysoftkeyboard \ 3" "\ 1 \ / \ 2effectedkeyboard \ 3" / p' | sh >> >>>>> SAÍDA >>>> mv: renomear ./base/src/main/java/com/anysoftkeyboard/base/dictionaries para ./base/src/main/java/com/effectedkeyboard/base/dictionaries/dictionaries : Esse arquivo ou diretório não
existe
Parece que está faltando a barra invertida entre os colchetes.
Diomidis Spinellis
1
Não use lsem scripts. A primeira variedade pode ser alcançada com printf '%s\n' * | sed ...e com alguma criatividade, você provavelmente também pode se livrar seddela. O Bash printfoferece um '%q'código de formato que pode ser útil se você tiver nomes de arquivo que contenham metacaracteres de shell literais.
tripleee
10

Vou fazer algo assim:

for file in *.pkg ; do
    mv $file $(echo $file | rev | cut -f2- -d- | rev).pkg
done

supostamente todos os seus arquivos estão no diretório atual. Se não, tente usar find conforme recomendado acima por Javier.

EDIT : Além disso, esta versão não usa nenhum recurso específico do bash, como os outros acima, o que leva você a mais portabilidade.

Diego sevilla
fonte
Eles estão todos no mesmo diretório. O que você acha da resposta de rq?
Jeremy L
Eu gosto de não usar recursos específicos do bash, mas sim um sh mais padrão.
Diego Sevilla,
1
Presumi que fosse para uma renomeação rápida, não para escrever o aplicativo corporativo portátil de plataforma cruzada de próxima geração ;-)
richq
1
Haha, muito justo ... Só de um homem que gosta de portabilidade como eu :)
Diego Sevilla
Este não leva em conta o terceiro exemplo: xorg-libXrandr (hífen usado no nome).
Jeremy L
5

Aqui está um POSIX quase equivalente à resposta atualmente aceita . Isso troca a ${variable/substring/replacement}expansão de parâmetro apenas do Bash por uma que está disponível em qualquer shell compatível com Bourne.

for i in ./*.pkg; do
    mv "$i" "${i%-[0-9.]*.pkg}.pkg"
done

O parâmetro expansion ${variable%pattern}produz o valor de variablecom qualquer sufixo que corresponda patternremovido. (Também é ${variable#pattern}necessário remover um prefixo.)

Eu mantive o subpadrão -[0-9.]*da resposta aceita, embora talvez seja enganoso. Não é uma expressão regular, mas um padrão glob; portanto, não significa "um traço seguido de zero ou mais números ou pontos". Em vez disso, significa "um travessão, seguido por um número ou um ponto, seguido por qualquer coisa". O "qualquer coisa" será a correspondência mais curta possível, não a mais longa. (O Bash oferece ##e %%para cortar o prefixo ou sufixo mais longo possível, em vez do mais curto.)

triplo
fonte
5

Podemos presumir que sedestá disponível em qualquer * nix, mas não podemos ter certeza de que suportará sed -na geração de comandos mv. ( NOTA: Apenas GNU sedfaz isso.)

Mesmo assim, bash builtins e sed, podemos rapidamente criar uma função shell para fazer isso.

sedrename() {
  if [ $# -gt 1 ]; then
    sed_pattern=$1
    shift
    for file in $(ls $@); do
      mv -v "$file" "$(sed $sed_pattern <<< $file)"
    done
  else
    echo "usage: $0 sed_pattern files..."
  fi
}

Uso

sedrename 's|\(.*\)\(-[0-9.]*\.pkg\)|\1\2|' *.pkg

before:

./Xft2-2.1.13.pkg
./jasper-1.900.1.pkg
./xorg-libXrandr-1.2.3.pkg

after:

./Xft2.pkg
./jasper.pkg
./xorg-libXrandr.pkg

Criação de pastas de destino:

Uma vez mvque não cria pastas de destino automaticamente, não podemos usar nossa versão inicial do sedrename.

É uma mudança bastante pequena, então seria bom incluir esse recurso:

Precisaremos de uma função de utilidade abspath(ou caminho absoluto) já que o bash não tem essa função.

abspath () { case "$1" in
               /*)printf "%s\n" "$1";;
               *)printf "%s\n" "$PWD/$1";;
             esac; }

Assim que tivermos isso, podemos gerar a (s) pasta (s) de destino para um padrão sed / rename que inclui uma nova estrutura de pastas.

Isso garantirá que saibamos os nomes de nossas pastas de destino. Quando renomearmos, precisaremos usá-lo no nome do arquivo de destino.

# generate the rename target
target="$(sed $sed_pattern <<< $file)"

# Use absolute path of the rename target to make target folder structure
mkdir -p "$(dirname $(abspath $target))"

# finally move the file to the target name/folders
mv -v "$file" "$target"

Aqui está o script com reconhecimento de pasta completo ...

sedrename() {
  if [ $# -gt 1 ]; then
    sed_pattern=$1
    shift
    for file in $(ls $@); do
      target="$(sed $sed_pattern <<< $file)"
      mkdir -p "$(dirname $(abspath $target))"
      mv -v "$file" "$target"
    done
  else
    echo "usage: $0 sed_pattern files..."
  fi
}

Claro, ele ainda funciona quando não temos pastas de destino específicas também.

Se quisermos colocar todas as músicas em uma pasta, ./Beethoven/podemos fazer o seguinte:

Uso

sedrename 's|Beethoven - |Beethoven/|g' *.mp3

before:

./Beethoven - Fur Elise.mp3
./Beethoven - Moonlight Sonata.mp3
./Beethoven - Ode to Joy.mp3
./Beethoven - Rage Over the Lost Penny.mp3

after:

./Beethoven/Fur Elise.mp3
./Beethoven/Moonlight Sonata.mp3
./Beethoven/Ode to Joy.mp3
./Beethoven/Rage Over the Lost Penny.mp3

Rodada de bônus ...

Usando este script para mover arquivos de pastas para uma única pasta:

Supondo que desejamos reunir todos os arquivos correspondentes e colocá-los na pasta atual, podemos fazer isso:

sedrename 's|.*/||' **/*.mp3

before:

./Beethoven/Fur Elise.mp3
./Beethoven/Moonlight Sonata.mp3
./Beethoven/Ode to Joy.mp3
./Beethoven/Rage Over the Lost Penny.mp3

after:

./Beethoven/ # (now empty)
./Fur Elise.mp3
./Moonlight Sonata.mp3
./Ode to Joy.mp3
./Rage Over the Lost Penny.mp3

Nota sobre padrões de regex sed

Regras regulares de padrão sed se aplicam a este script, esses padrões não são PCRE (Expressões regulares compatíveis com Perl). Você poderia ter estendido a sintaxe de expressão regular do sed, usando sed -rou sed -E dependendo da sua plataforma.

Consulte o POSIX compatível man re_formatpara uma descrição completa dos padrões de expressão regular básica e estendida sed.

ocodo
fonte
4

Acho que renomear é uma ferramenta muito mais simples de usar para esse tipo de coisa. Eu encontrei no Homebrew para OSX

Para o seu exemplo, eu faria:

rename 's/\d*?\.\d*?\.\d*?//' *.pkg

O 's' significa substituto. O formato é s / searchPattern / replacement / files_to_apply. Você precisa usar regex para isso, o que exige um pouco de estudo, mas vale a pena o esforço.

Simon Katan
fonte
Concordo. Para uma coisa ocasional, usar fore sedetc. é bom, mas é muito mais simples e rápido de usar renamese você fizer isso com frequência.
argentum2f
É você escapando de períodos?
Big Money
O problema é que isso não é portátil; não apenas nem todas as plataformas têm um renamecomando; Além disso, alguns terão um diferente rename comando com características diferentes, sintaxe e / ou opções. Isso se parece com o Perl, renameque é bastante popular; mas a resposta deve realmente esclarecer isso.
tripleee
3

melhor uso sedpara isso, algo como:

find . -type f -name "*.pkg" |
 sed -e 's/((.*)-[0-9.]*\.pkg)/\1 \2.pkg/g' |
 while read nameA nameB; do
    mv $nameA $nameB;
 done

descobrir a expressão regular é deixado como um exercício (assim como lidar com nomes de arquivos que incluem espaços)

Javier
fonte
Obrigado: Espaços não são usados ​​nos nomes.
Jeremy L
1

Isso parece funcionar assumindo que

  • tudo termina com $ pkg
  • sua versão # sempre começa com "-"

retire o .pkg, depois retire - ..

for x in $(ls); do echo $x $(echo $x | sed 's/\.pkg//g' | sed 's/-.*//g').pkg; done
Steve B.
fonte
Existem alguns pacotes que possuem um hífen no nome (veja o terceiro exemplo). Isso afeta seu código?
Jeremy L
1
Você pode executar várias expressões sed usando -e. Ex sed -e 's/\.pkg//g' -e 's/-.*//g'.
tacot Terça
Não analise a lssaída e cite suas variáveis. Consulte também shellcheck.net para diagnosticar problemas comuns e antipadrões em scripts de shell.
tripleee
1

Eu tinha vários *.txtarquivos para serem renomeados como .sqlna mesma pasta. abaixo funcionou para mim:

for i in \`ls *.txt | awk -F "." '{print $1}'\` ;do mv $i.txt $i.sql; done
Kumar Mashalkar
fonte
2
Você poderia melhorar sua resposta abstraindo-a do caso de uso real do OP.
Dakab
Isso tem vários problemas, bem como um erro aparente de sintaxe. Consulte shellcheck.net para começar.
tripleee
0

Obrigado por essas respostas. Eu também tive algum tipo de problema. Movendo arquivos .nzb.queued para arquivos .nzb. Tinha espaços e outras falhas nos nomes dos arquivos e isso resolveu meu problema:

find . -type f -name "*.nzb.queued" |
sed -ne "s/^\(\(.*\).nzb.queued\)$/mv -v \"\1\" \"\2.nzb\"/p" |
sh

É baseado na resposta de Diomidis Spinellis.

O regex cria um grupo para todo o nome do arquivo e um grupo para a parte anterior a .nzb.queued e, a seguir, cria um comando de movimentação do shell. Com as cordas entre aspas. Isso também evita a criação de um loop no script de shell porque isso já é feito pelo sed.

Jerry Jacobs
fonte
Deve-se notar que isso só é verdade para GNU sed. Em BSDs, o sed não interpretará a -nopção da mesma maneira.
ocodo