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 KAug 7 '13 at 18:51

3

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 GibsonOct 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. HauriOct 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

Heck, you could even write filename="${fullfile##*/}" and avoid calling an extra basename
– ephemientJun 9 '09 at 17:52

36

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.
– ncccJul 1 '12 at 3:42

37

Fix for dealing with file names without extension: extension=$([[ "$filename" = *.* ]] && echo ".${filename##*.}" || echo ''). Note that if an extension is present, it will be returned including the initial ., e.g., .txt.
– mklement0Sep 7 '12 at 14:41

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.
– rmeadorJun 8 '09 at 14:50

7

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.
– JulianoJun 8 '09 at 15:20

18

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 :)
– porgesJun 13 '09 at 9:11

6

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 SellersMar 5 '12 at 18:49

5

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.oddouNov 26 '13 at 7:29

${variable%pattern}
Trim the shortest match from the end
${variable##pattern}
Trim the longest match from the beginning
${variable%%pattern}
Trim the longest match from the end
${variable#pattern}
Trim the shortest match from the beginning

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.
– jwadsackJul 18 '14 at 16:40

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.
– dubiousjimMay 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 PersonMay 25 '13 at 20:32

@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 PersonJul 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?
– vol7ronJul 12 '13 at 22:08

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-HansenSep 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-HansenSep 17 '17 at 4:49

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.

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).
– hIpPyOct 7 '18 at 5:11

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 HughesDec 4 '13 at 0:39

@ClaytonHughes You're correct, and I should have tested it better. Added another solution.
– Some programmer dudeDec 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 LindahlDec 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 dudeDec 4 '13 at 8:03

Reference Implementation

Split the pathname path into a pair (root, ext) such that root + ext == path, and ext is empty or begins with a period and contains at most one period. Leading periods on the basename are ignored; splitext('.cshrc') returns ('.cshrc', '').

Test Results

no, the base file name for text.tar.gz should be text and extension be .tar.gz
– frederick99Nov 16 '18 at 17:22

@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.
– CykerDec 25 '18 at 19:11

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 *).
– tripleeeNov 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.
– RonApr 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.)
– tripleeeApr 17 '18 at 10:35

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.

FULLPATH=/usr/share/X11/xorg.conf.d/50-synaptics.conf
# Remove all the prefix until the "/" character
FILENAME=${FULLPATH##*/}
# Remove all the prefix until the "." character
FILEEXTENSION=${FILENAME##*.}
# Remove a suffix, in our case, the filename. This will return the name of the directory that contains this file.
BASEDIRECTORY=${FULLPATH%$FILENAME}
echo "path = $FULLPATH"
echo "file name = $FILENAME"
echo "file extension = $FILEEXTENSION"
echo "base directory = $BASEDIRECTORY"

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).

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.
– codeforesterMar 7 '18 at 15:13

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.

You shouldn't need the first awk statement in the last example, right?
– BHSPitMonkeyApr 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.
– tripleeeNov 10 '17 at 10:42

Did not work for me: "basename: missing operand Try 'basename --help' for more information."
– helmyJan 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.
– cvrFeb 1 '16 at 23:14

Maybe there is a space in the filename? Try using filename="$(basename "${fullname%.*}")"
– AdrianMar 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.
– tripleeeNov 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..

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 /.
– mklement0Nov 26 '13 at 14:18

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.
– mklement0Dec 9 '14 at 16:35

Thank you for your interest in this question.
Because it has attracted low-quality or spam answers that had to be removed, posting an answer now requires 10 reputation on this site (the association bonus does not count).