50

How do I get the file extension from bash? Here's what I tried:

filename=`basename $filepath`
fileext=${filename##*.}

By doing that I can get extension of bz2 from the path /dir/subdir/file.bz2, but I have a problem with the path /dir/subdir/file-1.0.tar.bz2.

I would prefer a solution using only bash without external programs if it is possible.

To make my question clear, I was creating a bash script to extract any given archive just by a single command of extract path_to_file. How to extract the file is determined by the script by seeing its compression or archiving type, that could be .tar.gz, .gz, .bz2 etc. I think this should involve string manipulation, for example if I get the extension .gz then I should check whether it has the string .tar before .gz — if so, the extension should be .tar.gz.

1

10 Answers 10

39

You might simplify matters by just doing pattern matching on the filename rather than extracting the extension twice:

case "$filename" in
    *.tar.bz2) bunzip_then_untar ;;
    *.bz2)     bunzip_only ;;
    *.tar.gz)  untar_with -z ;;
    *.tgz)     untar_with -z ;;
    *.gz)      gunzip_only ;;
    *.zip)     unzip ;;
    *.7z)      do something ;;
    *)         do nothing ;;
esac
0
22

If the file name is file-1.0.tar.bz2, the extension is bz2. The method you're using to extract the extension (fileext=${filename##*.}) is perfectly valid¹.

