如何从脚本中的文件或pipe道中select多行?

我想要一个脚本,名为lines.sh ,我可以通过pipe道数据来select一系列的行。

例如,如果我有以下文件:

的test.txt

 ab c d 

然后我可以运行:

 cat test.txt | lines 2,4 

它会输出

 b d 

我使用的是zsh,但如果可能的话,更喜欢bash解决scheme。

你可以使用这个awk:

 awk -vs='2,4' 'BEGIN{split(s, a, ","); for (i in a) b[a[i]]} NR in b' file two four 

通过一个单独的脚本lines.sh

 #!/bin/bash awk -vs="$1" 'BEGIN{split(s, a, ","); for (i in a) b[a[i]]} NR in b' "$2" 

然后给予执行权限:

 chmod +x lines.sh 

并称之为:

 ./lines.sh '2,4' 'test.txt' 

试试sed

 sed -n '2p; 4p' inputFile 

-n告诉sed抑制输出,但对于第2行和第4 ,使用p (打印)命令来打印这些行。

你也可以使用范围,例如:

 sed -n '2,4p' inputFile 

两个纯粹的Bash版本。 既然你正在寻找一般的和可重用的解决方案,那么你不妨在这方面投入一点努力。 (另请参阅上一节)。

版本1

这个脚本将整个stdin写入一个数组(使用mapfile ,所以效率很高),然后打印在其参数上指定的行。 范围是有效的,例如,

 1-4 # for lines 1, 2, 3 and 4 3- # for everything from line 3 till the end of the file 

你可以用空格或逗号分隔这些。 这些行按照参数的顺序完全打印:

 lines 1 1,2,4,1-3,4- 1 

将打印第一行两次,然后是第二行,然后是第四行,然后是第一行,第二行和第三行,然后是第四行到最后一行,最后是第一行。

干得好:

 #!/bin/bash lines=() # Slurp stdin in array mapfile -O1 -t lines # Arguments: IFS=', ' read -ra args <<< "$*" for arg in "${args[@]}"; do if [[ $arg = +([[:digit:]]) ]]; then arg=$arg-$arg fi if [[ $arg =~ ([[:digit:]]+)-([[:digit:]]*) ]]; then ((from=10#${BASH_REMATCH[1]})) ((to=10#${BASH_REMATCH[2]:-$((${#lines[@]}))})) ((from==0)) && from=1 ((to>=${#lines[@]})) && to=${#lines[@]} ((from<=to)) || printf >&2 'Argument %d-%d: lines not in increasing order' "$from" "$to" for((i=from;i<=to;++i)); do printf '%s\n' "${lines[i]}" done else printf >&2 "Error in argument \`%s'.\n" "$arg" fi done 
  • Pro:真的很酷。
  • Con:需要将整个流读入内存。 不适合无限的流。

版本2

这个版本解决了以前无限流的问题。 但是你将失去重复和重新排序的能力。

同样的事情,范围是允许的:

 lines 1 1,4-6 9- 

将打印行1,4,5,6,9和一切直到结束。 如果一组行被限制,则在读取最后一行时退出。

 #!/bin/bash lines=() tillend=0 maxline=0 # Process arguments IFS=', ' read -ra args <<< "$@" for arg in "${args[@]}"; do if [[ $arg = +([[:digit:]]) ]]; then arg=$arg-$arg fi if [[ $arg =~ ([[:digit:]]+)-([[:digit:]]*) ]]; then ((from=10#${BASH_REMATCH[1]})) ((from==0)) && from=1 ((tillend && from>=tillend)) && continue if [[ -z ${BASH_REMATCH[2]} ]]; then tillend=$from continue fi ((to=10#${BASH_REMATCH[2]})) if ((from>to)); then printf >&2 "Invalid lines order: %s\n" "$arg" exit 1 fi ((maxline<to)) && maxline=$to for ((i=from;i<=to;++i)); do lines[i]=1 done else printf >&2 "Invalid argument \`%s'\n" "$arg" exit 1 fi done # If nothing to read, exit ((tillend==0 && ${#lines[@]}==0)) && exit # Now read stdin linenb=0 while IFS= read -r line; do ((++linenb)) ((tillend==0 && maxline && linenb>maxline)) && exit if [[ ${lines[linenb]} ]] || ((tillend && linenb>=tillend)); then printf '%s\n' "$line" fi done 
  • Pro:这真的很酷,不会读取内存中的完整流。
  • 答:不能像版本1那样重复或重新排序。速度不是最强的。

更多的想法

如果你真的想要一个真正的版本1和版本2的通用脚本,你肯定应该考虑使用另一种语言,例如Perl:你会获得很多(特别是速度)! 你将能够有很好的选择,会做很多更酷的东西。 从长远来看,这可能是值得的,因为你需要一个通用的,可重用的脚本。 你甚至可能最终有一个脚本,读取电子邮件!


免责声明。 我没有彻底检查这些脚本…所以要小心错误!

快速解决你的朋友。 输入:

的test.txt

 a b c d e f g h i j 

test.sh

 lines (){ sed -n "$( echo "$@" | sed 's/[0-9]\+/&p;/g')" } cat 1.txt | lines 1 5 10 

或者如果你想把你的lines作为脚本:

lines.sh

 IFS=',' read -a lines <<< "$1"; sed -n "$( echo "${lines[@]}" | sed 's/[0-9]\+/&p;/g')" "$2" ./lines.sh 1,5,10 test.txt 

在这两种情况下输出:

 a e j 

如果这是一次性操作,并且没有多少行可以选择,可以使用pick来手动选择它们:

 cat test.txt | pick | ... 

交互式屏幕将打开,让您选择你想要的。

那么,只要:

  • 你的文件够小
  • 您在文件中没有任何分号(或您选择的其他特定字符)
  • 你不介意使用多个管道

你可以使用像这样的东西:

 cat test.txt |tr "\\n" ";"|cut -d';' -f2,4|tr ";" "\\n" 

其中-f2,4表示要提取的行

尝试这个 :

 file=$1 for var in "$@" //var is all line numbers do sed -n "${var}p" $file done 

我创建了一个具有1个文件参数的脚本,以及用于行号的无限数量的参数。 你会这样称呼它:

 lines txt 2 3 4...etc