如何在shell脚本中删除文件名的扩展名?

274

以下代码有什么问题?

name='$filename | cut -f1 -d'.''

目前我得到的是字面字符串$filename | cut -f1 -d'.',但如果我去掉引号就什么都得不到了。同时,输入

"test.exe" | cut -f1 -d'.'

在shell中可以得到我想要的输出结果test。我已经知道$filename已被指定了正确的值。我想做的是将文件名(不包括扩展名)分配给一个变量。


18
假设您始终知道要删除的文件扩展名,那么 basename $filename .exe 将执行相同的操作。 - mpe
13
@mpe,你的意思是basename "$filename" .exe。否则有空格的文件名会带来麻烦。 - Charles Duffy
请参阅在Bash中提取子字符串,其中也解释了"${file#prestring}" - Aron Hoogeveen
16个回答

478

您还可以使用参数扩展:

$ filename=foo.txt
$ echo "${filename%.*}"
foo

如果您有一个文件路径而不仅仅是一个文件,则需要先使用basename获取仅包括扩展名的文件名。否则,如果路径中只有一个点(例如path.to/myfile./myfile),则它将在路径内部修剪;即使路径中没有点,它也会获取(例如,如果路径为path/to/myfile.txt,则会获取path/to/myfile):

$ filepath=path.to/foo.txt
$ echo "${filepath%.*}"
path.to/foo
$ filename=$(basename $filepath)
$ echo $filename
foo.txt
$ echo "${filename%.*}"
foo

请注意,如果文件名只以点开头(例如.bashrc),它将删除整个文件名。


12
以下是该命令的说明文档链接:https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html - Carlos Robles
3
我本来打算使用 echo -n "This.File.Has.Periods.In.It.txt" | awk -F. '{$NF=""; print $0}' | tr ' ' '.' | rev | cut -c 2- | rev。谢谢。 - user208145
3
这是否适用于具有多个扩展名的文件,例如 image.png.gz - Hawker65
21
%.* 只会移除文件名中的最后一个扩展名;如果你想要移除所有扩展名,应该使用 %%.* - chepner
5
请注意,如果路径是相对路径且没有文件后缀名,执行此操作将删除整个文件名。例如,filename=./ihavenoextension - jpmc26
显示剩余6条评论

190
如果您知道文件扩展名,可以使用basename函数。
$ basename /home/jsmith/base.wiki .wiki
base

5
我应该如何移除.wiki并得到/home/jsmith/base呢? - Gabriel Staples
更新:答案:请参考https://dev59.com/42ct5IYBdhLWcg3wWMJF#32584935。完美运行! - Gabriel Staples
1
在我看来,这应该是被接受的答案。 - Nam G VU
1
@GabrielStaples 你可以将它与 dirname 结合使用,例如 file=/home/jsmith/base.wiki; echo $(dirname $file)/$(basename $file .wiki) - jena
1
我选择了这个。好处是basename专门为此而设计,我发现它可以做更多的事情。 - JL Peyret

177

当你想在脚本/命令中执行一个命令时,应该使用命令替换语法$(command)

因此,你的行应该是

name=$(echo "$filename" | cut -f 1 -d '.')

代码说明:

  1. echo 获取变量 $filename 的值并将其发送到标准输出
  2. 然后我们获取输出并将其管道传输到 cut 命令
  3. cut 将使用点号(也称为分隔符)作为定界符,将字符串切割成段,并通过 -f 选择要在输出中显示的段
  4. 然后 $() 命令替换将获取输出并返回其值
  5. 返回的值将被分配给名为 name 的变量

请注意,这会给出变量的部分,直到第一个句点 .

$ filename=hello.world
$ echo "$filename" | cut -f 1 -d '.'
hello
$ filename=hello.hello.hello
$ echo "$filename" | cut -f 1 -d '.'
hello
$ filename=hello
$ echo "$filename" | cut -f 1 -d '.'
hello

20
反引号已被POSIX弃用,建议使用 $() 代替。 - jordanm
7
使用分支和管道来获取一些字符是最糟糕的解决方案。 - Jens
54
这个答案存在问题,它假设输入字符串只有一个点...下面的@chepner提供了一个更好的解决方案...将文件名存储在name变量中,不包括最后一个点及其后面的内容。 - Scott Stensland
8
这个回答是初学者的典型错误,不应该被传播。请按照chepner的回答所描述的使用内置机制。 - neric
2
与其他答案一样,在“./ihavenoextension”上失败。 - jpmc26
显示剩余5条评论

50

