Quão complexo um programa pode ser escrito em puro Bash? [fechadas]

17

Após uma pesquisa muito rápida, parece que o Bash é uma linguagem completa de Turing .

Eu me pergunto, por que o Bash é usado quase exclusivamente para escrever scripts relativamente simples? Como um shell Bash vem com o Linux, você pode executar scripts de shell sem nenhum interpretador ou compilador externo, conforme necessário para outras linguagens de computador populares. Essa é uma enorme vantagem, que poderia compensar a mediocridade da própria linguagem em alguns casos.

Então, existe um limite para a complexidade de tais programas? O Bash puro é usado para escrever programas complexos? É possível escrever, digamos, um compressor / descompressor de arquivos no Bash puro? Um compilador? Um simples videogame?

É tão escassamente usado apenas porque existem apenas ferramentas de depuração muito limitadas?

Bregalad
fonte
2
O shscript configureusado como parte do processo de compilação para muitos pacotes un * x não é 'relativamente simples'.
user4556274
@ user4556274 Não é, mas geralmente não é escrito à mão, mas a partir de um extenso conjunto de m4macros.
Kusalananda
2
Há um assembler x86 no Bash, então sim, o Bash é usado ocasionalmente para escrever programas complexos. Por que as pessoas não fazem isso com mais frequência? Possivelmente porque o intérprete também é lento, ruim e é propenso a erros "interessantes" (consulte fi Shellshock ). Além disso, os scripts do Bash tendem a ficar exponencialmente mais difíceis de manter com o tamanho. Olhe para o montador acima; você pode dizer da fonte se segue a sintaxe da AT&T ou Intel?
Satō Katsura
configureOs scripts também são lentos, fazem um monte de trabalho inútil e foram alvo de alguns divertidos comentários. É claro que o shell pode ser usado para programas grandes, mas também as pessoas criaram computadores com o Game of Life e o Minecraft de Conway , e também existem linguagens de programação como Brainf ** k e Hexagony . Aparentemente, algumas pessoas gostam de criar algo com átomos realmente pequenos e confusos. Você pode até mesmo vender jogos com essa ideia ...
ilkkachu
Então, essa pergunta é responsável ou não? Eles o colocam em espera e dizem que não pode ser respondido, mas ainda tenho algumas ótimas respostas. Seria bom ser coerente, como sou novo nesta SE, para me direcionar para que tipo de perguntas são e não são desejáveis ​​nessa SE.
Bregalad

Respostas:

30

parece que Bash é uma linguagem completa de Turing

O conceito de Turing completude é totalmente separado de muitos outros conceitos úteis em uma linguagem de programação na grande : usabilidade, expressividade, understandabilty, velocidade, etc.

Se Turing-completude foram todos nós necessário, não teríamos quaisquer linguagens de programação em tudo , nem mesmo linguagem assembly . Todos os programadores de computador escreveriam apenas no código da máquina , já que nossas CPUs também são completas em Turing.

por que o Bash é usado quase exclusivamente para escrever scripts relativamente simples?

