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

我想单独获取文件名(不带扩展名)和扩展名。

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

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

这是错误的,因为如果文件名包含多个.字符,它就不起作用。如果,假设,我有a.b.js,它会考虑ab.js,而不是a.bjs

它可以在Python中轻松完成

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

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

有更好的主意吗?

2056969 次浏览
~% FILE="example.tar.gz"
~% echo "${FILE%%.*}"example
~% echo "${FILE%.*}"example.tar
~% echo "${FILE#*.}"tar.gz
~% echo "${FILE##*.}"gz

有关更多详细信息,请参阅Bash手册中的壳参数展开

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

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

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

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

您可能需要查看留档:

pax> echo a.b.js | sed 's/\.[^.]*$//'a.bpax> echo a.b.js | sed 's/^.*\.//'js

工作正常,所以你可以使用:

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

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

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

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

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

#!/bin/bashfor fullpath in "$@"dofilename="${fullpath##*/}"                      # Strip longest match of */ from startdir="${fullpath:0:${#fullpath} - ${#filename}}" # Substring from 0 thru pos of filenamebase="${filename%.[^.]*}"                       # Strip shortest match of . plus at least one non-dot char from endext="${filename:${#base} + 1}"                  # Substring from len of base thru endif [[ -z "$base" && -n "$ext" ]]; then          # If we have an extension and no base, it's really the basebase=".$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  = ""

