244

I am writing a nightly build script in bash.
Everything is fine and dandy except for one little snag:

#!/bin/bash

for file in "$PATH_TO_SOMEWHERE"; do
      if [ -d $file ]
      then
              # do something directory-ish
      else
              if [ "$file" == "*.txt" ]       #  this is the snag
              then
                     # do something txt-ish
              fi
      fi
done;

My problem is determining the file extension and then acting accordingly. I know the issue is in the if-statement, testing for a txt file.

How can I determine if a file has a .txt suffix?

vvvvv
  • 25,404
  • 19
  • 49
  • 81
  • This will break if you have a file with a space in its name. – jfg956 Aug 03 '11 at 12:11
  • In addition to the answer of Paul, you can use `$(dirname $PATH_TO_SOMEWHERE)` and `$(basename $PATH_TO_SOMEWHERE)` to split into folder and directory and do something directory-ish and file-ish – McPeppr May 17 '16 at 11:52

11 Answers11

371

Make

if [ "$file" == "*.txt" ]

like this:

if [[ $file == *.txt ]]

That is, double brackets and no quotes.

The right side of == is a shell pattern. If you need a regular expression, use =~ then.

  • 16
    I didn't know about this. It seems to be a special case that the right-hand side of == or != is expanded as a shell pattern. Personally I think this is clearer than my answer. – Paul Stephenson Jun 11 '09 at 08:12
  • 28
    I am new to bash and it took me a little while to figure out how to use this in a multi conditional `if` statement. I am sharing it here in case it helps someone. `if [[ ( $file == *.csv ) || ( $file == *.png ) ]]` – joelostblom Feb 07 '15 at 01:09
  • 13
    @cheflo that's good for multiple conditions in general. In this specific case, you could also use `if [[ $file =~ .*\.(csv|png) ]]`. It's shorter, clearer, easier to add additional extensions and could be easily made configurable (by putting "csv|png" in a variable). – jox May 29 '16 at 10:10
  • 13
    You can put double quotes around the file. `if [[ "$file" == *.txt ]]` If the file has spaces in its name, double-quoting is required. – shawnhcorey Aug 02 '16 at 11:02
  • 2
    Should I use this answer instead of the accepted one? – Freedo Nov 26 '18 at 02:02
  • 3
    @shawnhcorey There is no need for quoting $file, even if it contains a space. The [[ command does not do word splitting (nor pathname expansion). That is one of the main differences between [[ and [, and one of the main reasons to use [[. – Hontvári Levente May 09 '20 at 06:19
  • 1 Lesson learned: use `[[]]` if you need shell pattern, if not, `[]` is enough. – Timo Feb 07 '23 at 20:22
309

I think you want to say "Are the last four characters of $file equal to .txt?" If so, you can use the following:

if [ "${file: -4}" == ".txt" ]

Note that the space between file: and -4 is required, as the ':-' modifier means something different.

Teemu Leisti
  • 3,750
  • 2
  • 30
  • 39
Paul Stephenson
  • 67,682
  • 9
  • 49
  • 51
  • 1
    to that end, you can rename command.com to command.txt on a windows machine too. – hometoast Jun 09 '09 at 18:10
  • 12
    If you want to specify an inequality, remember to include extra brackets: if [[ ${file: -4} != ".txt" ]] – Ram Rajamony Jul 20 '13 at 23:38
  • 5
    @RamRajamony Why is it necessary to use [[ when testing inequality? – PesaThe Dec 08 '17 at 23:48
  • I wanted to point out that the space after the colon is important. `${var:-4}` is not the same as `${var: -4}`; the first (without a space) will expand as '-4' if var is unset the second (with a space) returns the last 4 characters of var. – pbatey Jul 19 '19 at 17:05
  • 3
    In bash, this will produce a "[: ==: unary operator expected" error unless you put quotes around the first variable. So `if [ "${file: -4}" == ".txt" ]` instead. – Giles B Oct 18 '19 at 15:03
  • Useful for me in particular if I want to not modify my bash script ending in `.sh` yet want to add `.csv` to every other file as referenced in this [post](https://unix.stackexchange.com/a/648496/466623) – GigaWatts May 06 '21 at 20:32
  • 1
    A note for those that may be new to the bash/shell syntax: the space before the -4 is significant. `"${file:-4}" == ".txt"` (no space) will not work as expected. – KOGI Jul 16 '21 at 18:35
34

You could also do:

   if [ "${FILE##*.}" = "txt" ]; then
       # operation for txt files here
   fi
kvz
  • 5,517
  • 1
  • 42
  • 33
29

You just can't be sure on a Unix system, that a .txt file truly is a text file. Your best bet is to use "file". Maybe try using:

file -ib "$file"

Then you can use a list of MIME types to match against or parse the first part of the MIME where you get stuff like "text", "application", etc.

Eldelshell
  • 6,683
  • 7
  • 44
  • 63
23

You can use the "file" command if you actually want to find out information about the file rather than rely on the extensions.

If you feel comfortable with using the extension you can use grep to see if it matches.

Adam Peck
  • 6,930
  • 3
  • 23
  • 27
  • yes I am aware of the `file` command. I had actually tried matching based on the output of said command... but I fail horribly at these if-statements. –  Jan 02 '09 at 15:57
17

The correct answer on how to take the extension available in a filename in linux is:

${filename##*\.} 

Example of printing all file extensions in a directory

for fname in $(find . -maxdepth 1 -type f) # only regular file in the current dir
    do  echo ${fname##*\.} #print extensions 
done
Ali Tou
  • 2,009
  • 2
  • 18
  • 33
Albin. Com.
  • 191
  • 1
  • 4
  • Your answer uses a double backslash, but your example only uses a single backslash. Your example is correct, your answer isn't. – gilbertpilz May 19 '17 at 00:52
  • Can you please explain the answer a little bit or link a documentation? I need to understand what does ${fname##* mean. Regards – thermz Aug 30 '22 at 09:39
12

Similar to 'file', use the slightly simpler 'mimetype -b' which will work no matter the file extension.

if [ $(mimetype -b "$MyFile") == "text/plain" ]
then
  echo "this is a text file"
fi

Edit: you may need to install libfile-mimeinfo-perl on your system if mimetype is not available

dargaud
  • 2,431
  • 2
  • 26
  • 39
3

I wrote a bash script that looks at the type of a file then copies it to a location, I use it to look through the videos I've watched online from my firefox cache:

#!/bin/bash
# flvcache script

CACHE=~/.mozilla/firefox/xxxxxxxx.default/Cache
OUTPUTDIR=~/Videos/flvs
MINFILESIZE=2M

for f in `find $CACHE -size +$MINFILESIZE`
do
    a=$(file $f | cut -f2 -d ' ')
    o=$(basename $f)
    if [ "$a" = "Macromedia" ]
        then
            cp "$f" "$OUTPUTDIR/$o"
    fi
done

nautilus  "$OUTPUTDIR"&

It uses similar ideas to those presented here, hope this is helpful to someone.

desdecode
  • 31
  • 1
3

I guess that '$PATH_TO_SOMEWHERE'is something like '<directory>/*'.

In this case, I would change the code to:

find <directory> -maxdepth 1 -type d -exec ... \;
find <directory> -maxdepth 1 -type f -name "*.txt" -exec ... \;

If you want to do something more complicated with the directory and text file names, you could:

find <directory> -maxdepth 1 -type d | while read dir; do echo $dir; ...; done
find <directory> -maxdepth 1 -type f -name "*.txt" | while read txtfile; do echo $txtfile; ...; done

If you have spaces in your file names, you could:

find <directory> -maxdepth 1 -type d | xargs ...
find <directory> -maxdepth 1 -type f -name "*.txt" | xargs ...
jfg956
  • 16,077
  • 4
  • 26
  • 34
  • These are great examples of how you do 'loops' in shell. Explicit `for` and `while` loops better be reserved for when the loop body needs to be more complex. –  Jan 09 '14 at 21:30
3

Credit to @Jox for the majority of this answer, though I found (js) was matching .json files so I added an eol character to more fully match the extension.

$file does not need to be quoted because [[ ]] won't expand and so spaces aren't an issue (credit: Hontvári Levente)

if [[ $file =~ .*\.(js$|json$) ]]; then
  echo "The extension of '$file' matches .js|.json";
fi
rainabba
  • 3,804
  • 35
  • 35
0

Another important detail, you don't use else with another if inside:

else
    if [ "$file" == "*.txt" ]       
    #  this is the snag
    then
    # do something txt-ish
fi

instead:

elif [ "$file" == "*.txt" ]       
    #  this is the snag
then
    # do something txt-ish
fi

else is used when there's nothing else left > do > thatcommand

just because you can do something that doesn't necessarily means you should always do it