¿Una semántica para los scripts Bash?

87

Más que cualquier otro idioma que conozco, he "aprendido" Bash buscando en Google cada vez que necesito algo. En consecuencia, puedo combinar pequeños guiones que parecen funcionar. Sin embargo, realmente no sé qué está pasando y esperaba una introducción más formal a Bash como lenguaje de programación. Por ejemplo: ¿Cuál es la orden de evaluación? ¿Cuáles son las reglas de alcance? ¿Cuál es la disciplina de mecanografía, por ejemplo, es todo una cadena? ¿Cuál es el estado del programa? ¿Es una asignación de valores clave de cadenas a nombres de variables? ¿Hay más que eso, por ejemplo, la pila? ¿Hay un montón? Y así.

Pensé en consultar el manual de GNU Bash para este tipo de información, pero no parece ser lo que quiero; es más una larga lista de azúcar sintáctico que una explicación del modelo semántico central. Los "tutoriales de bash" en línea del millón y uno son solo peores. ¿Quizás debería estudiar primero shy entender Bash como un azúcar sintáctico además de esto? Sin embargo, no sé si este es un modelo preciso.

¿Alguna sugerencia?

EDITAR: Me han pedido que proporcione ejemplos de lo que idealmente estoy buscando. Un ejemplo bastante extremo de lo que yo consideraría una "semántica formal" es este artículo sobre "la esencia de JavaScript" . Quizás un ejemplo un poco menos formal es el informe Haskell 2010 .

jameshfisher
fuente
3
¿Es la Advanced Bash Scripting Guide una de las "millones y uno"?
choroba
2
No estoy convencido de que bash tenga un "modelo semántico central" (bueno, tal vez "casi todo es una cadena"); Creo que es realmente azúcar sintáctico hasta el final.
Gordon Davisson
4
Lo que usted llama "una larga lista de azúcar sintáctico" es en realidad el modelo semántico de expansión, una parte extremadamente importante de la ejecución. El 90% de los errores y la confusión se deben a no comprender el modelo de expansión.
ese otro tipo
4
Puedo ver por qué alguien podría pensar que esta es una pregunta amplia si la lees como ¿cómo escribo un script de shell ? Pero la verdadera pregunta es ¿cuáles son la semántica formal y la base del lenguaje shell y bash en particular? , y es una buena pregunta con una única respuesta coherente. Votación para reabrir.
kojiro
1
Aprendí bastante en linuxcommand.org e incluso hay un pdf gratuito de un libro más detallado sobre la línea de comandos y la escritura de scripts de shell
samrap

Respuestas:

107

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

Puede combinar estas reglas de alcance:

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

Para más información sobre expansiones, lea la Parameter Expansionsecció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 # Must re-evaluate the assignment
$ echo $foo
20
$ echo "${foo:0:1}" # Still just a string
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 seto 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 $0como parámetro posicional. Encuentro esto confuso, porque no lo incluye en el recuento de argumentos $#, pero es un parámetro numerado, así que meh. $0es 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" # Even if the zeroth index isn't set

$ …

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:

  1. Los parámetros posicionales no son escasos. Si $12está configurado, puede estar seguro de que $11tambié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.
  2. 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.
  3. Para shiftuna 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.
  4. 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 GRAMMARsección. Los diferentes tipos de comandos son:

  1. Comandos simples (p $ startx. Ej. )
  2. Tuberías (p $ yes | make config. Ej. ) (Lol)
  3. Listas (p $ grep -qF foo file && sed 's/foo/bar/' file > newfile. Ej. )
  4. Comandos compuestos (p $ ( cd -P /var/www/webroot && echo "webroot is $PWD" ). Ej. )
  5. Coprocesos (complejo, sin ejemplo)
  6. 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 callerincorporada.

Referencias:

  1. La SHELL GRAMMARsección del manual de bash
  2. El XCU Shell Command Language documentación
  3. The Bash Guide en la wiki de Greycat.
  4. Programación avanzada en el entorno UNIX

Por favor haga comentarios si quiere que me expanda más en una dirección específica.

kojiro
fuente
16
+1: Gran explicación. Aprecie el tiempo dedicado a escribir esto con ejemplos.
jaypal singh
+1 para yes | make config;-) Pero en serio, una muy buena reseña.
Digital Trauma
acabo de empezar a leer esto .. agradable. dejará algunos comentarios. 1) una sorpresa aún mayor llega cuando ve que /bin/[y a /bin/testmenudo es la misma aplicación 2) "Suponga que la primera palabra es un comando". - esperar cuando hagas la asignación ...
Karoly Horvath
@KarolyHorvath Sí, excluí intencionalmente la asignación de mi shell de demostración porque las variables son un lío complicado. Ese shell de demostración no se escribió con esta respuesta en mente, se escribió mucho antes. Supongo que podría hacerlo execlee interpolar las primeras palabras en el entorno, pero eso aún lo haría un poco más complicado.
kojiro
@kojiro: nah, eso lo complicaría demasiado, ¡ciertamente esa no era mi intención! pero la asignación funciona un poco diferente (x), y en mi humilde opinión, deberías mencionarlo en algún lugar del texto. (x): y una fuente de confusión ... Ya ni siquiera puedo contar cuántas veces vi gente quejándose de a = 1no trabajar).
Karoly Horvath
5

La respuesta a su pregunta "¿Cuál es la disciplina de mecanografía, por ejemplo, es todo una cadena?" Las variables Bash son cadenas de caracteres. Pero, Bash permite operaciones aritméticas y comparaciones de variables cuando las variables son números enteros. La excepción a la regla de las variables de Bash son las cadenas de caracteres es cuando dichas variables se componen o declaran de otra manera

$ 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

Declarar los significados de las opciones:

  • -a Variable es una matriz.
  • -f Utilice únicamente nombres de funciones.
  • -i La variable debe tratarse como un número entero; La evaluación aritmética se realiza cuando se asigna un valor a la variable.
  • -p Muestra los atributos y valores de cada variable. Cuando se utiliza -p, se ignoran las opciones adicionales.
  • -r Hace que las variables sean de solo lectura. A estas variables no se les pueden asignar valores mediante declaraciones de asignación posteriores, ni tampoco se pueden desarmar.
  • -t Asigne a cada variable el atributo de seguimiento.
  • -x Marca cada variable para exportar a comandos posteriores a través del entorno.
Keith Reynolds
fuente
1

La página de manual de bash tiene bastante más información que la mayoría de las páginas de manual e incluye algo de lo que está pidiendo. Mi suposición después de más de una década de scripting bash es que, debido a su 'historia como una extensión de sh, tiene una sintaxis funky (para mantener la compatibilidad con versiones anteriores de sh).

FWIW, mi experiencia ha sido como la suya; aunque los diversos libros (por ejemplo, O'Reilly "Learning the Bash Shell" y similares) ayudan con la sintaxis, hay muchas formas extrañas de resolver varios problemas, y algunos de ellos no están en el libro y deben buscarse en Google.

philwalk
fuente