在 Bash 中提取文件名和扩展名

Extract filename and extension in Bash

提问人:ibz 提问时间:6/8/2009 最后编辑:Benjamin W.ibz 更新时间:5/22/2023 访问量:2215736

问:

我想分别获取文件名(不带扩展名)和扩展名。

到目前为止,我发现的最佳解决方案是:

NAME=`echo "$FILE" | cut -d'.' -f1`
EXTENSION=`echo "$FILE" | cut -d'.' -f2`

这是错误的,因为如果文件名包含多个字符,则它不起作用。如果,比方说,我有 ,它将考虑 和 ,而不是 和 。.a.b.jsab.jsa.bjs

它可以在 Python 中轻松完成

file, ext = os.path.splitext(path)

但如果可能的话,我宁愿不要为此启动 Python 解释器。

有什么更好的主意吗?

bash 字符串 文件名

评论

41赞 Krista K 8/8/2013
在应用下面的伟大答案时,不要简单地粘贴你的变量,就像我在这里显示的那样 错误:就像我做了一段时间一样!将卷曲向外移动:右:extension="{$filename##*.}"$ extension="${filename##*.}"
4赞 Peter Gibson 10/1/2015
这显然是一个不平凡的问题,对我来说,很难说下面的答案是否完全正确。令人惊讶的是,这不是 (ba)sh 中的内置操作(答案似乎使用模式匹配实现了该功能)。我决定使用上面的 Python......os.path.splitext
1赞 F. Hauri - Give Up GitHub 10/14/2016
由于扩展名必须代表文件的性质,因此有一个神奇的命令可以检查文件以神圣化他的性质并提供标准扩展名看看我的答案
6赞 C. M. 10/10/2018
这个问题首先是有问题的,因为......从操作系统和 unix 文件系统的角度来看,没有文件扩展名这样的东西。使用“.”来分隔部分是人类的惯例,只有在人类同意遵守的情况下才有效。例如,使用“tar”程序,可以决定使用“tar.”前缀而不是“.tar”后缀来命名输出文件 - 给出“tar.somedir”而不是“somedir.tar”。因此,没有“通用的、始终有效”的解决方案 - 您必须编写与您的特定需求和预期文件名相匹配的代码。
1赞 paxdiablo 3/30/2022
该文件的扩展名是什么?或?换句话说,你是把扩展看作是一个简单的技术问题还是一个语义问题?xyzzy.tar.gzplugh.cfg.saved

答:

1117赞 Juliano 6/8/2009 #1
~% FILE="example.tar.gz"

~% echo "${FILE%%.*}"
example

~% echo "${FILE%.*}"
example.tar

~% echo "${FILE#*.}"
tar.gz

~% echo "${FILE##*.}"
gz

更多细节,请参见 Bash 手册中的 shell 参数扩展

评论

37赞 rmeador 6/8/2009
您(也许是无意中)提出了一个很好的问题,即如果文件名的“扩展”部分中有 2 个点,就像在 .tar.gz 中一样,该怎么办......我从未考虑过这个问题,我怀疑如果不事先知道所有可能的有效文件扩展名,就无法解决。
10赞 Juliano 6/8/2009
为什么无法解决?在我的示例中,应该认为文件包含两个扩展名,而不是带有两个点的扩展名。您可以分别处理这两个扩展。
30赞 porges 6/13/2009
这是无法在词法基础上解决的,您需要检查文件类型。考虑一下,如果你有一个游戏叫,你把它压缩到:)dinosaurs.in.tardinosaurs.in.tar.gz
13赞 Walt Sellers 3/6/2012
如果您要通过完整路径,这将变得更加复杂。我的一个在路径中间的目录中有一个“.”,但文件名中没有。示例“a/b.c/d/e/filename”最终会变成“.c/d/e/filename”
16赞 v.oddou 11/26/2013
显然没有 的扩展名是,文件名就是这样。没有双重扩展这样的东西。我很确定 boost::filesystem 会以这种方式处理它。(拆分路径,change_extension...如果我没记错的话,它的行为是基于 python 的。x.tar.gzgzx.tar
4377赞 Anya Shenanigans 6/8/2009 #2

首先,获取不带路径的文件名:

filename=$(basename -- "$fullfile")
extension="${filename##*.}"
filename="${filename%.*}"

或者,您可以专注于路径的最后一个“/”而不是“.”,即使您的文件扩展名不可预测,这也应该有效:

filename="${fullfile##*/}"

您可能需要查看文档:

评论

93赞 D.Shawley 6/8/2009
请查看 gnu.org/software/bash/manual/html_node/...,了解完整的功能集。
27赞 lhunath 6/8/2009
在“$fullfile”中添加一些引号,否则可能会破坏文件名。
53赞 ephemient 6/10/2009
哎呀,你甚至可以写 filename=“${fullfile##*/}” 并避免调用额外的basename
54赞 nccc 7/1/2012
如果文件没有扩展名,则此“解决方案”不起作用 - 相反,输出整个文件名,考虑到没有扩展名的文件无处不在,这是非常糟糕的。
48赞 mklement0 9/7/2012
修复了处理不带扩展名的文件名的问题:.请注意,如果存在扩展名,则将返回该扩展名,包括首字母 ,例如 .extension=$([[ "$filename" = *.* ]] && echo ".${filename##*.}" || echo '')..txt
80赞 paxdiablo 6/8/2009 #3
pax> echo a.b.js | sed 's/\.[^.]*$//'
a.b
pax> echo a.b.js | sed 's/^.*\.//'
js

工作正常,因此您可以使用:

pax> FILE=a.b.js
pax> NAME=$(echo "$FILE" | sed 's/\.[^.]*$//')
pax> EXTENSION=$(echo "$FILE" | sed 's/^.*\.//')
pax> echo $NAME
a.b
pax> echo $EXTENSION
js

顺便说一句,这些命令的工作原理如下。

命令替换一个字符,后跟任意数量的非字符,直到行尾,没有任何内容(即,它删除了从行尾到行尾的所有内容,包括)。这基本上是使用正则表达式诡计的非贪婪替换。NAME"."".""."

命令 for 替换任意数量的字符,后跟行首的字符,不带任何内容(即,它删除从行首到最后一个点的所有内容,包括)。这是一个贪婪的替换,这是默认操作。EXTENSION"."

评论

0赞 hIpPy 10/7/2018
对于没有扩展名的文件,此中断,因为它会打印相同的名称和扩展名。因此,我使用 name 和 extension(使用 atypical 和 commands,以及 typical 命令)。sed 's,\.[^\.]*$,,'sed 's,.*\.,., ;t ;g'testgetsubstitute
0赞 JCCyC 12/8/2020
您可以在计算 NAME 后测试它和 FILE 是否相等,如果相等,请将 EXTENSION 设置为空字符串。
0赞 tripleee 8/19/2021
从根本上说,将外部进程用于 shell 本身可以做的事情是一种反模式。
0赞 paxdiablo 4/29/2022
Tripleee:shell 可以在一百行中完成很多事情,而像外部进程这样的外部进程可以在五行中完成 :-)awk
0赞 jubilatious1 5/9/2023
这是否处理多部分扩展,例如?.tar.gz
91赞 Doctor J 9/10/2009 #4

如果文件没有扩展名或没有文件名,这似乎不起作用。这是我正在使用的;它只使用内置函数并处理更多(但不是全部)病理文件名。

#!/bin/bash
for fullpath in "$@"
do
    filename="${fullpath##*/}"                      # Strip longest match of */ from start
    dir="${fullpath:0:${#fullpath} - ${#filename}}" # Substring from 0 thru pos of filename
    base="${filename%.[^.]*}"                       # Strip shortest match of . plus at least one non-dot char from end
    ext="${filename:${#base} + 1}"                  # Substring from len of base thru end
    if [[ -z "$base" && -n "$ext" ]]; then          # If we have an extension and no base, it's really the base
        base=".$ext"
        ext=""
    fi

    echo -e "$fullpath:\n\tdir  = \"$dir\"\n\tbase = \"$base\"\n\text  = \"$ext\""
done

以下是一些测试用例:

$ basename-and-extension.sh / /home/me/ /home/me/file /home/me/file.tar /home/me/file.tar.gz /home/me/.hidden /home/me/.hidden.tar /home/me/.. .
/:
    dir  = "/"
    base = ""
    ext  = ""
/home/me/:
    dir  = "/home/me/"
    base = ""
    ext  = ""
/home/me/file:
    dir  = "/home/me/"
    base = "file"
    ext  = ""
/home/me/file.tar:
    dir  = "/home/me/"
    base = "file"
    ext  = "tar"
/home/me/file.tar.gz:
    dir  = "/home/me/"
    base = "file.tar"
    ext  = "gz"
/home/me/.hidden:
    dir  = "/home/me/"
    base = ".hidden"
    ext  = ""
/home/me/.hidden.tar:
    dir  = "/home/me/"
    base = ".hidden"
    ext  = "tar"
/home/me/..:
    dir  = "/home/me/"
    base = ".."
    ext  = ""
.:
    dir  = ""
    base = "."
    ext  = ""

评论

3赞 dubiousjim 5/31/2012
而不是我经常看到的.写起来更简单。不确定是否有任何真正的速度差异或陷阱。dir="${fullpath:0:${#fullpath} - ${#filename}}"dir="${fullpath%$filename}"
2赞 Good Person 5/26/2013
这使用了 #!/bin/bash,这几乎总是错误的。如果可能,首选 #!/bin/sh,如果没有,则首选 #!/usr/bin/env bash。
3赞 Good Person 7/13/2013
@vol7ron - 在许多发行版上,bash 位于 /usr/local/bin/bash 中。在 OSX 上,许多人在 /opt/local/bin/bash 中安装了更新的 bash。因此,/bin/bash 是错误的,应该使用 env 来找到它。更好的是使用 /bin/sh 和 POSIX 结构。除了在solaris上,这是一个POSIX shell。
2赞 vol7ron 7/13/2013
@GoodPerson但是,如果你对 bash 更满意,为什么要使用 sh?这难道不是说,当你可以使用sh时,为什么要使用Perl?
1赞 Good Person 7/13/2013
@vol7ron,如果您使用 bash 知道它不会移植 - 当然为什么不呢。就像使用 python/ruby/etc 一样。只是碰巧 bash 不是大量系统的默认值,因此它不是一个可移植的结构。就我个人而言,我将 zsh 用于只有我使用的脚本,将 POSIX sh 用于我共享的脚本。
9赞 enyo 7/12/2010 #5

简单易用${parameter%word}

就您而言:

${FILE%.*}

如果你想测试它,请执行以下所有工作,只需删除扩展:

FILE=abc.xyz; echo ${FILE%.*};
FILE=123.abc.xyz; echo ${FILE%.*};
FILE=abc; echo ${FILE%.*};

评论

2赞 SilverWolf 5/14/2018
为什么投反对票?它仍然有用,尽管标志周围不应该有空格。=
1赞 Alex. S. 12/19/2018
这工作正常。谢谢!(现在它没有等号周围的空格,如果这就是它被否决的原因)
0赞 Zrin 3/4/2022
不适用于 dotfiles 并需要引号。
44赞 Kebabbert 7/21/2010 #6

Mellen 在一篇博文的评论中写道:

使用 Bash,还可以获取不带扩展名的文件名,并单独获取扩展名。那是${file%.*}${file##*.}

file="thisfile.txt"
echo "filename: ${file%.*}"
echo "extension: ${file##*.}"

输出:

filename: thisfile
extension: txt

评论

2赞 mklement0 12/9/2014
@REACHUS:看 gnu.org/software/bash/manual/html_node/...
14赞 miriam 4/10/2011 #7
$ F="text file.test.txt"  
$ echo ${F/*./}  
txt  

这迎合了文件名中的多个点和空格,但是如果没有扩展名,它会返回文件名本身。虽然很容易检查;只需测试文件名和扩展名是否相同即可。

当然,此方法不适用于 .tar.gz 文件。但是,这可以通过两步过程处理。如果扩展名是 gz,则再次检查是否还有 tar 扩展名。

评论

0赞 Barbaros Özhan 1/24/2021
非常干净和直截了当的答案,非常感谢。
-1赞 SCS 7/1/2011 #8

您还可以使用循环并从路径中提取文件名...fortr

for x in `echo $path | tr "/" " "`; do filename=$x; done

将 path 中的所有“/”分隔符替换为空格,从而形成字符串列表,循环扫描它们,将最后一个留在变量中。trforfilename

评论

3赞 dubiousjim 5/31/2012
如果您打算这样做,请为自己节省一些分叉,而是使用: .需要 subshell 才能将分配本地化到 IFS。(IFS=/ ; for x in $path; do filename=$x; done)(...)
18赞 Andrew Woolfgang 9/29/2011 #9

我认为如果你只需要文件名,你可以试试这个:

FULLPATH=/usr/share/X11/xorg.conf.d/50-synaptics.conf

# Remove all the prefix until the "/" character
FILENAME=${FULLPATH##*/}

# Remove all the prefix until the "." character
FILEEXTENSION=${FILENAME##*.}

# Remove a suffix, in our case, the filename. This will return the name of the directory that contains this file.
BASEDIRECTORY=${FULLPATH%$FILENAME}

echo "path = $FULLPATH"
echo "file name = $FILENAME"
echo "file extension = $FILEEXTENSION"
echo "base directory = $BASEDIRECTORY"

这就是全部=D。

评论

0赞 Carlos Ricardo 12/10/2012
只是想要 BASEDIRECTORY :)谢谢!
620赞 Tomi Po 10/19/2011 #10

通常您已经知道扩展,因此您可能希望使用:

basename filename .extension

例如:

basename /path/to/dir/filename.txt .txt

我们得到

filename

评论

96赞 akaIDIOT 1/23/2013
第二个论点令人大开眼界,先生/女士:)basename
14赞 Tomasz Gandor 2/13/2014
以及如何使用这种技术提取扩展名?;)哦,等等!我们实际上并不知道。
5赞 Dennis 3/31/2014
假设您有一个以 或 结尾的压缩目录。有没有办法做这样的事情?.zip.ZIPbasename $file {.zip,.ZIP}
11赞 sudo make install 12/1/2017
虽然这只回答了 OP 问题的一部分,但它确实回答了我在谷歌中输入的问题。:-)非常光滑!
1赞 gpanda 10/12/2018
简单且符合POSIX标准
6赞 smilyface 11/21/2011 #11

