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=/
we're going to have to split the current $PWD
and the most reliable way to do that is with $IFS
split on /
. No need to bother with it at all afterward - all splitting from here on out will be defined by the shell's positional parameter $@
array in the next command like:
set -- ${PWD%"${last=${PWD##/*/}}"}
So this one's a little tricky, but the main thing is that we're splitting $PWD
on /
symbols. I also use parameter expansion to assign to $last
everything after any value occurring between the left-most and right-most /
slash. In this way I know that if I'm just at /
and have only one /
then $last
will still equal the whole $PWD
and $1
will be empty. This matters. I also strip $last
from the tail end of $PWD
before assigning it to $@
.
printf "${1+%c/}" "$@"
So here - as long as ${1+is set}
we printf
the first %c
haracter of each our shell's arguments - which we've just set to each directory in our current $PWD
- less the top directory - split on /
. So we're essentially just printing the first character of every directory in $PWD
but the top one. It's important though to realize this only happens if $1
gets set at all, which will not happen at root /
or at one removed from /
such as in /etc
.
printf "$last > "
$last
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 >
for good measure.
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]}'
Again, ${parameter##expansion}
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 ##*
strip all from the head of our increment variable to the last character which can be anything from [$((PS2c=0))-9]
. 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.
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 >
The choice of the title of your question is a bit confusing.
pushd
/popd
, a csh
feature copied by bash
and zsh
, are a way to manage a stack of remembered directories.
pushd /some/dir
pushes the current working directory onto a stack, and then changes the current working directory (and then prints /some/dir
followed by the content of that stack (space-separated).
popd
prints the content of the stack (again, space separated) and then changes to the top element of the stack and pops it from the stack.
(also beware that some directories will be represented there with their ~/x
or ~user/x
notation).
So if the stack currently has /a
and /b
, the current directory is /here
and you're running:
pushd /tmp/whatever
popd
pushd
will print /tmp/whatever /here /a /b
and popd
will output /here /a /b
, not /tmp/whatever
. That's independent of using command substitution or not. popd
cannot be used to get the path of the previous directory, and in general its output cannot be post processed (see the $dirstack
or $DIRSTACK
array of some shells though for accessing the elements of that directory stack)
Maybe you want:
pushd "$(mktemp -d)" &&
popd &&
rmdir "$OLDPWD"
Or
cd "$(mktemp -d)" &&
cd - &&
rmdir "$OLDPWD"
Though, I'd use:
tmpdir=$(mktemp -d) || exit
(
cd "$tmpdir" || exit # in a subshell
# do what you have to do in that tmpdir
)
rmdir "$tmpdir"
In any case, pushd "$(mktemp -d)"
doesn't run pushd
in a subshell. If it did, it couldn't change the working directory. That's mktemp
that runs in a subshell. Since it is a separate command, it has to run in a separate process. It writes its output on a pipe, and the shell process reads it at the other end of the pipe.
ksh93 can avoid the separate process when the command is builtin, but even there, it's still a subshell (a different working environment) which this time is emulated rather than relying on the separate environment normally provided by forking. For example, in ksh93
, a=0; echo "$(a=1; echo test)"; echo "$a"
, no fork is involved, but still echo "$a"
outputs 0
.
Here, if you want to store the output of mktemp
in a variable, at the same time as you pass it to pushd
, with zsh
, you could do:
pushd ${tmpdir::="$(mktemp -d)"}
With other Bourne-like shells:
unset tmpdir
pushd "${tmpdir=$(mktemp -d)}"
Or to use the output of $(mktemp -d)
several times without explicitly storing it in a variable, you could use zsh
anonymous functions:
(){pushd ${1?} && cd - && rmdir $1} "$(mktemp -d)"
Best Answer
from
man bash
: