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.
Como hacer esto usando sed
es más simple, la primera respuesta usará sed .
Vea a continuación la solución pure bash .
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
:;$!{N;b};
), que end-of-line exit
por
. ( s/^\(.*\n\)*\(.*\)\n\2exit$/\2/
).
convierte en \1
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
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.
PROMPT_COMMAND
…