3

I want to write a PROMPT_COMMAND that's responsive to whatever was provided to the command prompt immediately previous. For example, to switch between an expansive, informative prompt or simple, compact prompt, like so:

mikemol@serenity ~ $ echo hi
hi
$ echo ho
ho
$ echo hum
hum
$
mikemol@serenity ~ $

Values only appear to get added to the shell's history if they're not empty, so I can't simply test if the most recent history entry is blank. Running set | grep some_command after running some_command gives me nothing, so it doesn't appear there's an environment variable containing that information.

I principally use bash, but would be curious about POSIX-compatible solutions and other shells.

2
  • No; if you hit enter a half dozen times at the prompt, no blank lines are added to the listing that fc -l provides. Apr 28, 2017 at 18:08
  • That might do it. Will have to wait until Monday to try, though. Apr 28, 2017 at 23:54

1 Answer 1

2

I ultimately didn't need PROMPT_COMMAND at all. Thanks to Christopher for pointing me in the right direction.

Instead, consider this file, ps1.prompt:

${__cmdnbary[\#]+$(
    echo '\u@\h: \w' # Your fancy prompt goes here, with all the usual special characters available.
) }${__cmdnbary[\#]=}\$

I can then feed this into my PS1:

PS1=$(cat ps1.prompt)

(You don't have to do it this way, but I found it convenient for illustration and editing.)

And so we see:

mikemol@zoe:~$ echo hi
hi
mikemol@zoe:~$ echo ho
ho
mikemol@zoe:~$ echo hum
hum
mikemol@zoe:~$ 
mikemol@zoe:~$ PS1=$(cat ps1.prompt)
$ 
mikemol@zoe: ~ $ echo hi
hi
$ echo ho
ho
$ echo hum
hum
$ 
mikemol@zoe: ~ $ 

We're using the array hack demonstrated here, but instead of using bash's ${parameter:-word} parameter substitution, we use ${parameter+word} so we trigger only on there having been no previous command run.

This requires some explanation, as we're forced to use a double-negative in our logic.

How ${__cmdnbary[\#]-word}${__cmdnbary[\#]=} works

In the original array hack demonstration, the construct ${__cmdnbary[\#]-word}${__cmdnbary[\#]=} was used. (I've replaced $? with word for clarity). If you're not particularly familiar with parameter expansion and arrays (I wasn't), it's not at all clear what's happening.

First, understand \#

Per the manual:

\# the command number of this command

...

The command number is the position in the sequence of commands executed during the current shell session.

This means \# will only change if and only if a command is executed. If the user enters a blank line at the prompt, no command is executed, and so \# won't change.

The setting of an empty string in ${__cmdnbary[#]=}

${__cmdnbary[\#]=} uses paramater expansion. Going back to the manual:

${parameter:=word}

Assign Default Values. If parameter is unset or null, the expansion of word is assigned to parameter. The value of parameter is then substituted.

So, if __cmdnbary[\#] is unset or null, this construct will assign an empty string (word is an empty string in our case) and the whole construct will be replaced in our output with that same empty string.

__cmdnbary[\#] will always be unset or null the first time we see it, since # is monotonic--it always increments or stays the same. (That is, until it loops, likely around 2^31 or 2^63, but there are other problems we'll have long before we get there. There's a reason I describe the solution as a bit of a hack.)

The conditional in ${__cmdnbary[\#]-word}

${__cmdnbary[\#]-word} is another parameter expansion. From the manual:

${parameter:-word}

Use Default Values. If parameter is unset or null, the expansion of word is substituted. Otherwise, the value of parameter is substituted.

So, if the array entry at \# is unset or null, word gets used in its place. Since we don't try to assign to __cmdnbary[\#] (using the ${parameter:=word} substitution) until after we check it, the first time we check it for a given \# should find that position in the array unset.

bash uses sparse arrays

One point of clarification for those accustomed to C-style arrays. bash actually uses sparse arrays; until you assign something to a position in an array, that position is unset. An empty string is not the same thing as "null or unset".

Why we use ${__cmdnbary[#]+word}${__cmdnbary[#]=} instead

${__cmdnbary[\#]+word}${__cmdnbary[\#]=} and ${__cmdnbary[#]-word}${__cmdnbary[#]=}look very siilar. The *only* thing we change between the two constructs can be found in the first portion; we use${parameter:+word}instead of${parameter:-word}`.

Remember that with ${parameter:-word}, word gets presented only if parameter is null or unset--in our case, only if we haven't set the position in the array yet, which we won't have done if and only if \# has incremented, which will only happen if we've just executed a command.

That means that, with ${parameter:-word}, we'll only present word if we haven't executed a command, which is precisely the opposite of what we want to do. So, we use ${parameter:-word} instead. Again, from the manual:

${parameter:+word}

Use Alternate Value. If parameter is null or unset, nothing is substituted, otherwise the expansion of word is substituted.

Which is (unfortunately), more double-negative logic to understand, but there you are.

The prompt itself

We've explained the switching mechanism, but what about the prompt itself?

Here, I use $( ... ) to contain the meat of the prompt. Primarily for my own benefit for readability; you don't have to do it that way. You can replace $( ... ) with whatever you might normally stuff in a variable assignment.

Why is it a hack?

Remember how we're adding entries to a sparse array? We're not removing those entries, and so the array will forever grow until the shell session is exited; the shell is leaking through PS1. And so far as I know, there's no way to unset a variable or array position in the prompt. You could try in $(), but you'll find it won't work; changes made to the variable namespace inside a subshell won't be applied to the space the subshell was forked from.

You might try using mktmp early in your .bashrc, before PS1 assignment, and stuff information in the resulting file; then you could compare your current \# against what you've stored in there, but now you've made your prompt dependent on disk I/O, which is a good way to lock yourself out in emergency situations.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.