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! :-)