[Desafio Bash 8] Você consegue resolver esse quebra-cabeça do Bash Script?

23 de julho de 2017

Bem-vindo ao Bash Challenge # 8 de Yes I Know IT e It's FOSS. Neste desafio semanal mostraremos uma tela do terminal e contaremos com você para nos ajudar a obter o resultado que desejamos. Pode haver muitas soluções, e ser criativo é a parte mais divertida do desafio.

Se você ainda não fez isso, dê uma olhada nos desafios anteriores:

Você também pode comprar esses desafios (com desafios não publicados) na forma de livro e nos apoiar:

Pronto para jogar? Então aqui está o desafio desta semana.

Como adicionar um cabeçalho?

Esta semana trabalho com vários arquivos de dados e um arquivo de cabeçalho. Eu só quero inserir o conteúdo do arquivo de cabeçalho no topo de cada arquivo de dados:

Bash Challenge 8 resolve problema de script bash

Para fins de demonstração, exibi apenas um arquivo. Mas você pode imaginar que tenho muitos deles - muitos para considerar a edição manual.

De qualquer forma, por algum motivo, minha solução não funcionou: não só perdi os dados, mas meu cabeçalho apareceu duas vezes.

Comandos para usar no terminal

cat HEADER DATA01 | tee DATA01

Month, Year, Est.Value

Month, Year, Est.Value

Como você pode ver, eu realmente preciso de sua ajuda aqui - tanto para me explicar o que estava acontecendo quanto para me ajudar a resolver esse problema. Estou realmente ansioso para ler suas soluções na seção de comentários abaixo!

Alguns detalhes

Para criar este desafio, usei:

  • GNU Bash, versão 4.4.5 (x86_64-pc-linux-gnu)
  • Debian 4.8.7-1 (amd64)
  • Todos os comandos são fornecidos com uma distribuição Debian padrão
  • Nenhum comando recebeu alias

Solução

Como reproduzir

Aqui está o código bruto que usamos para produzir este desafio. Se você executá-lo em um terminal, poderá reproduzir exatamente o mesmo resultado exibido na ilustração do desafio (presumindo que você esteja usando a mesma versão de software que eu):

Comandos para usar no terminal

rm -rf ItsFOSS mkdir -p ItsFOSS cd ItsFOSS cat > HEADER << EOT

Month, Year, Est.Value

EOT cat > DATA01 << EOT Dec, 2015, 15000 Jan, 2016, 12540 Feb, 2016, 11970 EOT clear head HEADER DATA01 cat HEADER DATA01 | tee DATA01

Qual era o problema?

Em um pipeline, todos os comandos são iniciados em paralelo. Isso significa que o comando cat lendo o arquivo DATA01 e o comando tee que substitui esse mesmo arquivo são iniciados simultaneamente.

Esta é realmente uma condição de corrida. No meu sistema, tee teve tempo de sobrescrever o arquivo de destino antes que cat tivesse a oportunidade de lê-lo. Para ilustrar isso, podemos atrasar os comandos e ver que a saída depende claramente do tempo:

Comandos para usar no terminal

cat HEADER DATA01 | ( sleep 1; tee DATA01 )

Month, Year, Est.Value

Dec, 2015, 15000 Jan, 2016, 12540 Feb, 2016, 11970

Comandos para usar no terminal

(sleep 1 ; cat HEADER DATA01 ) | tee DATA01

Month, Year, Est.Value

Eu teria um problema semelhante (embora seja determinístico desta vez) usando o mais simples:

Comandos para usar no terminal

cat HEADER DATA01 > DATA01

Nesse caso, o shell sempre sobrescreve o arquivo de destino antes de lançar o comando cat. Portanto, o conteúdo do arquivo é perdido muito antes de cat ter a oportunidade de lê-lo.

Como consertar isso?

Obviamente, ninguém jamais usaria o hack sleep em uma situação real. Mas isso não é um problema: como parte das ferramentas POSIX padrão, temos vários comandos à nossa disposição para inserir o cabeçalho no topo de um arquivo. Antes disso, vamos dar uma olhada na solução mais básica.

