agosto 04, 2019

Renomeando arquivos em massa com o 'xargs'

O xargs é um dos comandos mais impressionantes e uteis do shell do Linux. Infelizmente, ele também é uma das ferramentas menos conhecidas e utilizadas pela média dos usuários. Talvez, grande parte disso se deva ao seu comportamento quase "mágico". Neste artigo nós veremos como ele funciona e como utilizá-lo para resolver um problema bem cotidiano, que é renomear arquivos em massa.

A função básica do xargs é receber um ou mais argumentos da entrada padrão, construir uma lista e passá-los como parâmetros de execução de outros programas e comandos.

A sintaxe é bem simples:

xargs [comando [opções] [argumentos_iniciais]]

Se nenhum comando for passado, o xargs utilizará por padrão o comando echo.

Mas, a grande vantagem do xargs é que, ao criar listas de argumentos, ele torna processos repetitivos muito mais rápidos, simplesmente porque ele sempre tentará executar o maior grupo possível de comandos simultaneamente de cada vez, ao contrário, por exemplo, dos loops que executam repetidamente o mesmo comando várias vezes, uma depois da outra.

O que nós vamos ver, então, é um exemplo e uma aplicação desse comportamento.

Renomeando arquivos em massa

Com o comando abaixo, eu criei 100 arquivos na minha pasta de testes:

:~/testes$ touch teste-{0..99}.txt

A pergunta é: qual seria a forma mais eficiente de renomear todos esses arquivos de .txt para .bak?

Você já deve ter pesquisado muito no Google sobre como fazer isso. Se teve sorte, é bem provável que tenha encontrado respostas como essa...

:~/na/pasta/dos/arquivos$ for f in *.txt; do mv "$f" "${f%.txt}.bak"; done

Eu disse "sorte" porque este loop tem até um conceito interessante, que é o uso de uma expansão de parâmetros (${f%.txt}) para obter o nome do arquivo sem a extensão, mas isso é assunto para outro momento.

Então, o que esse comando em 3 linhas (cada ; separa uma linha) faria?

Trata-se de um loop, uma repetição do comando mv (usado para mover ou renomear) para cada ocorrência de arquivos terminados em .txt. Como são 100 arquivos, o comando mv será repetido 100 vezes.

Observação: é claro que esse método é correto e funciona, mas a ideia aqui é apresentar outras alternativas mais eficientes e, principalmente, demonstrar como é fácil utilizar o comando xargs.

A versão do 'xargs'

Como o xargs executa comandos em grupos simultâneos, e não um de cada vez, a primeira consequência é que nós podemos nos valer das vantagens de comandos e programas um pouco mais interessantes, como é o caso do comando find. Ele mesmo será assunto para outro artigos, mas nós podemos adiantar que ele é basicamente um comando cheio de recursos para realizar buscas de arquivos e pastas.

Com o comando abaixo, por exemplo, nós podemos listar todos os arquivos na pasta local, indicada pelo ponto (.), que sejam do tipo arquivo (-type f), e cujo nome corresponda à expressão *.txt (-name "*.txt").

find . -type f -name "*.txt"

Ao executá-lo na minha pasta de testes, o resultado seria algo assim:

:~/testes$ find . -type f -name "*.txt"
./teste-88.txt
./teste-18.txt
./teste-80.txt
...
./teste-99.txt
:~/testes$

De cada uma dessas linhas retornadas, o que me interessa é o nome do arquivo sem a extensão, e nós já podemos recorrer à ajuda do xargs para isso.

Através de um pipe (|), que é como um comando passa a sua saída para a entrada de outro comando, nós vamos repassar a lista de arquivos que o find nos entregou como parâmetro de um outro comando, o comando basename, que serve justamente para separar o caminho e a extensão do nome de um arquivo, o que pode ser feito assim...

basename nome_do_arquivo -s ".ext"

Mas, de onde viria o parâmetro nome_do_arquivo?

Exatamente! O find sozinho, através do pipe, não teria como mandar a lista de arquivos diretamente para o basename como parâmetro, e é aí que entra o xargs.

Portanto, o nosso comando de uma linha ficaria assim:

find . -type f -name "*.txt" | xargs basename -s ".txt"

Na minha pasta de testes, o resultado seria esse:

:~/testes$ find . -type f -name "*.txt" | xargs basename -s ".txt"
teste-88
teste-18
teste-80
...
teste-99
:~/testes$ 

Ou seja, nós já temos o que precisamos para renomear os arquivos, faltando apenas executar o comando mv da seguinte forma:

mv nome_antigo nome_novo

O problema, porém, é que o mv vai precisar de dois argumentos, o nome_antigo e o nome_novo, mas a saída do comando anterior só vai entregar um único nome para cada arquivo.

A solução é uma das opções do comando xargs, a opção -I, que substitui certos símbolos da linha de comando pelo que for recebido na entrada do xargs. Por padrão, estes símbolos são um par vazio de chaves ({}), o que nos permite utilizar a versão abreviada dessa mesma opção, que é -i.

Aplicando essa ideia ao comando mv, nós queremos algo assim...

mv {}.txt {}.bak

Onde os "alvos" da string na entrada do xargs são as chaves vazias ({}).

Juntando tudo, este seria o comando para renomear arquivos em massa:

find . -type f -name "*.txt" | xargs basename -s '.txt' | xargs -i mv {}.txt {}.bak

Agora, todos os meus arquivos .txt são arquivos .bak...

:~/testes$ ls -l
total 0
-rw-r--r-- 1 gda gda 0 ago  4 10:40 teste-0.bak
-rw-r--r-- 1 gda gda 0 ago  4 10:40 teste-10.bak
-rw-r--r-- 1 gda gda 0 ago  4 10:40 teste-11.bak
-rw-r--r-- 1 gda gda 0 ago  4 10:40 teste-12.bak
-rw-r--r-- 1 gda gda 0 ago  4 10:40 teste-13.bak
...
-rw-r--r-- 1 gda gda 0 ago  4 10:40 teste-99.bak
-rw-r--r-- 1 gda gda 0 ago  4 10:40 teste-9.bak
:~/testes$

Para que você tenha uma noção comparativa dos tempos dos dois métodos, observe os testes abaixo.

Com 'xargs'...

time find . -type f -name "*.txt" | xargs basename -s ".txt" | xargs -i mv {}.txt {}.bak

real    0m0,091s
user    0m0,077s
sys     0m0,022s

Com o loop...

time { for f in *.bak; do mv "$f" "${f%.txt}.txt"; done }

real    0m0,120s
user    0m0,086s
sys     0m0,043s

Dúvidas? Sugestões? Outras ideias?

Pode comentar!

Nenhum comentário:

Postar um comentário

O sistema de comentários do Blogspot é um lixo e praticamente me obriga a liberar ou moderar todos os comentários. Portanto, eu peço perdão antecipadamente caso o seu comentário demore para aparecer.

Mas não se acanhe por causa disso! :-)