Relative shell script includes with realpath on FreeBSD
patmaddox.com
patmaddox.com
* mktemp -d (mktemp itself is available via POSIX m4; the noclobber solution mentioned is moot, since FIFOs break it https://austingroupbugs.net/view.php?id=1364), very much needed and available on GNU, *BSD and MacOS.
* local or typeset: I think only ksh88/93 doesn't have it without using the non-standard "function" keyword, a runtime check with potential alias creation is my only "solution" (https://git.sr.ht/~q3cpma/scripts/tree/b21de0bfb0a9534469089...)
* realpath/readlink -f/-e: as said by some, realpath is getting standardized in the next POSIX; can be emulated via a cumbersome function (https://git.sr.ht/~q3cpma/scripts/tree/b21de0bfb0a9534469089...) but since you either put that massive block in your script or need realpath to reliably source it from another file...
* All the "newline in paths" mess: I gave up, just too many blind spots.
`xargs` is close, but only handles a few kinds of quotes, not e.g. `$''` which is the best kind of quoting, and notably does not adapt to the current shell.
`printf %q` is also pretty important to avoid injection attacks, though if you don't care about ugliness or performance you can hack it together pretty easily (but beware of trailing newlines in subshells!).
(also, I am obligated to point out that BSDers are lying when they say they're using "Bourne shell". FreeBSD uses a fork of `ash`, related to `dash` and `busybox ash`, but all 3 have evolved independently. OpenBSD uses a derivative of `pdksh` (other BSDs also ship derivatives but not as /bin/sh). NetBSD has their own from-scratch shell, but NetBSD is dead and irrelevant. I have a whole tree of shells; `ash` is notable for being the most unpredictable within the tree.)
https://stackoverflow.com/questions/29832037/how-to-get-scri...
LIB="$(dirname "$(realpath "$0")")"
It seems like overkill, but just get in the habit of quoting anything and everything.https://unix.stackexchange.com/questions/68694/when-is-doubl...
The extra quotes don't hurt, but also: use shellcheck. It will tell you if you've omitted necessary quotes.
I have the habit of quoting everything, that way I don't have to think about any corner case where I can skip quotes, and thus conversely never miss anything where I should quote.
I do the same with curly braces, that way I don't have to think whether "${foo_bar}" or "${foo}_bar" was meant, and makes later adding or reading string manipulation (e.g "${haystack/needle/replacement}") or arrays (e.g "${foo[42]}" or "${foo[@]}" that much more consistent: variable references are always bounded by ${} which makes code easier to scan.
I apply that to "${1}" and "${@}" too whenever possible, unfortunately bash 3.x doesn't quite like that so it's not always the case (I can't decide which one I hate more: macOS for the refusal to either update bash or outright drop it† and just have a plain posix sh + zsh, or GNU for the viral license move which is highly detrimental in practice for that case).
I also often quote "semantically", e.g:
# instead of:
PATH="/some/path/${foo}/bar:${PATH}"
/some/command "--foo=${bar}"
# I'd rather do:
PATH="/some/path/${foo}/bar":"${PATH}"
/some/command --foo="${bar}"
† I guess there's a crapton of bash scripts that have been written and possibly embedded in apps or installers, so backwards compatibility won here.But I quote everything, always, because then I don’t have to rely on remembering when it’s necessary and when it isn’t. Just do the safe thing every time and you don’t have to think.
the caveat being if you actually do want things expanded then you should not quote them.
for i in $(seq ...)
comes to mind...what kind of savage puts spaces on their paths? Failing in such cases is a neat feature it would seem.
I never understood why common filesystems allow this foolishness. There are already other semantically meaningful characters in paths that are forbidden, like '/' or '\0'. Disallowing bare spaces in filenames is such a natural feature that it makes no sense to deal with all this stupid quoting pain. Any space that is entered by a user in a filename context should be transparently translated to a unicode non-breaking space. Most users wouldn't notice, and shell scripts authors would be much happier!
> Most users wouldn't notice, and shell scripts authors would be much happier!
They would, because people want to use spaces, and they wouldn't, because there's a bunch of characters you need to quote for (in POSIX sh/bash).
The solution is to realize it's not 1986 and evolve shell scripting beyond that. Fish and zsh have done that. Probably others as well. It's not even that hard and doesn't even need to woefully break compatibility.
Oh, and even if you did somehow convince the Unix world to outlaw spaces and other special shell characters by some super-human feat of charisma, you still haven't really solved anything because there's no way in hell you're going to convince the Windows or macOS people to do that, so you're still going to end up in trouble when your spouse or grandma or boss gives you their USB drive, or sends you files (which you can conveniently no longer store), or whatnot.
Might as well argue for the moon to be painted green. It's just not going to happen.
windows noobs (as in embedded development), but more likely grabbed music.
Because back in the days of K&R the kernel just blatted out whatever bytes it was given, and the C/Unix folks refuse to even consider that old APIs might have problems and need changing.
Best practice, IMHO, is usually for scripts not to touch the current directory, and to be agnostic to it.
readlink -f $0
work to get the actual location of the script, and from there get the base for relative scripts? rundir=$(cd -P -- "$(dirname -- "$0")" && printf '%s\n' "$(pwd -P)")Additionally (and the article doesn't solve this either), when invoked through $PATH, $0 will not be a full path but only the name of the command. In that case you want to start with "$(command -v "$0")" instead of just "$0".
printf '%s/%s\n' "$(pwd -P)" "$(basename "$0")"
It's just been in place, in so many places, so long it's become unquestionable. So yes the `pwd -P` in this case will suffice.This approach breaks if you run the script from a symlink.
Will be added in the upcoming revision: https://www.austingroupbugs.net/view.php?id=1457
20 years ago it was a "BSD thing", but it seems to be more wide-spread these days; even my Alpine busybox system has it. I'm sure there are some platforms without it, but there are also platforms without some POSIX tools – it's never a guarantee.
Prior to that you would need to do 'brew install coreutils' to get realpath.
Here's my expectation: if a script is linked into some location, and invoked at that location, it should behave the same as if it had been copied to that location.
If it wouldn't work if it was copied, it isn't reasonable to expect that it should work if it's symlinked. There's no point catering to that use case.
Moreover, let's say i symlink the including script, then copy the included script to the right relative position, then edit the included script. I would expect that running the symlinked including script should include the copied and modified included script, not the original.
In general, i think programmers are far too eager to apply realpath or readlink -f to paths. If a path has symbolic elements in it, it's because someone has put them there on purpose. Don't just remove them. It's actually very rare to really need to know the canonical path to a file.
To your other point, I think this is a valid use-case and do not agree with your rejection. I have had experience with such very examples and submitted PRs to add `realpath` to fix them. One example, a linting app I installed put itself in `/usr/local/<linter-name>/bin/<linter-executable>`. It was really painful to type this full path every time (even with tab completion). I created a symlink from `/usr/bin/` to the executable, and voila, the command is on my path now and easy to use; except the script does not expect itself to be a symlink and broke. To work around this, I created an actual script in `/usr/bin` that could invoke the utility (which then worked because it did properly detect path at least with $0). I don't think that extra script should have been necessary. Further, if I only updated my path, then `dirname $0` fails (returns '.') and that was not even a solution.
Second question, as a counter-point, couldn't someone also reject the expectation that scripts should work when invoked from any path at all? That is saying, as a script writer, when you file a bug report, I can respond "closed, this script requires a `cd` to the install directory and only then can be run. RTFM!". At what point is this just ignoring UX?
In your linter example, i think the most normal solution would be to add /usr/local/<linter-name>/bin to your PATH. Your solution, of a script which just does exec /usr/local/<linter-name>/bin/<linter-name> "$@", also seems fine to me - quite simple, and less likely to cause weird behaviour than a symlink.
I'm not sure what point you're making at the end. We agree that a script not working when invoked from anywhere is bad UX, because people expect to be able to do that. I think that scripts not working through a symlink is not bad UX, because people can't expect to be able to do that, because in many cases that doesn't work!
So I built it into Oils (both OSH and YSH):
source $_this_dir/lib/util.ysh
$_this_dir is a special variable that’s relative to the file it’s being sourced from.https://www.oilshell.org/release/0.19.0/doc/ref/chap-special...
https://www.oilshell.org/release/latest/doc/ysh-tour.html#ap...
It starts with a _ to let you know it’s magic, similar to _status and _process_sub_status (the latter being unavailable in any shell besides Oils BTW)
So yeah this one reason I say POSIX and shell are “rotting” – they lack extremely basic functionality like this. This could have been done 20 years ago, but there’s nobody really pushing it.
(We may add namespaces to shell, which will change some things in the example above, but it won’t change _this_dir. We can use feedback and help! - See recent story https://lobste.rs/s/h4egaz/oils_winter_status_update )
(copy of lobste.rs comment - https://lobste.rs/s/4emmdl/sh_relative_shell_script_includes...)
more:
Oils Winter Status Update - https://www.oilshell.org/blog/2023/11/status-update.html
/usr/sbin/./././some_admin_tool
/usr/sbin/../sbin/../sbin/some_admin_tool
/my_rigged_folder/haha (using symlink)
but i guess it would have required allocating a string or something which may have mattered 40 years ago even though we could have had a better OS in every way 20 years ago
and the same for the process list (in multiple ways, one you can see other people's passwords if they do HISTFILE=/dev/null something --password password, two you can just modify the process list)
and i get the last laugh because un*x lovers will reply to criticism such as "your CLI shit is retarded it logs the passwords to disk" with "NO BRO I USE HISTFILE=/dev/null" to which i reply "that's a lot of shit to type just to not log your password to disk" to which he replies with "NO BRO I USE SPACE PREFIX" and he's still wrong due to previous paragraph. similarily the un*x lover will boast about how he can detect malware / bad guys by using the process list yet nope it can just be bypassed like anything in un*x.
and don't even get me started with how moronic it is for a CLI program to "prompt" the user for a password to avoid this issue (actually you can use patches to make CLI paramters not shared to all users of the system, which is still stupid because none of that type of information should ever be shared in he first place: no other user's process should see any attribute of mine)
and the icing on the cake is the typical way people inside the industry are completely oblivious and say "no man this is actually amazing stuff here"
> It's possible to write extremely powerful tools using shell scripts, and modularize your code. Bourne shell doesn't provide a clear mechanism for referencing relative files. By establishing a command file's base dir, you can reliably reference relative shell scripts.
wow a turing complete language is powerful who knew now if only we had one that wasn't just a cesspit of footguns most of which are there for no real reason and even historically were still invalid
The incantation at the top of all my scripts, that I learned from other HN folks, is:
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
And I guess throw a realpath in there too for symlinks, per TFA.The following worked for everyone:
SCRIPT_DIR=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")").BASH_SOURCE[0] obviously only works with bash.
Yes you do (or a suitable alternative, e.g. GNU readlink).
> The `cd` into the directory + `pwd` is equivalent.
You won't know which directory to cd to without realpath. Just stripping off the filename does not get you the correct directory if the file itself is a symlink, which is a common use case.