A solução KISS

Comandos para usar no terminal

cat HEADER DATA01 > DATA01.NEW mv -f DATA01.NEW DATA01

Eu realmente preciso comentar isso? Bem, embora seja rudimentar, esta solução tem um bom recurso: como rm usará a chamada do sistema rename, que por si só é atômica nesse sentido que referencia o arquivo DATA01, outros processos verão o conteúdo antigo ou o novo - mas nenhum um conteúdo escrito pela metade.

Uma solução um tanto semelhante, mas evitando criar um arquivo temporário visível no sistema de arquivos, obteria primeiro um descritor de arquivo para ler do arquivo original antes de substituí-lo:

Comandos para usar no terminal

exec 3<DATA01 # (1) rm -f DATA01 # (2) cat HEADER - <&3 >DATA01 # (3) exec 3<&- # (4)

  • Abra o arquivo DATA1 para leitura usando o descritor de arquivo 3;

  • Desvincular o arquivo original (ou seja: remover a entrada do diretório, mas não os dados, pois o arquivo ainda está aberto);

  • Use cat para ler o cabeçalho primeiro, seguido por uma leitura stdin do descritor de arquivo 3 e gravar em um novo arquivo DATA01;

  • Feche o descritor de arquivo 3 Isso excluirá efetivamente o conteúdo DATA01 antigo.

Observe que esta solução não é mais atômica no sentido usado acima. De qualquer forma, parabéns a Adithya Kiran Gangu por ter proposto essa solução!

Usando sed

Ao encontrar problemas semelhantes pela primeira vez, minha ideia era usar sed. É muito fácil inserir um cabeçalho após a primeira linha usando sed. Mas é mais difícil inserir algo antes da primeira linha . Na verdade, para isso, precisaremos de um pouco de magia:

Comandos para usar no terminal

sed -i '1{ r HEADER N }' DATA01

Para entender completamente, você precisa saber que o comando (r) ead insere o conteúdo de um arquivo no fluxo de destino, mas apenas quando o processamento da linha atual terminar. É por isso que usei o comando (N) ext: ele encerrará o processamento da linha 1 mais cedo (ou seja: antes da saída de linha normal). Assim, ao encontrar esse comando, sed termina o processamento da linha 1. O que dispara a saída do conteúdo do arquivo HEADER. Mas a própria linha 1 não é enviada para a saída. Ele é mantido no buffer sed.

Em seguida, sed lê a próxima linha de entrada, anexa-a ao buffer, e como não temos nenhuma regra para a linha 2, processe-a como de costume enviando seu buffer para a saída (lembre-se, nesse estágio, o buffer contém ambos linha 1 e linha 2).

Essa solução tem uma grande desvantagem: ela assume que uma linha 2. Se o arquivo de dados contiver apenas uma linha, isso falhará terrivelmente.

Usando ed ou ex

Temos muito poucas ocasiões em que usamos ed ou seu primo ex. Ambos são editores orientados por linha. Seu comportamento é muito semelhante ao vi, nesse sentido, você carrega o arquivo na memória e envia comandos ao editor para modificar esse arquivo. A única diferença aqui é que faremos o script dos comandos em vez de enviá-los interativamente.

Comandos para usar no terminal

ed DATA01 << . 0r HEADER wq .

Comandos para usar no terminal

ex -s DATA01 << . 0r HEADER wq .

Isso funciona muito bem, mas como temos que carregar o arquivo inteiro na memória, o que pode ser um problema para arquivos muito grandes.

Como sempre, essas são provavelmente apenas um subconjunto de todas as soluções possíveis. Portanto, não hesite em usar a seção de comentários para compartilhar suas próprias ideias.

E fique ligado para mais diversão!

Confira também a versão original desse post em inglês
Esse post foi originalmente escrito por Sylvain Leroux e publicado no site itsfoss.com. Tradução sujeita a revisão.

[Bash Challenge 8] Can You Solve This Bash Script Puzzle?

Propaganda
Blog Comments powered by Disqus.
Propaganda