Scripts shell grandes e complexos - como os configurescripts produzidos pelo GNU Autoconf - são atípicos por vários motivos:

  1. Até relativamente recentemente, você não podia contar com um shell compatível com POSIX em todos os lugares .

    Muitos sistemas, principalmente os mais antigos, tecnicamente possuem um shell compatível com POSIX em algum lugar do sistema, mas pode não estar em um local previsível /bin/sh. Se você está escrevendo um script de shell e ele precisa ser executado em muitos sistemas diferentes, como então você escreve a linha shebang ? Uma opção é seguir em frente e usar /bin/sh, mas opte por se restringir ao dialeto shell Bourne anterior ao POSIX, caso ele seja executado em um sistema desse tipo.

    Os reservatórios Bourne pré-POSIX nem possuem aritmética embutida; você precisa chamar exprou bcfazer isso.

    Mesmo com um shell POSIX, você está perdendo matrizes associativas e outros recursos que esperamos encontrar nas linguagens de script Unix desde que o Perl se tornou popular no início dos anos 90 .

    Esse fato da história significa que há uma tradição de décadas em ignorar muitos dos recursos poderosos dos modernos interpretadores de scripts da família Bourne, puramente porque você não pode contar com eles em todos os lugares.

    Na verdade, isso ainda continua até hoje: o Bash não conseguiu matrizes associativas até a versão 4 , mas você pode se surpreender com a quantidade de sistemas ainda em uso baseados no Bash 3. A Apple ainda envia o Bash 3 com o macOS em 2017 - aparentemente para motivos de licenciamento - e os servidores Unix / Linux geralmente são executados praticamente sem tocar por muito tempo, portanto, você pode ter um sistema antigo estável ainda executando o Bash 3, como uma caixa do CentOS 5. Se você possui esses sistemas em seu ambiente, não pode usar matrizes associativas em scripts de shell que precisam ser executados neles.

    Se sua resposta para esse problema é que você apenas escreve scripts de shell para sistemas "modernos", precisa lidar com o fato de que o último ponto de referência comum para a maioria dos shells do Unix é o padrão de shell POSIX , que permanece praticamente inalterado desde que foi introduzido em 1989. Existem muitas conchas diferentes com base nesse padrão, mas todas divergiram em graus variados desse padrão. Para tirar arrays associativos de novo, bash, zsh, e ksh93todos têm essa característica, mas existem várias incompatibilidades de implementação. Sua escolha, então, é usar apenas o Bash, ou apenas o Zsh, ou apenas o uso ksh93.

    Se a sua resposta para esse problema for "instale o Bash 4" ou ksh93, o que for, por que não "instale" o Perl, o Python ou o Ruby? Isso é inaceitável em muitos casos; os padrões importam.

  2. Nenhuma das linguagens de script de shell da família Bourne suporta módulos .

    O mais próximo que você pode chegar de um sistema de módulo em um script de shell é o .comando - também conhecido sourceem variantes de shell Bourne mais modernas - que falha em vários níveis em relação a um sistema de módulo apropriado, o mais básico dos quais é o namespacing .

    Independentemente da linguagem de programação, o entendimento humano começa a sinalizar quando qualquer arquivo único em um programa geral maior excede alguns milhares de linhas. O motivo pelo qual estruturamos programas grandes em muitos arquivos é para que possamos abstrair seu conteúdo em uma ou duas frases, no máximo. O arquivo A é o analisador de linha de comando, o arquivo B é a bomba de E / S da rede, o arquivo C é o calço entre a biblioteca Z e o restante do programa, etc. Quando seu único método para reunir muitos arquivos em um único programa é a inclusão de texto , você limita o tamanho dos seus programas para crescer razoavelmente.

    Para comparação, seria como se a linguagem de programação C não tivesse vinculador, apenas #includeinstruções. Esse dialeto C-lite não precisaria de palavras-chave como externou static. Esses recursos existem para permitir modularidade.

  3. O POSIX não define uma maneira de definir variáveis ​​de escopo para uma única função de script de shell, muito menos para um arquivo.

    Isso efetivamente torna todas as variáveis ​​globais , o que prejudica a modularidade e a composição.

    Existem soluções para este em conchas de pós-POSIX - certamente bash, ksh93e zshpelo menos - mas isso só traz de volta ao ponto 1 acima.

    Você pode ver o efeito disso nos guias de estilo na gravação de macro do GNU Autoconf, onde eles recomendam que você prefixe os nomes das variáveis ​​com o nome da própria macro, levando a nomes de variáveis ​​muito longos apenas para reduzir a chance de colisão de maneira aceitável perto de zero.

    Mesmo C é melhor nessa pontuação, por uma milha. Além de a maioria dos programas C serem escritos principalmente com variáveis ​​locais de função, C também oferece suporte ao escopo de blocos, permitindo que vários blocos em uma única função reutilizem nomes de variáveis ​​sem contaminação cruzada.

  4. As linguagens de programação do shell não possuem biblioteca padrão.

    É possível argumentar que a biblioteca padrão de uma linguagem de script de shell é o conteúdo de PATH, mas que apenas diz que, para obter alguma conseqüência, um script de shell precisa chamar outro programa inteiro, provavelmente um escrito em uma linguagem mais poderosa para começar com.

    Também não existe um arquivo amplamente usado de bibliotecas de utilitários de shell, como no CPAN do Perl . Sem uma grande biblioteca disponível de código de utilitário de terceiros, um programador deve escrever mais código manualmente, para que seja menos produtivo.

    Mesmo ignorando o fato de que a maioria dos shell scripts dependem de programas externos normalmente escritos em C para obter alguma coisa útil fazer, há a sobrecarga de todos aqueles pipe()fork()exec()cadeias de chamadas. Esse padrão é bastante eficiente no Unix, comparado ao IPC e ao processo iniciado em outros sistemas operacionais, mas aqui está efetivamente substituindo o que você faria com uma chamada de sub - rotina em outra linguagem de script, que é muito mais eficiente ainda. Isso coloca um limite sério no limite superior da velocidade de execução de scripts de shell.

  5. Os scripts de shell têm pouca capacidade interna de aumentar seu desempenho via execução paralela.

    Shells Bourne tem &, waite dutos para isso, mas isso é em grande parte apenas útil para compor vários programas, não para alcançar CPU ou I / paralelismo S. É provável que você não consiga identificar os núcleos ou saturar uma matriz RAID apenas com scripts de shell e, se o fizer, provavelmente poderá obter um desempenho muito maior em outros idiomas.

    Os pipelines, em particular, são maneiras fracas de aumentar o desempenho via execução paralela. Ele permite apenas que dois programas sejam executados em paralelo, e um dos dois provavelmente será bloqueado na E / S de / para o outro a qualquer momento.

    Há maneiras dos últimos dias em torno deste, como xargs -Pe GNUparallel , mas isto só recai para o ponto 4 acima.

    Com efetivamente nenhuma capacidade embutida de tirar o máximo proveito dos sistemas com vários processadores, os scripts de shell sempre serão mais lentos do que um programa bem escrito em uma linguagem que pode usar todos os processadores do sistema. Para pegar o configureexemplo de script GNU Autoconf novamente, dobrar o número de núcleos no sistema fará pouco para melhorar a velocidade na qual ele é executado.

  6. As linguagens de script do shell não têm ponteiros ou referências .

    Isso impede que você faça várias coisas facilmente em outras linguagens de programação.

    Por um lado, a incapacidade de se referir indiretamente a outra estrutura de dados na memória do programa significa que você está limitado às estruturas de dados internas . Seu shell pode ter matrizes associativas , mas como elas são implementadas? Existem várias possibilidades, cada uma com diferentes vantagens: árvores vermelho-pretas , árvores AVL e tabelas de hash são as mais comuns, mas existem outras. Se você precisar de um conjunto diferente de vantagens e desvantagens, ficará sem dinheiro porque, sem referências, não há como manipular manualmente muitos tipos de estruturas de dados avançadas. Você está preso ao que recebeu.

    Ou pode ser que você precise de uma estrutura de dados que nem sequer tenha uma alternativa adequada incorporada ao seu interpretador de script de shell, como um gráfico acíclico direcionado , necessário para modelar um gráfico de dependência . Eu tenho sido programação por décadas, e a única maneira que eu posso pensar em fazer isso em um shell script seria abusar do sistema de arquivos , usando links simbólicos como referências falsas. Esse é o tipo de solução que você obtém quando confia apenas na integridade de Turing, que não diz nada sobre se a solução é elegante, rápida ou fácil de entender.

    Estruturas de dados avançadas são apenas um uso para ponteiros e referências. Existem vários outros aplicativos para eles , o que simplesmente não pode ser feito facilmente em uma linguagem de script de shell da família Bourne.

