Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Configure shell to always print prompt on new line, like zsh

Tags:

bash

shell

prompt

If a command's output does not end with a \n, the next prompt appears, awkwardly, immediately afterwards:

$ echo -n hai
hai$

I just noticed a colleague whose shell (zsh, for what it's worth) is configured to print a % (with background and foreground colours inverted for emphasis) followed by a \n in such cases:

$ echo -n hai
hai%
$

I'd like to do the same. I use Bash. Is this possible? If so, what would I add to my ~/.bashrc?


UPDATE

I've spent several hours gaining an understanding of how gniourf_gniourf's solution works. I'll share my findings here, in case they are of use to others.

  • ESC[6n is the control sequence introducer for accessing the cursor position (http://en.wikipedia.org/wiki/ANSI_escape_code).

  • \e is not a valid representation of ESC when using echo on OS X (https://superuser.com/q/33914/176942). \033 can be used instead.

  • IFS is Bash's internal field separator (http://tldp.org/LDP/abs/html/internalvariables.html#IFSREF).

  • read -sdR looks like shorthand for read -s -d -R, but in fact the "R" is not a flag, it's the value of the -d (delimiter) option. I decided to write read -s -d R instead to avoid confusion.

  • The double-parentheses construct, (( ... )), permits arithmetic expansion and evaluation (http://tldp.org/LDP/abs/html/dblparens.html).

Here's the relevant snippet from my .bashrc:

set_prompt() {
  # CSI 6n reports the cursor position as ESC[n;mR, where n is the row
  # and m is the column. Issue this control sequence and silently read
  # the resulting report until reaching the "R". By setting IFS to ";"
  # in conjunction with read's -a flag, fields are placed in an array.
  local curpos
  echo -en '\033[6n'
  IFS=';' read -s -d R -a curpos
  curpos[0]="${curpos[0]:2}"  # strip leading ESC[
  (( curpos[1] > 1 )) && echo -e '\033[7m%\033[0m'

  # set PS1...
}

export PROMPT_COMMAND=set_prompt

Note: The curpos[0]="${curpos[0]:2}" line is unnecessary. I included it so this code could be used in a context where the row is also relevant.

like image 319
davidchambers Avatar asked Nov 13 '13 01:11

davidchambers


People also ask

How do I set the prompt in zsh?

To customize the ZSH prompt, we need to modify the prompt= variable inside the . zshrc file. We can populate the prompt variable with various placeholders, which will alter how the ZSH prompt appears.

How do I change zsh prompt on Mac?

To make any change to the default zsh prompt, you'll have to add relevant values for the prompt to appear differently than the default. It'll be blank if you're accessing it for the first time. You can add a new line with the text PROMPT='...' and include relevant values in the ellipses.

How do I make my zsh look like bash?

Short answer: use %(!. #. $)


1 Answers

A little trick using PROMPT_COMMAND:

The value of the variable PROMPT_COMMAND is examined just before Bash prints each primary prompt. If PROMPT_COMMAND is set and has a non-null value, then the value is executed just as if it had been typed on the command line.

Hence, if you put this in your .bashrc:

_my_prompt_command() {
    local curpos
    echo -en "\E[6n"
    IFS=";" read -sdR -a curpos
    ((curpos[1]!=1)) && echo -e '\E[1m\E[41m\E[33m%\E[0m'
}
PROMPT_COMMAND=_my_prompt_command

you'll be quite good. Feel free to use other fancy colors in the echo "%" part. You can even put the content of that in a variable so that you can modify it on the fly.

The trick: obtain the column of the cursor (with echo -en "\E[6n" followed by the read command) before printing the prompt and if it's not 1, print a % and a newline.

Pros:

  • pure bash (no external commands),
  • no subshells,
  • leaves your PS1 all nice and clean: if you want to change your PS1 sometimes (I do this when I work in deeply nested directory — I don't like having prompts that run on several miles), this will still work.

As tripleee comments, you could use stty instead of echoing a hard-coded control sequence. But that uses an external command and is not pure bash anymore. Adapt to your needs.


Regarding your problem with the ugly character codes that get randomly printed: this might be because there's still some stuff in the tty buffer. There might be several fixes:

  1. Turn off and then on the echo of the terminal, using stty.

    set_prompt() {
        local curpos
        stty -echo
        echo -en '\033[6n'
        IFS=';' read -d R -a curpos
        stty echo
        (( curpos[1] > 1 )) && echo -e '\033[7m%\033[0m'
    }
    PROMPT_COMMAND=set_prompt
    

    the main difference is that the echo/read combo has been wrapped with stty -echo/stty echo that respectively disables and enables echoing on terminal (that's why the -s option to read is now useless). In this case you won't get the cursor position correctly and this might lead to strange error messages, or the % not being output at all.

  2. Explicitly clear the tty buffer:

    set_prompt() {
        local curpos
        while read -t 0; do :; done
        echo -en '\033[6n'
        IFS=';' read -s -d R -a curpos
        (( curpos[1] > 1 )) && echo -e '\033[7m%\033[0m'
    }
    PROMPT_COMMAND=set_prompt
    
  3. Just give up if the tty buffer can't be cleaned:

    set_prompt() {
        local curpos
        if ! read -t 0; then
            echo -en '\033[6n'
            IFS=';' read -s -d R -a curpos
            (( curpos[1] > 1 )) && echo -e '\033[7m%\033[0m'
        # else
        #     here there was still stuff in the tty buffer, so I couldn't query the cursor position
        fi
    }
    PROMPT_COMMAND=set_prompt
    

As a side note: instead of reading in an array curpos, you can directly obtain the position of the cursor in variables, say, curx and cury as so:

IFS='[;' read -d R _ curx cury

If you only need the y-position cury:

IFS='[;' read -d R _ _ cury
like image 198
gniourf_gniourf Avatar answered Nov 07 '22 13:11

gniourf_gniourf