如何在Bash中从路径字符串中删除文件后缀名和路径部分?

531

给定一个字符串文件路径,例如 /foo/fizzbuzz.bar,如何使用 bash 提取该字符串中的 fizzbuzz 部分?


你可以在Bash手册中找到相关信息,查看“${parameter%word}”和“${parameter%%word}”在尾部匹配部分的内容。 - 1ac0
15个回答

759

以下是如何在Bash中使用 # 和 % 运算符进行操作。

$ x="/foo/fizzbuzz.bar"
$ y=${x%.bar}
$ echo ${y##*/}
fizzbuzz

${x%.bar}也可以写成${x%.*},用于删除点号之后的所有内容,或者使用${x%%.*}来删除第一个点号之后的所有内容。

示例:

$ x="/foo/fizzbuzz.bar.quux"
$ y=${x%.*}
$ echo $y
/foo/fizzbuzz.bar
$ y=${x%%.*}
$ echo $y
/foo/fizzbuzz

参考Bash手册,查找${parameter%word}${parameter%%word}匹配末尾部分的章节。


7
这个叫做${x%.bar}是什么? 我想了解更多相关信息。 - Basil
17
@Basil: 参数展开。在终端上输入"man bash",然后输入"/parameter expansion"。 - Zan Lynx
1
如果你已经知道它是什么或者自己尝试过,那么“man bash”的解释可能是有意义的。这几乎和git参考一样糟糕。我建议直接在谷歌上搜索。 - triplebig
有没有不使用 x 变量的方法来实现这个?如果我尝试 echo ${"/foo/fizzbuzz.bar.quux"%.*},会得到 -bash: ${"/foo/fizzbuzz.bar.quux"%.*}: bad substitution 的错误提示。 - Alec Jacobson
@AlecJacobson 在元编程中,这甚至更加愚蠢,因为生成器程序应该首先处理它,然后将其写成正确的字面字符串。如果它不能是字面常量,那么它就是一个变量 - Zan Lynx
显示剩余7条评论

330

看一下basename命令:

NAME="$(basename /foo/fizzbuzz.bar .bar)"

指示它删除后缀.bar,导致NAME=fizzbuzz


13
目前提供的解决方案中,可能是最简单的...尽管我会使用$(...)而不是反引号。 - Michael Johnson
7
最简单的方法是增加一个依赖项(我承认并不是很大或奇怪)。还需要知道后缀。 - Vinko Vrsalovic
4
问题在于时间开销。在使用basename处理800个文件时,我几乎花费了5分钟的时间,之后观看了本次讨论的问题并进行了搜索。使用以上提到的正则表达式方法,时间缩短到了约7秒。虽然对于程序员来说这个答案更容易执行,但时间开销实在太大了。想象一下一个包含几千个文件的文件夹!我有一些这样的文件夹。 - xizdaqrian
3
@xizdaqrian 这是绝对错误的。这是一个简单的程序,不应该需要半秒钟就能返回结果。我刚刚执行了 time find /home/me/dev -name "*.py" .py -exec basename {} ; 命令,它在总共1秒钟内就为1500个文件去掉了扩展名和目录。 - Laszlo Treszkai
2
尽可能避免使用外部进程的一般想法是正确的,这也是Shell编程的基本原则。 - tripleee
显示剩余3条评论

65

纯Bash实现,分为两个独立的操作:

  1. 从路径字符串中删除路径:

path=/foo/bar/bim/baz/file.gif

file=${path##*/}  
#$file is now 'file.gif'
  • 从路径字符串中删除扩展名:

    base=${file%.*}
    #${base} is now 'file'.
    

  • 20

    使用basename,我使用以下代码来实现这个目标:

    for file in *; do
        ext=${file##*.}
        fname=`basename $file $ext`
    
        # Do things with $fname
    done;
    

    这不需要先验的文件扩展名知识,即使您有一个文件名中有点的文件名(在其扩展名之前),它也可以工作;但是它确实需要程序basename,但这是GNU核心实用程序的一部分,因此它应该随任何发行版一起提供。


    1
    非常好的答案!以非常干净的方式删除了扩展名,但它没有删除文件名末尾的“.”。 - metrix
    4
    @metrix 只需在 $ext 前添加 ".",即:fname=\basename $file .$ext`` - Carlos Troncoso
    如果文件名中有空格,这可能会导致一些问题。你应该将 $file$ext 和反引号部分(包括反引号本身)用双引号括起来。 - mwfearnley
    踩个负评。这对我来说根本不起作用。它会在我的文件名中插入垃圾换行符,并且不会删除扩展名。根本无法使用。 - John Smith

    19

    basename 和 dirname 函数是你需要的:

    mystring=/foo/fizzbuzz.bar
    echo basename: $(basename "${mystring}")
    echo basename + remove .bar: $(basename "${mystring}" .bar)
    echo dirname: $(dirname "${mystring}")
    

    有输出:

    basename: fizzbuzz.bar
    basename + remove .bar: fizzbuzz
    dirname: /foo
    

    2
    修复引用可能会有所帮助 - 也许可以通过使用mystring=$1在http://shellcheck.net/上运行而不是当前的常量值(这将抑制几个警告,确保不包含空格/ glob字符/等),并解决它发现的问题? - Charles Duffy
    1
    我对 $mystring 进行了一些适当的更改,以支持引号。天哪,这是我很久以前写的 :) - Jerub
    1
    进一步改进的方法是引用结果:echo“basename:$(basename”$ mystring“)” - 这样,如果 mystring ='/ foo / *',则在 basename 完成后,您不会将 * 替换为当前目录中的文件列表。 - Charles Duffy

    14

    纯 bash 方式:

    ~$ x="/foo/bar/fizzbuzz.bar.quux.zoom"; 
    ~$ y=${x/\/*\//}; 
    ~$ echo ${y/.*/}; 
    fizzbuzz
    

    这个功能在“参数扩展”下的bash手册中有解释。也可使用其他非bash的方法,如awk、perl、sed等。

    编辑:适用于文件后缀名中包含点号,并且不需要知道后缀(扩展名),但是对于名称本身中包含点号则不起作用


    7

    使用basename假设您知道文件扩展名,是吗?

    我认为各种正则表达式建议不能处理文件名包含多个“。”的情况。

    以下似乎可以处理双点。哦,还有包含“/”本身的文件名(只是为了好玩)

    用Pascal的话来说,“对不起,这个脚本太长了。我没时间把它缩短”

    
      #!/usr/bin/perl
      $fullname = $ARGV[0];
      ($path,$name) = $fullname =~ /^(.*[^\\]\/)*(.*)$/;
      ($basename,$extension) = $name =~ /^(.*)(\.[^.]*)$/;
      print $basename . "\n";
     

    1
    这很好而且健壮。 - Gaurav Jain

    6

    除了POSIX兼容的语法,在这个答案中使用的语法外,

    basename <i>string</i> [<i>suffix</i>]

    如同

    basename /foo/fizzbuzz.bar .bar
    

    basename命令支持另一种语法:

    basename -s .bar /foo/fizzbuzz.bar
    

    使用相同的结果。不同之处和优势在于-s暗示了-a,支持多个参数:

    $ basename -s .bar /foo/fizzbuzz.bar /baz/foobar.bar
    fizzbuzz
    foobar
    

    使用-z选项,可以通过使用NUL字节将输出分隔开来,使其成为适合文件名的内容。例如,对于包含空格、换行符和通配符字符(由ls引用)的这些文件。
    $ ls has*
    'has'$'\n''newline.bar'  'has space.bar'  'has*.bar'
    

    将内容读入数组中:

    $ readarray -d $'\0' arr < <(basename -zs .bar has*)
    $ declare -p arr
    declare -a arr=([0]=$'has\nnewline' [1]="has space" [2]="has*")
    

    readarray -d 命令需要 Bash 4.4 或更高版本。对于旧版本,我们需要使用循环:

    while IFS= read -r -d '' fname; do arr+=("$fname"); done < <(basename -zs .bar has*)
    

    此外,如果指定的后缀存在于输出中,则将其删除(否则将被忽略)。 - aksh1618

    4

    如果您不能像其他帖子中建议的那样使用basename,您始终可以使用sed。这是一个(丑陋的)示例。它不是最好的,但它通过提取所需的字符串并将输入替换为所需的字符串来工作。

    echo '/foo/fizzbuzz.bar' | sed 's|.*\/\([^\.]*\)\(\..*\)$|\1|g'
    

    这将为您提供输出

    fizzbuzz


    虽然这是对原始问题的答案,但当我有一些路径行需要从文件中提取基本名称并将它们打印到屏幕上时,这个命令非常有用。 - Sangcheol Choi

    3
    perl -pe 's/\..*$//;s{^.*/}{}'
    

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接