Eu poderia continuar, mas acho que você está entendendo o ponto aqui. Simplificando, existem muitas linguagens de programação mais poderosas para sistemas do tipo Unix.

Essa é uma enorme vantagem, que poderia compensar a mediocridade da própria linguagem em alguns casos.

Claro, e é exatamente por isso que o GNU Autoconf usa um subconjunto intencionalmente restrito da família Bourne de linguagens de script shell para suas configuresaídas de script: para que seus configurescripts sejam executados praticamente em todos os lugares.

Você provavelmente não encontrará um grupo maior de crentes na utilidade de escrever em um dialeto Bourne shell altamente portátil do que os desenvolvedores do GNU Autoconf, mas sua própria criação é escrita principalmente em Perl, além de alguns m4, e apenas um pouco de shell roteiro; somente a saída do Autoconf é um script shell Bourne puro. Se isso não implora a questão de quão útil é o conceito "Bourne em todos os lugares", não sei o que será.

Então, existe um limite para a complexidade de tais programas?

Tecnicamente falando, não, como sugere a observação de Turing-completeness.

Mas isso não é o mesmo que dizer que scripts de shell arbitrariamente grandes são agradáveis ​​de escrever, fáceis de depurar ou rápidos de executar.

É possível escrever, digamos, um compressor / descompressor de arquivos no bash puro?

