r/bash • u/da_Aresinger • 21h ago
Replacing dirname with only builtins
I am writing a bunch of scripts for which I want to only use builtins and realised that dirname is not a builtin, I replaced it thusly:
SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SRC_DIR2="$({ cd "${BASH_SOURCE[0]%/*}" 2>/dev/null || :; } && pwd)"
I think these two lines are functionally equivalent, considering that BASH_SOURCE[n] will never contain a trailing /.
Then again I am often stupid. Does anyone see any issues?
1
u/jjangg96 21h ago
they differ for a script that sits directly in /. BASH_SOURCE[0] is /x.sh there, the %/* strips it down to an empty string, and cd "" succeeds without moving, so pwd prints your current directory instead of /. dirname gives / for that case. rare, but it's the one place they aren't equivalent.
the other thing is the || : swallowing errors. if the cd fails for any reason you silently get the current directory back and keep going, where the dirname version gives you an empty SRC_DIR and a non-zero status. fine if you want that, just know it's there.
1
u/da_Aresinger 20h ago
I think I fixed the root dir issue by simply appending a
/to the parameter expansion:SRC_DIR2="$({ cd "${BASH_SOURCE[0]%/*}/" 2>/dev/null || :; } && pwd)" ^since
cddoesn't care whether or not a path terminates in/As for the second point: the only situations in which I can think of this being an issue are either
The script deletes its own directory before this line is called, which... don't do that, I guess?
Or a "race condition" where an external process deletes the directory between script invocation and this line. That is a risk I am willing to take.
1
u/jjangg96 17h ago
yeah the trailing slash fixes it, /x.sh becomes / and cd / works.
one case left is a bare name with no slash, like running bash x.sh from its own dir. nothing gets stripped so you cd into "x.sh/", that fails, the || : eats it, and pwd happens to give the right dir. so it works there, just by accident.
→ More replies (1)
1
u/roxalu 6h ago edited 6h ago
Not any difference between both implementation - but a small miss in both:
In case you also want to support edge cases, then replace
cd
with
cd --
Otherwise directory names, starting with "-" could cause both implementation to fail.
Edited: Missed, that even
dirname --
were needed in that case as well.
But to be true, this is a super rare edge case cause it isn't easy to start a script within a folder name starting with '-' in a way, so that the
${BASH_SOURCE[0]}has a "-" as first character. I doubt any standard user would run into that error by chance, because already bash would complain first in most cases.
1
u/michaelpaoli 4h ago
No, if you want to replace dirname, you need work on the string, no cd, no pwd, etc. as dirname works regardless as to whether or not the path exists or is accessible.
$ cd $(dirname $(mktemp -u $(mktemp -u)/tmp.XXXXXXXXXX)) -bash: cd: /tmp/tmp.bLAwbN85vl: No such file or directory $
So, how 'bout something like this (not a full reimplementation of dirname(1)):
$ expand -t 2 < dirname
#!/usr/bin/env bash
dirname(){
[ $# -ge 1 ] || {
1>&2 printf '%s\n' 'dirname: missing operand'
exit 1
}
(
for d in "$@"
do
# strip traling / characters that aren't first character:
while :
do
case "$d" in
*?*/)
d="${d/%\//}"
;;
*)
break
;;
esac
done
# strip traling non-/ characters:
while :
do
case "$d" in
*[!/])
d="${d/%[!\/]/}"
;;
*)
break
;;
esac
done
# strip traling / characters that aren't first character:
while :
do
case "$d" in
*?*/)
d="${d/%\//}"
;;
*)
break
;;
esac
done
[ -n "$d" ] || d=.
printf '%s\n' "$d"
done
)
}
dirname "$@"
$ t='/abs/x rel/x /abs///x/// rel///x/// /one//two///three rone//two///three file file/ file/// file///. ./File .///////File ./File/// .///////File/// . / // /// //// /////. /////.///'
$ dirname $t | cmp - <(./dirname $t)
$
-2
u/bac0on 21h ago
I use this function, but every time I put it to the test, it breaks, its a start though:
shopt -s extglob
## Sanitize dirname.
__dirname__(){
local -n __d__=$1
local -- __a__=${2:-.}; __a__=${__a__%%+(/)}
[[ $__a__ = '' ]] && __a__=/ || { \
__a__=${__a__//+(\/)/\/} ; [[ $__a__ = */* ]] && __d__=${__a__%/*} || __d__=. ; \
}
}
__dirname__ dirname "directory"
4
u/snarkofagen 21h ago
dirname solution fails if path doesn't exist , as it should