Cómo solucionar el error "bash:!d':evento no encontrado” en la sustitución del comando Bash [duplicado]

StackOverflow https://stackoverflow.com//questions/25003162

Pregunta

Estoy intentando analizar el resultado de un evento de inicio del servidor VNC y me encontré con un problema al analizar el uso de sed en una sustitución de comando.Específicamente, el servidor VNC remoto se inicia de una manera como la siguiente:

address1="user1@lxplus.cern.ch"
VNCServerResponse="$(ssh "${address1}" 'vncserver' 2>&1)"

Luego, se analizará la salida de error estándar producida en este evento de inicio para extraer el servidor y mostrar información.En este punto el contenido de la variable VNCServerResponse es algo como lo siguiente:

New 'lxplus0186.cern.ch:1 (user1)' desktop is lxplus0186.cern.ch:1

Starting applications specified in /afs/cern.ch/user/u/user1/.vnc/xstartup
Log file is /afs/cern.ch/user/u/user1/.vnc/lxplus0186.cern.ch:1.log

Esta salida se puede analizar de la siguiente manera para extraer el servidor y mostrar información:

echo "${VNCServerResponse}" | sed '/New.*desktop.*is/!d' \
    | awk -F" desktop is " '{print $2}'

El resultado es algo como lo siguiente:

lxplus0186.cern.ch:1

Lo que quiero hacer es usar este análisis en una sustitución de comando similar a lo siguiente:

VNCServerAndDisplayNumber="$(echo "${VNCServerResponse}" \
    | sed '/New.*desktop.*is/!d' | awk -F" desktop is " '{print $2}')"

Al intentar hacer esto, aparece el siguiente error:

bash: !d': event not found

No estoy seguro de cómo abordar esto.Parece haber un problema en la forma en que se utiliza sed en la sustitución de comandos.Agradecería orientación.

¿Fue útil?

Solución

El problema es que entre comillas dobles, bash intenta expandirse !d antes de pasarlo a la subcapa.Puedes solucionar este problema eliminando las comillas dobles, pero también propondría una simplificación de tu script:

VNCServerAndDisplayNumber=$(echo "$VNCServerResponse" | awk '/desktop/ {print $NF}')

Esto simplemente imprime el último campo de la línea que contiene la palabra "escritorio".

En un bash más nuevo, puedes usar un herestring en lugar de canalizar un echo:

VNCServerAndDisplayNumber=$(awk '/desktop/ {print $NF}' <<<"$VNCServerResponse")

Otros consejos

La expansión del historial de Bash es un rincón muy extraño en el analizador de la línea de comandos de Bash, y claramente se está topando con una expansión del historial inesperada, que se explica a continuación.Sin embargo, cualquier tipo de expansión del historial en un script es inesperada, porque normalmente la expansión del historial no está habilitada en los scripts;ni siquiera los scripts se ejecutan con el source (o .) incorporado.

Cómo se habilita (o deshabilita) la expansión del historial

Hay dos opciones de shell que controlan la expansión del historial:

  • set -o history:Requerido para que se registre el historial.

  • set -H (o set -o histexpand):También es necesario para habilitar la expansión del historial.

Se deben configurar ambas opciones para que se reconozca la expansión de la historia.(El manual me pareció poco claro sobre esta interacción, pero es bastante lógico).

Según el manual de bash, estas opciones no están configuradas para shells no interactivos, por lo que si desea habilitar la expansión del historial en un script (y no puedo imaginar una razón por la que desearía esto), deberá configurar ambas:

set -o history -o histexpand

La situación de los scripts ejecutados con source es más complicado (y lo que voy a decir solo se aplica a bash v4, y dado que no está documentado podría cambiar en el futuro).[Nota 3]

La grabación del historial (y, en consecuencia, la expansión) está desactivada en source'd scripts, pero a través de una bandera interna que, hasta donde yo sé, no se hace visible.Ciertamente no aparece en $SHELLOPTS.Desde un sourced script se ejecuta en el contexto bash actual, comparte el entorno de ejecución actual, incluidas las opciones de shell.Así, en la ejecución de un sourced script iniciado desde una sesión interactiva, verá ambos history y histexpand en $SHELLOPTS, pero no se llevará a cabo ninguna expansión de la historia.Para habilitarlo, necesita:

set -o history

lo cual no es una operación no operativa porque tiene el efecto secundario de restablecer el indicador interno que suprime la grabación del historial.Configurando el histexpand La opción Shell no tiene este efecto secundario.

En resumen, no estoy seguro de cómo logró habilitar la expansión del historial en un script (si, de hecho, el comando que se comportaba mal estaba en un script y no en un shell interactivo), pero es posible que desee considerar no hacerlo, a menos que tener una muy buena razón.

Cómo se analiza la expansión histórica

La implementación bash de la expansión histórica está diseñada para funcionar con readline, para que pueda realizarse durante la entrada del comando.(Por defecto esta función está vinculada a Meta-^;generalmente Meta es ESC, pero también puedes personalizarlo). Sin embargo, también se realiza inmediatamente después de ingresar cada línea, antes de realizar cualquier análisis de bash.

