Sim, vemos várias coisas como:
while read line; do
echo $line | cut -c3
done
Ou pior:
for line in `cat file`; do
foo=`echo $line | awk '{print $2}'`
echo whatever $foo
done
(não ria, eu já vi muitos deles).
Geralmente de iniciantes em scripts de shell. Essas são traduções literais ingênuas do que você faria em linguagens imperativas como C ou python, mas não é assim que você faz as coisas em shells, e esses exemplos são muito ineficientes, completamente não confiáveis (potencialmente levando a problemas de segurança) e, se você conseguir, para corrigir a maioria dos erros, seu código fica ilegível.
Conceitualmente
Em C ou na maioria dos outros idiomas, os blocos de construção estão apenas um nível acima das instruções do computador. Você diz ao seu processador o que fazer e depois o que fazer em seguida. Você pega seu processador manualmente e o administra de maneira micro: você abre esse arquivo, lê muitos bytes, faz isso, faz isso com ele.
Os reservatórios são uma linguagem de nível superior. Pode-se dizer que nem é uma língua. Eles estão antes de todos os intérpretes de linha de comando. O trabalho é realizado por esses comandos que você executa e o shell serve apenas para orquestrá-los.
Uma das grandes coisas que o Unix introduziu foi o pipe e os fluxos stdin / stdout / stderr padrão que todos os comandos manipulam por padrão.
Em 45 anos, não achamos melhor que essa API para aproveitar o poder dos comandos e fazê-los cooperar em uma tarefa. Essa é provavelmente a principal razão pela qual as pessoas ainda usam conchas hoje.
Você tem uma ferramenta de corte e uma ferramenta de transliteração e pode simplesmente:
cut -c4-5 < in | tr a b > out
O shell está apenas fazendo o encanamento (abra os arquivos, configure os canos, chame os comandos) e, quando estiver pronto, ele flui sem que o shell faça qualquer coisa. As ferramentas realizam seu trabalho simultaneamente, eficientemente em seu próprio ritmo, com buffer suficiente, para não bloquear um ao outro, é simplesmente bonito e ao mesmo tempo tão simples.
Invocar uma ferramenta tem um custo (e vamos desenvolvê-la no ponto de desempenho). Essas ferramentas podem ser escritas com milhares de instruções em C. Um processo deve ser criado, a ferramenta deve ser carregada, inicializada e, em seguida, limpa, destruída e aguardada.
Invocar cut
é como abrir a gaveta da cozinha, pegue a faca, use-a, lave-a, seque-a e coloque-a de volta na gaveta. Quando você faz:
while read line; do
echo $line | cut -c3
done < file
É como em cada linha do arquivo, pegar a read
ferramenta na gaveta da cozinha (muito desajeitada porque não foi projetada para isso ), ler uma linha, lavar a ferramenta de leitura e recolocá-la na gaveta. Em seguida, agende uma reunião para a ferramenta echo
e cut
, pegue-a na gaveta, chame-a, lave-a, seque-a, coloque-a de volta na gaveta e assim por diante.
Algumas dessas ferramentas ( read
e echo
) são construídos na maioria das conchas, mas que dificilmente faz a diferença aqui desde echo
e cut
ainda precisam ser executados em processos separados.
É como cortar uma cebola, mas lavar a faca e colocá-la de volta na gaveta da cozinha entre cada fatia.
Aqui, a maneira mais óbvia é tirar a cut
ferramenta da gaveta, cortar sua cebola inteira e recolocá-la na gaveta após todo o trabalho.
IOW, em shells, especialmente para processar texto, você invoca o menor número possível de utilitários e os coopera com a tarefa, não executa milhares de ferramentas em sequência, esperando que cada um inicie, execute, limpe antes de executar o próximo.
Leitura adicional na boa resposta de Bruce . As ferramentas internas de processamento de texto de baixo nível em shells (exceto talvez zsh
) são limitadas, pesadas e geralmente não são adequadas para o processamento geral de texto.
atuação
Como dito anteriormente, executar um comando tem um custo. Um custo enorme se esse comando não estiver embutido, mas mesmo se estiverem embutidos, o custo será alto.
E os shells não foram projetados para funcionar assim, não têm pretensão de serem linguagens de programação com desempenho. Eles não são, são apenas intérpretes de linha de comando. Portanto, pouca otimização foi feita nessa frente.
Além disso, os shells executam comandos em processos separados. Esses componentes não compartilham uma memória ou estado comum. Quando você faz um fgets()
ou fputs()
em C, isso é uma função no stdio. O stdio mantém buffers internos para entrada e saída para todas as funções do stdio, para evitar fazer chamadas dispendiosas do sistema com muita freqüência.
Os correspondentes até mesmo utilitários de shell builtin ( read
, echo
, printf
) não pode fazer isso. read
destina-se a ler uma linha. Se ele ler além do caractere de nova linha, isso significa que o próximo comando que você executar perderá. Portanto, read
é necessário ler a entrada um byte de cada vez (algumas implementações têm uma otimização se a entrada for um arquivo regular, pois eles lêem pedaços e procuram novamente, mas isso só funciona para arquivos regulares e, bash
por exemplo, lê apenas pedaços de 128 bytes, o que é ainda muito menos do que os utilitários de texto).
O mesmo no lado da saída, echo
não pode apenas armazenar sua saída em buffer, ele precisa enviá-la imediatamente, porque o próximo comando que você executar não compartilhará esse buffer.
Obviamente, executar comandos sequencialmente significa que você precisa esperar por eles; é uma pequena dança do agendador que fornece controle do shell e das ferramentas e vice-versa. Isso também significa (em oposição ao uso de instâncias de ferramentas de execução longa em um pipeline) que você não pode aproveitar vários processadores ao mesmo tempo, quando disponíveis.
Entre esse while read
loop e o (supostamente) equivalente cut -c3 < file
, no meu teste rápido, há uma taxa de tempo de CPU de cerca de 40000 nos meus testes (um segundo versus meio dia). Mas mesmo se você usar apenas os recursos internos do shell:
while read line; do
echo ${line:2:1}
done
(aqui com bash
), isso ainda é cerca de 1: 600 (um segundo vs 10 minutos).
Confiabilidade / legibilidade
É muito difícil acertar esse código. Os exemplos que dei são vistos com muita frequência na natureza, mas eles têm muitos bugs.
read
é uma ferramenta útil que pode fazer muitas coisas diferentes. Pode ler a entrada do usuário, dividi-la em palavras para armazenar em diferentes variáveis. read line
se não ler uma linha de entrada, ou talvez ele lê uma linha de uma maneira muito especial. É realmente lê palavras a partir da entrada aquelas palavras separadas por $IFS
e onde barra invertida pode ser usado para escapar dos separadores ou o caractere de nova linha.
Com o valor padrão de $IFS
, em uma entrada como:
foo\/bar \
baz
biz
read line
armazenará "foo/bar baz"
em $line
, não " foo\/bar \"
como seria de esperar.
Para ler uma linha, você realmente precisa:
IFS= read -r line
Isso não é muito intuitivo, mas é assim que é, lembre-se de que as conchas não foram feitas para serem usadas assim.
Mesmo para echo
. echo
expande seqüências. Você não pode usá-lo para conteúdos arbitrários, como o conteúdo de um arquivo aleatório. Você precisa printf
aqui em vez disso.
E, claro, há o típico esquecimento de citar sua variável na qual todos caem. Então é mais:
while IFS= read -r line; do
printf '%s\n' "$line" | cut -c3
done < file
Agora, mais algumas advertências:
- exceto
zsh
, isso não funcionará se a entrada contiver caracteres NUL enquanto pelo menos os utilitários de texto GNU não tiverem o problema.
- se houver dados após a última nova linha, eles serão ignorados
- dentro do loop, o stdin é redirecionado, portanto você precisa prestar atenção para que os comandos nele não sejam lidos no stdin.
- para os comandos nos loops, não estamos prestando atenção se eles são bem-sucedidos ou não. Geralmente, as condições de erro (disco cheio, erros de leitura ...) serão mal tratadas, geralmente mais mal do que com o equivalente correto .
Se quisermos abordar alguns desses problemas acima, isso se tornará:
while IFS= read -r line <&3; do
{
printf '%s\n' "$line" | cut -c3 || exit
} 3<&-
done 3< file
if [ -n "$line" ]; then
printf '%s' "$line" | cut -c3 || exit
fi
Isso está se tornando cada vez menos legível.
Existem vários outros problemas ao passar dados para comandos por meio dos argumentos ou recuperar sua saída em variáveis:
- a limitação no tamanho dos argumentos (algumas implementações de utilitário de texto também têm um limite lá, embora o efeito daquelas que são alcançadas seja geralmente menos problemático)
- o caractere NUL (também é um problema com os utilitários de texto).
- argumentos tomados como opções quando começam com
-
(ou +
às vezes)
- diversas peculiaridades de vários comandos normalmente utilizados nesses loops como
expr
, test
...
- os operadores de manipulação de texto (limitados) de vários shells que manipulam caracteres de vários bytes de maneiras inconsistentes.
- ...
Considerações de segurança
Quando você começa a trabalhar com variáveis de shell e argumentos para comandos , está inserindo um campo minado.
Se você esquecer de citar suas variáveis , esquecer o marcador de fim de opção , trabalhar em locais com caracteres de vários bytes (a norma atualmente), certamente introduzirá erros que mais cedo ou mais tarde se tornarão vulnerabilidades.
Quando você pode querer usar loops.
TBD
yes
gravação no arquivo é tão rápida?