Bash "puro", sem chamadas para as coisas no PATH? O compressor provavelmente é possível usando echoseqüências de escape hexagonais, mas seria bastante doloroso. Pode ser impossível escrever o descompactador dessa maneira devido à incapacidade de manipular dados binários no shell . Você acabaria chamando ode traduzindo dados binários para o formato de texto, a maneira nativa do shell de manipular dados.

Depois que você começa a falar sobre o uso de scripts de shell da maneira que se pretendia, como cola para direcionar outros programas PATH, as portas se abrem, porque agora você está limitado apenas ao que pode ser feito em outras linguagens de programação, ou seja, você não tem limites. Um script shell que recebe todo o seu poder, chamando a outros programas no PATHnão correr tão rápido como programas monolíticas escritos em linguagens mais poderosas, mas não executado.

E esse é o ponto. Se você precisa de um programa para executar rapidamente, ou se precisa ser poderoso por si só, em vez de emprestar energia de outras pessoas, não o escreve com casca.

Um simples videogame?

Aqui está Tetris com casca . Outros jogos estão disponíveis, se você for procurar.

existem apenas ferramentas de depuração muito limitadas

Eu colocaria o suporte à ferramenta de depuração em 20º lugar na lista de recursos necessários para dar suporte à programação em geral. Muitos programadores confiam muito mais na printf()depuração do que nos depuradores apropriados, independentemente da linguagem.

No shell, você tem echoe set -x, que juntos são suficientes para depurar muitos problemas.

Warren Young
fonte
2
"Os scripts do shell têm pouca capacidade interna de executar execução paralela." Na minha opinião, o shell tem melhor suporte para processamento paralelo do que a maioria dos outros idiomas. Com um único caractere, &você pode executar processos em paralelo. Você pode waitconcluir os processos filhos. Você pode configurar pipelines e redes mais complexas de pipes usando pipes nomeados. Mais importante, é simples executar o processamento paralelo da maneira correta, com muito pouco código padrão e evitar os riscos e as dificuldades do multiencadeamento de memória compartilhada.
Sam Watkins
@ SamWatkins: atualizei o ponto 5 acima para responder à sua resposta. Embora eu também seja fã da passagem de mensagens entre processos separados, como uma maneira de evitar muitos dos problemas inerentes ao paralelismo de memória compartilhada, o que eu estava enfatizando aqui é sobre o aumento do desempenho, não sobre a composição e coisas assim. geralmente requer paralelismo de memória compartilhada.
21817 Warren Young
Os scripts de shell são bons para a criação de protótipos - mas, eventualmente, um projeto deve passar para uma linguagem de programação adequada e, em seguida, idealmente para uma linguagem compilada. Então, em casos extremos de montagem, como você veria no projeto FFmpeg. O Cmake é um bom exemplo do que deve acontecer ao Autotools - está escrito em C e não requer Perl, Texinfo ou M4. Seu tipo de embaraçoso realmente que Autotools ainda depende muito shell scripts depois de 30 anos wikipedia.org/wiki/GNU_Build_System#Criticism
Steven Penny
9

Podemos caminhar ou nadar em qualquer lugar, então por que nos incomodamos com bicicletas, carros, trens, barcos, aviões e outros veículos? Claro, caminhar ou nadar pode ser cansativo, mas há uma enorme vantagem em não precisar de nenhum equipamento extra.

Por um lado, embora o bash seja Turing-complete, ele não é bom para manipular dados que não sejam números inteiros (não muito grandes), strings, matrizes (unidimensionais) de strings e mapas finitos de strings para strings. Qualquer outro tipo de dados precisa de uma codificação incômoda, o que dificulta a gravação do programa e geralmente impõe um desempenho que não é bom o suficiente na prática. Por exemplo, operações de ponto flutuante no bash são difíceis e lentas.

Além disso, o bash tem muito poucas maneiras de interagir com seu ambiente. Ele pode executar processos, pode executar alguns tipos simples de acesso a arquivos (através do redirecionamento), e é isso. O Bash também possui um cliente de rede do lado do cliente. O Bash pode emitir bytes nulos com facilidade ( printf \\0), mas não analisa bytes nulos em sua entrada, o que o torna inadequado para ler dados binários. O Bash não pode fazer outras coisas diretamente: precisa chamar programas externos para isso. E tudo bem: os shells são projetados com o objetivo principal de executar programas externos! Os reservatórios são a linguagem da cola para combinar programas. Mas se você estiver executando um programa externo, isso significa que o programa precisa estar disponível - e você reduz a vantagem da portabilidade:)

