Uma semântica para scripts Bash?

87

Mais do que qualquer outra língua que conheço, "aprendi" o Bash pesquisando no Google sempre que preciso de alguma coisa. Consequentemente, posso remendar pequenos scripts que parecem funcionar. No entanto, não sei realmente o que está acontecendo e esperava uma introdução mais formal ao Bash como linguagem de programação. Por exemplo: Qual é a ordem de avaliação? quais são as regras de escopo? Qual é a disciplina de digitação, por exemplo, tudo é uma string? Qual é o estado do programa - é uma atribuição de valor-chave de strings para nomes de variáveis; há mais do que isso, por exemplo, a pilha? Existe uma pilha? E assim por diante.

Pensei em consultar o manual do GNU Bash para esse tipo de ideia, mas não parece ser o que desejo; é mais uma lista de roupas sujas de açúcar sintático do que uma explicação do modelo semântico central. Os milhões e um "tutoriais de bash" online são apenas piores. Talvez eu deva estudar primeirosh e entender o Bash como um açúcar sintático em cima disso? Não sei se este é um modelo preciso, no entanto.

Alguma sugestão?

EDIT: Fui solicitado a fornecer exemplos do que idealmente estou procurando. Um exemplo bastante extremo do que eu consideraria uma "semântica formal" é este artigo sobre "a essência do JavaScript" . Talvez um exemplo um pouco menos formal seja o relatório Haskell 2010 .

Jameshfisher
fonte
3
O Advanced Bash Scripting Guide é um dos "milhões e um"?
choroba de
2
Não estou convencido de que o bash tenha um "modelo semântico central" (bem, talvez "quase tudo seja uma string"); eu acho que é realmente um açúcar sintático em todo o caminho.
Gordon Davisson
4
O que você chama de "lista de lavanderia de açúcar sintático" é na verdade o modelo semântico de expansão - uma parte extremamente importante da execução. 90% dos bugs e da confusão se devem ao não entendimento do modelo de expansão.
aquele outro cara de
4
Posso ver por que alguém pode pensar que esta é uma questão ampla se você a ler como faço para escrever um script de shell ? Mas a verdadeira questão é: qual é a semântica formal e a base para a linguagem shell e o bash em particular? , e é uma boa pergunta com uma única resposta coerente. Votando para reabrir.
kojiro de
1
Aprendi um pouco sobre linuxcommand.org e existe até um pdf grátis de um livro mais aprofundado sobre linha de comando e como escrever scripts de shell
samrap

Respostas:

107

Um shell é uma interface para o sistema operacional. Geralmente é uma linguagem de programação mais ou menos robusta por si só, mas com recursos projetados para facilitar a interação específica com o sistema operacional e o sistema de arquivos. A semântica do shell POSIX (doravante referida apenas como "o shell") é um pouco confusa, combinando alguns recursos de LISP (expressões s têm muito em comum com a divisão de palavras do shell ) e C (grande parte da sintaxe aritmética do shell semântica vem de C).

