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
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:
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 EXPANSION
seçã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'
123
Você pode combinar estas regras de escopo:
$ foo() {
> local -x bar=123
> 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
$ "${fail:0:-2}" Hello World
+ echo Hello World
Hello World
Para mais informações sobre expansões, leia a Parameter Expansion
seçã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
$ echo $foo
20
$ echo "${foo:0:1}"
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 set
ou shift
builtins, 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 $0
um 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"
$ …
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:
- Os parâmetros posicionais não são esparsos. Se
$12
estiver definido, você pode ter certeza de que também $11
está 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.
- 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.
- Para
shift
um 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.
- 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 GRAMMAR
seção. Os diferentes tipos de comandos são:
- Comandos simples (por exemplo
$ startx
)
- Pipelines (por exemplo
$ yes | make config
) (lol)
- Listas (por exemplo
$ grep -qF foo file && sed 's/foo/bar/' file > newfile
)
- Comandos compostos (por exemplo
$ ( cd -P /var/www/webroot && echo "webroot is $PWD" )
)
- Coprocessos (complexo, sem exemplo)
- 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 caller
integrado.
Referências:
- A
SHELL GRAMMAR
seção do manual do bash
- O XCU Shell Command Language documentação
- O Guia do Bash na wiki de Greycat.
- Programação Avançada no Ambiente UNIX
Por favor, faça comentários se quiser expandir mais em uma direção específica.