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
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:
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 EXPANSION
section 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'
123
Vous pouvez combiner ces règles de portée:
$ foo() {
> local -x bar=123
> 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
$ "${fail:0:-2}" Hello World
+ echo Hello World
Hello World
Pour plus d'informations sur les extensions, lisez la Parameter Expansion
section 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
$ echo $foo
20
$ echo "${foo:0:1}"
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 set
ou 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 à $0
un 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. $0
est 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"
$ …
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:
- Les paramètres de position ne sont pas rares. Si
$12
est défini, vous pouvez être sûr qu'il $11
est é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.
- 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.
- Pour
shift
un 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.
- 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 GRAMMAR
section. Les différents types de commandes sont:
- Commandes simples (par exemple
$ startx
)
- Pipelines (par exemple
$ yes | make config
) (lol)
- Listes (par exemple
$ grep -qF foo file && sed 's/foo/bar/' file > newfile
)
- Commandes composées (par exemple
$ ( cd -P /var/www/webroot && echo "webroot is $PWD" )
)
- Coprocesses (Complexe, pas d'exemple)
- 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 caller
intégrée.
Références:
- La
SHELL GRAMMAR
section du manuel bash
- La documentation du langage de commande XCU Shell
- Le guide Bash sur le wiki de Greycat.
- Programmation avancée dans l'environnement UNIX
Veuillez faire des commentaires si vous souhaitez que je développe davantage dans une direction spécifique.