O Bash não possui nenhum tipo de recurso que facilite a gravação de programas robustos, além de set -e. Não possui tipos (úteis), espaços para nome, módulos ou estruturas de dados aninhadas. Bugs são a dificuldade número um na programação; embora a facilidade de escrever programas livres de bugs nem sempre seja o fator decisivo na escolha de um idioma, o bash está mal classificado nesse sentido. O Bash também tem um desempenho ruim ao fazer outras coisas além de combinar programas.

Por um longo tempo, o bash não funcionou no Windows, e ainda hoje não está presente em uma instalação padrão do Windows e não é executado de forma totalmente nativa (mesmo na WSL) no sentido de que não possui interfaces para Recursos nativos do Windows. O Bash não é executado no iOS e não é instalado por padrão no Android. Portanto, a menos que você esteja escrevendo um aplicativo somente para Unix, o bash não é de todo portátil.

Exigir um compilador não é um problema de portabilidade. O compilador é executado na máquina dos desenvolvedores. Exigir um intérprete ou bibliotecas de terceiros pode ser um problema, mas no Linux é um problema resolvido por meio de pacotes de distribuição e, no Windows, Android e iOS, as pessoas geralmente agrupam componentes de terceiros em seus pacotes de aplicativos. Portanto, o tipo de preocupação de portabilidade que você tem em mente não é uma preocupação prática para aplicativos comuns.

Minha resposta se aplica a conchas que não sejam o bash. Alguns detalhes variam de shell para shell, mas a idéia geral é a mesma.

Gilles 'SO- parar de ser mau'
fonte
11
Acredito que o mito da portabilidade tenha sido discutido com bastante frequência, não tenho certeza se usaria esse item em particular como negativo, uma vez que também se aplica à maioria das outras linguagens, incluindo Java. Mesmo o PHP rodando em um servidor Windows vs um servidor * nix tem algumas pequenas diferenças das quais você sempre deve estar ciente, caso seja tolo o suficiente para executar qualquer coisa em um servidor Windows, ou seja. Muitas coisas não são executadas no Android ou no iOS, portanto, não tenho certeza de como isso poderia ser um comentário válido.
Lizardx
7

Algumas razões para não usar scripts de shell para programas grandes, bem no topo da minha cabeça:

  • A maioria das funções é executada executando comandos externos, o que é lento. Por outro lado, linguagens de programação como Perl podem fazer o equivalente mkdirou grepinternamente.
  • Não há uma maneira fácil de acessar as bibliotecas C ou fazer chamadas diretas ao sistema, o que significa que, por exemplo, seria difícil criar um videogame
  • Linguagens de programação adequadas têm melhor suporte para estruturas de dados complexas. Embora o Bash tenha matrizes e matrizes associativas, eu não gostaria de pensar em uma lista vinculada ou em uma árvore.
  • O shell é feito para processar comandos que são feitos se texto. Dados binários (ou seja, variáveis ​​contendo bytes NUL (bytes com valor zero)) são difíceis de serem impossíveis de manipular. Depende um pouco do shell, zshtem algum suporte. Isso ocorre também porque a interface para programas externos é baseada principalmente em texto e \0é usada como um separador.
  • Também por causa de comandos externos, a separação entre código e dados é um pouco difícil. Testemunhe todo o problema que existe ao citar dados para outro shell (ou seja, ao executar bash -c ...ou ssh -c ...)
ilkkachu
fonte
Este é o conjunto mais preciso de negativos para mim, como alguém que faz muitos scripts bash grandes, esses seriam aproximadamente o que eu listaria também como negativos. No entanto, uma coisa que eu descobri é que o Bash não é realmente muito mais lento do que outras linguagens compiladas ao comparar funcionalidades semelhantes. Eu tenho uma suspeita furtiva de que, se eu tentasse escrever algumas das coisas mais complicadas que tenho no bash em python, a diferença de velocidade não faria o trabalho monstruoso envolvido valer a pena. No entanto, só o Bash achei muito limitado, mas o Bash + gawk funciona bem, o gawk é quase real.
Lizardx