Escape non-printing characters in a function for a Bash prompt

23

8

In a Bash Prompt (PS1 variable), I'm calling a function to potentially add text to the prompt: export PS1="\u@\h \$(my_function) \$ "

However, the function in the prompt contains ANSI color codes that change based on the output of the function (sometimes red, sometimes green). Adding "\[" to the PS1 variable should escape those codes as non-printing, but if I do an echo in the function, the "\[" get printed literally in the prompt.

How can I escape these ANSI color codes from within a function for use in a bash prompt?

MidnightLightning

Posted 2011-06-23T20:18:08.890

Reputation: 385

Answers

35

The readline library accepts \001 and \002 (ASCII SOH and STX) as non-printable text delimiters. These also work in any application that uses readline.

From lib/readline/display.c:243 in bash source code:

243 /* Current implementation:
244         \001 (^A) start non-visible characters
245         \002 (^B) end non-visible characters
246    all characters except \001 and \002 (following a \001) are copied to
247    the returned string; all characters except those between \001 and
248    \002 are assumed to be `visible'. */

The bash-specific \[ and \] are in fact translated to \001 and \002 at y.tab.c:7640.


Note: If you use bash's printf or echo -e, and if your text has \001 or \002 immediately before a number, you'll hit a bash bug that causes it to eat one digit too many when processing octal escapes – that is, \00142 will be interpreted as octal 014 (followed by ASCII "2"), instead of the correct octal 01 (followed by ASCII "42"). For this reason, use hexadecimal versions \x01 and \x02 instead.

user1686

Posted 2011-06-23T20:18:08.890

Reputation: 283 655

Sorry to resurrect an answer, but what's the equivalent on dash/ash/sh? – Hosh Sadiq – 2018-06-07T20:55:19.600

@Hosh If they use readline, \001 and \002 will work. Otherwise I'm not sure. Dash for example definitely doesn't use readline.

– wjandrea – 2019-04-12T04:16:32.970

That does it! echo -e "\001\e[31m\002RED" works as expected. Thanks! – MidnightLightning – 2011-06-23T20:38:19.887

1

Here's a nice complete answer. I had to do a lot more digging to figure out where the \001 etc. had to go. Hope this helps.

# Color prompt for git
reset=$(tput sgr0)
boldgreen=$(tput setaf 2)$(tput bold)
cyan=$(tput sgr0)$(tput setaf 6)
boldred=$(tput setaf 1)$(tput bold)
boldwhite=$(tput setaf 7)$(tput bold)
boldyellow=$(tput setaf 3)$(tput bold)

PARENCLR=$'\001\e[0;36m\002'
BRANCHCLR=$'\001\e[1;33m\002'

alias branchname="git branch 2>/dev/null | grep '*' | sed 's/* \(.*\)/ ${PARENCLR}(${BRANCHCLR}\1${PARENCLR}\)/'"

GIT_STATUS='$(branchname)'

PROMPT_CHAR="\$"
PS1="\[$boldgreen\]\u\[$cyan\]::\[$boldred\]\h \[$cyan\]{\[$boldwhite\].../\W\[$cyan\]}\[$reset\]$GIT_STATUS\[$reset\]$PROMPT_CHAR "

The way I have it set up here, the git branch parentheses only appear if you're in a git branch, otherwise it's blank.

Dan L

Posted 2011-06-23T20:18:08.890

Reputation: 11

0

Based on grawity's answer, 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

Posted 2011-06-23T20:18:08.890

Reputation: 1 348

-2

If you want to use them in the prompt, then you do need to do the \[. But if you want to use it in an echo, you have to use \033[.

Wuffers

Posted 2011-06-23T20:18:08.890

Reputation: 16 645

Hmmm... Adding \033[ before the ANSI command ("\e[31m") and \033] after it seems to make the next printed character in the prompt not print. – MidnightLightning – 2011-06-23T20:31:40.313

1You don't want to do \033] after it. \033[31m starts the color, after that you need to set it back with \033[0m – Wuffers – 2011-06-23T20:38:48.620