下面是使用 AWK 的代码。它可以更简单地完成。但我不擅长 AWK。

filename$ ls
abc.a.txt  a.b.c.txt  pp-kk.txt
filename$ find . -type f | awk -F/ '{print $2}' | rev | awk -F"." '{$1="";print}' | rev | awk 'gsub(" ",".") ,sub(".$", "")'
abc.a
a.b.c
pp-kk
filename$ find . -type f | awk -F/ '{print $2}' | awk -F"." '{print $NF}'
txt
txt
txt

评论

0赞 BHSPitMonkey 4/6/2013
在最后一个示例中,您不应该需要第一个 awk 语句,对吧?
0赞 tripleee 11/10/2017
您可以通过执行另一个 ./' 作为顶级分隔符,但随后拆分第二个字段并打印新数组中的最后一个元素。split()awk -F / '{ n=split($2, a, "."); print a[n] }' uses .
10赞 Al3xXx 12/10/2011 #12

好的,如果我理解正确的话,这里的问题是如何获取具有多个扩展名的文件的名称和完整扩展名,例如.stuff.tar.gz

这对我有用:

fullfile="stuff.tar.gz"
fileExt=${fullfile#*.}
fileName=${fullfile%*.$fileExt}

这将为您提供文件名和扩展名。它适用于任意数量的扩展,包括 0。希望这对任何有相同问题的人有所帮助=)stuff.tar.gz

评论

0赞 Cyker 12/2/2016
正确的结果(根据 ,这是 OP 想要的)是 。os.path.splitext('stuff.tar', '.gz')
1赞 chown 5/20/2012 #13

使用示例文件,此代码:/Users/Jonathan/Scripts/bash/MyScript.sh

MY_EXT=".${0##*.}"
ME=$(/usr/bin/basename "${0}" "${MY_EXT}")

将导致存在和存在:${ME}MyScript${MY_EXT}.sh


脚本:

#!/bin/bash
set -e

MY_EXT=".${0##*.}"
ME=$(/usr/bin/basename "${0}" "${MY_EXT}")

echo "${ME} - ${MY_EXT}"

一些测试:

$ ./MyScript.sh 
MyScript - .sh

$ bash MyScript.sh
MyScript - .sh

$ /Users/Jonathan/Scripts/bash/MyScript.sh
MyScript - .sh

$ bash /Users/Jonathan/Scripts/bash/MyScript.sh
MyScript - .sh

评论

2赞 mklement0 12/10/2014
不知道为什么这有这么多的反对票 - 它实际上比公认的答案更有效。(与后者一样,它也中断了没有扩展名的输入文件名)。使用明确的路径可能是矫枉过正。basename
14赞 maciek gajewski 9/10/2012 #14

您可以强制剪切以显示所有字段,以及添加到字段编号的后续字段。-

NAME=`basename "$FILE"`
EXTENSION=`echo "$NAME" | cut -d'.' -f2-`

因此,如果 FILE 是 ,则 EXTENSION 将是eth0.pcap.gzpcap.gz

