444

Given file names like these:

/the/path/foo.txt
bar.txt

I hope to get:

foo
bar

Why this doesn't work?

#!/bin/bash

fullfile=$1
fname=$(basename $fullfile)
fbname=${fname%.*}
echo $fbname

What's the right way to do it?

alex
  • 6,818
  • 9
  • 52
  • 103
neversaint
  • 60,904
  • 137
  • 310
  • 477
  • 1
    This code should work *in most cases* as long as the value of `$1` is actually what you think it is. However, it is subject to [word splitting](http://wiki.bash-hackers.org/syntax/expansion/wordsplit) and [filename expansion](http://wiki.bash-hackers.org/syntax/expansion/globs) due to improper [quoting](http://mywiki.wooledge.org/Quotes). – toxalot Mar 18 '14 at 20:19
  • 1
    Closer @tripleee: this is not a duplicate of [Extract filename and extension in Bash](https://stackoverflow.com/q/965053/6136214). The other more complex Q requires the extension and filename be *separate*. If anything, the other Q. is an elaboration of this more basic one. – agc Jan 10 '20 at 06:38
  • @agc The answers seem very similar, but the older question has more of them. Can you explain why you think these should be kept separate? – tripleee Jan 10 '20 at 07:55
  • 1
    @tripleee Users interested in the simpler problem might be needlessly confused by the added or differing code required to solve the more complex problem. – agc Jan 13 '20 at 05:18
  • Did you read the answers to the other question? Out of the top five, four very distinctly demonstrate how to do exactly this, and explain the options, most of them quite succinctly. I'm happy to be convinced if you can point to actual differences, but I'm not seeing them. Perhaps raise this on [meta] for broader visibility. – tripleee Jan 13 '20 at 06:25
  • Try this one-liner: `fname=$(basename "${1%.*}")` – Noam Manos Jul 05 '20 at 16:02
  • I never understood why, on stack overflow, we close different questions when they have overlapping answers. Users search for a question, not an answer, and will never land that the "correct" question if they are not already aware of the similarity. The site won't work as a `key → value` store if we keep deleting distinct keys that map to similar values. – MRule Jun 12 '23 at 17:03

9 Answers9

804

You don't have to call the external basename command. Instead, you could use the following commands:

$ s=/the/path/foo.txt
$ echo "${s##*/}"
foo.txt
$ s=${s##*/}
$ echo "${s%.txt}"
foo
$ echo "${s%.*}"
foo

Note that this solution should work in all recent (post 2004) POSIX compliant shells, (e.g. bash, dash, ksh, etc.).

Source: Shell Command Language 2.6.2 Parameter Expansion

More on bash String Manipulations: http://tldp.org/LDP/LG/issue18/bash.html

tripleee
  • 175,061
  • 34
  • 275
  • 318
ghostdog74
  • 327,991
  • 56
  • 259
  • 343
  • 70
    fantastic answer... bash String Manipulations ( like `${s##*/}` ) are explained here http://linuxgazette.net/18/bash.html – chim Dec 20 '11 at 15:00
  • 7
    @chim have you found an updated reference to your link? It's dead. – yurisich Jul 09 '14 at 19:59
  • 30
    @Droogans found it after some digging :) http://www.tldp.org/LDP/LG/issue18/bash.html didn't realise I had 27 upvotes on this comment :) – chim Jul 11 '14 at 10:07
  • 1
    Very handy, used this with file expansion and select. Example: `options=~/path/user.*` and then `select result in ${options[@]##*/};` – Anthony Hatzopoulos Nov 10 '14 at 19:19
  • 39
    Is there a way to combine `##*/` at `%.*` (by nesting or piping or whatnot) arrive at foo directly? – bongbang Nov 26 '14 at 22:09
  • BTW, TLDP's documentation isn't very well maintained; http://wiki.bash-hackers.org/syntax/pe and http://mywiki.wooledge.org/BashFAQ/100 might be better references. – Charles Duffy Apr 21 '15 at 01:36
  • 1
    @bongbang, no; bash doesn't allow PEs to be nested. (zsh does, but it's pretty much alone in that). – Charles Duffy Apr 21 '15 at 01:36
  • 1
    doesn't work in ubuntu bash script with output as described – cerd Aug 07 '15 at 05:32
  • @bongbang - You can have everything in one line, with a Little variation on this post, see http://stackoverflow.com/a/36341390/2707864. – sancho.s ReinstateMonicaCellio Mar 31 '16 at 18:53
  • Linux Gazette link is now broken: LG #18 is now available via http://tldp.org/LDP/LG/issue18/bash.html – DrUseful Apr 20 '16 at 10:50
  • FYI, worked also on `zsh`. – guneysus Nov 03 '16 at 07:22
  • 7
    Documentation is also available via `man -P "less -p 'Parameter Expansion'" bash` – Mark Maglana Feb 09 '17 at 18:39
  • 1
    @chim the mentioned link is dead. here is archived version https://web-beta.archive.org/web/20111026140412/http://linuxgazette.net:80/18/bash.html – jaggi Apr 03 '17 at 19:54
  • How do you do this in `fish`? – Post Self Jan 27 '18 at 10:15
  • What happen if my path is something like `user/my_folder/[this_is_my_file]*`? What I obtain when I follow these steps is `[this_is_my_file]*` – Henry Navarro Dec 05 '18 at 12:11
  • 2
    this needs an explanation for what each step does, this answer is very hard to apply to other solutions otherwise! – DrCord Dec 19 '19 at 19:42
  • 1
    I know in most cases parameter substitution is enough for the case, but they don't take care of the edge cases. `basename /path/name/` will give `name`, but `${var##*/}` will give ''. `dirname /path` will give '/', but `${var%/*}` give ''. Sure we can normalize the path before and after, but we have to write far more code to reach the reliability. – David Pi Mar 07 '21 at 00:39
  • What does # stand for? – Caterina Apr 18 '21 at 10:54
369

The basename command has two different invocations; in one, you specify just the path, in which case it gives you the last component, while in the other you also give a suffix that it will remove. So, you can simplify your example code by using the second invocation of basename. Also, be careful to correctly quote things:

fbname=$(basename "$1" .txt)
echo "$fbname"
Michael Aaron Safyan
  • 93,612
  • 16
  • 138
  • 200
81

A combination of basename and cut works fine, even in case of double ending like .tar.gz:

fbname=$(basename "$fullfile" | cut -d. -f1)

Would be interesting if this solution needs less arithmetic power than Bash Parameter Expansion.

kenorb
  • 155,785
  • 88
  • 678
  • 743
kom lim
  • 811
  • 6
  • 2
  • 4
    This is my preferred way - with the minor change of using `$(..)` - so this becomes: `fbname=$(basename "$fullfile" | cut -d. -f1)` – FarmerGedden Apr 22 '15 at 13:06
  • This is a nice solution in that it will snip *all* (dot) extensions. – user2023370 Jun 01 '15 at 13:41
  • 7
    If a file has dots elsewhere in the name, this would truncate it incorrectly. This may work better: `fbname=$(basename "$fullfile" | sed -r 's|^(.*?)\.\w+$|\1|')`. More choices: : `'s|^(.*?)\..+$|\1|'`, `'s|^(.*?)\.[^\.]+$|\1|'`. – Pysis Nov 14 '18 at 18:34
49

Here are oneliners:

  1. $(basename "${s%.*}")
  2. $(basename "${s}" ".${s##*.}")

I needed this, the same as asked by bongbang and w4etwetewtwet.

Example:

$ s=/the/path/foo.txt
$ echo "$(basename "${s%.*}")"
foo
$ echo "$(basename "${s}" ".${s##*.}")"
foo
parsley72
  • 8,449
  • 8
  • 65
  • 98
26

Pure bash, no basename, no variable juggling. Set a string and echo:

p=/the/path/foo.txt
echo "${p//+(*\/|.*)}"

Output:

foo

Note: the bash extglob option must be "on", (Ubuntu sets extglob "on" by default), if it's not, do:

shopt -s extglob

Walking through the ${p//+(*\/|.*)}:

  1. ${p -- start with $p.
  2. // substitute every instance of the pattern that follows.
  3. +( match one or more of the pattern list in parenthesis, (i.e. until item #7 below).
  4. 1st pattern: *\/ matches anything before a literal "/" char.
  5. pattern separator | which in this instance acts like a logical OR.
  6. 2nd pattern: .* matches anything after a literal "." -- that is, in bash the "." is just a period char, and not a regex dot.
  7. ) end pattern list.
  8. } end parameter expansion. With a string substitution, there's usually another / there, followed by a replacement string. But since there's no / there, the matched patterns are substituted with nothing; this deletes the matches.

Relevant man bash background:

  1. pattern substitution:
  ${parameter/pattern/string}
          Pattern substitution.  The pattern is expanded to produce a pat
          tern just as in pathname expansion.  Parameter is  expanded  and
          the  longest match of pattern against its value is replaced with
          string.  If pattern begins with /, all matches  of  pattern  are
          replaced   with  string.   Normally  only  the  first  match  is
          replaced.  If pattern begins with #, it must match at the begin‐
          ning of the expanded value of parameter.  If pattern begins with
          %, it must match at the end of the expanded value of  parameter.
          If string is null, matches of pattern are deleted and the / fol
          lowing pattern may be omitted.  If parameter is @ or *, the sub
          stitution  operation  is applied to each positional parameter in
          turn, and the expansion is the resultant list.  If parameter  is
          an  array  variable  subscripted  with  @ or *, the substitution
          operation is applied to each member of the array  in  turn,  and
          the expansion is the resultant list.
  1. extended pattern matching:
  If the extglob shell option is enabled using the shopt builtin, several
   extended  pattern  matching operators are recognized.  In the following
   description, a pattern-list is a list of one or more patterns separated
   by a |.  Composite patterns may be formed using one or more of the fol
   lowing sub-patterns:

          ?(pattern-list)
                 Matches zero or one occurrence of the given patterns
          *(pattern-list)
                 Matches zero or more occurrences of the given patterns
          +(pattern-list)
                 Matches one or more occurrences of the given patterns
          @(pattern-list)
                 Matches one of the given patterns
          !(pattern-list)
                 Matches anything except one of the given patterns
tripleee
  • 175,061
  • 34
  • 275
  • 318
agc
  • 7,973
  • 2
  • 29
  • 50
13

Here is another (more complex) way of getting either the filename or extension, first use the rev command to invert the file path, cut from the first . and then invert the file path again, like this:

filename=`rev <<< "$1" | cut -d"." -f2- | rev`
fileext=`rev <<< "$1" | cut -d"." -f1 | rev`
higuaro
  • 15,730
  • 4
  • 36
  • 43
  • 1
    I've never seen those triple angle bracket `<<<` doohickeys--what's that? – Alex Hall Sep 12 '16 at 04:32
  • 3
    They are called "Here Strings" (more info [here](http://tldp.org/LDP/abs/html/x17837.html)), basically it takes the input as a string and feed it to your program as it was reading through stdin. – higuaro Sep 12 '16 at 05:00
2

If you want to play nice with Windows file paths (under Cygwin) you can also try this:

fname=${fullfile##*[/|\\]}

This will account for backslash separators when using BaSH on Windows.

vincent gravitas
  • 354
  • 2
  • 10
1

Just an alternative that I came up with to extract an extension, using the posts in this thread with my own small knowledge base that was more familiar to me.

ext="$(rev <<< "$(cut -f "1" -d "." <<< "$(rev <<< "file.docx")")")"

Note: Please advise on my use of quotes; it worked for me but I might be missing something on their proper use (I probably use too many).

Diomoid
  • 101
  • 10
  • 1
    You do not need a single quote, you try to do the same as: `rev <<< file.docx | cut -f1 -d. | rev` just without the quotes and without the nested sub shells. Also I don't think the above can work at all. – matthias krull May 16 '17 at 22:59
-15

Use the basename command. Its manpage is here: http://unixhelp.ed.ac.uk/CGI/man-cgi?basename

Bandicoot
  • 3,811
  • 7
  • 35
  • 39