在 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 解释器。

有更好的想法吗?

答1:

保持自己快人一步,享受全网独家提供的一站式外包任务、远程工作、创意产品订阅服务–huntsbot.com

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

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

或者,您可以关注路径的最后一个“/”而不是“。”即使您有不可预测的文件扩展名,它也应该工作:

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

您可能需要查看文档:

在网络上的“3.5.3 Shell 参数扩展”部分

在名为“参数扩展”部分的 bash 手册页中

查看 gnu.org/software/bash/manual/html_node/… 了解完整的功能集。

在 "$fullfile" 中添加一些引号,否则您将面临破坏文件名的风险。

哎呀,您甚至可以编写 filename="${fullfile##*/}" 并避免调用额外的 basename

如果文件没有扩展名,则此“解决方案”不起作用-相反,会输出整个文件名,考虑到无扩展名的文件无处不在,这非常糟糕。

修复处理不带扩展名的文件名:extension=$([[ "$filename" = *.* ]] && echo ".${filename##*.}" || echo '')。请注意,如果存在扩展,则将返回包括初始.,例如.txt。

答2:

保持自己快人一步,享受全网独家提供的一站式外包任务、远程工作、创意产品订阅服务–huntsbot.com

~% FILE="example.tar.gz"

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

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

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

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

有关详细信息,请参阅 Bash 手册中的 shell parameter expansion。

您(也许是无意的)提出了一个很好的问题,即如果文件名的“扩展名”部分中有 2 个点,例如 .tar.gz,该怎么办......我从来没有考虑过这个问题,我怀疑它是如果不预先知道所有可能的有效文件扩展名,则无法解决。

为什么不能解决?在我的示例中,应该认为该文件包含两个扩展名,而不是带有两个点的扩展名。您分别处理这两个扩展。

在词汇基础上无法解决,您需要检查文件类型。考虑一下您是否有一个名为 dinosaurs.in.tar 的游戏并将其压缩到 dinosaurs.in.tar.gz :)

如果您在完整路径中传递,这会变得更加复杂。我的一个有一个'。在路径中间的目录中,但文件名中没有。示例“a/bc/d/e/filename”将结束“.c/d/e/filename”

显然没有 x.tar.gz 的扩展名是 gz,文件名是 x.tar 就是这样。没有双重扩展之类的东西。我很确定 boost::filesystem 就是这样处理的。 (分割路径,change_extension ...)如果我没记错的话,它的行为是基于python的。

答3:

huntsbot.com精选全球7大洲远程工作机会,涵盖各领域,帮助想要远程工作的数字游民们能更精准、更高效的找到对方。

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

basename filename .extension

例如:

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

我们得到

filename

basename 的第二个论点令人大开眼界,亲切的先生/女士 :)

以及如何使用这种技术提取扩展名? ;) 等一下!我们实际上并不事先知道。

假设您有一个以 .zip 或 .ZIP 结尾的压缩目录。有没有办法可以做类似 basename $file {.zip,.ZIP} 的事情?

虽然这只回答了部分 OPs 问题,但它确实回答了我输入谷歌的问题。 :-) 非常漂亮!

简单且符合 POSIX

答4:

huntsbot.com汇聚了国内外优秀的初创产品创意,可按收入、分类等筛选,希望这些产品与实践经验能给您带来灵感。

您可以使用 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%%.*}

)

此site解释更多。

${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

比 Joachim 的答案简单得多,但我总是要查找 POSIX 变量替换。此外,这在 Max OSX 上运行,其中 cut 没有 --complement 并且 sed 没有 -r。

答5:

huntsbot.com聚合了超过10+全球外包任务平台的外包需求,寻找外包任务与机会变的简单与高效。

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

#!/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%$filename}" 而不是 dir="${fullpath:0:${#fullpath} - ${#filename}}"。写起来更简单。不确定是否有任何真正的速度差异或陷阱。

这使用 #!/bin/bash 这几乎总是错误的。如果可能,首选 #!/bin/sh 或 #!/usr/bin/env bash 如果不是。

@Good Person:我不知道这几乎总是错误的:which bash -> /bin/bash ;也许这是你的发行版?

@vol7ron - 在许多发行版中,bash 位于 /usr/local/bin/bash 中。在 OSX 上,许多人在 /opt/local/bin/bash 中安装更新的 bash。因此 /bin/bash 是错误的,应该使用 env 来找到它。更好的是使用 /bin/sh 和 POSIX 结构。除了在 solaris 上,这是一个 POSIX shell。

@GoodPerson,但如果您更喜欢 bash,为什么要使用 sh?这不是说,既然可以使用 sh,为什么还要使用 Perl?

答6:

huntsbot.com高效搞钱,一站式跟进超10+任务平台外包需求

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

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

对于没有扩展名的文件,此中断会打印相同的名称和扩展名。所以我使用 sed 's,\.[^\.]*$,,' 作为名称,使用 sed 's,.*\.,., ;t ;g' 作为扩展名(使用非典型的 test 和 get 命令,以及典型的 substitute 命令)。

您可以在计算 NAME 之后测试它和 FILE 是否相等,如果相等,请将 EXTENSION 设置为空字符串。

从根本上说,将外部进程用于 shell 可以自己做的事情是一种反模式。

Tripleee:shell 可以在一百行内完成很多事情,而像 awk 这样的外部进程可以在五行内完成 :-)

