The Woodshed

Behind here, no one can hear you scream

Prompting Expletives 2

Electric Boogaloo

Alright, picking up where I left off previously

After abbreviating long hostnames, I thought it’d be neat to add some color to my prompt. I’d never used the standard mechanism for setting font properties in a console before; basically how it works is, you type an escape sequence representing a certain non-printing character, and any text following that gets the chosen attribute. Unsurprisingly, the attribute codes consist of dense, unreadable gobbledygook. So, some variables:

norm_color='\033[33m'   # The "33" before the "m" represents the color yellow
abbr_color='\033[1;32m' # "1;32" means "bold text, green color"
no_color='\033[0m'      # "0" undoes the effect of previous attributes

The above took some work to get correct. These sequences are called “ANSI escape codes.” ANSI stands for “A Needed Standard, Ignored”, I assume, since support for these “standardized” sequences varies from terminal to terminal. Indeed, if you do a bit of Googling, you’ll see various ways of representing the Escape character (\e, \E, \ESC x1B, \033 were a few examples), different meanings assigned to the numbers separated by the semicolon, different colors represented by the same numbers, etc. In the original code I adapted which used ANSI color setting, it used \e which worked exactly half the time on my machine (it was properly understood by Bash when embedded in the PS1 string, but not understood by my shell’s echo builtin).

Fun fact: if you want to test these out on your terminal, you’ll probably have to pass an argument (usually -e) to echo to have them be understood at all.

Funner fact: if you want to use non-printing characters (such as the color sequences) in your PS (prompt string) definition, you have to enclose them in square brackets. Square brackets with backslashes. So:

norm_color='\[\033[33m\]'
abbr_color='\[\033[1;32m\]'
no_color='\[\033[0m\]'

I wanted directory separators in my path to always be the normal color, and didn’t feel like typing that out every time:

dir_sep="$norm_color/"

(Just in case you are super new to (ba)sh, note that the $ sigil announces to the shell that the word following is a variable whose value should be substituted. Don’t make the assumption that it works like Perl/PHP; if you use that sigil in an assignment statement like $foo=bar, the shell will substitute the current value (or lack thereof) of foo before interpreting the rest of the expression. So what actually gets executed might be something like 42=bar—probably an unrecognized command.)

(Also note that everything you type is interpreted as a string—you don’t need to surround strings with quotes to identify them as such to the interpreter. [The quotes have a different meaning.] The above could equivalently have been written dir_sep="$norm_color"/)

The code I adapted also had a cute line for changing the color of your username based on whether or not you are the superuser:

[ "$UID" -eq 0 ] && user_color='\[\033[1;31m\]' || user_color='\[\033[1;34m\]'

Ultimately, I ended up abandoning this line. (Not that I didn’t like the concept, but because in practice I had a hell of a time getting my prompt [and specifically, the custom function I call to set it] to properly carry over through the various combinations of sudo and su I was using to test it out. Sometimes I set arbitrary constraints on things, though [such as in this case, I made it a “requirement” that I wouldn’t edit any system-wide files like /etc/bashrc or /etc/sudoers to accommodate this playing around]. I think it’s a mental condition.)

However, I left the line in this blog post because it’s pretty Bashy. It works similarly to the “ternary operator” in C, and looks strikingly similar to an idiom used in Lua and other languages. As you’ve gleaned by now, looks are deceiving, and it actually works differently (which I think is the “theme” of these Bash blogs). I won’t go into detail, but know that it’s not using traditional Boolean logic or operator precedence to do what it’s doing.

Oh, and the way the original author got it working is by putting all of his code into a quoted literal string which he embedded in the PS1 prompt string variable; thus when the shell would eval $PS1, his code would get executed (even in superuser context, assuming PS1 is exported and he switches users in a way that keeps that environment variable).

I imagine that what constitutes “eval abuse” in Bash is much hazier than in other dynamic languages that allow the practice. Still, that seemed pretty ugly to me, and I wanted to keep things in a function. (Doing so is a bit more efficient [not that shell scripts are an area where you probably care about efficiency much] in addition to being much cleaner organization-wise.)

So with that in mind, I can start pasting from my actual code:

COLOR_ABBR_PS () {
    local norm_color='\[\033[33m\]'
    local abbr_color='\[\033[1;32m\]'
    local user_color='\[\033[1;34m\]'
    local no_color='\[\033[0m\]'
    local dir_sep="$norm_color/"
# ...
}

I found function declarations in Bash to be quite amusing. For one thing, the () that looks like it’s there for specifying arguments actually isn’t for arguments at all—those parens are merely there to indicate to the interpreter that COLOR_ABBR_PS is a function definition. (Alternatively, you can use the keyword function and omit the parens, but apparently that is “less portable” to other shells.) Indeed, the way you actually refer to function arguments is (surprise!) more retarded: Bash defines some global-ish variables for you during script execution named $1, $2 etc. that refer to the arguments passed to that script. Then for some reason, in a function definition, it replaces those “positional parameter variables,” making them refer instead to that function’s arguments. Oh, and I thought this remark in the manual was funny too: “When a positional parameter consisting of more than a single digit is expanded, it must be enclosed in braces.” Hooray for arbitrary rules!

It came as a surprise to me that given the aforementioned parameter silliness, as well as general absence of namespacing and scoping rules in Bash, that function-local variables are actually supported (sort-of; it’s not true lexical scoping). Within a function definition you can use the local built-in command when you declare a variable, and then that variable’s name won’t hang around after the function returns.

Here I am, writing too many words again. Weirder stuff awaits, I promise.