286

What's wrong with the following code?

name='$filename | cut -f1 -d'.''

As is, I get the literal string $filename | cut -f1 -d'.', but if I remove the quotes I don't get anything. Meanwhile, typing

"test.exe" | cut -f1 -d'.'

in a shell gives me the output I want, test. I already know $filename has been assigned the right value. What I want to do is assign to a variable the filename without the extension.

3
  • 18
    basename $filename .exe would do the same thing. That's assuming you always know what extension you want to remove.
    – mpe
    Commented Aug 28, 2012 at 14:54
  • 14
    @mpe, you mean basename "$filename" .exe. Otherwise filenames with spaces would be bad news. Commented Jun 15, 2016 at 19:35
  • See also Extract substring in Bash where also "${file#prestring}" is explained. Commented May 20, 2022 at 15:43

15 Answers 15

498

You can also use parameter expansion:

$ filename=foo.txt
$ echo "${filename%.*}"
foo

If you have a filepath and not just a filename, you'll want to use basename first to get just the filename including the extension. Otherwise, if there's a dot only in the path (e.g. path.to/myfile or ./myfile), then it will trim inside the path; even if there isn't a dot in the path, it will get the (e.g. path/to/myfile if the path is path/to/myfile.txt):

$ filepath=path.to/foo.txt
$ echo "${filepath%.*}"
path.to/foo
$ filename=$(basename $filepath)
$ echo $filename
foo.txt
$ echo "${filename%.*}"
foo

Just be aware that if the filename only starts with a dot (e.g. .bashrc) it will remove the whole filename.

11
  • 12
    here the explanation of the command: gnu.org/software/bash/manual/html_node/… Commented Jul 6, 2016 at 16:33
  • 3
    And here I was about to use echo -n "This.File.Has.Periods.In.It.txt" | awk -F. '{$NF=""; print $0}' | tr ' ' '.' | rev | cut -c 2- | rev. Thanks.
    – user208145
    Commented Aug 30, 2016 at 23:18
  • 3
    Does this work with files with multiple extensions like image.png.gz?
    – Hawker65
    Commented Apr 26, 2018 at 8:58
  • 22
    %.* will only remove the last extension; if you want to remove all the extensions, use %%.*.
    – chepner
    Commented Apr 26, 2018 at 13:20
  • 5
    Be warned this deletes the entire name if there is no extension and the path is relative. E.g., filename=./ihavenoextension.
    – jpmc26
    Commented Sep 14, 2019 at 9:02
199

If you know the extension, you can use basename

$ basename /home/jsmith/base.wiki .wiki
base
5
  • 5
    How can I remove just .wiki and end up with /home/jsmith/base? Commented Jul 1, 2019 at 16:30
  • Update: answer: See stackoverflow.com/a/32584935/4561887. Works perfectly! Commented Jul 1, 2019 at 16:33
  • 2
    this should be the accepted answer in my view
    – Nam G VU
    Commented Sep 22, 2021 at 3:46
  • 1
    @GabrielStaples you could combine it with dirname, e.g. file=/home/jsmith/base.wiki; echo $(dirname $file)/$(basename $file .wiki)
    – jena
    Commented Nov 12, 2021 at 15:34
  • 1
    I went with this. The nice thing is that basename is made for this and I discovered a new capability in what it can do.
    – JL Peyret
    Commented Nov 14, 2021 at 5:18
179

You should be using the command substitution syntax $(command) when you want to execute a command in script/command.

So your line would be

name=$(echo "$filename" | cut -f 1 -d '.')

Code explanation:

  1. echo get the value of the variable $filename and send it to standard output
  2. We then grab the output and pipe it to the cut command
  3. The cut will use the . as delimiter (also known as separator) for cutting the string into segments and by -f we select which segment we want to have in output
  4. Then the $() command substitution will get the output and return its value
  5. The returned value will be assigned to the variable named name

Note that this gives the portion of the variable up to the first period .:

$ filename=hello.world
$ echo "$filename" | cut -f 1 -d '.'
hello
$ filename=hello.hello.hello
$ echo "$filename" | cut -f 1 -d '.'
hello
$ filename=hello
$ echo "$filename" | cut -f 1 -d '.'
hello
10
  • 21
    Backticks are deprecated by POSIX, $() is preferred.
    – jordanm
    Commented Aug 28, 2012 at 4:48
  • 7
    Forking and piping to get at a few characters is about the worst solution imaginable.
    – Jens
    Commented Sep 15, 2015 at 11:35
  • 55
    The problem with this answer is it assumes input string has ONLY one dot ... @chepner below has a much better solution ... name=${filename%.*} Commented Apr 30, 2016 at 12:24
  • 8
    This answer is characteristic of beginners and should not be spreaded. Use the builtin mechanism as described by chepner's answer
    – neric
    Commented Apr 24, 2017 at 13:02
  • 2
    Fails on ./ihavenoextension, just like the other answer.
    – jpmc26
    Commented Sep 14, 2019 at 9:10
53

If your filename contains a dot (other than the one of the extension) then use this:

echo $filename | rev | cut -f 2- -d '.' | rev
8
  • 1
    I forgot the middle rev, but once I saw it, this was great! Commented Sep 29, 2016 at 17:11
  • It’s even better with an -s option given to cut, so that it returns an empty string whenever the filename contains no dot.
    – Hibou57
    Commented Apr 27, 2020 at 10:59
  • 4
    This should be the accepted answer in my opinion, since it works on path with dots in them, with hidden files starting with a dot, or even with file with multiple extensions.
    – Tim Krief
    Commented Jun 22, 2020 at 8:02
  • 2
    What happens with a file like this: filename=/tmp.d/foo?
    – FedKad
    Commented Sep 13, 2020 at 11:17
  • 2
    It has problems with "dot files" also. Try with .profile for example.
    – FedKad
    Commented Sep 15, 2020 at 12:45
43

Using POSIX's built-in only:

#!/usr/bin/env sh
path=this.path/with.dots/in.path.name/filename.tar.gz

# Get the basedir without external command
# by stripping out shortest trailing match of / followed by anything
dirname=${path%/*}

# Get the basename without external command
# by stripping out longest leading match of anything followed by /
basename=${path##*/}

# Strip uptmost trailing extension only
# by stripping out shortest trailing match of dot followed by anything
oneextless=${basename%.*}; echo "$oneextless" 

# Strip all extensions
# by stripping out longest trailing match of dot followed by anything
noext=${basename%%.*}; echo "$noext"

# Printout demo
printf %s\\n "$path" "$dirname" "$basename" "$oneextless" "$noext"

Printout demo:

this.path/with.dots/in.path.name/filename.tar.gz
this.path/with.dots/in.path.name
filename.tar.gz
filename.tar
filename
3
  • What happens with a path like /home/user/.profile?
    – FedKad
    Commented Nov 30, 2022 at 13:00
  • 1
    @FedKad How about you test it?
    – Léa Gris
    Commented Dec 1, 2022 at 8:11
  • For a path like /home/user/.profile the $oneextless and $noext variables become empty strings; in other words the "dot-file" name vanishes.
    – FedKad
    Commented Dec 1, 2022 at 10:36
28
file1=/tmp/main.one.two.sh
t=$(basename "$file1")                        # output is main.one.two.sh
name=$(echo "$file1" | sed -e 's/\.[^.]*$//') # output is /tmp/main.one.two
name=$(echo "$t" | sed -e 's/\.[^.]*$//')     # output is main.one.two

use whichever you want. Here I assume that last . (dot) followed by text is extension.

1
  • What happens when file1=/tmp.d/mainonetwosh? The sed expression should be replaced with 's/\.[^./]*$//'
    – FedKad
    Commented Sep 13, 2020 at 10:23
8
#!/bin/bash
file=/tmp/foo.bar.gz
echo $file ${file%.*}

outputs:

/tmp/foo.bar.gz /tmp/foo.bar

Note that only the last extension is removed.

1
  • 3
    What happens with a file like this: file=/tmp.d/foo ?
    – FedKad
    Commented Sep 13, 2020 at 11:15
5

In Zsh:

fullname=bridge.zip
echo ${fullname:r}

It's simple, clean and it can be chained to remove more than one extension:

fullname=bridge.tar.gz
echo ${fullname:r:r}

