Shell命令输出中的字符数


12

我正在编写一个脚本,该脚本需要一步来计算命令输出中的字符数。

例如,使用该命令readlink -f /etc/fstab应该返回,10因为该命令的输出为10个字符长。

使用以下代码,使用存储的变量已经可以做到这一点:

variable="somestring";
echo ${#variable};
# 10

不幸的是,对命令生成的字符串使用相同的公式不起作用:

${#(readlink -f /etc/fstab)};
# bash: ${#(readlink -f /etc/fstab)}: bad substitution

我知道可以通过首先将输出保存到变量来做到这一点:

variable=$(readlink -f /etc/fstab);
echo ${#variable};

但我想删除多余的步骤。

这可能吗?仅使用内置或标准实用程序与Almquist shell(sh)兼容是可取的。


1
输出readlink -f /etc/fstab11个字符。不要忘记换行符。否则,您将/etc/fstabluser@cern:~$ 在从外壳运行时看到它。
Phil Frost 2014年

@PhilFrost您似乎有一个有趣的提示,您是否在CERN工作?
德米特里·格里戈里耶夫

Answers:


9

使用GNU expr

$ expr length + "$(readlink -f /etc/fstab)"
10

+有GNU的一个特殊的功能expr,以确保下一个参数被视为一个字符串,即使它正好是一个expr像运营商matchlength+...

上面的内容将删除输出的所有尾随换行符。要变通解决:

$ expr length + "$(readlink -f /etc/fstab; printf .)" - 2
10

由于将的最后一个换行符和我们添加的字符相加,结果减去了2readlink.

对于Unicode字符串,expr它似乎不起作用,因为它以字节为单位而不是字符计数返回字符串的长度(请参阅第654行

$ LC_ALL=C.UTF-8 expr length ăaa
4

因此,您可以使用:

$ printf "ăaa" | LC_ALL=C.UTF-8 wc -m
3

适当地:

$ expr " $(readlink -f /etc/fstab; printf .)" : ".*" - 3
10

命令替换前的空格可防止命令以开头的字符串崩溃-,因此我们需要减去3。


谢谢!似乎您的第三个示例即使没有使用也可以工作LC_ALL=C.UTF-8,如果事先不知道字符串的编码,则可以大大简化事情。
user339676 2014年

2
expr length $(echo "*")- 不。至少使用双引号:expr length "$(…)"。但这从命令中删除了结尾的换行符,这是命令替换的一项不可回避的功能。(您可以解决它,但答案会变得更加复杂。)
吉尔斯(Gilles)'“别再邪恶了”

6

不确定如何使用shell内置程序(Gnouc是)来执行此操作,但是标准工具可以帮助您:

  1. 您可以使用wc -m哪个计数字符。不幸的是,它也计入了最后的换行符,因此您必须首先摆脱掉它:

    readlink -f /etc/fstab | tr -d '\n' | wc -m
  2. 你当然可以用 awk

    readlink -f /etc/fstab | awk '{print length($0)}'
  3. 或Perl

    readlink -f /etc/fstab | perl -lne 'print length'

您的意思expr是内置的吗?在哪个外壳中?
mikeserv

5

我通常这样做:

$ echo -n "$variable" | wc -m
10

要执行命令,我会像这样修改它:

$ echo -n "$(readlink -f /etc/fstab)" | wc -m
10

此方法与您在两个步骤中所做的工作类似,不同之处在于我们将它们组合为一个单一的班轮。


2
您必须使用-m代替-c。使用unicode字符,您的方法将被打破。
cuonglm

1
为什么不简单readlink -f /etc/fstab | wc -m
Phil Frost 2014年

1
为什么使用这种不可靠的方法代替${#variable}?至少使用双引号echo -n "$variable",但是如果例如variableis 的值,这仍然会失败-e。当您将其与命令替换结合使用时,请记住,尾随的换行符已被删除。
吉尔斯(Gillles)“所以-别再作恶了” 2014年

@philfrost b / c我所展示的内容是基于操作人员已经在思考的内容。它也适用于他可能在vars中预先设置的任何cmd,并希望它们的长度为后缀。terdon也已经有该示例。
slm

1

您可以调用外部实用程序(请参阅其他答案),但是它们会使脚本变慢,并且很难正确设置管道。

sh

在zsh中,您可以编写${#$(readlink -f /etc/fstab)}以获取命令替换的长度。注意,这不是命令输出的长度,而是没有尾随换行符的输出长度。

如果需要输出的确切长度,请在末尾输出一个额外的非换行符,然后减去一个。

$((${#$(readlink -f /etc/fstab; echo .)} - 1))

如果您想要的是命令输出中的有效负载,那么您需要在此处减去两个,因为的输出readlink -f是规范路径和换行符。

$((${#$(readlink -f /etc/fstab; echo .)} - 2))

这与${#$(readlink -f /etc/fstab)}标准路径本身以换行符结尾的罕见但可能的情况不同。

对于此特定示例,您根本不需要外部实用程序,因为zsh具有一个内置的结构,该结构等效于readlink -fhistory修饰符A

echo /etc/fstab(:A)

要获取长度,请在参数扩展中使用history修饰符:

${#${:-/etc/fstab}:A}

如果文件名包含在变量中filename,则为${#filename:A}

Bourne / POSIX风格的壳

我所知道的纯Bourne / POSIX外壳(Bourne,ash,mksh,ksh93,bash,yash…)都没有类似的扩展名。如果需要将参数替换应用于命令替换的输出或嵌套参数替换,请使用连续的阶段。

您可以根据需要将处理填充到一个函数中。

command_output_length_sans_trailing_newlines () {
  set -- "$("$@")"
  echo "${#1}"
}

要么

command_output_length () {
  set -- "$("$@"; echo .)"
  echo "$((${#1} - 1))"
}

但是通常没有好处;除了ksh93之外,这会导致额外的fork能够使用该函数的输出,因此使您的脚本变慢,并且几乎没有可读性。

再次,输出readlink -f是规范路径和换行符;如果您需要规范路径的长度,请减去2而不是中的1 command_output_lengthcommand_output_length_sans_trailing_newlines仅当规范路径本身未以换行符结尾时,使用才能给出正确的结果。

字节与字符

${#…}应该是字符长度,而不是字节长度,这会导致多字节语言环境有所不同。ksh93,bash和zsh的最新版本合理地根据扩展构造LC_CTYPE时的value计算以字符为单位的长度${#…}。许多其他常见的shell并不真正支持多字节语言环境:从破折号0.5.7,mksh 46和posh 0.12.3开始,${#…}返回以字节为单位的长度。如果您希望以可靠的方式输入字符长度,请使用该wc实用程序:

$(readlink -f /etc/fstab | wc -m)

只要$LC_CTYPE指定有效的语言环境,您就可以确信这会出错(在不支持多字节语言环境的古老或受限平台上)或返回正确的字符长度。(对于Unicode,“字符长度”表示代码点的数量-由于合并字符等复杂性,字形的数量又是另一回事了。)

如果要以字节为单位的长度,请LC_CTYPE=C临时设置,或使用wc -c代替wc -m

使用来计数字节或字符wc包括命令中的任何尾随换行符。如果要以字节为单位的规范路径的长度,则为

$(($(readlink -f /etc/fstab | wc -c) - 1))

要以字符为单位,请减去2。


@cuonglm不,您需要减去1。echo .添加两个字符,但是第二个字符是结尾的换行符,该行由命令替换除去。
吉尔斯(Gillles)“所以-别再邪恶了”

换行符来自readlink输出,再加上.by echo。我们都同意echo .添加两个字符,但是尾随换行符被删除。尝试使用printf .或查看我的答案unix.stackexchange.com/a/160499/38906
cuonglm

@cuonglm该问题询问命令输出中的字符数。的输出readlink是链接目标加上换行符。
吉尔斯(Gillles)“所以-别再邪恶了”

0

这可以正常工作,dash但是它确实要求目标var必须为空或未设置。这就是为什么这实际上是两个命令的原因-我$l在第一个命令中明确显示为空:

l=;printf '%.slen is %d and result is %s\n' \
    "${l:=$(readlink -f /etc/fstab)}" "${#l}" "$l"

输出值

len is 10 and result is /etc/fstab

这就是所有shell内置函数- readlink当然不包括-而是在当前shell中对其进行评估,这意味着您必须在获取len之前进行赋值,这就是为什么我%.sprintf格式字符串中的第一个参数设为无效,并再次将其添加为printf的arg列表尾部的文字值。

eval

l=$(readlink -f /etc/fstab) eval 'l=${#l}:$l'
printf %s\\n "$l"

输出值

10:/etc/fstab

您可以接近同一件事,但是可以在stdout上获取它而不是第一个命令中的变量输出:

PS4='${#0}:$0' dash -cx '2>&1' "$(readlink -f /etc/fstab)"

写着

10:/etc/fstab

...到文件描述符1,而不给当前shell中的任何var分配任何值。


1
这不是OP想要避免的吗?“我知道可以通过首先将输出保存到变量中来做到这一点:variable=$(readlink -f /etc/fstab); echo ${#variable};但是我想删除多余的步骤。”
terdon

@terdon,可能我误会了,但是我的印象是分号是问题,而不是变量。这就是为什么这些仅使用shell内置函数在单个简单命令中获得len并输出的原因。例如,外壳程序不执行exec readlink,然后执行exec expr在某种程度上获取len遮盖了值可能很重要,我承认我很难理解为什么会这样,但是我怀疑在某些情况下它很重要。
mikeserv

1
eval方式,顺便说一下,大概是这里最干净的-它分配输出,并在一个单一的执行LEN相同的变量名称- 非常接近这个目标l=length(l):out(l)。顺便说一句,这样做expr length $(command) 确实会掩盖有利于len的价值。
mikeserv
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.