3

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'

Ari157
  • 95
  • 4
  • 16
  • So your question is if that same result can be achieved without using extglob? "Proper" is a vague term. – KamilCuk Oct 16 '22 at 18:54
  • @KamilCuk yep, sorry for being vague – Ari157 Oct 16 '22 at 18:54
  • @KamilCuk I added it to the question :) – Ari157 Oct 16 '22 at 18:55
  • 1
    Well, `[[ $1 ~ ([^/]*)/*$ ]]; echo ${BASH_REMATCH[1]}` – KamilCuk Oct 16 '22 at 18:56
  • @KamilCuk o, interesting, let me see – Ari157 Oct 16 '22 at 18:57
  • But.. also why not just set extglob? – KamilCuk Oct 16 '22 at 18:58
  • @KamilCuk It gives me a syntax error, even shellcheck is screaming: SC1009, SC1073 and SC1072 – Ari157 Oct 16 '22 at 19:00
  • @KamilCuk I just don't want to depend on extglob if possible – Ari157 Oct 16 '22 at 19:00
  • (+ doesn't `extglob` slow things down? this script even though it's a script needs performance, as much as it can get @KamilCuk) – Ari157 Oct 16 '22 at 19:03
  • @Ari157 Since you're so concerned about performance, have you measured the difference? Also, you could probably write whatever you are doing in a compiled language to boost the overall performance a lot. – Ted Lyngmo Oct 16 '22 at 19:10
  • @TedLyngmo I have, the current solution is ~0.021 ms slower, and I need to do it in BASH because I am making a plugin manager and am concerened about speed because well... I don't want people to wait for like 10s for all of their plugins to load :) – Ari157 Oct 16 '22 at 19:13
  • If I could I'd do it in C, but I don't think BASH even supports any way to do the same things as `source` can – Ari157 Oct 16 '22 at 19:13
  • Does this answer your question? [Get last dirname/filename in a file path argument in Bash](https://stackoverflow.com/questions/3294072/get-last-dirname-filename-in-a-file-path-argument-in-bash) – tink Oct 16 '22 at 19:16
  • @tink 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`) – Ari157 Oct 16 '22 at 19:18
  • What does _"I don't think BASH even supports any way to do the same things as `source` can"_ mean? – Ted Lyngmo Oct 16 '22 at 19:24
  • @TedLyngmo Okay, let's say I want to load an alias into bash, I don't think there's a proper way to do that in C, but when you source some BASH file it can be easily done – Ari157 Oct 16 '22 at 19:25
  • @TedLyngmo you got me thinking right now, I could make a program to generate the lines of bash that need to be ran in C and then run them ! – Ari157 Oct 16 '22 at 19:26
  • @TedLyngmo How good of an idea do you think generating the commands in C and then just running them would be? Maybe some caching too ? – Ari157 Oct 16 '22 at 19:27
  • If you're comfortable with C, why not write the whole thing in C? Why does it have to generate bash commands? – Ted Lyngmo Oct 16 '22 at 19:29
  • @TedLyngmo I am quite comfy with C, but I don't think C can for example set aliases in BASH and stuff :) – Ari157 Oct 16 '22 at 19:32
  • Ok, that's what you meant by `source`. So the script in question is to be `source`d? No, in that case I guess generating the output to be sourced is an option. Perhaps even using an evil eval: `eval "$(the_c_program)"` – Ted Lyngmo Oct 16 '22 at 19:37
  • @TedLyngmo I meant `source loader.sh`, it just has a bunch of functions which set up the env variables, bash functions, aliases, etc. and when I'll make that program it's just gonna be `$(the_c_program)` – Ari157 Oct 16 '22 at 19:38
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/248846/discussion-between-ari157-and-ted-lyngmo). – Ari157 Oct 16 '22 at 19:39

3 Answers3

3

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
oguz ismail
  • 1
  • 16
  • 47
  • 69
2

Ideas:

get_base() { [[ $1 =~ ([^/]*)/*$ ]]; echo "${BASH_REMATCH[1]}"; };
get_base() { while [[ "${1%/}" != "$1" ]]; do set -- "${1%%/}"; done; echo "${1##*/}"; }
KamilCuk
  • 120,984
  • 8
  • 59
  • 111
2

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")
F. Hauri - Give Up GitHub
  • 64,122
  • 17
  • 116
  • 137