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 configure
script prodotti da GNU Autoconf, sono atipici per molte ragioni:
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 expr
o bc
per 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 ksh93
tutti 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" ksh93
o qualsiasi altra cosa, allora perché non "semplicemente" installa Perl o Python o Ruby? Ciò è inaccettabile in molti casi; le impostazioni predefinite contano.
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 source
nelle 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 #include
dichiarazioni. Un tale dialetto C-lite non avrebbe bisogno di parole chiave come extern
o static
. Tali funzionalità esistono per consentire la modularità.
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
, ksh93
e zsh
almeno - 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.
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.
Gli script della shell hanno poca capacità incorporata di aumentare le loro prestazioni tramite l'esecuzione parallela.
Le shell Bourne hanno &
, wait
e 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 -P
e 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' configure
esempio dello script GNU Autoconf , raddoppiare il numero di core nel sistema farà poco per migliorare la velocità con cui viene eseguito.
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 configure
output di script: in modo che i suoi configure
script 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 echo
sequenze 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 od
e 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 PATH
non 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 echo
e set -x
, che insieme sono sufficienti per il debug di molti problemi.
sh
scriptconfigure
utilizzato come parte del processo di compilazione per molti pacchetti un * x non è "relativamente semplice".