How do I remove the file suffix and path portion from a path string in Bash?
Asked Answered
B

15

539

Given a string file path such as /foo/fizzbuzz.bar, how would I use bash to extract just the fizzbuzz portion of said string?

Belay answered 24/9, 2008 at 3:37 Comment(1)
Informations you can find in Bash manual, look for ${parameter%word} and ${parameter%%word} in trailing portion matching section.Neumark
A
776

Here's how to do it with the # and % operators in Bash.

$ x="/foo/fizzbuzz.bar"
$ y=${x%.bar}
$ echo ${y##*/}
fizzbuzz

${x%.bar} could also be ${x%.*} to remove everything after a dot or ${x%%.*} to remove everything after the first dot.

Example:

$ x="/foo/fizzbuzz.bar.quux"
$ y=${x%.*}
$ echo $y
/foo/fizzbuzz.bar
$ y=${x%%.*}
$ echo $y
/foo/fizzbuzz

Documentation can be found in the Bash manual. Look for ${parameter%word} and ${parameter%%word} trailing portion matching section.

Awry answered 24/9, 2008 at 3:54 Comment(12)
I ended up using this one because it was the most flexible and there were a couple other similar things I wanted to do as well that this did nicely.Belay
This is probably the most flexible of all the answers posted, but I think the answers suggesting the basename and dirname commands deserve some attention as well. They may be just the trick if you don't need any other fancy pattern matching.Halyard
What is this called ${x%.bar}? I would like to learn more about it.Toole
@Basil: Parameter Expansion. On a console type "man bash" and then type "/parameter expansion"Awry
I guess the 'man bash' explanation makes sense if you already know what it does or if you tried it out yourself the hard way. It's almost as bad as git reference. I'd just google it instead.Trophoblast
Is there a way to do this without using the x variable? If I try echo ${"/foo/fizzbuzz.bar.quux"%.*} I get -bash: ${"/foo/fizzbuzz.bar.quux"%.*}: bad substitutionRebut
@AlecJacobson No because without a variable it would be silly; you would just write it out as it is supposed to be in the first place.Awry
In a en.wikipedia.org/wiki/Metaprogramming context you might not be able to. Is the answer really "no"? Or did you assume it's no because you thought it'd be silly?Rebut
@AlecJacobson With metaprogramming it is even more silly because the generator program should process it first and write it out as the correct literal string. If it could not be a literal constant then it would be a variable.Awry
Sometimes it would be convenient to be able to pass in a string directly to a parameter expansion, without using a variable; but no, the shell does not suppert that.Hachmin
Slightly more convenient than @ZanLynx: On a console type "man bash" and then type /^ +parameter expansionOleate
Works on zsh as well (at least, on 5.8.1).Indigent
G
341

look at the basename command:

NAME="$(basename /foo/fizzbuzz.bar .bar)"

instructs it to remove the suffix .bar, results in NAME=fizzbuzz

Gefell answered 24/9, 2008 at 3:38 Comment(8)
Probably the simplest of all the currently offered solutions... although I'd use $(...) instead of backticks.Hadhramaut
Simplest but adds a dependency (not a huge or weird one, I admit). It also needs to know the suffix.Telencephalon
And can be used to remove anything from the end, basically it does just a string removal from end.Alyss
The problem is the time hit. I just searched the question for this discussion after watching bash take almost 5min to process 800 files, using basename. Using the above regex method, the time was reduced to about 7sec. Though this answer is easier to perform for the programmer, the time hit is just too much. Imagine a folder with a couple thousand files in it! I have some such folders.Progress
@Progress This is absolutely false. This is a simple program, which shouldn't take half a second to return. I just executed time find /home/me/dev -name "*.py" .py -exec basename {} \; and it stripped the extension and directory for 1500 files in 1 second total.Dramshop
The general idea to avoid an external process whenever you can is sound, though. and a basic tenet of shell programming.Hachmin
Perfect! Never new basename takes that 2nd argument.Jueta
@LaszloTreszkai while true on many system, the penalty for running external process in subshells on other systems may be significant. I'm specifically thinking about Msys2 on Windows, aka git-bash.Methodize
F
66

Pure bash, done in two separate operations:

  1. Remove the path from a path-string:

    path=/foo/bar/bim/baz/file.gif
    
    file=${path##*/}  
    #$file is now 'file.gif'
    
  2. Remove the extension from a path-string:

    base=${file%.*}
    #${base} is now 'file'.
    
Francie answered 6/5, 2014 at 14:20 Comment(0)
K
20

The basename and dirname functions are what you're after:

mystring=/foo/fizzbuzz.bar
echo basename: $(basename "${mystring}")
echo basename + remove .bar: $(basename "${mystring}" .bar)
echo dirname: $(dirname "${mystring}")

Has output:

basename: fizzbuzz.bar
basename + remove .bar: fizzbuzz
dirname: /foo
Kristykristyn answered 24/9, 2008 at 3:40 Comment(3)
It would be helpful to fix the quoting here -- maybe run this through shellcheck.net with mystring=$1 rather than the current constant value (which will suppress several warnings, being certain not to contain spaces/glob characters/etc), and address the issues it finds?Bacteriolysis
Well, I made some appropriate changes to support quotation marks in $mystring. Gosh this was a long time ago I wrote this :)Kristykristyn
Would be further improvement to quote the results: echo "basename: $(basename "$mystring")" -- that way if mystring='/foo/*' you don't get the * replaced with a list of files in the current directory after basename finishes.Bacteriolysis
V
20

Using basename I used the following to achieve this:

for file in *; do
    ext=${file##*.}
    fname=`basename $file $ext`

    # Do things with $fname
done;

This requires no a priori knowledge of the file extension and works even when you have a filename that has dots in it's filename (in front of it's extension); it does require the program basename though, but this is part of the GNU coreutils so it should ship with any distro.

Vive answered 11/4, 2014 at 17:7 Comment(4)
Excellent answer! removes the extension in a very clean way, but it doesn't remove the . at the end of the filename.Corvette
@Corvette just add the "." before $ext, ie: fname=`basename $file .$ext`Traveled
This could do bad things if there are spaces in the filenames. You'll should wrap $file, $ext, and the backticked section (including the backticks themselves) in double quotes.Senegambia
Downvote. This doesn't work right at all for me. It's inserting junk newlines into my filenames and not removing the extensions. Not even remotely usable.Unamuno
T
14

Pure bash way:

~$ x="/foo/bar/fizzbuzz.bar.quux.zoom"; 
~$ y=${x/\/*\//}; 
~$ echo ${y/.*/}; 
fizzbuzz

This functionality is explained on man bash under "Parameter Expansion". Non bash ways abound: awk, perl, sed and so on.

EDIT: Works with dots in file suffixes and doesn't need to know the suffix (extension), but doesn’t work with dots in the name itself.

Telencephalon answered 24/9, 2008 at 3:39 Comment(0)
N
7

Using basename assumes that you know what the file extension is, doesn't it?

And I believe that the various regular expression suggestions don't cope with a filename containing more than one "."

The following seems to cope with double dots. Oh, and filenames that contain a "/" themselves (just for kicks)

To paraphrase Pascal, "Sorry this script is so long. I didn't have time to make it shorter"


  #!/usr/bin/perl
  $fullname = $ARGV[0];
  ($path,$name) = $fullname =~ /^(.*[^\\]\/)*(.*)$/;
  ($basename,$extension) = $name =~ /^(.*)(\.[^.]*)$/;
  print $basename . "\n";
 
Northerly answered 24/9, 2008 at 5:7 Comment(1)
This is nice and robustGarden
O
6

In addition to the POSIX conformant syntax used in this answer,

basename string [suffix]

as in

basename /foo/fizzbuzz.bar .bar

GNU basename supports another syntax:

basename -s .bar /foo/fizzbuzz.bar

with the same result. The difference and advantage is that -s implies -a, which supports multiple arguments:

$ basename -s .bar /foo/fizzbuzz.bar /baz/foobar.bar
fizzbuzz
foobar

This can even be made filename-safe by separating the output with NUL bytes using the -z option, for example for these files containing blanks, newlines and glob characters (quoted by ls):

$ ls has*
'has'$'\n''newline.bar'  'has space.bar'  'has*.bar'

Reading into an array:

$ readarray -d $'\0' arr < <(basename -zs .bar has*)
$ declare -p arr
declare -a arr=([0]=$'has\nnewline' [1]="has space" [2]="has*")

readarray -d requires Bash 4.4 or newer. For older versions, we have to loop:

while IFS= read -r -d '' fname; do arr+=("$fname"); done < <(basename -zs .bar has*)
Overcome answered 24/10, 2018 at 22:1 Comment(1)
Also, the suffix specified is removed in the output if present (and ignored otherwise).Yovonnda
L
4

If you can't use basename as suggested in other posts, you can always use sed. Here is an (ugly) example. It isn't the greatest, but it works by extracting the wanted string and replacing the input with the wanted string.

echo '/foo/fizzbuzz.bar' | sed 's|.*\/\([^\.]*\)\(\..*\)$|\1|g'

Which will get you the output

fizzbuzz

Leverrier answered 24/9, 2008 at 11:12 Comment(1)
Although this is the answer to the original question, this command is useful when I have lines of paths in a file to extract base names to print them out to the screen.Fimbriation
Z
3
perl -pe 's/\..*$//;s{^.*/}{}'
Zasuwa answered 24/9, 2008 at 3:39 Comment(0)
J
2

Beware of the suggested perl solution: it removes anything after the first dot.

$ echo some.file.with.dots | perl -pe 's/\..*$//;s{^.*/}{}'
some

If you want to do it with perl, this works:

$ echo some.file.with.dots | perl -pe 's/(.*)\..*$/$1/;s{^.*/}{}'
some.file.with

But if you are using Bash, the solutions with y=${x%.*} (or basename "$x" .ext if you know the extension) are much simpler.

Joijoice answered 19/6, 2010 at 10:35 Comment(0)
D
2

If you want to keep just the filename with extension and strip the file path

$ x="myfile/hello/foo/fizzbuzz.bar"
$ echo ${x##*/}
$ fizzbuzz.bar

Explanation in Bash manual, see ${parameter##word}

Drug answered 5/12, 2022 at 18:10 Comment(0)
T
1

The basename does that, removes the path. It will also remove the suffix if given and if it matches the suffix of the file but you would need to know the suffix to give to the command. Otherwise you can use mv and figure out what the new name should be some other way.

Trachytic answered 15/11, 2008 at 0:47 Comment(0)
L
1

Combining the top-rated answer with the second-top-rated answer to get the filename without the full path:

$ x="/foo/fizzbuzz.bar.quux"
$ y=(`basename ${x%%.*}`)
$ echo $y
fizzbuzz
Lynxeyed answered 20/8, 2014 at 18:30 Comment(2)
Why are you using an array here? Also, why use basename at all?Substantialism
Also, broken quoting.Hachmin
T
-2

You can use

mv *<PATTERN>.jar "$(basename *<PATTERN>.jar <PATTERN>.jar).jar"

For e.g:- I wanted to remove -SNAPSHOT from my file name. For that used below command

 mv *-SNAPSHOT.jar "$(basename *-SNAPSHOT.jar -SNAPSHOT.jar).jar"
Throughway answered 19/7, 2021 at 8:35 Comment(1)
The wildcards here are horribly wrong unless you have only exactly one matching file.Hachmin

© 2022 - 2024 — McMap. All rights reserved.