How do you decide that you want the extension to be tar.bz2 and not bz2 or 0.tar.bz2? You need to answer this question first. Then you can figure out what shell command matches your specification.

  • One possible specification is that extensions must begin with a letter. This heuristic fails for a few common extensions like 7z, which might be best treated as a special case. Here's a bash/ksh/zsh implementation:

    basename=$filename; fileext=
    while [[ $basename = ?*.* &&
             ( ${basename##*.} = [A-Za-z]* || ${basename##*.} = 7z ) ]]
    do
      fileext=${basename##*.}.$fileext
      basename=${basename%.*}
    done
    fileext=${fileext%.}
    

    For POSIX portability, you need to use a case statement for pattern matching.

    while case $basename in
            ?*.*) case ${basename##*.} in [A-Za-z]*|7z) true;; *) false;; esac;;
            *) false;;
          esac
    do …
    
  • Another possible specification is that some extensions denote encodings and indicate that further stripping is needed. Here's a bash/ksh/zsh implementation (requiring shopt -s extglob under bash and setopt ksh_glob under zsh):

    basename=$filename
    fileext=
    while [[ $basename = ?*.@(bz2|gz|lzma) ]]; do
      fileext=${basename##*.}.$fileext
      basename=${basename%.*}
    done
    if [[ $basename = ?*.* ]]; then
      fileext=${basename##*.}.$fileext
      basename=${basename%.*}
    fi
    fileext=${fileext%.}
    

    Note that this considers 0 to be an extension in file-1.0.gz.

¹ ${VARIABLE##SUFFIX} and related constructs are in POSIX, so they work in any non-antique Bourne-style shell such as ash, bash, ksh or zsh.

3
  • that should be solved, by checking if the string before last . token is archive type, for example tar, if its not archive type like 0 iteration should end.
    – uray
    Commented Sep 4, 2010 at 12:41
  • 2
    @uray: that works in this particular case, but it's not a general solution. Consider Maciej's example of .patch.lzma. A better heuristic would be to consider the string after the last .: if it's a compression suffix (.7z, .bz2, .gz, ...), continue stripping. Commented Sep 4, 2010 at 13:15
  • @NoamM What was wrong with the indentation? It's definitely broken after your edit: doubly-nested code is indented the same as singly-nested. Commented Aug 28, 2019 at 11:12
15
$ echo "thisfile.txt"|awk -F . '{print $NF}'

Comments on this here: http://liquidat.wordpress.com/2007/09/29/short-tip-get-file-extension-in-shell-script/

2
  • 2
    not work for .tar.gz extension
    – uray
    Commented Sep 4, 2010 at 12:02
  • 6
    Well a .tar.gz is actually a tar inside a gzip file so it does work in the sense that it removes a gz extension from a gzip file.
    – Chris
    Commented Sep 4, 2010 at 15:24
5

Here's my shot at it: Translate dots to newlines, pipe through tail, get last line:

$> TEXT=123.234.345.456.456.567.678
$> echo $TEXT | tr . \\n | tail -n1
678
1

One day I've created those tricky functions:

# args: string how_many
function get_last_letters(){ echo ${1:${#1}-$2:$2}; }
function cut_last_letters(){ echo ${1:0:${#1}-$2}; }

I've found this straightforward approach, very useful in many cases, not only when it goes about extensions.

For checking extensions - It's simple and reliable

~$ get_last_letters file.bz2 4
.bz2
~$ get_last_letters file.0.tar.bz2 4
.bz2

For cutting-off extension:

~$ cut_last_letters file.0.tar.bz2 4
file.0.tar

For changing extension:

~$ echo $(cut_last_letters file.0.tar.bz2 4).gz
file.0.tar.gz

Or, if you like "handy functions:

~$ function cut_last_letters_and_add(){ echo ${1:0:${#1}-$2}"$3"; }
~$ cut_last_letters_and_add file.0.tar.bz2 4 .gz
file.0.tar.gz

P.S. If you liked those functions or found them usedfull, please refer to this post :) (and hopefully put a comment).

1

Note: this doesn't just answer the OP's question. Rather, it also answers the generic question posed by just the title of this question:

Grabbing the extension in a file name

To answer the OP:

echo "/dir/subdir/file-1.0.tar.bz2" \
    | sed 's/.*\///' | grep -oE "\.[^0-9]*\..*$"

Output:

.tar.bz2

The sed part obtains just the string after the last /, and the grep part obtains the extension beginning with a period (\.), any number of chars not containing a number ([^0-9]*), and then a period followed by any char to the end of the line (\..*$).

General answer: how to extract just the extensions from a multi-line string containing a bunch of filenames

...including extracting more-complicated extensions, such as .tar.gz from file.10.5.2.tar.gz (basically just ignoring numeric 0-9 portions of extensions).

I really like this solution, piping the filenames to sed 's/.*\///' | grep -oE "(^[^.]*$|\.[^0-9]*\..*$)" | sort -u. The grep regex portion of the answer is pretty complicated because it needs to remove the portion of the extensions which contain numbers 0-9.

Example:

filenames_str="\
/some/file.txt
/whatever/prog.c
/something/abc.tar.bz
/something/abc.123.456.789.tar.bz
/something/abc.c
/something/abc.h
/path/to/file.10.5.2.tar.gz
/path/to/file.10.5.2.tar.gz.whatever
/path/to/file.10.5.2.tar.gz.whatever.7.pdf
/noextension"


echo "$filenames_str" \
    | sed 's/.*\///' | grep -oE "(^[^.]*$|(\.[^0-9])*(\.[^0-9]*$))" | sort -u

Output:

.c
.h
noextension
.pdf
.tar.bz
.tar.gz
.tar.gz.whatever
.txt

Explanation, from my answer here (although the above is modified from my other answer, to answer the OP's question above as well): All about finding, filtering, and sorting with find, based on file size:

The sed part retains just the contents after the last /. The grep part then keeps only the extension, including the dot (.), if it has one, and the whole string otherwise. And finally, sort -u removes duplicates to leave only unique strings.

0
0
echo ${filename#$(echo $filename | sed 's/\.[^[:digit:]].*$//g;')}

For example:

% echo $filename
2.6.35-zen2.patch.lzma
% echo ${filename#$(echo $filename | sed 's/\.[^[:digit:]].*$//g;')}
.patch.lzma
4
  • Does not work for all cases. Try with 'foo.7z'
    – axel_c
    Commented Sep 4, 2010 at 12:19
  • You need quotes, and better use printf in case the file name contains a backslash or begins with -: "${filename#$(printf %s "$filename" | sed 's/\.[^[:digit:]].*$//g;')}" Commented Sep 4, 2010 at 12:20
  • @axel_c: right, and I've implemented the same specification as Maciej as an example. What heuristic do you suggest that's better than “begins with a letter”? Commented Sep 4, 2010 at 12:22
  • 1
    @Gilles: i just think there's not a solution unless you use a precomputed list of known extensions, because an extension can be anything.
    – axel_c
    Commented Sep 4, 2010 at 12:42
0

the jackman case-based answer is pretty good and portable, but if you just want the filename and extension in a variable i've found this solution:

INPUTFILE="$1"
INPUTFILEEXT=$( echo -n "$INPUTFILE" | rev | cut -d'.' -f1 | rev )
INPUTFILEEXT=$( echo -n $INPUTFILEEXT | tr '[A-Z]' '[a-z]' ) # force lowercase extension
INPUTFILENAME="`echo -n \"$INPUTFILE\" | rev | cut -d'.' -f2- | rev`"

# fix for files with multiple extensions like "gbamidi-v1.0.tar.gz"
INPUTFILEEXT2=$( echo -n "$INPUTFILENAME" | rev | cut -d'.' -f1 | rev )
if [ "$INPUTFILEEXT2" = "tar" ]; then
    # concatenate the extension
    INPUTFILEEXT="$INPUTFILEEXT2.$INPUTFILEEXT"
    # update the filename
    INPUTFILENAME="`echo -n \"$INPUTFILENAME\" | rev | cut -d'.' -f2- | rev`"
fi

It only works with double extensions and the first one must be "tar".

But you can change the "tar" test line with a string length test and repeat the fix multiple times.

0

Using Raku (formerly known as Perl_6)

~$ raku -ne 'put .IO.extension();'  file

#OR (handles up to an 8-part file extension):

~$ raku -ne 'put .IO.extension( :parts(^9) );'  file

Sample Input:

/test1/a/sample1.xls
/test2/demo.sh
/some/file.txt
/whatever/prog.c
/something/abc.tar.bz
/something/abc.123.456.789.tar.bz
/something/abc.c
/something/abc.h
/path/to/file.10.5.2.tar.gz
/path/to/file.10.5.2.tar.gz.whatever
/path/to/file.10.5.2.tar.gz.whatever.7.pdf
/noextension

Sample Output:

xls
sh
txt
c
tar.bz
123.456.789.tar.bz
c
h
10.5.2.tar.gz
10.5.2.tar.gz.whatever
10.5.2.tar.gz.whatever.7.pdf

Briefly, the file is read linewise using the -ne non-autoprinting linewise flags. The code is run over each line: First the path is interpreted as an IO object, for which an extension can be identified/extracted. Within the extension parameters, adding the :parts parameter (a.k.a. "adverb") allows multi-part file-extension identification, in this case :parts(^9) means 0..8 parts.

Note, because filepaths are understood by Raku with OS-specific settings, the codes above should work unmodified on Windows to extract the correct extensions from Windows paths (i.e. Raku understands backslash as a path-separator on Windows OS).

https://docs.raku.org/type/IO/Path
https://docs.raku.org/routine/basename
https://docs.raku.org/routine/extension
https://raku.org

Example Source (thanks to @Gabriel_Staples):
https://unix.stackexchange.com/a/731665/227738

-1

i solved it using this:

filename=`basename $filepath`
fileext=${filename##*.}
fileext2=${filename%.*}
fileext3=${fileext2##*.}
if [ "$fileext3" == "tar" ]; then
    fileext="tar."$fileext
fi

but this only work for known archiving type, in this case only tar

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .