11

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!

Lee Netherton
  • 21,347
  • 12
  • 68
  • 102

3 Answers3

24

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.

chepner
  • 497,756
  • 71
  • 530
  • 681
  • Thank you. This works perfectly! Actually, now I know the escape codes to use, I found the other posts that you refer to: http://stackoverflow.com/a/22706988/341459 http://stackoverflow.com/a/14637194/341459 http://stackoverflow.com/a/19501528/341459 – Lee Netherton Jul 19 '14 at 13:40
  • 7
    You're entirely right. `\001` and `\002`, aka RL_PROMPT_START_IGNORE and RL_PROMPT_END_IGNORE, are a poorly documented [readline feature](http://git.savannah.gnu.org/cgit/readline.git/tree/display.c?id=fb914b20839322c962918590b3ea449555d7d9f9#n262). Bash converts `\[..\]` to `\001..\002` for Readline, but neglects to escape existing ones so that this implementation detail leaks through (and it's so useful that you can hardly call it a bug). – that other guy Jul 19 '14 at 14:11
  • When I saw `PROMPT_COMMAND` on a recent post I remembered this question and went back to it but it seems like the idea was already given. I also had the idea that bash may actually be producing characters out of `\[` and `\]` and tried to examine it but the output of `ttyrec` was just confusing so I just gave up. +1 btw. – konsolebox Jul 20 '14 at 09:30
0

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.

Tom Hale
  • 40,825
  • 36
  • 187
  • 242
-1

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

  • sounds fair, does not work. chepner described already how the escaping happens *before* the external command is called. – phil294 Aug 03 '17 at 15:57