23

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 which 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.

oguz ismail
  • 34,491
  • 11
  • 33
  • 56
l0b0
  • 48,420
  • 21
  • 118
  • 185
  • My head hurts. I know you probably want to use the `eval echo` idiom, but I can't think how to get the colors safely through. (My prompt is even worse - I have it red/green based on exit status, so the escaped characters for colors have to be dealt with *after* the expansion.) – Cascabel Aug 10 '10 at 18:27
  • My guess at this point is that if we can find whatever will expand `\u` to `username`, it's easy peasy. But it's not documented (and I don't know enough C to dig into Bash). – l0b0 Aug 10 '10 at 19:14
  • It looks like the command to run is `expand_prompt_string` from subst.c in the Bash source tree. Now to figure out how to call that from within the script... – l0b0 Aug 10 '10 at 19:56
  • @I0b0: Oh, right, I forgot about the extra directives like `\u`. And... I'm not sure that expand_prompt_string is going to be accessible from the shell, unless you patch it. (Maybe you could compile it separately...) – Cascabel Aug 10 '10 at 20:15

6 Answers6

21

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}" )
gniourf_gniourf
  • 38,851
  • 8
  • 82
  • 94
  • Tested on Bash 4.4 RC1, and it kinda works. `printf '%s\n' "${PS1@P}"` prints my prompt exactly as it appears to me, but when I try to `printf '%q\n' "${PS1@P}"` I see the the username and host twice, and the path once with `~` and once with `/home/username`: `$'username\001\E[1m\002\001\E[33m\002^2\001\E(B\E[m\002@hostname:\001\E[1m\002\001\E[34m\002~/download/bash-4.4-rc1\001\E(B\E[m\002\001\E]0;username@hostname:/home/username/download/bash-4.4-rc1\a\002\n$ '`. Strange. – l0b0 May 10 '16 at 18:49
  • 1
    @lobo If you run `echo "$PS1"`, you'll see something like `\[\e]0;\u@\h:$PWD\a\]`. That sets the title for xterm-compatible terminals. – wjandrea Dec 09 '17 at 08:37
  • 1
    Using `PROMPTCOMMAND` to separate "things to do at prompt time" from "things to display in the prompt" is usually a good idea. – chepner Apr 27 '18 at 19:12
12

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.

