Una semantica per gli script Bash?


87

Più di ogni altra lingua che conosco, ho "imparato" Bash su Google ogni volta che ho bisogno di qualcosa. Di conseguenza, posso mettere insieme piccoli script che sembrano funzionare. Tuttavia, non so davvero cosa stia succedendo e speravo in un'introduzione più formale a Bash come linguaggio di programmazione. Ad esempio: qual è l'ordine di valutazione? quali sono le regole di scoping? Qual è la disciplina di battitura, ad es. Tutto è una stringa? Qual è lo stato del programma: è un'assegnazione di valori-chiave di stringhe a nomi di variabili; c'è di più, ad esempio lo stack? C'è un mucchio? E così via.

Ho pensato di consultare il manuale GNU Bash per questo tipo di intuizione, ma non sembra essere quello che voglio; è più una lunga lista di zucchero sintattico piuttosto che una spiegazione del modello semantico di base. I milioni e uno "tutorial bash" online sono solo peggio. Forse dovrei prima studiare she capire Bash come uno zucchero sintattico oltre a questo? Non so se questo sia un modello accurato, però.

Eventuali suggerimenti?

EDIT: Mi è stato chiesto di fornire esempi di ciò che idealmente sto cercando. Un esempio piuttosto estremo di ciò che considererei una "semantica formale" è questo articolo sull '"essenza di JavaScript" . Forse un esempio leggermente meno formale è il rapporto Haskell 2010 .


3
La Advanced Bash Scripting Guide è una delle "million and one"?
Choroba

2
Non sono convinto che bash abbia un "modello semantico di base" (beh, forse "quasi tutto è una stringa"); Penso che sia davvero zucchero sintattico fino in fondo.
Gordon Davisson

4
Quella che chiamate "una lista di zucchero sintattico" è in realtà il modello semantico di espansione - una parte estremamente importante dell'esecuzione. Il 90% dei bug e della confusione è dovuto alla non comprensione del modello di espansione.
quell'altro ragazzo il

4
Posso capire perché qualcuno potrebbe pensare che questa sia una domanda ampia se la leggi come faccio a scrivere uno script di shell ? Ma la vera domanda è: quali sono la semantica formale e le basi per il linguaggio shell e bash in particolare? , ed è una buona domanda con un'unica risposta coerente. Votazioni per riaprire.
kojiro

1
Ho imparato un bel po 'su linuxcommand.org e c'è anche un pdf gratuito di un libro più approfondito sulla riga di comando e sulla scrittura di script di shell
samrap

Risposte:


107

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 # Tested as-is on OS X, without the `]`
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:
      # We're in a child process
      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 EXPANSIONsezione 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' # another way to transiently export a name
123

Puoi combinare queste regole di ambito:

$ foo() {
>   local -x bar=123 # Export foo, but only in this scope
>   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 # So we can see what's going on
$ "${fail:0:-2}" Hello World
+ echo Hello World
Hello World

Per ulteriori informazioni sulle espansioni, leggere la Parameter Expansionsezione 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 # Must re-evaluate the assignment
$ echo $foo
20
$ echo "${foo:0:1}" # Still just a string
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 seto shiftbuiltins, 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 $0un 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" # Even if the zeroth index isn't set

$ …

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:

  1. I parametri posizionali non sono sparsi. Se $12è impostato, puoi essere certo che $11sia 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.
  2. 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.
  3. Per shiftun array, devi tagliarlo e riassegnarlo, come arr=( "${arr[@]:1}" ). Potresti anche farlo unset arr[0], ma ciò renderebbe il primo elemento all'indice 1.
  4. 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 GRAMMARsezione. I diversi tipi di comandi sono:

  1. Comandi semplici (es. $ startx)
  2. Condutture (es. $ yes | make config) (Lol)
  3. Liste (ad es. $ grep -qF foo file && sed 's/foo/bar/' file > newfile)
  4. Comandi composti (es. $ ( cd -P /var/www/webroot && echo "webroot is $PWD" ))
  5. Coprocessi (Complesso, nessun esempio)
  6. 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 callerbuiltin.

Riferimenti:

  1. La SHELL GRAMMARsezione del manuale di bash
  2. Il XCU Shell Command Language documentazione
  3. La guida di Bash sul wiki di Greycat.
  4. Programmazione avanzata nell'ambiente UNIX

Si prega di fare commenti se si desidera che mi espanda ulteriormente in una direzione specifica.


16
+1: ottima spiegazione. Apprezza il tempo speso per scrivere questo con esempi.
jaypal singh

+1 per yes | make config;-) Ma seriamente, un ottimo commento.
Digital Trauma