De forma predeterminada, el carácter de expansión del historial es !, y, como está mayoritariamente documentado, eso desencadenará la expansión de la historia, excepto:

  1. cuando va seguido de espacios en blanco o =

  2. si la opción shell extglob está establecido, y es seguido por ( [Nota 1]

  3. si aparece en una cadena entre comillas simples

  4. si va precedido de un \ [Nota 2 y ver más abajo]

  5. si está precedido por $ o ${ [Nota 1]

  6. si está precedido por [ [Nota 1]

  7. (A partir de bash v4.3) si es el último carácter de una cadena entre comillas dobles.

La cuestión inmediata aquí es la interpretación precisa del tercer caso, un ! que aparece dentro de una cadena entre comillas simples.Normalmente, bash inicia un nuevo contexto de cita para una sustitución de comando ($(...) o la notación de acento grave obsoleta).Por ejemplo:

$ s=SUBSTITUTED
$ # The interior single quotes are just characters
$ echo "'Echoing $s'"
'Echoing SUBSTITUTED'
$ # The interior single quotes are single quotes
$ echo "$(echo 'Echoing $s')"
Echoing $s

Sin embargo, el escáner de expansión del historial no es tan inteligente.Realiza un seguimiento de las comillas, pero no de la sustitución de comandos.En lo que a él respecta, las dos comillas simples del ejemplo anterior son comillas simples dobles, es decir, caracteres ordinarios.Entonces la expansión de la historia ocurre en ambos:

# A no-op to indicated history expansion
$ HIST() { :; }
# Single-quoted strings inhibit history expansion
$ HIST
$ echo '!!'
!!
# Double-quoted strings allow history expansion
$ HIST
$ echo "'!!'"
echo "'HIST'"
'HIST'
# ... and it applies also to interior command substitution.
$ HIST
$ echo "$(echo '!!')"
echo "$(echo 'HIST')"
HIST

Entonces, si tienes un comando perfectamente normal como sed '/foo/!d' file, donde esperarías que las comillas simples te protejan de la expansión del historial, y lo colocas dentro de una sustitución de comando entre comillas dobles:

result="$(sed '/foo/!d' file)"

de repente descubres que el ! es un personaje de expansión de la historia.Peor aún, no puedes solucionar este problema con una barra invertida que escape del signo de exclamación, porque aunque "\!" inhibe la expansión del historial, no elimina la barra invertida:

$ echo "\!"
\!

En este ejemplo particular, y en el del OP, las comillas dobles son completamente innecesarias, porque el lado derecho de una asignación de variable no sufre expansión de nombre de archivo ni división de palabras.Sin embargo, hay otros contextos en los que eliminar las comillas dobles cambiaría la semántica:

# Undesired history expansion
printf "The answer is '%s'\n" "$(sed '/foo/!d' file)"

# Undesired word splitting
printf "The answer is '%s'\n" $(sed '/foo/!d' file)

En este caso, la mejor solución probablemente sea poner el argumento sed en una variable

# Works
sed_prog='/foo/!d'
printf "The answer is '%s'\n" "$(sed "$sed_prog" file)"

(Las comillas alrededor de $sed_prog no eran necesarias en este caso, pero normalmente lo serían y no causan ningún daño).


Notas:

  1. La inhibición de la expansión del historial cuando el siguiente carácter es alguna forma de paréntesis abierto solo funciona si hay un paréntesis de cierre correspondiente en el resto de la cadena.Sin embargo, no es necesario que coincida realmente con el paréntesis abierto.Por ejemplo:

    # No matching close parenthesis
    $ echo "!("
    bash: !: event not found
    # The matching close parenthesis has nothing to do with the open
    $ echo "!(" ")"
    !( )
    # An actual extended glob: files whose names don't start with a
    $ echo "!(a*)"
    b
    
  2. Como se indica en el bash En este manual, un carácter de expansión del historial se trata como un carácter normal si está inmediatamente precedido por una barra invertida.Esto es literalmente cierto;no importa si la barra invertida se considerará posteriormente un carácter de escape o no:

    $ echo \!
    !
    $ echo \\!
    \!
    $ echo \\\!
    \!
    

    \ también inhibe la expansión del historial entre comillas dobles, pero \! no es una secuencia de escape válida dentro de la cadena entre comillas dobles, por lo que la barra invertida no se elimina:

    $ echo "\!"
    \!
    $ echo "\\!"
    \!
    $ echo "\\\!"
    \\!
    
  3. Me refiero al código fuente de bash v4.2 mientras escribo esto, por lo que cualquier comportamiento no documentado puede ser completamente diferente a partir de v4.3.

No envuelvas el $(...) sustitución de comandos entre comillas dobles.Le está pidiendo al shell que realice una evaluación del contenido de las citas y está accediendo a la función de expansión de sustitución del historial.Elimina las comillas y dejarás de decirle al shell que haga eso y no encontrarás ese problema.

Y , eliminar esas comillas es seguro en esa línea de asignación incluso si el resultado puede contener espacios o nuevas líneas o lo que sea.Las asignaciones de ese tipo no se dividirán en aquellas de la misma manera que lo harán la sustitución de comandos o la evaluación de variables en una línea de ejecución de shell normal.

Alternativamente, deshabilite la expansión del historial en su shell/script antes de ejecutarlo.(De todos modos, creo que debería estar desactivado cuando se ejecuta un script de forma predeterminada).

Esto solo sucede cuando la expansión del historial está habilitada, lo cual normalmente no es así y definitivamente no debería serlo para los scripts.

En lugar de intentar solucionarlo, averigüe por qué la expansión del historial está habilitada y qué hacer para que no lo esté.

  1. Si está ejecutando su script con . foo o source foo, usar ./foo en cambio.

  2. Si estás escribiendo esto como una función en .bashrc o similar, considere convertirlo en un guión separado.

  3. Si su script (o BASH_ENV) lo hace explícitamente set -H, no.

Cítelo con '' o \ o deshabilitar la expansión del historial con set +H o shopt -u -o histexpand.Ver Expansión de la historia.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top