在bash中提取文件名(不包括路径和扩展名)

454

假设有以下文件名:

/the/path/foo.txt
bar.txt

我希望得到:

foo
bar

为什么这个不起作用?

#!/bin/bash

fullfile=$1
fname=$(basename $fullfile)
fbname=${fname%.*}
echo $fbname

怎样才是正确的做法?


1
只要 $1 的值确实是您所认为的值,该代码 在大多数情况下 应该能够正常工作。但是,由于引号使用不当,它会受到 单词拆分文件名扩展 的影响。 - toxalot
1
Closer @tripleee:这不是在Bash中提取文件名和扩展名的重复。另一个更复杂的问题需要将扩展名和文件名分开。如果有什么区别,那么另一个问题是对这个更基本问题的阐述。 - agc
@agc 这些答案看起来非常相似,但是旧问题的回答更多。您能解释一下为什么您认为这些应该保持分开吗? - tripleee
1
@tripleee 对于那些只关心简单问题的用户来说,为了解决更复杂的问题所需的额外或不同的代码可能会让他们感到困惑。 - agc
你看过其他问题的答案吗?在前五个答案中,有四个非常明显地展示了如何做到这一点,并解释了选项,其中大部分都相当简洁。如果你能指出实际上的差异,我很乐意被说服,但我没有看到它们。也许可以提到[meta]以获得更广泛的可见性。 - tripleee
显示剩余2条评论
9个回答

814

你不必调用外部的basename命令。相反,你可以使用以下命令:

$ s=/the/path/foo.txt
$ echo "${s##*/}"
foo.txt
$ s=${s##*/}
$ echo "${s%.txt}"
foo
$ echo "${s%.*}"
foo
请注意,这个解决方案应该适用于所有近期(2004年以后)的POSIX兼容的shell,比如bashdashksh等。
来源:Shell命令语言2.6.2参数扩展 更多有关bash字符串操作的信息,请参见http://tldp.org/LDP/LG/issue18/bash.html

70
很棒的回答...这里解释了Bash字符串操作(如${s##*/})的内容,网址为http://linuxgazette.net/18/bash.html。 - chim
7
@chim,你有没有找到更新的链接参考?它已经失效了。 - yurisich
30
@Droogans经过一番搜索找到了它 :) http://www.tldp.org/LDP/LG/issue18/bash.html 我没意识到这条评论已经有27个赞了 :) - chim
40
有没有办法通过嵌套、管道或其他方式将 ##*/%.* 结合起来,直接得到 foo? - bongbang
7
文档也可通过 man -P "less -p '参数扩展'" bash 获取,该文档将涵盖参数扩展的相关内容。 - Mark Maglana
显示剩余14条评论

371

basename命令有两种不同的用法;其中一种是只指定路径,这时它会返回路径的最后一个组成部分;而另一种用法中您还可以指定要删除的后缀。因此,您可以通过使用basename的第二种用法来简化您的示例代码。此外,请注意正确地引用内容:

fbname=$(basename "$1" .txt)
echo "$fbname"

21
有没有办法使它删除任何后缀? - w4etwetewtwet
4
抱歉,basename 不支持通配符。提供第二个参数只能从末尾删除确切的文字字符串。 - toxalot
3
在基于 8.4 的机器上,它按照这个答案的规定正常工作,但对于我来说(使用 GNU coreutils 中的基于 8.22 的 basename),这将按照 basename -s <extension> <filename> 的方式工作。 - Seldom 'Where's Monica' Needy
3
您可以使用basename命令移除文件扩展名,请参考https://dev59.com/vnE85IYBdhLWcg3wqleE#36341390。 - sancho.s ReinstateMonicaCellio
@sancho.sReinstateMonicaCellio 不是basename在移除扩展名,而是bash的扩展功能。这与@ghostdog74的解决方案是一样的。使用basename单独是不可能移除任何扩展名的。 - undefined
显示剩余2条评论

84
结合使用basename和cut指令可以很好地处理带有双重后缀(如.tar.gz)的情况:
fbname=$(basename "$fullfile" | cut -d. -f1)

这个解决方案是否比Bash参数扩展需要更少的算术能力,这很有趣。

4
这是我首选的方式——唯一的改变就是使用 $(..)——因此代码如下:fbname=$(basename "$fullfile" | cut -d. -f1) - FarmerGedden
这是一个不错的解决方案,因为它将剪切所有(点)扩展名。 - user2023370
7
如果文件名中有其他点,使用原来的方法会截断文件名。这个新的方法可能更加可靠:fbname=$(basename "$fullfile" | sed -r 's|^(.*?)\.\w+$|\1|')。还有其他选择:'s|^(.*?)\..+$|\1|''s|^(.*?)\.[^\.]+$|\1|' - Pysis
真的非常感谢你提供的真实而有用的答案! - undefined

52
以下是一些简洁的代码片段:
  1. $(basename "${s%.*}")
  2. $(basename "${s}" ".${s##*.}")
我需要这个,就像bongbang和w4etwetewtwet所问的一样。
示例:
$ s=/the/path/foo.txt
$ echo "$(basename "${s%.*}")"
foo
$ echo "$(basename "${s}" ".${s##*.}")"
foo

27

纯粹的 bash,没有 basename,没有变量操纵。设置一个字符串并使用 echo 命令:

Pure bash, no basename, no variable juggling. Set a string and echo:

p=/the/path/foo.txt
echo "${p//+(*\/|.*)}"

输出:

foo

注意:必须开启 bashextglob 选项(Ubuntu 默认已开启 extglob),否则请执行以下命令:

shopt -s extglob
通过${p//+(*\/|.*)}步骤:
  1. ${p -- 从$p开始。
  2. // 替换所有遵循的模式实例。
  3. +( 匹配圆括号中的一个或多个模式列表,(例如 直到下面的第7项)。
  4. 第1个 模式:*\/匹配斜杠字符"/"之前的任何内容。
  5. 模式分隔符| 在这种情况下充当逻辑或
  6. 第2个 模式: .* 匹配字面上的"."之后的任何内容--也就是说,在bash中,"."只是一个句点字符,而不是正则表达式中的句点
  7. ) 结束模式列表
  8. } 结束参数扩展。在字符串替换中,通常会有另一个/,然后是一个替换字符串。但是由于这里没有/,因此匹配的模式将被替换为“nothing”,从而删除了匹配项。

相关的man bash背景知识:

  1. 模式替换:
  ${parameter/pattern/string}
          Pattern substitution.  The pattern is expanded to produce a pat
          tern just as in pathname expansion.  Parameter is  expanded  and
          the  longest match of pattern against its value is replaced with
          string.  If pattern begins with /, all matches  of  pattern  are
          replaced   with  string.   Normally  only  the  first  match  is
          replaced.  If pattern begins with #, it must match at the begin‐
          ning of the expanded value of parameter.  If pattern begins with
          %, it must match at the end of the expanded value of  parameter.
          If string is null, matches of pattern are deleted and the / fol
          lowing pattern may be omitted.  If parameter is @ or *, the sub
          stitution  operation  is applied to each positional parameter in
          turn, and the expansion is the resultant list.  If parameter  is
          an  array  variable  subscripted  with  @ or *, the substitution
          operation is applied to each member of the array  in  turn,  and
          the expansion is the resultant list.
  1. 扩展模式匹配:
  If the extglob shell option is enabled using the shopt builtin, several
   extended  pattern  matching operators are recognized.  In the following
   description, a pattern-list is a list of one or more patterns separated
   by a |.  Composite patterns may be formed using one or more of the fol
   lowing sub-patterns:

          ?(pattern-list)
                 Matches zero or one occurrence of the given patterns
          *(pattern-list)
                 Matches zero or more occurrences of the given patterns
          +(pattern-list)
                 Matches one or more occurrences of the given patterns
          @(pattern-list)
                 Matches one of the given patterns
          !(pattern-list)
                 Matches anything except one of the given patterns

13

以下是另一种获取文件名或扩展名的较为复杂的方法。首先使用rev命令来倒转文件路径,然后从第一个.处进行切割,最后再次倒转文件路径即可,如下:

filename=`rev <<< "$1" | cut -d"." -f2- | rev`
fileext=`rev <<< "$1" | cut -d"." -f1 | rev`

1
我从未见过那些三角括号 <<< --这是什么? - Alex Hall
3
它们被称为“Here Strings”(更多信息请参见[此处](http://tldp.org/LDP/abs/html/x17837.html)),基本上将输入作为字符串提供,并将其传递给您的程序,就好像正在通过stdin读取一样。 - higuaro

2

如果您想在Cygwin下与Windows文件路径相配合,您也可以尝试以下方法:

fname=${fullfile##*[/|\\]}

这将考虑在Windows上使用BaSH时的反斜杠分隔符。

1

我提出了一种替代方法来提取文件扩展名,使用本帖中的帖子和我自己更熟悉的小型知识库。

ext="$(rev <<< "$(cut -f "1" -d "." <<< "$(rev <<< "file.docx")")")"

注意:请就我的引号使用提供建议;对我来说它起作用了,但我可能在正确使用它们方面遗漏了一些东西(我可能使用了太多)。

1
你不需要单引号,你可以尝试做与 rev <<< file.docx | cut -f1 -d. | rev 相同的事情,只是没有引号和嵌套子 shell。此外,我认为上述内容根本无法工作。 - matthias krull

-16

11
他正在使用basename,但这不是他的问题。 - chepner

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