A outra raiz da sintaxe do shell vem de sua criação como uma mistura de utilitários UNIX individuais. A maior parte do que costuma ser embutido no shell pode, na verdade, ser implementado como comandos externos. Muitos neófitos de shell ficam confusos quando percebem que /bin/[existe em muitos sistemas.

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

wat?

Isso faz muito mais sentido se você observar como um shell é implementado. Aqui está uma implementação que fiz como exercício. Está em Python, mas espero que não seja um problema para ninguém. Não é muito robusto, mas é instrutivo:

#!/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 o exposto acima deixe claro que o modelo de execução de um shell é basicamente:

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

Expansão, resolução de comando, execução. Todas as semânticas do shell estão vinculadas a uma dessas três coisas, embora sejam muito mais ricas do que a implementação que escrevi acima.

Nem todos os comandos fork. Na verdade, há um punhado de comandos que não fazem muito sentido implementados como externos (de forma que eles teriam quefork ), mas mesmo aqueles geralmente estão disponíveis como externos para conformidade estrita com POSIX.

O Bash se baseia nessa base adicionando novos recursos e palavras-chave para aprimorar o shell POSIX. É quase compatível com sh, e o bash é tão onipresente que alguns autores de script passam anos sem perceber que um script pode não funcionar em um sistema POSIXly estrito. (Eu também me pergunto como as pessoas podem se importar tanto com a semântica e o estilo de uma linguagem de programação, e tão pouco com a semântica e o estilo do shell, mas eu discordo.)

Ordem de avaliação

Esta é uma questão um pouco capciosa: o Bash interpreta expressões em sua sintaxe primária da esquerda para a direita, mas em sua sintaxe aritmética segue a precedência C. No entanto, as expressões diferem das expansões . Na EXPANSIONseção do manual do bash:

A ordem das expansões é: expansão da cinta; expansão de til, expansão de parâmetro e variável, expansão aritmética e substituição de comando (feito da esquerda para a direita); divisão de palavras; e expansão do nome do caminho.

Se você entende divisão de palavras, expansão de nome de caminho e expansão de parâmetro, você está no caminho certo para entender a maior parte do que o bash faz. Observe que a expansão do nome do caminho após a divisão de palavras é crítica, pois garante que um arquivo com espaço em branco no nome ainda possa ser correspondido por um glob. É por isso que o bom uso de expansões glob é melhor do que analisar comandos , em geral.

Escopo

Escopo de função

Muito parecido com o ECMAscript antigo, o shell tem escopo dinâmico, a menos que você declare explicitamente os nomes dentro de uma função.

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

$ bar

$ x=123
$ foo
123
$ bar

$ …

Ambiente e "escopo" de processo

Os subshells herdam as variáveis ​​de seus shells pais, mas outros tipos de processos não herdam nomes não 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

Você pode combinar estas regras de escopo:

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

$

Disciplina de digitação

Hum, tipos. Sim. Bash realmente não tem tipos, e tudo se expande para uma string (ou talvez uma palavra seja mais apropriada). Mas vamos examinar os diferentes tipos de expansões.

Cordas

Quase tudo pode ser tratado como uma string. Barewords em bash são strings cujo significado depende inteiramente da expansão aplicada a ele.

Sem expansão

Pode valer a pena demonstrar que uma palavra simples é realmente apenas uma palavra e que as aspas não mudam nada a respeito.

$ echo foo
foo
$ 'echo' foo
foo
$ "echo" foo
foo
Expansão de substring
$ fail='echoes'
$ set -x # So we can see what's going on
$ "${fail:0:-2}" Hello World
+ echo Hello World
Hello World

Para mais informações sobre expansões, leia a Parameter Expansionseção do manual. É muito poderoso.

Expressões inteiras e aritméticas

Você pode imbuir nomes com o atributo integer para dizer ao shell para tratar o lado direito das expressões de atribuição como aritmética. Então, quando o parâmetro se expande, ele será avaliado como matemática inteira antes de expandir para ... uma string.

$ 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

Arrays

Argumentos e parâmetros posicionais

Antes de falar sobre arrays, pode valer a pena discutir os parâmetros posicionais. Os argumentos para um script shell pode ser acessado usando parâmetros numerados, $1, $2, $3, etc. Você pode acessar todos estes parâmetros ao mesmo tempo usando "$@", que a expansão tem muitas coisas em comum com matrizes. Você pode definir e alterar os parâmetros de posição usando o setou shiftbuiltins, ou simplesmente invocando o shell ou uma função shell com estes 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

O manual do bash também às vezes se refere a $0um parâmetro posicional. Acho isso confuso, porque não inclui na contagem de argumentos $#, mas é um parâmetro numerado, então meh.$0é o nome do shell ou o script de shell atual.

Arrays

A sintaxe dos arrays é modelada a partir de parâmetros posicionais, portanto, é mais saudável pensar nos arrays como um tipo de nome de "parâmetros posicionais externos", se quiser. Os arrays podem ser declarados usando as seguintes abordagens:

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

Você pode acessar os elementos da matriz por índice:

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

Você pode dividir matrizes:

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

Se você tratar uma matriz como um parâmetro normal, obterá o índice zero.

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

$ …

Se você usar aspas ou barras invertidas para evitar a divisão de palavras, a matriz manterá a divisão de palavras especificada:

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

A principal diferença entre matrizes e parâmetros posicionais são:

  1. Os parâmetros posicionais não são esparsos. Se $12estiver definido, você pode ter certeza de que também $11está definido. (Pode ser definido como uma string vazia, mas $#não será menor que 12.) Se "${arr[12]}"for definido, não há garantia de que "${arr[11]}"esteja definido e o comprimento da matriz pode ser tão pequeno quanto 1.
  2. O elemento zero de uma matriz é inequivocamente o elemento zero dessa matriz. Em parâmetros posicionais, o elemento zeroth não é o primeiro argumento , mas o nome do shell ou script de shell.
  3. Para shiftum array, você precisa dividir e reatribuí-lo, como arr=( "${arr[@]:1}" ). Você também pode fazer unset arr[0], mas isso tornaria o primeiro elemento no índice 1.
  4. Os arrays podem ser compartilhados implicitamente entre as funções do shell como globais, mas você deve passar explicitamente os parâmetros posicionais para uma função do shell para que os veja.

Muitas vezes, é conveniente usar expansões de nome de caminho para criar matrizes de nomes de arquivos:

$ dirs=( */ )

Comandos

Os comandos são essenciais, mas também são abordados em mais detalhes do que o manual. Leia a SHELL GRAMMARseção. Os diferentes tipos de comandos são:

  1. Comandos simples (por exemplo $ startx)
  2. Pipelines (por exemplo $ yes | make config) (lol)
  3. Listas (por exemplo $ grep -qF foo file && sed 's/foo/bar/' file > newfile)
  4. Comandos compostos (por exemplo $ ( cd -P /var/www/webroot && echo "webroot is $PWD" ))
  5. Coprocessos (complexo, sem exemplo)
  6. Funções (um comando composto nomeado que pode ser tratado como um comando simples)

Modelo de Execução

O modelo de execução, é claro, envolve um heap e uma pilha. Isso é endêmico para todos os programas UNIX. O Bash também tem uma pilha de chamadas para funções do shell, visível por meio do uso aninhado do callerintegrado.

Referências:

  1. A SHELL GRAMMARseção do manual do bash
  2. O XCU Shell Command Language documentação
  3. O Guia do Bash na wiki de Greycat.
  4. Programação Avançada no Ambiente UNIX

Por favor, faça comentários se quiser expandir mais em uma direção específica.

kojiro
fonte
16
+1: Ótima explicação. Agradeço o tempo gasto escrevendo isso com exemplos.
Jaypal Singh
1 para yes | make config;-) Mas, falando sério, um ótimo artigo.
Digital Trauma
apenas comecei a ler isso .. legal. vai deixar alguns comentários. 1) uma surpresa ainda maior vem quando você vê isso /bin/[e /bin/testgeralmente é a mesma aplicação 2) "Suponha que a primeira palavra é um comando." - espere quando você fizer a atribuição ...
Karoly Horvath
@KarolyHorvath Sim, excluí intencionalmente a atribuição do meu shell de demonstração porque as variáveis ​​são uma bagunça complicada. Esse shell de demonstração não foi escrito com essa resposta em mente - foi escrito muito antes. Suponho que poderia fazer execlee interpolar as primeiras palavras no ambiente, mas isso ainda tornaria tudo um pouco mais complicado.
kojiro
@kojiro: nah isso complicaria demais, certamente não era minha intenção! mas a atribuição funciona um pouco diferente (x), e IMHO você deve mencioná-lo em algum lugar do texto. (x): e fonte de alguma confusão ... já nem consigo contar quantas vezes vi gente reclamando de a = 1não trabalhar).
Karoly Horvath
5

