O uso de um loop while para processar o texto geralmente é considerado uma má prática nos shells POSIX?
Como Stéphane Chazelas apontou , algumas das razões para não usar o loop de shell são conceitual , confiabilidade , legibilidade , desempenho e segurança .
Esta resposta explica os aspectos de confiabilidade e legibilidade :
while IFS= read -r line <&3; do
printf '%s\n' "$line"
done 3< "$InputFile"
Para desempenho , o while
loop e a leitura são tremendamente lentos ao ler de um arquivo ou canal, porque o shell de leitura interno lê um caractere de cada vez.
E quanto aos aspectos conceituais e de segurança ?
shell
text-processing
cuonglm
fonte
fonte
yes
gravação no arquivo é tão rápida?bash
, lê um tamanho de buffer de cada vez, tentedash
por exemplo. Veja também unix.stackexchange.com/q/209123/38906Respostas:
Sim, vemos várias coisas como:
Ou pior:
(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:
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:É 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 ferramentaecho
ecut
, pegue-a na gaveta, chame-a, lave-a, seque-a, coloque-a de volta na gaveta e assim por diante.Algumas dessas ferramentas (
read
eecho
) são construídos na maioria das conchas, mas que dificilmente faz a diferença aqui desdeecho
ecut
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()
oufputs()
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) equivalentecut -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:(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:read line
armazenará"foo/bar baz"
em$line
, não" foo\/bar \"
como seria de esperar.Para ler uma linha, você realmente precisa:
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ê precisaprintf
aqui em vez disso.E, claro, há o típico esquecimento de citar sua variável na qual todos caem. Então é mais:
Agora, mais algumas advertências:
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 quisermos abordar alguns desses problemas acima, isso se tornará:
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:
-
(ou+
às vezes)expr
,test
...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
fonte
cut
por exemplo, é eficiente.cut -f1 < a-very-big-file
é eficiente, o mais eficiente possível, se você o escrever em C. O que é terrivelmente ineficiente e suscetível a erros é invocadocut
para cada linha de uma-very-big-file
loop em um shell, que é o ponto a ser destacado nesta resposta. Isso concorda com sua última afirmação sobre a criação de códigos desnecessários, o que me faz pensar que talvez eu não entenda seu comentário.No que diz respeito ao conceito e à legibilidade, os shells normalmente estão interessados em arquivos. A "unidade endereçável" é o arquivo e o "endereço" é o nome do arquivo. Os shells têm todos os tipos de métodos de teste para existência de arquivos, tipo de arquivo, formatação de nome de arquivo (começando com globbing). Os shells têm muito poucas primitivas para lidar com o conteúdo do arquivo. Os programadores de shell precisam chamar outro programa para lidar com o conteúdo do arquivo.
Por causa da orientação do arquivo e do nome do arquivo, a manipulação de texto no shell é muito lenta, como você notou, mas também requer um estilo de programação pouco claro e distorcido.
fonte
Existem algumas respostas complicadas, dando muitos detalhes interessantes para os geeks entre nós, mas é realmente bastante simples - processar um arquivo grande em um loop de shell é muito lento.
Eu acho que o questionador é interessante em um tipo típico de script de shell, que pode começar com algumas análises de linha de comando, configuração do ambiente, verificação de arquivos e diretórios e um pouco mais de inicialização, antes de iniciar seu trabalho principal: passar por uma grande arquivo de texto orientado a linhas.
Para as primeiras partes (
initialization
), geralmente não importa que os comandos do shell sejam lentos - ele está executando apenas algumas dezenas de comandos, talvez com alguns loops curtos. Mesmo se escrevermos essa parte de maneira ineficiente, geralmente levará menos de um segundo para fazer toda essa inicialização, e tudo bem - isso só acontece uma vez.Porém, quando processamos o arquivo grande, que pode ter milhares ou milhões de linhas, não é bom que o script do shell leve uma fração significativa de segundo (mesmo que sejam apenas algumas dezenas de milissegundos) para cada linha, pois isso pode levar horas.
É quando precisamos usar outras ferramentas, e a beleza dos scripts de shell do Unix é que eles tornam muito fácil fazer isso.
Em vez de usar um loop para examinar cada linha, precisamos passar o arquivo inteiro por um pipeline de comandos . Isso significa que, em vez de chamar os comandos milhares ou milhões de vezes, o shell os chama apenas uma vez. É verdade que esses comandos terão loops para processar o arquivo linha por linha, mas eles não são scripts de shell e foram projetados para serem rápidos e eficientes.
O Unix possui muitas ferramentas maravilhosas, que vão do simples ao complexo, que podemos usar para construir nossos pipelines. Eu normalmente começaria com os mais simples, e só usava os mais complexos quando necessário.
Eu também tentava usar as ferramentas padrão disponíveis na maioria dos sistemas e tentava manter meu uso portátil, embora isso nem sempre seja possível. E se o seu idioma favorito for Python ou Ruby, talvez você não se importe com o esforço extra de garantir que ele esteja instalado em todas as plataformas em que o seu software precisa rodar :-)
Ferramentas simples incluem
head
,tail
,grep
,sort
,cut
,tr
,sed
,join
(ao mesclar 2 arquivos) eawk
one-liners, entre muitos outros. É incrível o que algumas pessoas podem fazer com correspondência de padrões esed
comandos.Quando fica mais complexo, e você realmente precisa aplicar alguma lógica a cada linha,
awk
é uma boa opção - seja uma linha (algumas pessoas colocam scripts awk inteiros em 'uma linha', embora isso não seja muito legível) ou em um script externo curto.Como
awk
é uma linguagem interpretada (como o seu shell), é incrível que ele possa executar o processamento linha a linha com tanta eficiência, mas foi desenvolvido especificamente para isso e é realmente muito rápido.Além disso, há
Perl
um grande número de outras linguagens de script que são muito boas no processamento de arquivos de texto e também vêm com muitas bibliotecas úteis.E, finalmente, há o bom e velho C, se você precisar de velocidade máxima e alta flexibilidade (embora o processamento de texto seja um pouco entediante). Mas é provavelmente um uso muito ruim do seu tempo escrever um novo programa C para cada tarefa de processamento de arquivos que você se deparar. Como trabalho muito com arquivos CSV, escrevi vários utilitários genéricos em C que podem ser reutilizados em muitos projetos diferentes. Na verdade, isso expande o leque de 'ferramentas simples e rápidas do Unix' que posso chamar dos meus scripts de shell, para que eu possa lidar com a maioria dos projetos apenas escrevendo scripts, o que é muito mais rápido do que escrever e depurar códigos C personalizados sempre!
Algumas dicas finais:
export LANG=C
, ou muitas ferramentas tratarão seus arquivos ASCII simples como Unicode, tornando-os muito mais lentosexport LC_ALL=C
se desejarsort
produzir pedidos consistentes, independentemente do ambiente!sort
seus dados, isso provavelmente levará mais tempo (e recursos: CPU, memória, disco) do que tudo o resto; tente minimizar o número desort
comandos e o tamanho dos arquivos que eles estão classificandofonte
Sim mas...
A resposta correta de Stéphane Chazelas é baseado em shell conceito de delegação de cada operação de texto para binários específicos, como
grep
,awk
,sed
e outros.Como o bash é capaz de fazer muitas coisas sozinho, soltar os garfos pode se tornar mais rápido (mesmo que executar outro intérprete para fazer todo o trabalho).
Por exemplo, dê uma olhada neste post:
https://stackoverflow.com/a/38790442/1765658
e
https://stackoverflow.com/a/7180078/1765658
testar e comparar ...
Claro
Não há consideração sobre a entrada e segurança do usuário !
Não escreva aplicação web sob bash !!
Porém, para muitas tarefas de administração de servidor, nas quais o bash poderia ser usado no lugar do shell , o uso do builtins bash poderia ser muito eficiente.
Meu significado:
Escrever ferramentas como bin utils não é o mesmo tipo de trabalho que a administração do sistema.
Então não são as mesmas pessoas!
Onde os administradores de sistemas precisam saber
shell
, eles podem escrever protótipos usando sua ferramenta preferida (e mais conhecida).Se esse novo utilitário (protótipo) for realmente útil, outras pessoas poderão desenvolver uma ferramenta dedicada usando uma linguagem mais apropriada.
fonte
bash
. (mais de três vezes mais rápido com o ksh93 no meu teste no meu sistema).bash
geralmente é o shell mais lento. Evenzsh
é duas vezes mais rápido nesse script. Você também tem alguns problemas com variáveis não citadas e com o uso deread
. Então você está ilustrando muitos dos meus pontos aqui.sh
, Awk , Sed ,grep
,ed
,ex
,cut
,sort
,join
... tudo com mais confiabilidade do que Bash ou Perl.bash
instalado por padrão.bash
é encontrada principalmente apenas em Apple MacOS e sistemas GNU (eu suponho que é o que você chama principais distribuições ), embora muitos sistemas também tê-lo como um pacote opcional (comozsh
,tcl
,python
...)