9

I know this can be easily done using regex like I answered on https://stackoverflow.com/a/33379831/3962126, however I need to do this in bash.

So the closest question on Stackoverflow I found is this one bash: extracting last two dirs for a pathname, however the difference is that if

DIRNAME = /a/b/c/d/e

then I need to extract

d
Community
  • 1
  • 1
Sasha
  • 171
  • 1
  • 7
  • 1
    Does `$(basename $(dirname "$DIRNAME"))` meet your requirements? Failing that, does `x=${DIRNAME%/*}; echo ${x##*/}` do the job you require? Beware degenerate cases. – Jonathan Leffler Oct 27 '15 at 23:06
  • I saw your comment after I posted my answer below. Yes, it does. I really don't understand how/why this command extracts the string between the last two slashes, but I don't care as far as it works for me. – Sasha Oct 27 '15 at 23:10
  • 1
    The `dirname` command drops the last component of the file name, removing the `/e`; the `basename` command drops all except the last component of the file name, removing the `/a/b/c`, leaving just `d`. – Jonathan Leffler Oct 27 '15 at 23:12
  • @Jonathan, notice the solution `x=${DIRNAME%/*}; echo ${x##*/}` also matches `d` here: `DIRNAME=a/b`, and I think that's not what the OP wants. – whoan Oct 28 '15 at 10:04
  • @JonathanLeffler, that's a bit buggy as given -- you need quotes around the dirname expansion too. – Charles Duffy Oct 28 '15 at 14:06
  • @Sasha, can you speak to what behavior you want if `DIRNAME=d/e` or `DIRNAME=e`? – Charles Duffy Oct 28 '15 at 14:10
  • @j.a.: I said 'Beware degenerate cases'; you're telling me that I need to be careful of degenerate cases. – Jonathan Leffler Oct 28 '15 at 14:17
  • @CharlesDuffy: It's commentary to get towards an answer; it is not a polished solution. It works on the sample data; it is not fully generalized. And if you keep within the portable file name character set (and even with a moderate number of extensions over the base portable file name character set), the quotes aren't necessary. The problem is that people don't keep within the portable file name character set. – Jonathan Leffler Oct 28 '15 at 14:20
  • @JonathanLeffler, ...and restricting names to that set is an utterly unrealistic request to make of people. Folks whose native language uses a different alphabet should be able to name files in that language. Support for whitespace within filenames, likewise, is a reasonable end-user expectation of anyone coming from major desktop operating systems. – Charles Duffy Oct 28 '15 at 15:08
  • @JonathanLeffler, ...moreover, even if names *were* restricted to that set, we can't say whether names will survive unquoted expansion unmodified without knowing the value of IFS. And regardless -- failing to quote is asking the shell to do *more* processing, in a case where that processing isn't desired or necessary. In what world is running data through unneeded, irrelevant processing a good practice? – Charles Duffy Oct 28 '15 at 15:09

6 Answers6

12

This may be relatively long, but it's also much faster to execute than most preceding answers (other than the zsh-only one and that by j.a.), since it uses only string manipulations built into bash and uses no subshell expansions:

string='/a/b/c/d/e'  # initial data
dir=${string%/*}     # trim everything past the last /
dir=${dir##*/}       # ...then remove everything before the last / remaining
printf '%s\n' "$dir" # demonstrate output

printf is used in the above because echo doesn't work reliably for all values (think about what it would do on a GNU system with /a/b/c/-n/e).

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • This might be the best answer as it's also stricly POSIX-compliant. – gniourf_gniourf Oct 28 '15 at 08:38
  • The only problem with this @gniourf is that it matches also any string before the last slash (e.g. `string="d/e"` matches `d`) and the OP asked for the string between the last **two** slashes. – whoan Oct 28 '15 at 09:59
  • It even prints any string if there is no slashes (e.g.: `string=hello` -> prints `hello`). – whoan Oct 28 '15 at 10:38
  • @j.a., I don't read either case as a bug -- both fall outside the assumptions implicit in the specification. If one wanted to put in a `if [[ $string = */*/* ]]; then` ... `fi` wrapping this code to avoid them both, trivially done. – Charles Duffy Oct 28 '15 at 14:03
  • It should be noted that this solution performs significantly better than that of @j.a. at scale. Even with the above check in place, I see ~1.07 seconds for 1,000,000 iterations on the path `/home/a/b/c/d/e/f/g.txt`, vs. ~7.57 seconds for the regex replacement solution. – ethan.roday Dec 07 '15 at 19:41
4

Here a pure bash solution:

[[ $DIRNAME =~ /([^/]+)/[^/]*$ ]] && printf '%s\n' "${BASH_REMATCH[1]}"

Compared to some of the other answers:

  • It matches the string between the last two slashes. So, for example, it doesn't match d if DIRNAME=d/e.
  • It's shorter and fast (just uses built-ins and doesn't create subprocesses).
  • Support any character between last two slashes (see Charles Duffy's answer for more on this).

Also notice that is not the way to assign a variable in bash:

DIRNAME = /a/b/c/d/e
       ^ ^

Those spaces are wrong, so remove them:

DIRNAME=/a/b/c/d/e
Community
  • 1
  • 1
whoan
  • 8,143
  • 4
  • 39
  • 48
3

Using awk:

echo "/a/b/c/d/e" | awk -F / '{ print $(NF-1) }' # d

Edit: This does not work when the path contains newlines, and still gives output when there are less than two slashes, see comments below.

Shelvacu
  • 4,245
  • 25
  • 44
  • This also matches `d` here: `DIRNAME=a/b` and the OP asked for the string between the last **two** slashes. – whoan Oct 28 '15 at 10:30
  • This even prints any string if there is no slashes (e.g.: `echo hello | awk -F / '{ print $(NF-1) }'` -> prints `hello`). – whoan Oct 28 '15 at 10:40
  • ...and gives incorrect results when the filename contains literal newlines. – Charles Duffy Oct 28 '15 at 15:12
1

OMG, maybe this was obvious, but not to me initially. I got the right result with:

dir=$(basename -- "$(dirname -- "$str")")
echo "$dir"
gniourf_gniourf
  • 44,650
  • 9
  • 93
  • 104
Sasha
  • 171
  • 1
  • 7
  • The correct way to write that would have been `dir=$(basename "$(dirname "$str")")`. – Charles Duffy Oct 27 '15 at 23:25
  • ...otherwise, you'll get bugs when your strings contain whitespace, glob expressions that match files, etc. – Charles Duffy Oct 27 '15 at 23:25
  • ...also, this is slow enough that you wouldn't want to put it in an inner loop that's run hundreds or thousands of times; command substitutions (that is, the `$(...)` syntax) have a substantial performance penalty. – Charles Duffy Oct 27 '15 at 23:29
1

Using sed

if you want to get the fourth element

DIRNAME="/a/b/c/d/e"
echo "$DIRNAME" | sed -r 's_^(/[^/]*){3}/([^/]*)/.*$_\2_g'

if you want to get the before last element

DIRNAME="/a/b/c/d/e"
echo "$DIRNAME" | sed -r 's_^.*/([^/]*)/[^/]*$_\1_g'
Jose Ricardo Bustos M.
  • 8,016
  • 6
  • 40
  • 62
0

Using zsh parameter substitution is pretty cool too

echo ${${DIRNAME%/*}##*/}

I think it's faster than the double $() as well, because it won't need any subprocesses.

Basically it slices off the right side first, and then all the remaining left side second.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
fivetentaylor
  • 1,277
  • 7
  • 11