Como substituir espaços nos nomes de arquivo usando um script bash

264

Alguém pode recomendar uma solução segura para substituir recursivamente espaços por sublinhados nos nomes de arquivos e diretórios a partir de um determinado diretório raiz? Por exemplo:

$ tree
.
|-- a dir
|   `-- file with spaces.txt
`-- b dir
    |-- another file with spaces.txt
    `-- yet another file with spaces.pdf

torna-se:

$ tree
.
|-- a_dir
|   `-- file_with_spaces.txt
`-- b_dir
    |-- another_file_with_spaces.txt
    `-- yet_another_file_with_spaces.pdf
armandino
fonte
9
O que você deseja que aconteça se houver um arquivo chamado foo bare outro arquivo foo_barno mesmo diretório?
Mark Byers
Boa pergunta. Eu não gostaria de substituir os arquivos existentes ou perder dados. Deve deixá-lo inalterado .. idealmente, imprimindo um aviso, mas isso provavelmente está pedindo muito.
Armandino

Respostas:

312

Use rename(aka prename), que é um script Perl que já pode estar no seu sistema. Faça isso em duas etapas:

find -name "* *" -type d | rename 's/ /_/g'    # do the directories first
find -name "* *" -type f | rename 's/ /_/g'

Baseado na resposta de Jürgen e capaz de lidar com várias camadas de arquivos e diretórios em um único limite, usando a versão "Revisão 1.5 1998/12/18 16:16:31 rmb1" de /usr/bin/rename(um script Perl):

find /tmp/ -depth -name "* *" -execdir rename 's/ /_/g' "{}" \;
Pausado até novo aviso.
fonte
5
Não há necessidade de duas etapas: use a pesquisa em profundidade: find dir -thth
Jürgen Hötzel
3
Oh, eu acabei de ler a renamepágina de manual (Eu não sabia que a ferramenta) e eu acho que você pode otimizar o seu código, alterando s/ /_/ga y/ /_/;-)
Michael Krelin - hacker de
1
Claro que você não receberá um aumento de desempenho. É mais sobre como usar a ferramenta certa. E toda essa questão é sobre micro-otimizar mais ou menos. Afinal, não é divertido? ;-)
Michael Krelin - hacker de
14
Se você estiver executando isso no OS X, você precisabrew install rename
loeschg
7
Isso não funciona no Centos 7, pois o comando rename é completamente diferente (é um script binário, não perl) e não aceita dados do stdin.
CpnCrunch #
357

Eu uso:

for f in *\ *; do mv "$f" "${f// /_}"; done

Embora não seja recursivo, é bastante rápido e simples. Tenho certeza de que alguém aqui pode atualizá-lo para ser recursivo.