使用相同的逻辑,您还可以使用带有剪切的“-”获取文件名,如下所示:

NAME=`basename "$FILE" | cut -d'.' -f-1`

这甚至适用于没有任何扩展名的文件名。

50赞 Bjarke Freund-Hansen 2/5/2013 #15

您可以使用 basename

例:

$ basename foo-bar.tar.gz .tar.gz
foo-bar

您确实需要为 basename 提供要删除的扩展名,但是如果您总是执行 ,那么您就知道扩展将是 .tar-z.tar.gz

这应该可以做你想要的:

tar -zxvf $1
cd $(basename $1 .tar.gz)

评论

2赞 SS Hegde 2/5/2013
我想适用于.gz文件。但在问题中,他提到cd $(basename $1 .tar.gz)Archive files have several extensions: tar.gz, tat.xz, tar.bz2
0赞 phil294 9/16/2017
Tomi Po 在 2 年前发布了同样的东西。
0赞 Bjarke Freund-Hansen 9/17/2017
嗨,Blauhirn,这是一个老问题。我认为日期发生了一些事情。我清楚地记得在被问到这个问题后不久就回答了这个问题,那里只有几个其他答案。会不会是这个问题与另一个问题合并了,所以这样做了吗?
0赞 Bjarke Freund-Hansen 9/17/2017
是的,我记得没错。我最初回答这个问题 stackoverflow.com/questions/14703318/......在被问到的同一天,2年后它被合并到这个问题中。当我的答案以这种方式移动时,我很难因为重复的答案而受到责备。
0赞 Édouard Lopez 2/5/2013 #16

也许有一个选项可以做到这一点;你检查过那个人吗?否则,可以使用 Bash 字符串扩展tar

test="mpc-1.0.1.tar.gz"
noExt="${test/.tar.gz/}" # Remove the string '.tar.gz'
echo $noExt

评论

0赞 Brent 2/15/2014
cd $(tar tf $1 | sed -n 1p)
27赞 Some programmer dude 2/5/2013 #17

您可以使用 cut 命令删除最后两个扩展名(部分):".tar.gz"

$ echo "foo.tar.gz" | cut -d'.' --complement -f2-
foo

正如克莱顿·休斯(Clayton Hughes)在评论中指出的那样,这不适用于问题中的实际示例。因此,作为替代方案,我建议使用扩展正则表达式,如下所示:sed

$ echo "mpc-1.0.1.tar.gz" | sed -r 's/\.[[:alnum:]]+\.[[:alnum:]]+$//'
mpc-1.0.1

它的工作原理是无条件地删除最后两个(字母数字)扩展名。

[在安德斯·林达尔(Anders Lindahl)发表评论后再次更新]

评论

