25

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?

Sergio Losilla
  • 730
  • 1
  • 5
  • 14

6 Answers6

28

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
AsukaMinato
  • 1,017
  • 12
  • 21
Evan Benn
  • 1,571
  • 2
  • 14
  • 20
15

--- 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
mattmc3
  • 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
13

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
glenn jackman
  • 238,783
  • 38
  • 220
  • 352
11

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.

Claudio
  • 3,089
  • 2
  • 18
  • 22
  • 2
    You 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
4

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

Benjamin Buch
  • 4,752
  • 7
  • 28
  • 51
Jose M Vidal
  • 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
-1

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
Ninja Inc
  • 478
  • 1
  • 4
  • 10