paxdiablo
  • 772,407
  • 210
  • 1,477
  • 1,841
  • Cool! Might it be easier to use as a [dyamically-loadable builtin](http://cfajohnson.com/shell/articles/dynamically-loadable/) that any bash process could use? – andrewdotn Feb 01 '13 at 05:22
  • @Andrew, I'm not sure DLBs get access to the code already in the shell, do they? The "command" has to be able to call `decode_prompt_string()`. I could be wrong about that (it happens more often than I care to admit to) but I though DLB had to be pretty well self-contained. – paxdiablo Feb 01 '13 at 05:44
  • Yes, you’re right; my mistake. If you have an unstripped bash binary you could use some hacks to read the addresses of internal bash functions and variables from the symbol table, but the system bash is almost certainly stripped :/ – andrewdotn Feb 01 '13 at 06:15
  • The main problem I could see by using this method is: You have to run/use your own modified version of bash. With the high frequence bash sources have to be patched, you will have a lot of work in futur for maintaining your version up to date! (Until your patch may be intagrated in official bash sources)... – F. Hauri May 18 '16 at 10:38
  • @F.Hauri, given the localisation of the changes required, it would be a simple matter to write a script to pull down updated source, make the mods, then compile the modified code. This is exactly what we do in Yocto distro builds (there's a `do_patch` step somewhere between `do_fetch` and `do_compile`). On the other hand, I tend to only update software when needed, not just because the devs think I should :-) – paxdiablo May 19 '16 at 01:24
  • Calling `decode_prompt_string()` from a dynamically loaded builtin seemed to work for me. I asked https://stackoverflow.com/questions/50068834/can-bashs-c-functions-safely-be-called-from-dynamically-loadable-builtins to find out whether or not this is actually safe. – Joseph Sible-Reinstate Monica Apr 27 '18 at 19:10
  • > *However, this requires a lot of duplication of functionality and ensuring that the code is kept in sync with whatever bash does in future.* This can be sidestepped by defining a prompt language that **you** control, which your own code compiles to Bash PS1 syntax, and to an expansion. – Kaz Aug 22 '19 at 22:19
9

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}
mklement0
  • 245,023
  • 45
  • 419
  • 492
Dennis Williamson
  • 303,596
  • 86
  • 357
  • 418
  • This works in limited cases, but a complete solution would require many substitutions, some of them involving more than just simple substitution based on existing variables (see `man bash`, section `PROMPTING`); also, the solution would have to be kept current should future bash versions introduce new `PS1` features. – mklement0 Jul 21 '14 at 16:32
5

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.

Community
  • 1
  • 1
James Haigh
  • 1,062
  • 1
  • 11
  • 23
  • +1, great workaround, though I also don't understand it fully - it seems that the child bash instance sends *everything* to *stderr*, possibly because of the competing concepts of executing-then-exiting a 'script' passed via stdin vs. a keep-open interactive shell. The question is whether that behavior can be relied upon in future versions. It does work on bash 4.x and can be made to work on 3.x as well (see below). – mklement0 Jul 21 '14 at 18:37
  • There are some issues, though: (a) by using a pipeline and only prepending `PS1="$PS1"` to the _first_ segment, the bash instance in the subshell won't see that variable (unless it happens to be _exported_); (b) it's better to call `"$BASH"` rather than `bash` so as to ensure that the _same_ executable as the current shell's is invoked; (c) did you mean to have the `‘` `’` chars in the final variable expansion? See below for a reformulation that addresses these issues. – mklement0 Jul 21 '14 at 18:41
  • 1
    The following variation addresses above issues; it also works on bash 3.x, where a spurious `bash: no job control in this shell` error message must be worked around. Note the trailing 'exit' is removed via a `sed` command inside the command substitution so as to make do with a _single_ command: `x=$(PS1="$PS1" "$BASH" --norc -i &1 | sed -n '${s/^\(.*\)exit$/\1/p;}')`. – mklement0 Jul 21 '14 at 18:45
  • Finally, it's worth stating the _limitations_ of this solution: (a) it won't work if your prompt string _references non-exported shell variables and/or calls non-exported shell functions_ - you can work around that by exporting what's needed, but that's clearly not a generic solution; (b) it won't work if you prompt string *modifies* variables (such as a command sequence counter), because a child process cannot modify its parent's environment; there's no good workaround that I'm aware of. – mklement0 Jul 21 '14 at 18:51
  • @mklement0 This almost works for me, except for the fact that there seems to be length limit of 82(?) characters, after which the beginning of the line is overwritten, mangling prompts such as `FIRST_PART_LAST_PART$` to `ART$T_PART_LAST_P`. Any idea how this could be circumvented? Thanks! – David Nemeskey Jul 24 '15 at 09:58
  • @DavidNemeskey: From what I can tell, there's no length limit per se; what you describe sounds more like you have a `\r` (CR) char. in there somewhere. You can simulate your symptom with the following: `echo $'FIRST_PART_LAST_P\rART$'`. I suggest examining your output with `... | od -c`. I have noticed one strange thing in Bash 3.x, though: an invisible terminal escape sequence is prepended to the output, which can be suppressed by temporarily resetting the `TERM` variable, which gives us `TERM= PS1="$PS1" "$BASH" --norc -i &1 | sed -n '$ s/^\(.*\)exit$/\1/p'`. – mklement0 Jul 24 '15 at 13:50
  • @mklement0 There is a `\r` in it, but that only appears when the length of the prompt reaches 81 characters. Then, that character is duplicated, and a `\r` is added between them (so my last example was wrong, it should have been `PART$T_PART_LAST_P`). I tried resetting the `TERM` variable, but then the output became `< 001 ... 001 \r \n` (a ` – David Nemeskey Jul 26 '15 at 15:11
  • @DavidNemeskey That's strange - I have no explanation. I suggest you create a new question and provide more details. – mklement0 Jul 26 '15 at 20:15
3

Two answer: "Pure bash" and "bash + sed"

Intro

Of course, from version 4.4 of , 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...

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.

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

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}" )
F. Hauri
  • 51,421
  • 13
  • 88
  • 109
  • both methods was tested with `PS1='Test string\n\D{%a %d %b %Y, %Hh, %Mm, %Ss.}\n\[\e]0;\u@\h: \w\a\]${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '` – F. Hauri May 10 '16 at 11:26
1

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.

  1. The entire code can also be encapsulated into a function.
  2. Also, the output prompt can also be assigned to a variable.
  3. This approach also supports parsing of PROMPT_COMMAND...
anishsane
  • 17,770
  • 5
  • 33
  • 64
  • Interesting approach, but - as you hint at by mentioning Ubuntu - it's limited to platforms running _GNU_ utilities (other platforms, such as OSX, have utilities of the same name, but they behave differently), whereas `bash` runs on many more platforms. Also note that `script` translates `\n` in the prompt string to `\r\n`. – mklement0 Jul 28 '14 at 02:35