A resposta à sua pergunta "Qual é a disciplina de digitação, por exemplo, é tudo uma string" Variáveis ​​Bash são strings de caracteres. Mas o Bash permite operações aritméticas e comparações em variáveis ​​quando as variáveis ​​são inteiras. A exceção à regra as variáveis ​​Bash são cadeias de caracteres é quando as referidas variáveis ​​são compostas ou declaradas de outra forma

$ 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

Declare os significados das opções:

  • -a Variável é uma matriz.
  • -f Usa apenas nomes de funções.
  • -i A variável deve ser tratada como um inteiro; a avaliação aritmética é realizada quando um valor é atribuído à variável.
  • -p Exibe os atributos e valores de cada variável. Quando -p é usado, opções adicionais são ignoradas.
  • -r Torna as variáveis ​​somente leitura. Essas variáveis ​​não podem então receber valores atribuídos por instruções de atribuição subsequentes, nem podem ser removidas.
  • -t Fornece a cada variável o atributo trace.
  • -x Marca cada variável para exportação para comandos subsequentes por meio do ambiente.
Keith Reynolds
fonte
1

A página de manual do bash tem um pouco mais de informações do que a maioria das páginas de manual e inclui algumas das coisas que você está pedindo. Minha suposição, depois de mais de uma década de bash de script, é que, devido à sua 'história como uma extensão do sh, ele tem uma sintaxe funky (para manter compatibilidade com versões anteriores do sh).

FWIW, minha experiência tem sido como a sua; embora os vários livros (por exemplo, O'Reilly "Learning the Bash Shell" e similares) ajudem com a sintaxe, existem muitas maneiras estranhas de resolver vários problemas, e alguns deles não estão no livro e devem ser pesquisados ​​no Google.

philwalk
fonte