561

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

1
  • Informations you can find in Bash manual, look for ${parameter%word} and ${parameter%%word} in trailing portion matching section. Commented Sep 20, 2016 at 11:44

15 Answers 15

800

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 the last 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.

Sign up to request clarification or add additional context in comments.

12 Comments

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.
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.
What is this called ${x%.bar}? I would like to learn more about it.
@Basil: Parameter Expansion. On a console type "man bash" and then type "/parameter expansion"
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.
|
360

look at the basename command:

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

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

8 Comments

Probably the simplest of all the currently offered solutions... although I'd use $(...) instead of backticks.
Simplest but adds a dependency (not a huge or weird one, I admit). It also needs to know the suffix.
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.
@xizdaqrian 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.
The general idea to avoid an external process whenever you can is sound, though. and a basic tenet of shell programming.
|
71

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

Comments

24

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.

4 Comments

Excellent answer! removes the extension in a very clean way, but it doesn't remove the . at the end of the filename.
@metrix just add the "." before $ext, ie: fname=`basename $file .$ext`
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.
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.
23

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

3 Comments

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?
Well, I made some appropriate changes to support quotation marks in $mystring. Gosh this was a long time ago I wrote this :)
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.
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.

Comments

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";
 

1 Comment

This is nice and robust
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*)

1 Comment

Also, the suffix specified is removed in the output if present (and ignored otherwise).
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

1 Comment

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.
3
perl -pe 's/\..*$//;s{^.*/}{}'

Comments

3

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}

Comments

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.

Comments

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.

Comments

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

2 Comments

Why are you using an array here? Also, why use basename at all?
-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"

1 Comment

The wildcards here are horribly wrong unless you have only exactly one matching file.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.