Lee Netherton
Lee Netherton

Reputation: 22512

Bash PS1: line wrap issue with non-printing characters from an external command

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

Answers (3)

Tom Hale
Tom Hale

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

Dan Martinez
Dan Martinez

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

chepner
chepner

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

Related Questions