Criação de arquivos vazios no shell

Criação de arquivos vazios no shell

A partir deste artigo, eu pretendo falar regularmente sobre coisas que dizem que você faz (ou aprendeu) errado, mas acabam explicando de um jeito mais errado ainda! Antes de mais nada, acredite, existem sim explicações certas e erradas -- e isso faz toda diferença na construção dos saberes de qualquer área. Para abrir a série, vamos falar sobre a criação de arquivos vazios no shell do GNU/Linux.

Como criar arquivos no shell?

A rigor, com qualquer editor de textos ou programa que produza uma saída de dados:

nano ARQUIVO
vim ARQUIVO
cat ARQUIVOS > ARQUIVO
grep FILTRO UM_ARQUIVO >> ARQUIVO
echo ARGUMENTOS > ARQUIVO
...

Todos esses procedimentos (e muitos outros) implicam na criação de arquivos. Então, qual é o ponto de alguém se dar ao trabalho de dizer que nós aprendemos a criar arquivos do jeito errado? No mínimo, falta um contexto.

Contexto

Para começar, quando surge esse tipo de "dica", ela geralmente se refere à criação de arquivos vazios (seja lá qual for a utilidade de criar um arquivo vazio) através de comandos no shell. Mais especificamente, são "dicas" que tentam nos mostrar o quanto é errado utilizar o programa touch para esta finalidade.

A propósito, a popularidade do touch é uma das consequências de outro problema: as famosas tabelinhas de comandos "do Linux" (sic), porque é lá que a maioria dos iniciantes vai procurar ajuda sobre como realizar as operações mais básicas em seu sistema operacional através de comandos no terminal -- mas isso é uma outra história.

Qual é o problema do 'touch'?

Nenhum. O touch é uma ferramenta desenvolvida para o sistema operacional Unix, também implementada em sistemas unix like, com a finalidade de alterar as informações de data e hora de acesso e modificação de um ou mais arquivos:

touch [OPÇÕES] ARQUIVOS

Como o 'touch' funciona

Quando o arquivo existe, suas informações de acesso e modificação serão aletradas conforme os argumentos descritos em OPÇÕES ou, se nenhuma opção for utilizada, as informações serão definidas para a data e a hora correntes.

Porém, o que nos interessa é que, se os ARQUIVOS não existirem nos caminhos informados, eles serão criados, a menos que o comando seja executado com a opção -c (do not create).

Por que o 'touch' é implementado para criar arquivos quando eles não existem?

Essa característica nos remete à própria filosofia de projeto do sistema operacional Unix, em especial, dois aspectos: o projeto dos sistemas de interfaces e as limitações de hardware no fim dos anos 1960.

Resumindo o que nos interessa no momento, do mesmo modo que o gerenciamento da execução de programas está associada aos processos, cada arquivo está associado a uma tabela de informações que contém tudo sobre ele.

Observe o que podemos obter do arquivo que acabamos de criar:

:~/tmp $ stat banana
    Arquivo: banana
    Tamanho: 0         	Blocos: 0          bloco de E/S: 4096   arquivo comum vazio
Dispositivo: 803h/2051d	Inode: 7602178     Links: 1
     Acesso: (0644/-rw-r--r--)  Uid: ( 1000/    blau)   Gid: ( 1000/    blau)
     Acesso: 2022-04-01 12:08:24.857573174 -0300
Modificação: 2022-04-01 12:08:24.857573174 -0300
  Alteração: 2022-04-01 12:08:24.857573174 -0300
    Criação: 2022-04-01 12:04:55.827121815 -0300

O utilitário stat, do GNU coreutils, exibe o estado de um arquivo ou de um sistema de arquivos.

Para manipular cada uma das informações que podem ser alteradas (como nome, data e hora de acesso, propriedade e permissões, por exemplo), existe uma ferramenta do sistema operacional. O mais interessante, entretanto, é que essas informações podem ser utilizadas como fontes de dados para diversas finalidades. No caso da data e hora de acesso (ou da última modificação), as possibilidades de uso são muitas.

Por exemplo, sem carregar o arquivo na memória (limitadíssima nos anos 1960, quando o sistema foi desenvolvido), é possível saber quando algo foi alterado e, assim, automatizar alguma tarefa. Com o touch, nós podemos, inclusive, manipular a data e a hora de acesso e modificação de arquivos vazios e utilizar essa informação como referência em scripts.