如果您的文件名包含一个点(不是扩展名中的那个点),请使用以下方法:

echo $filename | rev | cut -f 2- -d '.' | rev

1
我忘记了中间的修订,但一旦我看到它,这就很棒了! - supreme Pooba
4
在我看来,这应该成为被接受的答案,因为它适用于路径中包含点的情况,适用于以点开头的隐藏文件,甚至适用于具有多个扩展名的文件。 - Tim Krief
2
这样的文件会发生什么情况:filename=/tmp.d/foo - FedKad
2
它也有“点文件”问题。例如,尝试使用.profile - FedKad
@FedonKadifeli 可以使用类似这样的代码:basename "$INPUT" | rev | cut -f 2- -d '.' | rev,它可以完美地工作。 $INPUT 是文件名或路径。 - donvercety
显示剩余3条评论

41

只使用 POSIX 内置的功能:

#!/usr/bin/env sh
path=this.path/with.dots/in.path.name/filename.tar.gz

# Get the basedir without external command
# by stripping out shortest trailing match of / followed by anything
dirname=${path%/*}

# Get the basename without external command
# by stripping out longest leading match of anything followed by /
basename=${path##*/}

# Strip uptmost trailing extension only
# by stripping out shortest trailing match of dot followed by anything
oneextless=${basename%.*}; echo "$oneextless" 

# Strip all extensions
# by stripping out longest trailing match of dot followed by anything
noext=${basename%%.*}; echo "$noext"

# Printout demo
printf %s\\n "$path" "$dirname" "$basename" "$oneextless" "$noext"

打印演示:

this.path/with.dots/in.path.name/filename.tar.gz
this.path/with.dots/in.path.name
filename.tar.gz
filename.tar
filename

/home/user/.profile 这样的 path 会发生什么? - FedKad
1
@FedKad 你怎么测试一下呢? - Léa Gris
对于像/home/user/.profile这样的path,变量$oneextless$noext将变为空字符串;换句话说,“点文件”名称消失了。 - FedKad

28
file1=/tmp/main.one.two.sh
t=$(basename "$file1")                        # output is main.one.two.sh
name=$(echo "$file1" | sed -e 's/\.[^.]*$//') # output is /tmp/main.one.two
name=$(echo "$t" | sed -e 's/\.[^.]*$//')     # output is main.one.two

使用任何您想要的。在这里,我假设最后一个跟随文本的 .(点)是扩展名。


file1=/tmp.d/mainonetwosh 时会发生什么?sed表达式应该被替换为 's/\.[^./]*$//' - FedKad

8
#!/bin/bash
file=/tmp/foo.bar.gz
echo $file ${file%.*}

输出:

/tmp/foo.bar.gz /tmp/foo.bar

请注意,仅删除最后一个扩展名。

3
这样的文件file=/tmp.d/foo会发生什么? - FedKad

4
在Zsh中:
fullname=bridge.zip
echo ${fullname:r}

这很简单,干净利落且可以链接到一起以删除多个扩展名:

fullname=bridge.tar.gz
echo ${fullname:r:r}

它可以与其他类似的修饰符组合使用。


3
#!/bin/bash
filename=program.c
name=$(basename "$filename" .c)
echo "$name"

输出:

program

1
这与三年前Steven Penny给出的答案有何不同? - gniourf_gniourf
@gniourf_gniourf 点赞是因为你通过展示如何正确地使用变量使其成为一个有用的答案。 - mwfearnley

3
我的建议是使用basename。它在Ubuntu中默认存在,代码简单易懂,能处理大多数情况。
以下是一些特殊情况,需要处理空格和多个点/子扩展名:
pathfile="../space fld/space -file.tar.gz"
echo ${pathfile//+(*\/|.*)}

通常会从第一个 . 处去除扩展名,但在我们的 .. 路径中失败了。
echo **"$(basename "${pathfile%.*}")"**  
space -file.tar     # I believe we needed exatly that

这里有一个重要的提示:

我在双引号内使用了双引号来处理空格。 由于文本中包含 $,单引号将无法通过。 Bash 不同寻常地读取“second "first" quotes”以进行扩展。

但是,你仍然需要考虑 .hidden_files

hidden="~/.bashrc"
echo "$(basename "${hidden%.*}")"  # will produce "~" !!!  

结果并非预期的 ""。为了让它发生,请使用 $HOME/home/user_path/
因为 bash 是 "不寻常的",不会扩展 "~"(查找 bash BashPitfalls)。

hidden2="$HOME/.bashrc" ;  echo '$(basename "${pathfile%.*}")'

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