Renomeia muitos arquivos com todos os tipos de caracteres, com portabilidade POSIX

2

Às vezes, preciso renomear todos os arquivos (a convenção de renomeação segue depois) em um diretório onde o nome do arquivo está sempre na forma de 'nome_do_arquivo.extensão' (a extensão sempre existe e varia). O nome pode conter espaços em branco e caracteres da classe [: graph:]. Meu primeiro problema é que ele deve ser absolutamente portátil entre os sistemas * NIX (especialmente Linux, BSD, outros sistemas posteriores, como o AIX). Meu segundo problema é com a classe [: graph:]. Nomes de arquivo podem ser:

cat.txt
dog_and_cat.txt
Where is the cat?.png
my.cat.is.cute.txt.js.html
;;; ;;; ;;;.......321
áéúő _[a lot of whitespaces]_ óü^^^^^ö.jpg

Fácil de ver, são difíceis de manipular e colocar em um loop for. Por exemplo, o

for i in *; do something; done

nem sempre gosta dos espaços em branco e dos caracteres estranhos, especialmente em sistemas operacionais diferentes.

A convenção de renomeação é renomear todos os arquivos para o formulário $FOOBAR.$EXTENSION , em que $FOOBAR é algum tipo de hash, por exemplo, md5sum. Então, no loop for eu tenho uma linha que é como

mv $FILE $(md5sum $FILE | sed 's/\ \ .\+//');

Ele moverá o arquivo para o md5sum de si mesmo, mas a extensão desaparecerá. Eu quero preservar as extensões, que quase sempre estão no formulário .[a-zA-Z0-9]{1,3} . Ocasionalmente, existem extensões como .tar.gz , que também precisam ser preservadas (certamente eu poderia adicioná-las em uma variável, digamos, MYEXTENSIONS='tar.gz tar.bz2 foo.bar' ).

Minha intuição me diz que o problema é solucionável com comandos UNIX / shell padrão bem parametrizados, mas é extremamente difícil para mim agora. Tenho certeza que aprenderei muito com as respostas. Eu sei que eu disse a palavra mágica portabilidade , mas a solução é preferida no bash, se eu precisar especificar a linguagem.

    
por vakufo 08.03.2012 / 11:51

2 respostas

6

Na verdade, for i in *; do something; done trata cada nome de arquivo corretamente, exceto que os nomes de arquivos que começam com . são excluídos da correspondência de curinga. Para fazer a correspondência de todos os arquivos (exceto . e .. ) de forma portável, combine * .[!.]* ..?* e pule qualquer arquivo inexistente resultante de um padrão não correspondente sendo deixado intacto.

Se você teve problemas, provavelmente é porque você não citou $i corretamente depois. Sempre coloque aspas duplas em torno de substituições de variáveis e substituições de comandos: "$foo" , "$(cmd)" , a menos que você pretenda que a divisão de campos e globbing ocorram.

Se você precisar passar o nome do arquivo para um comando externo (você não o faz aqui), tome cuidado para que echo "$foo" nem sempre imprima $foo literalmente. Alguns shells executam expansão de barra invertida, e alguns valores de $foo começando com - serão tratados como uma opção. A maneira segura e compatível com POSIX de imprimir uma string exatamente é

printf '%s' "$foo"

ou printf '%s\n' "$foo" para adicionar uma nova linha no final. Outra coisa a observar é que a substituição de comandos remove as novas linhas finais; Se você precisar manter novas linhas, um possível truque é acrescentar um caractere não-nova linha aos dados, certifique-se de que a transformação retenha esse caractere e, finalmente, trunque esse caractere. Por exemplo:

mangled_file_name="$(printf '%sa' "$file_name" | tr -sc '[:alnum:]-+_.' '[_*]')"
mangled_file_name="${mangled_file_name%a}"

Para extrair o md5sum do arquivo, evite ter o nome do arquivo na saída md5sum , pois isso dificultará a remoção. Passe os dados na entrada padrão de md5sum .

Observe que o comando md5sum não está no POSIX. Algumas variantes unix têm md5 ou nada. cksum é POSIX, mas sujeito a colisão.

Veja Agarrando a extensão em um nome de arquivo sobre como obter a extensão do arquivo.

Vamos colocar tudo junto (não testado). Tudo aqui funciona em qualquer shell POSIX; você pode ganhar um pouco, mas não muito, com os recursos do bash.

for old_name in * .[!.]* ..?*; do
  if ! [ -e "$old_name" ]; then continue; fi
  hash=$(md5sum <"$old_name")
  case "$old_name" in
    *.*.gz|*.*.bz2)                   # double extension
      ext=".${old_name##*.}"
      tmp="${old_name%.*}"
      ext=".${old_name##*.}$ext";;
    ?*.*) ext=".${old_name##*.}";;    # simple extension
    *) ext=;;                         # no extension
  esac
  mv -- "$old_name" "$hash$ext"
done

Observe que não considerei o caso em que já existe um arquivo de destino pelo nome especificado. Em particular, se você tiver arquivos cujo nome se pareça com sua convenção adotada, mas onde a parte da soma de verificação não corresponde ao conteúdo do arquivo e em vez disso a de outro arquivo com a mesma extensão, o que acontecerá dependerá da ordem lexicográfica relativa os nomes dos arquivos.

    
por 08.03.2012 / 20:16
6

Como essa é uma pergunta bastante complicada, forneço apenas algumas diretrizes :

  • Citar duas vezes variáveis de nome de arquivo em todos os lugares. Isso evitará quase todos os problemas de espaço em branco devido à divisão de palavras.
  • As variáveis dentro de $() precisam ser citadas como fora dessa construção. Nenhum escape adicional é necessário.
  • As $() e '' constroem tiras à direita de novas linhas , portanto, você precisa adicionar um caractere diferente e, em seguida, retirá-lo da construção $() :

    varx="$([command which might print a value ending in \n]; echo x)"
    var="${varx%x}"
    
  • -- em comandos é necessário para separar argumentos de nomes de arquivos , pois nomes de arquivos podem começar com -- e, portanto, seriam tratados como parâmetros.
    • find não suporta esta sintaxe, portanto, use readlink para obter um caminho absoluto que, por definição, comece com barra ou certifique-se de que o caminho fornecido para find já seja absoluto ou comece com ./ .
  • Use a substituição de processo com <( em vez de pipes para evitar canais quebrados quando o processo de envio terminar.
  • Use um descritor de arquivo entre 3 e 9 para a passagem de dados em vez da entrada padrão para evitar comandos gananciosos, como cat ou ssh , sugando tudo.
  • Acima de tudo, teste ! Eu costumo usar esse nome de arquivo para testar o material mencionado acima: $'--$'!*@\a\b\E\f\r\t\v\\'"0021 \n'
por 08.03.2012 / 12:52