Quanto può essere complesso un programma scritto in puro Bash? [chiuso]


17

Dopo alcune ricerche molto rapide, sembra che Bash sia un linguaggio completo di Turing .

Mi chiedo, perché Bash è usato quasi esclusivamente per scrivere script relativamente semplici? Poiché una shell Bash viene fornita con Linux, è possibile eseguire script di shell senza alcun interprete o compilatore esterno, come richiesto per altri linguaggi di computer comuni. Questo è un enorme vantaggio, che in alcuni casi potrebbe compensare la mediocrità della lingua stessa.

Quindi, esiste un limite alla complessità di tali programmi? Pure Bash è usato per scrivere programmi complessi? È possibile scrivere, per esempio, un compressore / decompressore di file in puro Bash? Un compilatore? Un semplice videogioco?

È così scarsamente usato solo perché ci sono solo strumenti di debug molto limitati?


2
Lo shscript configureutilizzato come parte del processo di compilazione per molti pacchetti un * x non è "relativamente semplice".
user4556274,

@ user4556274 Non lo è, ma di solito non è nemmeno scritto a mano ma da una vasta serie di m4macro.
Kusalananda

2
C'è un assemblatore x86 in Bash, quindi sì, Bash viene occasionalmente usato per scrivere programmi complessi. Perché le persone non lo fanno più spesso? Forse perché anche l'interprete è lento, schifoso ed è incline a bug "interessanti" (vedi fi Shellshock ). Inoltre, gli script Bash tendono a diventare esponenzialmente più difficili da mantenere con le dimensioni. Guarda l'assemblatore sopra; puoi dire dalla fonte se segue la sintassi AT&T o Intel?
Satō Katsura,

configuregli script sono anche lenti, fanno un sacco di lavoro inutile e sono stati oggetto di alcuni divertimenti. Ovviamente la shell può essere utilizzata per programmi di grandi dimensioni, ma anche in questo caso le persone hanno anche realizzato i computer con Game of Life e Minecraft di Conway , e ci sono anche linguaggi di programmazione come Brainf ** k ed Hexagony . Apparentemente ad alcune persone piace solo costruire qualcosa con atomi davvero piccoli e confusi. Puoi anche vendere giochi con quell'idea ...
ilkkachu,

Quindi, questa domanda risponde o no? Lo hanno messo in attesa e hanno detto che è senza risposta, ma ho comunque delle ottime risposte. Sarebbe bello essere coerente, dato che sono nuovo di questo SE, al fine di indirizzarmi a che tipo di domande sono e non sono desiderabili su questo SE.
Bregalad,

Risposte:


30

sembra che Bash sia un linguaggio completo di Turing

Il concetto di completezza di Turing è completamente separato da molti altri concetti utili in un linguaggio per la programmazione in generale : usabilità, espressività, comprensibilità, velocità, ecc.

Se Turing-completezza erano tutti abbiamo richiesto, non avremmo ogni linguaggio di programmazione a tutti , nemmeno assemblaggio lingua . Tutti i programmatori di computer scriverebbero semplicemente nel codice macchina , poiché anche le nostre CPU sono complete di Turing.

perché Bash viene usato quasi esclusivamente per scrivere script relativamente semplici?