答7:

与HuntsBot一起,探索全球自由职业机会–huntsbot.com

您可以使用 basename。

例子:

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

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

这应该做你想要的:

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

我想 cd $(basename $1 .tar.gz) 适用于 .gz 文件。但在问题中他提到了Archive files have several extensions: tar.gz, tat.xz, tar.bz2

Tomi Po 在 2 年前发布了同样的内容。

嗨 Blauhirn,哇,这是一个老问题。我认为日期发生了一些事情。我清楚地记得在被问到这个问题后不久就回答了这个问题,而且那里只有几个其他答案。难道是这个问题与另一个问题合并了,这样做吗?

是的,我没记错。我最初在被问到的同一天回答了这个问题 stackoverflow.com/questions/14703318/…,2 年后它被合并到这个问题中。当我的答案以这种方式移动时,我几乎不能因为重复的答案而受到指责。

答8:

huntsbot.com全球7大洲远程工作机会,探索不一样的工作方式

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

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

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

输出:

filename: thisfile
extension: txt

@REACHUS:见 gnu.org/software/bash/manual/html_node/…

答9:

huntsbot.com高效搞钱,一站式跟进超10+任务平台外包需求

对于这个简单的任务,无需费心使用 awk 或 sed 甚至 perl。有一个纯 Bash、os.path.splitext() 兼容的解决方案,它只使用参数扩展。

参考实现

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

将路径名路径拆分为一对 (root, ext),使得 root + ext == path,并且 ext 为空或以句点开头并且最多包含一个句点。基本名称上的前导句点被忽略; 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

@frederick99 正如我所说,这里的解决方案与 Python 中 os.path.splitext 的实现相匹配。对于可能有争议的输入,该实现是否合理是另一个话题。

模式 ("$root") 中的引号是如何工作的?如果它们被省略会发生什么? (我找不到有关此事的任何文档。)此外,这如何处理其中包含 * 或 ? 的文件名?

好的,测试表明引号使模式成为文字,即 * 和 ? 并不特殊。所以我的问题的两个部分互相回答。我是否正确,这没有记录?还是应该从引号通常禁用全局扩展这一事实来理解?

绝妙的答案!我将建议一个稍微简单的变体来计算根:root="${path#?}";root="${path::1}${root%.*}" - 然后继续进行相同的操作以提取扩展名。

答10:

huntsbot.com – 程序员副业首选,一站式外包任务、远程工作、创意产品分享订阅平台。

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

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

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

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

它通过无条件地删除最后两个(字母数字)扩展来工作。

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

这仅适用于文件名/路径不包含任何其他点的情况: echo "mpc-1.0.1.tar.gz" |切-d'。 --complement -f2- 产生“mpc-1”(仅由 . 分隔后的前 2 个字段。)

@ClaytonHughes你是对的,我应该更好地测试它。添加了另一个解决方案。

sed 表达式应使用 $ 来检查匹配的扩展名是否位于文件名的末尾。否则,像 i.like.tar.gz.files.tar.bz2 这样的文件名可能会产生意想不到的结果。

@AndersLindahl 如果扩展的顺序与 sed 链顺序相反,它仍然会。即使以 $ 结尾,像 mpc-1.0.1.tar.bz2.tar.gz 这样的文件名也会删除 .tar.gz 和 .tar.bz2。

$ echo "foo.tar.gz" |切-d'。 -f2- WITHOUT --complement 将第二个拆分项放到字符串的末尾 $ echo "foo.tar.gz" |切-d'。 -f2- tar.gz

答11:

huntsbot.com – 程序员副业首选,一站式外包任务、远程工作、创意产品分享订阅平台。

accepted answer 在典型 情况 中运行良好,但在边缘 情况下失败,即:

对于没有扩展名的文件名(在此答案的其余部分中称为后缀), extension=${filename##*.} 返回输入文件名而不是空字符串。

extension=${filename##*.} 不包含开头的 .,这与约定相反。盲目的前置。不适用于没有后缀的文件名。

盲目的前置。不适用于没有后缀的文件名。

如果输入文件名以 .并且不包含进一步的内容。字符(例如,.bash_profile) - 违反约定。


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

示例调用:

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

请注意,输入路径之后的参数是自由选择的,位置变量 names。 要跳过那些之前不感兴趣的变量,请指定 (使用一次性变量$) 或 ‘’;例如,要仅提取文件名根和扩展名,请使用 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

原文链接:https://www.huntsbot.com/qa/A8k3/extract-filename-and-extension-in-bash?lang=zh_CN&from=csdn

打造属于自己的副业,开启自由职业之旅,从huntsbot.com开始!

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值