Reputation: 22512
I am using an external command to populate my bash prompt, which is run each time PS1 is evaluated. However, I have a problem when this command outputs non-printable characters (like color escape codes). Here is an example:
$ cat green_cheese.sh
#!/bin/bash
echo -e "\033[32mcheese\033[0m"
$ export PS1="\$(./green_cheese.sh) \$"
cheese $ # <- cheese is green!
cheese $ <now type really long command>
The canonical way of dealing with non-printing characters in the PS1 prompt is to enclose them in \[
and \]
escape sequences. The problem is that if you do this from the external command those escapes are not parsed by the PS1 interpreter:
$ cat green_cheese.sh
#!/bin/bash
echo -e "\[\033[32m\]cheese\[\033[0m\]"
$ export PS1="\$(./green_cheese.sh) \$"
\[\]cheese\[\] $ # <- FAIL!
Is there a particular escape sequence I can use from the external command to achieve the desired result? Or is there a way I can manually tell the prompt how many characters to set the prompt width to?
Assume that I can print anything I like from the external command, and that this command can be quite intelligent (for example, counting characters in the output). I can also make the export PS1=...
command as complicated as required. However, the escape codes for the colors must come from the external command.
Thanks in advance!
Upvotes: 12
Views: 3798
Reputation: 46795
If you can't edit the code generating the string containing ANSI color / control codes, you can wrap them after the fact.
The following will enclose ANSI control sequences in ASCII SOH
(^A
) and STX
(^B
) which are equivalent to \[
and \]
respectively:
function readline_ANSI_escape() {
if [[ $# -ge 1 ]]; then
echo "$*"
else
cat # Read string from STDIN
fi | \
perl -pe 's/(?:(?<!\x1)|(?<!\\\[))(\x1b\[[0-9;]*[mG])(?!\x2|\\\])/\x1\1\x2/g'
}
Use it like:
$ echo $'\e[0;1;31mRED' | readline_ANSI_escape
Or:
$ readline_ANSI_escape "$string"
As a bonus, running the function multiple times will not re-escape already escaped control codes.
Upvotes: 0
Reputation: 21
I suspect that if you echo the value of $PS1
after your first example, you’ll find that its value is the word “cheese” in green. (At least, that’s what I see when I run your example.) At first glance, this is what you want — the word “cheese” in green! Except that what you really wanted was the word cheese preceded by the escape codes that produce green. What you did by using the -e
flag for echo is produce a value with the escape codes already evaluated.
That happens to work for the specification of colors, but as you’ve found, it mangles the “non-printing sequence” markers into something the $PS1
interpreter doesn’t properly understand.
Fortunately, the solution is simple: drop the -e
flag. echo
will then leave the escape sequences untouched, and the $PS1
interpreter will Do The Right Thing™.
Upvotes: -1
Reputation: 531165
I couldn't tell you exactly why this works, but replace \[
and \]
with the actual characters that bash
generates from them in your prompt:
echo -e "\001\033[32m\002cheese\001\033[0m\002"
[I learned this from some Stack Overflow post that I cannot find now.]
If I had to guess, it's that bash
replaces \[
and \]
with the two ASCII characters before executing the command that's embedded in the prompt, so that by the time green_cheese.sh
completes, it's too late for bash
to process the wrappers correctly, and so they are treated literally. One way to avoid this is to use PROMPT_COMMAND
to build your prompt dynamically, rather than embedding executable code in the value of PS1
.
prompt_cmd () {
PS1="$(green_cheese.sh)"
PS1+=' \$ '
}
PROMPT_COMMAND=prompt_cmd
This way, the \[
and \]
are added to PS1
when it is defined, not when it is evaluated, so you don't need to use \001
and \002
directly.
Upvotes: 26