Is there any convenient way to strip an arbitrary extension from a file name, something à la bash ${i%%.*}
? Do I stick to my friend sed
?

- 730
- 1
- 5
- 14
6 Answers
If you know the extension (eg _bak, a common usecase) this is possibly more convenient:
for f in (ls *_bak)
mv $f (basename $f _bak)
end

- 1,017
- 12
- 21

- 1,571
- 2
- 14
- 20
--- Update 2022-08-02 ---
As of fish 3.5+, there is a path
command (docs) which was designed to handle stripping extensions:
$ touch test.txt.bak
$ path change-extension '' ./test.txt.bak
test.txt
You can also strip a set number of extensions:
set --local file ./test.txt.1.2.3
for i in (seq 3)
set file (path change-extension '' $file)
end
echo $file
# ./test.txt
Or strip all extensions:
set --local file ./test.txt.1.2.3
while path extension $file
set file (path change-extension '' $file)
end
echo $file
# ./test
--- Original answer ---
The fish string
command is still the canonical way to handle this. It has some really nice sub commands that haven't been shown in other answers yet.
split
lets you split from the right with a max of 1, so that you just get the last extension.
for f in *
echo (string split -m1 -r '.' "$f")[1]
end
replace
lets you use a regex to lop off the extension, defined as the final dot to the end of the string
for f in *
string replace -r '\.[^\.]*$' '' "$f"
end
man string
for more info and some great examples.
Update:
If your system has proper basename
and dirname
utilities, you can use something like this:
function stripext \
--description "strip file extension"
for arg in $argv
echo (dirname $arg)/(string replace -r '\.[^\.]+$' '' (basename $arg))
end
end

- 17,595
- 7
- 83
- 103
-
Careful as this removes anything after the last dot on filename with no ext, ex: `/this/is.another/test` gives `/this/is` – Ninja Inc Nov 12 '21 at 22:58
-
1@NinjaInc - this is why I'm pretty excited to finally see the `path` utility make it's way into Fish: https://github.com/fish-shell/fish-shell/pull/8265 – mattmc3 Nov 14 '21 at 00:12
-
I am not here to say anything bad about fish, since I also use it as my default shell... but sometimes bash is much simpler.... `bash -c for a in *.txt; do mv $a ${a%.*}.json; done` – fsan Jun 29 '23 at 10:26
-
@fsan - bash may be more terse, but I don't agree that it's simpler. If you don't remember that `%.*` strips an extension, discovering that is a pain compared to fish's `help path`. And what if your files had spaces? Will bash's word splitting burn you because you didn't quote it? The fish version of your example isn't that much different, but is arguably better in nearly every respect `for f in *.txt; mv $f (path change-extension 'json' $f); end`. – mattmc3 Jun 29 '23 at 15:44
Nope. fish has a much smaller feature set than bash, relying on external commands:
$ set filename foo.bar.baz
$ set rootname (echo $filename | sed 's/\.[^.]*$//')
$ echo $rootname
foo.bar

- 238,783
- 38
- 220
- 352
-
Thanks, that's what I suspected. Good you posted the workaround :) – Sergio Losilla Jun 04 '15 at 14:09
-
As mentioned below you can achieve the same result without external commands by using fish' `string` command. – acorello Oct 03 '21 at 18:29
-
1Careful as the sed regex doesn't handle special cases, ex: `/this/is.another/test` gives `/this/is` – Ninja Inc Nov 12 '21 at 22:51
You can strip off the extension from a filename using the string command:
echo (string split -r -m1 . $filename)[1]
This will split filename
at the right-most dot and print the first element of the resulting list. If there is no dot, that list will contain a single element with filename
.
If you also need to strip off leading directories, combine it with basename:
echo (basename $filename | string split -r -m1 .)[1]
In this example, string
reads its input from stdin rather than being passed the filename as a command line argument.

- 3,089
- 2
- 18
- 22
-
2You can also use the option `--field` (`-f`) to select the first field instead of using `[1]`. Example: `string split --right --max 1 --field 1 . $filename`; Also I don't think you need `echo` because `string split` will print the output already. – acorello Oct 03 '21 at 18:28
-
Careful as this splits at the last dot of a filepath when a filename has no ext, ex: `/this/is.another/test` gives `/this/is` – Ninja Inc Nov 12 '21 at 22:55
With the string match
function built into fish you can do
set rootname (string match -r "(.*)\.[^\.]*\$" $filename)[2]
The string match returns a list of 2 items. The first is the whole string, and the second one is the first regexp match (the stuff inside the parentheses in the regex). So, we grab the second one with the [2].

- 4,752
- 7
- 28
- 51

- 8,816
- 6
- 43
- 48
-
Careful as this removes anything after the last dot on filename with no ext, ex: `/this/is.another/test` gives `/this/is` – Ninja Inc Nov 12 '21 at 22:56
I too need a function to split random files root and extension. Rather than re-implementing naively the feature at the risk of meeting caveats (ex: dot before separator), I am forwarding the task to Python's built-in POSIX path libraries and inherit from their expertise.
Here is an humble example of what one may prefer:
function splitext --description "Print filepath(s) root, stem or extension"
argparse 'e/ext' 's/stem' -- $argv
for arg in $argv
if set -q _flag_ext
set cmd 'import os' \
"_, ext = os.path.splitext('$arg')" \
'print(ext)'
else if set -q _flag_stem
set cmd 'from pathlib import Path' \
"p = Path('$arg')" \
'print(p.stem)'
else
set cmd 'import os' \
"root, _ = os.path.splitext('$arg')" \
'print(root)'
end
python3 -c (string join ';' $cmd)
end
end
Examples:
$ splitext /this/is.a/test.path
/this/is.a/test
$ splitext --ext /this/is.a/test.path
.path
$ splitext --stem /this/is.a/test.path
test
$ splitext /this/is.another/test
/this/is.another/test

- 478
- 1
- 4
- 10