Script di shell complessi e di grandi dimensioni, come gli configurescript prodotti da GNU Autoconf, sono atipici per molte ragioni:

  1. Fino a tempi relativamente recenti, non si poteva contare su una shell compatibile con POSIX ovunque .

    Molti sistemi, in particolare quelli più vecchi, hanno tecnicamente una shell compatibile con POSIX da qualche parte sul sistema, ma potrebbe non essere in una posizione prevedibile come /bin/sh. Se stai scrivendo uno script di shell e deve essere eseguito su molti sistemi diversi, come scrivi la riga shebang ? Un'opzione è quella di andare avanti e usare /bin/sh, ma scegliere di limitarsi al dialetto shell pre-POSIX Bourne nel caso in cui venga eseguito su tale sistema.

    Le shell Bourne pre-POSIX non hanno nemmeno l'aritmetica integrata; devi chiamare expro bcper farlo.

    Anche con una shell POSIX, ti stai perdendo array associativi e altre funzionalità che ci aspettavamo di trovare nei linguaggi di scripting Unix da quando Perl è diventato popolare nei primi anni '90 .

    Questo fatto della storia significa che esiste una tradizione pluridecennale nell'ignorare molte delle potenti funzionalità dei moderni interpreti di script della shell della famiglia Bourne puramente perché non si può contare sul fatto di averli ovunque.

    Questo continua ancora oggi, infatti: Bash non ha ottenuto array associativi fino alla versione 4 , ma potresti essere sorpreso di quanti sistemi ancora in uso si basano su Bash 3. Apple ha ancora Bash 3 con macOS nel 2017 - apparentemente per motivi di licenza - e i server Unix / Linux spesso eseguono quasi intatti nella produzione per molto tempo, quindi potresti avere un vecchio sistema stabile che esegue ancora Bash 3, come un box CentOS 5. Se si dispone di tali sistemi nel proprio ambiente, non è possibile utilizzare matrici associative negli script di shell che devono essere eseguiti su di essi.

    Se la tua risposta a questo problema è che scrivi solo script di shell per sistemi "moderni", devi affrontare il fatto che l'ultimo punto di riferimento comune per la maggior parte delle shell Unix è lo standard di shell POSIX , che è sostanzialmente invariato da quando era introdotto nel 1989. Ci sono molte conchiglie diverse basate su quello standard, ma sono state tutte divergenti in misura diversa da quello standard. Per riprendere array associativi, bash, zsh, e ksh93tutti hanno questa caratteristica, ma ci sono più incompatibilità di implementazione. La tua scelta, quindi, è usare solo Bash, o usare solo Zsh, o solo usare ksh93.

    Se la tua risposta a questo problema è "quindi installa Bash 4" ksh93o qualsiasi altra cosa, allora perché non "semplicemente" installa Perl o Python o Ruby? Ciò è inaccettabile in molti casi; le impostazioni predefinite contano.

  2. Nessuno dei moduli di supporto per i linguaggi di scripting della shell della famiglia Bourne .

    Il più vicino che puoi trovare in un sistema di moduli in uno script di shell è il .comando - ovvero sourcenelle più moderne varianti di shell Bourne - che fallisce su più livelli rispetto a un sistema di moduli adeguato, il più semplice dei quali è lo spazio dei nomi .

    Indipendentemente dal linguaggio di programmazione, la comprensione umana inizia a segnalare quando un singolo file in un programma complessivo più ampio supera qualche migliaio di righe. La vera ragione per cui strutturiamo programmi di grandi dimensioni in molti file è in modo da poter astrarre il loro contenuto a una frase o due al massimo. Il file A è il parser della riga di comando, il file B è la pompa I / O di rete, il file C è lo spessore tra la libreria Z e il resto del programma, ecc. Quando l'unico metodo per assemblare molti file in un singolo programma è l'inclusione testuale , imposti un limite su quanto possano crescere ragionevolmente i tuoi programmi.

    Per fare un confronto, sarebbe come se il linguaggio di programmazione C non avesse linker, solo #includedichiarazioni. Un tale dialetto C-lite non avrebbe bisogno di parole chiave come externo static. Tali funzionalità esistono per consentire la modularità.

  3. POSIX non definisce un modo per oscillare le variabili in una singola funzione di script shell, tanto meno in un file.

    Ciò rende effettivamente globali tutte le variabili , il che danneggia nuovamente la modularità e la componibilità.

    Ci sono soluzioni in questo caso nelle shell post-POSIX - sicuramente in bash, ksh93e zshalmeno - ma questo ti riporta al punto 1 sopra.

    Puoi vedere l'effetto di questo nelle guide di stile sulla scrittura di macro di GNU Autoconf, dove raccomandano di aggiungere prefissi ai nomi delle variabili con il nome della macro stessa, portando a nomi di variabili molto lunghi puramente al fine di ridurre la possibilità di collisioni in modo accettabile vicino zero.

    Anche C su questo punto è migliore di un miglio. Non solo la maggior parte dei programmi C sono scritti principalmente con variabili locali di funzione, C supporta anche l'ambito dei blocchi, consentendo a più blocchi all'interno di una singola funzione di riutilizzare i nomi delle variabili senza contaminazione incrociata.

  4. I linguaggi di programmazione Shell non hanno una libreria standard.

    È possibile sostenere che la libreria standard di un linguaggio di script di shell è il contenuto di PATH, ma ciò dice solo che per ottenere qualsiasi risultato di conseguenza, uno script di shell deve chiamare un altro intero programma, probabilmente uno scritto in un linguaggio più potente per iniziare con.

    Né esiste un archivio ampiamente utilizzato di librerie di utilità di shell come nel CPAN di Perl . Senza una vasta libreria disponibile di codice di utilità di terze parti, un programmatore deve scrivere più codice a mano, quindi è meno produttiva.

    Anche ignorando il fatto che la maggior parte degli script di shell si basano su programmi esterni tipicamente scritti in C per ottenere qualcosa di utile, c'è l'overhead di tutte quelle pipe()fork()exec()catene di chiamate. Questo modello è abbastanza efficiente su Unix, rispetto all'IPC e all'avvio del processo su altri sistemi operativi, ma qui sostituisce efficacemente ciò che faresti con una chiamata di subroutine in un altro linguaggio di scripting, che è ancora molto più efficiente. Ciò pone un limite serio al limite superiore della velocità di esecuzione degli script di shell.

  5. Gli script della shell hanno poca capacità incorporata di aumentare le loro prestazioni tramite l'esecuzione parallela.

    Le shell Bourne hanno &, waite pipeline per questo, ma ciò è in gran parte utile solo per comporre più programmi, non per ottenere parallelismo CPU o I / O. Non è probabile che tu sia in grado di agganciare i core o saturare un array RAID esclusivamente con script di shell e, in tal caso, potresti ottenere prestazioni molto più elevate in altre lingue.

    Le condutture, in particolare, sono modi deboli per aumentare le prestazioni tramite l'esecuzione parallela. Permette solo di eseguire due programmi in parallelo e uno dei due sarà probabilmente bloccato su I / O verso o dall'altro in un dato momento.

    Ci sono modi degli ultimi giorni attorno a questo, come xargs -Pe GNUparallel , ma questo spetta solo al punto 4 di cui sopra.

    Con nessuna capacità integrata di sfruttare appieno i sistemi multiprocessore, gli script di shell saranno sempre più lenti di un programma ben scritto in un linguaggio che può usare tutti i processori nel sistema. Per riprendere l' configureesempio dello script GNU Autoconf , raddoppiare il numero di core nel sistema farà poco per migliorare la velocità con cui viene eseguito.

  6. I linguaggi di scripting della shell non hanno puntatori o riferimenti .

    Questo ti impedisce di fare un sacco di cose facilmente realizzabili in altri linguaggi di programmazione.

    Per prima cosa, l'incapacità di riferirsi indirettamente a un'altra struttura di dati nella memoria del programma significa che sei limitato alle strutture di dati integrate . La shell può avere array associativi , ma come vengono implementati? Esistono diverse possibilità, ognuna con diversi compromessi: alberi rosso-neri , alberi AVL e tabelle hash sono i più comuni, ma ce ne sono altri. Se hai bisogno di un diverso set di compromessi, sei bloccato, perché senza riferimenti, non hai un modo per eseguire manualmente il rollback di molti tipi di strutture dati avanzate. Sei bloccato con quello che ti è stato dato.

    Oppure, potrebbe essere necessario disporre di una struttura di dati che non abbia nemmeno un'alternativa adeguata incorporata nell'interprete dello script della shell, come un grafico aciclico diretto , che potrebbe essere necessario per modellare un grafico delle dipendenze . Ho programmato per decenni e l'unico modo in cui mi viene in mente di farlo in uno script di shell sarebbe abusare del file system , usando i collegamenti simbolici come riferimenti falsi. Questo è il tipo di soluzione che ottieni quando ti affidi semplicemente alla completezza di Turing, che non ti dice nulla sul fatto che la soluzione sia elegante, veloce o facile da capire.

    Le strutture dati avanzate sono solo un uso per puntatori e riferimenti. Ci sono pile di altre applicazioni per loro , che semplicemente non possono essere fatte facilmente in un linguaggio di scripting della shell della famiglia Bourne.

