如何在Bash中从字符串中删除最后n个字符?

460

我在一个Bash脚本中有一个字符串变量var:

echo $var
"some string.rtf"

我想删除这个字符串的最后四个字符,并将结果分配给一个新变量var2,以便:

echo $var2
"some string"

我该怎么做?


2
重复的在Bash中提取子字符串 - Håkon Hægland
1
如何将此代码转换为一行?export HOSTNAME=hostname; echo "/lfs/${HOSTNAME::-13}/0/brando9" - Charlie Parker
12个回答

432
你可以这样做(在bash v4或更高版本中):
#!/bin/bash

v="some string.rtf"

v2=${v::-4}

echo "$v --> $v2"

注意:macOS 默认使用 bash 3.x


96
这是bash的4及以上版本,但在早期版本中,您可以使用稍微冗长一些的${v::${#v}-4} - chepner
19
对我没有用,Bash 显示“Bad substitution”。 - Ivan Marjanovic
37
以某种原因,在zsh中${v::-4}会报错“zsh:期望闭括号”。但是@fredtantini下面的答案${v:0:-4}可以正常工作。 - Pierre D
20
出错了:-2:子字符串表达式 < 0。 - Edward
4
@Edward 我也遇到了这个问题,在我的情况下似乎是因为 macOS 不支持子字符串中的负数缩写(https://superuser.com/questions/1033273/bash-4-3-substring-negative-length-on-os-x)。那里的解决方案有效,但很丑陋:`v2=${v:0:$((${#v} - 4))}`。 - Toastrackenigma
显示剩余5条评论

316

要从字符串末尾删除四个字符,请使用${var%????}

要删除最后一个.以及之后的所有内容,请使用${var%.*}

有关参数扩展的更多信息,请参见Bash文档


23
只使用纯shell命令,适用于许多系统上的BASH 3.x版本,因为这些系统由于许可问题而拒绝实施BASH 4.x。 - David W.
2
这很酷,因为它对于较短的字符串具有强大的鲁棒性;索引偏移量变体则会失败。 - Raphael
1
适用于bash 3.2.25 - 最佳答案 - 正是我所寻找的。 - capser
1
非常好的答案。那么如何删除字符串前面的内容? - user2023370
4
@user2023370,查阅文档应该会发现有一个相应的参数替换${var#????} - tripleee
显示剩余3条评论

155

首先,在表达意图时通常最好是明确的。所以,如果您知道字符串以.rtf结尾,并且您想要删除它,您可以使用var2=${var%.rtf}。这种方法的一个潜在有用之处是,如果该字符串不以.rtf结尾,则根本不会被更改; var2将包含var的未修改副本。

如果您想要删除文件名后缀但并不知道或关心其确切内容,您可以使用var2=${var%.*}来删除从最后一个.开始的所有内容。或者,如果您只想保留第一个.之前的所有东西而不包括它,您可以使用var2=${var%%.*}。如果字符串中只有一个.,那么这两个选项具有相同的结果,但是如果可能存在多个,您可以选择从哪一端对字符串进行操作。另一方面,如果字符串中根本没有.,那么var2将再次成为var的未修改副本。

如果您真的想始终删除特定数量的字符,这里有一些选项。

您特别标记了这个问题与bash有关,因此我们首先介绍bash内置命令。其中已经使用最久的是我上面使用的相同后缀删除语法:var2=${var%????},用于删除四个字符。或者只有第一个字符是点时才删除四个字符,使用var2=${var%.???},这类似于var2=${var%.*},但只有在点后面的部分恰好为三个字符时才删除后缀。如您所见,要以这种方式计算字符数,您需要为每个未知字符删除一个问号,因此对于较长的子字符串长度,这种方法变得棘手。
在新版本的shell中的一种选项是子字符串提取:var2=${var:0:${#var}-4}。在这里,您可以将任何数字放在4的位置上,以删除不同数量的字符。 ${#var}被替换为字符串的长度,因此实际上是要求提取并保留从第一个字符(索引为0)开始的(length-4)个字符。使用此方法,您失去了仅在字符串匹配模式时进行更改的选项。只要字符串至少有四个字符,无论其实际值如何,复制都将包括除其最后四个字符之外的所有字符。您可以省略开始索引;它默认为0,因此您可以将其缩短为 var2=${var::${#var}-4}。实际上,较新版本的 bash(特别是 4+ 版本,这意味着随 MacOS 附带的版本无法工作)将负长度识别为要停止的字符索引,从字符串末尾开始计数。因此,在这些版本中,您也可以摆脱字符串长度表达式:var2=${var::-4}。如果字符串长度小于四个字符,则也会触发此解释,因为此时${#var}-4将是负数。例如,如果字符串有三个字符,则${var:0:${#var}-4}变成了${var:0:-1},只删除了最后一个字符。如果你并不使用 bash 而是其他的 POSIX 类型 shell,基于模式的后缀删除操作仍然适用——即使在旧版 dash 中,其中的基于索引的子字符串提取操作则不支持。Ksh 和 zsh 都支持子字符串提取,但需要显式的 0 开始索引;zsh 还支持负数结尾索引,而ksh需要长度表达式。请注意,zsh 从 1 开始索引数组,但如果你使用此兼容bash的语法,则对于字符串也是从 0 开始索引。但是,zsh 还允许您将标量参数视为字符数组,在这种情况下,子字符串语法使用基于 1 的计数,并将起始位置和(包括)终止位置放在以逗号分隔的方括号中:var2=$var[1,-5]。
当然,您可以运行一些实用程序来修改字符串,并通过命令替换捕获其输出,而不是使用内置的 shell 参数扩展。有几个可行的命令;其中之一是 var2=$(sed 's/.\{4\}$//' <<<"$var")。

这段程序相关的内容是:???? 是一把双刃剑,但它完美地用于打开 .git 存储库到 /branches 文件夹。__repo=https://github.com/me/something.git

REM 现在去检查 __repo 的分支。 ???? 以删除 .git

打开 "${__repo%????}/branches"
- granadaCoder
很高兴能够帮助,但更明确并使用${__repo%.git}可能会更清晰,因为您知道要删除哪四个字符。如果字符串不以.git结尾,则也不会删除任何内容,因此即使__repo的值由于某种原因不包括后缀,它也可以正常工作。 - Mark Reed
1
@mark reed cut -c -4 并不是你想象中的那样。它只会从字符串的第一个字符打印到第四个字符,这里只会简单地输出 some。你无法使用 cut 实现这一点。 - Atralb
谢谢,@Atralb。我可能测试了一个恰好有8个字符长的字符串。已更新。 - Mark Reed
如何将此代码转换为一行?export HOSTNAME=hostname; echo "/lfs/${HOSTNAME::-13}/0/brando9" - Charlie Parker
@CharlieParker,如果你没有先将值分配给参数,就不能对该值进行参数扩展。而且你不能在同一次扩展中同时使用子字符串删除和搜索/替换等操作。因此,你需要使用三个命令,或者使用外部工具的管道来代替参数扩展。 - Mark Reed

127

对我有用的方法是:

echo "hello world" | rev | cut -c5- | rev
# hello w

但我用它来修剪文件中的行,这就是为令它看起来很奇怪的原因。真正的用途是:

cat somefile | rev | cut -c5- | rev

cut 命令仅能从某个起始位置开始裁剪,这在需要变长行时很不方便。因此,该解决方案先反转字符串 (rev) 并将其与结束位置关联,然后使用上述提到的cut命令进行裁剪,最后再将其反转 (再次使用 rev) 回原来的顺序。


1
这不是正确的答案,也不是对所问问题的回答。rev 反转行而不是字符串。echo $SOME_VAR | rev | ...可能不会按照人们的期望运行。 - Mohammad Nasirifar
2
@Mohammad 由于 引用错误,这确实是一行代码。 - tripleee
对我来说,这个命令在管道和变量中都可以正常工作,例如 var2=$(echo $var | rev | cut -c5- | rev),但是是的,从技术上讲,这个命令只适用于单行。如果您不想将每一行作为单独的字符串处理,则可能无法按预期工作。 - Zbyszek
@MohammadNasirifar 你写道“rev反转的是行,而不是字符串”<--抛开行就是字符串这一点。echo -e 'abc\ndef' | rev 显示rev将每行反转,因此,如果只有一行,它将反转该行。而且如果给出的只是没有\n的字符,例如 echo -n abc|rev,它仍然会将其反转。那么你的问题是,如果字符串中有 \n,它会将其视为新行,并在 \n 字符之间反转而不是跨越它们吗?问题的示例中没有换行符。你有没有想过使用 cat file | rev ,但这已经不再是他们的答案了? - barlop
@tripleee 他们的引用在哪里出了问题? - barlop
显示剩余2条评论

50

使用变量扩展/子字符串替换

${var/%Pattern/Replacement}

如果 var 的后缀匹配 Pattern,则将 Replacement 替换为 Pattern。

因此,您可以执行以下操作:

~$ echo ${var/%????/}
some string

如果您需要翻译,请告诉我需要翻译成哪种语言。

或者,

如果您一直使用相同的4个字母,

~$ echo ${var/.rtf/}
some string

如果它总是以.xyz结尾:
~$ echo ${var%.*}
some string

您也可以使用字符串的长度:

~$ len=${#var}
~$ echo ${var::len-4}
some string

或者简单地使用echo ${var::-4}

1
len=${#var}; echo ${var::len-4} 可以缩写为 echo ${var:0:-4} :-) 编辑:或者如 @iyonizer 指出的那样,只需使用 echo ${var::-4} ... - anishsane
echo ${var::${#var}-4}; - Michael Dimmitt

39

您可以使用sed命令,

sed 's/.\{4\}$//' <<< "$var"

例子:

$ var="some string.rtf"
$ var1=$(sed 's/.\{4\}$//' <<< "$var")
$ echo $var1
some string

1
我该如何将结果赋值给var2 - a06e
3
这很好,因为它可以像这样在一行上工作... | sed 's/.{4}$//'。可以在不使用变量的情况下使用。 - Sam
sed 's/....$//' <<< "$var"(只是稍微简单一点) - undefined

14

这也可以完成工作:

... | head -c -1
-c, --bytes=[-]NUM
              print the first NUM bytes of each file; with the leading '-', print all but the last NUM bytes of each file

1
echo "$v" | head -c -1 在 BSD head 中会出现 "head: illegal byte count -- -1" 的错误。 使用 GNU head (ghead),echo "$v" | ghead -c -1 不会删除任何字符,结果为 "some string.rtf%"。 ghead -c -1 <<<'some string.rtf' 也不会删除任何字符。 最后,为了确认没有任何问题,wc -c <<<"$v"; wc -c <<<"$(ghead -c -1 <<<"$v")" 显示两者都有 16 个字节。你必须使用 head/ghead -c -2。对于那些感兴趣的人,ghead --version 会输出 "head (GNU coreutils) 9.0"。 - SgtPooki
@SgtPooki,那个错误可能是由多字节字符引起的,并且你的语言环境很重要。 - ryenus

8

希望以下示例能够帮到您:

echo ${name:0:$((${#name}-10))} --> ${name:start:len}

  • 在上述命令中,name是变量。
  • start是要删除字符串的起始点。
  • len是要删除的字符串长度。

例如:

    read -p "Enter:" name
    echo ${name:0:$((${#name}-10))}

输出:

    Enter:Siddharth Murugan
    Siddhar

注意:Bash 4.2新增了对负数子字符串的支持。

这比Etan Reisner答案中的Bourne兼容的简单模式替换要冗长得多。 - tripleee
1
我还不得不使用 echo "{${name:0:${#name} - 1}}" 来删除最后一个字符并避免“子字符串表达式<0”错误。 - Clemens
这个答案很容易理解,而且类似于Python的语法。不错。 - GoingMyWay

8

在这种情况下,假设你要删除的文件具有相同的后缀名,你可以使用basename。

示例:

basename -s .rtf "some string.rtf"

这将返回"some string"

如果您不知道后缀,并希望删除最后一个点及其后面的所有内容:

f=file.whateverthisis
basename "${f%.*}"

输出 "file"

% 表示剪切,. 是要剪切的内容,* 是通配符。


7
我尝试了以下方法,对我起作用了:
#! /bin/bash

var="hello.c"
length=${#var}
endindex=$(expr $length - 4)
echo ${var:0:$endindex}

输出:hel

这太完美了。我已经寻找这个确切答案相当长的一段时间了。这是到目前为止我发现的最接近Python可迭代切片功能的代码。 - VanBantam

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