¿Por qué el signo de exclamación `!` A veces molesta a bash?

14

Me doy cuenta de que !tiene un significado especial en la línea de comandos en el contexto del historial de la línea de comandos, pero aparte de eso, en un script de ejecución, el signo de exclamación a veces puede causar un error de análisis.
Creo que tiene algo que ver con un event, pero no tengo idea de qué es un evento o qué hace. Aun así, el mismo comando puede comportarse de manera diferente en diferentes situaciones.
El último ejemplo, a continuación, causa un error; pero ¿por qué, cuando el mismo código funcionó fuera de la sustitución del comando? .. utilizando 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
fuente
@ Warren .. Gracias. Había visto ese control de calidad, pero en realidad solo habla de cómo escapar de la barra diagonal inversa ... Mi problema se relaciona más con por qué el código aparentemente ya escapado funciona en una situación y no en otra ...
Peter.O
@fred: "aparentemente ya escapó"? No veo ningún escape en absoluto y estás usando comillas dobles. Vea mi respuesta (revisada). ¿Qué parte crees que se escapó?
Caleb
@Caleb. Sí, usé el término incorrecto ... protectedhubiera sido más apropiado. (protegido por 'comillas simples')
Peter
Si solo le preocupan las tareas simples, puede usar var=$(…)(sin comillas dobles), y funcionará como (creo) que espera. Esto sigue siendo “seguro” porque la parte del valor de una asignación simple no está sujeta a la división de palabras o comodines (aunque esto puede no ser cierto de las asignaciones hechas a través de órdenes internas (por ejemplo export, local, etc.) en todas las conchas). Desafortunadamente, esto no se extiende más allá de las tareas simples, ya que las comillas dobles son la forma de protegerse contra la división y el bloqueo de palabras mientras se obtienen otros tipos de expansión en otros contextos.
Chris Johnsen

Respuestas:

12

El !personaje invoca la sustitución de la historia de bash. Cuando lo sigue una cadena (como en su ejemplo de error) intenta expandirse al último evento del historial que comenzó con esa cadena. Al igual que $varse expande al valor de esa cadena, !echose expandiría al último comando de eco en su historial.

El espacio es un personaje rompedor en tales expansiones. Primero observe cómo funcionaría esto con las variables:

# 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

Lo mismo sucederá con la expansión de la historia. El carácter de explosión ( !) comienza una secuencia de reemplazo del historial, pero solo si es seguido por una cadena. Seguirlo con un espacio lo convierte en una explosión literal en lugar de parte de una secuencia de reemplazo.

Puede evitar este tipo de reemplazo para la expansión de variables e historial utilizando comillas simples. Sus primeros ejemplos utilizaron comillas simples y, por lo tanto, funcionaron bien. Sus últimos ejemplos están entre comillas dobles y, por lo tanto, bash los escaneó en busca de secuencias de expansión antes de hacer cualquier otra cosa. La única razón por la que el primero no se disparó es porque el espacio es un carácter de ruptura como se muestra arriba.

Caleb
fuente
Gracias Caleb ... Otra de mis preconcepciones descartadas ... Pensé que el análisis bash se realizó desde el corchete o la abrazadera más interno, y luego funcionó hacia afuera ... Parece que bash analiza de manera diferente a mi suposición.
Peter.O
1
Citar es bastante confuso en bash sin que cambie en cadenas anidadas. Tal como están las cosas, los reemplazos ocurren muy temprano en el proceso. Considere este ejemplo:var=word; echo "test '$var'"; echo 'test "$var"'
Caleb
.. Sí, no entendido. Estaba al tanto de las comillas anidadas entre comillas ... Mi malentendido fue que pensé que el código dentro de los corchetes de la sustitución de comandos se analizaría por separado, según lo que rodea esos corchetes; pero aparentemente no .. gracias.
Peter.O
6

Como ya dijo Caleb , !se usa para invocar la sustitución de la historia de bash.

Si, como yo, siente que no necesita dicha función, puede deshabilitarla insertando la siguiente línea en ~/.bashrc:

set +H

Yo no lo necesito porque la historia se puede recuperar mediante la flecha hacia arriba y Ctrl- rbúsqueda hacia atrás incrementales. Consulte la página del manual de bash, sección Comandos para manipular el historial para obtener una lista detallada de accesos directos.

enzotib
fuente
2
¿Cómo vives sin él !!?
Caleb
Gracias. Creo que podría ser un problema, en cuanto a la portabilidad, pero usarlo set +Hen el script funciona igual de bien :) +1
Peter.O
2
@fred: extraño, en general, la expansión del historial está "activada" solo para shells interactivos.
enzotib
@enzo ... Gracias de nuevo ... Lo había probado desde la línea de comandos ... ¡Ah! Si aprender no fuera tan divertido, sería tedioso ... ¿mencioné el café? también ayuda :)
Peter.O
Ya, eso es una trampa. Copié el código pegado de los scripts que fallaron en la línea de comando solo por esta razón. La expansión de la historia no era una preocupación en el script, pero está en un shell interactivo.
Caleb
2

tu primer ejemplo:

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

podría reducirse a

echo '! p' 
echo '!p'

Entre comillas simples, todos los caracteres conservan sus valores literales. Por lo tanto, !ha perdido su significado especial y la expansión de la historia no se realiza.

su segundo y tercer ejemplo:

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

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

podría reducirse a

echo "'! p'"

echo "'!p'"

'! p'y '!p'son esencialmente partes de las cadenas entre comillas dobles.

Entre comillas dobles, todos los personajes conservan sus valores literales Excepto $ , `, \y !.

Eso implica las comillas simples de '! p'y '!p'ha perdido sus significados especiales (es decir: no puede escapar !) pero !aún conserva su significado especial, por lo que se realiza la expansión de la historia.

Sin embargo, cuando !le sigue un carácter de espacio, no se realiza la expansión del historial.

Citando de man bash:

CITA

[...]

El encerrar caracteres entre comillas simples conserva el valor literal de cada carácter dentro de las comillas. [...]

El encerrar caracteres entre comillas dobles conserva el valor literal de todos los caracteres dentro de las comillas, con la excepción de $, `, \ y, cuando la expansión del historial está habilitada,!. [...] Si está habilitado, la expansión del historial se realizará a menos que un! aparecer entre comillas dobles se escapa mediante una barra invertida. La barra diagonal inversa que precede a! No se elimina.

EXPANSIÓN DE HISTORIA

[...]

Las expansiones de historia se introducen por la aparición del personaje de expansión de historia, que es! por defecto. Solo la barra invertida (\) y las comillas simples pueden citar el carácter de expansión del historial.

Varios caracteres inhiben la expansión del historial si se encuentran inmediatamente después del carácter de expansión del historial, incluso si no está entre comillas: espacio, tabulación, nueva línea, retorno de carro y =. Si la opción de shell extglob está habilitada, (también inhibirá la expansión.

cychoi
fuente