Qual é a diferença entre shell builtin e shell keyword?

33

Quando executo esses dois comandos, recebo

$ type cd
cd is a shell builtin
$ type if
if is a shell keyword

É claramente mostrado que cdé um shell embutido e ifé uma palavra-chave do shell. Então, qual é a diferença entre shell embutido e palavra-chave?

Avinash Raj
fonte
2
relacionado: unix.stackexchange.com/questions/267761/…
Ciro Santilli escreveu:

Respostas:

45

Há uma forte diferença entre um builtin e uma palavra-chave, na maneira como o Bash analisa seu código. Antes de falarmos sobre a diferença, vamos listar todas as palavras-chave e componentes internos:

Builtins:

$ compgen -b
.         :         [         alias     bg        bind      break     
builtin   caller    cd        command   compgen   complete  compopt   
continue  declare   dirs      disown    echo      enable    eval      
exec      exit      export    false     fc        fg        getopts   
hash      help      history   jobs      kill      let       local     
logout    mapfile   popd      printf    pushd     pwd       read      
readarray readonly  return    set       shift     shopt     source    
suspend   test      times     trap      true      type      typeset   
ulimit    umask     unalias   unset     wait                          

Palavras-chave:

$ compgen -k
if        then      else      elif      fi        case      
esac      for       select    while     until     do        
done      in        function  time      {         }         
!         [[        ]]        coproc              

Observe que, por exemplo, [é um builtin e essa [[é uma palavra-chave. Usarei esses dois para ilustrar a diferença abaixo, pois são operadores bem conhecidos: todos os conhecem e os usam regularmente (ou deveriam).

Uma palavra-chave é verificada e entendida pelo Bash muito cedo em sua análise. Isso permite, por exemplo, o seguinte:

string_with_spaces='some spaces here'
if [[ -n $string_with_spaces ]]; then
    echo "The string is non-empty"
fi

Isso funciona bem, e o Bash terá uma saída feliz

The string is non-empty

Note que eu não citei $string_with_spaces. Considerando o seguinte:

string_with_spaces='some spaces here'
if [ -n $string_with_spaces ]; then
    echo "The string is non-empty"
fi

mostra que Bash não está feliz:

bash: [: too many arguments

Por que funciona com palavras-chave e não com builtins? porque quando o Bash analisa o código, ele vê [[qual é uma palavra-chave e entende muito cedo que é especial. Por isso, procurará o fechamento ]]e tratará o interior de uma maneira especial. Um builtin (ou comando) é tratado como um comando real que será chamado com argumentos. Neste último exemplo, o bash entende que deve executar o comando [com argumentos (mostrado um por linha):

-n
some
spaces
here
]

desde que ocorre expansão variável, remoção de cotação, expansão de nome de caminho e divisão de palavras. O comando[ acaba sendo construído no shell, portanto, ele é executado com esses argumentos, o que resulta em um erro, daí a reclamação.

Na prática, você vê que essa distinção permite um comportamento sofisticado, que não seria possível com os builtins (ou comandos).

Ainda na prática, como você pode distinguir um builtin de uma palavra-chave? este é um experimento divertido de executar:

$ a='['
$ $a -d . ]
$ echo $?
0

Quando o Bash analisa a linha $a -d . ], ele não vê nada de especial (ou seja, sem alias, sem redirecionamentos, sem palavras-chave), portanto, apenas realiza a expansão variável. Após expansões variáveis, ele vê:

[ -d . ]

assim executa o comando (embutido) [com argumentos -d, .e ], que, claro é verdadeiro (isto só testa se .é um diretório).

Agora veja:

$ a='[['
$ $a -d . ]]
bash: [[: command not found

Oh. Isso ocorre porque, quando o Bash vê essa linha, ele não vê nada de especial e, portanto, expande todas as variáveis ​​e, eventualmente, vê:

[[ -d . ]]

No momento, há muito tempo que expansões de alias e verificação de palavras-chave são executadas e não serão mais executadas, então o Bash tenta encontrar o comando chamado [[ , não o encontra e reclama.

Na mesma linha:

$ '[' -d . ]
$ echo $?
0
$ '[[' -d . ]]
bash: [[: command not found

e

$ \[ -d . ]
$ echo $?
0
$ \[[ -d . ]]
bash: [[: command not found

A expansão de alias também é algo especial. Todos vocês fizeram o seguinte pelo menos uma vez:

$ alias ll='ls -l'
$ ll
.... <list of files in long format> ....
$ \ll
bash: ll: command not found
$ 'll'
bash: ll: command not found

O raciocínio é o mesmo: a expansão do alias ocorre muito antes da expansão variável e da remoção da cotação.


Alias ​​vs palavra-chave

Agora, o que você acha que acontece se definirmos um alias para ser uma palavra-chave?

$ alias mytest='[['
$ mytest -d . ]]
$ echo $?
0

Oh, isso funciona! para que aliases possam ser usados ​​para alias palavras-chave! bom saber.


Conclusão: builtins realmente se comportam como comandos: eles correspondem a uma ação sendo executada com argumentos que sofrem expansão direta de variável e divisão e globbing de palavras. É realmente como ter um comando externo em algum lugar /binou /usr/biné chamado com os argumentos dados após a expansão de variáveis, etc. Observe que quando digo é realmente como ter um comando externo quero dizer apenas com relação a argumentos, divisão de palavras, globbing, expansão variável, etc. Um builtin pode modificar o estado interno do shell!

As palavras-chave, por outro lado, são verificadas e compreendidas muito cedo e permitem um comportamento sofisticado do shell: o shell poderá proibir a divisão de palavras ou a expansão do nome do caminho etc.

Agora, observe a lista de itens incorporados e palavras-chave e tente descobrir por que alguns precisam ser palavras-chave.


!é uma palavra-chave. Parece que seria possível imitar seu comportamento com uma função:

not() {
    if "$@"; then
        return false
    else
        return true
    fi
}

mas isso proibiria construções como:

$ ! ! true
$ echo $?
0

ou

$ ! { true; }
echo $?
1

O mesmo para time: é mais poderoso ter uma palavra-chave para que possa programar comandos e pipelines compostos complexos com redirecionamentos:

$ time grep '^#' ~/.bashrc | { i=0; while read -r; do printf '%4d %s\n' "$((++i))" "$REPLY"; done; } > bashrc_numbered 2>/dev/null

Se timeonde um mero comando (mesmo embutido), seria só vê os argumentos grep, ^#e /home/gniourf/.bashrc, tempo este, em seguida, sua saída seria percorrer as restantes partes do pipeline. Mas com uma palavra-chave, o Bash pode lidar com tudo! pode timeo pipeline completo, incluindo os redirecionamentos! Se timefosse um mero comando, não poderíamos fazer:

$ time { printf 'hello '; echo world; }

Tente:

$ \time { printf 'hello '; echo world; }
bash: syntax error near unexpected token `}'

Tente consertar (?):

$ \time { printf 'hello '; echo world;
time: cannot run {: No such file or directory

Sem esperança.


Palavra-chave vs alias?

$ alias mytime=time
$ alias myls=ls
$ mytime myls

O que você acha que acontece?


Realmente, um builtin é como um comando, exceto que ele é construído no shell, enquanto uma palavra - chave é algo que permite um comportamento sofisticado! podemos dizer que faz parte da gramática do shell.

gniourf_gniourf
fonte
2
Concordado com @JohnyTex, esta é uma das respostas mais abrangentes e adequadas que já vi nos sites da Stack. Obrigado. Uma pergunta talvez não relacionado: apenas por curiosidade eu estou tentando encontrar a documentação para a funcionalidade 'desactivar temporariamente apelido' do anterior um comando com =\'usando man, apropose, helpe eu não tive nenhuma sorte. Alguma idéia de onde eu iria encontrar essa informação? Principalmente para que no futuro eu possa ver o que mais existe, porque acho que estou perdendo uma fonte de referência.
nc.
@ NC: você não o encontrará explicitamente. A razão pela qual funciona é explicada nesta resposta. O mais próximo que você encontrará está no manual de referência na seção Operação do Shell . Você verá que a expansão do alias é feita muito cedo (algo que tentei enfatizar nesta resposta), na etapa 2. A remoção de cotação, expansão de parâmetros, globbing etc. é executada posteriormente. Então, para desativar um alias, você pode usar algum tipo de citar a proibir o shell de compreender um símbolo como um alias, por exemplo, \ll, "ll"ou 'll'.
gniourf_gniourf
1
Na verdade, recebi a ordem, aliases e palavras-chave sendo demitidos acontecem primeiro. Como citamos o [[rendimento, \[[ele não é analisado como um alias. Certo até agora? Onde eu me perdi não estava percebendo que a barra invertida era uma coisa citada no Bash, e tentei procurar por aliases e palavras-chave e me perdi completamente ... Tudo de bom agora. Na seção Citações:> Uma barra invertida não citada () é o caractere de escape.
nc.
1
Portanto, podemos pensar em (2) nessa lista Op como Tokenization, e \[[é um token único do tipo LiteralQuoteToken, em oposição ao [[qual será um OpenTestKeywordToken, e que requer um ]]CloseTestKeywordToken de fechamento para compilação ou sintaxe correta. Posteriormente, em (4) o LiteralQuoteToken será avaliado [[como o nome do bulitin / comando a ser executado e em (6) o Bash vomitará porque não há [[comando ou construtor. Agradável. Posso esquecer os detalhes precisos ao longo do tempo, mas, neste momento, a maneira como o Bash executa as coisas é muito mais clara para mim; obrigado.
nc.
9

man bashliga para eles SHELL BUILTIN COMMANDS. Portanto, um "shell embutido" é como um comando normal, como grepetc., mas, em vez de estar contido em um arquivo separado, ele é incorporado ao próprio bash . Isso os faz executar com mais eficiência do que comandos externos.

Uma palavra - chave também é "codificada no Bash, mas, diferentemente de um built-in, uma palavra-chave não é em si um comando, mas uma subunidade de uma construção de comando". Interpreto que isso significa que as palavras-chave não têm função sozinha, mas exigem comandos para fazer qualquer coisa. (A partir do link, outros exemplos são for, while, do, e !, e há mais em minha resposta à sua outra pergunta.)

Sparhawk
fonte
1
Fato interessante: [[ is a shell keywordmas [ is a shell builtin. Eu não tenho ideia do porquê.
Sparhawk
1
É provável que, por razões históricas, o padrão POSIX esteja em conformidade com o antigo shell Bourne o mais próximo possível, pois [existia como comando separado na época. [[não é especificado pelo padrão, então os desenvolvedores podem optar por incluí-lo como palavra-chave ou como incorporado.
Sergiy Kolodyazhnyy
1

O manual da linha de comando que acompanha o Ubuntu não fornece uma definição de palavras-chave; no entanto, o manual on - line (veja a nota em anexo) e as especificações padrão da POSIX Shell Command Language referem-se a elas como "Palavras Reservadas" e ambas fornecem listas delas. Do padrão POSIX:

Este reconhecimento deve ocorrer apenas quando nenhum dos caracteres é citado e quando a palavra é usada como:

  • A primeira palavra de um comando

  • A primeira palavra após uma das palavras reservadas que não seja maiúsculas, minúsculas, ou

  • A terceira palavra em um comando case (somente in é válida nesse caso)

  • A terceira palavra em um comando for (somente in e do são válidos neste caso)

A chave aqui é que palavras-chave / palavras reservadas têm um significado especial porque facilitam a sintaxe do shell, servem para sinalizar certos blocos de código, como loops, comandos compostos, instruções de ramificação (if / case) etc. Eles permitem a formação de instruções de comando, mas por si só - não fazer nada, e, de fato, se você digitar palavras-chave como for, until, case- o shell vai esperar uma declaração completa, caso contrário - erro de sintaxe:

$ for
bash: syntax error near unexpected token `newline'
$  

No nível do código-fonte, as palavras reservadas para o bash são definidas em parese.y , enquanto os internos têm um diretório inteiro dedicado a eles.

Nota

O índice GNU mostra [como palavra reservada, no entanto, é um comando interno de fato.[[pelo contrário, é uma palavra reservada.

Consulte também: Diferenças entre palavra-chave, palavra reservada e incorporada?

Sergiy Kolodyazhnyy
fonte