Orthanc Systems

«The Utility Muffin Research Kitchen»

When I said “debug” I meant debug!

4.4BSD’s pmake(1) introduced a beautiful elegance to the crafting of makefiles. But no good deed goes unpunished, and the FreeBSD folks have managed to contort a clear and simple concept into an apocalyptic nightmare of knobs and features that would do a Linux distribution proud.

Back in the day, setting DEBUG_FLAGS=-g would ensure commands and libraries were built with debug symbols, and that optimizations were disabled. But today, FreeBSD cheerfully includes -O2 in CFLAGS despite a declaration of DEBUG_FLAGS being in scope. In the face of modern optimizing compilers, -Oanything is often enough to make it impossible for any debugger to reasonably deal with the resulting object deck.

Now you might think, after examining /usr/share/mk/sys.mk, that saying CFLAGS= DEBUG_FLAGS=-g on the make(1) command line would take care of the -O2 issue. Alas, this has the side effect of clobbering many other additions to CFLAGS the make(1) templates would normally perform. (Try it!)

But there is a way out of this madness. At the top of /etc/make.conf, add the following phrase:

.if !empty(DEBUG_FLAGS)
CFLAGS=
.endif
This gets interpreted (accidentally, I'm sure) at the correct time during the processing of the make(1) templates to give the desired behaviour, avoiding the -O2 initialization of CFLAGS.

It also elides the inclusion of -pipe, which is bundled with the -O2 initialization. This is Mostly Harmless (-pipe has been the compiler default for decades now). But surely it would be better to split the optimization settings off into their own macro that could be manipulated independently, in a manner that doesn't clobber the other CFLAGS settings. (Wasn’t COPT used for this, once upon a time … ?)

Echoes of a Pointless Argument

How much time has been wasted arguing over arguments to echo(1)?

The C shell, the Korn shell, and the Bourne shell all have echo built-in commands, which, by default, is invoked if the user calls echo without a full pathname. See shell_builtins(1). sh’s echo, ksh’s echo, ksh93’s echo, and /usr/bin/echo understand the back-slashed escape characters, except that sh’s echo does not understand \a as the alert character. In addition, ksh’s and ksh93’s echo does not have an -n option. sh’s echo and /usr/bin/echo have an -n option if the SYSV3 environment variable is set (see ENVIRONMENT VARIABLES below). csh’s echo and /usr/ucb/echo, on the other hand, have an -n option, but do not understand the back- slashed escape characters. sh and ksh determine whether /usr/ucb/echo is found first in the PATH and, if so, they adapt the behavior of the echo builtin to match /usr/ucb/echo.

[Grammar aside … Really? Can it possibly be this difficult? (Thanks, Solaris, for the manpage excerpt. But this nonsense dates back to at least Seventh Edition.)

Plan 9 aimed to eliminate that cruft. But even it got it wrong.

Echo should do one thing only: echo. Not translate — just mindlessly parrot what it’s fed, in the manner of political PR hacks everywhere.

echo -n is a recursive descent into madness. How do I echo the string ‘-n’ ? I won’t even begin with the absurd arguments this question leads to. Instead, I present the obvious solution: the echon command.

How hard can it be? echo prints its arguments, then prints a newline. echon prints its arguments, then stops. If you really need to translate backslash-escaped sequences along the way, consult printf(1).

A Prompt Execution

A dynamic shell prompt is a useful tool. I use it to track which host a shell is running on, whom as, and its current directory. But did you know it can be made snarf-n-barf safe? On UNIX it is trivial to create a shell prompt that can be spit back at the shell with no ill effect: simply wrap the prompt between : and ; delimiters:

PS1=': xyzzy;'

The string : xyzzy; is a valid shell command that evaluates to true with no side effects. The : command ignores its arguments and returns a 0 exit value. It’s a built in command in most modern shells, incurring no exec(2) penalty. For shells such as Plan 9’s rc(1), which doesn’t have a built in :, the null function is a suitable replacement. In the worst case, a shell script that executes true(1) can be substituted.

My preferred shell prompt format is

user@hostname:cwd

On the systems I maintain, my login environment is usually an NFS mounted home directory, using either the Bourne or Korn shell. In rare circumstances I must use bash (shudder …). That home directory is shared across a variety of UNIX variants, including all the current BSDs, Solaris, the OpenSolaris derivatives, and a few Linuxen.

Making this work portably across the various shells is most easily handed by defining a few POSIX shell functions:

function setprompt {
        PS1=': '`id -un`@`hostname|sed 's/\..*$//'`:`pwd`; '; export PS1
}

function cd {
        command cd "$@" && setprompt
}

setprompt

I write that in my $HOME/.env file, then say export ENV=${HOME}/.env in my $HOME/.profile. This ensures the functions are instantiated with the correct variable interpretations in each new interactive shell instance. However, be aware that POSIX (well, SUSv3 is the specification I am working with) does not define a way for a function that re-defines a built-in command to invoke the underlying built-in. By layering a function cd over the built-in cd, what I am doing is — by definition — not portable. Fortunately, all the modern Bourne–ish shell implementations seem to provide an escape clause that allows this to work. In my case, the command built-in lets me call down to the shell's cd from within my function, across all the UNIX variants I run. But beware that you might have to special-case your .env file to change this syntax on an OS-specific basis. (Some shells I have come across use local instead of command; others require you to backslash-escape or single-quote the internal command.)

If you use sudo(1), modify the sudoers(5) file to ensure $ENV is preserved, then the customized prompts will follow you around as you switch accounts to do maintenance tasks.

Plan 9 uses a slightly different implementation, to accomodate its environment and shell syntax variations:

# /n/sources/contrib/lyndon/prompt.rc
fn : {}
fn setprompt {
	prompt = (': '^`{cat /dev/user}^@^`{cat /dev/sysname}^':'^`{pwd}^'; '  '	')
}
fn cd { builtin cd $* && setprompt && awd }
setprompt