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