Potrei andare avanti all'infinito, ma penso che tu abbia capito il punto qui. In poche parole, ci sono molti più potenti linguaggi di programmazione per sistemi di tipo Unix.

Questo è un enorme vantaggio, che in alcuni casi potrebbe compensare la mediocrità della lingua stessa.

Certo, ed è proprio per questo che GNU Autoconf usa un sottoinsieme appositamente limitato della famiglia Bourne di linguaggi di script di shell per i suoi configureoutput di script: in modo che i suoi configurescript funzionino praticamente ovunque.

Probabilmente non troverai un gruppo più ampio di credenti nell'utilità di scrivere in un dialetto shell Bourne altamente portatile rispetto agli sviluppatori di GNU Autoconf, ma la loro stessa creazione è scritta principalmente in Perl, più alcuni m4, e solo un po 'di shell sceneggiatura; solo l' output di Autoconf è un puro script di shell Bourne. Se ciò non pone la questione di quanto sia utile il concetto di "Bourne ovunque", non so quale sarà.

Quindi, esiste un limite alla complessità di tali programmi?

Tecnicamente parlando, no, come suggerisce la tua osservazione di completezza di Turing.

Ma non è la stessa cosa che dire che script di shell arbitrariamente grandi sono piacevoli da scrivere, facili da eseguire il debug o veloci da eseguire.

