如何从脚本中的文件或管道中选择多行?

时间:2014-12-16 16:58:31

标签: linux bash sed text-processing

我希望有一个名为lines.sh的脚本,我可以将数据通过管道来选择一系列行。

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

的test.txt

a 
b
c
d

然后我可以跑:

cat test.txt | lines 2,4

并输出

b
d

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

7 个答案:

答案 0 :(得分:7)

你可以使用这个awk:

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

通过单独的脚本lines.sh

#!/bin/bash
awk -v s="$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'

答案 1 :(得分:3)

尝试sed

sed -n '2p; 4p' inputFile

-n告诉sed取消输出,但对于行24p(打印)命令用于打印这些行

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

sed -n '2,4p' inputFile

答案 2 :(得分:3)

两个纯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

将打印第1行两次,然后第2行,第4行,第1行,第2行和第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
  • 亲:真的很酷。
  • 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:这真的很酷,并没有在内存中读取整个流。
  • Con:不能重复或重新排序行作为版本1.速度不是它的最强点。

进一步的想法

如果你真的想要一个能够完成版本1和版本2的功能的超级通用脚本,那么你绝对应该考虑使用其他语言,例如Perl:你会获得很多(特别是速度)!你将能够有很好的选择,可以做很多很酷的东西。从长远来看,它可能是值得的,因为您需要一个通用且可重用的脚本。你甚至可能最终得到一个阅读电子邮件的脚本!


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

答案 3 :(得分:2)

嗯,只要:

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

你可以使用类似的东西:

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

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

答案 4 :(得分:2)

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

<强>的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

答案 5 :(得分:2)

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

cat test.txt | pick | ...

将打开一个交互式屏幕,允许您选择所需内容。

答案 6 :(得分:1)

试试这个:

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

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

lines txt 2 3 4...etc