O custo de acesso a dados em mídias não voláteis, como discos, é bem maior do que o custo de acesso a dados na memória RAM, mas o espaço disponível para dados na memória, ainda mais na época do desenvolvimento do Unix, é bem menor do que o espaço disponível em mídias não voláteis.

Sendo assim, se um script é criado de modo a contar com a verificação de datas e horas de acesso e modificação de arquivos, nada faz mais sentido do que, ao tentar alterar essas informações em arquivos inexistentes, criá-los para que verificações e modificações subsequentes possam ser feitas.

Resumindo o 'touch'

  • É implementado como um binário executável (não é builtin do shell).
  • Manipula data e hora de acesso e modificação de arquivos.
  • Por motivos históricos e funcionais, cria arquivos se não existirem.
  • A criação de arquivos pode ser inibida com a opção -c.

Criando arquivos vazios com recursos do shell

Como vimos, lá no começo, o ponto dessa discussão não é "como criar arquivos", mas como criar arquivos vazios. Agora, nós podemos até especificar melhor o contexto: como criar arquivos vazios utilizando apenas recursos do shell.

Dada a contextualização, sim, nós podemos dizer que o jeito certo de criar arquivos vazios utilizando apenas recursos do shell é através dos redirecionamentos de escrita: fora deste contexto, não existe apenas uma resposta certa.

Redirecionamentos

Por regra, todo processo recebe acesso a três dispositivos padrão de entrada e saída de dados conectados ao terminal:

  • stdin - entrada de dados padrão (leitura do teclado).
  • stdout - saída de dados padrão (exibição no terminal).
  • stderr - saída de erros padrão (exibição no terminal).

Os acessos a esses dispositivos são individualizados por processo através de uma ligação simbólica em /proc/PID/fd (onde PID é o número de identificação do processo). Neste diretório, os dispositivos padrão são chamados de "descritores de arquivos" e seus nomes são os números 0, 1 e 2:

  • stdin - descritor de arquivos 0
  • stdout - descritor de arquivos 1
  • stderr - descritor de arquivos 2

Deste modo, todo comando no shell que envolva a leitura de dados vindos do teclado utilizará o descritor de arquivos 0, enquanto todo comando que exibir algum tipo de saída, fará isso escrevendo no descritor de arquivos 1.

O descritor de arquivos 2 (stderr) também exibe suas mensagens no terminal devido a uma conexão interna no terminal, mas o fluxo de dados em si corre por uma via própria.

O redirecionamento, então, surge no shell como um mecanismo que possibilita a transferência do papel, originalmente atribuído aos dispositivos padrão de entrada e saída de dados, para arquivos. Assim, os dados que seriam lidos por um comando através da digitação de algo no teclado, poderão ser lidos diretamente do conteúdo de um arquivo. Do mesmo modo, dados que seriam exibidos no terminal, serão desviados para a escrita em arquivos.

Para implementar essa possibilidade, o shell nos oferece os operadores de redirecionamento:

Operador Função Descrição
< Leitura Redireciona o conteúdo de um arquivo para a entrada padrão de um processo.
> Escrita Redireciona a saída de um processo para a escrita em um arquivo. Se o arquivo de destino não existir, ele será criado; se existir, seu conteúdo será truncado (zerado) antes da escrita ser processada.
>> Escrita Redireciona a saída de um processo para a escrita no fim de um arquivo. Se o arquivo de destino não existir, ele será criado; se existir, seu conteúdo é mantido e os novos dados são anexados.

Como podemos ver, ambos os redirecionamentos de escrita resultam na criação do arquivo especificado caso ele não exista:

# O arquivo não existe...
:~/tmp $ ls arquivo
ls: não foi possível acessar 'arquivo': Arquivo ou diretório inexistente

# Redirecionando a saída do comando 'echo' para 'arquivo'...
:~/tmp $ echo teste > arquivo

# Verificando se 'arquivo' existe...
:~/tmp $ ls arquivo
arquivo

# Exibindo o conteúdo de arquivo...
:~/tmp $ cat arquivo
teste

Atenção para o tipo de escrita!

É preciso prestar muita atenção ao comportamento dos dos operadores de escrita: enquanto > trunca o conteúdo do arquivo de destino antes do comando ser executado, o operador >> apenas cria uma ligação com o arquivo sem alterá-lo:

# Conteúdo de 'arquivo' será truncado...
:~/tmp $ cat arquivo
teste
:~/tmp $ echo laranja > arquivo
:~/tmp $ cat arquivo
laranja

# Conteúdo de 'arquivo' será mantido...
:~/tmp $ echo abacate >> arquivo
:~/tmp $ cat arquivo
laranja
abacate

Por que os redirecionamentos de escrita criam arquivos?

Resposta rápida: porque este é o papel deles! A própria razão de ser dos redirecionamentos de escrita é destinar fluxos de dados a arquivos, logo, eles terão que existir ou terão que ser criados.

A ligação entre a saída de um processo e um arquivo de destino é feita no momento em que nós teclamos Enter, muito antes da linha de comando ser processada pelo shell e efetivamente executada. Nós podemos comprovar isso com o exemplo abaixo:

# O arquivo 'destino' não existe...
:~/tmp $ ls
arquivo

# Vamos mandar a saída do 'ls' para o arquivo 'destino'...
:~/tmp $ ls > destino

# Agora o arquivo 'destino' existe...
:~/tmp $ ls
arquivo  destino

# Mas, qual será seu conteúdo?
:~/tmp $ cat destino
arquivo
destino <-- Foi criado *antes* do 'ls' ser executado!

Mas nós queremos criar arquivos vazios!

Sem problemas! Num primeiro momento, o que importa para o shell é criar a ligação entre a saída do processo e um arquivo de destino: se haverá ou não um fluxo de dados a ser escrito, isso caberá ao comando resolver quando for executado.

Na prática, isso quer dizer que podemos utilizar qualquer comando que não produza saída via descritor de arquivos 1 (stdout) para que o arquivo criado termine vazio:

:~/tmp $ grep banana /etc/passwd > teste1
:~/tmp $ cat teste1
:~/tmp $

:~/tmp $ true > teste2
:~/tmp $ cat teste2
:~/tmp $

:~/tmp $ : > teste3
:~/tmp $ cat teste3
:~/tmp $

:~/tmp $ ls abacaxi > teste4
ls: não foi possível acessar 'abacaxi': Arquivo ou diretório inexistente
:~/tmp $ cat teste4
:~/tmp $

# Conferindo se os arquivos foram mesmo criados

:~/tmp $ ls
arquivo  destino  teste1  teste2  teste3  teste4

No exemplo do ls, a mensagem exibida veio pela saída padrão de erros (stderr).

Nos shells compatíveis com o Bourne Shell (shell sh), nós podemos até dispensar a execução de um comando, pois o processo do shell em execução também pode destinar os fluxos de dados no seu descritor de arquivos 1 (stdout) para um arquivo:

:~/tmp $ > teste5
:~/tmp $ cat teste5
:~/tmp $

A cegueira do palco

Quando assuntos relevantes para a sua compreensão do shell são reduzidos ao espetáculo, aos truques de mágica, não é nem um pouco raro o show man se esquecer de pequenos detalhes que podem causar grandes desastres.

Na ânsia de mostrar que > ARQUIVO é o jeito certo de criar arquivos no shell, eles se esquecem de avisar que isso vai destruir o conteúdo de arquivos existentes, o que nos leva a ampliar o nosso contexto para: como criar arquivos vazios utilizando apenas recursos do shell e de forma segura.

A resposta segura

Nós vimos que os redirecionamentos de escrita podem ser feitos com dois operadores: > e >>, sendo que apenas o último (>>) preserva o conteúdo do arquivo quando a ligação entre ele e o processo é feita pelo shell. Portanto, a forma segura (não "mais segura") de criar arquivos vazios só com recursos do shell é:

>> ARQUIVO

Aliás, a não ser a título de alerta, a forma > ARQUIVO jamais deveria ser mostrada nesses espetáculos de mágica! Não existe nenhuma situação que justifique criar arquivos vazios com o operador >. Se for para garantir que o conteúdo de um arquivo possivelmente existente seja "zerado", tudo bem, mas isso seria no contexto específico de uma decisão a ser tomada em um script, jamais como regra.

O que usar: 'touch' ou '>>'?

Como sempre, quando falamos de shell, tudo é um grande "depende". A ideia é justamente conhecermos bem como as coisas funcionam para tomarmos decisões conscientes caso a caso.

