bublina

Love people, use things.

POSIX shell-only dirname replacement

For many years I have been using a snippet that worked well for all purposes I tried:

a="/$0"; a=${a%/*}; a=${a#/}; a=${a:-.}; BINDIR=$(cd "$a"; pwd)

But today I realized it does not play well with /, i.e. if the script is placed in /script.sh, the BINDIR will contain current directory (whatever it is) instead of /.

TL;DR; the answer is

a="/$0"; a="${a%/*}"; a="${a:-.}"; a="${a##/}/"; BINDIR=$(cd "$a"; pwd)

UPDATE: Fixed also space handling reported at stackoverflow.

Tested with busybox sh (1.26.2), loksh (OpenBSD 6.1 ksh), mksh (R55), as well as /bin/sh from NetBSD 7.0.1_PATCH.

If you want to learn more read on.

In order to play with it properly, I wrote a test script:

#!/bin/sh

a="/$0"; a="${a%/*}"; a="${a:-.}"; a="${a##/}/"; BINDIR=$(cd "$a" || exit; pwd)

test "$1" = "-d" && DEBUG=1

debug() {
  test -n "$DEBUG" && echo "$@" 1>&2
}

oldf() {
  a="/$1"; a=${a%/*}; a=${a#/}; a=${a:-.}; BINDIR=$(cd "$a" || exit ; pwd)
  echo "$BINDIR"
}

gdir() {
  a="/$1";
  debug "$a"
  a="${a%/*}";
  debug "$a"
  a="${a:-.}";
  debug "$a"
  a="${a##/}";
  debug "$a"
  a="${a:-/}";
  echo "$a"
}

# grdir = Get Real Directory
# works only on existing directories because it uses 'cd' in the last step
grdir() {
  #a="/$1"; a=${a%/*}; a=${a#/}; a=${a:-.}; BINDIR=$(cd "$a"; pwd)
  a=$(gdir "$1")
  A=$(cd "$a" || exit; pwd)
  echo "$A"
}

BINDIR=$(grdir "$0")

expectit() {
  func=${3:-grdir}
  A=$($func "$1")
  if
    test "$A" = "$2"
  then
    echo "OK: $func $1 ==> $2"
  else
    echo "FAILED: $func $1 !=> $A"
  fi
}

expectit "/a.sh" "/"
expectit /usr/bin/a.sh /usr/bin
expectit ./a.sh "$PWD"
expectit a.sh "$PWD"

expectit /a.sh / gdir
expectit /made/up/a.sh /made/up gdir
expectit ./a.sh . gdir
expectit a.sh . gdir
expectit "/this is spaced/dir/a.sh" "/this is spaced/dir" gdir

# Here you can see what went wrong with the old one
echo expect failure in the next line
expectit /a.sh / oldf
expectit /usr/bin/a.sh /usr/bin oldf
expectit ./a.sh "$PWD" oldf
expectit a.sh "$PWD" oldf

Let it be in public domain.