如何在shell脚本中提取字符串的前两个字符?

152

例如,给定以下内容:

USCAGoleta9311734.5021-120.1287855805

我想要仅提取:

US

7
谢谢大家。最终我使用了'cut -c1-2',说实话我甚至不知道有'cut'这个命令。虽然我觉得我在命令行方面很有经验,但显然我还有很多需要学习的地方。 - Greg
1
@Greg,请注意cut是作为单独的进程运行的 - 它将比我在答案中同时发布的内部bash解决方案慢。这不会有任何影响,除非你正在处理大型数据集,但你需要记住这一点。 - paxdiablo
实际上,我认为这行代码可能会在每个报告中执行大约50,000次。所以我可能会选择内部Bash方法 - 正如你所说,这将节省一些非常需要的资源。 - Greg
15个回答

228

如果你正在使用 bash shell,并且根据你的评论似乎是这样,那么可能是最有效的方法是使用参数扩展的子字符串变体:

pax> long="USCAGol.blah.blah.blah"
pax> short="${long:0:2}" ; echo "${short}"
US

这将把 long 的前两个字符赋值给 short。如果 long 长度小于两个字符,则 short 将与之相同。

如果你需要频繁地执行此操作(如你所提到的每个报告执行 50,000 次),那么使用这种在 shell 中的方法通常更好,因为它没有进程创建开销。所有使用外部程序的解决方案都会受到该开销的影响。

如果你还想确保最小长度,可以先用类似下面的方式将其填充:

pax> long="A"
pax> tmpstr="${long}.."
pax> short="${tmpstr:0:2}" ; echo "${short}"
A.

这会确保长度小于两个字符的任何内容在右侧用句点(或其他字符,只需在创建tmpstr时更改使用的字符)进行填充。不清楚您是否需要此功能,但出于完整性考虑,我认为应该加上。


话虽如此,有许多使用外部程序(例如,如果您没有bash可用)完成此操作的方法,其中一些方法包括:

short=$(echo "${long}" | cut -c1-2)
short=$(echo "${long}" | head -c2)
short=$(echo "${long}" | awk '{print substr ($0, 0, 2)}'
short=$(echo "${long}" | sed 's/^\(..\).*/\1/')

对于单行字符串,前两个方法(cuthead)是相同的 - 它们基本上只会返回前两个字符。它们之间的区别在于 cut 会给你每一行的前两个字符,而 head 会给你整个输入的前两个字符。

第三个方法使用 awk 子字符串函数提取前两个字符,第四个方法使用 sed 捕获组(使用()\1)捕获前两个字符并用它们替换整行内容。它们都类似于 cut - 它们会提供输入中每一行的前两个字符。

如果您确定输入是单行,则这些内容都无关紧要,它们具有相同的效果。


我更喜欢使用printf '%s'而不是echo,以防字符串中有奇怪的字符:https://dev59.com/t5zha4cB1Zd3GeqPBje1#40423558 对于POSIX迷,head -c不是POSIX标准,cut -cawk substr是,sed \1不确定。 - Ciro Santilli OurBigBook.com
1
使用printf,您甚至不需要额外的程序即可实现@CiroSantilli新疆改造中心996ICU六四事件。请参见我的答案 - bschlueter

79

最简单的方法是:

${string:position:length}

这个从$string$position位置提取$length个字符的子串,是Bash内置函数,不需要使用awk或sed。


1
这是获取子字符串的简短、简单和最容易的方法。 - ani627
我想尝试这个,但是缺少关键细节,不知道如何运行。请使用 OP 添加完整的命令。 - John

36

你已经得到了几个很好的答案,我会选择使用Bash内置函数,但是由于你询问了sedawk,而且(几乎)没有人提供基于它们的解决方案,所以我给你提供以下内容:

echo "USCAGoleta9311734.5021-120.1287855805" | awk '{print substr($0,1,2)}'

echo "USCAGoleta9311734.5021-120.1287855805" | sed 's/\(^..\).*/\1/'

awk 的用法非常显然,但这里是对 sed 代码的解释:

  • 使用 "s/" 命令替换
  • 以 "^" 开头的任意字符 ".",重复零次或更多次 "*",且为两个字符 ".." 组成的组 "()"
  • 替换为 "/" 后加上第一个(在本例中是唯一的)组的内容(此处反斜杠是一个特殊转义,指代匹配子表达式)
  • 最终用 "/" 表示结束

1
在awk中,字符串从索引1开始,因此您应该使用substr($0,1,2) - user8017719
有趣的是,字符串既可以是0索引也可以是1索引(在Gawk 5.1.0和MacOS awk 20070501中测试过)。但为了与index()保持一致,最好使用1。回答已更新。 - Dennis Williamson

11

只需使用grep:

echo 'abcdef' | grep -Po "^.."        # ab

符合我的需求。您可以删除“-P”选项来使其更短。所有正则表达式都将理解该模式。 - datashaman

10
如果您想使用shell脚本而不依赖非posix扩展(例如所谓的bashism),则可以使用不需要分叉外部工具(如grep,sed,cut,awk等)的技术,从而使您的脚本效率更低。也许效率和posix可移植性在您的用例中并不重要。但是,如果是这种情况(或者只是作为一个好习惯),您可以使用以下“参数扩展”选项方法来提取shell变量的前两个字符:
$ sh -c 'var=abcde; echo "${var%${var#??}}"'
ab

这里使用了"最小前缀"参数扩展来删除前两个字符(即${var#??}部分),然后使用"最小后缀"参数扩展${var%部分)从原始值中删除除第一个两个字符之外的所有字符串。

这种方法先前在答案中描述了“Shell = Check if variable begins with #”问题。该答案还描述了几种类似的参数扩展方法,可以在略微不同的上下文中使用,适用于原始问题所涉及的上下文。


2
最佳答案应该在顶部。没有分支,没有bashisms。即使是像dash这样的小shell也可以工作。 - exore
2
我喜欢尽可能避免使用Bashism。 - 3ronco
不错的回答。如果内部参数扩展被引用为 echo "${var%"${var#??}"}",那就更好了。(参考 - https://www.shellcheck.net/wiki/SC2295) - midnite

9

如果你正在使用 bash,你可以这样说:

bash-3.2$ var=abcd
bash-3.2$ echo ${var:0:2}
ab

这可能正是你需要的...

最简单和最简单的答案!像魔法一样奏效。 - aloha

8
你可以使用 printf
$ original='USCAGoleta9311734.5021-120.1287855805'
$ printf '%-.2s' "$original"
US

6

colrm — 从文件中删除列

要保留前两个字符,只需从第三列开始删除即可。

cat file | colrm 3

4

使用:

sed 's/.//3g'

或者

awk NF=1 FPAT=..

或者

perl -pe '$_=unpack a2'

2

仅仅是为了好玩,我会添加一些虽然过于复杂和无用的内容,但它们并未被提到:

head -c 2 <( echo 'USCAGoleta9311734.5021-120.1287855805')

echo 'USCAGoleta9311734.5021-120.1287855805' | dd bs=2 count=1 status=none

sed -e 's/^\(.\{2\}\).*/\1/;' <( echo 'USCAGoleta9311734.5021-120.1287855805')

cut -c 1-2 <( echo 'USCAGoleta9311734.5021-120.1287855805')

python -c "print(r'USCAGoleta9311734.5021-120.1287855805'[0:2])"

ruby -e 'puts "USCAGoleta9311734.5021-120.1287855805"[0..1]'

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