ho appena iniziato a leggere questo .. bello. lascerò alcuni commenti. 1) una sorpresa ancora più grande arriva quando lo vedi /bin/[e /bin/testspesso è la stessa applicazione 2) "Supponi che la prima parola sia un comando". - aspettati quando fai l'incarico ...
Karoly Horvath,

@KarolyHorvath Sì, ho escluso intenzionalmente l'assegnazione dalla mia shell demo perché le variabili sono un pasticcio complicato. Quella demo shell non è stata scritta con questa risposta in mente: è stata scritta molto prima. Suppongo che potrei farcela execlee interpolare le prime parole nell'ambiente, ma questo lo renderebbe ancora un po 'più complicato.
kojiro

@kojiro: nah questo avrebbe complicato troppo, non era certo mia intenzione! ma l'assegnazione funziona in modo leggermente diverso (x), e IMHO dovresti menzionarlo da qualche parte nel testo. (x): e fonte di un po 'di confusione ... non riesco nemmeno più a contare quante volte ho visto persone lamentarsi di a = 1non lavorare).
Karoly Horvath

5

La risposta alla tua domanda "Qual è la disciplina di battitura, ad es. Tutto è una stringa" Le variabili Bash sono stringhe di caratteri. Ma Bash consente operazioni aritmetiche e confronti su variabili quando le variabili sono numeri interi. L'eccezione alla regola delle variabili Bash sono le stringhe di caratteri quando dette variabili sono composte o dichiarate diversamente

$ A=10/2
$ echo "A = $A"           # Variable A acting like a String.
A = 10/2

$ B=1
$ let B="$B+1"            # Let is internal to bash.
$ echo "B = $B"           # One is added to B was Behaving as an integer.
B = 2

$ A=1024                  # A Defaults to string
$ B=${A/24/STRING01}      # Substitute "24"  with "STRING01".
$ echo "B = $B"           # $B STRING is a string
B = 10STRING01

$ B=${A/24/STRING01}      # Substitute "24"  with "STRING01".
$ declare -i B
$ echo "B = $B"           # Declaring a variable with non-integers in it doesn't change the contents.
B = 10STRING01

$ B=${B/STRING01/24}      # Substitute "STRING01"  with "24".
$ echo "B = $B"
B = 1024

$ declare -i B=10/2       # Declare B and assigning it an integer value
$ echo "B = $B"           # Variable B behaving as an Integer
B = 5

Dichiarare i significati delle opzioni:

  • -una variabile è un array.
  • -f Usa solo nomi di funzioni.
  • -i La variabile deve essere trattata come un numero intero; la valutazione aritmetica viene eseguita quando alla variabile viene assegnato un valore.
  • -p Visualizza gli attributi e i valori di ogni variabile. Quando viene utilizzato -p, le opzioni aggiuntive vengono ignorate.
  • -r Rende le variabili di sola lettura. Queste variabili non possono quindi essere assegnate valori da successive istruzioni di assegnazione, né possono essere annullate.
  • -t Assegna a ciascuna variabile l'attributo trace.
  • -x Contrassegna ogni variabile per l'esportazione ai comandi successivi tramite l'ambiente.

1

La manpage bash ha un po 'più di informazioni rispetto alla maggior parte delle manpage e include alcune di quelle richieste. La mia ipotesi dopo più di un decennio di scripting bash è che, a causa della sua storia come estensione di sh, ha una sintassi originale (per mantenere la retrocompatibilità con sh).

FWIW, la mia esperienza è stata come la tua; sebbene i vari libri (ad esempio, O'Reilly "Learning the Bash Shell" e simili) aiutino con la sintassi, ci sono molti modi strani per risolvere vari problemi, e alcuni di essi non sono nel libro e devono essere cercati su Google.

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.