And it can be combined with other similar modifiers.

3
#!/bin/bash
filename=program.c
name=$(basename "$filename" .c)
echo "$name"

outputs:

program
2
  • 1
    How is this different from the answer given by Steven Penny 3 years ago? Commented Apr 28, 2018 at 21:49
  • @gniourf_gniourf upvoting because you've made it a useful answer by showing how to correctly use it with a variable.
    – mwfearnley
    Commented Sep 30, 2020 at 10:32
3

My recommendation is to use basename.
It is by default in Ubuntu, visually simple code and deal with majority of cases.

Here are some sub-cases to deal with spaces and multi-dot/sub-extension:

pathfile="../space fld/space -file.tar.gz"
echo ${pathfile//+(*\/|.*)}

It usually get rid of extension from first ., but fail in our .. path

echo **"$(basename "${pathfile%.*}")"**  
space -file.tar     # I believe we needed exatly that

Here is an important note:

I used double quotes inside double quotes to deal with spaces. Single quote will not pass due to texting the $. Bash is unusual and reads "second "first" quotes" due to expansion.

However, you still need to think of .hidden_files

hidden="~/.bashrc"
echo "$(basename "${hidden%.*}")"  # will produce "~" !!!  

not the expected "" outcome. To make it happen use $HOME or /home/user_path/
because again bash is "unusual" and don't expand "~" (search for bash BashPitfalls)

hidden2="$HOME/.bashrc" ;  echo '$(basename "${pathfile%.*}")'
2

Answers provided previously have problems with paths containing dots. Some examples:

/xyz.dir/file.ext
./file.ext
/a.b.c/x.ddd.txt

I prefer to use |sed -e 's/\.[^./]*$//'. For example:

$ echo "/xyz.dir/file.ext" | sed -e 's/\.[^./]*$//'
/xyz.dir/file
$ echo "./file.ext" | sed -e 's/\.[^./]*$//'
./file
$ echo "/a.b.c/x.ddd.txt" | sed -e 's/\.[^./]*$//'
/a.b.c/x.ddd

Note: If you want to remove multiple extensions (as in the last example), use |sed -e 's/\.[^/]*$//':

$ echo "/a.b.c/x.ddd.txt" | sed -e 's/\.[^/]*$//'
/a.b.c/x

However, this method will fail in "dot-files" with no extension:

$ echo "/a.b.c/.profile" | sed -e 's/\.[^./]*$//'
/a.b.c/

To cover also such cases, you can use:

$ echo "/a.b.c/.profile" | sed -re 's/(^.*[^/])\.[^./]*$/\1/'
/a.b.c/.profile
2
  • Thanks, the only answer that works with ./ihavenoextension!
    – virtualdj
    Commented Aug 13, 2021 at 13:15
  • I took a look at all answers and assuming you have sed available then this seems best, covers all edge cases. Here is a function rm_ext () { sed -re 's/(^.*[^/])\.[^./]*$/\1/' <<< "$1" }
    – DKebler
    Commented Mar 2 at 16:29
2

This one covers all possibilities! (dot in the path or not; with extension or no extension):

tmp1=${filename##*/};tmp2=${tmp1:1};filename_noextension=$(echo -n ${tmp1:0:1};echo ${tmp2%.*});echo $filename_noextension

Notes:

  • It gives you the filename without any extension. So there is no path in the $filename_noextension variable.
  • You end up with two unwanted variables $tmp1 and $tmp2. Make sure you are not using them in your script.

examples to test:

filename=.bashrc; echo "filename: $filename"; tmp1=${filename##*/};tmp2=${tmp1:1};filename_noextension=$(echo -n ${tmp1:0:1};echo ${tmp2%.*}); echo "filename without extension: $filename_noextension"

filename=.bashrc.txt; echo "filename: $filename"; tmp1=${filename##*/};tmp2=${tmp1:1};filename_noextension=$(echo -n ${tmp1:0:1};echo ${tmp2%.*}); echo "filename without extension: $filename_noextension"

filename=.bashrc.txt.tar; echo "filename: $filename"; tmp1=${filename##*/};tmp2=${tmp1:1};filename_noextension=$(echo -n ${tmp1:0:1};echo ${tmp2%.*}); echo "filename without extension: $filename_noextension"

filename=~/.bashrc; echo "filename: $filename"; tmp1=${filename##*/};tmp2=${tmp1:1};filename_noextension=$(echo -n ${tmp1:0:1};echo ${tmp2%.*}); echo "filename without extension: $filename_noextension"

filename=~/.bashrc.txt.tar; echo "filename: $filename"; tmp1=${filename##*/};tmp2=${tmp1:1};filename_noextension=$(echo -n ${tmp1:0:1};echo ${tmp2%.*}); echo "filename without extension: $filename_noextension"

filename=bashrc; echo "filename: $filename"; tmp1=${filename##*/};tmp2=${tmp1:1};filename_noextension=$(echo -n ${tmp1:0:1};echo ${tmp2%.*}); echo "filename without extension: $filename_noextension"

filename=bashrc.txt; echo "filename: $filename"; tmp1=${filename##*/};tmp2=${tmp1:1};filename_noextension=$(echo -n ${tmp1:0:1};echo ${tmp2%.*}); echo "filename without extension: $filename_noextension"

filename=bashrc.txt.tar; echo "filename: $filename"; tmp1=${filename##*/};tmp2=${tmp1:1};filename_noextension=$(echo -n ${tmp1:0:1};echo ${tmp2%.*}); echo "filename without extension: $filename_noextension"

filename=~/bashrc; echo "filename: $filename"; tmp1=${filename##*/};tmp2=${tmp1:1};filename_noextension=$(echo -n ${tmp1:0:1};echo ${tmp2%.*}); echo "filename without extension: $filename_noextension"

filename=~/bashrc.txt.tar; echo "filename: $filename"; tmp1=${filename##*/};tmp2=${tmp1:1};filename_noextension=$(echo -n ${tmp1:0:1};echo ${tmp2%.*}); echo "filename without extension: $filename_noextension"

2

Two problems with your code:

  1. You used a ' (tick) instead of a ` (back tick) to surround the commands that generate the string you want to store in the variable.
  2. You didn't "echo" the variable "$filename" to the pipe into the "cut" command.

I'd change your code to "name=`echo $filename | cut -f 1 -d '.' `", as shown below (again, notice the back ticks surrounding the name variable definition):

$> filename=foo.txt
$> echo $filename
foo.txt
$> name=`echo $filename | cut -f1 -d'.'`
$> echo $name
foo
$> 

EDIT: If I can add to the answer, I'd use the $( ... ) construct as opposed to using back ticks. I think it is a whole lot cleaner, and less prone to tick confusion.

1
  • 2
    This is the only answer that actually tries to answer the question What's wrong with the following code?
    – kvantour
    Commented Feb 7, 2023 at 12:42
1

As pointed out by Hawker65 in the comment of chepner answer, the most voted solution does neither take care of multiple extensions (such as filename.tar.gz), nor of dots in the rest of the path (such as this.path/with.dots/in.path.name). A possible solution is:

a=this.path/with.dots/in.path.name/filename.tar.gz
echo $(dirname $a)/$(basename $a | cut -d. -f1)
2
  • This one strips "tar.gz" by selecting characters before the first instance of a dot in the filename not counting the path. One probably doesn't want to strip extensions that way.
    – Frotz
    Commented Oct 24, 2018 at 8:30
  • If you know it ends in .tar.gz, you can just use $(basename "$a" .tar.gz). Also, make sure to wrap your variable in quotes everywhere if there's any chance it will contain spaces or other weird characters!
    – mwfearnley
    Commented Sep 30, 2020 at 10:26
0

After trying several solutions that didn't work for dot files, here is my Bash compatible unelegant solution.

# Given a file path, return only the file name, stripped of its extension.
basefilename()
{
    filename="$(basename "$1")"
    prefix=

    if [[ ${filename::1} == . ]]; then
        prefix=.
        filename="${filename:1}"
    fi

    echo -n "$prefix"
    echo "$filename" | rev | cut -f 2- -d '.' | rev
}

I tested it only on a few cases though. Let me know if you have some improvements.

Not the answer you're looking for? Browse other questions tagged or ask your own question.