1

The Issues:

Open command line prompt:

enter image description here

Enter the letter a multiple times:

enter image description here

Instead of wrapping to a new line the text entered wraps onto the same line:

enter image description here

Now, start pressing b. The second time a line wrap is required, it will wrap to a new line:

enter image description here


What causes this behavior?

Using a PS1 like this causes the behavior:

ps1Color="\033[1;35m"
export PS1='$(echo -en $ps1Color) Baz $'

Note the reason I want to use echo instead of the color directly is because I want to add the color conditionally based on the exit status of the previous command.

Using the color directly doesn't cause this behavior to occur.


My questions are:

  • How can I print color codes for use in a PS1 using echo?
  • If I want to make my PS1 a different color conditionally what is the best way to do this?
  • Why am I seeing this behavior?

Update

To be clear, I really want to do this using echo because I want to change the color conditionally.

This is what I currently have right now:

function setPs1Colors_start () {

    local previousExit=$?

    local ps1Color="\033[1;35m"
    local ps1FailBackground="\e[41m"

    echo -en $ps1Color

    if [[ previousExit -ne 0 ]]
    then
        echo -en $ps1FailBackground
    fi

}

function setPs1Colors_end () {
    local ps1DefaultColor="\033[0m"
    echo -en $ps1DefaultColor
}

export PS1='$(setPs1Colors_start)[$(date +%b\-%d\ %k:%M)][$(versionControlInfo)\W]\$$(setPs1Colors_end) '
  • You can use \e in PS1 for a literal escape character: PS1='\e[1;35m Baz \$'. – DopeGhoti May 26 '17 at 20:29
  • just in case anyone else has same issue as me... my issue was using $(tput bold) inside the PS1 variable - use the ANSI escape sequences instead – Mudassir Feb 08 '20 at 13:24

1 Answers1

8

\033[1;35m is 7 characters. bash cannot guess that those 7 characters have actually a null width. If not, it will think that they are 7 columns wide.

It (or rather readline the underlying line-editor it uses) wants to know what's the current position on the screen because it's using cursor positioning sequences (up, down, left, right) to move the cursor when you're using editing keys.

So you've got to tell it which of the characters in the prompt do not move the cursor. With bash, that's done by using \[...\] which tells the shell that what's inside has zero-width.

Also note that prompt expansion in bash does recognise \e as an ESC character, so you don't have to use echo -e. You can just do:

PS1='\[\e[1;35m\] blah $ '

If you have to use echo, or better printf, you'd do:

PS1='\[$(if ...; then printf "$color1"; fi)\] blah $ '

Or:

PS1='$(if ...; then printf "\[$color1\]"; fi) blah $ '

In zsh, the equivalent of bash's \[...\] is %{...%} like in tcsh, but zsh has directives to change character attributes, so there you'd rather do:

PS1='%B%F{magenta}blah $ '

For bold-magenta foreground. It also has some forms of conditional tests, including on $?, so your red if error, green otherwise could be written:

PS1='%F{%(?:green:red%)}blah%f $ '

tcsh has %B, but not %F{color}. So there, you'd use:

set prompt = '%{\e[1;35m%}blah $ '

In ksh88 or pdksh, you'd do:

PS1=$(printf '\5\r\5\33[1;35m\5blah $ ')

That defines a character (here 0x5) to be the escape character. Then by enclosing text between a pair of them, you're telling the shell that it's not visible. You can use any character other than 0x5, but it must not otherwise occur in your prompt and except in mksh, it has to be one ignored by the terminal because the shell does actually write it (along with the CR character).

ksh93 uses only one cursor positioning sequence: BS (which moves the cursor one column to the left). To move to the right, it just redraws the same characters. So it doesn't need to know the cursor position, only the width of each character your enter. That works as long as the terminal wraps at the margin by itself (so won't work properly with this terminator for instance). One side effect if you have a prompt with control sequences is that your tab-stops won't be properly aligned.

  • This doesn't answer my question because I want to do it using an echo, so that I can do my color change conditionally.

    Essentially, I want to do something like this:

    PS1='$(if ... then echo $color1 fi) $'

    – sixtyfootersdude May 26 '17 at 21:46
  • Added an update on why I want to use echo. – sixtyfootersdude May 26 '17 at 21:49
  • "bash cannot guess that those 7 characters have actually a null width" – Well, actually it wouldn't need to guess, it could know that everything between \e[ and m are ANSI escapes. :-) This is perhaps one of the most frequently asked questions on stackoverflow sites / most frequently reported "bugs" against terminal emulators. I've been planning to check if it's ever been discussed on bash/readline mailing list and recommend them to autodetect. Explicit \[ and \] could remain for the less obvious / less typical cases. – egmont May 26 '17 at 22:19
  • @egmont, short of knowing the behaviour of every sequence on every terminal, that wouldn't be really realistic. Sure, it could do it for ANSI SGR escapes only that are supported by most terminals, but then best would be to do the right thing in the first place like in zsh and provide prompt escapes for those attributes. – Stéphane Chazelas May 26 '17 at 22:39
  • Your suggestion is nicer technically, although probably wouldn't fully protect against this question getting asked over and over again since people could still use the raw escape sequences. Another approach could be just to emit a warning if there's an unbracketed escape byte. – egmont May 27 '17 at 14:41
  • 1
    @egmont. That could be useful. But note that it would have to be only escapes that are after the last occurrence of a newline. There's also the question of when to issue the warning message. And there's the problem that it's $PS1, not $BASH_PS1 so the message would annoy people who define (and export) a portable PS1 with colours in ~/.profile and only occasionally run bash. (unlikely though). Being not a bash user, I'm quite annoyed myself when OSes set a bash-specific $PS1 in the default ~/.profile (or export PS1 in /etc/bash.bashrc). – Stéphane Chazelas May 28 '17 at 07:00
  • Good points. Should be discussed on bash/readline dev lists anyways. I'll send them the request when I have a bit more time... – egmont May 28 '17 at 10:47