A ${f// /_}peça utiliza o mecanismo de expansão de parâmetros do bash para substituir um padrão dentro de um parâmetro pela string fornecida. A sintaxe relevante é ${parameter/pattern/string}. Consulte: https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html ou http://wiki.bash-hackers.org/syntax/pe .

Naidim
fonte
9
Simples e trabalho em mac. (mac esquentar têm rename, e sua muito difícil de instalar este com bebida ..)
JonnieJS
6
resposta incrível. Eu usei for d in *\ *; do mv "$d" "${d// /}"; donenão sob pontuação.
Yoon Lee
1
Ao contrário da resposta 'find-name', esta funcionou no meu OS X! Obrigado senhor!
Julio Faerman
1
Para referência, isso pode facilmente se tornar recursivo no bash para o uso de shopt -s globstare for f in **/*\ *; do .... A globstaropção é interna ao bash, enquanto o renamecomando é uma ferramenta comum do Linux e não faz parte do bash.
ghoti
2
${f// /_}é uma expansão de variável Bash para pesquisa e substituição . - A fé a variável do forloop para cada arquivo que contém um espaço. - O primeiro //significa "substituir tudo" (não pare na primeira ocorrência). - Então o `/ _` significa" substituir espaço por sublinhado "
Ari
101
find . -depth -name '* *' \
| while IFS= read -r f ; do mv -i "$f" "$(dirname "$f")/$(basename "$f"|tr ' ' _)" ; done

não consegui acertar no começo, porque não pensei em diretórios.

Michael Krelin - hacker
fonte
1
Funciona como um encanto. Obrigado Michael.
Armandino
Dennis, boa captura, facilmente consertado, colocando IFS=''na frente read. Além disso, pelo que posso dizer por outros comentários, o sortpasso pode ser descartado em favor da -depthopção find.
Michael Krelin - hacker de
1
Não funciona se um nome de arquivo contiver um \ (barra invertida). Pode ser corrigido adicionando uma -ropção para ler.
Jfg956
8
Esta deve ser a 50ª vez que visito esta página para copiar e usar sua solução. Obrigado muito . Prefiro sua resposta, pois estou em um Mac e não tenho o renamecomando sugerido por Dennis.
Alex Constantin
@AlexConstantin, não macportstem o rename? Eu nunca me preocupei em descobrir porque acho que a tarefa não justifica a utilidade. E se você não tem macports, você deve considerar instalá-los;)
Michael Krelin - hacker de
30

você pode usar detoxpor Doug Harple

detox -r <folder>
user78274
fonte
12

Uma solução de localização / renomeação . renomear faz parte do util-linux.

É necessário descer primeiro a profundidade, porque um nome de arquivo de espaço em branco pode fazer parte de um diretório de espaço em branco:

find /tmp/ -depth -name "* *" -execdir rename " " "_" "{}" ";"
Jürgen Hötzel
fonte
Não recebo nenhuma alteração quando corro a sua.
Pausado até novo aviso.
Verifique a configuração do util-linux: $ rename --version rename (util-linux-ng 2.17.2)
Jürgen Hötzel
Grepping / usr / bin / rename (um script Perl) revela "Revisão 1.5 1998/12/18 16:16:31 rmb1"
Pausado até novo aviso.
Hmm ... onde está o seu binário util-linux? Esse caminho do arquivo deve pertencer ao util-linux. Você não é um sistema GNU-Linux?
Jürgen Hötzel
1
que muda apenas um espaço na minha corrida, então "vá dizer fogo na montanha" se torna "go_tell fogo na montanha".
brokk e eitri
6

bash 4.0

#!/bin/bash
shopt -s globstar
for file in **/*\ *
do 
    mv "$file" "${file// /_}"       
done
ghostdog74
fonte
Parece que este vai fazer um mv a si mesmo se um nome de arquivo ou diretório não tem espaço nele (mv: não pode mover a' to a subdirectory of itself, a / a ')
Armandino
não importa. basta remover a mensagem de erro redirecionando para /dev/null.
precisa saber é o seguinte
ghostdog, gerar mvcinquenta e cinco mil vezes apenas para renomear quatro arquivos pode ser um pouco sobrecarregado, mesmo se você não inundar o usuário com mensagens.
Michael Krelin - hacker de
krelin, até o find irá percorrer os 55000 arquivos que você mencionou para encontrar aqueles com espaços e depois renomear. No final, ainda está passando por tudo. Se desejar, uma verificação inicial de espaços antes da renomeação o fará.
ghostdog74
Eu estava falando sobre gerar mv, não passar. Não for file in *' '*ou algo assim fazer um trabalho melhor?
Michael Krelin - hacker de
6

você pode usar isso:

    find . -name '* *' | while read fname 

do
        new_fname=`echo $fname | tr " " "_"`

        if [ -e $new_fname ]
        then
                echo "File $new_fname already exists. Not replacing $fname"
        else
                echo "Creating new file $new_fname to replace $fname"
                mv "$fname" $new_fname
        fi
done
Itamar
fonte
3

Versão recursiva das respostas de Naidim.

find . -name "* *" | awk '{ print length, $0 }' | sort -nr -s | cut -d" " -f2- | while read f; do base=$(basename "$f"); newbase="${base// /_}"; mv "$(dirname "$f")/$(basename "$f")" "$(dirname "$f")/$newbase"; done
Junyeop Lee
fonte
2

Aqui está uma solução find -exec (bastante detalhada) que grava avisos "o arquivo já existe" no stderr:

function trspace() {
   declare dir name bname dname newname replace_char
   [ $# -lt 1 -o $# -gt 2 ] && { echo "usage: trspace dir char"; return 1; }
   dir="${1}"
   replace_char="${2:-_}"
   find "${dir}" -xdev -depth -name $'*[ \t\r\n\v\f]*' -exec bash -c '
      for ((i=1; i<=$#; i++)); do
         name="${@:i:1}"
         dname="${name%/*}"
         bname="${name##*/}"
         newname="${dname}/${bname//[[:space:]]/${0}}"
         if [[ -e "${newname}" ]]; then
            echo "Warning: file already exists: ${newname}" 1>&2
         else
            mv "${name}" "${newname}"
         fi
      done
  ' "${replace_char}" '{}' +
}

trspace rootdir _
yabt
fonte
2

Este faz um pouco mais. Eu o uso para renomear meus torrents baixados (sem caracteres especiais (não ASCII), espaços, vários pontos, etc.).

#!/usr/bin/perl

&rena(`find . -type d`);
&rena(`find . -type f`);

sub rena
{
    ($elems)=@_;
    @t=split /\n/,$elems;

    for $e (@t)
    {
    $_=$e;
    # remove ./ of find
    s/^\.\///;
    # non ascii transliterate
    tr [\200-\377][_];
    tr [\000-\40][_];
    # special characters we do not want in paths
    s/[ \-\,\;\?\+\'\"\!\[\]\(\)\@\#]/_/g;
    # multiple dots except for extension
    while (/\..*\./)
    {
        s/\./_/;
    }
    # only one _ consecutive
    s/_+/_/g;
    next if ($_ eq $e ) or ("./$_" eq $e);
    print "$e -> $_\n";
    rename ($e,$_);
    }
}
degi
fonte
1

Eu encontrei em torno deste script, pode ser interessante :)

 IFS=$'\n';for f in `find .`; do file=$(echo $f | tr [:blank:] '_'); [ -e $f ] && [ ! -e $file ] && mv "$f" $file;done;unset IFS
Juan Sebastian Totero
fonte
Falha em arquivos com novas linhas em seus nomes.
ghoti
1

Aqui está uma solução de script bash de tamanho razoável

#!/bin/bash
(
IFS=$'\n'
    for y in $(ls $1)
      do
         mv $1/`echo $y | sed 's/ /\\ /g'` $1/`echo "$y" | sed 's/ /_/g'`
      done
)
jojohtf
fonte
1

Para aqueles que estão enfrentando problemas com o macOS, primeiro instale todas as ferramentas:

 brew install tree findutils rename

Quando for necessário renomear, crie um alias para o GNU find (gfind) como find. Em seguida, execute o código de @Michel Krelin:

alias find=gfind 
find . -depth -name '* *' \
| while IFS= read -r f ; do mv -i "$f" "$(dirname "$f")/$(basename "$f"|tr ' ' _)" ; done   
Mohamed El-Nakib
fonte
0

Isso localiza apenas arquivos dentro do diretório atual e os renomeia . Eu tenho esse alias.

find ./ -name "* *" -type f -d 1 | perl -ple '$file = $_; $file =~ s/\s+/_/g; rename($_, $file);

user1060059
fonte
0

Eu apenas faço um para meu próprio propósito. Você pode usá-lo como referência.

#!/bin/bash
cd /vzwhome/c0cheh1/dev_source/UB_14_8
for file in *
do
    echo $file
    cd "/vzwhome/c0cheh1/dev_source/UB_14_8/$file/Configuration/$file"
    echo "==> `pwd`"
    for subfile in *\ *; do [ -d "$subfile" ] && ( mv "$subfile" "$(echo $subfile | sed -e 's/ /_/g')" ); done
    ls
    cd /vzwhome/c0cheh1/dev_source/UB_14_8
done
Hongtao
fonte
0

Para arquivos na pasta denominada / files

for i in `IFS="";find /files -name *\ *`
do
   echo $i
done > /tmp/list


while read line
do
   mv "$line" `echo $line | sed 's/ /_/g'`
done < /tmp/list

rm /tmp/list
Marcos Jean Sampaio
fonte
0

Minha solução para o problema é um script bash:

#!/bin/bash
directory=$1
cd "$directory"
while [ "$(find ./ -regex '.* .*' | wc -l)" -gt 0 ];
do filename="$(find ./ -regex '.* .*' | head -n 1)"
mv "$filename" "$(echo "$filename" | sed 's|'" "'|_|g')"
done

basta colocar o nome do diretório, no qual você deseja aplicar o script, como argumento depois de executar o script.

Ágoston Volcz
fonte
0

No macOS

Assim como a resposta escolhida.

brew install rename

# 
cd <your dir>
find . -name "* *" -type d | rename 's/ /_/g'    # do the directories first
find . -name "* *" -type f | rename 's/ /_/g'
CK
fonte