È possibile scrivere, per esempio, un compressore / decompressore di file in puro bash?

Bash "puro", senza alcun richiamo alle cose nel PATH? Il compressore è probabilmente fattibile usando echosequenze di escape esadecimali, ma sarebbe abbastanza doloroso da fare. Il decompressore potrebbe essere impossibile scrivere in questo modo a causa dell'incapacità di gestire i dati binari nella shell . Finiresti per chiamare ode simili per tradurre i dati binari in formato testo, il modo nativo della shell di gestire i dati.

Una volta che inizi a parlare dell'uso della shell scripting nel modo in cui era inteso, come colla per guidare altri programmi in PATH, le porte si aprono, perché ora sei limitato solo a ciò che può essere fatto in altri linguaggi di programmazione, vale a dire non ha limiti. Uno script di shell che ottiene tutta la sua potenza chiamando fuori per altri programmi nel PATHnon correre veloce come programmi monolitici scritti in linguaggi più potenti, ma non eseguito.

E questo è il punto. Se hai bisogno di un programma per funzionare velocemente, o se deve essere potente a sé stante piuttosto che prendere in prestito il potere dagli altri, non lo scrivi in ​​shell.

Un semplice videogioco?

Ecco Tetris in guscio . Altri giochi simili sono disponibili, se vai a cercare.

ci sono solo strumenti di debug molto limitati

Metterei il supporto degli strumenti di debug al 20 ° posto nell'elenco delle funzionalità necessarie per supportare la programmazione in generale. Molti programmatori fanno molto più affidamento sul printf()debug rispetto ai debugger appropriati, indipendentemente dalla lingua.

In shell, hai echoe set -x, che insieme sono sufficienti per il debug di molti problemi.


2
"Gli script della shell hanno poca capacità integrata di eseguire un'esecuzione parallela." A mio avviso, la shell ha un supporto migliore per l'elaborazione parallela rispetto alla maggior parte delle altre lingue. Con un singolo carattere &è possibile eseguire processi in parallelo. È possibile completare waiti processi figlio. È possibile impostare pipeline e reti di condotte più complesse utilizzando named pipe. Ancora più importante, è semplice eseguire l'elaborazione parallela nel modo giusto, con pochissimo codice boilerplate ed evitando i rischi e le difficoltà del multi-threading a memoria condivisa.
Sam Watkins il

@SamWatkins: ho aggiornato il punto 5 sopra per rispondere alla tua risposta. Anche se io sono un fan del passaggio di messaggi tra processi separati come un modo per evitare molti dei problemi inerenti al parallelismo della memoria condivisa, il punto che ho sollevato qui riguarda l'aumento delle prestazioni, non la componibilità e simili, e che spesso richiede il parallelismo della memoria condivisa.
Warren Young,

Gli script Shell sono utili per la prototipazione, ma alla fine un progetto dovrebbe passare a un linguaggio di programmazione adeguato, quindi idealmente un linguaggio compilato. Quindi in casi estremi il montaggio, come si vedrebbe con il progetto FFmpeg. Cmake è un buon esempio di cosa dovrebbe accadere ad Autotools - è scritto in C e non richiede Perl, Texinfo o M4. Il suo tipo di imbarazzo è che Autotools si affida ancora così tanto agli script di shell dopo 30 anni wikipedia.org/wiki/GNU_Build_System#Criticism
Steven Penny

9

Possiamo camminare o nuotare ovunque, quindi perché preoccuparci di biciclette, automobili, treni, barche, aerei e altri veicoli? Certo, camminare o nuotare può essere stancante, ma c'è un enorme vantaggio nel non aver bisogno di attrezzature extra.

Per prima cosa, sebbene bash sia Turing completo, non è bravo a manipolare dati diversi da interi (non troppo grandi), stringhe, array (unidimensionali) di stringhe e mappe finite da stringhe a stringhe. Qualsiasi altro tipo di dati necessita di una fastidiosa codifica, il che rende difficile scrivere il programma e imporrebbe spesso prestazioni non abbastanza buone nella pratica. Ad esempio, le operazioni in virgola mobile in bash sono difficili e lente.

