Eco expandido PS1

Tengo un script de shell que ejecuta el mismo comando en varios directorios ( fgit ). Para cada directorio, me gustaría que muestre el prompt actual + el comando que se ejecutará allí. ¿Cómo obtengo la cadena que corresponde a la PS1 descodificada (expandida)? Por ejemplo, mi PS1 predeterminada es

 ${debian_chroot:+($debian_chroot)}\[\e[1;32m\]\u\[\e[0m\]@\[\e[1;32m\]\h\[\e[0m\]:\[\e[1;34m\]\w\[\e[0m\]$(__git_ps1 ' (%s)')$ 

y me gustaría repetir el prompt resultante username@hostname:/path$ , preferiblemente (pero no necesariamente) con los bonitos colores. Una mirada superficial al manual de Bash no reveló ninguna respuesta definitiva, y echo -e $PS1 solo evalúa los colores.

Una gran ventaja del software de código abierto es que la fuente está, bueno, abierta 🙂

Bash en sí mismo no proporciona esta funcionalidad, pero hay varios trucos que puede usar para proporcionar un subconjunto (como sustituir \u con $USER etc.). Sin embargo, esto requiere mucha duplicación de funcionalidad y garantizar que el código se mantenga sincronizado con lo que haga bash en el futuro.

Si quieres obtener toda la potencia de las variables rápidas (y no te importa ensuciarte las manos con un poco de encoding (y, si te importa, ¿por qué estás aquí?)), Es bastante fácil de agregar a la Concha en sí.

Si descarga el código para bash (estoy buscando en la versión 4.2), hay un archivo y.tab.c que contiene la función decode_prompt_string() :

 char *decode_prompt_string (string) char *string; { ... } 

Esta es la función que evalúa las variables PSx para PSx . Para permitir que esta funcionalidad se proporcione a los usuarios del propio shell (en lugar de solo usarla en el shell), puede seguir estos pasos para agregar un comando interno evalps1 .

Primero, cambie support/mkversion.sh para que no lo confunda con un bash “real”, y para que la FSF pueda denegar todo conocimiento a efectos de garantía 🙂 Simplemente cambie una línea (agregué el bit -pax ) :

 echo "#define DISTVERSION \"${float_dist}-pax\"" 

En segundo lugar, cambie `builtins / Makefile.in para agregar un nuevo archivo fuente. Esto implica una serie de pasos.

(a) Agregue $(srcdir)/evalps1.def al final de DEFSRC .

(b) Agregue evalps1.o al final de OFILES .

(c) Agregue las dependencias requeridas:

 evalps1.o: evalps1.def $(topdir)/bashtypes.h $(topdir)/config.h \ $(topdir)/bashintl.h $(topdir)/shell.h common.h 

En tercer lugar, agregue el archivo builtins/evalps1.def sí mismo, este es el código que se ejecuta cuando ejecuta el comando evalps1 :

 This file is evalps1.def, from which is created evalps1.c. It implements the builtin "evalps1" in Bash. Copyright (C) 1987-2009 Free Software Foundation, Inc. This file is part of GNU Bash, the Bourne Again SHell. Bash is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Bash is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Bash. If not, see . $PRODUCES evalps1.c $BUILTIN evalps1 $FUNCTION evalps1_builtin $SHORT_DOC evalps1 Outputs the fully interpreted PS1 prompt. Outputs the PS1 prompt, fully evaluated, for whatever nefarious purposes you require. $END #include  #include "../bashtypes.h" #include  #include "../bashintl.h" #include "../shell.h" #include "common.h" int evalps1_builtin (list) WORD_LIST *list; { char *ps1 = get_string_value ("PS1"); if (ps1 != 0) { ps1 = decode_prompt_string (ps1); if (ps1 != 0) { printf ("%s", ps1); } } return 0; } 

La mayor parte de eso es la licencia GPL (ya que la modifiqué de exit.def ) con una función muy simple al final para obtener y decodificar PS1 .

Por último, solo crea la cosa en el directorio de nivel superior:

 ./configure make 

El ejecutable bash que aparece puede cambiarse a paxsh , aunque dudo que llegue a ser tan frecuente como su antecesor 🙂

Y al ejecutarlo, puedes verlo en acción:

 pax> mv bash paxsh pax> ./paxsh --version GNU bash, version 4.2-pax.0(1)-release (i686-pc-linux-gnu) Copyright (C) 2011 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later  This is free software; you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. pax> ./paxsh pax> echo $BASH_VERSION 4.2-pax.0(1)-release pax> echo "[$PS1]" [pax> ] pax> echo "[$(evalps1)]" [pax> ] pax> PS1="\h: " paxbox01: echo "[$PS1]" [\h: ] paxbox01: echo "[$(evalps1)]" [paxbox01: ] 

Cuando coloca una de las variables PSx en el prompt, echoing $PS1 simplemente le da la variable, mientras que el comando evalps1 evalúa y genera el resultado.

Ahora, concedido, hacer cambios de código en bash para agregar un comando interno puede ser considerado por muchos como excesivo, pero, si quieres una evaluación perfecta de la PS1 , sin duda es una opción.

Desde Bash 4.4 puedes usar la expansión @P :

Primero coloco su cadena de solicitud en una variable myprompt usando read -r y una cita aquí-doc:

 read -r myprompt <<'EOF' ${debian_chroot:+($debian_chroot)}\[\e[1;32m\]\u\[\e[0m\]@\[\e[1;32m\]\h\[\e[0m\]:\[\e[1;34m\]\w\[\e[0m\]$(__git_ps1 ' (%s)')$ EOF 

Para imprimir el mensaje (como se interpretaría si fuera PS1 ), use la expansión ${myprompt@P} :

 $ printf '%s\n' "${myprompt@P}" gniourf@rainbow:~$ $ 

(De hecho, hay algunos \001 y \002 caracteres, que provienen de \[ y \] que no puedes ver aquí, pero puedes verlos si tratas de editar esta publicación; también los verás en tu terminal si escribe los comandos).


Para deshacerse de estos, el truco enviado por Dennis Williamson en la lista de correo de bash es usar read -e -p para que estos caracteres sean interpretados por la biblioteca readline:

 read -e -p "${myprompt@P}" 

Esto pedirá al usuario que interprete correctamente el myprompt .

Para esta publicación, Greg Wooledge respondió que también podría simplemente quitar el \001 y \002 de la cadena. Esto se puede lograr así:

 myprompt=${myprompt@P} printf '%s\n' "${myprompt//[$'\001'$'\002']}" 

A esta publicación, Chet Ramey respondió que también se puede desactivar la edición de líneas con set +o emacs +o vi . Entonces esto también lo hará

 ( set +o emacs +o vi; printf '%s\n' "${myprompt@P}" ) 

¿Por qué no $PS1 sustituciones de escape de $PS1 tú mismo? Una serie de sustituciones como estas:

 p="${PS1//\\u/$USER}"; p="${p//\\h/$HOSTNAME}" 

Por cierto, zsh tiene la capacidad de interpretar escapes rápidos.

 print -P '%n@%m %d' 

o

 p=${(%%)PS1} 

Me gusta la idea de arreglar Bash para hacerlo mejor, y aprecio la respuesta detallada de paxdiablo sobre cómo aplicar parches a Bash. Voy a ir en algún momento.

Sin embargo, sin parchar el código fuente de Bash, tengo un hack de una sola línea que es portátil y no duplica la funcionalidad, porque la solución alternativa usa solo Bash y sus construcciones internas.

 x="$(PS1=\"$PS1\" echo -n | bash --norc -i 2>&1)"; echo "'${x%exit}'" 

Tenga en cuenta que está pasando algo extraño con tty ‘s y stdio ya que esto también funciona:

 x="$(PS1=\"$PS1\" echo -n | bash --norc -i 2>&1 > /dev/null)"; echo "'${x%exit}'" 

Entonces, aunque no entiendo qué está pasando con el stdio aquí, mi truco está funcionando para mí en Bash 4.2, NixOS GNU / Linux. Parchar el código fuente de Bash definitivamente es una solución más elegante, y debería ser bastante fácil y seguro hacerlo ahora que estoy usando Nix.

Dos respuestas: “Pure bash” y “bash + sed”

Como hacer esto usando sed es más simple, la primera respuesta usará sed .

Vea a continuación la solución pure bash .

bash prompt expansion, bash + sed

Hay mi truco:

 ExpPS1="$(bash --rcfile <(echo "PS1='$PS1'") -i <<<'' 2>&1 | sed ':;$!{N;b};s/^\(.*\n\)*\(.*\)\n\2exit$/\2/p;d')" 

Explicación:

Ejecutando bash --rcfile <(echo "PS1='$PS1'") -i <<<'' 2>&1

Puede devolver algo como:

 To run a command as administrator (user "root"), use "sudo ". See "man sudo_root" for details. ubuntu@ubuntu:~$ ubuntu@ubuntu:~$ exit 

El comando sed entonces

  • tomar todas las líneas en un buffer ( :;$!{N;b}; ), que
  • reemplace end-of-lineexit por . ( s/^\(.*\n\)*\(.*\)\n\2exit$/\2/ ).
    • donde convierte en \1
    • y convertirse en \2 .

Caso de prueba:

 while ExpPS1="$(bash --rcfile <(echo "PS1='$PS1'") -i <<<'' 2>&1 | sed ':;$!{N;b};s/^\(.*\n\)*\(.*\)\n\2exit$/\2/p;d')" read -rp "$ExpPS1" && [ "$REPLY" != exit ] ;do eval "$REPLY" done 

A partir de ahí, estás en una especie de shell pseudo interactivo (sin recursos de lectura, pero eso no importa) …

 ubuntu@ubuntu:~$ cd /tmp ubuntu@ubuntu:/tmp$ PS1="${debian_chroot:+($debian_chroot)}\[\e[1;32m\]\u\[\e[0m\]@\[\e[1;32m\]\h\[\e[0m\]:\[\e[1;34m\]\w\[\e[0m\]$ " ubuntu@ubuntu:/tmp$ 

(Última línea imprime tanto ubuntu en verde, @ ubuntu y $ en negro y ruta ( /tmp ) en azul)

 ubuntu@ubuntu:/tmp$ exit ubuntu@ubuntu:/tmp$ od -A n -tc <<< $ExpPS1 033 [ 1 ; 3 2 mubuntu 033 [ 0 m @ 033 [ 1 ; 3 2 mubuntu 033 [ 0 m : 033 [ 1 ; 3 4 m ~ 033 [ 0 m $ \n 

Bash puro

 ExpPS1="$(bash --rcfile <(echo "PS1='$PS1'") -i <<<'' 2>&1)" ExpPS1_W="${ExpPS1%exit}" ExpPS1="${ExpPS1_W##*$'\n'}" ExpPS1_L=${ExpPS1_W%$'\n'$ExpPS1} while [ "${ExpPS1_W%$'\n'$ExpPS1}" = "$ExpPS1_W" ] || [ "${ExpPS1_L%$'\n'$ExpPS1}" = "$ExpPS1_L" ] ;do ExpPS1_P="${ExpPS1_L##*$'\n'}" ExpPS1_L=${ExpPS1_L%$'\n'$ExpPS1_P} ExpPS1="$ExpPS1_P"$'\n'"$ExpPS1" done 

Se requiere el ciclo while para garantizar el manejo correcto de las solicitudes de línea múltiple:

reemplace la 1ra línea por:

 ExpPS1="$(bash --rcfile <(echo "PS1='${debian_chroot:+($debian_chroot)}\[\e[1;32m\]\u\[\e[0m\]@\[\e[1;32m\]\h\[\e[0m\]:\[\e[1;34m\]\w\[\e[0m\]$ '") -i <<<'' 2>&1)" 

o

 ExpPS1="$(bash --rcfile <(echo "PS1='Test string\n$(date)\n$PS1'") -i <<<'' 2>&1)"; 

La última multilínea se imprimirá:

 echo "$ExpPS1" Test string Tue May 10 11:04:54 UTC 2016 ubuntu@ubuntu:~$ od -A n -tc <<<${ExpPS1} T eststring \r T ue M ay 1 0 1 1 : 0 4 : 5 4 UTC 2 0 1 6 \r 033 ] 0 ; u buntu @ ubuntu : ~ \a ubuntu @ ubuntu : ~ $ \n 

Puede que tenga que escribir un pequeño progtwig en C que use el mismo código que bash (¿es una llamada a la biblioteca?) Para mostrar ese aviso, y simplemente llame al progtwig C. Por supuesto, eso no es muy portátil, ya que tendrás que comstackrlo en cada plataforma, pero es una solución posible.

Una posibilidad más: sin editar el código fuente de bash, usando la utilidad de script (parte del paquete bsdutils en ubuntu):

 $ TEST_PS1="\e[31;1m\u@\h:\n\e[0;1m\$ \e[0m" $ RANDOM_STRING=some_random_string_here_that_is_not_part_of_PS1 $ script /dev/null <<-EOF | awk 'NR==2' RS=$RANDOM_STRING PS1="$TEST_PS1"; HISTFILE=/dev/null echo -n $RANDOM_STRING echo -n $RANDOM_STRING exit EOF  

script comando script genera un archivo especificado y la salida también se muestra en stdout. Si se omite el nombre de archivo, genera un archivo llamado typescript.

Como en este caso no estamos interesados ​​en el archivo de registro, el nombre de archivo se especifica como /dev/null . En cambio, la salida estándar del comando de script se pasa a awk para su posterior procesamiento.

  1. El código completo también se puede encapsular en una función.
  2. Además, la solicitud de salida también se puede asignar a una variable.
  3. Este enfoque también admite el análisis de PROMPT_COMMAND