2

Recently I wanted to know the best way to get the path to the currently running script/file in Bash.

I found a lot of answers. In particular one was:

"${BASH_SOURCE:-${(%):-%x}}"

I remember deciding at the time that this was the best solution for my needs. Now I'm trying to remember why. I know what some of the pieces mean, but not all. And even with what I do know I can't put it all together to make any sense of it. It works. But why?

Every effort I've made to find this specific code anywhere on the web (including searching for bits of it) has failed, so I can't find where I found it originally.

Can anyone tell me what each piece means (and perhaps even suggest why I might have chosen this over other answers)?

Thanks!


UPDATE next day: great answer (and comments) from @rici which explain everything, including why I chose it.

It seems I chose this entire expression to enable the sourced files I'm using it in to be used reliably in both bash and zsh - which was and is a goal. With the dual substitutions, the entire expression gets the same answer in either shell.

Explains why I couldn't make any sense of (or find anything sensible via google for) the ${(%):-%x} part in bash because... well... it doesn't make much sense in bash, lol, since it's for zsh. I'm now adding a zsh tag to this for that reason.


Second UPDATE an hour later, in case this helps anyone: I've now tracked down this:

${BASH_SOURCE[0]} equivalent in zsh?

... which is where I got the ${(%):-%x} from originally, most notably this specific answer:

https://stackoverflow.com/a/28336473/4516124

DavidT
  • 655
  • 7
  • 19
  • 1
    What version of Bash is this? (`${(%):-%x}` gives me "bad substitution", but I don't have the very latest version of Bash, so maybe it's a new feature?) – ruakh Mar 20 '20 at 23:38
  • I tried it on bash v5.0.7(1), and it's still a bad substitution there. zsh seems to think it's ok (and it does print the script name under zsh), but the `BASH_SOURCE` isn't useful there, so it makes no sense to me. – Gordon Davisson Mar 21 '20 at 00:34
  • I'm on 4.4.23(1)-release and amazingly it works fine. The `BASH_SOURCE` makes sense, but the remainder (use as value if `BASH_SOURCE` not set) is a bit bewildering -- even looking at man bash... Works on 5.0.16(1)-release too. (Where are Ed and Charles when you need 'em...) – David C. Rankin Mar 21 '20 at 00:37
  • @David: It works if BASH_SOURCE is defined, because the substitution isn't really parsed unless it is used. So different ways of testing will produce different results. – rici Mar 21 '20 at 01:01
  • Got it -- it's just a bad substitution all the way around (which explains why my 10 minutes in man bash was fruitless) – David C. Rankin Mar 21 '20 at 01:09
  • My guess is that it's supposed to be a substitution that prints the name of the script it's used in and works for both Bash and zsh. A simple polyglot, so to speak. – Benjamin W. Mar 21 '20 at 01:14
  • 1
    @BenjaminW. Precisely. See my answer. (It took me a while because I was trying to explain "every piece") :.) – rici Mar 21 '20 at 01:24

2 Answers2

7

This peculiar expression is attempting to provide the same result in bash and in zsh: the filepath of the currently running script.

In bash,

"${BASH_SOURCE:-${(%):-%x}}"

means "$BASH_SOURCE if it exists and is not empty and otherwise ${(%):-%x}". As long as you use this in a context in which $BASH_SOURCE is defined as a non-empty value, the otherwise invalid substitution ${(%):-%x} will never be used and bash won't complain about it. [Note 1]

For a detailed explanation of $BASH_SOURCE please see this excellent answer by @mklement0.

Now, in zsh the variable $BASH_SOURCE is normally not defined (although I suppose it might be exported from the parent), so the substitution does happen and zsh then replaces it with the expansion ${(%):-%x}. Bash users might find that one even more mysterious, but we can take it apart as follows:

  1. ${...:-...} means the same thing as in bash: use the right hand part if the left hand part is empty or undefined. Unlike bash, zsh allows the left-hand part to be completely empty, not just a variable whose value is empty. So {:-foo} is a complicated way of writing foo.

  2. ${(flags)...} causes the specified flags to be evaluated on the expansion. In this case, we have the flag %, which means that "prompt expansion" should be done on the parameter expansion. [Note 2]

  3. Without the flags, we're left with {:-%x}. As indicated in point 1, this is equivalent to the string %x. But the (%) flag triggers prompt expansion of %x. And in zsh prompt expansion, %x is -- wait for it -- the name of the currently executing script file.

In short, in zsh, ${(%):-%x} means (almost) exactly the same thing as $BASH_SOURCE does in bash. And therefore, ${BASH_SOURCE:-${(%):-%x}} expands to the current script source file in both bash and zsh.


Notes

  1. If $BASH_SOURCE is undefined or empty, on the other hand, this will produce a "bad substitution". As we'll see in a moment, ${(flags)...} is a zsh-specific syntax, but that's not why bash complains about it. To bash, it looks like a suffix deletion (the % operator) of the non-existent parameter $(. And it complains because $( is not a valid parameter name.

    For comparison, consider the very similar but valid substitution ${#%):-%x}. That has the same value as $#, because $# doesn't end with ):-%x. That parse might be surprising to human eyes, but bash sees it plain as day.

  2. Zsh, like bash, has extremely customisable prompts using special escape sequences in one of the prompt variables. (Posix standard prompt variables are $PS1, $PS2 and $PS4; both bash and zsh offer several others.) Bash offers some special backslash sequences (\u is the current username, for example), and also allows arbitrary parameter expansions to be interpreted when the prompt is expanded. Zsh also has a variety of escape sequences, but uses % as an escape character (%n is the current username), and allows parameter expansions if the prompt_subst shell option is set.

    Note that you usually can't experiment with zsh prompts by simply setting $PS1, as you can in bash, because most zsh installations use "prompt themes". Once prompt themes are enabled, you need to create your own theme in order to change the prompt strings, because the theme settings are automatically applied before every command.

rici
  • 234,347
  • 28
  • 237
  • 341
  • Hmm... that makes a lot of sense. At the time I was looking for this I was looking at switching to zsh from bash at some point in the near future, and was testing some stuff in my sourced files in both shells (still wanting to do that at some time so it would seem I need to keep this in there). This also explains why I couldn't find it again with google, since I imagine it's pretty rare anyone would want to do this. I did know what ${…:-…} means (substitution). I must have constructed this myself, rather than someone proposing it all. I'll add some of this to the "question". :) – DavidT Mar 22 '20 at 00:47
  • One question though... Can you elaborate on "the flag %, ... means that $PS1-style prompt expansion should be done on the rest of the parameter expansion". I don't really understand what that means. – DavidT Mar 22 '20 at 00:48
  • Sorry... I deleted my previous comment about the `(%)` because I realized right away that what I said there was wrong - for zsh. But you still got the notification. Makes sense now I think. And with that information I was able to find the page where I got this from in the first place: https://stackoverflow.com/questions/9901210/bash-source0-equivalent-in-zsh - which in turn explains in more detail some remaining confusion I had (particularly why the `:-` is needed instead of just `${(%)%x}`. Mystery solved and mission accomplished. Thanks! – DavidT Mar 22 '20 at 02:11
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/210084/discussion-between-davidt-and-rici). – DavidT Mar 22 '20 at 02:18
  • To clarify, I think it's mklement0's answer on that page that explains it best though it currently has less votes than the other answer by Hui Zheng (which is still helpful). I'm not sure your comments add much to the answer. I think I failed to read it (your answer) properly the first couple of times. I might have a suggestion for how to improve your answer. Moving this to chat for that. – DavidT Mar 22 '20 at 02:18
1

${parameter:-word} expands to word if parameter is unset. For example:

$ unset a b
$ c=123
$ echo ${a:-${b:-$c}}
123

Here both a and b are unset, therefore c is expanded. The parameter expansion ${(%):-%x} gives a bad substitution error. This is because $( is not a valid parameter. Bash manual defines parameter as:

A parameter is an entity that stores values. It can be a name, a number, or one of the special characters listed below. A variable is a parameter denoted by a name. A variable has a value and zero or more attributes. Attributes are assigned using the declare builtin command (see the description of the declare builtin in Bash Builtins).

and name as:

name: A 'word' consisting solely of letters, numbers, and underscores, and beginning with a letter or underscore. 'Name's are used as shell variable and function names. Also referred to as an 'identifier'.

builder-7000
  • 7,131
  • 3
  • 19
  • 43
  • 2
    It's a bad substitution because `$(` is an invalid parameter name. Compare it with `${*%)}` – rici Mar 21 '20 at 00:46
  • Cool. It's an interesting one because it's really hard to look at `${(%):-%x}` and see `%` as the operator – rici Mar 21 '20 at 20:37