A fast and proper way to get the basename of a directory or a file in bash

280 Views Asked by At

I have this code:

get_base() { echo "${1##*/}"; }

And this works fine until we have 1 or more trailing slashes

I have found a solution on how to fix it, but the problem is that we'll need extglob enabled, and I don't want that:

ari@ari-gentoo ~ % x='/home/ari///'

ari@ari-gentoo ~ % echo "${x%%+(/)}"
/home/ari

Then we can obv save it into a tmp var and run the basename substitution on it

Anyway, my question is, is there any proper way to do it and it still being fast (meaning no calls to external commands because this function gets called quite a lot) without needing any fancy features enabled?

Thanks for the answers in advance :)

Questions and answers

no as all of those solutions either use commands or have a substitution expression that only strips the last slash, not multiples :) (e.g. /home/ari/// with expr ${x%/} would become only /home/ari// when it needs to be /home/ari)

  • What do you mean by 'proper'

By proper I mean 'achieved without enabling extglob or any other fancy features'

3

There are 3 best solutions below

1
F. Hauri  - Give Up GitHub On BEST ANSWER

A fast and proper way to get ... in bash

In complement to oguz ismail's correct answer I would like to suggest use of -v flag for this kind of function, in order to reduce forks:

get_base() {
    if [[ $1 == -v ]] ;then
        local -n _res=$2
        shift 2
    else
        local _res
    fi
    set -- "${1%"${1##*[!/]}"}"
    printf -v _res %s "${1##*/}"
    [[ ${_res@A} == _res=* ]] && echo "$_res"
}

This let you try this function by

$ get_base /path/entry////
entry

But for storing result into some variable, you would avoid useless fork like

myvar=$(get_base /path/entry////)

and use prefered syntax:

$ get_base -v myvar /path/entry////
$ echo $myvar
entry

To become a function to split entry and path:

get_base() {
    if [[ $1 == -v ]] ;then
        local -n _res=$2
        shift 2
    else
        local _res
    fi
    set -- "${1%"${1##*[!/]}"}"
    printf -v _res %s "${1##*/}"
    [[ ${_res@A} == _res=* ]] &&
        echo "$_res" "${1%/$_res}" && return
    printf -v _res[1] %s "${1%/$_res}"
}

Then:

$ get_base /path/to/entry////
entry /path/to

and

$ get_base -v myvar /path/to/entry////
$ declare -p myvar 
declare -a myvar=([0]="entry" [1]="/path/to")
$ echo ${myvar[0]}
entry
$ echo ${myvar[1]}
/path/to

With spaced unicode filenames:

$ get_base -v myvar '/path/to/some dir/some loved file ♥♥♥.xtns'
$ declare -p myvar 
declare -a myvar=([0]="some loved file ♥♥♥.xtns" [1]="/path/to/some dir")
0
KamilCuk On

Ideas:

get_base() { [[ $1 =~ ([^/]*)/*$ ]]; echo "${BASH_REMATCH[1]}"; };
get_base() { while [[ "${1%/}" != "$1" ]]; do set -- "${1%%/}"; done; echo "${1##*/}"; }
0
oguz ismail On

Here is one way:

get_base() {
  set -- "${1%"${1##*[!/]}"}"
  printf '%s\n' "${1##*/}"
}
$ get_base /home/oguz//
oguz
$ get_base /root
root
$ get_base /

$ get_base .bash_history
.bash_history