提问人:ibz 提问时间:6/8/2009 最后编辑:Benjamin W.ibz 更新时间:5/22/2023 访问量:2215736
在 Bash 中提取文件名和扩展名
Extract filename and extension in Bash
问:
我想分别获取文件名(不带扩展名)和扩展名。
到目前为止,我发现的最佳解决方案是:
NAME=`echo "$FILE" | cut -d'.' -f1`
EXTENSION=`echo "$FILE" | cut -d'.' -f2`
这是错误的,因为如果文件名包含多个字符,则它不起作用。如果,比方说,我有 ,它将考虑 和 ,而不是 和 。.
a.b.js
a
b.js
a.b
js
它可以在 Python 中轻松完成
file, ext = os.path.splitext(path)
但如果可能的话,我宁愿不要为此启动 Python 解释器。
有什么更好的主意吗?
答:
~% FILE="example.tar.gz"
~% echo "${FILE%%.*}"
example
~% echo "${FILE%.*}"
example.tar
~% echo "${FILE#*.}"
tar.gz
~% echo "${FILE##*.}"
gz
更多细节,请参见 Bash 手册中的 shell 参数扩展。
评论
dinosaurs.in.tar
dinosaurs.in.tar.gz
x.tar.gz
gz
x.tar
首先,获取不带路径的文件名:
filename=$(basename -- "$fullfile")
extension="${filename##*.}"
filename="${filename%.*}"
或者,您可以专注于路径的最后一个“/”而不是“.”,即使您的文件扩展名不可预测,这也应该有效:
filename="${fullfile##*/}"
您可能需要查看文档:
- 在 Web 上的“3.5.3 Shell 参数扩展”部分"
- 在 bash 手册页中名为“参数扩展”的部分
评论
basename
extension=$([[ "$filename" = *.* ]] && echo ".${filename##*.}" || echo '')
.
.txt
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
"."
评论
sed 's,\.[^\.]*$,,'
sed 's,.*\.,., ;t ;g'
test
get
substitute
awk
.tar.gz
如果文件没有扩展名或没有文件名,这似乎不起作用。这是我正在使用的;它只使用内置函数并处理更多(但不是全部)病理文件名。
#!/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 = ""
评论
dir="${fullpath:0:${#fullpath} - ${#filename}}"
dir="${fullpath%$filename}"
简单易用${parameter%word}
就您而言:
${FILE%.*}
如果你想测试它,请执行以下所有工作,只需删除扩展:
FILE=abc.xyz; echo ${FILE%.*};
FILE=123.abc.xyz; echo ${FILE%.*};
FILE=abc; echo ${FILE%.*};
评论
=
Mellen 在一篇博文的评论中写道:
使用 Bash,还可以获取不带扩展名的文件名,并单独获取扩展名。那是${file%.*}
${file##*.}
file="thisfile.txt"
echo "filename: ${file%.*}"
echo "extension: ${file##*.}"
输出:
filename: thisfile
extension: txt
评论
$ F="text file.test.txt"
$ echo ${F/*./}
txt
这迎合了文件名中的多个点和空格,但是如果没有扩展名,它会返回文件名本身。虽然很容易检查;只需测试文件名和扩展名是否相同即可。
当然,此方法不适用于 .tar.gz 文件。但是,这可以通过两步过程处理。如果扩展名是 gz,则再次检查是否还有 tar 扩展名。
评论
您还可以使用循环并从路径中提取文件名...for
tr
for x in `echo $path | tr "/" " "`; do filename=$x; done
将 path 中的所有“/”分隔符替换为空格,从而形成字符串列表,循环扫描它们,将最后一个留在变量中。tr
for
filename
评论
(IFS=/ ; for x in $path; do filename=$x; done)
(...)
我认为如果你只需要文件名,你可以试试这个:
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。
评论
通常您已经知道扩展,因此您可能希望使用:
basename filename .extension
例如:
basename /path/to/dir/filename.txt .txt
我们得到
filename
评论
basename
.zip
.ZIP
basename $file {.zip,.ZIP}
下面是使用 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
评论
split()
awk -F / '{ n=split($2, a, "."); print a[n] }' uses
.
好的,如果我理解正确的话,这里的问题是如何获取具有多个扩展名的文件的名称和完整扩展名,例如.stuff.tar.gz
这对我有用:
fullfile="stuff.tar.gz"
fileExt=${fullfile#*.}
fileName=${fullfile%*.$fileExt}
这将为您提供文件名和扩展名。它适用于任意数量的扩展,包括 0。希望这对任何有相同问题的人有所帮助=)stuff
.tar.gz
评论
os.path.splitext
('stuff.tar', '.gz')
使用示例文件,此代码:/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
评论
basename
您可以强制剪切以显示所有字段,以及添加到字段编号的后续字段。-
NAME=`basename "$FILE"`
EXTENSION=`echo "$NAME" | cut -d'.' -f2-`
因此,如果 FILE 是 ,则 EXTENSION 将是eth0.pcap.gz
pcap.gz
使用相同的逻辑,您还可以使用带有剪切的“-”获取文件名,如下所示:
NAME=`basename "$FILE" | cut -d'.' -f-1`
这甚至适用于没有任何扩展名的文件名。
您可以使用 basename
。
例:
$ basename foo-bar.tar.gz .tar.gz
foo-bar
您确实需要为 basename 提供要删除的扩展名,但是如果您总是执行 ,那么您就知道扩展将是 .tar
-z
.tar.gz
这应该可以做你想要的:
tar -zxvf $1
cd $(basename $1 .tar.gz)
评论
cd $(basename $1 .tar.gz)
Archive files have several extensions: tar.gz, tat.xz, tar.bz2
也许有一个选项可以做到这一点;你检查过那个人吗?否则,可以使用 Bash 字符串扩展:tar
test="mpc-1.0.1.tar.gz"
noExt="${test/.tar.gz/}" # Remove the string '.tar.gz'
echo $noExt
评论
您可以使用 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)发表评论后再次更新]
评论
$
i.like.tar.gz.files.tar.bz2
sed
$
mpc-1.0.1.tar.bz2.tar.gz
.tar.gz
.tar.bz2
您可以使用POSIX参数扩展的魔力:
bash-3.2$ FILENAME=somefile.tar.gz
bash-3.2$ echo "${FILENAME%%.*}"
somefile
bash-3.2$ echo "${FILENAME%.*}"
somefile.tar
有一点需要注意的是,如果你的文件名是这样的,那么会贪婪地删除与 的最长匹配项,你就会得到空字符串。./somefile.tar.gz
echo ${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
评论
cut
--complement
sed
-r
魔术文件识别
除了关于这个 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.types
man file
man mime.types
file
mime-support
grep $( file -b --mime-type myfile.txt ) </etc/mime.types
text/plain asc txt text pot brf srt
您可以创建一个 bash 函数来确定正确的扩展名。 有一个小(不完美)的例子:
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"
评论
-c
$(python -c "'$1'.split('/')[-1]")
$1
为了使 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"
公认的答案在典型情况下效果很好,但在边缘情况下会失败,即:
- 对于不带扩展名的文件名(在此答案的其余部分称为后缀),返回输入文件名而不是空字符串。
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
主要基于 @mklement0 出色的、充满随机、有用的抨击——以及对这个/其他问题/“那个的互联网”的其他答案......我把它全部包装在一个稍微更容易理解、可重用的函数中,用于我(或你的)处理什么(我认为)应该是 / / 你有什么的更强大的版本。.bash_profile
dirname
basename
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>
评论
$IFS
local
local
stderr
stdout
1>&2
fullname
basename
name
.
basename
/
从上面的答案来看,模仿 Python 的最短单行代码
file, ext = os.path.splitext(path)
假设您的文件确实有一个扩展名,是
EXT="${PATH##*.}"; FILE=$(basename "$PATH" .$EXT)
评论
EXT
我使用以下脚本
$ echo "foo.tar.gz"|rev|cut -d"." -f3-|rev
foo
评论
如何提取鱼中的文件名和扩展名:
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
这没有作为第一个示例的警告,但您必须处理每种情况,因此它可能会更加繁琐,具体取决于您可以预期的扩展数量。
你可以使用
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
一个简单的答案:
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.
评论
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
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
评论
extension=$([[ "$theFileName" == *.* ]] && echo ".${theFileName##*.}" || echo '')
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.
评论
Building from Petesh answer, if only the filename is needed, both path and extension can be stripped in a single line,
filename=$(basename ${fullname%.*})
评论
filename="$(basename "${fullname%.*}")"
basename
basename
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
解析输入路径,并按顺序在单独的行上打印以下路径组件:
- 带有尾部斜杠字符的目录路径
- 不带尾部斜杠字符的目录路径
- 带扩展名的文件名
- 不带扩展名的文件名
- 带有前导点字符的文件扩展名
- 不带前导点字符的文件扩展名
tr
将 SED
输出转换为上述路径组件的分隔符分隔字符串。
read
使用分隔符作为字段分隔符 (),并将每个路径组件分配给其各自的变量。IFS="$separatorChar"
以下是 sed
构造的工作原理:
s/^[[:space:]]+/
/ 和s/[[:space:]]+$
// 去除任何前导和/或尾随空格字符t
l1 和:l1
刷新下一个函数的函数t
s
- s/^
([^/]|$)//
和t
测试无效的输入路径(不以正斜杠开头的路径),在这种情况下,它将所有输出行留空并退出命令sed
s/[/]+$//
去除任何尾部斜杠t
l2 和:l2
刷新下一个函数的函数t
s
- s/^$/filesystem\/\
\[换行符]filesystem/p
和t
测试输入路径由根目录 / 组成的特殊情况,在这种情况下,它会打印 dirPathWithSlash 和 dirPath 输出行的 filesystem/ 和文件系统,将所有其他输出行留空,并退出 sed 命令 h
将输入路径保存在保持空间中- s/^(.*)([/])([^/]+)$/\1\2\\[换行符]\1\\
[换行符]\3/p
打印 dirPathWithSlash、dirPath 和 fileNameWithExt 输出行 g
从保持空间中检索输入路径t
l3 和:l3
刷新下一个函数的函数t
s
- s/^.*\[/]
([^/]+)([.])([a-zA-Z0-9]+)$
/\1\\[换行符]\2\3\\[换行符]\3/p 和t
打印 fileName、fileExtWithDot 和 fileExt 输出行,以表示存在文件扩展名(假定仅由字母数字字符组成),然后退出命令sed
s/^.*\[/](.+)$/\1/p
打印 fileName,但不打印 fileExtWithDot,如果文件扩展名不存在,则 fileExt 输出行,然后退出命令。sed
无需为这个简单的任务而烦恼,甚至无需为这个简单的任务而烦恼。有一个纯 Bash 兼容的解决方案,它只使用参数扩展。awk
sed
perl
os.path.splitext()
参考实现
将路径名路径拆分为一对,使 和 ext 为空或以句点开头,并且最多包含一个句点。基本名称上的前导句点将被忽略; 返回。
(root, ext)
root + ext == path
splitext('.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' |
|---------------|-----------|-------|
测试结果
所有测试均通过。
评论
text.tar.gz
text
.tar.gz
os.path.splitext
"$root"
*
?
*
?
root="${path#?}";root="${path::1}${root%.*}"
最小和最简单的解决方案(单行)是:
$ file=/blaabla/bla/blah/foo.txt
echo $(basename ${file%.*}) # foo
评论
echo
的无用使用。一般来说,除非您特别要求 shell 在显示结果之前对输出执行空格标记化和通配符扩展,否则最好编写。测验:输出是什么(如果这是你真正想要的,你真的真的想要)。echo $(command)
command
command
echo $(echo '*')
echo *
echo
foo
basename "${file%.*}"
echo
echo
$(basename ${file%.*})
echo
echo
如果您还想允许空扩展,这是我能想到的最短的:
echo 'hello.txt' | sed -r 's/.+\.(.+)|.*/\1/' # EXTENSION
echo 'hello.txt' | sed -r 's/(.+)\..+|(.*)/\1\2/' # FILENAME
第一行解释:它匹配 PATH。EXT 或 ANYTHING 并将其替换为 EXT。如果匹配了 ANYTHING,则不会捕获 ext 组。
恕我直言,已经给出了最佳解决方案(使用 shell 参数扩展),并且是目前评价最高的解决方案。
然而,我添加了这个只使用哑巴命令的命令,效率不高,没有人应该认真使用:
FILENAME=$(echo $FILE | cut -d . -f 1-$(printf $FILE | tr . '\n' | wc -l))
EXTENSION=$(echo $FILE | tr . '\n' | tail -1)
添加只是为了好玩:-)
这是唯一对我有用的:
path='folder/other_folder/file.js'
base=${path##*/}
echo ${base%.*}
>> file
这也可用于字符串插值,但不幸的是,您必须事先设置。base
之前没有答案使用 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
如您所见,该行为与 和 不同。
例如,输出,而正则表达式将为您提供一个空文件名。和 相同,它们被视为目录,而不是文件名。basename
dirname
basename 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_files
tar xf my_tarred_files
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.$
评论
extension="{$filename##*.}"
$
extension="${filename##*.}"
os.path.splitext
xyzzy.tar.gz
plugh.cfg.saved