Para criar arquivos vazios, a verdade é que tanto faz: a diferença é que uma técnica envolve a invocação de um binário executável e a outra nem comandos internos utiliza. Obviamente, isso tem impactos diferentes quando falamos de quantidades absurdas de arquivos a serem criados, mas eu garanto que não é nada considerável (e o touch pode até surpreender):

# O redirecionamento terá que ser repetido tantas vezes
# quantos forem os arquivos a serem criados.

# Criando 1000 arquivos em loop...
:~/tmp $ time { for f in {1..1000}; do >> redir$f; done; }

real	0m0,038s
user	0m0,012s
sys	0m0,026s

# O 'touch' criará todos os arquivos cujos nomes forem
# expandidos como argumentos na linha do comando.

# Expandindo 1000 nomes em um comando só...
:~/tmp/touch $ time touch touch{1..1000}

real	0m0,023s <--- mais rápido!
user	0m0,003s
sys	0m0,021s

Repare que, mesmo sendo mais rápido, a diferença entre o touch e o redirecionamento não passou de 15 milésimos de segundo: quem se importa com 15ms de diferença na criação de mil arquivos vazios?!

Preservação de atributos

Vamos criar dois arquivos (pode ser com o touch) e observar suas datas e horas:

:~/tmp $ touch teste1 teste2
:~/tmp $ ls -l
total 0
-rw-r--r-- 1 blau blau 0 abr  1 15:36 teste1
-rw-r--r-- 1 blau blau 0 abr  1 15:36 teste2

Digamos, agora, que queremos criar dois arquivos, um pelo touch e outro pelo redirecionamento. Só que, de propósito, nós vamos utilizar os mesmos nomes:

:~/tmp $ touch teste1
:~/tmp $ >> teste2

Vamos checar as datas e horas?

:~/tmp/touch $ ls -l
total 0
-rw-r--r-- 1 blau blau 0 abr  1 15:40 teste1 <-- mudou
-rw-r--r-- 1 blau blau 0 abr  1 15:36 teste2 <-- manteve

Como esperado, o touch não afetou o conteúdo do arquivo teste1, mas alterou seus atributos de data e hora. Por outro lado, o redirecionamento com >> não causou qualquer efeito no arquivo teste2, o que podemos confirmar com o utilitário stat:

:~/tmp $ stat teste1 | tail -4
     Acesso: 2022-04-01 15:40:03.790680830 -0300
Modificação: 2022-04-01 15:40:03.790680830 -0300
  Alteração: 2022-04-01 15:40:03.790680830 -0300
    Criação: 2022-04-01 15:36:30.399975476 -0300

:~/tmp $ stat teste2 | tail -4
     Acesso: 2022-04-01 15:36:36.228049201 -0300
Modificação: 2022-04-01 15:36:36.228049201 -0300
  Alteração: 2022-04-01 15:36:36.228049201 -0300
    Criação: 2022-04-01 15:36:30.399975476 -0300

Então, se a ideia for manter os atributos de data e hora do arquivo, o redirecionamento é o caminho. Caso contrário, tanto faz um ou outro.

Conclusão: saber sempre mais, não menos

Se você esperava uma conclusão sobre "qual é o jeito certo", você não leu o artigo, para dizer o mínimo. 😉

Tão importante quanto os conceitos, nós temos que entender algo de uma vez por todas: qualquer um que se disponha a gravar um vídeo ou escrever um artigo para ditar regras e enfiar um jeito certo seja lá do que for por nossas goelas abaixo, pode acreditar, não tem o menor interesse de compartilhar nada conosco e não está nem aí para o nosso aprendizado. A ideia é só oferecer um espetáculo capaz de prender a nossa atenção, ganhar nossos cliques, vender seus produtos e construir uma imagem de autoridade.

Ainda que, às vezes, o reducionismo do espetáculo seja divertido, nós precisamos conhecer as coisas como elas são, ou jamais seremos capazes de tomar decisões sobre o que fazer diante de problemas reais nos nossos scripts ou na linha de comandos do shell. Eu repito o que tenho dito: as regras e os supostos "jeitos certos" não servem para que as pessoas saibam mais -- é justamente o contrário! As regras nos limitam, nos nivelam por baixo, pelo saber apenas o suficiente para sermos nada além de máquinas de produtividade.

Músico, programador, designer e apaixonado pelos ideais do Software Livre.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Post comment