简单地使用${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: thisfileextension: 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

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

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

FULLPATH=/usr/share/X11/xorg.conf.d/50-synaptics.conf
# Remove all the prefix until the "/" characterFILENAME=${FULLPATH##*/}
# Remove all the prefix until the "." characterFILEEXTENSION=${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

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

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

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

这对我有用:

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

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

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

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

将导致${ME}MyScript${MY_EXT}.sh


脚本:

#!/bin/bashset -e
MY_EXT=".${0##*.}"ME=$(/usr/bin/basename "${0}" "${MY_EXT}")
echo "${ME} - ${MY_EXT}"

一些测试:

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

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

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

因此,如果FILE是eth0.pcap.gz,则扩展将是pcap.gz

使用相同的逻辑,您也可以使用'-'和Cut获取文件名,如下所示:

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

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

您可以使用#0

示例:

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

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

这应该做你想要的:

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

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

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

您可以使用#0命令删除最后两个扩展(".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评论后再次更新]

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

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

有一个警告,如果你的文件名是./somefile.tar.gz的形式,那么echo ${FILENAME%%.*}会贪婪地删除与.最长的匹配,你会有一个空字符串。

(你可以用一个临时变量来解决这个问题:

FULL_FILENAME=$FILENAMEFILENAME=${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

魔术文件识别

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

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

file myfile.txtmyfile.txt: UTF-8 Unicode text
file -b --mime-type myfile.txttext/plain

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

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

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

file2ext() {local _mimetype=$(file -Lb --mime-type "$1") _line _basemimetypecase ${_mimetype##*[/.-]} ingzip | 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='';;esacwhile read -a _line ;doif [ "$_line" == "$_basemimetype" ] ;then[ "$_line[1]" ] &&_basemimetype=${_line[1]} ||_basemimetype=${_basemimetype##*[/.-]}breakfidone </etc/mime.typescase ${_basemimetype##*[/.-]} inexecutable ) _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"

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

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

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

outfile=${dir}${base}_suffix.${ext}
testcase: foo.bardir: "./"base: "foo"ext: "bar"outfile: "./foo_suffix.bar"
testcase: /home/me/foo.bardir: "/home/me/"base: "foo"ext: "bar"outfile: "/home/me/foo_suffix.bar"

接受的答案典型案例中效果很好,但dge情况下失败,即:

  • 对于没有扩展名的文件名(在本答案的其余部分称为后缀),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[@]}"; doecho ----- "$p"parentpath= fname= fnameroot= suffix=splitPath "$p" parentpath fname fnameroot suffixfor n in parentpath fname fnameroot suffix; doecho "$n=${!n}"donedone

执行该函数的测试代码:

test_paths=('/etc/bash.bashrc''/usr/bin/grep''/Users/jdoe/.bash_profile''/Library/Application Support/''readme.new.txt')
for p in "${test_paths[@]}"; doecho ----- "$p"parentpath= fname= fnameroot= suffix=splitPath "$p" parentpath fname fnameroot suffixfor n in parentpath fname fnameroot suffix; doecho "$n=${!n}"donedone

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

  • 没有后缀的文件名
  • .开头的文件名(没有被认为是后缀的开头)
  • /结尾的输入路径(尾随/被忽略)
  • 仅为文件名的输入路径(.作为父路径返回)
  • 具有超过.前缀标记的文件名(只有最后一个被认为是后缀):
----- /etc/bash.bashrcparentpath=/etcfname=bash.bashrcfnameroot=bashsuffix=.bashrc----- /usr/bin/grepparentpath=/usr/binfname=grepfnameroot=grepsuffix=----- /Users/jdoe/.bash_profileparentpath=/Users/jdoefname=.bash_profilefnameroot=.bash_profilesuffix=----- /Library/Application Support/parentpath=/Libraryfname=Application Supportfnameroot=Application Supportsuffix=----- readme.new.txtparentpath=.fname=readme.new.txtfnameroot=readme.newsuffix=.txt

主要基于@mclement0的优秀,充满随机,有用的巴什主义-以及对这个/其他问题/“那个该死的互联网”的其他答案……我把它全部包装成一点,稍微更容易理解,可重用函数为我的(或你的).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 pathdir=${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" indir) echo      "${dir%/*}"; ;;name) echo      "${fnr%.*}"; ;;fullname) echo "${fnr%.*}.$ext"; ;;ext) echo           "$ext"; ;;esac}IFS=$SAVEIFS}

使用示例…

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

从上面的答案来看,模仿Python的最短oneliner

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

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

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

我使用以下脚本

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

如何提取<强>鱼中的文件名和扩展名:

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

注意事项:在最后一个点上拆分,这适用于带有点的文件名,但不适用于带有点的扩展名。参见下面的示例。

用法:

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

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


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

switch $filecase *.tarecho (basename $file .tar) tarcase *.tar.bz2echo (basename $file .tar.bz2) tar.bz2case *.tar.gzecho (basename $file .tar.gz) tar.gz# and so onend

作为第一个示例,没有确实有警告,但您必须处理每个案例,因此根据您可以期望的扩展数量,它可能会更乏味。

您可以使用

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  | revecho "filename"        | sed 's/^/./' | rev | cut -d. -f2- | rev | cut -c2-echo "filename"        | sed 's/^/./' | rev | cut -d. -f1  | revecho "filename.tar.gz" | sed 's/^/./' | rev | cut -d. -f2- | rev | cut -c2-echo "filename.tar.gz" | sed 's/^/./' | rev | cut -d. -f1  | rev

一个简单的答案:

要扩展POSIX变量答案,请注意你可以做更多有趣的模式。所以对于这里详述的案例,你可以简单地这样做:

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

这将切断最后一次出现的. tar.<某物>

更一般地说,如果您想删除最后出现的.<某物>.<其他东西>,那么

${1.*.*}

应该工作得很好。

上面答案的链接似乎已经死了。这里有一个很好的解释,你可以直接在Bash中进行字符串操作,来自TLDP

一个简单的bash单行程序。我用它从pwd中的所有文件中删除了第一扩展名

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

它能做什么?

1)ls -1 *.rst将在新行(try)中列出标准输出上的所有文件。

2)echo $each | wc -c计算每个文件名中的字符数。

3)echo $each | cut -c -$(( $a-5 ))最多选择最后4个字符,即.rst

这是我在编写Bash脚本时用于查找文件名称和扩展名的算法,以便在名称与大小写冲突时使名称唯一。

#! /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[@]}"doecho "theFileName=${theFileName}"
# Get the proposed name by chopping off the extensionname="${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

测试运行。

$ config/Name\&Extension.bashScript config/Name&Extension.bash finding name and extension pairs.
theFileName=.Montrealname=.Montrealextension=theFileName=.Rome.txtname=.Romeextension=.txttheFileName=Loundon.txtname=Loundonextension=.txttheFileName=Parisname=Parisextension=theFileName=San Diego.txtname=San Diegoextension=.txttheFileName=San Francisconame=San Franciscoextension=$

仅供参考:完整的音译程序和更多的测试用例可以在这里找到:https://www.dropbox.com/s/4c6m0f2e28a1vxf/avoid-clashes-code.zip?dl=0

以下是一些替代建议(主要在awk中),包括一些高级用例,例如提取软件包的版本号。

请注意,如果输入略有不同,其中一些可能会失败,因此任何使用这些的人都应该验证他们的预期输入,并在需要时调整regex表达式。

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]}'# orecho "$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 '^[/]\|^~/' 

所有用例都使用原始完整路径作为输入,而不依赖于中间结果。

Petesh构建答案,如果只需要文件名,路径和扩展都可以在一行中剥离,

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

这是一个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:l1s/^([^/]|$)//ts/[/]+$//t l2:l2s/^$/filesystem\/\filesystem/pths/^(.*)([/])([^/]+)$/\1\2\\1\\3/pgt l3:l3s/^.*[/]([^/]+)([.])([a-zA-Z0-9]+)$/\1\\2\3\\3/pts/^.*[/](.+)$/\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          =

以下是它的工作原理:

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

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

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

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

以下是#0构造的工作原理:

  • #0#1去掉任何前导和/或尾随空格字符
  • #0#1为下一个s函数刷新t函数
  • #0#1测试无效的输入路径(不以正斜杠开头的路径),在这种情况下,它将所有输出行留空并退出sed命令
  • #0去掉任何拖尾斜杠
  • #0#1为下一个s函数刷新t函数
  • #0#1测试输入路径由根目录//组成的特殊情况,在这种情况下,它为斜线路径描述diPath输出行打印文件系统/文件系统,将所有其他输出行留空,并退出ed命令
  • #0将输入路径保存在保持空间中
  • #0打印斜线路径描述diPath外部文件名输出行
  • #0从保持空间中检索输入路径
  • #0#1为下一个s函数刷新t函数

将路径名路径拆分成一对(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' ||---------------|-----------|-------|

测试结果

所有测试都通过了。

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

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

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

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

第一行解释:它匹配PATH.EXT或任何东西,并将其替换为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

它是如何工作的?

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

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

如您所见,行为与basenamedirname不同。例如basename dir/输出dir,而正则表达式将为您提供一个空文件名。...也是如此,这些被视为目录,而不是文件名。

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

备注:没有必要测试包含\n或其他臭名昭著的字符的路径,因为所有字符都由bash的正则表达式引擎以相同的方式处理。能够破坏当前逻辑的唯一字符是/.,它们以意想不到的方式混合或乘以目前。当我第一次发布我的答案时,我发现了一些我必须修复的边界情况;我不能说正则表达式是100%防弹的,但现在应该相当健壮了。


顺便说一句,这是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-nullfilename=${fullname#.}filename="${fullname%"$filename"}${filename%.*}"filext=${fullname#"$filename"};;esac
################################
printf '%s=%s\n' \path     "$path" \dirpath  "$dirpath" \filename "$filename" \filext   "$filext"
done

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

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

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

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