Inoltre bash ha pochissimi modi di interagire con il suo ambiente. Può eseguire processi, può eseguire alcuni semplici tipi di accesso ai file (tramite reindirizzamento), e questo è tutto. Bash ha anche un client di rete sul lato client. Bash può emettere byte null abbastanza facilmente ( printf \\0) ma non analizzare byte null nel suo input, il che lo rende inadatto alla lettura di dati binari. Bash non può fare direttamente altre cose: per questo deve chiamare programmi esterni. E va bene: le shell sono progettate per lo scopo principale di eseguire programmi esterni! Le conchiglie sono il linguaggio colla per combinare i programmi insieme. Ma se stai eseguendo un programma esterno, significa che quel programma deve essere disponibile e quindi riduci il vantaggio della portabilità:).

Bash non ha alcun tipo di funzionalità che semplifica la scrittura di programmi robusti, a parte set -e. Non ha tipi (utili) di spazi dei nomi, moduli o strutture di dati nidificati. I bug sono la difficoltà numero uno nella programmazione; mentre la facilità di scrivere programmi senza bug non è sempre il fattore decisivo nella scelta di una lingua, bash si classifica male su questo punto. Bash si classifica anche male in termini di prestazioni quando fa cose diverse dalla combinazione di programmi.

Per molto tempo bash non è stato eseguito su Windows e anche oggi non è presente in un'installazione di Windows predefinita e non funziona in modo nativo (anche in WSL) nel senso che non ha interfacce per Funzionalità native di Windows. Bash non funziona su iOS e non è installato per impostazione predefinita su Android. Quindi, a meno che tu non stia scrivendo un'applicazione solo per Unix, bash non è affatto portatile.

La richiesta di un compilatore non è un problema per la portabilità. Il compilatore viene eseguito sul computer degli sviluppatori. Richiedere un interprete o librerie di terze parti può essere un problema, ma sotto Linux è un problema risolto attraverso i pacchetti di distribuzione e, in Windows, Android e iOS, le persone generalmente raggruppano componenti di terze parti nel loro pacchetto applicativo. Quindi il tipo di problemi di portabilità che hai in mente non sono problemi pratici per le applicazioni più comuni.

La mia risposta si applica a shell diverse da bash. Alcuni dettagli variano da shell a shell ma l'idea generale è la stessa.


1
Credo che il mito della portabilità sia stato discusso abbastanza frequentemente, non sono sicuro di usare quel particolare elemento come negativo poiché si applica anche alla maggior parte delle altre lingue, incluso Java. Anche PHP in esecuzione su un server Windows rispetto a un server * nix presenta alcune piccole differenze di cui devi sempre essere consapevole, se dovessi essere abbastanza sciocco da eseguire qualsiasi cosa su un server Windows, vale a dire. Molte cose non funzionano su Android o iOS, quindi non sono sicuro di come possa essere un commento valido.
Lizardx,

7

Alcuni motivi per non usare gli script di shell per programmi di grandi dimensioni, appena fuori dalla mia testa:

  • La maggior parte delle funzioni viene eseguita eliminando i comandi esterni, che è lento. Al contrario, linguaggi di programmazione come Perl possono fare l'equivalente mkdiro grepinternamente.
  • Non esiste un modo semplice per accedere alle librerie C o effettuare chiamate di sistema dirette, il che significa che, ad esempio, il videogioco sarebbe difficile da creare
  • Linguaggi di programmazione adeguati offrono un supporto migliore per strutture dati complesse. Sebbene Bash abbia array e array associativi, ma non vorrei pensare a un elenco collegato o ad un albero.
  • La shell è fatta per elaborare comandi che vengono fatti se il testo. I dati binari (ovvero variabili contenenti byte NUL (byte con valore zero)) sono difficili da gestire. Dipende un po 'dalla shell,zsh ha un po' di supporto. Questo anche perché l'interfaccia per programmi esterni è per lo più basata su testo e \0viene utilizzata come separatore.
  • Anche a causa di comandi esterni, la separazione tra codice e dati è leggermente difficile. Testimone di tutti i problemi che si verificano quando si citano i dati su un'altra shell (ad esempio durante l'esecuzione bash -c ...o ssh -c ...)

Questa è la serie più accurata di negativi per me, in quanto qualcuno che fa molti script bash di grandi dimensioni, questi sarebbero all'incirca quelli che elencherei anche come negativi. Tuttavia, una cosa che ho scoperto è che Bash in realtà non è molto più lento di altri linguaggi compilati quando si confrontano funzionalità simili. Ho un sospetto furtivo che se dovessi tentare di scrivere alcune delle cose più complicate che ho in bash in Python, la differenza di velocità non farebbe valere la pena del lavoro mostruoso coinvolto. Tuttavia, Bash da solo l'ho trovato troppo limitato, ma Bash + gawk funziona bene, gawk è quasi reale.
Lizardx,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.