Un shell es una interfaz para el sistema operativo. Por lo general, es un lenguaje de programación más o menos robusto por derecho propio, pero con características diseñadas para facilitar la interacción específicamente con el sistema operativo y el sistema de archivos. La semántica del shell POSIX (en lo sucesivo denominado "el shell") es un poco mutt, combinando algunas características de LISP (las expresiones s tienen mucho en común con la división de palabras del shell ) y C (gran parte de la sintaxis aritmética del shell la semántica viene de C).
La otra raíz de la sintaxis del shell proviene de su crianza como una mezcolanza de utilidades individuales de UNIX. La mayoría de los elementos integrados en el shell se pueden implementar como comandos externos. A muchos neófitos de caparazón les da un vuelco cuando se dan cuenta de que /bin/[
existe en muchos sistemas.
$ if '/bin/[' -f '/bin/['; then echo t; fi
t
¿Wat?
Esto tiene mucho más sentido si observa cómo se implementa un shell. Aquí hay una implementación que hice como ejercicio. Está en Python, pero espero que no sea un problema para nadie. No es terriblemente robusto, pero es instructivo:
#!/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()
Espero que lo anterior deje en claro que el modelo de ejecución de un shell es prácticamente:
1. Expand words.
2. Assume the first word is a command.
3. Execute that command with the following words as arguments.
Expansión, resolución de comandos, ejecución. Toda la semántica del shell está ligada a una de estas tres cosas, aunque son mucho más ricas que la implementación que escribí anteriormente.
No todos los comandos fork
. De hecho, hay un puñado de comandos que no tienen mucho sentido implementados como externos (de modo que tendrían que hacerlo fork
), pero incluso esos a menudo están disponibles como externos para el estricto cumplimiento de POSIX.
Bash se basa en esta base agregando nuevas funciones y palabras clave para mejorar el shell POSIX. Es casi compatible con sh, y bash es tan omnipresente que algunos autores de guiones pasan años sin darse cuenta de que es posible que un guión no funcione en un sistema POSIX estrictamente estricto. (También me pregunto cómo la gente puede preocuparse tanto por la semántica y el estilo de un lenguaje de programación, y tan poco por la semántica y el estilo del shell, pero diverjo).
Orden de evaluación
Esta es una pregunta un poco capciosa: Bash interpreta las expresiones en su sintaxis principal de izquierda a derecha, pero en su sintaxis aritmética sigue la precedencia C. Sin embargo, las expresiones difieren de las expansiones . De la EXPANSION
sección del manual de bash:
El orden de las expansiones es: expansión de abrazaderas; expansión de tilde, expansión de parámetros y variables, expansión aritmética y sustitución de comandos (hecho de izquierda a derecha); división de palabras; y expansión de nombre de ruta.
Si comprende la división de palabras, la expansión de nombre de ruta y la expansión de parámetros, está bien encaminado para comprender la mayor parte de lo que hace bash. Tenga en cuenta que la expansión del nombre de la ruta que viene después de la división de palabras es crítica, porque asegura que un archivo con espacios en blanco en su nombre aún pueda coincidir con un glob. Esta es la razón por la que un buen uso de las expansiones globales es mejor que analizar los comandos , en general.
Alcance
Alcance de la función
Al igual que el antiguo ECMAscript, el shell tiene un alcance dinámico a menos que declare explícitamente nombres dentro de una función.
$ foo() { echo $x; }
$ bar() { local x; echo $x; }
$ foo
$ bar
$ x=123
$ foo
123
$ bar
$ …
Medio ambiente y "alcance" del proceso
Las subcapas heredan las variables de sus capas principales, pero otros tipos de procesos no heredan nombres no exportados.
$ x=123
$ ( echo $x )
123
$ bash -c 'echo $x'
$ export x
$ bash -c 'echo $x'
123
$ y=123 bash -c 'echo $y'
123
Puede combinar estas reglas de alcance:
$ foo() {
> local -x bar=123
> bash -c 'echo $bar'
> }
$ foo
123
$ echo $bar
$
Disciplina de mecanografía
Um, tipos. Si. Bash realmente no tiene tipos, y todo se expande a una cadena (o quizás una palabra sería más apropiada). Pero examinemos los diferentes tipos de expansiones.
Instrumentos de cuerda
Prácticamente cualquier cosa puede tratarse como una cuerda. Las palabras desnudas en bash son cadenas cuyo significado depende completamente de la expansión que se le aplique.
Sin expansión
Puede valer la pena demostrar que una palabra simple es realmente solo una palabra, y que las comillas no cambian nada de eso.
$ echo foo
foo
$ 'echo' foo
foo
$ "echo" foo
foo
Expansión de subcadena
$ fail='echoes'
$ set -x
$ "${fail:0:-2}" Hello World
+ echo Hello World
Hello World
Para más información sobre expansiones, lea la Parameter Expansion
sección del manual. Es bastante poderoso.
Enteros y expresiones aritméticas
Puede imbuir nombres con el atributo integer para decirle al shell que trate el lado derecho de las expresiones de asignación como aritmética. Luego, cuando el parámetro se expanda, se evaluará como matemático entero antes de expandirse a ... una cadena.
$ foo=10+10
$ echo $foo
10+10
$ declare -i foo
$ foo=$foo
$ echo $foo
20
$ echo "${foo:0:1}"
2
Matrices
Argumentos y parámetros posicionales
Antes de hablar de matrices, podría valer la pena discutir los parámetros posicionales. Los argumentos de un script de shell se puede acceder mediante parámetros numerados, $1
, $2
, $3
,, etc Usted puede acceder a todos estos parámetros a la vez usando "$@"
, que la expansión tiene muchas cosas en común con las matrices. Puede establecer y cambiar los parámetros posicionales utilizando las funciones internas set
o shift
, o simplemente invocando el shell o una función de shell con estos parámetros:
$ 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
El manual de bash también se refiere a veces $0
como parámetro posicional. Encuentro esto confuso, porque no lo incluye en el recuento de argumentos $#
, pero es un parámetro numerado, así que meh. $0
es el nombre del shell o el script de shell actual.
Matrices
La sintaxis de las matrices se modela a partir de parámetros posicionales, por lo que es más saludable pensar en las matrices como un tipo de "parámetros posicionales externos", si lo desea. Las matrices se pueden declarar utilizando los siguientes enfoques:
$ foo=( element0 element1 element2 )
$ bar[3]=element3
$ baz=( [12]=element12 [0]=element0 )
Puede acceder a los elementos de la matriz por índice:
$ echo "${foo[1]}"
element1
Puede cortar matrices:
$ printf '"%s"\n' "${foo[@]:1}"
"element1"
"element2"
Si trata una matriz como un parámetro normal, obtendrá el índice cero.
$ echo "$baz"
element0
$ echo "$bar"
$ …
Si usa comillas o barras invertidas para evitar la división de palabras, la matriz mantendrá la división de palabras especificada:
$ foo=( 'elementa b c' 'd e f' )
$ echo "${#foo[@]}"
2
La principal diferencia entre matrices y parámetros posicionales son:
- Los parámetros posicionales no son escasos. Si
$12
está configurado, puede estar seguro de que $11
también lo está. (Se podría establecer en la cadena vacía, pero $#
no será menor que 12.) Si "${arr[12]}"
se establece, no hay garantía de que "${arr[11]}"
se establezca, y la longitud de la matriz podría ser tan pequeña como 1.
- El elemento cero de una matriz es inequívocamente el elemento cero de esa matriz. En los parámetros posicionales, el elemento cero no es el primer argumento , sino el nombre del shell o script de shell.
- Para
shift
una matriz, debe dividirla y reasignarla, como arr=( "${arr[@]:1}" )
. También podría hacerlo unset arr[0]
, pero eso haría que el primer elemento en el índice 1.
- Las matrices se pueden compartir implícitamente entre funciones de shell como globales, pero debe pasar explícitamente parámetros posicionales a una función de shell para que los vea.
A menudo es conveniente utilizar expansiones de nombre de ruta para crear matrices de nombres de archivo:
$ dirs=( */ )
Comandos
Los comandos son clave, pero también están cubiertos con mayor profundidad que el manual. Lea la SHELL GRAMMAR
sección. Los diferentes tipos de comandos son:
- Comandos simples (p
$ startx
. Ej. )
- Tuberías (p
$ yes | make config
. Ej. ) (Lol)
- Listas (p
$ grep -qF foo file && sed 's/foo/bar/' file > newfile
. Ej. )
- Comandos compuestos (p
$ ( cd -P /var/www/webroot && echo "webroot is $PWD" )
. Ej. )
- Coprocesos (complejo, sin ejemplo)
- Funciones (un comando compuesto con nombre que puede tratarse como un comando simple)
Modelo de ejecución
El modelo de ejecución, por supuesto, implica tanto un montón como una pila. Esto es endémico de todos los programas de UNIX. Bash también tiene una pila de llamadas para funciones de shell, visible mediante el uso anidado de la función caller
incorporada.
Referencias:
- La
SHELL GRAMMAR
sección del manual de bash
- El XCU Shell Command Language documentación
- The Bash Guide en la wiki de Greycat.
- Programación avanzada en el entorno UNIX
Por favor haga comentarios si quiere que me expanda más en una dirección específica.