Por que o ponto de exclamação `!` Às vezes incomoda o bash?

13

Sei que isso !tem um significado especial na linha de comando no contexto do histórico da linha de comando, mas fora isso, em um script em execução, o ponto de exclamação às vezes pode causar um erro de análise.
Eu acho que tem algo a ver com um event, mas não tenho idéia do que é um evento ou do que ele faz. Mesmo assim, o mesmo comando pode se comportar de maneira diferente em situações diferentes.
O último exemplo, abaixo, causa um erro; mas por que quando o mesmo código funcionou fora da substituição de comando? .. usando GNU bash 4.1.5

# This works, with or without a space between ! and p
  { echo -e "foo\nbar" | sed -nre '/foo/! p'
    echo -e "foo\nbar" | sed -nre '/foo/!p'; }
# bar
# bar

# This works, works when there is a space between ! and p
  var="$(echo -e "foo\nbar" | sed -nre '/foo/! p')"; echo "$var"
# bar

# This causes an ERROR, with NO space between ! and p
  var="$(echo -e "foo\nbar" | sed -nre '/foo/!p')"; echo "$var"
# bash: !p': event not found
Peter.O
fonte
@ Warren .. Obrigado. Eu já tinha visto que QA, mas realmente só fala sobre como escapar a barra invertida ... Meu problema se relaciona mais de porque o aparentemente código já escapou funciona em uma situação e não outro ...
Peter.O
@ Fred: "aparentemente já escapou"? Não vejo escapes e você está usando aspas duplas. Veja minha resposta (revisada). Que parte você acha que escapou?
Caleb
@Caleb. Sim, usei o termo errado .. protectedteria sido mais apropriado. (protegido por 'aspas simples')
Peter.O
Se você se preocupa apenas com tarefas simples, pode usar var=$(…)(sem aspas duplas) e funcionará como (eu acho) que você espera. Este é ainda “seguro” porque a parte do valor de uma atribuição simples não está sujeito à repartição de palavras ou englobamento (embora isso não pode ser verdade de atribuições feitas através builtins (por exemplo export, local, etc.) em todas as conchas). Infelizmente, isso não se estende para além de atribuições simples, pois as aspas duplas são a maneira de se proteger contra a divisão de palavras e globbing, enquanto ainda se obtém outros tipos de expansão em outros contextos.
precisa

Respostas:

11

O !personagem invoca a substituição do histórico do bash. Quando seguido por uma sequência (como no exemplo que falhou), ele tenta expandir para o último evento do histórico iniciado com essa sequência. Assim como $varé expandido para o valor dessa string, !echoseria expandido para o último comando de eco em seu histórico.

O espaço é um personagem de ruptura em tais expansões. Primeira nota como isso funcionaria com variáveis:

# var="like"
# echo "$var"
like
# echo "$"
$
# echo "Do you $var frogs?"
Do you like frogs?       <- as expected, variable name broken at space
# echo "Do you $varfrogs?"
Do you?                  <- $varfrogs not defined, replaced with blank
# echo "Do you $ var frogs?"
Do you $ var frogs?      <- $ not a valid variable name, ignored

O mesmo acontecerá com a expansão da história. O caractere de estrondo ( !) inicia uma sequência de substituição do histórico, mas apenas se for seguido por uma sequência. Seguindo-o com um espaço, torne-o literal bang, em vez de parte de uma sequência de substituição.

Você pode evitar esse tipo de substituição para expansão de variável e histórico usando aspas simples. Seus primeiros exemplos usavam aspas simples e, portanto, funcionavam bem. Seus últimos exemplos estão entre aspas duplas e, portanto, o bash os examinou em busca de sequências de expansão antes de fazer qualquer outra coisa. A única razão pela qual o primeiro não tropeçou é que o espaço é um caractere de interrupção, como mostrado acima.

Caleb
fonte
Obrigado Caleb .. Outra das minhas pré-concepções descartadas ... Eu pensei que a análise do bash era feita a partir do suporte interno, e depois trabalhava para fora ... Parece que o bash analisa de maneira diferente da minha suposição.
precisa saber é o seguinte
1
A citação é bastante confusa no bash sem que seja alterada nas cadeias aninhadas. No momento, as substituições acontecem muito cedo no processo. Considere este exemplo:var=word; echo "test '$var'"; echo 'test "$var"'
Caleb
.. sim undestood. Eu estava ciente de aninhar aspas entre aspas ... Meu mal-entendido era que eu pensava que o código entre parênteses da substituição de comando seria analisado separadamente, para o que os rodeia; mas aparentemente não .. obrigado.
Peter.O 23/08/11
6

Como já foi dito por Caleb , !é usado para invocar a substituição do histórico do bash.

Se, como eu, achar que não precisa desse recurso, desative-o inserindo a seguinte linha ~/.bashrc:

set +H

Não preciso disso porque o histórico pode ser recuperado pela seta para cima e Ctrl- rpesquisa reversa incremental. Consulte a página de manual do bash, seção Comandos para manipular o histórico para obter uma lista detalhada de atalhos.

enzotib
fonte
2
Como você mora sem !!?
Caleb
. Obrigado, eu acho que poderia ser um problema, portabilidade-wise, mas utilizando set +Hno script funciona tão bem :) +1
Peter.O
2
@ Fred: estranho, geralmente a expansão do histórico é "on" apenas para shells interativos.
enzotib
@enzo .. Mais uma vez obrigado .. Eu tinha testado a partir da linha de comando .. Ah! Se aprender não fosse tão divertido, seria entediante ... eu mencionei café? ele ajuda também :)
Peter.O
Ya, isso é uma pegadinha. Copiei o código colado de scripts que falharam na linha de comando por esse motivo. A expansão do histórico não era uma preocupação no script, mas está em um shell interativo.
Caleb
2

seu primeiro exemplo:

{ echo -e "foo\nbar" | sed -nre '/foo/! p'
    echo -e "foo\nbar" | sed -nre '/foo/!p'; }

pode ser reduzido para

echo '! p' 
echo '!p'

Dentro de aspas simples, todos os caracteres preservam seus valores literais. Assim !, perdeu seu significado especial e a expansão da história não é pré-formada.

seu segundo e terceiro exemplos:

var="$(echo -e "foo\nbar" | sed -nre '/foo/! p')"; echo "$var"

var="$(echo -e "foo\nbar" | sed -nre '/foo/!p')"; echo "$var"

pode ser reduzido para

echo "'! p'"

echo "'!p'"

'! p'e '!p'são essencialmente partes das cadeias de caracteres duplas.

Dentro de aspas duplas, todos os caracteres preservar seus valores literais , exceto $ , `, \e !.

Isso implica aspas simples '! p'e '!p'perdeu seus significados especiais (isto é, incapaz de escapar !), mas !ainda mantém seu significado especial, portanto, a expansão da história é realizada.

No entanto, quando !é seguido por um caractere de espaço, a expansão do histórico não é executada.

Citação de man bash:

CITAÇÕES

[...]

Incluir caracteres entre aspas simples preserva o valor literal de cada caractere dentro das aspas. [...]

A inclusão de caracteres entre aspas duplas preserva o valor literal de todos os caracteres entre aspas, com exceção de $, `, \ e, quando a expansão do histórico está ativada,!. [...] Se ativado, a expansão do histórico será realizada, a menos que um! aparecer entre aspas duplas é escapado usando uma barra invertida. A barra invertida que precede o! não é removido.

EXPANSÃO HISTÓRICA

[...]

As expansões do histórico são introduzidas pela aparência do caractere de expansão do histórico, que é! por padrão. Apenas barra invertida (\) e aspas simples podem citar o caractere de expansão do histórico.

Vários caracteres inibem a expansão do histórico se forem encontrados imediatamente após o caractere de expansão do histórico, mesmo se não estiver entre aspas: espaço, tabulação, nova linha, retorno de carro e =. Se a opção extglob shell estiver ativada, (também inibirá a expansão.

cychoi
fonte