
To get the same output you note in your question, all that is needed is this:
PS1='${PS2c##*[$((PS2c=0))-9]}- > '
PS2='$((PS2c=PS2c+1)) > '
You need not contort. Those two lines will do it all in any shell that pretends to anything close to POSIX compatibility.
- > cat <<HD
1 > line 1
2 > line $((PS2c-1))
3 > HD
line 1
line 2
- > echo $PS2c
0
But I liked this. And I wanted to demonstrate the fundamentals of what makes this work a little better. So I edited this a little. I stuck it in /tmp
for now but I think I'm going to keep it for myself, too. It's here:
cat /tmp/prompt
PROMPT SCRIPT:
ps1() { IFS=/
set -- ${PWD%"${last=${PWD##/*/}}"}
printf "${1+%c/}" "$@"
printf "$last > "
}
PS1='$(ps1)${PS2c##*[$((PS2c=0))-9]}'
PS2='$((PS2c=PS2c+1)) > '
Note: having recently learned of yash, I built it yesterday. For whatever reason it doesn't print the first byte of every argument with the %c
string - though the docs were specific about wide-char extensions for that format and so it maybe related - but it does just fine with %.1s
That's the whole thing. There are two main things going on up there. And this is what it looks like:
/u/s/m/man3 > cat <<HERE
1 > line 1
2 > line 2
3 > line $((PS2c-1))
4 > HERE
line 1
line 2
line 3
/u/s/m/man3 >
PARSING $PWD
Every time $PS1
is evaluated it parses and prints $PWD
to add to the prompt. But I don't like the whole $PWD
crowding my screen, so I want just the first letter of every breadcrumb in the current path down to the current directory, which I'd like to see in full. Like this:
/h/mikeserv > cd /etc
/etc > cd /usr/share/man/man3
/u/s/m/man3 > cd /
/ > cd ~
/h/mikeserv >
There are a few steps here:
IFS=/
<p>we're going to have to split the current <code>$PWD</code> and the most reliable way to do that is with <code>$IFS</code> split on <code>/</code>. No need to bother with it at all afterward - all splitting from here on out will be defined by the shell's positional parameter <code>$@</code> array in the next command like:</p>
<p><code>set -- ${PWD%"${last=${PWD##/*/}}"}</code> </p>
<p>So this one's a little tricky, but the main thing is that we're splitting <code>$PWD</code> on <code>/</code> symbols. I also use parameter expansion to assign to <code>$last</code> everything after any value occurring between the left-most and right-most <code>/</code> slash. In this way I know that if I'm just at <code>/</code> and have only one <code>/</code> then <code>$last</code> will still equal the whole <code>$PWD</code> and <code>$1</code> will be empty. This matters. I also strip <code>$last</code> from the tail end of <code>$PWD</code> before assigning it to <code>$@</code>.</p>
<p><code>printf "${1+%c/}" "$@"</code> </p>
<p>So here - as long as <code>${1+is set}</code> we <code>printf</code> the first <code>%c</code>haracter of each our shell's arguments - which we've just set to each directory in our current <code>$PWD</code> - less the top directory - split on <code>/</code>. So we're essentially just printing the first character of every directory in <code>$PWD</code> but the top one. It's important though to realize this only happens if <code>$1</code> gets set at all, which will not happen at root <code>/</code> or at one removed from <code>/</code> such as in <code>/etc</code>.</p>
<p><code>printf "$last > "</code></p>
<p><code>$last</code> is the variable I just assigned to our top directory. So now this is our top directory. It prints whether or not the last statement did. And it takes a neat little <code>></code> for good measure.</p>
BUT WHAT ABOUT THE INCREMENT?
And then there's the matter of the $PS2
conditional. I showed earlier how this can be done which you can still find below - this is fundamentally an issue of scope. But there's a little more to it unless you want to start doing a bunch of printf \b
ackspaces and then trying to balance out their character count... ugh. So I do this:
PS1='$(ps1)${PS2c##*[$((PS2c=0))-9]}'
<p>Again, <code>${parameter##expansion}</code> saves the day. It's a little strange here though - we actually set the variable while we strip it of itself. We use its new value - set mid-strip - as the glob from which we strip. You see? We <code>##*</code>strip all from the head of our increment variable to the last character which can be anything from <code>[$((PS2c=0))-9]</code>. We're guaranteed in this way not to output the value, and yet we still assign it. It's pretty cool - I've never done that before. But POSIX also guarantees us that this is the most portable way this can be done. </p>
And it's thanks to POSIX-specified ${parameter} $((expansion))
that keeps these definitions in the current shell without requiring that we set them in a separate subshell, regardless of where we evaluate them. And this is why it works in dash
and sh
just as well as it does in bash
and zsh
. We use no shell/terminal dependent escapes and we let the variables test themselves. That's what makes portable code quick.
The rest is fairly simple - just increment our counter for every time $PS2
is evaluated until $PS1
once again resets it. Like this:
PS2='$((PS2c=PS2c+1)) > '
So now I can:
DASH DEMO
ENV=/tmp/prompt dash -i
/h/mikeserv > cd /etc
/etc > cd /usr/share/man/man3
/u/s/m/man3 > cat <<HERE
1 > line 1
2 > line 2
3 > line $((PS2c-1))
4 > HERE
line 1
line 2
line 3
/u/s/m/man3 > printf '\t%s\n' "$PS1" "$PS2" "$PS2c"
$(ps1)${PS2c##*[$((PS2c=0))-9]}
$((PS2c=PS2c+1)) >
0
/u/s/m/man3 > cd ~
/h/mikeserv >
SH DEMO
It works the same in bash
or sh
:
ENV=/tmp/prompt sh -i
/h/mikeserv > cat <<HEREDOC
1 > $( echo $PS2c )
2 > $( echo $PS1 )
3 > $( echo $PS2 )
4 > HEREDOC
4
$(ps1)${PS2c##*[$((PS2c=0))-9]}
$((PS2c=PS2c+1)) >
/h/mikeserv > echo $PS2c ; cd /
0
/ > cd /usr/share
/u/share > cd ~
/h/mikeserv > exit
As I said above, the primary problem is that you need to consider where you do your computation. You don't get the state in the parent shell - so you don't compute there. You get the state in the subshell - so that's where you compute. But you do the definition in the parent shell.
ENV=/dev/fd/3 sh -i 3<<\PROMPT
ps1() { printf '$((PS2c=0)) > ' ; }
ps2() { printf '$((PS2c=PS2c+1)) > ' ; }
PS1=$(ps1)
PS2=$(ps2)
PROMPT
0 > cat <<MULTI_LINE
1 > $(echo this will be line 1)
2 > $(echo and this line 2)
3 > $(echo here is line 3)
4 > MULTI_LINE
this will be line 1
and this line 2
here is line 3
0 >
man 1 mktemp
. – goldilocks Apr 15 '14 at 16:39