ChatGPT解决这个技术问题 Extra ChatGPT

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

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

Informations you can find in Bash manual, look for ${parameter%word} and ${parameter%%word} in trailing portion matching section.

Z
Zan Lynx

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.


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.
r
rogerdpack

look at the basename command:

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

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


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.
A
Arseni Mourzenko

Pure bash, done in two separate operations:

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


m
mike

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.


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.
J
Jerub

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

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.
N
Nick Lockwood

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.


A
Andrew Edgecombe

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

This is nice and robust
B
Benjamin W.

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*)

Also, the suffix specified is removed in the output if present (and ignored otherwise).
m
mopoke
perl -pe 's/\..*$//;s{^.*/}{}'

n
nymacro

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


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.
m
mivk

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.


w
waynecolvin

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.


c
c.gutierrez

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

Why are you using an array here? Also, why use basename at all?
s
sblive

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"

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