prompt-engineering

a low-resource dynamic shell prompt for bash
git clone http://git.permacomputing.net/repos/prompt-engineering.git # read-only access
Log | Files | Refs | README

README.md (6627B)


      1 # Prompt Engineering
      2 
      3 ## A dynamic low-resource shell prompt for bash
      4 
      5  If you're opening a shell from your own desktop system, it just shows the path:
      6 
      7 ```console
      8    ~$
      9 ```
     10 
     11  If you got to this shell via sudo, or doas, we show the usernames involved:
     12 
     13 ```console
     14    root<myuser>@~#
     15 ```
     16 
     17  If you came via ssh, we show the hostname and username:
     18 
     19 ```console
     20    remoteuser@remotehost:~$
     21 ```
     22 
     23  We display non-zero values for command exit codes and background/suspended jobs:
     24 
     25 ```console
     26    root<remoteuser>@remotehost:/var/www/$ less index.html
     27    [1]+  Stopped                    less index.html
     28    (148)[1]root<remoteuser>@remotehost:/var/www/$ 
     29 ```
     30 
     31  We also support $debian_chroot and the git shell status if available:
     32 
     33 ```console
     34    (buildchroot)remoteuser@remotehost:~/src/project{main +}$
     35 ```
     36 
     37  Colours have been chosen to highlight urgent information, such as non-zero
     38  exit codes blinking the $ or # as appropriate.
     39 
     40 # Installation
     41 
     42 The smoothest way to install this system-wide is to clone this repository somewhere visible, such as `/opt` or somewhere under `/usr/local` or something.  Then symlink it into a directory your system uses for bash startup scripts.  For example on Debian:
     43 
     44 ```
     45 cd /opt
     46 git clone http://git.permacomputing.net/repos/prompt-engineering.git
     47 cd /etc/bash_completion.d
     48 ln -sf /opt/prompt-engineering/prompt.sh
     49 ```
     50 
     51 or on Alpine-derived systems, you'd do something like this:
     52 
     53 ```
     54 cd /opt
     55 git clone http://git.permacomputing.net/repos/prompt-engineering.git
     56 cd /etc/bash
     57 ln -sf /opt/prompt-engineering/prompt.sh
     58 ```
     59 
     60 You may need to tinker with which directory runs most reliably on interactive shell session startup for your system type.
     61 
     62 # Implementation
     63 
     64 The `bash(1)` manual page describes the behaviour of `$PS1` as follows:
     65 
     66 >  After the string is decoded, it is expanded via  parameter expansion, command substitution, arithmetic expansion, and quote removal
     67 
     68 This means that we theoretically *could* call external programs to glean information between commands at the shell prompt.  We could also use `$PROMPT_COMMAND` to set up the environment each time.  Some systems (such as [PowerLine](https://powerline.readthedocs.io/en/master/usage/shell-prompts.html) or [Starship](https://starship.rs/)) do this, and achieve stunning effects at the cost of some fairly heavyweight computation.
     69 
     70 The inspiration for this prompt came from the way Debian displays the value of the `$debian_chroot` varible iff it is defined:
     71 
     72 ```
     73 ${debian_chroot:+($debian_chroot)}
     74 ```
     75 
     76 This is what the `bash(1)` man page meant by "parameter expansion" above: `${foo:+bar}` means "If `$foo` is defined, ignore its value and use `"bar"` instead."  This lets Debian show the variable contents wrapped in parentheses, but doesn't get stuck showing empty parens if you're not in a compatible chroot.  Incidentally, this variable is set via `/etc/debian_chroot`, and it can be handy to make that show system roles on non-chroot environments.
     77 
     78 ## Exit Codes
     79 
     80 The `$?` variable shows the exit code of the last command that ran.  We use this value to do `if`/`elif`/`else`/`fi` blocks, and to conditionally chain commands with `||` or `&&`.  We can do a trick with parameter expansion to only display it if it is non-zero, like so:
     81 
     82 ```
     83 echo ${?#0}
     84 ```
     85 
     86 This means "chop leading zeroes off of `$?` before displaying it."  Since the only time there's a `0` at the front is when it's just `0`, this works quickly and efficiently.  Unfortunately, it's not as pretty as the `$debian_chroot` example above.  For example, if we run `PS1='${?#0}\$ `, we get something like the following:
     87 
     88 ```
     89 $ true
     90 $ false
     91 1$
     92 ```
     93 
     94 This is already useful!  But since a value of `0` is still *defined*, `${?:+($?)}` won't work for us.  It will always show a 0, which is distracting noise that could hide important program failure codes.
     95 
     96 ### Array Indices
     97 
     98 Bash supports arrays, which can let us use numbers as indices into them.  It also supports integer arithmetic such as `echo $(( 22 / 7 ))`.  We can combine these to come up with a test that lets us output text iff something is non-zero:
     99 
    100 ```
    101 _noop[0]='array accesses turn 0/nonzero into defined/undef!'
    102 PS1='${_noop[$(($?==0))]:+($?)}\$ '
    103 ```
    104 
    105 If `$?` is `0`, the expression `$(($?==0))` will be `1`, which isn't defined.  Anything else will match `${noop[0]}` and return `($?)`.  Now our shell looks like this:
    106 
    107 ```
    108 $ true
    109 $ false
    110 (1)$ 
    111 ```
    112 
    113 ## `$XDG_SESSION_TYPE`
    114 
    115 The FreeDesktop folks and systemd give us some environment variables that tell us whether we're on a full local desktop environment or on the other end of a terminal connection of some sort.  The one I use is `$XDG_SESSION_TYPE`.
    116 
    117 This variable is set to `"tty"` when you ssh, sudo, doas, or otherwise connect to a new shell environment via terminal-based tools.  It's often set to something like `"wayland"` when it's a local desktop.
    118 
    119 This is new enough that we can't rely on it, and not everyone is eager to follow these standards.  But we can use it along with other information:
    120 
    121 ```
    122 unset _tty
    123 _tty=$SSH_CONNECTION$SUDO_USER$DOAS_USER
    124 [ "$XDG_SESSION_TYPE" = "tty" ] && _tty="yes"
    125 ```
    126 
    127 We make sure `$_tty` is defined iff sudo, doas, or ssh have set an environment variable pointing back at some original shell session, or if `$XDG_SESSION_TYPE` is `"tty"`.
    128 
    129 ## hostnames and usernames
    130 
    131 We also use this information to decide whether to display the username or hostname.  If you're on your local system, you already know your username and hostname.  So we do something like the following:
    132 
    133 ```
    134 PS1='${_tty:+\u${SSH_CONNECTION:+\h:}\w\$ '
    135 ```
    136 
    137 This would result in the following if you opened up a terminal on your desktop:
    138 
    139 ```
    140 ~$ 
    141 ```
    142 
    143 But if someone ssh'd into your desktop with the same `$PS1`, they'd see this:
    144 
    145 ```
    146 someone@yourdesktop:~$ 
    147 ```
    148 
    149 ## Colo[u]r
    150 
    151 Detecting colour is likewise hard to do portably, so we use some hints.  First, if the `$TERM` variable has the word `"color"` in it, we'll consider that good enough.  A lot of terminals these days use strings like `tmux-256color-bce` to indicate features, so this catches quite a few.  We also match some other patterns (to catch `foot`, `kitty`, `alacritty`, and `ghostty`) and call it done.
    152 
    153 But this doesn't get everyone.  The `ncurses` system (which was more popular back when terminals used loads of different formats for colour codes) includes a program called `tput` which can test colour support quite simply.  This is our one forked off process, run once at the time the prompt is loaded:
    154 
    155 ```
    156 case "$TERM" in 
    157 	*[is]tty*|foot*|*color*) color_prompt=yes;; 
    158 	*) [ -x /usr/bin/tput ] && tput setaf 1 >&/dev/null && color_prompt=yes;;
    159 esac
    160 ```
    161