How to expand PS1?
I have a shell script that runs the same command in several directories (fgit). For each directory, I would like it to show the current prompt + the command 开发者_如何学Gowhich will be run there. How do I get the string that corresponds to the decoded (expanded)PS1
? For example, my default PS1 is
${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)')$
and I'd like to echo the resulting prompt username@hostname:/path$
, preferably (but not necessarily) with the nice colors. A cursory look at the Bash manual didn't reveal any definite answer, and echo -e $PS1
only evaluates the colors.
Since Bash 4.4 you can use the @P
expansion:
First I put your prompt string in a variable myprompt
using read -r
and a quoted here-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
To print the prompt (as it would be interpreted if it were PS1
), use the expansion ${myprompt@P}
:
$ printf '%s\n' "${myprompt@P}"
gniourf@rainbow:~$
$
(In fact there are some \001
and \002
characters, coming from \[
and \]
that you can't see in here, but you can see them if you try to edit this post; you'll also see them in your terminal if you type the commands).
To get rid of these, the trick sent by Dennis Williamson on the bash mailing list is to use read -e -p
so that these characters get interpreted by the readline library:
read -e -p "${myprompt@P}"
This will prompt the user, with the myprompt
correctly interpreted.
To this post, Greg Wooledge answered that you might as well just strip the \001
and \002
from the string. This can be achieved like so:
myprompt=${myprompt@P}
printf '%s\n' "${myprompt//[$'\001'$'\002']}"
To this post, Chet Ramey answered that you could also turn off line editing altogether with set +o emacs +o vi
. So this will do too:
( set +o emacs +o vi; printf '%s\n' "${myprompt@P}" )
One great advantage of open source software is that the source is, well, open :-)
Bash itself does not provide this functionality but there are various tricks you can use to provide a subset (such as substituting \u
with $USER
and so on). However, this requires a lot of duplication of functionality and ensuring that the code is kept in sync with whatever bash
does in future.
If you want to get all the power of prompt variables (and you don't mind getting your hands dirty with a bit of coding (and, if you do mind, why are you here?)), it's easy enough to add to the shell itself.
If you download the code for bash
(I'm looking at version 4.2), there's a y.tab.c
file which contains the decode_prompt_string()
function:
char *decode_prompt_string (string) char *string; { ... }
This is the function that evaluates the PSx
variables for prompting. In order to allow this functionality to be provided to users of the shell itself (rather than just used by the shell), you can follow these steps to add an internal command evalps1
.
First, change support/mkversion.sh
so that you won't confuse it with a "real" bash
, and so that the FSF can deny all knowledge for warranty purposes :-) Simply change one line (I added the -pax
bit):
echo "#define DISTVERSION \"${float_dist}-pax\""
Second, change builtins/Makefile.in
to add a new source file. This entails a number of steps.
(a) Add $(srcdir)/evalps1.def
to the end of DEFSRC
.
(b) Add evalps1.o
to the end of OFILES
.
(c) Add the required dependencies:
evalps1.o: evalps1.def $(topdir)/bashtypes.h $(topdir)/config.h \
$(topdir)/bashintl.h $(topdir)/shell.h common.h
Third, add the builtins/evalps1.def
file itself, this is the code that gets executed when you run the evalps1
command:
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 <http://www.gnu.org/licenses/>.
$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 <config.h>
#include "../bashtypes.h"
#include <stdio.h>
#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;
}
The bulk of that is the GPL licence (since I modified it from exit.def
) with a very simple function at the end to get and decode PS1
.
Lastly, just build the thing in the top level directory:
./configure
make
The bash
executable that appears can be renamed to paxsh
, though I doubt it will ever become as prevalent as its ancestor :-)
And running it, you can see it in action:
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 <http://gnu.org/licenses/gpl.html>
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: ]
When you put one of the PSx
variables into the prompt, echoing $PS1
simply gives you the variable, while the evalps1
command evaluates it and outputs the result.
Now, granted, making code changes to bash
to add an internal command may be considered by some to be overkill but, if you want an perfect evaluation of PS1
, it's certainly an option.
Why don't you just process the $PS1
escape substitutions yourself? A series of substitutions such as these:
p="${PS1//\\u/$USER}"; p="${p//\\h/$HOSTNAME}"
By the way, zsh has the ability to interpret prompt escapes.
print -P '%n@%m %d'
or
p=${(%%)PS1}
I like the idea of fixing Bash to make it better, and I appreciate paxdiablo's verbose answer on how to patch Bash. I'll have a go sometime.
However, without patching Bash source-code, I have a one-liner hack that is both portable and doesn't duplicate functionality, because the workaround uses only Bash and its builtins.
x="$(PS1=\"$PS1\" echo -n | bash --norc -i 2>&1)"; echo "'${x%exit}'"
Note that there's something strange going on with tty
's and stdio
seeing as this also works:
x="$(PS1=\"$PS1\" echo -n | bash --norc -i 2>&1 > /dev/null)"; echo "'${x%exit}'"
So although I don't understand what's going on with the stdio
here, my hack is working for me on Bash 4.2, NixOS GNU/Linux. Patching the Bash source-code is definitely a more elegant solution, and it should be pretty easy and safe to do now that I'm using Nix.
Two answer: "Pure bash" and "bash + sed"
Intro
Of course, from version 4.4 of bash, as gniourf_gniourf correctly answered, you have to use parameter transformation:
ExpPS1=${PS1@P}
echo ${ExpPS1@Q}
$'\001\E]0;user@host: ~\a\002user@host:~$ '
See man -Pless\ +/parameter\\\ transformation bash
But for older bash, or even just for playing with strings and variables...
bash prompt expansion, using bash
+ sed
There is my hack:
ExpPS1="$(bash --rcfile <(echo "PS1='$PS1'") -i <<<'' 2>&1 |
sed ':a;$!{N;ba};s/^\(.*\n\)*\(.*\)\n\2exit$/\2/p;d')"
Explanation:
Running bash --rcfile <(echo "PS1='$PS1'") -i <<<'' 2>&1
May return something like:
user@host:~$ user@host:~$ exit
The sed
command will then
- take all lines into one buffer (
:a;$!{N;ba};
), then - replace
<everything, terminated by end-of-line><prompt>end-of-line<prompt>exit
by<prompt>
. (s/^\(.*\n\)*\(.*\)\n\2exit$/\2/
).- where
<everything, terminated by end-of-line>
become\1
- and
<prompt>
become\2
.
- where
Test case:
while ExpPS1="$(bash --rcfile <(echo "PS1='$PS1'") -i <<<'' 2>&1 |
sed ':a;$!{N;ba};s/^\(.*\n\)*\(.*\)\n\2exit$/\2/p;d')"
read -rp "$ExpPS1" && [ "$REPLY" != exit ] ;do
eval "$REPLY"
done
From there, you're in a kind of pseudo interactive shell (without readline facilities, but that's does not matter)...
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$
(Last line print both ubuntu
in green, @
, :
and $
in black and path (/tmp
) in blue)
ubuntu@ubuntu:/tmp$ exit
ubuntu@ubuntu:/tmp$ od -A n -t c <<< $ExpPS1
033 [ 1 ; 3 2 m u b u n t u 033 [ 0
m @ 033 [ 1 ; 3 2 m u b u n t u 033
[ 0 m : 033 [ 1 ; 3 4 m ~ 033 [ 0 m
$ \n
Pure bash
Simple and quick:
ExpPS1="$(bash --rcfile <(echo "PS1='$PS1'") -i <<<'' 2>&1)"
mapfile ExpPS1 <<<"${ExpPS1%exit}"
ExpPS1=( "${ExpPS1[*]::${#ExpPS1[@]}/2}" )
Then now
declare -p ExpPS1
declare -a ExpPS1=([0]=$'\E]0;ubuntu@ubuntu: ~\aubuntu@ubuntu:~$ \n')
or
echo ${ExpPS1@Q}
$'\E]0;ubuntu@ubuntu: ~\aubuntu@ubuntu:~$ \n'
Quick test with multiline prompts:
ExpPS1="$(bash --rcfile <(echo "PS1='Test string\n$(date)\n$PS1'"
) -i <<<'' 2>&1)";
mapfile ExpPS1 <<<"${ExpPS1%exit}"
ExpPS1=( "${ExpPS1[*]::${#ExpPS1[@]}/2}" )
echo ${ExpPS1@Q}
$'Test string\r\n Sat Jan 9 19:23:47 CET 2021\r\n \E]0;ubuntu@ubuntu: ~\aubuntu@ubuntu:~$ \n'
Or
od -A n -t c <<<${ExpPS1}
T e s t s t r i n g \r \n S a
t J a n 9 1 9 : 2 6 : 3
9 C E T 2 0 2 1 \r \n 033 ] 0
; u b u n t u @ u b u n t u :
~ \a u b u n t u @ u b u n t u :
~ $ \n \n
Note you could add a little test to ensure string is correct:
ExpPS1="$(bash --rcfile <(echo "PS1='$PS1'") -i <<<'' 2>&1)"
mapfile ExpPS1 <<<"${ExpPS1%exit}"
[ "${ExpPS1[*]::${#ExpPS1[@]}/2}" = "${ExpPS1[*]: -${#ExpPS1[@]}/2}" ] ||
echo WARNING: First half seem not match last half string.
ExpPS1=( "${ExpPS1[*]::${#ExpPS1[@]}/2}" )
One more possibility: without editing bash source code, using script
utility (part of bsdutils
package on 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
<prints the prompt properly here>
script
command generates a file specified & the output is also shown on stdout. If filename is omitted, it generates a file called typescript.
Since we are not interested in the log file in this case, filename is specified as /dev/null
. Instead the stdout of the script command is passed to awk for further processing.
- The entire code can also be encapsulated into a function.
- Also, the output prompt can also be assigned to a variable.
- This approach also supports parsing of
PROMPT_COMMAND
...
- Expand it with
ps=${ps@P}
(bash 4.4) - Remove between
\x01
and\x02
(created by bash replacing the\[
and\]
placeholder. - Check all characters left
ps1_size(){
# Ref1: https://stackoverflow.com/questions/3451993/how-to-expand-ps1
>&2 echo -e "\nP0: Raw"
local ps=$PS1
echo -n "$ps" | xxd >&2
>&2 echo -e "\nP1: Expanding (require bash 4.4)"
ps=${ps@P}
echo -n "$ps" | xxd >&2
>&2 echo -e "\nP2: Removing everything 01 and 02"
shopt -s extglob
ps=${ps//$'\x01'*([^$'\x02'])$'\x02'}
echo -n "$ps" | xxd >&2
>&2 echo -e "\nP3: Checking"
if [[ "$ps" =~ [\x07\x1b\x9c] ]]; then
# Check if escape inside
# 07 => BEL
# 1b => ESC
# 9C => ST
>&2 echo 'Warning: There is an escape code in your PS1 which is not betwwen \[ \]'
>&2 echo "Tip: put \[ \] around your escape codes (ctlseqs + associated parameters)"
echo -n "$ps" | xxd >&2
# Check printable characters <= 20 .. 7e, and newline
# -- Remove the trailing 0x0a (BEL)
elif [[ "$ps" =~ [^[:graph:][:space:]] ]]; then
>&2 echo 'Warning: There is a non printable character in PS1 which is not between \[ \]'
>&2 echo "Tip: put \[ \] around your escape codes (ctlseqs + associated parameters)"
echo "$ps"
echo -n "$ps" | xxd >&2
fi
# Echo result
echo -n "${#ps}"
}
ps1_size
Should output something like this:
~/Software/Bash/Mouse (master)$ source ../ps1_size.sh
P0: Raw
00000000: 5c5b 5c65 5d30 3b60 7061 7273 655f 7469 \[\e]0;`parse_ti
00000010: 746c 6560 5c30 3037 5c5d 5c5b 5c65 5b33 tle`\007\]\[\e[3
00000020: 326d 5c5d 5c77 205c 5b5c 655b 3333 6d5c 2m\]\w \[\e[33m\
00000030: 5d60 7061 7273 655f 6769 745f 6272 616e ]`parse_git_bran
00000040: 6368 605c 5b5c 655b 306d 5c5d 2420 ch`\[\e[0m\]$
P1: Expanding (require bash 4.4)
00000000: 011b 5d30 3b7e 2f53 6f66 7477 6172 652f ..]0;~/Software/
00000010: 4261 7368 2f4d 6f75 7365 0702 011b 5b33 Bash/Mouse....[3
00000020: 326d 027e 2f53 6f66 7477 6172 652f 4261 2m.~/Software/Ba
00000030: 7368 2f4d 6f75 7365 2001 1b5b 3333 6d02 sh/Mouse ..[33m.
00000040: 286d 6173 7465 7229 011b 5b30 6d02 2420 (master)..[0m.$
P2: Removing everything 01 and 02
00000000: 7e2f 536f 6674 7761 7265 2f42 6173 682f ~/Software/Bash/
00000010: 4d6f 7573 6520 286d 6173 7465 7229 2420 Mouse (master)$
P3: Checking
32~/Software/Bash/Mouse (master)$
If some control characters are present, you can remove it as stated in this stackoverflow: Removing ANSI color codes from text stream. I used the following to remove SCI and OSC in github: mouse_xterm.
# Sanitize, in case
ps=$(LC_ALL=C sed '
# Safety
s/\x01\|\x02//g;
# Safety Remove OSC https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
# 20 .. 7e => printable characters
# 07 => BEL
# 9C => ST
# 1b 5C => ESC + BS
s/\x1b\][0-9;]*[\x20-\x7e]*\([\x07\x9C]\|\x1b\\\)//g;
# Safety: Remove all escape sequences https://superuser.com/questions/380772/removing-ansi-color-codes-from-text-stream
s/\x1b\[[0-9;]*[a-zA-Z]//g;
' <<< "$ps")
精彩评论