如何找到最少字符的行


22

我正在使用任何常规UNIX命令编写Shell脚本。我必须检索具有最少字符(包括空格)的行。最多可以有20条线。

我知道我可以head -$L | tail -1 | wc -m用来查找第L行的字符数。问题是,我想到的唯一方法是使用手动编写一堆if语句,比较值的方法。

示例数据:

seven/7
4for
8 eight?
five!

4for由于该行的字符最少,将返回。

在我的情况下,如果多行的长度最短,则应返回一行。只要选择最小长度,就无关紧要。但是我看不到在其他情况下向其他用户显示两种方式的危害。


5
如果有多行长度为4怎么办?是否也应该打印?
混乱

在我的情况下,如果多行的长度最短,则应返回一行。只要选择最小长度,就无关紧要。但是我看不到在其他情况下向其他用户显示两种方式的危害。
Matthew D. Scholefield

Answers:


13

一种Perl方式。请注意,如果有许多相同,最短长度的行,则此方法将只打印其中之一:

perl -lne '$m//=$_; $m=$_ if length()<length($m); END{print $m if $.}' file 

说明

  • perl -lne-n表示“逐行读取输入文件”,-l导致从每个输入行中删除尾随的换行符,并在每个print调用中添加一个换行符;并且-e是将应用于每一行的脚本。
  • $m//=$_:除非定义,否则设置$m为当前行($_$m//=从Perl 5.10.0开始可以使用该运算符。
  • $m=$_ if length()<length($m):如果当前值$m的长度大于当前行的长度,则将当前行($_)保存为$m
  • END{print $m if $.}:处理完所有行后,请打印$m最短行的当前值。的if $.确保当行数(这只是发生$.)被定义,从而避免打印空白输入一个空行。

或者,由于您的文件足够小以适合内存,您可以执行以下操作:

perl -e '@K=sort{length($a) <=> length($b)}<>; print "$K[0]"' file 

说明

  • @K=sort{length($a) <=> length($b)}<><>这是一个数组,其元素是文件的行。在sort将根据它们的长度对它们进行排序和排序行被保存为阵列@K
  • print "$K[0]":打印array的第一个元素@K:最短的一行。

如果要打印所有最短的行,可以使用

perl -e '@K=sort{length($a) <=> length($b)}<>; 
         print grep {length($_)==length($K[0])}@K; ' file 

1
添加-C以测量字符数而不是字节数的长度。在UTF-8语言环境中,$$字节数少于(2 vs 3),但字符更多(2 vs 1)。
斯特凡Chazelas

17

sqlite3

sqlite3 <<EOT
CREATE TABLE file(line);
.import "data.txt" file
SELECT line FROM file ORDER BY length(line) LIMIT 1;
EOT

那是我最喜欢的一个,从未想到过SQL ...
混乱

2
这是代码高尔夫状态的巧妙之处
Shadowtalker,2015年

2
这会把整个文件读入内存和/或创建第二个磁盘副本吗?如果是这样,那么它是聪明的但效率低下。
约翰·库格曼

1
@JohnKugelman这可能会将整整4行吸收到一个仅临时内存的数据库中(即strace指示内容)。如果您需要处理非常大的文件(并且系统没有交换),则可以通过仅附加一个文件名(例如)来强制执行操作sqlite3 $(mktemp),所有数据都将写入磁盘。
FloHimself 2015年

我收到以下错误:“”“ xaa:8146:未转义的”字符“”“和”“” xaa:8825:预期1列,但发现2-多余字符被忽略“”“。该文件由json文档组成,每行1个。
Ahmedov

17

这是awk打印第一个找到的最小行的解决方案的变体:

awk '
  NR==1 || length<len {len=length; line=$0}
  END {print line}
'

只需将其扩展一个条件即可打印所有最小行:

awk '
  length==len {line=line ORS $0}
  NR==1 || length<len {len=length; line=$0}
  END {print line}'
'

12

Python简洁明了,并且代码做到了如是:

python -c "import sys; print min(sys.stdin, key=len),"

我承认,最后的逗号是晦涩的。它可以防止print语句添加额外的换行符。此外,您可以在Python 3中编写此代码,支持0行,例如:

python3 -c "import sys; print(min(sys.stdin, key=len, default='').strip('\n'))"


锡怎么说?
mikeserv

@mikeserve:它说:“使用len作为键,打印sys.stdin的最小值” ;-)
Steve Jessop

1
啊 那么关于二进制大小,依赖关系蠕变或执行时间什么也没有?
mikeserv

2
@mikeserv:不,小字样不在罐头上。它在一个上锁的文件柜里的咨询传单中,在地窖里,门后面标有“当心豹子”。
史蒂夫·杰索普

陷阱-如此显示。
mikeserv

10

我一直喜欢纯外壳脚本的解决方案(没有exec!)。

#!/bin/bash
min=
is_empty_input="yes"

while IFS= read -r a; do
    if [ -z "$min" -a "$is_empty_input" = "yes" ] || [ "${#a}" -lt "${#min}" ]; then
        min="$a"
    fi
    is_empty_input="no"
done

if [ -n "$a" ]; then
    if [ "$is_empty_input" = "yes" ]; then
        min="$a"
        is_empty_input="no"
    else
        [ "${#a}" -lt "${#min}" ] && min="$a"
    fi
fi

[ "$is_empty_input" = "no" ] && printf '%s\n' "$min"

注意事项

输入中的NUL字节有问题。因此,printf "ab\0\0\ncd\n" | bash this_script打印ab而不是cd


这真的是最纯正的。虽然,测试的笨拙bash会让我说服将中间结果输入sort
Orion

2
您是否尝试过替您的高管换板凳解决方案与其他解决方案相对应?这是exec之间性能差异的比较而且没有高管!解决类似问题的方法。当它像蜘蛛一样爬行时(例如var=$(get data)将数据流限制在单个上下文中),执行单独的进程几乎是没有优势的;但是当您通过流水线在流中移动数据时,每个应用的exec通常都是有用的-因为它可以实现专门化仅在必要时应用模块化程序。
mikeserv

1
@DigitalTrauma-扩展的连续数字字符串与任何其他扩展的字符串相比,或多或少都不受使shell引用成为必要条件的限制。尽管$IFS没有任何区别-即使默认$IFS值中没有默认值,尽管许多shell都会接受预设的环境配置$IFS-但这并不是特别可靠的默认值。
mikeserv


1
谢谢大家的评论和赞扬(一些代表应该去@cuonglm改正我的回答)。通常,我不建议其他人每天练习纯Shell脚本,但是在某些除静态链接之外没有其他/bin/sh可用条件的极端条件下,可以发现该技能非常有用。对于SunOS4主机/usr丢失或.so损坏的情况,这是我经历过的几次。现在,在现代Linux时代,我仍然偶尔会遇到嵌入式系统或启动故障系统的类似情况。BusyBox是我们最近获得的伟大成就之一。
yaegashi 2015年

9

这是一个纯zsh解决方案(它以的最小长度打印所有行file):

IFS=$'\n'; print -l ${(M)$(<file):#${~${(o@)$(<file)//?/?}[1]}}

输入示例:

seven/7
4for
8 eight?
five!
four

输出为:

4for
four

我认为需要简短说明:-)


首先,我们将内部字段分隔符设置为换行符:

IFS=$'\n';

到目前为止一切顺利,现在是困难的部分。print使用该-l标志来打印结果,并用换行符代替空格。

现在,我们从内部开始:

$(<file)

逐行读取文件并将其视为数组。然后:

${(o@)...//?/?}

o标志表示,结果应该在升序排列,该@方法治疗结果作为数组了。(//?/?)后面的部分是替换a,将所有字符替换为?。现在:

${~...[1]}

我们采用[1]最短的第一个数组元素,在您的情况下为now ????

${(M)$(<file):#...}

匹配分别在每个数组元素上执行,未匹配的数组元素被删除(M)。每个匹配的元素????(4个字符)都留在数组中。因此,其余元素是具有4个字符的元素(最短的元素)。

编辑:如果只需要最短的一行,则此修改后的版本将打印第一行:

IFS=$'\n'; print -l ${${(M)$(<file):#${~${(o@)$(<file)//?/?}[1]}}[1]}

8
tr -c \\n 1 <testfile |   #first transform every [^\n] char to a 1
grep -nF ''           |   #next get line numbers
paste -d: - testfile  |   #then paste it together with itself
sort  -t: -nk2,2          #then sort on second field

...获胜者似乎是第2行。

2:1111:4for
4:11111:five!
1:1111111:seven/7
3:11111111:8 eight?

但是这样做的问题是,每一行的长度都必须超过一倍才能正常工作-因此LINE_MAX实际上减半了。原因是它正在使用-什么是基数1?-代表线的长度。一种类似的(也许更整洁的)方法可能是在流中压缩该信息。我想到的第一个想法是,我应该这样unexpand做:

tr -c \\n \  <testfile    |   #transform all [^\n] to <space>
unexpand -t10             |   #squeeze every series of 10 to one tab
grep -nF ''               |   #and get the line numbers
sed    's/:/!d;=;:/;h;:big    #sed compares sequential lines
$P;$!N; /\(:[^ ]*\)\( *\)\n.*\1.*\2/!D     #newest line is shorter or...
        g;/:./!q;b big'   |   #not; quit input entirely for blank line
sed -f - -e q testfile        #print only first occurrence of shortest line

打印...

2
4for

另一个,就是sed

sed -n '/^\n/D;s/\(.\)\(\n.*\)*/\1/g
$p;h;   s// /g;G;x;n;//!g;H;s// /g
G;      s/^\( *\)\(\n \1 *\)\{0,1\}\n//
D'      <infile >outfile

该语法符合标准-但这不能保证任何旧版本sed都可以\(reference-group\)\{counts\}正确处理-许多版本则不能。

它基本上将相同的正则表达式重复应用于输入-当需要编译它们时,这将非常有益。该模式是:

\(.\)\(\n.*\)*

它以不同的方式匹配不同的字符串。例如:

string1\nstring2\nstring3

...与sin \1和in中''的空字符串匹配\2

1\nstring2\nstring3

...与1in \1\nstring2\nstring3in 匹配\2

\nstring2\nstring3

...与\nin \1和in中''的空字符串匹配\2。如果\n在模式空间的开头有可能出现ewline,这将是有问题的/^\n/D,但是使用和//!g命令可以防止这种情况的发生。我确实使用过,[^\n]但是对于这个小脚本的其他需求使可移植性成为一个问题,并且我对它经常被误解的许多方式不满意。另外,.速度更快。

\nstring2
string1

...匹配\ns再次输入\1,都获得的''空字符串\2。空行根本不匹配。

当以g叶状方式应用图案时,两个偏差(最左侧的标准偏差和较小的右侧\n背线偏差)将相互抵消以实现跳过。一些例子:

s/\(.\)\(\n.*\)*/\1:\2/g
s/\(.\)\(\n.*\)*/\2\1:/g
s/\(.\)\(\n.*\)*/\1: /g
s/\(.\)\(\n.*\)*/ :\2/g

...如果全部(不是连续地)应用于以下字符串...

string1\nstring2

...将其转换为...

s:t:r:i:n:g:1:\nstring2
s:t:r:i:n:g:\nstring21:
s:t:r:i:n:g:1: 
 : : : : : : :\nstring2

基本上,我使用regexp始终只处理我要应用到的任何模式空间中的第一行。这样一来,我就可以兼顾保留的最短匹配线和最新线的两个不同版本,而无需借助测试循环-所应用的每个替换都可以立即处理整个模式空间。

文字/字符串比较需要不同的版本-因此,每行必须有一个版本,保证所有字符都相等。但是,当然,如果一个或另一个实际上应该是输入中最早出现的最短行,那么打印到输出的行可能应该是该行的原始版本,而不是我为了比较而进行过消毒/均质化的行。因此,我每个人都需要两个版本。

不幸的是,另一个必要条件是需要进行很多缓冲区切换以处理相同的缓冲区-但至少两个缓冲区都不会超过保持最新状态所需的四行-因此也许这并不可怕。

无论如何,对于每个循环,发生的第一件事是在记住的行上进行转换-因为实际保存的唯一副本是原义文字-转换为...

^               \nremembered line$

...然后,next输入行将覆盖所有旧缓冲区。如果它至少不包含单个字符,则将其有效地忽略。只q用第一个出现的空白行会容易得多,但是,嗯,我的测试数据有很多,所以我想处理多个段落。

因此,如果它确实包含一个字符,则其文字版本将附加到记住的行上,并且其隔开的比较版本将位于模式空间的开头,如下所示:

^   \n               \nremembered line\nnew$

最后,将替换应用于该模式空间:

s/^\( *\)\(\n \1 *\)\{0,1\}\n//

因此,如果换行符可以容纳包含至少要保留一个字符的记住的行所需的空间,则前两行将被替换掉,否则仅替换第一行。

不管结果如何,模式空间中的第一行总是D在循环结束时被选中,然后再次开始。这意味着如果新行比最后一行短,那么...

new

...被发送回循环中的第一个替换项,该替换项将始终仅从第一个换行符开始-保持完整。但是如果不是,那么字符串...

remembered line\nnew

...将改为开始下一个循环,并且第一个替换项将删除字符串...

\nnew

...每次。

在最后一行,将记住的行打印为标准输出,因此对于给出的示例数据,它打印:

4for

但是,认真使用tr



您甚至需要插入行号吗?我对OP的理解是只需要最短的行,而不必是该行的行号。我想证明它的完整性是没有害处的。
Digital Trauma 2015年

@DigitalTrauma-不,可能不是。但是,如果没有它们,它几乎没有什么用处-而且它们的价格如此便宜。当工作流时,我总是更喜欢在输出中包含相同地再现原始输入的方法-行号使这里成为可能。例如,将第一个管道的结果转为:REINPUT | sort -t: -nk1,1 | cut -d: -f3-。第二个简单的问题是sed --expression在尾部添加另一个脚本。
mikeserv

@DigitalTrauma-哦,在第一个示例中,当输入中出现相同长度的行时,行号的确会影响sort平局的行为-因此,在这种情况下最早出现的行始终浮在顶部。
mikeserv

7

尝试:

awk '{ print length, $0 }' testfile | sort -n | cut -d" " -f2- | head -1

这个想法是用来awk先打印每行的长度。这将显示为:

echo "This is a line of text" | awk '{print length, $0}'
22 This is a line of text

然后,使用字符计数按来对行进行排序sortcut以摆脱计数并head保留第一行(字符最少的那一行)。tail在这种情况下,您当然可以使用来获得最多字符的行。

(这是从此答案中采纳的)


逻辑为+1,但并非在所有情况下都有效。如果两行的字符数相同且为最小。它只会给您遇到的第一行,原因是head -1
Suchhi

要获得最长的行,反转排序要比使用排序更有效率tail(因为head可以在完成工作后立即退出,而无需读取其余输入)。
Toby Speight 2015年

@Thushi使用一点正则表达式,在打印行号之后,可以删除除与第1行相同的行以外的所有行,从而输出所有最短的行。
马修·斯科尔菲尔德

5

使用POSIX awk:

awk 'FNR==1{l=$0;next};length<length(l){l=$0};END{print l}' file

如果多于一行的字符数相同且也是最少,则行不通。
2015年

@Thushi:它将报告第一最小行。
cuonglm

是的,但这不是正确的输出,对吗?甚至其他行的字符数也最少。
2015年

1
@Thushi:在OP要求中没有提及,正在等待OP中的更新。
cuonglm

3
我认为不是L最好的字母来命名该变量:D诸如此类的min事情会使事情变得更清楚
fedorqui 2015年

3

借用@mikeserv的一些想法:

< testfile sed 'h;s/./:/g;s/.*/expr length "&"/e;G;s/\n/\t/' | \
sort -n | \
sed -n '1s/^[0-9]+*\t//p'

第一个sed执行以下操作:

  • h 将原始行保存到保持缓冲区
  • 用该行替换每个字符:-这是为了消除代码注入的任何危险
  • expr length "whole line"- 替换整行-这是一个可以评估的shell表达式
  • e命令sGNU sed扩展,用于评估模式空间并将结果放回模式空间。
  • G 将换行符和保留空间的内容(原始行)追加到模式空间
  • 最后s用制表符替换换行符

现在,字符数是每行开头的数字,因此sort -n按行长排序。

最后,sed然后删除除第一(最短)行和行长以外的所有行,并打印结果。


1
@mikeserv是的,我觉得expr这里更好。是的,e将为每行生成一个外壳。我编辑了sed表达式,以便它将:eval之前的字符串中的每个字符替换为eval,我认为这应该消除任何代码注入的可能性。
Digital Trauma

我通常会xargs expr亲自选择-但是,除了避免使用中间壳之外,这可能更像是一种风格。无论如何,我都喜欢。
mikeserv

3

在我看来,整个事情可以用一种sed表达来表达。这不是很漂亮:

$ sed '1h;s/.*/&\n&/;G;:l;s/\n[^\n]\([^\n]*\)\n[^\n]/\n\1\n/;tl;/\n\n/{s/\n.*//;x};${x;p};d' testfile
4for
$ 

分解如下:

1h            # save line 1 in the hold buffer (shortest line so far)
s/.*/&\n&/    # duplicate the line with a newline in between
G             # append newline+hold buffer to current line
:l            # loop start
s/\n[^\n]\([^\n]*\)\n[^\n]/\n\1\n/
              # attempt to remove 1 char both from current line and shortest line
tl            # jump back to l if the above substitution succeeded
/\n\n/{       # matches if current line is shorter
  s/\n.*//    # remove all but original line
  x           # save new shortest line in hold buffer
}
${            # at last line
  x           # get shortest line from hold buffer
  p           # print it
}
d             # don't print any other lines

OS X中的BSD在换行符方面有些挑剔。此版本适用于sed的BSD和GNU版本:

$ sed -e '1h;G;s/\([^\n]*\)\(\n\)\(.*\)/\1\2\1\2\3/;:l' -e 's/\(\n\)[^\n]\([^\n]*\n\)[^\n]/\1\2/;tl' -e '/\n\n/{s/\n.*//;x;};${x;p;};d' testfile
4for
$

请注意,与其说是认真尝试给出最佳实践答案,不如说是“因为可能”答案。我想这意味着我玩了太多的代码集


@mikeserv从man sedOS X上开始:“转义序列\ n与嵌入在模式空间中的换行符匹配”。因此,我认为GNU sed允许\n在正则表达式中和替换中使用,而BSD仅允许\n在正则表达式中而不是替换中使用。
Digital Trauma

\n从模式空间借入是一个好主意,并且可以在第二个s///表达式中使用,但是该s/.*/&\n&/表达式将a插入\n到以前没有一个的模式空间中。同样,BSD sed似乎在标签定义和分支之后要求使用换行符。
Digital Trauma 2015年

1
这些换行符是参数定界符-您需要它们来分隔可能接受任意参数的任何命令-至少,这就是规范所说的。规范还说sed脚本应该是文本文件,只是它不需要以换行符结尾。因此,您通常也可以将它们分隔为单独的arg- sed -e :\ label -e :\ label2依此类推。既然您仍在进行操作1h,则可以基于某种逻辑x;H来获取换行符,并且可以在循环结束时从模式空间中修剪出一个领先的换行符,而无需引入w / D
mikeserv

@mikeserv不错。是的,我通过执行G第一行并更改s///表达式来插入所需的换行符。使用拆分它-e可以使所有内容都排成一行(长行),而没有文字换行符。
Digital Trauma 2015年

\n转义为只具备sed的LHS,也和我认为这是规范的语句一字不差,只是POSIX括号表达式也只具备这样的方式,所有的字符失去其特殊的含义- (包括显式\\ -在除括号外的一个破折号中,破折号作为范围分隔符,点号,等于,插入号,冒号用于排序规则,对等,取反和类。
mikeserv

2

另一个perl解决方案:将行存储在哈希数组中,哈希键是行的长度。然后,用最小键打印行。

perl -MList::Util=min -ne '
    push @{$lines{ length() }}, $_;
} END {
    print @{$lines{ min keys %lines }};
' sample 
4for

您可以使用push @{$lines{+length}};print @{$lines{+min keys %lines}};减少键入的时间:)
cuonglm

如果我打高尔夫球,那么我也不会使用变量名“ lines”:perl -MList::Util=min -nE'push @{$l{+length}},$_}END{say@{$l{min keys%l}}' sample
glenn jackman

对于非高尔夫版本+1(有效!),但仅适用于打印所有变体。– perl对于我们中那些不敢与神秘性相提并论perl的人有些不满。顺便说一句。打say高尔夫球的人在输出的末尾打印出虚假的空白行。
Peter.O 2015年

2

要获得第一条最短的行:

f=file; sed -n "/^$(sed 's/./1/g' $f | sort -ns | sed 's/././g;q')$/{p;q}" $f

要获得最短的棉绒,只需更改{p;q}p


另一种方法(有点不寻常)是按lengthsort进行实际排序。即使是短线,它也相对较慢,并且随着线长的增加,它会显着变慢。
但是,我发现通过重叠键进行排序的想法非常有趣。我发布它是为了防止其他人也觉得它有趣/有益。

工作原理:
按同一键的长度变量排序- key 1跨越整行
每一个连续的键变量将键长度增加一个字符,直到文件最长的行的长度(由决定wc -L

要获得第一(排序的)最短行:

f=file; sort -t'\0' $(seq -f "-k1.%0.0f" $(<"$f" wc -L) -1 1) "$f" | head -n1

与以下内容相同:

f=file.in; 
l=$(<"$f" wc -L)
k=$(seq -f "-k1.%0.0f" $l -1 1) 
sort -st'\0' $k "$f" | head -n1

2

假设空白行不被认为是最短的行,并且可能存在空白行,那么以下纯AWK将起作用:

awk '
    {
        len   = length;
        a[$0] = len
    }
    !len { next }
    !min { min = len }
    len < min { min = len }
    END {
        for (i in a)
            if (min == a[i])
                print i
    }
' infile.txt

2

那使用排序呢?

awk '{ print length($0) "\t" $0 }' input.txt | sort -n | head -n 1 | cut -f2-

1

使用GNU awk

gawk '
    {
         a[length]=$0
    };
    END
    {
        PROCINFO["sorted_in"]="@ind_num_asc";
        for (i in a)
        {
            print a[i]; 
            exit
        }
    }
    ' file
  • 将每行读入按行长索引的数组。

  • 设置PROCINFO["sorted_in"]@ind_num_asc强制按数组索引对数组扫描进行排序,并按数字排序

  • PROCINFO以上述方式进行设置会强制在数组遍历中首先拾取长度最小的线。因此,从数组中打印第一个元素并退出

这样做的缺点是nlogn有些其他方法n适时


1

中级外壳工具方法,不带sedawk

f=inputfile
head -n $(xargs -d '\n' -L 1 -I % sh -c 'exec echo "%" | wc -c' < $f | 
          cat -n | sort -n -k 2 | head -1 | cut -f 1)  $f | tail -1

不需要$f变量会很好。我有一个想法,也许可以通过tee某种方式使用...
agc
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.