How to expand PS1?

后端 未结 7 1172
孤城傲影
孤城傲影 2020-11-27 17:57

I have a shell script that runs the same command in several directories (fgit). For each directory, I would like it to show the current prompt + the command which will be ru

相关标签:
7条回答
  • 2020-11-27 18:23

    You may have to write a small C program that uses the same code bash does (is it a library call?) to display that prompt, and just call the C program. Granted, that's not very portable since you'll have to compile it on each platform, but it's a possible solution.

    0 讨论(0)
  • 2020-11-27 18:30

    Why don't you just process the $PS1 escape substitutions yourself? A series of substitutions such as these:

    p="${PS1//\\u/$USER}"; p="${p//\\h/$HOSTNAME}"
    

    By the way, zsh has the ability to interpret prompt escapes.

    print -P '%n@%m %d'
    

    or

    p=${(%%)PS1}
    
    0 讨论(0)
  • 2020-11-27 18:32

    Two answer: "Pure bash" and "bash + sed"

    As doing this by using sed is simplier, the first answer will use sed.

    See below for pure bash solution.

    bash prompt expansion, bash + sed

    There is my hack:

    ExpPS1="$(bash --rcfile <(echo "PS1='$PS1'") -i <<<'' 2>&1 |
                  sed ':;$!{N;b};s/^\(.*\n\)*\(.*\)\n\2exit$/\2/p;d')"
    

    Explanation:

    Running bash --rcfile <(echo "PS1='$PS1'") -i <<<'' 2>&1

    May return something like:

    To run a command as administrator (user "root"), use "sudo <command>".
    See "man sudo_root" for details.
    
    ubuntu@ubuntu:~$ 
    ubuntu@ubuntu:~$ exit
    

    The sed command will then

    • take all lines into one buffer (:;$!{N;b};), than
    • replace <everything, terminated by end-of-line><prompt>end-of-line<prompt>exit by <prompt>. (s/^\(.*\n\)*\(.*\)\n\2exit$/\2/).
      • where <everything, terminated by end-of-line> become \1
      • and <prompt> become \2.
    Test case:
    while ExpPS1="$(bash --rcfile <(echo "PS1='$PS1'") -i <<<'' 2>&1 |
              sed ':;$!{N;b};s/^\(.*\n\)*\(.*\)\n\2exit$/\2/p;d')"
        read -rp "$ExpPS1" && [ "$REPLY" != exit ] ;do
        eval "$REPLY"
      done
    

    From there, you're in a kind of pseudo interactive shell (without readline facilities, but that's does not matter)...

    ubuntu@ubuntu:~$ cd /tmp
    ubuntu@ubuntu:/tmp$ PS1="${debian_chroot:+($debian_chroot)}\[\e[1;32m\]\u\[\e[0m\]@\[\e[1;32m\]\h\[\e[0m\]:\[\e[1;34m\]\w\[\e[0m\]$ "
    ubuntu@ubuntu:/tmp$ 
    

    (Last line print both ubuntu in green, @, : and $ in black and path (/tmp) in blue)

    ubuntu@ubuntu:/tmp$ exit
    ubuntu@ubuntu:/tmp$ od -A n -t c <<< $ExpPS1 
     033   [   1   ;   3   2   m   u   b   u   n   t   u 033   [   0
       m   @ 033   [   1   ;   3   2   m   u   b   u   n   t   u 033
       [   0   m   : 033   [   1   ;   3   4   m   ~ 033   [   0   m
       $  \n
    

    Pure bash

    ExpPS1="$(bash --rcfile <(echo "PS1='$PS1'") -i <<<'' 2>&1)"
    ExpPS1_W="${ExpPS1%exit}"
    ExpPS1="${ExpPS1_W##*$'\n'}"
    ExpPS1_L=${ExpPS1_W%$'\n'$ExpPS1}
    while [ "${ExpPS1_W%$'\n'$ExpPS1}" = "$ExpPS1_W" ] ||
          [ "${ExpPS1_L%$'\n'$ExpPS1}" = "$ExpPS1_L" ] ;do
        ExpPS1_P="${ExpPS1_L##*$'\n'}"
        ExpPS1_L=${ExpPS1_L%$'\n'$ExpPS1_P}
        ExpPS1="$ExpPS1_P"$'\n'"$ExpPS1"
      done
    

    The while loop is required to ensure correct handling of multiline prompts:

    replace 1st line by:

    ExpPS1="$(bash --rcfile <(echo "PS1='${debian_chroot:+($debian_chroot)}\[\e[1;32m\]\u\[\e[0m\]@\[\e[1;32m\]\h\[\e[0m\]:\[\e[1;34m\]\w\[\e[0m\]$ '") -i <<<'' 2>&1)"
    

    or

    ExpPS1="$(bash --rcfile <(echo "PS1='Test string\n$(date)\n$PS1'") -i <<<'' 2>&1)";
    

    The last multiline will print:

    echo "$ExpPS1"
    Test string
    Tue May 10 11:04:54 UTC 2016
    ubuntu@ubuntu:~$ 
    
    od -A n -t c  <<<${ExpPS1}
       T   e   s   t       s   t   r   i   n   g  \r       T   u   e
           M   a   y       1   0       1   1   :   0   4   :   5   4
           U   T   C       2   0   1   6  \r     033   ]   0   ;   u
       b   u   n   t   u   @   u   b   u   n   t   u   :       ~  \a
       u   b   u   n   t   u   @   u   b   u   n   t   u   :   ~   $
      \n
    
    0 讨论(0)
  • 2020-11-27 18:38

    Since Bash 4.4 you can use the @P expansion:

    First I put your prompt string in a variable myprompt using read -r and a quoted here-doc:

    read -r myprompt <<'EOF'
    ${debian_chroot:+($debian_chroot)}\[\e[1;32m\]\u\[\e[0m\]@\[\e[1;32m\]\h\[\e[0m\]:\[\e[1;34m\]\w\[\e[0m\]$(__git_ps1 ' (%s)')$ 
    EOF
    

    To print the prompt (as it would be interpreted if it were PS1), use the expansion ${myprompt@P}:

    $ printf '%s\n' "${myprompt@P}"
    gniourf@rainbow:~$
    $
    

    (In fact there are some \001 and \002 characters, coming from \[ and \] that you can't see in here, but you can see them if you try to edit this post; you'll also see them in your terminal if you type the commands).


    To get rid of these, the trick sent by Dennis Williamson on the bash mailing list is to use read -e -p so that these characters get interpreted by the readline library:

    read -e -p "${myprompt@P}"
    

    This will prompt the user, with the myprompt correctly interpreted.

    To this post, Greg Wooledge answered that you might as well just strip the \001 and \002 from the string. This can be achieved like so:

    myprompt=${myprompt@P}
    printf '%s\n' "${myprompt//[$'\001'$'\002']}"
    

    To this post, Chet Ramey answered that you could also turn off line editing altogether with set +o emacs +o vi. So this will do too:

    ( set +o emacs +o vi; printf '%s\n' "${myprompt@P}" )
    
    0 讨论(0)
  • 2020-11-27 18:40

    I like the idea of fixing Bash to make it better, and I appreciate paxdiablo's verbose answer on how to patch Bash. I'll have a go sometime.

    However, without patching Bash source-code, I have a one-liner hack that is both portable and doesn't duplicate functionality, because the workaround uses only Bash and its builtins.

    x="$(PS1=\"$PS1\" echo -n | bash --norc -i 2>&1)"; echo "'${x%exit}'"
    

    Note that there's something strange going on with tty's and stdio seeing as this also works:

    x="$(PS1=\"$PS1\" echo -n | bash --norc -i 2>&1 > /dev/null)"; echo "'${x%exit}'"
    

    So although I don't understand what's going on with the stdio here, my hack is working for me on Bash 4.2, NixOS GNU/Linux. Patching the Bash source-code is definitely a more elegant solution, and it should be pretty easy and safe to do now that I'm using Nix.

    0 讨论(0)
  • 2020-11-27 18:42

    One more possibility: without editing bash source code, using script utility (part of bsdutils package on ubuntu):

    $ TEST_PS1="\e[31;1m\u@\h:\n\e[0;1m\$ \e[0m"
    $ RANDOM_STRING=some_random_string_here_that_is_not_part_of_PS1
    $ script /dev/null <<-EOF | awk 'NR==2' RS=$RANDOM_STRING
    PS1="$TEST_PS1"; HISTFILE=/dev/null
    echo -n $RANDOM_STRING
    echo -n $RANDOM_STRING
    exit
    EOF
    <prints the prompt properly here>
    

    script command generates a file specified & the output is also shown on stdout. If filename is omitted, it generates a file called typescript.

    Since we are not interested in the log file in this case, filename is specified as /dev/null. Instead the stdout of the script command is passed to awk for further processing.

    1. The entire code can also be encapsulated into a function.
    2. Also, the output prompt can also be assigned to a variable.
    3. This approach also supports parsing of PROMPT_COMMAND...
    0 讨论(0)
提交回复
热议问题