Una shell è un'interfaccia per il sistema operativo. Di solito è un linguaggio di programmazione più o meno robusto a sé stante, ma con funzionalità progettate per facilitare l'interazione specifica con il sistema operativo e il file system. La semantica della shell POSIX (di seguito denominata semplicemente "la shell") è un po 'un mutt, combinando alcune caratteristiche di LISP (le espressioni s hanno molto in comune con la suddivisione delle parole della shell ) e C (gran parte della shell sintassi aritmetica la semantica viene da C).
L'altra radice della sintassi della shell deriva dalla sua educazione come un miscuglio di singole utilità UNIX. La maggior parte di quelli che sono spesso incorporati nella shell possono effettivamente essere implementati come comandi esterni. Lancia molti neofiti della shell per un ciclo quando si rendono conto che /bin/[
esiste su molti sistemi.
$ if '/bin/[' -f '/bin/['; then echo t; fi
t
cosa?
Questo ha molto più senso se guardi come viene implementata una shell. Ecco un'implementazione che ho fatto come esercizio. È in Python, ma spero che non sia un problema per nessuno. Non è particolarmente robusto, ma è istruttivo:
#!/usr/bin/env python
from __future__ import print_function
import os, sys
'''Hacky barebones shell.'''
try:
input=raw_input
except NameError:
pass
def main():
while True:
cmd = input('prompt> ')
args = cmd.split()
if not args:
continue
cpid = os.fork()
if cpid == 0:
os.execl(args[0], *args)
else:
os.waitpid(cpid, 0)
if __name__ == '__main__':
main()
Spero che quanto sopra chiarisca che il modello di esecuzione di una shell è praticamente:
1. Expand words.
2. Assume the first word is a command.
3. Execute that command with the following words as arguments.
Espansione, risoluzione dei comandi, esecuzione. Tutta la semantica della shell è legata a una di queste tre cose, sebbene siano molto più ricche dell'implementazione che ho scritto sopra.
Non tutti i comandi fork
. In effetti, ci sono una manciata di comandi che non hanno molto senso implementati come esterni (tali che dovrebberofork
), ma anche quelli sono spesso disponibili come esterni per la stretta conformità POSIX.
Bash si basa su questa base aggiungendo nuove funzionalità e parole chiave per migliorare la shell POSIX. È quasi compatibile con sh e bash è così onnipresente che alcuni autori di script passano anni senza rendersi conto che uno script potrebbe non funzionare effettivamente su un sistema POSIXly rigoroso. (Mi chiedo anche come le persone possano preoccuparsi così tanto della semantica e dello stile di un linguaggio di programmazione, e così poco per la semantica e lo stile della shell, ma divergo.)
Ordine di valutazione
Questa è una domanda a trabocchetto: Bash interpreta le espressioni nella sua sintassi primaria da sinistra a destra, ma nella sua sintassi aritmetica segue la precedenza C. Tuttavia, le espressioni differiscono dalle espansioni . Dalla EXPANSION
sezione del manuale di bash:
L'ordine delle espansioni è: espansione delle parentesi graffe; espansione di tilde, espansione di parametri e variabili, espansione aritmetica e sostituzione di comandi (eseguita da sinistra a destra); divisione delle parole; e l'espansione del nome del percorso.
Se comprendi la divisione delle parole, l'espansione del nome del percorso e l'espansione dei parametri, sei sulla buona strada per capire la maggior parte di ciò che fa bash. Nota che l'espansione del nome del percorso che viene dopo Wordsplitting è fondamentale, perché garantisce che un file con spazi bianchi nel suo nome possa ancora essere abbinato a un glob. Questo è il motivo per cui un buon uso delle espansioni glob è migliore dell'analisi dei comandi , in generale.
Scopo
Ambito della funzione
Proprio come il vecchio ECMAscript, la shell ha un ambito dinamico a meno che tu non dichiari esplicitamente i nomi all'interno di una funzione.
$ foo() { echo $x; }
$ bar() { local x; echo $x; }
$ foo
$ bar
$ x=123
$ foo
123
$ bar
$ …
Ambiente e "ambito" di processo
Le subshell ereditano le variabili delle loro shell genitrici, ma altri tipi di processi non ereditano nomi non esportati.
$ x=123
$ ( echo $x )
123
$ bash -c 'echo $x'
$ export x
$ bash -c 'echo $x'
123
$ y=123 bash -c 'echo $y'
123
Puoi combinare queste regole di ambito:
$ foo() {
> local -x bar=123
> bash -c 'echo $bar'
> }
$ foo
123
$ echo $bar
$
Disciplina di battitura
Ehm, tipi. Si. Bash in realtà non ha tipi e tutto si espande in una stringa (o forse una parola sarebbe più appropriata). Ma esaminiamo i diversi tipi di espansioni.
stringhe
Praticamente qualsiasi cosa può essere trattata come una stringa. Le parole nude in bash sono stringhe il cui significato dipende interamente dall'espansione ad esse applicata.
Nessuna espansione
Può essere utile dimostrare che una semplice parola è davvero solo una parola e che le virgolette non cambiano nulla al riguardo.
$ echo foo
foo
$ 'echo' foo
foo
$ "echo" foo
foo
Espansione della sottostringa
$ fail='echoes'
$ set -x
$ "${fail:0:-2}" Hello World
+ echo Hello World
Hello World
Per ulteriori informazioni sulle espansioni, leggere la Parameter Expansion
sezione del manuale. È abbastanza potente.
Numeri interi ed espressioni aritmetiche
Puoi impregnare i nomi con l'attributo integer per dire alla shell di trattare il lato destro delle espressioni di assegnazione come aritmetica. Quindi, quando il parametro si espande, verrà valutato come numero intero prima di espandersi in ... una stringa.
$ foo=10+10
$ echo $foo
10+10
$ declare -i foo
$ foo=$foo
$ echo $foo
20
$ echo "${foo:0:1}"
2
Array
Argomenti e parametri posizionali
Prima di parlare di array, potrebbe valere la pena discutere i parametri posizionali. Gli argomenti di uno script è possibile accedere utilizzando parametri numerati, $1
, $2
, $3
, ecc È possibile accedere a tutti questi parametri contemporaneamente utilizzando "$@"
, quale espansione ha molte cose in comune con gli array. È possibile impostare e modificare i parametri posizionali utilizzando la set
o shift
builtins, o semplicemente invocando la shell o una funzione shell con questi parametri:
$ bash -c 'for ((i=1;i<=$#;i++)); do
> printf "\$%d => %s\n" "$i" "${@:i:1}"
> done' -- foo bar baz
$1 => foo
$2 => bar
$3 => baz
$ showpp() {
> local i
> for ((i=1;i<=$#;i++)); do
> printf '$%d => %s\n' "$i" "${@:i:1}"
> done
> }
$ showpp foo bar baz
$1 => foo
$2 => bar
$3 => baz
$ showshift() {
> shift 3
> showpp "$@"
> }
$ showshift foo bar baz biz quux xyzzy
$1 => biz
$2 => quux
$3 => xyzzy
Il manuale di bash a volte si riferisce anche a $0
un parametro posizionale. Lo trovo confuso, perché non lo include nel conteggio degli argomenti $#
, ma è un parametro numerato, quindi meh.$0
è il nome della shell o dello script di shell corrente.
Array
La sintassi degli array è modellata in base a parametri posizionali, quindi è più che normale pensare agli array come una sorta di "parametri posizionali esterni", se lo desideri. Gli array possono essere dichiarati utilizzando i seguenti approcci:
$ foo=( element0 element1 element2 )
$ bar[3]=element3
$ baz=( [12]=element12 [0]=element0 )
Puoi accedere agli elementi dell'array in base all'indice:
$ echo "${foo[1]}"
element1
Puoi suddividere gli array:
$ printf '"%s"\n' "${foo[@]:1}"
"element1"
"element2"
Se tratti un array come un parametro normale, otterrai l'indice zero.
$ echo "$baz"
element0
$ echo "$bar"
$ …
Se usi virgolette o barre rovesciate per impedire la divisione delle parole, l'array manterrà la divisione delle parole specificata:
$ foo=( 'elementa b c' 'd e f' )
$ echo "${#foo[@]}"
2
La principale differenza tra array e parametri posizionali sono:
- I parametri posizionali non sono sparsi. Se
$12
è impostato, puoi essere certo che $11
sia impostato anche tu . (Potrebbe essere impostato sulla stringa vuota, ma $#
non sarà inferiore a 12.) Se "${arr[12]}"
è impostato, non è garantito che "${arr[11]}"
sia impostato e la lunghezza dell'array potrebbe essere piccola come 1.
- L'elemento zero di un array è senza ambiguità l'elemento zero di quell'array. Nei parametri posizionali, l'elemento zeroth non è il primo argomento , ma il nome della shell o dello script della shell.
- Per
shift
un array, devi tagliarlo e riassegnarlo, come arr=( "${arr[@]:1}" )
. Potresti anche farlo unset arr[0]
, ma ciò renderebbe il primo elemento all'indice 1.
- Gli array possono essere condivisi implicitamente tra le funzioni di shell come globali, ma è necessario passare esplicitamente i parametri posizionali a una funzione di shell affinché possa vederli.
È spesso conveniente utilizzare le espansioni del nome del percorso per creare array di nomi di file:
$ dirs=( */ )
Comandi
I comandi sono fondamentali, ma sono anche trattati in modo più approfondito di quanto possa fare il manuale. Leggi la SHELL GRAMMAR
sezione. I diversi tipi di comandi sono:
- Comandi semplici (es.
$ startx
)
- Condutture (es.
$ yes | make config
) (Lol)
- Liste (ad es.
$ grep -qF foo file && sed 's/foo/bar/' file > newfile
)
- Comandi composti (es.
$ ( cd -P /var/www/webroot && echo "webroot is $PWD" )
)
- Coprocessi (Complesso, nessun esempio)
- Funzioni (un comando composto denominato che può essere trattato come un comando semplice)
Modello di esecuzione
Il modello di esecuzione ovviamente coinvolge sia un heap che uno stack. Questo è endemico per tutti i programmi UNIX. Bash ha anche uno stack di chiamate per le funzioni della shell, visibile tramite l'uso annidato del caller
builtin.
Riferimenti:
- La
SHELL GRAMMAR
sezione del manuale di bash
- Il XCU Shell Command Language documentazione
- La guida di Bash sul wiki di Greycat.
- Programmazione avanzata nell'ambiente UNIX
Si prega di fare commenti se si desidera che mi espanda ulteriormente in una direzione specifica.