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

我想分别获取文件名(不带扩展名)和扩展名。

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

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

这是错误的,因为如果文件名包含多个,则无法使用.字符。如果说我有abjs ,它将考虑ab.js ,而不是abjs

可以使用 Python 轻松完成

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

但如果可能的话,我不希望为此而启动 Python 解释器。

还有更好的主意吗?

答案

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

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

或者,您可以将焦点放在路径的最后一个 “/” 而不是 “。”。即使您具有无法预测的文件扩展名,它也应能正常工作:

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

您可能需要检查文档:

~% FILE="example.tar.gz"

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

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

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

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

有关更多详细信息,请参见 Bash 手册中的shell 参数扩展

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

basename filename .extension

例如:

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

我们得到

filename

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

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

需要注意的是,如果文件名的格式为./somefile.tar.gzecho ${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

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

#!/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  = ""

您可以使用basename

例:

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

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

这应该做您想要的:

tar -zxvf $1
cd $(basename $1 .tar.gz)
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的命令将替换任意数量的字符,后跟"."行开头的字符,没有任何内容(即,它将删除从行开头到最后一个点的所有内容)。这是一个贪婪的替换,这是默认操作。

梅伦(Mellen)在博客文章中发表评论:

使用 Bash,还有${file%.*}来获取不带扩展名的文件名,还有${file##*.}来获取扩展名。那是,

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

输出:

filename: thisfile
extension: txt

无需为这个简单的任务而烦恼awksed甚至perl 。有一个纯 Bash 的os.path.splitext()兼容解决方案,仅使用参数扩展。

参考实施

os.path.splitext(path)文档:

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

检测结果

所有测试均通过。

您可以使用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)的评论后再次更新]