I want to get the filename (without extension) and the extension separately.

The best solution I found so far is:


This is wrong because it doesn't work if the file name contains multiple . characters. If, let's say, I have a.b.js, it will consider a and b.js, instead of a.b and js.

It can be easily done in Python with

file, ext = os.path.splitext(path)

but I'd prefer not to fire up a Python interpreter just for this, if possible.

Any better ideas?

  • This question explains this bash technique and several other related ones. – jjclarkson Jun 12 '09 at 20:34
  • 28
    When applying the great answers below, do not simply paste in your variable like I show here Wrong: extension="{$filename##*.}" like I did for a while! Move the $ outside the curlys: Right: extension="${filename##*.}" – Chris K Aug 7 '13 at 18:51
  • 4
    This is clearly a non-trivial problem and for me it is hard to tell if the answers below are completely correct. It's amazing this is not a built in operation in (ba)sh (answers seem to implement the function using pattern matching). I decided to use Python's os.path.splitext as above instead... – Peter Gibson Oct 1 '15 at 8:01
  • 1
    As extension have to represent nature of a file, there is a magic command which check file to divine his nature and offert standard extension. see my answer – F. Hauri Oct 14 '16 at 8:02
  • 2
    The question is problematic in the first place because.. From the perspective of the OS and unix file-systems in general, there is no such thing as a file extension. Using a "." to separate parts is a human convention, that only works as long as humans agree to follow it. For example, with the 'tar' program, it could have been decided to name output files with a "tar." prefix instead of a ".tar" suffix -- Giving "tar.somedir" instead of "somedir.tar". There is no "general, always works" solution because of this--you have to write code that matches your specific needs and expected filenames. – C. M. Oct 10 '18 at 0:08

37 Answers 37


First, get file name without the path:

filename=$(basename -- "$fullfile")

Alternatively, you can focus on the last '/' of the path instead of the '.' which should work even if you have unpredictable file extensions:


You may want to check the documentation :

| improve this answer | |
  • 85
    Check out gnu.org/software/bash/manual/html_node/… for the full feature set. – D.Shawley Jun 8 '09 at 14:08
  • 24
    Add some quotes to "$fullfile", or you'll risk breaking the filename. – lhunath Jun 8 '09 at 14:34
  • 47
    Heck, you could even write filename="${fullfile##*/}" and avoid calling an extra basename – ephemient Jun 9 '09 at 17:52
  • 45
    This "solution" does not work if the file does not have an extension -- instead, the whole file name is output, which is quite bad considering that files without extensions are omnipresent. – nccc Jul 1 '12 at 3:42
  • 43
~% FILE="example.tar.gz"

~% echo "${FILE%%.*}"

~% echo "${FILE%.*}"

~% echo "${FILE#*.}"

~% echo "${FILE##*.}"

For more details, see shell parameter expansion in the Bash manual.

| improve this answer | |
  • 22
    You (perhaps unintentionally) bring up the excellent question of what to do if the "extension" part of the filename has 2 dots in it, as in .tar.gz... I've never considered that issue, and I suspect it's not solvable without knowing all the possible valid file extensions up front. – rmeador Jun 8 '09 at 14:50
  • 8
    Why not solvable? In my example, it should be considered that the file contains two extensions, not an extension with two dots. You handle both extensions separately. – Juliano Jun 8 '09 at 15:20
  • 22
    It is unsolvable on a lexical basis, you'll need to check the file type. Consider if you had a game called dinosaurs.in.tar and you gzipped it to dinosaurs.in.tar.gz :) – porges Jun 13 '09 at 9:11
  • 11
    This gets more complicated if you are passing in full paths. One of mine had a '.' in a directory in the middle of the path, but none in the file name. Example "a/b.c/d/e/filename" would wind up ".c/d/e/filename" – Walt Sellers Mar 5 '12 at 18:49
  • 6
    clearly no x.tar.gz's extension is gz and the filename is x.tar that is it. There is no such thing as dual extensions. i'm pretty sure boost::filesystem handles it that way. (split path, change_extension...) and its behavior is based on python if I'm not mistaken. – v.oddou Nov 26 '13 at 7:29

Usually you already know the extension, so you might wish to use:

basename filename .extension

for example:

basename /path/to/dir/filename.txt .txt

and we get

| improve this answer | |
  • 61
    That second argument to basename is quite the eye-opener, ty kind sir/madam :) – akaIDIOT Jan 23 '13 at 9:37
  • 10
    And how to extract the extension, using this technique? ;) Oh, wait! We actually don't know it upfront. – Tomasz Gandor Feb 13 '14 at 11:48
  • 3
    Say you have a zipped directory that either ends with .zip or .ZIP. Is there a way you could do something like basename $file {.zip,.ZIP}? – Dennis Mar 30 '14 at 20:56
  • 8
    While this only answers part of the OPs question, it does answer the question I typed into google. :-) Very slick! – sudo make install Nov 30 '17 at 16:28
  • 1
    easy and POSIX compliant – gpanda Oct 12 '18 at 15:36

