17

I’d like to implement a function in Bash which increases (and returns) a count with every call. Unfortunately this seems non-trivial since I’m invoking the function inside a subshell and it consequently cannot modify its parent shell’s variables.

Here’s my attempt:

PS_COUNT=0

ps_count_inc() {
    let PS_COUNT=PS_COUNT+1
    echo $PS_COUNT
}

ps_count_reset() {
    let PS_COUNT=0
}

This would be used as follows (and hence my need to invoke the functions from a subshell):

PS1='$(ps_count_reset)> '
PS2='$(ps_count_inc)   '

That way, I’d have a numbered multi-line prompt:

> echo 'this
1   is
2   a
3   test'

Cute. But due to the above mentioned limitation doesn’t work.

A non-working solution would be to write the count to a file instead of a variable. However, this would create a conflict between multiple, simultaneously running sessions. I could append the process ID of the shell to the file name, of course. But I’m hoping there’s a better solution which won’t clutter my system with lots of files.

4 Answers4

15

enter image description here

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 &gt; "</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>&gt;</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 \backspaces 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 >
mikeserv
  • 58,310
  • @KonradRudolph The variable is changed as a result of the function's output. You see? It can read the variable after it's changed, but needn't directly change it itself because we do that by processing its output. The for loop is completely unnecessary by the way, it was just the easiest way to demo it. – mikeserv Apr 15 '14 at 15:07
  • @mikeserv This doesn't provide a way to change the value of count between uses of PS2, however. – chepner Apr 15 '14 at 15:42
  • @KonradRudolph You can change the value of count, of course, within the subshell. You then have to print to count. That's all. – mikeserv Apr 15 '14 at 15:55
  • @KonradRudolph The consensus is wrong. Of course you can change it in the subshell. count=2; count=$(count=3 ; echo $count) ; echo $count ; output 3 – mikeserv Apr 15 '14 at 16:00
  • @KonradRudolph This is no work-around. This is scope. Plain and simple. It's how it works - you just need to take that in, man. You do what logic you need to arrive at the value you need in the subshell, then you print it to the parent shell and absorb that output into your counter. – mikeserv Apr 15 '14 at 16:04
  • @KonradRudolph see above edit. Does it behave as you expect? – mikeserv Apr 15 '14 at 16:21
  • 1
    @KonradRudolph what's to stop you from defining them twice? Which is what my original thing did... I have to look at your answer... This is done all of the time. – mikeserv Apr 15 '14 at 16:28
  • 1
    @mikeserv Type echo 'this at a prompt, then explain how to update the value of PS2 before typing the closing single quote. – chepner Apr 15 '14 at 17:12
  • @KonradRudolph - I did get it to work. See above. – mikeserv Apr 15 '14 at 17:37
  • I would use ((PS2c=PS2c+1)) so that the value of PS2c can be used more flexibly in the value of PS2, but the latest update looks good. – chepner Apr 15 '14 at 17:40
  • @chepner - I don't do bashisms. But how is that any different from what I did? – mikeserv Apr 15 '14 at 17:43
  • $((...)) is an expression, so it needs to be used in a context where a value is expected. ((...)) is a standalone command, so you can use it to update the value of PS2c without having to use it in another command. For example, ps2 () { ((PS2c=PS2c+1)); printf "prompt $PS2c -> "; }. – chepner Apr 15 '14 at 17:52
  • @chepner, I understand what you mean, now. You could still do that here without the bashism : ps2() { printf '$((PS2c=PS2c+1))\b' ; } – mikeserv Apr 15 '14 at 17:58
  • 1
    Okay, this answer is now officially amazing. I also like the breadcrumbs, although I won’t adopt it since I print the full path in a separate line anyway: http://i.imgur.com/xmqrVxL.png – Konrad Rudolph Apr 16 '14 at 07:51
  • 1
    @KonradRudolph - well, it was a really good question. And it was a challenge, so I stuck with it. And thanks for not hating me for trying. I can be... persistent. – mikeserv Apr 16 '14 at 08:00
  • @RichardHansen - thanks twice, actually - I don't need to restore $IFS in any way. I don't know why I bother trying. That must have been a holdover from my trying at other methods. The function only ever sets in the $(command substitution) subshell anyway - the second assignment is useless as any word splitting from that point is already defined by the arg $@. I'll just leave $IFS set to / for the duration of the function - it will make no difference and it's one less shell memory op per $PS1 eval. – mikeserv Apr 17 '14 at 20:31
  • @RichardHansen - I did kind of blur the lines a little here, which is probably why I forgot as well. If you hadn't mentioned it might have been awhile before someone else did. – mikeserv Apr 17 '14 at 20:44
  • @JohnB - This is convoluted? PS1='${PS2c##*[$((PS2c=0))-9]}- > '; PS2='$((PS2c=PS2c+1)) > ' – mikeserv Mar 28 '15 at 17:50
  • 1
    No, it was a wrong choice of words. It's actually a cool solution. However, the parts about c, yash, and and especially your personal preferences with how PWD is displayed initially just seem unnecessary. The other solutions don't require much further explanation than the code itself. – John B Mar 28 '15 at 20:07
  • @JohnB - The other solutions mostly don't work. This one does. At the time it was written, most reading it didn't believe it. And so it took on a shape. This doesn't need any explanation but the code itself - and that's why the code is two sentences at the very top. If you choose to read more, good for you. – mikeserv Mar 28 '15 at 23:50
  • 1
    The other solutions mostly do work. And I agree that this is probably the most elegant soltion, but this site is not a blog, and half of the post explains in detail things not relevant to the question or the OP specific solution. – John B Mar 29 '15 at 03:14
  • @JohnB - when the other solutions work, it is only because certain special criteria are met - criteria of a kind this solution does not require. In any case, this answer was not for you, but for the asker, whose opinion you can find above. Now, I'll thank you to keep your opinions to yourself. – mikeserv Mar 29 '15 at 03:26
