Une sémantique pour les scripts Bash?

87

Plus que toute autre langue que je connais, j'ai "appris" Bash en googlant à chaque fois que j'ai besoin de quelque chose. Par conséquent, je peux assembler des petits scripts qui semblent fonctionner. Cependant, je ne sais pas vraiment ce qui se passe et j'espérais une introduction plus formelle à Bash en tant que langage de programmation. Par exemple: quel est l'ordre d'évaluation? quelles sont les règles de cadrage? Quelle est la discipline de frappe, par exemple, tout est-il une chaîne? Quel est l'état du programme - est-ce une attribution clé-valeur de chaînes aux noms de variables; y a-t-il plus que cela, par exemple la pile? Y a-t-il un tas? Etc.

J'ai pensé consulter le manuel GNU Bash pour ce genre d'idées, mais cela ne semble pas être ce que je veux; c'est plus une longue liste de sucre syntaxique plutôt qu'une explication du modèle sémantique de base. Les millions et un "tutoriels bash" en ligne ne sont que pires. Peut-être devrais-je d'abord étudier shet comprendre Bash comme un sucre syntaxique en plus de cela? Je ne sais pas si c'est un modèle précis, cependant.

Aucune suggestion?

EDIT: on m'a demandé de fournir des exemples de ce que je recherche idéalement. Un exemple assez extrême de ce que je considérerais comme une «sémantique formelle» est cet article sur «l'essence de JavaScript» . Le rapport Haskell 2010 est peut-être un exemple un peu moins formel .

Jameshfisher
la source
3
Le Advanced Bash Scripting Guide est-il l' un des "millions et un"?
choroba
2
Je ne suis pas convaincu que bash ait un "modèle sémantique de base" (enfin, peut-être "presque tout est une chaîne"); Je pense que c'est vraiment du sucre syntaxique jusqu'au bout.
Gordon Davisson
4
Ce que vous appelez "une longue liste de sucre syntaxique" est en fait le modèle sémantique d'expansion - une partie extrêmement importante de l'exécution. 90% des bugs et de la confusion sont dus à la non-compréhension du modèle d'expansion.
cet autre gars
4
Je peux voir pourquoi quelqu'un pourrait penser que c'est une question générale si vous la lisez comme comment écrire un script shell ? Mais la vraie question est de savoir quelle est la sémantique formelle et la base du langage shell et de bash en particulier? , et c'est une bonne question avec une seule réponse cohérente. Voter pour rouvrir.
kojiro
1
J'ai beaucoup appris sur linuxcommand.org et il y a même un pdf gratuit d'un livre plus détaillé sur la ligne de commande et l' écriture de scripts shell
samrap

Réponses:

107

Un shell est une interface pour le système d'exploitation. C'est généralement un langage de programmation plus ou moins robuste à part entière, mais avec des fonctionnalités conçues pour faciliter l'interaction spécifique avec le système d'exploitation et le système de fichiers. La sémantique du shell POSIX (ci-après appelée simplement "le shell") est un peu un mutt, combinant certaines fonctionnalités de LISP (les expressions s ont beaucoup en commun avec le fractionnement de mots du shell ) et C (une grande partie de la syntaxe arithmétique du shell la sémantique vient de C).

L'autre racine de la syntaxe du shell vient de son éducation en tant que méli-mélo d'utilitaires UNIX individuels. La plupart de ce qui est souvent intégré au shell peut en fait être implémenté en tant que commandes externes. Il jette de nombreux néophytes shell pour une boucle lorsqu'ils se rendent compte que cela /bin/[existe sur de nombreux systèmes.

$ if '/bin/[' -f '/bin/['; then echo t; fi # Tested as-is on OS X, without the `]`
t

wat?

Cela a beaucoup plus de sens si vous regardez comment un shell est implémenté. Voici une implémentation que j'ai faite comme exercice. C'est en Python, mais j'espère que ce n'est un problème pour personne. Ce n'est pas très robuste, mais c'est instructif:

#!/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()

J'espère que ce qui précède montre clairement que le modèle d'exécution d'un shell est à peu près:

1. Expand words.
2. Assume the first word is a command.
3. Execute that command with the following words as arguments.

Expansion, résolution de commande, exécution. Toute la sémantique du shell est liée à l'une de ces trois choses, bien qu'elles soient beaucoup plus riches que l'implémentation que j'ai écrite ci-dessus.

Pas toutes les commandes fork. En fait, il existe une poignée de commandes qui n'ont pas beaucoup de sens implémentées en tant qu'externes (de sorte qu'elles devraientfork ), mais même celles-ci sont souvent disponibles en tant qu'externes pour une conformité POSIX stricte.

Bash s'appuie sur cette base en ajoutant de nouvelles fonctionnalités et mots clés pour améliorer le shell POSIX. Il est presque compatible avec sh, et bash est si omniprésent que certains auteurs de scripts passent des années sans se rendre compte qu'un script peut ne pas fonctionner sur un système strict POSIXly. (Je me demande aussi comment les gens peuvent se soucier autant de la sémantique et du style d'un langage de programmation, et si peu de la sémantique et du style du shell, mais je diverge.)

Ordre d'évaluation

C'est un peu une question piège: Bash interprète les expressions dans sa syntaxe principale de gauche à droite, mais dans sa syntaxe arithmétique, il suit la priorité C. Les expressions diffèrent des extensions , cependant. De la EXPANSIONsection du manuel bash:

L'ordre des expansions est: l'expansion d'accolades; expansion du tilde, expansion des paramètres et des variables, expansion arithmétique et substitution de commande (effectuée de gauche à droite); fractionnement de mots; et l'expansion des chemins.

Si vous comprenez le découpage de mots, l'expansion des chemins et l'expansion des paramètres, vous êtes sur la bonne voie pour comprendre l'essentiel de ce que fait bash. Notez que l'expansion du chemin après la séparation des mots est critique, car elle garantit qu'un fichier avec un espace dans son nom peut toujours être mis en correspondance avec un glob. C'est pourquoi une bonne utilisation des extensions glob est meilleure que l' analyse des commandes , en général.

Portée

Portée de la fonction

Tout comme l'ancien ECMAscript, le shell a une portée dynamique, sauf si vous déclarez explicitement des noms dans une fonction.

$ foo() { echo $x; }
$ bar() { local x; echo $x; }
$ foo

$ bar

$ x=123
$ foo
123
$ bar

$ …

Environnement et "portée" du processus

Les sous-shells héritent des variables de leurs shells parents, mais d'autres types de processus n'héritent pas de noms non exportés.

$ 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

Vous pouvez combiner ces règles de portée:

$ foo() {
>   local -x bar=123 # Export foo, but only in this scope
>   bash -c 'echo $bar'
> }
$ foo
123
$ echo $bar

$

Discipline de frappe

Hum, types. Ouais. Bash n'a vraiment pas de types, et tout se développe en une chaîne (ou peut-être un mot serait plus approprié.) Mais examinons les différents types d'expansion.

Cordes

Presque tout peut être traité comme une chaîne. Les mots nus en bash sont des chaînes dont la signification dépend entièrement de l'expansion qui lui est appliquée.

Aucune expansion

Cela peut valoir la peine de démontrer qu'un simple mot n'est en réalité qu'un mot, et que les citations n'y changent rien.

$ echo foo
foo
$ 'echo' foo
foo
$ "echo" foo
foo
Expansion de sous-chaîne
$ fail='echoes'
$ set -x # So we can see what's going on
$ "${fail:0:-2}" Hello World
+ echo Hello World
Hello World

Pour plus d'informations sur les extensions, lisez la Parameter Expansionsection du manuel. C'est assez puissant.

Entiers et expressions arithmétiques

Vous pouvez imprégner les noms de l'attribut entier pour indiquer au shell de traiter le côté droit des expressions d'affectation comme de l'arithmétique. Ensuite, lorsque le paramètre se développe, il sera évalué comme un nombre entier avant de se développer en… une chaîne.

$ 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

Tableaux

Arguments et paramètres de position

Avant de parler de tableaux, il peut être utile de discuter des paramètres de position. Les arguments à un script shell sont accessibles en utilisant des paramètres numérotés, $1, $2, $3, etc. Vous pouvez accéder à tous ces paramètres à la fois à l' aide "$@", l' expansion qui a beaucoup de choses en commun avec les tableaux. Vous pouvez définir et modifier les paramètres de position à l'aide des fonctions intégrées setou shift, ou simplement en invoquant le shell ou une fonction shell avec ces paramètres:

$ 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

Le manuel bash fait également parfois référence à $0un paramètre de position. Je trouve cela déroutant, car il ne l'inclut pas dans le nombre d'arguments $#, mais c'est un paramètre numéroté, donc meh. $0est le nom du shell ou du script shell actuel.

Tableaux

La syntaxe des tableaux est modélisée d'après les paramètres de position, il est donc généralement sain de considérer les tableaux comme une sorte nommée de "paramètres de position externes", si vous le souhaitez. Les tableaux peuvent être déclarés à l'aide des approches suivantes:

$ foo=( element0 element1 element2 )
$ bar[3]=element3
$ baz=( [12]=element12 [0]=element0 )

Vous pouvez accéder aux éléments du tableau par index:

$ echo "${foo[1]}"
element1

Vous pouvez découper des tableaux:

$ printf '"%s"\n' "${foo[@]:1}"
"element1"
"element2"

Si vous traitez un tableau comme un paramètre normal, vous obtiendrez l'index zéro.

$ echo "$baz"
element0
$ echo "$bar" # Even if the zeroth index isn't set

$ …

Si vous utilisez des guillemets ou des barres obliques inverses pour empêcher la division des mots, le tableau conservera la division des mots spécifiée:

$ foo=( 'elementa b c' 'd e f' )
$ echo "${#foo[@]}"
2

La principale différence entre les tableaux et les paramètres de position est:

  1. Les paramètres de position ne sont pas rares. Si $12est défini, vous pouvez être sûr qu'il $11est également défini. (Il pourrait être défini sur la chaîne vide, mais $#ne sera pas inférieur à 12.) Si "${arr[12]}"est défini, il n'y a aucune garantie qu'il "${arr[11]}"soit défini, et la longueur du tableau pourrait être aussi petite que 1.
  2. L'élément zéro d'un tableau est sans ambiguïté l'élément zéro de ce tableau. Dans les paramètres de position, l'élément zéro n'est pas le premier argument , mais le nom du shell ou du script shell.
  3. Pour shiftun tableau, vous devez le découper et le réaffecter, comme arr=( "${arr[@]:1}" ). Vous pourriez aussi faire unset arr[0], mais cela ferait le premier élément à l'index 1.
  4. Les tableaux peuvent être partagés implicitement entre les fonctions shell en tant que globaux, mais vous devez explicitement passer des paramètres positionnels à une fonction shell pour qu'elle les voie.

Il est souvent pratique d'utiliser des extensions de chemins pour créer des tableaux de noms de fichiers:

$ dirs=( */ )

Commandes

Les commandes sont essentielles, mais elles sont également couvertes plus en profondeur que je ne peux le faire dans le manuel. Lisez la SHELL GRAMMARsection. Les différents types de commandes sont:

  1. Commandes simples (par exemple $ startx)
  2. Pipelines (par exemple $ yes | make config) (lol)
  3. Listes (par exemple $ grep -qF foo file && sed 's/foo/bar/' file > newfile)
  4. Commandes composées (par exemple $ ( cd -P /var/www/webroot && echo "webroot is $PWD" ))
  5. Coprocesses (Complexe, pas d'exemple)
  6. Fonctions (une commande composée nommée qui peut être traitée comme une simple commande)

Modèle d'exécution

Le modèle d'exécution implique bien sûr à la fois un tas et une pile. Ceci est endémique à tous les programmes UNIX. Bash a également une pile d'appels pour les fonctions shell, visible via l'utilisation imbriquée de la fonction callerintégrée.

Références:

  1. La SHELL GRAMMARsection du manuel bash
  2. La documentation du langage de commande XCU Shell
  3. Le guide Bash sur le wiki de Greycat.
  4. Programmation avancée dans l'environnement UNIX

Veuillez faire des commentaires si vous souhaitez que je développe davantage dans une direction spécifique.

Kojiro
la source
16
+1: Excellente explication. Appréciez le temps passé à écrire ceci avec des exemples.
jaypal singh
+1 pour yes | make config;-) Mais sérieusement, une très bonne rédaction.
Digital Trauma
je viens de commencer à lire ça ... sympa. laissera quelques commentaires. 1) une surprise encore plus grande vient quand vous voyez cela /bin/[et /bin/testc'est souvent la même application 2) "Supposons que le premier mot est une commande." - attendez-vous quand vous faites la mission ...
Karoly Horvath
@KarolyHorvath Oui, j'ai intentionnellement exclu l'affectation de mon shell de démonstration parce que les variables sont un désordre compliqué. Ce shell de démonstration n'a pas été écrit avec cette réponse à l'esprit - il a été écrit beaucoup plus tôt. Je suppose que je pourrais le faire execleet interpoler les premiers mots dans l'environnement, mais cela rendrait encore un peu plus compliqué.
kojiro
@kojiro: non, ça compliquerait tout simplement les choses, ce n'était certainement pas mon intention! mais l'affectation fonctionne un peu différemment (x), et à mon humble avis, vous devriez le mentionner quelque part dans le texte. (x): et une source de confusion ... je ne peux même plus compter combien de fois ai-je vu des gens se plaindre de a = 1ne pas travailler).
Karoly Horvath
5

La réponse à votre question "Quelle est la discipline de frappe, par exemple tout est-il une chaîne" Les variables Bash sont des chaînes de caractères. Mais, Bash permet des opérations arithmétiques et des comparaisons sur des variables lorsque les variables sont des entiers. L'exception à la règle Les variables Bash sont des chaînes de caractères lorsque lesdites variables sont composées ou déclarées autrement

$ 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

Déclarez la signification des options:

  • -a Variable est un tableau.
  • -f Utiliser uniquement les noms de fonction.
  • -i La variable doit être traitée comme un entier; l'évaluation arithmétique est effectuée lorsque la variable reçoit une valeur.
  • -p Afficher les attributs et les valeurs de chaque variable. Lorsque -p est utilisé, les options supplémentaires sont ignorées.
  • -r Rendre les variables en lecture seule. Ces variables ne peuvent alors pas être affectées de valeurs par des instructions d'affectation ultérieures, ni être annulées.
  • -t Donne à chaque variable l'attribut trace.
  • -x Marque chaque variable pour l'exportation vers les commandes suivantes via l'environnement.
Keith Reynolds
la source
1

La page de manuel bash contient un peu plus d'informations que la plupart des pages de manuel et inclut une partie de ce que vous demandez. Mon hypothèse après plus d'une décennie de scripting bash est que, en raison de son histoire en tant qu'extension de sh, il a une syntaxe géniale (pour maintenir la compatibilité descendante avec sh).

FWIW, mon expérience a été comme la vôtre; bien que les différents livres (par exemple, O'Reilly "Learning the Bash Shell" et autres) aident à la syntaxe, il existe de nombreuses façons étranges de résoudre divers problèmes, et certains d'entre eux ne sont pas dans le livre et doivent être recherchés sur Google.

Philwalk
la source