You can use the magic of POSIX parameter expansion:

bash-3.2$ FILENAME=somefile.tar.gz
bash-3.2$ echo "${FILENAME%%.*}"
bash-3.2$ echo "${FILENAME%.*}"

There's a caveat in that if your filename was of the form ./somefile.tar.gz then echo ${FILENAME%%.*} would greedily remove the longest match to the . and you'd have the empty string.

(You can work around that with a temporary variable:

echo ${FILENAME%%.*}


This site explains more.

  Trim the shortest match from the end
  Trim the longest match from the beginning
  Trim the longest match from the end
  Trim the shortest match from the beginning
| improve this answer | |
  • 5
    Much simpler than Joachim's answer but I always have to look up POSIX variable substitution. Also, this runs on Max OSX where cut doesn't have --complement and sed doesn't have -r. – jwadsack Jul 18 '14 at 16:40

That doesn't seem to work if the file has no extension, or no filename. Here is what I'm using; it only uses builtins and handles more (but not all) pathological filenames.

for fullpath in "$@"
    filename="${fullpath##*/}"                      # Strip longest match of */ from start
    dir="${fullpath:0:${#fullpath} - ${#filename}}" # Substring from 0 thru pos of filename
    base="${filename%.[^.]*}"                       # Strip shortest match of . plus at least one non-dot char from end
    ext="${filename:${#base} + 1}"                  # Substring from len of base thru end
    if [[ -z "$base" && -n "$ext" ]]; then          # If we have an extension and no base, it's really the base

    echo -e "$fullpath:\n\tdir  = \"$dir\"\n\tbase = \"$base\"\n\text  = \"$ext\""

And here are some testcases:

$ basename-and-extension.sh / /home/me/ /home/me/file /home/me/file.tar /home/me/file.tar.gz /home/me/.hidden /home/me/.hidden.tar /home/me/.. .
    dir  = "/"
    base = ""
    ext  = ""
    dir  = "/home/me/"
    base = ""
    ext  = ""
    dir  = "/home/me/"
    base = "file"
    ext  = ""
    dir  = "/home/me/"
    base = "file"
    ext  = "tar"
    dir  = "/home/me/"
    base = "file.tar"
    ext  = "gz"
    dir  = "/home/me/"
    base = ".hidden"
    ext  = ""
    dir  = "/home/me/"
    base = ".hidden"
    ext  = "tar"
    dir  = "/home/me/"
    base = ".."
    ext  = ""
    dir  = ""
    base = "."
    ext  = ""
| improve this answer | |
  • 2
    Instead of dir="${fullpath:0:${#fullpath} - ${#filename}}" I've often seen dir="${fullpath%$filename}". It's simpler to write. Not sure if there is any real speed difference or gotchas. – dubiousjim May 30 '12 at 21:37
  • 2
    This uses #!/bin/bash which is almost always wrong. Prefer #!/bin/sh if possible or #!/usr/bin/env bash if not. – Good Person May 25 '13 at 20:32
  • @Good Person: I don't know how it's almost always wrong: which bash -> /bin/bash ; perhaps it's your distro? – vol7ron Jul 10 '13 at 22:30
  • 2
    @vol7ron - on many distros bash is in /usr/local/bin/bash. On OSX many people install a updated bash in /opt/local/bin/bash. As such /bin/bash is wrong and one should use env to find it. Even better is to use /bin/sh and POSIX constructs. Except on solaris this is a POSIX shell. – Good Person Jul 12 '13 at 21:28
  • 2
    @GoodPerson but if you are more comfortable with bash, why use sh? Isn't that like saying, why use Perl when you can use sh? – vol7ron Jul 12 '13 at 22:08

You can use basename.


$ basename foo-bar.tar.gz .tar.gz

You do need to provide basename with the extension that shall be removed, however if you are always executing tar with -z then you know the extension will be .tar.gz.

This should do what you want:

tar -zxvf $1
cd $(basename $1 .tar.gz)
| improve this answer | |
  • 2
    I suppose cd $(basename $1 .tar.gz) works for .gz files. But in question he mentioned Archive files have several extensions: tar.gz, tat.xz, tar.bz2 – SS Hegde Feb 5 '13 at 9:00
  • Tomi Po posted the same things 2 years before. – phil294 Sep 16 '17 at 2:38
  • Hi Blauhirn, wauw this is an old questions. I think something have happened to the dates. I distinctively remember answering the question shortly after it was asked, and there where only a couple of other answers. Could it be that the question was merged with another one, does SO do that? – Bjarke Freund-Hansen Sep 17 '17 at 4:43
  • Yep I remember correctly. I originally answers this question stackoverflow.com/questions/14703318/… on the same day it was asked, 2 years later it was merged into this one. I can hardly be blamed for a duplicate answer when my answer was moved in this way. – Bjarke Freund-Hansen Sep 17 '17 at 4:49
pax> echo a.b.js | sed 's/\.[^.]*$//'
pax> echo a.b.js | sed 's/^.*\.//'

works fine, so you can just use:

pax> FILE=a.b.js
pax> NAME=$(echo "$FILE" | sed 's/\.[^.]*$//')
pax> EXTENSION=$(echo "$FILE" | sed 's/^.*\.//')
pax> echo $NAME
pax> echo $EXTENSION

The commands, by the way, work as follows.

The command for NAME substitutes a "." character followed by any number of non-"." characters up to the end of the line, with nothing (i.e., it removes everything from the final "." to the end of the line, inclusive). This is basically a non-greedy substitution using regex trickery.

The command for EXTENSION substitutes a any number of characters followed by a "." character at the start of the line, with nothing (i.e., it removes everything from the start of the line to the final dot, inclusive). This is a greedy substitution which is the default action.

| improve this answer | |
  • This break for files without extension as it would print the same for name and extension. So I use sed 's,\.[^\.]*$,,' for name, and sed 's,.*\.,., ;t ;g' for extension (uses the atypical test and get commands, along with the typical substitute command). – hIpPy Oct 7 '18 at 5:11

Mellen writes in a comment on a blog post:

Using Bash, there’s also ${file%.*} to get the filename without the extension and ${file##*.} to get the extension alone. That is,

echo "filename: ${file%.*}"
echo "extension: ${file##*.}"


filename: thisfile
extension: txt
| improve this answer | |

No need to bother with awk or sed or even perl for this simple task. There is a pure-Bash, os.path.splitext()-compatible solution which only uses parameter expansions.

Reference Implementation

Documentation of os.path.splitext(path):


Python code:

root, ext = os.path.splitext(path)

Bash Implementation

Honoring leading periods


Ignoring leading periods



Here are test cases for the Ignoring leading periods implementation, which should match the Python reference implementation on every input.

|path           |root       |ext    |
|' .txt'        |' '        |'.txt' |
|' .txt.txt'    |' .txt'    |'.txt' |
|' txt'         |' txt'     |''     |
|'*.txt.txt'    |'*.txt'    |'.txt' |
|'.cshrc'       |'.cshrc'   |''     |
|'.txt'         |'.txt'     |''     |
|'?.txt.txt'    |'?.txt'    |'.txt' |
|'\n.txt.txt'   |'\n.txt'   |'.txt' |
|'\t.txt.txt'   |'\t.txt'   |'.txt' |
|'a b.txt.txt'  |'a b.txt'  |'.txt' |
|'a*b.txt.txt'  |'a*b.txt'  |'.txt' |
|'a?b.txt.txt'  |'a?b.txt'  |'.txt' |
|'a\nb.txt.txt' |'a\nb.txt' |'.txt' |
|'a\tb.txt.txt' |'a\tb.txt' |'.txt' |
|'txt'          |'txt'      |''     |
|'txt.pdf'      |'txt'      |'.pdf' |
|'txt.tar.gz'   |'txt.tar'  |'.gz'  |
|'txt.txt'      |'txt'      |'.txt' |

Test Results

All tests passed.

| improve this answer | |
  • 2
    no, the base file name for text.tar.gz should be text and extension be .tar.gz – frederick99 Nov 16 '18 at 17:22
  • 2
    @frederick99 As I said the solution here matches the implementation of os.path.splitext in Python. Whether that implementation is sane for possibly controversial inputs is another topic. – Cyker Dec 25 '18 at 19:11
  • How do the quotes within the pattern ("$root") work? What could happen if they were omitted? (I couldn't find any documentation on the matter.) Also how does this handle filenames with * or ? in them? – ymett Jun 12 '19 at 8:14
  • Ok, testing shows me that the quotes make the pattern a literal, i.e. * and ? aren't special. So the two parts of my question answer each other. Am I correct that this isn't documented? Or is this supposed to be understood from the fact that quotes disable glob expansion in general? – ymett Jun 12 '19 at 8:25
  • Brilliant answer! I’ll just suggest a slightly simpler variant for computing the root: root="${path#?}";root="${path::1}${root%.*}" — then proceed the same to extract the extension. – Maëlan Feb 9 at 15:23

You could use the cut command to remove the last two extensions (the ".tar.gz" part):

$ echo "foo.tar.gz" | cut -d'.' --complement -f2-

As noted by Clayton Hughes in a comment, this will not work for the actual example in the question. So as an alternative I propose using sed with extended regular expressions, like this:

$ echo "mpc-1.0.1.tar.gz" | sed -r 's/\.[[:alnum:]]+\.[[:alnum:]]+$//'

It works by removing the last two (alpha-numeric) extensions unconditionally.

[Updated again after comment from Anders Lindahl]

| improve this answer | |
  • 4
    This only works in the case where the filename/path doesn't contain any other dots: echo "mpc-1.0.1.tar.gz" | cut -d'.' --complement -f2- produces "mpc-1" (just the first 2 fields after delimiting by .) – Clayton Hughes Dec 4 '13 at 0:39
  • @ClaytonHughes You're correct, and I should have tested it better. Added another solution. – Some programmer dude Dec 4 '13 at 7:52
  • The sed expressions should use $ to check that the matched extension is at the end of the file name. Otherwise, a filename like i.like.tar.gz.files.tar.bz2 might produce unexpected result. – Anders Lindahl Dec 4 '13 at 7:56
  • @AndersLindahl It still will, if the order of the extensions is the reverse of the sed chain order. Even with $ at the end a filename such as mpc-1.0.1.tar.bz2.tar.gz will remove both .tar.gz and then .tar.bz2. – Some programmer dude Dec 4 '13 at 8:03
  • $ echo "foo.tar.gz" | cut -d'.' -f2- WITHOUT --complement will get the 2nd split item to the end of the string $ echo "foo.tar.gz" | cut -d'.' -f2- tar.gz – Gene Black Mar 26 at 17:03

Here are some alternative suggestions (mostly in awk), including some advanced use cases, like extracting version numbers for software packages.


# Filename : 'file.1.0.x.tar.gz'
    echo "$f" | awk -F'/' '{print $NF}'

# Extension (last): 'gz'
    echo "$f" | awk -F'[.]' '{print $NF}'

# Extension (all) : '1.0.1.tar.gz'
    echo "$f" | awk '{sub(/[^.]*[.]/, "", $0)} 1'

# Extension (last-2): 'tar.gz'
    echo "$f" | awk -F'[.]' '{print $(NF-1)"."$NF}'

# Basename : 'file'
    echo "$f" | awk '{gsub(/.*[/]|[.].*/, "", $0)} 1'

# Basename-extended : 'file.1.0.1.tar'
    echo "$f" | awk '{gsub(/.*[/]|[.]{1}[^.]+$/, "", $0)} 1'

# Path : '/path/to/complex/'
    echo "$f" | awk '{match($0, /.*[/]/, a); print a[0]}'
    # or 
    echo "$f" | grep -Eo '.*[/]'

# Folder (containing the file) : 'complex'
    echo "$f" | awk -F'/' '{$1=""; print $(NF-1)}'

# Version : '1.0.1'
    # Defined as 'number.number' or 'number.number.number'
    echo "$f" | grep -Eo '[0-9]+[.]+[0-9]+[.]?[0-9]?'

    # Version - major : '1'
    echo "$f" | grep -Eo '[0-9]+[.]+[0-9]+[.]?[0-9]?' | cut -d. -f1

    # Version - minor : '0'

    # Version - patch : '1'

# All Components : "path to complex file 1 0 1 tar gz"
    echo "$f" | awk -F'[/.]' '{$1=""; print $0}'

# Is absolute : True (exit-code : 0)
    # Return true if it is an absolute path (starting with '/' or '~/'
    echo "$f" | grep -q '^[/]\|^~/'

All use cases are using the original full path as input, without depending on intermediate results.

| improve this answer | |

The accepted answer works well in typical cases, but fails in edge cases, namely:

  • For filenames without extension (called suffix in the remainder of this answer), extension=${filename##*.} returns the input filename rather than an empty string.
  • extension=${filename##*.} does not include the initial ., contrary to convention.
    • Blindly prepending . would not work for filenames without suffix.
  • filename="${filename%.*}" will be the empty string, if the input file name starts with . and contains no further . characters (e.g., .bash_profile) - contrary to convention.


Thus, the complexity of a robust solution that covers all edge cases calls for a function - see its definition below; it can return all components of a path.

Example call:

splitPath '/etc/bash.bashrc' dir fname fnameroot suffix
# -> $dir == '/etc'
# -> $fname == 'bash.bashrc'
# -> $fnameroot == 'bash'
# -> $suffix == '.bashrc'

Note that the arguments after the input path are freely chosen, positional variable names.
To skip variables not of interest that come before those that are, specify _ (to use throw-away variable $_) or ''; e.g., to extract filename root and extension only, use splitPath '/etc/bash.bashrc' _ _ fnameroot extension.

#   splitPath path varDirname [varBasename [varBasenameRoot [varSuffix]]] 
#   Splits the specified input path into its components and returns them by assigning
#   them to variables with the specified *names*.
#   Specify '' or throw-away variable _ to skip earlier variables, if necessary.
#   The filename suffix, if any, always starts with '.' - only the *last*
#   '.'-prefixed token is reported as the suffix.
#   As with `dirname`, varDirname will report '.' (current dir) for input paths
#   that are mere filenames, and '/' for the root dir.
#   As with `dirname` and `basename`, a trailing '/' in the input path is ignored.
#   A '.' as the very first char. of a filename is NOT considered the beginning
#   of a filename suffix.
#   splitPath '/home/jdoe/readme.txt' parentpath fname fnameroot suffix
#   echo "$parentpath" # -> '/home/jdoe'
#   echo "$fname" # -> 'readme.txt'
#   echo "$fnameroot" # -> 'readme'
#   echo "$suffix" # -> '.txt'
#   ---
#   splitPath '/home/jdoe/readme.txt' _ _ fnameroot
#   echo "$fnameroot" # -> 'readme'  
splitPath() {
  local _sp_dirname= _sp_basename= _sp_basename_root= _sp_suffix=
    # simple argument validation
  (( $# >= 2 )) || { echo "$FUNCNAME: ERROR: Specify an input path and at least 1 output variable name." >&2; exit 2; }
    # extract dirname (parent path) and basename (filename)
  _sp_dirname=$(dirname "$1")
  _sp_basename=$(basename "$1")
    # determine suffix, if any
  _sp_suffix=$([[ $_sp_basename = *.* ]] && printf %s ".${_sp_basename##*.}" || printf '')
    # determine basename root (filemane w/o suffix)
  if [[ "$_sp_basename" == "$_sp_suffix" ]]; then # does filename start with '.'?
  else # strip suffix from filename
  # assign to output vars.
  [[ -n $2 ]] && printf -v "$2" "$_sp_dirname"
  [[ -n $3 ]] && printf -v "$3" "$_sp_basename"
  [[ -n $4 ]] && printf -v "$4" "$_sp_basename_root"
  [[ -n $5 ]] && printf -v "$5" "$_sp_suffix"
  return 0

  '/Library/Application Support/'

for p in "${test_paths[@]}"; do
  echo ----- "$p"
  parentpath= fname= fnameroot= suffix=
  splitPath "$p" parentpath fname fnameroot suffix
  for n in parentpath fname fnameroot suffix; do
    echo "$n=${!n}"

Test code that exercises the function:

  '/Library/Application Support/'

  echo ----- "$p"
  parentpath= fname= fnameroot= suffix=
  splitPath "$p" parentpath fname fnameroot suffix
  for n in parentpath fname fnameroot suffix; do
    echo "$n=${!n}"

Expected output - note the edge cases:

  • a filename having no suffix
  • a filename starting with . (not considered the start of the suffix)
  • an input path ending in / (trailing / is ignored)
  • an input path that is a filename only (. is returned as the parent path)
  • a filename that has more than .-prefixed token (only the last is considered the suffix):
----- /etc/bash.bashrc
----- /usr/bin/grep
----- /Users/jdoe/.bash_profile
----- /Library/Application Support/
fname=Application Support
fnameroot=Application Support
----- readme.new.txt
| improve this answer | |

Smallest and simplest solution (in single line) is:

$ file=/blaabla/bla/blah/foo.txt
echo $(basename ${file%.*}) # foo
| improve this answer | |
  • That's a useless use of echo. In general, echo $(command) is better written simply command unless you specifically require the shell to perform whitespace tokenization and wildcard expansion on the output from command before displaying the result. Quiz: what's the output of echo $(echo '*') (and if that's what you really want, you really really want just echo *). – tripleee Nov 10 '17 at 10:34
  • @triplee I didn't use echo command at all. I just used it to demonstrate the result foo which is appearing in the 3rd line as the result of the 2nd line. – Ron Apr 17 '18 at 10:07
  • But just basename "${file%.*}" would do the same; you are using a command substitution to capture its output, only to echo that same output immediately. (Without quoting, the result is nominally different; but that's hardly relevant, much less a feature, here.) – tripleee Apr 17 '18 at 10:35
  • Also basename "$file" .txt avoids the complexity of the parameter substitution. – tripleee Apr 17 '18 at 11:21
  • 1
    @Ron Read his first comment before accusing him of wasting our time. – frederick99 Nov 16 '18 at 17:27

I think that if you just need the name of the file, you can try this:


# Remove all the prefix until the "/" character

# Remove all the prefix until the "." character

# Remove a suffix, in our case, the filename. This will return the name of the directory that contains this file.

echo "path = $FULLPATH"
echo "file name = $FILENAME"
echo "file extension = $FILEEXTENSION"
echo "base directory = $BASEDIRECTORY"

And that is all =D.

| improve this answer | |
  • Just wanted BASEDIRECTORY :) Thanks! – Carlos Ricardo Dec 9 '12 at 20:42

You can force cut to display all fields and subsequent ones adding - to field number.

NAME=`basename "$FILE"`
EXTENSION=`echo "$NAME" | cut -d'.' -f2-`

So if FILE is eth0.pcap.gz, the EXTENSION will be pcap.gz

Using the same logic, you can also fetch the file name using '-' with cut as follows :

NAME=`basename "$FILE" | cut -d'.' -f-1`

This works even for filenames that do not have any extension.

| improve this answer | |

Magic file recognition

In addition to the lot of good answers on this Stack Overflow question I would like to add:

Under Linux and other unixen, there is a magic command named file, that do filetype detection by analysing some first bytes of file. This is a very old tool, initialy used for print servers (if not created for... I'm not sure about that).

file myfile.txt
myfile.txt: UTF-8 Unicode text

file -b --mime-type myfile.txt

Standards extensions could be found in /etc/mime.types (on my Debian GNU/Linux desktop. See man file and man mime.types. Perhaps you have to install the file utility and mime-support packages):

grep $( file -b --mime-type myfile.txt ) </etc/mime.types
text/plain      asc txt text pot brf srt

You could create a function for determining right extension. There is a little (not perfect) sample:

file2ext() {
    local _mimetype=$(file -Lb --mime-type "$1") _line _basemimetype
    case ${_mimetype##*[/.-]} in
        gzip | bzip2 | xz | z )
            _basemimetype=$(file -zLb --mime-type "$1")
        stream )
            _mimetype=($(file -Lb "$1"))
            [ "${_mimetype[1]}" = "compressed" ] &&
                _basemimetype=$(file -b --mime-type - < <(
                        ${_mimetype,,} -d <"$1")) ||
        executable )  _mimetype='' _basemimetype='' ;;
        dosexec )     _mimetype='' _basemimetype='exe' ;;
        shellscript ) _mimetype='' _basemimetype='sh' ;;
        * )
    while read -a _line ;do
        if [ "$_line" == "$_basemimetype" ] ;then
            [ "$_line[1]" ] &&
                _basemimetype=${_line[1]} ||
        done </etc/mime.types
    case ${_basemimetype##*[/.-]} in
        executable ) _basemimetype='' ;;
        shellscript ) _basemimetype='sh' ;;
        dosexec ) _basemimetype='exe' ;;
        * ) ;;
    [ "$_mimetype" ] && [ "$_basemimetype" != "$_mimetype" ] &&
      printf ${2+-v} $2 "%s.%s" ${_basemimetype##*[/.-]} ${_mimetype##*[/.-]} ||
      printf ${2+-v} $2 "%s" ${_basemimetype##*[/.-]}

This function could set a Bash variable that can be used later:

(This is inspired from @Petesh right answer):

filename=$(basename "$fullfile")
file2ext "$fullfile" extension

echo "$fullfile -> $filename . $extension"
| improve this answer | |

Ok so if I understand correctly, the problem here is how to get the name and the full extension of a file that has multiple extensions, e.g., stuff.tar.gz.

This works for me:


This will give you stuff as filename and .tar.gz as extension. It works for any number of extensions, including 0. Hope this helps for anyone having the same problem =)

| improve this answer | |
  • The correct result (according to os.path.splitext, which is what the OP wants) is ('stuff.tar', '.gz'). – Cyker Dec 2 '16 at 9:15

I use the following script

$ echo "foo.tar.gz"|rev|cut -d"." -f3-|rev
| improve this answer | |
  • This is not efficient at all. To forks too many times which is quite unnecessary since this operation can be performed in pure Bash without the need for any external commands and forking. – codeforester Mar 7 '18 at 15:13
$ F = "text file.test.txt"  
$ echo ${F/*./}  

This caters for multiple dots and spaces in a filename, however if there is no extension it returns the filename itself. Easy to check for though; just test for the filename and extension being the same.

Naturally this method doesn't work for .tar.gz files. However that could be handled in a two step process. If the extension is gz then check again to see if there is also a tar extension.

| improve this answer | |

How to extract the filename and extension in fish:

function split-filename-extension --description "Prints the filename and extension"
  for file in $argv
    if test -f $file
      set --local extension (echo $file | awk -F. '{print $NF}')
      set --local filename (basename $file .$extension)
      echo "$filename $extension"
      echo "$file is not a valid file"

Caveats: Splits on the last dot, which works well for filenames with dots in them, but not well for extensions with dots in them. See example below.


$ split-filename-extension foo-0.4.2.zip bar.tar.gz
foo-0.4.2 zip  # Looks good!
bar.tar gz  # Careful, you probably want .tar.gz as the extension.

There's probably better ways to do this. Feel free to edit my answer to improve it.

If there's a limited set of extensions you'll be dealing with and you know all of them, try this:

switch $file
  case *.tar
    echo (basename $file .tar) tar
  case *.tar.bz2
    echo (basename $file .tar.bz2) tar.bz2
  case *.tar.gz
    echo (basename $file .tar.gz) tar.gz
  # and so on

This does not have the caveat as the first example, but you do have to handle every case so it could be more tedious depending on how many extensions you can expect.

| improve this answer | |

Here is code with AWK. It can be done more simply. But I am not good in AWK.

filename$ ls
abc.a.txt  a.b.c.txt  pp-kk.txt
filename$ find . -type f | awk -F/ '{print $2}' | rev | awk -F"." '{$1="";print}' | rev | awk 'gsub(" ",".") ,sub(".$", "")'
filename$ find . -type f | awk -F/ '{print $2}' | awk -F"." '{print $NF}'
| improve this answer | |
  • You shouldn't need the first awk statement in the last example, right? – BHSPitMonkey Apr 5 '13 at 21:13
  • You can avoid piping Awk to Awk by doing another split(). awk -F / '{ n=split($2, a, "."); print a[n] }' uses /` as the top-level delimiter but then splits the second fields on . and prints the last element from the new array. – tripleee Nov 10 '17 at 10:42

Simply use ${parameter%word}

In your case:


If you want to test it, all following work, and just remove the extension:

FILE=abc.xyz; echo ${FILE%.*};
FILE=123.abc.xyz; echo ${FILE%.*};
FILE=abc; echo ${FILE%.*};
| improve this answer | |
  • 2
    Why the downvote? It's still useful, although there shouldn't be spaces around the = signs. – SilverWolf - Reinstate Monica May 14 '18 at 15:24
  • 1
    This works fine. Thank you! (now it doesn't have the spaces around the equal signs, if that was why it was downvoted) – Alex. S. Dec 19 '18 at 3:11

Building from Petesh answer, if only the filename is needed, both path and extension can be stripped in a single line,

filename=$(basename ${fullname%.*})
| improve this answer | |
  • Did not work for me: "basename: missing operand Try 'basename --help' for more information." – helmy Jan 20 '16 at 17:29
  • Strange, are you certain you're using Bash? In my case, with both versions 3.2.25 (old CentOS) and 4.3.30 (Debian Jessie) it works flawlessly. – cvr Feb 1 '16 at 23:14
  • Maybe there is a space in the filename? Try using filename="$(basename "${fullname%.*}")" – Adrian Mar 14 '17 at 20:08
  • The second argument to basename is optional, but specifies the extension to strip off. The substitution might still be useful but perhaps basename actually isn't, since you can actually perform all of these substitutions with shell builtins. – tripleee Nov 10 '17 at 10:38

Based largely off of @mklement0's excellent, and chock-full of random, useful bashisms - as well as other answers to this / other questions / "that darn internet"... I wrapped it all up in a little, slightly more comprehensible, reusable function for my (or your) .bash_profile that takes care of what (I consider) should be a more robust version of dirname/basename / what have you..

function path { SAVEIFS=$IFS; IFS=""   # stash IFS for safe-keeping, etc.
    [[ $# != 2 ]] && echo "usage: path <path> <dir|name|fullname|ext>" && return    # demand 2 arguments
    [[ $1 =~ ^(.*/)?(.+)?$ ]] && {     # regex parse the path
        ext=$([[ $file = *.* ]] && printf %s ${file##*.} || printf '')
        # edge cases for extensionless files and files like ".nesh_profile.coffee"
        [[ $file == $ext ]] && fnr=$file && ext='' || fnr=${file:0:$((${#file}-${#ext}))}
        case "$2" in
             dir) echo      "${dir%/*}"; ;;
            name) echo      "${fnr%.*}"; ;;
        fullname) echo "${fnr%.*}.$ext"; ;;
             ext) echo           "$ext"; ;;

Usage examples...

SOMEPATH=/path/to.some/.random\ file.gzip
path $SOMEPATH dir        # /path/to.some
path $SOMEPATH name       # .random file
path $SOMEPATH ext        # gzip
path $SOMEPATH fullname   # .random file.gzip                     
path gobbledygook         # usage: -bash <path> <dir|name|fullname|ext>
| improve this answer | |
  • 1
    Nicely done; a few suggestions: - You don't seem to be relying on $IFS at all (and if you were, you could use local to localize the effect of setting it). - Better to use local variables. - Your error message should be output to stderr, not stdout (use 1>&2), and you should return a non-zero exit code. - Better to rename fullname to basename (the former suggests a path with dir components). - name unconditionally appends a . (period), even if the original had none. You could simply use the basename utility, but note that it ignores a terminating /. – mklement0 Nov 26 '13 at 14:18

A simple answer:

To expand on the POSIX variables answer, note that you can do more interesting patterns. So for the case detailed here, you could simply do this:

tar -zxvf $1
cd ${1%.tar.*}

That will cut off the last occurrence of .tar.<something>.

More generally, if you wanted to remove the last occurrence of .<something>.<something-else> then


should work fine.

The link the above answer appears to be dead. Here's a great explanation of a bunch of the string manipulation you can do directly in Bash, from TLDP.

| improve this answer | |
  • Is there a way to make the match case-insensitive? – tonix Jan 2 '15 at 9:42

If you also want to allow empty extensions, this is the shortest I could come up with:

echo 'hello.txt' | sed -r 's/.+\.(.+)|.*/\1/' # EXTENSION
echo 'hello.txt' | sed -r 's/(.+)\..+|(.*)/\1\2/' # FILENAME

1st line explained: It matches PATH.EXT or ANYTHING and replaces it with EXT. If ANYTHING was matched, the ext group is not captured.

| improve this answer | |

This is the only one that worked for me:


echo ${base%.*}

>> file

This can also be used in string interpolation as well, but unfortunately you have to set base beforehand.

| improve this answer | |

Here is the algorithm I used for finding the name and extension of a file when I wrote a Bash script to make names unique when names conflicted with respect to casing.

#! /bin/bash 

# Finds 
# -- name and extension pairs
# -- null extension when there isn't an extension.
# -- Finds name of a hidden file without an extension

declare -a fileNames=(
  'San Diego.txt'
  'San Francisco' 

echo "Script ${0} finding name and extension pairs."

for theFileName in "${fileNames[@]}"
     echo "theFileName=${theFileName}"  

     # Get the proposed name by chopping off the extension

     # get extension.  Set to null when there isn't an extension
     # Thanks to mklement0 in a comment above.
     extension=$([[ "$theFileName" == *.* ]] && echo ".${theFileName##*.}" || echo '')

     # a hidden file without extenson?
     if [ "${theFileName}" = "${extension}" ] ; then
         # hidden file without extension.  Fixup.

     echo "  name=${name}"
     echo "  extension=${extension}"

The test run.

$ config/Name\&Extension.bash 
Script config/Name&Extension.bash finding name and extension pairs.

theFileName=San Diego.txt
  name=San Diego
theFileName=San Francisco
  name=San Francisco

FYI: The complete transliteration program and more test cases can be found here: https://www.dropbox.com/s/4c6m0f2e28a1vxf/avoid-clashes-code.zip?dl=0

| improve this answer | |
  • From all the solutions this is the only one that returns an empty string when the file has no extension with: extension=$([[ "$theFileName" == *.* ]] && echo ".${theFileName##*.}" || echo '') – f0nzie May 25 at 19:38

Using example file /Users/Jonathan/Scripts/bash/MyScript.sh, this code:

ME=$(/usr/bin/basename "${0}" "${MY_EXT}")

will result in ${ME} being MyScript and ${MY_EXT} being .sh:


set -e


echo "${ME} - ${MY_EXT}"

Some tests:

$ ./MyScript.sh 
MyScript - .sh

$ bash MyScript.sh
MyScript - .sh

$ /Users/Jonathan/Scripts/bash/MyScript.sh
MyScript - .sh

$ bash /Users/Jonathan/Scripts/bash/MyScript.sh
MyScript - .sh
| improve this answer | |
  • 2
    Not sure why this has so many downvotes - it's actually more efficient than the accepted answer. (As the latter, it also breaks with input filenames without an extension). Using an explicit path to basename is, perhaps, overkill. – mklement0 Dec 9 '14 at 16:35

From the answers above, the shortest oneliner to mimic Python's

file, ext = os.path.splitext(path)

presuming your file really does have an extension, is

EXT="${PATH##*.}"; FILE=$(basename "$PATH" .$EXT)
| improve this answer | |
  • I've got downvotes on this. I'm considering to remove the answer, people somehow dislike it. – commonpike Dec 11 '14 at 11:26
  • basename does not remove the extension, just the path. – David Cullen Feb 9 '16 at 17:19
  • It has been so long since I looked at the man page I forgot about the SUFFIX option. – David Cullen Feb 10 '16 at 0:13
  • You have to know which extension you want to strip off before you know what to put in EXT so this is turtles all the way down. (Also, you should avoid all uppercase for your private variable names; they are reserved for system variables.) – tripleee Nov 10 '17 at 10:39