8

With this approach (function running in a subshell) you aren't going to be able to update the master shell process's state without going through contortions. Instead, arrange for the function to run in the master process.

The value of the PROMPT_COMMAND variable is interpereted as a command which is executed before printing the PS1 prompt.

For PS2, there's nothing comparable. But you can use a trick instead: since all you want to do is an arithmetic operation, you can use arithmetic expansion, which doesn't involve a subshell.

PROMPT_COMMAND='PS_COUNT=0'
PS2='$((++PS_COUNT))  '

The result of the arithmetic computation ends up in the prompt. If you want to hide it, you can pass it as an array subscript that doesn't exist.

PS1='${nonexistent_array[$((PS_COUNT=0))]}\$ '
4

It's a bit I/O-intensive, but you'll need to use a temporary file to hold the value of the count.

ps_count_inc () {
   read ps_count < ~/.prompt_num
   echo $((++ps_count)) | tee ~/.prompt_num
}

ps_count_reset () {
   echo 0 > ~/.prompt_num
}

If you are concerned about needing a separate file per shell session (which seems like a minor concern; will you really be typing multi-line commands in two different shells at the same time?), you should use mktemp to create a new file for each use.

ps_count_reset () {
    rm -f "$prompt_count"
    prompt_count=$(mktemp)
    echo 0 > "$prompt_count"
}

ps_count_inc () {
    read ps_count < "$prompt_count"
    echo $((++ps_count)) | tee "$prompt_count"
}
chepner
  • 7,501
  • +1 The I/O is probably not very significant since if the file is small and being frequently accessed, it will be cached, i.e., it's essentially functioning as shared memory. – goldilocks Apr 15 '14 at 15:53
1

You can not use a shell variable this way and you already understand why. A subshell inherits variables exactly the same way a process inherits its environment: any changes made apply only to it and its children and not to any ancestor process.

As per other answers the easiest thing to do is stash that data in a file.

echo $count > file
count=$(<file)

Etc.

goldilocks
  • 87,661
  • 30
  • 204
  • 262
  • Of course you can set a variable in this way. You do not need a temp file. You set in the variable in the subshell and print its value to parent shell where you absorb that value. You get all of the state you need to compute its value in the subshell so that's where you do it. – mikeserv Apr 15 '14 at 16:07
  • 1
    @mikeserv That's not the same thing, which is why the OP has said such a solution will not work (although this should have been made more clear in the question). What you are referring to is passing a value to another process via IPC so that it can assign that value to whatever. What the OP wanted/needed to do was affect the value of a global variable shared by a number of processes, and you cannot do that via the environment; it is not very useful for IPC. – goldilocks Apr 15 '14 at 16:17
  • Man, either I've completely misunderstood what is needed here, or everyone else has. It seems really simple to me. You see my edit? What's wrong with it? – mikeserv Apr 15 '14 at 16:27
  • @mikeserv I don't think you've misunderstood and to be fair, what you have is a form of IPC and could work. It's not clear to me why Konrad doesn't like it, but if it isn't flexible enough, then the file stash is pretty straightforward (and so are ways to avoid collisions, e.g. mktemp). – goldilocks Apr 15 '14 at 16:38
  • 2
    @mikeserv The intended function is called when the value of PS2 is expanded by the shell. You don't have an opportunity to update the value of a variable in the parent shell at that time. – chepner Apr 15 '14 at 17:16