4赞 Clayton Hughes 12/4/2013
这仅适用于文件名/路径不包含任何其他点的情况:echo “mpc-1.0.1.tar.gz” |cut -d'.' --complement -f2- 生成 “mpc-1”(仅用 。
0赞 Some programmer dude 12/4/2013
@ClaytonHughes 你是对的,我应该测试得更好。添加了另一个解决方案。
0赞 Anders Lindahl 12/4/2013
sed 表达式应用于检查匹配的扩展名是否位于文件名的末尾。否则,像这样的文件名可能会产生意外的结果。$i.like.tar.gz.files.tar.bz2
0赞 Some programmer dude 12/4/2013
@AndersLindahl 如果扩展的顺序与链顺序相反,它仍然会。即使在末尾有一个文件名,例如 将删除两者,然后删除 .sed$mpc-1.0.1.tar.bz2.tar.gz.tar.gz.tar.bz2
0赞 Gene Black 3/27/2020
$ echo “foo.tar.gz” |剪切 -d'.' -f2- 没有 --complement 将得到字符串末尾的第二个拆分项 $ echo “foo.tar.gz” |切 -d'.' -f2- tar.gz
212赞 sotapme 2/5/2013 #18

您可以使用POSIX参数扩展的魔力:

bash-3.2$ FILENAME=somefile.tar.gz
bash-3.2$ echo "${FILENAME%%.*}"
somefile
bash-3.2$ echo "${FILENAME%.*}"
somefile.tar

有一点需要注意的是,如果你的文件名是这样的,那么会贪婪地删除与 的最长匹配项,你就会得到空字符串。./somefile.tar.gzecho ${FILENAME%%.*}.

(您可以使用临时变量来解决此问题:

FULL_FILENAME=$FILENAME
FILENAME=${FULL_FILENAME##*/}
echo ${FILENAME%%.*}

)


这个网站解释了更多。

${variable%pattern}
  Trim the shortest match from the end
${variable##pattern}
  Trim the longest match from the beginning
${variable%%pattern}
  Trim the longest match from the end
${variable#pattern}
  Trim the shortest match from the beginning

评论

5赞 jwadsack 7/19/2014
比 Joachim 的答案简单得多,但我总是需要查找 POSIX 变量替换。此外,这在 Max OSX 上运行,那里没有也没有 .cut--complementsed-r
0赞 Mache 5/25/2023
适用于 Windows 的 git-bash - GNU bash,版本 5.2.15(1)-release (x86_64-pc-msys) 也适用于
11赞 F. Hauri - Give Up GitHub 7/7/2013 #19

魔术文件识别

除了关于这个 Stack Overflow 问题的许多好答案之外,我还想补充:

在 Linux 和其他 unixen 下,有一个名为 的魔术命令,它通过分析文件的一些第一个字节来执行文件类型检测。这是一个非常古老的工具,最初用于打印服务器(如果不是为...我不确定)。file

file myfile.txt
myfile.txt: UTF-8 Unicode text

file -b --mime-type myfile.txt
text/plain

标准扩展可以在(在我的 Debian GNU/Linux 桌面上找到。请参见 和 。也许您必须安装实用程序和软件包):/etc/mime.typesman fileman mime.typesfilemime-support

grep $( file -b --mime-type myfile.txt ) </etc/mime.types
text/plain      asc txt text pot brf srt

您可以创建一个 函数来确定正确的扩展名。 有一个小(不完美)的例子:

file2ext() {
    local _mimetype=$(file -Lb --mime-type "$1") _line _basemimetype
    case ${_mimetype##*[/.-]} in
        gzip | bzip2 | xz | z )
            _mimetype=${_mimetype##*[/.-]}
            _mimetype=${_mimetype//ip}
            _basemimetype=$(file -zLb --mime-type "$1")
            ;;
        stream )
            _mimetype=($(file -Lb "$1"))
            [ "${_mimetype[1]}" = "compressed" ] &&
                _basemimetype=$(file -b --mime-type - < <(
                        ${_mimetype,,} -d <"$1")) ||
                _basemimetype=${_mimetype,,}
            _mimetype=${_mimetype,,}
            ;;
        executable )  _mimetype='' _basemimetype='' ;;
        dosexec )     _mimetype='' _basemimetype='exe' ;;
        shellscript ) _mimetype='' _basemimetype='sh' ;;
        * )
            _basemimetype=$_mimetype
            _mimetype=''
            ;;
    esac
    while read -a _line ;do
        if [ "$_line" == "$_basemimetype" ] ;then
            [ "$_line[1]" ] &&
                _basemimetype=${_line[1]} ||
                _basemimetype=${_basemimetype##*[/.-]}
            break
        fi
        done </etc/mime.types
    case ${_basemimetype##*[/.-]} in
        executable ) _basemimetype='' ;;
        shellscript ) _basemimetype='sh' ;;
        dosexec ) _basemimetype='exe' ;;
        * ) ;;
    esac
    [ "$_mimetype" ] && [ "$_basemimetype" != "$_mimetype" ] &&
      printf ${2+-v} $2 "%s.%s" ${_basemimetype##*[/.-]} ${_mimetype##*[/.-]} ||
      printf ${2+-v} $2 "%s" ${_basemimetype##*[/.-]}
}

此函数可以设置以后可以使用的 Bash 变量:

(这是从@Petesh正确答案中获得的):

filename=$(basename "$fullfile")
filename="${filename%.*}"
file2ext "$fullfile" extension

echo "$fullfile -> $filename . $extension"

评论

2赞 mp035 1/6/2021
虽然不是对原始帖子的直接回答,但这是迄今为止最明智的回应。感谢您提供它。
0赞 Jason R Stevens CFA 4/10/2022
我真的很欣赏这个详尽的答案,突出了常见的内置功能。虽然我最终只是在 python 中使用标志执行此操作,但如果我仅限于使用 shell 脚本,我会使用此处概述的概念。谢谢!-c
0赞 F. Hauri - Give Up GitHub 4/10/2022
@JasonRStevensCFA在 Python 下,您将使用 python-magic 库!
0赞 Jason R Stevens CFA 4/11/2022
@F.Hauri Cool lib,感谢分享。我只是使用标准的东西,因为用于脚本的字符串内置函数非常简单。例如,将使用subshell从路径字符串变量中获取带有扩展名的文件名(我在一些本地脚本中这样使用它)。我没有在生产中使用这种“魔法”,但 Python 语言的这些特性对于基于任务的简单事情来说非常棒。$(python -c "'$1'.split('/')[-1]")$1
0赞 F. Hauri - Give Up GitHub 4/12/2022
@JasonRStevensCFA 像任何其他语言(perl、awk 等)一样,对 python 使用分叉来满足如此微小的需求会适得其反!尝试运行相同的 fork 1000 次并与参数扩展进行比较......
0赞 Bill Gale 8/9/2013 #20

为了使 dir 更有用(在指定没有路径的本地文件作为输入的情况下),我做了以下操作:

# Substring from 0 thru pos of filename
dir="${fullpath:0:${#fullpath} - ${#filename}}"
if [[ -z "$dir" ]]; then
    dir="./"
fi

这允许您做一些有用的事情,例如在输入文件 basename 中添加后缀,如下所示:

outfile=${dir}${base}_suffix.${ext}

testcase: foo.bar
dir: "./"
base: "foo"
ext: "bar"
outfile: "./foo_suffix.bar"

testcase: /home/me/foo.bar
dir: "/home/me/"
base: "foo"
ext: "bar"
outfile: "/home/me/foo_suffix.bar"
29赞 mklement0 8/9/2013 #21

公认的答案典型情况下效果很好但在边缘情况下会失败,即:

  • 对于不带扩展名的文件名(在此答案的其余部分称为后缀),返回输入文件名而不是空字符串。extension=${filename##*.}
  • extension=${filename##*.}不包括首字母 ,与惯例相反。.
    • 盲目预置对于没有后缀的文件名不起作用。.
  • filename="${filename%.*}"如果输入文件名以 开头且不包含其他字符(例如,),则为空字符串 - 与约定相反。...bash_profile

---------

因此,涵盖所有边缘情况的鲁棒解决方案的复杂性需要一个函数 - 请参阅下面的定义;它可以返回路径的所有组件

调用示例:

splitPath '/etc/bash.bashrc' dir fname fnameroot suffix
# -> $dir == '/etc'
# -> $fname == 'bash.bashrc'
# -> $fnameroot == 'bash'
# -> $suffix == '.bashrc'

请注意,输入路径后面的参数是自由选择的位置变量名称
要跳过不感兴趣的变量,请指定(使用抛弃变量)或 ;例如,要仅提取文件名根目录和扩展名,请使用 .
_$_''splitPath '/etc/bash.bashrc' _ _ fnameroot extension


# SYNOPSIS
#   splitPath path varDirname [varBasename [varBasenameRoot [varSuffix]]] 
# DESCRIPTION
#   Splits the specified input path into its components and returns them by assigning
#   them to variables with the specified *names*.
#   Specify '' or throw-away variable _ to skip earlier variables, if necessary.
#   The filename suffix, if any, always starts with '.' - only the *last*
#   '.'-prefixed token is reported as the suffix.
#   As with `dirname`, varDirname will report '.' (current dir) for input paths
#   that are mere filenames, and '/' for the root dir.
#   As with `dirname` and `basename`, a trailing '/' in the input path is ignored.
#   A '.' as the very first char. of a filename is NOT considered the beginning
#   of a filename suffix.
# EXAMPLE
#   splitPath '/home/jdoe/readme.txt' parentpath fname fnameroot suffix
#   echo "$parentpath" # -> '/home/jdoe'
#   echo "$fname" # -> 'readme.txt'
#   echo "$fnameroot" # -> 'readme'
#   echo "$suffix" # -> '.txt'
#   ---
#   splitPath '/home/jdoe/readme.txt' _ _ fnameroot
#   echo "$fnameroot" # -> 'readme'  
splitPath() {
  local _sp_dirname= _sp_basename= _sp_basename_root= _sp_suffix=
    # simple argument validation
  (( $# >= 2 )) || { echo "$FUNCNAME: ERROR: Specify an input path and at least 1 output variable name." >&2; exit 2; }
    # extract dirname (parent path) and basename (filename)
  _sp_dirname=$(dirname "$1")
  _sp_basename=$(basename "$1")
    # determine suffix, if any
  _sp_suffix=$([[ $_sp_basename = *.* ]] && printf %s ".${_sp_basename##*.}" || printf '')
    # determine basename root (filemane w/o suffix)
  if [[ "$_sp_basename" == "$_sp_suffix" ]]; then # does filename start with '.'?
      _sp_basename_root=$_sp_basename
      _sp_suffix=''
  else # strip suffix from filename
    _sp_basename_root=${_sp_basename%$_sp_suffix}
  fi
  # assign to output vars.
  [[ -n $2 ]] && printf -v "$2" "$_sp_dirname"
  [[ -n $3 ]] && printf -v "$3" "$_sp_basename"
  [[ -n $4 ]] && printf -v "$4" "$_sp_basename_root"
  [[ -n $5 ]] && printf -v "$5" "$_sp_suffix"
  return 0
}

test_paths=(
  '/etc/bash.bashrc'
  '/usr/bin/grep'
  '/Users/jdoe/.bash_profile'
  '/Library/Application Support/'
  'readme.new.txt'
)

for p in "${test_paths[@]}"; do
  echo ----- "$p"
  parentpath= fname= fnameroot= suffix=
  splitPath "$p" parentpath fname fnameroot suffix
  for n in parentpath fname fnameroot suffix; do
    echo "$n=${!n}"
  done
done

测试执行该函数的代码:

test_paths=(
  '/etc/bash.bashrc'
  '/usr/bin/grep'
  '/Users/jdoe/.bash_profile'
  '/Library/Application Support/'
  'readme.new.txt'
)

for p in "${test_paths[@]}"; do
  echo ----- "$p"
  parentpath= fname= fnameroot= suffix=
  splitPath "$p" parentpath fname fnameroot suffix
  for n in parentpath fname fnameroot suffix; do
    echo "$n=${!n}"
  done
done

预期输出 - 注意边缘情况:

  • 没有后缀的文件名
  • 以开头的文件名(被视为后缀的开头).
  • 以结尾的输入路径(忽略尾随)//
  • 仅作为文件名的输入路径(作为父路径返回).
  • 具有多个 -prefixed 标记的文件名(仅最后一个被视为后缀):.
----- /etc/bash.bashrc
parentpath=/etc
fname=bash.bashrc
fnameroot=bash
suffix=.bashrc
----- /usr/bin/grep
parentpath=/usr/bin
fname=grep
fnameroot=grep
suffix=
----- /Users/jdoe/.bash_profile
parentpath=/Users/jdoe
fname=.bash_profile
fnameroot=.bash_profile
suffix=
----- /Library/Application Support/
parentpath=/Library
fname=Application Support
fnameroot=Application Support
suffix=
----- readme.new.txt
parentpath=.
fname=readme.new.txt
fnameroot=readme.new
suffix=.txt
5赞 Alex Gray 8/27/2013 #22

主要基于 @mklement0 出色的、充满随机、有用的抨击——以及对这个/其他问题/“那个的互联网”的其他答案......我把它全部包装在一个稍微更容易理解、可重用的函数中,用于我(或你的)处理什么(我认为)应该是 / / 你有什么的更强大的版本。.bash_profiledirnamebasename

function path { SAVEIFS=$IFS; IFS=""   # stash IFS for safe-keeping, etc.
    [[ $# != 2 ]] && echo "usage: path <path> <dir|name|fullname|ext>" && return    # demand 2 arguments
    [[ $1 =~ ^(.*/)?(.+)?$ ]] && {     # regex parse the path
        dir=${BASH_REMATCH[1]}
        file=${BASH_REMATCH[2]}
        ext=$([[ $file = *.* ]] && printf %s ${file##*.} || printf '')
        # edge cases for extensionless files and files like ".nesh_profile.coffee"
        [[ $file == $ext ]] && fnr=$file && ext='' || fnr=${file:0:$((${#file}-${#ext}))}
        case "$2" in
             dir) echo      "${dir%/*}"; ;;
            name) echo      "${fnr%.*}"; ;;
        fullname) echo "${fnr%.*}.$ext"; ;;
             ext) echo           "$ext"; ;;
        esac
    }
    IFS=$SAVEIFS
}     

使用示例...

SOMEPATH=/path/to.some/.random\ file.gzip
path $SOMEPATH dir        # /path/to.some
path $SOMEPATH name       # .random file
path $SOMEPATH ext        # gzip
path $SOMEPATH fullname   # .random file.gzip                     
path gobbledygook         # usage: -bash <path> <dir|name|fullname|ext>

评论

1赞 mklement0 11/26/2013
干得好;一些建议: - 你似乎根本不依赖(如果你依赖,你可以用它来定位设置它的效果)。- 最好使用变量。- 您的错误消息应输出到 ,而不是 (use ),并且您应该返回非零退出代码。- 最好重命名为(前者建议使用目录组件的路径)。- 无条件地附加一个(句点),即使原始句点没有。您可以简单地使用该实用程序,但请注意,它忽略了终止 .$IFSlocallocalstderrstdout1>&2fullnamebasenamename.basename/
1赞 commonpike 1/3/2014 #23

从上面的答案来看,模仿 Python 的最短单行代码

file, ext = os.path.splitext(path)

假设您的文件确实有一个扩展名,是

EXT="${PATH##*.}"; FILE=$(basename "$PATH" .$EXT)

评论

0赞 commonpike 12/11/2014
我对此投了反对票。我正在考虑删除答案,人们不知何故不喜欢它。
0赞 2/10/2016
basename 不会删除扩展名,只会删除路径。
0赞 2/10/2016
我已经很久没有看过手册页了,我忘记了 SUFFIX 选项。
0赞 tripleee 11/10/2017
在你知道要放什么之前,你必须知道你想剥离哪个扩展,所以这是一直到下的海龟。(此外,应避免私有变量名称全部大写;它们保留给系统变量。EXT
7赞 Joydip Datta 3/22/2014 #24

我使用以下脚本

$ echo "foo.tar.gz"|rev|cut -d"." -f3-|rev
foo

评论

0赞 codeforester 3/7/2018
这根本没有效率。分叉次数太多,这是完全不必要的,因为此操作可以在纯 Bash 中执行,而无需任何外部命令和分叉。
7赞 Dennis 3/31/2014 #25

如何提取中的文件名和扩展名:

function split-filename-extension --description "Prints the filename and extension"
  for file in $argv
    if test -f $file
      set --local extension (echo $file | awk -F. '{print $NF}')
      set --local filename (basename $file .$extension)
      echo "$filename $extension"
    else
      echo "$file is not a valid file"
    end
  end
end

警告:在最后一个点上拆分,这适用于带有点的文件名,但不适用于带有点的扩展名。请参阅下面的示例。

用法:

$ split-filename-extension foo-0.4.2.zip bar.tar.gz
foo-0.4.2 zip  # Looks good!
bar.tar gz  # Careful, you probably want .tar.gz as the extension.

可能有更好的方法可以做到这一点。随意编辑我的答案以改进它。


如果您要处理的扩展集有限,并且您知道所有这些扩展,请尝试以下操作:

switch $file
  case *.tar
    echo (basename $file .tar) tar
  case *.tar.bz2
    echo (basename $file .tar.bz2) tar.bz2
  case *.tar.gz
    echo (basename $file .tar.gz) tar.gz
  # and so on
end

没有作为第一个示例的警告,但您必须处理每种情况,因此它可能会更加繁琐,具体取决于您可以预期的扩展数量。

0赞 Vytenis Bivainis 5/23/2014 #26

你可以使用

sed 's/^/./' | rev | cut -d. -f2- | rev | cut -c2-

获取文件名和

sed 's/^/./' | rev | cut -d. -f1  | rev

获取扩展。

测试用例:

echo "filename.gz"     | sed 's/^/./' | rev | cut -d. -f2- | rev | cut -c2-
echo "filename.gz"     | sed 's/^/./' | rev | cut -d. -f1  | rev
echo "filename"        | sed 's/^/./' | rev | cut -d. -f2- | rev | cut -c2-
echo "filename"        | sed 's/^/./' | rev | cut -d. -f1  | rev
echo "filename.tar.gz" | sed 's/^/./' | rev | cut -d. -f2- | rev | cut -c2-
echo "filename.tar.gz" | sed 's/^/./' | rev | cut -d. -f1  | rev
4赞 RandyP 6/10/2014 #27

一个简单的答案:

To expand on the POSIX variables answer, note that you can do more interesting patterns. So for the case detailed here, you could simply do this:

tar -zxvf $1
cd ${1%.tar.*}

That will cut off the last occurrence of .tar.<something>.

More generally, if you wanted to remove the last occurrence of .<something>.<something-else> then

${1.*.*}

should work fine.

The link the above answer appears to be dead. Here's a great explanation of a bunch of the string manipulation you can do directly in Bash, from TLDP.

评论

0赞 tonix 1/2/2015
Is there a way to make the match case-insensitive?
-2赞 Srikant 7/30/2014 #28

A simple bash one liner. I used this to remove rst extension from all files in pwd

for each in `ls -1 *.rst`
do
     a=$(echo $each | wc -c)
     echo $each | cut -c -$(( $a-5 )) >> blognames
done

What it does ?

1) will list all the files on stdout in new line (try).ls -1 *.rst

2) counts the number of characters in each filename .echo $each | wc -c

3) selects up to last 4 characters, i.e, .echo $each | cut -c -$(( $a-5 )).rst

2赞 historystamp 10/22/2014 #29

Here is the algorithm I used for finding the name and extension of a file when I wrote a Bash script to make names unique when names conflicted with respect to casing.

#! /bin/bash 

#
# Finds 
# -- name and extension pairs
# -- null extension when there isn't an extension.
# -- Finds name of a hidden file without an extension
# 

declare -a fileNames=(
  '.Montreal' 
  '.Rome.txt' 
  'Loundon.txt' 
  'Paris' 
  'San Diego.txt'
  'San Francisco' 
  )

echo "Script ${0} finding name and extension pairs."
echo 

for theFileName in "${fileNames[@]}"
do
     echo "theFileName=${theFileName}"  

     # Get the proposed name by chopping off the extension
     name="${theFileName%.*}"

     # get extension.  Set to null when there isn't an extension
     # Thanks to mklement0 in a comment above.
     extension=$([[ "$theFileName" == *.* ]] && echo ".${theFileName##*.}" || echo '')

     # a hidden file without extenson?
     if [ "${theFileName}" = "${extension}" ] ; then
         # hidden file without extension.  Fixup.
         name=${theFileName}
         extension=""
     fi

     echo "  name=${name}"
     echo "  extension=${extension}"
done 

The test run.

$ config/Name\&Extension.bash 
Script config/Name&Extension.bash finding name and extension pairs.

theFileName=.Montreal
  name=.Montreal
  extension=
theFileName=.Rome.txt
  name=.Rome
  extension=.txt
theFileName=Loundon.txt
  name=Loundon
  extension=.txt
theFileName=Paris
  name=Paris
  extension=
theFileName=San Diego.txt
  name=San Diego
  extension=.txt
theFileName=San Francisco
  name=San Francisco
  extension=
$ 

FYI: The complete transliteration program and more test cases can be found here: https://www.dropbox.com/s/4c6m0f2e28a1vxf/avoid-clashes-code.zip?dl=0

评论

0赞 f0nzie 5/26/2020
From all the solutions this is the only one that returns an empty string when the file has no extension with: extension=$([[ "$theFileName" == *.* ]] && echo ".${theFileName##*.}" || echo '')
37赞 henfiber 6/16/2015 #30

Here are some alternative suggestions (mostly in ), including some advanced use cases, like extracting version numbers for software packages.awk

Just note that with a slightly different input some of these may fail, therefore anyone using these should validate on their expected input and adapt the regex expression if required.

f='/path/to/complex/file.1.0.1.tar.gz'

# Filename : 'file.1.0.x.tar.gz'
    echo "$f" | awk -F'/' '{print $NF}'

# Extension (last): 'gz'
    echo "$f" | awk -F'[.]' '{print $NF}'
    
# Extension (all) : '1.0.1.tar.gz'
    echo "$f" | awk '{sub(/[^.]*[.]/, "", $0)} 1'
    
# Extension (last-2): 'tar.gz'
    echo "$f" | awk -F'[.]' '{print $(NF-1)"."$NF}'

# Basename : 'file'
    echo "$f" | awk '{gsub(/.*[/]|[.].*/, "", $0)} 1'

# Basename-extended : 'file.1.0.1.tar'
    echo "$f" | awk '{gsub(/.*[/]|[.]{1}[^.]+$/, "", $0)} 1'

# Path : '/path/to/complex/'
    echo "$f" | awk '{match($0, /.*[/]/, a); print a[0]}'
    # or 
    echo "$f" | grep -Eo '.*[/]'
    
# Folder (containing the file) : 'complex'
    echo "$f" | awk -F'/' '{$1=""; print $(NF-1)}'
    
# Version : '1.0.1'
    # Defined as 'number.number' or 'number.number.number'
    echo "$f" | grep -Eo '[0-9]+[.]+[0-9]+[.]?[0-9]?'

    # Version - major : '1'
    echo "$f" | grep -Eo '[0-9]+[.]+[0-9]+[.]?[0-9]?' | cut -d. -f1

    # Version - minor : '0'
    echo "$f" | grep -Eo '[0-9]+[.]+[0-9]+[.]?[0-9]?' | cut -d. -f2

    # Version - patch : '1'
    echo "$f" | grep -Eo '[0-9]+[.]+[0-9]+[.]?[0-9]?' | cut -d. -f3

# All Components : "path to complex file 1 0 1 tar gz"
    echo "$f" | awk -F'[/.]' '{$1=""; print $0}'
    
# Is absolute : True (exit-code : 0)
    # Return true if it is an absolute path (starting with '/' or '~/'
    echo "$f" | grep -q '^[/]\|^~/'
 

All use cases are using the original full path as input, without depending on intermediate results.

评论

0赞 Lucas Soares 12/22/2022
My version of awk doesn't like # Path : '/path/to/complex/' echo "$f" | awk '{match($0, /.*[/]/, a); print a[0]}'
6赞 cvr 12/5/2015 #31

Building from Petesh answer, if only the filename is needed, both path and extension can be stripped in a single line,

filename=$(basename ${fullname%.*})

评论

1赞 helmy 1/21/2016
Did not work for me: "basename: missing operand Try 'basename --help' for more information."
0赞 cvr 2/2/2016
Strange, are you certain you're using Bash? In my case, with both versions 3.2.25 (old CentOS) and 4.3.30 (Debian Jessie) it works flawlessly.
0赞 Adrian 3/15/2017
Maybe there is a space in the filename? Try using filename="$(basename "${fullname%.*}")"
0赞 tripleee 11/10/2017
The second argument to is optional, but specifies the extension to strip off. The substitution might still be useful but perhaps actually isn't, since you can actually perform all of these substitutions with shell builtins.basenamebasename
0赞 scolfax 10/13/2016 #32

Here is a solution that extracts path components in a variety of forms and can handle most edge cases:sed

## Enter the input path and field separator character, for example:
## (separatorChar must not be present in inputPath)

inputPath="/path/to/Foo.bar"
separatorChar=":"

## sed extracts the path components and assigns them to output variables

oldIFS="$IFS"
IFS="$separatorChar"
read dirPathWithSlash dirPath fileNameWithExt fileName fileExtWithDot fileExt <<<"$(sed -En '
s/^[[:space:]]+//
s/[[:space:]]+$//
t l1
:l1
s/^([^/]|$)//
t
s/[/]+$//
t l2
:l2
s/^$/filesystem\/\
filesystem/p
t
h
s/^(.*)([/])([^/]+)$/\1\2\
\1\
\3/p
g
t l3
:l3
s/^.*[/]([^/]+)([.])([a-zA-Z0-9]+)$/\1\
\2\3\
\3/p
t
s/^.*[/](.+)$/\1/p
' <<<"$inputPath" | tr "\n" "$separatorChar")"
IFS="$oldIFS"

## Results (all use separatorChar=":")

## inputPath        = /path/to/Foo.bar
## dirPathWithSlash = /path/to/
## dirPath          = /path/to 
## fileNameWithExt  = Foo.bar
## fileName         = Foo
## fileExtWithDot   = .bar
## fileExt          = bar

## inputPath        = /path/to/Foobar
## dirPathWithSlash = /path/to/
## dirPath          = /path/to 
## fileNameWithExt  = Foobar
## fileName         = Foobar
## fileExtWithDot   =
## fileExt          =

## inputPath        = /path/to/...bar
## dirPathWithSlash = /path/to/
## dirPath          = /path/to 
## fileNameWithExt  = ...bar
## fileName         = ..
## fileExtWithDot   = .bar
## fileExt          = bar

## inputPath        = /path/to/..bar
## dirPathWithSlash = /path/to/
## dirPath          = /path/to 
## fileNameWithExt  = ..bar
## fileName         = .
## fileExtWithDot   = .bar
## fileExt          = bar

## inputPath        = /path/to/.bar
## dirPathWithSlash = /path/to/
## dirPath          = /path/to 
## fileNameWithExt  = .bar
## fileName         = .bar
## fileExtWithDot   = 
## fileExt          = 

## inputPath        = /path/to/...
## dirPathWithSlash = /path/to/
## dirPath          = /path/to 
## fileNameWithExt  = ...
## fileName         = ...
## fileExtWithDot   =
## fileExt          =

## inputPath        = /path/to/Foo.
## dirPathWithSlash = /path/to/
## dirPath          = /path/to 
## fileNameWithExt  = Foo.
## fileName         = Foo.
## fileExtWithDot   =
## fileExt          =

## inputPath        = / (the root directory)
## dirPathWithSlash = filesystem/
## dirPath          = filesystem
## fileNameWithExt  =
## fileName         =
## fileExtWithDot   =
## fileExt          =

## inputPath        =  (invalid because empty)
## dirPathWithSlash =
## dirPath          =
## fileNameWithExt  =
## fileName         =
## fileExtWithDot   =
## fileExt          =

## inputPath        = Foo/bar (invalid because doesn't start with a forward slash)
## dirPathWithSlash =
## dirPath          =
## fileNameWithExt  =
## fileName         =
## fileExtWithDot   =
## fileExt          =

其工作原理如下:

SED 解析输入路径,并按顺序在单独的行上打印以下路径组件:

  • 带有尾部斜杠字符的目录路径
  • 不带尾部斜杠字符的目录路径
  • 带扩展名的文件名
  • 不带扩展名的文件名
  • 带有前导点字符的文件扩展名
  • 不带前导点字符的文件扩展名

trSED 输出转换为上述路径组件的分隔符分隔字符串。

read 使用分隔符作为字段分隔符 (),并将每个路径组件分配给其各自的变量。IFS="$separatorChar"

以下是 sed 构造的工作原理:

  • s/^[[:space:]]+// 和 s/[[:space:]]+$// 去除任何前导和/或尾随空格字符
  • t l1 和 :l1 刷新下一个函数的函数ts
  • s/^([^/]|$)//t 测试无效的输入路径(不以正斜杠开头的路径),在这种情况下,它将所有输出行留空并退出命令sed
  • s/[/]+$// 去除任何尾部斜杠
  • t l2 和 :l2 刷新下一个函数的函数ts
  • s/^$/filesystem\/\\[换行符]filesystem/pt 测试输入路径由根目录 / 组成的特殊情况,在这种情况下,它会打印 dirPathWithSlashdirPath 输出行的 filesystem/文件系统,将所有其他输出行留空,并退出 sed 命令
  • h 将输入路径保存在保持空间中
  • s/^(.*)([/])([^/]+)$/\1\2\\[换行符]\1\\[换行符]\3/p 打印 dirPathWithSlash、dirPathfileNameWithExt 输出行
  • g 从保持空间中检索输入路径
  • t l3 和 :l3 刷新下一个函数的函数ts
  • s/^.*\[/]([^/]+)([.])([a-zA-Z0-9]+)$/\1\\[换行符]\2\3\\[换行符]\3/p 和 t 打印 fileNamefileExtWithDotfileExt 输出行,以表示存在文件扩展名(假定仅由字母数字字符组成),然后退出命令sed
  • s/^.*\[/](.+)$/\1/p 打印 fileName,但不打印 fileExtWithDot,如果文件扩展名不存在,则 fileExt 输出行,然后退出命令。sed
35赞 Cyker 12/2/2016 #33

无需为这个简单的任务而烦恼,甚至无需为这个简单的任务而烦恼。有一个纯 Bash 兼容的解决方案,它只使用参数扩展。awksedperlos.path.splitext()

参考实现

os.path.splitext(path) 的文档:

将路径名路径拆分为一对,使 和 ext 为空或以句点开头,并且最多包含一个句点。基本名称上的前导句点将被忽略; 返回。(root, ext)root + ext == pathsplitext('.cshrc')('.cshrc', '')

Python 代码:

root, ext = os.path.splitext(path)

Bash 实现

尊重领先时期

root="${path%.*}"
ext="${path#"$root"}"

忽略前导句点

root="${path#.}";root="${path%"$root"}${root%.*}"
ext="${path#"$root"}"

测试

以下是忽略前导句点实现的测试用例,这些用例应与每个输入的 Python 参考实现匹配。

|---------------|-----------|-------|
|path           |root       |ext    |
|---------------|-----------|-------|
|' .txt'        |' '        |'.txt' |
|' .txt.txt'    |' .txt'    |'.txt' |
|' txt'         |' txt'     |''     |
|'*.txt.txt'    |'*.txt'    |'.txt' |
|'.cshrc'       |'.cshrc'   |''     |
|'.txt'         |'.txt'     |''     |
|'?.txt.txt'    |'?.txt'    |'.txt' |
|'\n.txt.txt'   |'\n.txt'   |'.txt' |
|'\t.txt.txt'   |'\t.txt'   |'.txt' |
|'a b.txt.txt'  |'a b.txt'  |'.txt' |
|'a*b.txt.txt'  |'a*b.txt'  |'.txt' |
|'a?b.txt.txt'  |'a?b.txt'  |'.txt' |
|'a\nb.txt.txt' |'a\nb.txt' |'.txt' |
|'a\tb.txt.txt' |'a\tb.txt' |'.txt' |
|'txt'          |'txt'      |''     |
|'txt.pdf'      |'txt'      |'.pdf' |
|'txt.tar.gz'   |'txt.tar'  |'.gz'  |
|'txt.txt'      |'txt'      |'.txt' |
|---------------|-----------|-------|

测试结果

所有测试均通过。

评论

4赞 frederick99 11/17/2018
否,的基本文件名应为 ,扩展名应为text.tar.gztext.tar.gz
3赞 Cyker 12/26/2018
@frederick99 正如我所说,这里的解决方案与 Python 中的实现相匹配。对于可能引起争议的输入,这种实现是否合理是另一个话题。os.path.splitext
0赞 ymett 6/12/2019
模式 () 中的引号如何工作?如果省略它们会发生什么?(我找不到任何关于此事的文档。此外,这如何处理带有或其中的文件名?"$root"*?
0赞 ymett 6/12/2019
好的,测试表明引号使模式成为文字,即 而且并不特别。因此,我问题的两个部分相互回答。我没有记录在案,我说得对吗?或者这是否应该从引号通常禁用全球扩展这一事实来理解?*?
1赞 Maëlan 2/9/2020
绝妙的答案!我只建议一个稍微简单的变体来计算根: — 然后继续执行相同的操作以提取扩展名。root="${path#?}";root="${path::1}${root%.*}"
24赞 Ron 4/22/2017 #34

最小和最简单的解决方案(单行)是:

$ file=/blaabla/bla/blah/foo.txt
echo $(basename ${file%.*}) # foo

评论

0赞 tripleee 11/10/2017
这是对 echo 的无用使用。一般来说,除非您特别要求 shell 在显示结果之前对输出执行空格标记化和通配符扩展,否则最好编写。测验:输出是什么(如果这是你真正想要的,你真的真的想要)。echo $(command)commandcommandecho $(echo '*')echo *
1赞 Ron 4/17/2018
@triplee我根本没有使用命令。我只是用它来演示结果,该结果作为第 2 行的结果出现在第 3 行中。echofoo
0赞 tripleee 4/17/2018
但只是会做同样的事情;您正在使用命令替换来捕获其输出,但会立即捕获相同的输出。(如果不引用,结果名义上是不同的;但这几乎无关紧要,更不用说这里的功能了。basename "${file%.*}"echo
2赞 frederick99 11/17/2018
@Ron 在指责他浪费我们的时间之前,先阅读他的第一条评论。
1赞 Ron 11/18/2018
@frederick99我做到了。我回答说!他在胡说八道,这甚至不是命令的一部分。您需要从我的代码中提取的只是这一部分: 甚至不要看.它没有目的,但它仍然在示例中,只是为了帮助人们清楚地了解引擎盖下发生的事情。 与“在 bash 中提取文件名和扩展名”这个主题完全无关。echo$(basename ${file%.*})echoecho
3赞 phil294 9/16/2017 #35

如果您还想允许扩展,这是我能想到的最短的:

echo 'hello.txt' | sed -r 's/.+\.(.+)|.*/\1/' # EXTENSION
echo 'hello.txt' | sed -r 's/(.+)\..+|(.*)/\1\2/' # FILENAME

第一行解释:它匹配 PATH。EXT 或 ANYTHING 并将其替换为 EXT。如果匹配了 ANYTHING,则不会捕获 ext 组。

3赞 Bruno BEAUFILS 12/11/2018 #36

恕我直言,已经给出了最佳解决方案(使用 shell 参数扩展),并且是目前评价最高的解决方案。

然而,我添加了这个只使用哑巴命令的命令,效率不高,没有人应该认真使用:

FILENAME=$(echo $FILE | cut -d . -f 1-$(printf $FILE | tr . '\n' | wc -l))
EXTENSION=$(echo $FILE | tr . '\n' | tail -1)

添加只是为了好玩:-)

12赞 Ken Mueller 6/8/2019 #37

这是唯一对我有用的:

path='folder/other_folder/file.js'

base=${path##*/}
echo ${base%.*}

>> file

这也可用于字符串插值,但不幸的是,您必须事先设置。base

7赞 Fravadona 2/4/2022 #38

之前没有答案使用 bash 正则表达式
这是一个纯 bash 解决方案,它将路径拆分为:

  • 目录路径,存在
    时带有尾随 丢弃尾随 / 的正则表达式太长了,以至于我没有发布它
    /
  • 文件名,不包括(最后一个)点扩展名
  • (最后一个)点扩展名,其前导.

该代码旨在处理所有可能的情况,欢迎您尝试。

#!/bin/bash

for path; do

####### the relevant part ######

[[ $path =~ ^(\.{1,2}|.*/\.{0,2})$|^(.*/)([^/]+)(\.[^/]*)$|^(.*/)(.+)$|^(.+)(\..*)$|^(.+)$ ]]

dirpath=${BASH_REMATCH[1]}${BASH_REMATCH[2]}${BASH_REMATCH[5]}
filename=${BASH_REMATCH[3]}${BASH_REMATCH[6]}${BASH_REMATCH[7]}${BASH_REMATCH[9]}
filext=${BASH_REMATCH[4]}${BASH_REMATCH[8]}

# dirpath should be non-null
[[ $dirpath ]] || dirpath='.'

################################

printf '%s=%q\n' \
    path     "$path" \
    dirpath  "$dirpath" \
    filename "$filename" \
    filext   "$filext"

done

它是如何工作的?

基本上,它确保只有一个子表达式(在正则表达式中分隔)能够捕获输入。多亏了这一点,您可以连接存储在 中的所有相同类型的捕获组(例如,与目录路径相关的捕获组),因为最多有一个是非 null。|BASH_REMATCH

以下是一组扩展但并非详尽的示例的结果:
+--------------------------------------------------------+
| input             dirpath        filename       filext |
+--------------------------------------------------------+
''                  .              ''             ''
.                   .              ''             ''
..                  ..             ''             ''
...                 .              ..             .
.file               .              .file          ''
.file.              .              .file          .
.file..             .              .file.         .
.file.Z             .              .file          .Z
.file.sh.Z          .              .file.sh       .Z
file                .              file           ''
file.               .              file           .
file..              .              file.          .
file.Z              .              file           .Z
file.sh.Z           .              file.sh        .Z
dir/                dir/           ''             ''
dir/.               dir/.          ''             ''
dir/...             dir/           ..             .
dir/.file           dir/           .file          ''
dir/.file.          dir/           .file          .
dir/.file..         dir/           .file.         .
dir/.file.Z         dir/           .file          .Z
dir/.file.x.Z       dir/           .file.x        .Z
dir/file            dir/           file           ''
dir/file.           dir/           file           .
dir/file..          dir/           file.          .
dir/file.Z          dir/           file           .Z
dir/file.x.Z        dir/           file.x         .Z
dir./.              dir./.         ''             ''
dir./...            dir./          ..             .
dir./.file          dir./          .file          ''
dir./.file.         dir./          .file          .
dir./.file..        dir./          .file.         .
dir./.file.Z        dir./          .file          .Z
dir./.file.sh.Z     dir./          .file.sh       .Z
dir./file           dir./          file           ''
dir./file.          dir./          file           .
dir./file..         dir./          file.          .
dir./file.Z         dir./          file           .Z
dir./file.x.Z       dir./          file.x         .Z
dir//               dir//          ''             ''
dir//.              dir//.         ''             ''
dir//...            dir//          ..             .
dir//.file          dir//          .file          ''
dir//.file.         dir//          .file          .
dir//.file..        dir//          .file.         .
dir//.file.Z        dir//          .file          .Z
dir//.file.x.Z      dir//          .file.x        .Z
dir//file           dir//          file           ''
dir//file.          dir//          file           .
dir//file..         dir//          file.          .
dir//file.Z         dir//          file           .Z
dir//file.x.Z       dir//          file.x         .Z
dir.//.             dir.//.        ''             ''
dir.//...           dir.//         ..             .
dir.//.file         dir.//         .file          ''
dir.//.file.        dir.//         .file          .
dir.//.file..       dir.//         .file.         .
dir.//.file.Z       dir.//         .file          .Z
dir.//.file.x.Z     dir.//         .file.x        .Z
dir.//file          dir.//         file           ''
dir.//file.         dir.//         file           .
dir.//file..        dir.//         file.          .
dir.//file.Z        dir.//         file           .Z
dir.//file.x.Z      dir.//         file.x         .Z
/                   /              ''             ''
/.                  /.             ''             ''
/..                 /..            ''             ''
/...                /              ..             .
/.file              /              .file          ''
/.file.             /              .file          .
/.file..            /              .file.         .
/.file.Z            /              .file          .Z
/.file.sh.Z         /              .file.sh       .Z
/file               /              file           ''
/file.              /              file           .
/file..             /              file.          .
/file.Z             /              file           .Z
/file.sh.Z          /              file.sh        .Z
/dir/               /dir/          ''             ''
/dir/.              /dir/.         ''             ''
/dir/...            /dir/          ..             .
/dir/.file          /dir/          .file          ''
/dir/.file.         /dir/          .file          .
/dir/.file..        /dir/          .file.         .
/dir/.file.Z        /dir/          .file          .Z
/dir/.file.x.Z      /dir/          .file.x        .Z
/dir/file           /dir/          file           ''
/dir/file.          /dir/          file           .
/dir/file..         /dir/          file.          .
/dir/file.Z         /dir/          file           .Z
/dir/file.x.Z       /dir/          file.x         .Z
/dir./.             /dir./.        ''             ''
/dir./...           /dir./         ..             .
/dir./.file         /dir./         .file          ''
/dir./.file.        /dir./         .file          .
/dir./.file..       /dir./         .file.         .
/dir./.file.Z       /dir./         .file          .Z
/dir./.file.sh.Z    /dir./         .file.sh       .Z
/dir./file          /dir./         file           ''
/dir./file.         /dir./         file           .
/dir./file..        /dir./         file.          .
/dir./file.Z        /dir./         file           .Z
/dir./file.x.Z      /dir./         file.x         .Z
/dir//              /dir//         ''             ''
/dir//.             /dir//.        ''             ''
/dir//...           /dir//         ..             .
/dir//.file         /dir//         .file          ''
/dir//.file.        /dir//         .file          .
/dir//.file..       /dir//         .file.         .
/dir//.file.Z       /dir//         .file          .Z
/dir//.file.x.Z     /dir//         .file.x        .Z
/dir//file          /dir//         file           ''
/dir//file.         /dir//         file           .
/dir//file..        /dir//         file.          .
/dir//file.Z        /dir//         file           .Z
/dir//file.x.Z      /dir//         file.x         .Z
/dir.//.            /dir.//.       ''             ''
/dir.//...          /dir.//        ..             .
/dir.//.file        /dir.//        .file          ''
/dir.//.file.       /dir.//        .file          .
/dir.//.file..      /dir.//        .file.         .
/dir.//.file.Z      /dir.//        .file          .Z
/dir.//.file.x.Z    /dir.//        .file.x        .Z
/dir.//file         /dir.//        file           ''
/dir.//file.        /dir.//        file           .
/dir.//file..       /dir.//        file.          .
/dir.//file.Z       /dir.//        file           .Z
/dir.//file.x.Z     /dir.//        file.x         .Z
//                  //             ''             ''
//.                 //.            ''             ''
//..                //..           ''             ''
//...               //             ..             .
//.file             //             .file          ''
//.file.            //             .file          .
//.file..           //             .file.         .
//.file.Z           //             .file          .Z
//.file.sh.Z        //             .file.sh       .Z
//file              //             file           ''
//file.             //             file           .
//file..            //             file.          .
//file.Z            //             file           .Z
//file.sh.Z         //             file.sh        .Z
//dir/              //dir/         ''             ''
//dir/.             //dir/.        ''             ''
//dir/...           //dir/         ..             .
//dir/.file         //dir/         .file          ''
//dir/.file.        //dir/         .file          .
//dir/.file..       //dir/         .file.         .
//dir/.file.Z       //dir/         .file          .Z
//dir/.file.x.Z     //dir/         .file.x        .Z
//dir/file          //dir/         file           ''
//dir/file.         //dir/         file           .
//dir/file..        //dir/         file.          .
//dir/file.Z        //dir/         file           .Z
//dir/file.x.Z      //dir/         file.x         .Z
//dir./.            //dir./.       ''             ''
//dir./...          //dir./        ..             .
//dir./.file        //dir./        .file          ''
//dir./.file.       //dir./        .file          .
//dir./.file..      //dir./        .file.         .
//dir./.file.Z      //dir./        .file          .Z
//dir./.file.sh.Z   //dir./        .file.sh       .Z
//dir./file         //dir./        file           ''
//dir./file.        //dir./        file           .
//dir./file..       //dir./        file.          .
//dir./file.Z       //dir./        file           .Z
//dir./file.x.Z     //dir./        file.x         .Z
//dir//             //dir//        ''             ''
//dir//.            //dir//.       ''             ''
//dir//...          //dir//        ..             .
//dir//.file        //dir//        .file          ''
//dir//.file.       //dir//        .file          .
//dir//.file..      //dir//        .file.         .
//dir//.file.Z      //dir//        .file          .Z
//dir//.file.x.Z    //dir//        .file.x        .Z
//dir//file         //dir//        file           ''
//dir//file.        //dir//        file           .
//dir//file..       //dir//        file.          .
//dir//file.Z       //dir//        file           .Z
//dir//file.x.Z     //dir//        file.x         .Z
//dir.//.           //dir.//.      ''             ''
//dir.//...         //dir.//       ..             .
//dir.//.file       //dir.//       .file          ''
//dir.//.file.      //dir.//       .file          .
//dir.//.file..     //dir.//       .file.         .
//dir.//.file.Z     //dir.//       .file          .Z
//dir.//.file.x.Z   //dir.//       .file.x        .Z
//dir.//file        //dir.//       file           ''
//dir.//file.       //dir.//       file           .
//dir.//file..      //dir.//       file.          .
//dir.//file.Z      //dir.//       file           .Z
//dir.//file.x.Z    //dir.//       file.x         .Z

如您所见,该行为与 和 不同。 例如,输出,而正则表达式将为您提供一个空文件名。和 相同,它们被视为目录,而不是文件名。basenamedirnamebasename dir/dir...

我用 10000 条 256 个字符的路径对其进行计时,大约需要 1 秒,而等效的 POSIX shell 解决方案慢 2 倍,基于狂野分叉(循环内的外部调用)的解决方案至少慢 60 倍。for

备注:没有必要测试包含或其他臭名昭著的字符的路径,因为 bash 的正则表达式引擎以相同的方式处理所有字符。唯一能够打破当前逻辑的字符是 和 ,以当前意想不到的方式混合或相乘。当我第一次发布我的答案时,我发现了一些我必须解决的边境案例;我不能说正则表达式是 100% 防弹的,但它现在应该非常强大。\n/.


顺便说一句,这里是产生相同输出的 POSIX shell 解决方案:

#!/bin/sh

for path; do

####### the relevant part ######

fullname=${path##*/}

case $fullname in
. | ..)
    dirpath="$path"
    filename=''
    filext=''
    ;;
*)
    dirpath=${path%"$fullname"}
    dirpath=${dirpath:-.}       # dirpath should be non-null
    filename=${fullname#.}
    filename="${fullname%"$filename"}${filename%.*}"
    filext=${fullname#"$filename"}
    ;;
esac

################################

printf '%s=%s\n' \
    path     "$path" \
    dirpath  "$dirpath" \
    filename "$filename" \
    filext   "$filext"

done

附言:有些人可能不同意上述代码给出的结果:

  • dotfiles 的特例:原因是 dotfiles 是一个 UNIX 概念。

  • 和的特殊情况:恕我直言,将它们视为目录似乎很明显,但大多数库不会这样做并强制用户对结果进行后处理。...

  • 不支持双扩展名:这是因为您需要一个完整的数据库来存储所有有效的双扩展名,最重要的是,因为文件扩展名在 UNIX 中没有任何意义;例如,您可以调用 tar 存档,这完全没问题,您将能够毫无问题地这样做。my_tarred_filestar xf my_tarred_files

0赞 Mateja Petrovic 12/6/2023 #39
echo {} | grep -Eo "\w+$" # alphanumeric characters

echo {} | grep -Eo "[a-z]+$" # just lower case characters

echo {} | grep -Eo "[^.]+$" # any character outside the "."

echo {} | grep -Eo "\w+\.\w+$" # last two segments e.g. test.ts
# and so on

Imo, this approach is worth a mention because using grep extended regex to match against the last segment(s) a.k.a extension is simple but effective. Note the symbol denoting the end of the pattern/path.$