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

8
我可以为您提供一份名为lines.sh的脚本,用于选择一系列行。该脚本可与数据进行管道连接。
例如,如果我有以下文件:

test.txt

a 
b
c
d

那么我可以运行:

cat test.txt | lines 2,4

并且它将输出
b
d

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

7个回答

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'

太棒了!也许这与我在Mac上运行有关,但我不得不将$2更改为${2:-/dev/stdin}以便它能够接受管道输入。 - Brad Parks

5
尝试使用sed命令:
sed -n '2p; 4p' inputFile

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

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

sed -n '2,4p' inputFile

这只是其中的一部分,但我该如何将其放入可重用的 lines.sh 脚本中呢?实际上,我想在脚本中对每行执行更多操作,但这个核心功能是必需的。 - Brad Parks

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

这段代码会先输出两次第一行,然后是第二行,接着是第四行,再输出第一、二、三行,最后输出从第四行到结尾的所有内容,最终再输出一次第一行。

#!/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
  • 优点:它真的很酷。
  • 缺点:需要将整个流读入内存。不适用于无限流。

第二版

该版本解决了无限流的问题。但您将失去重复和重新排序行的能力。

同样,范围是允许的:

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
  • 优点:非常酷,不会在内存中读取整个流。
  • 缺点:不能重复或重新排序行,就像版本1一样。速度不是它的强项。

进一步的想法

如果你真的想要一个做版本1和版本2所做的事情,并且还有更多功能的通用脚本,你绝对应该考虑使用另一种语言,比如Perl:你将获得很多好处(特别是速度)!你将能够拥有很棒的选项,可以做很多更酷的事情。从长远来看,这可能是值得的,因为你想要一个通用和可重用的脚本。你甚至可能最终得到一个读取电子邮件的脚本!


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


2

如果这是一次性操作,且需要选择的行数不多,你可以使用pick手动选择它们:

cat test.txt | pick | ...

一块交互式屏幕会打开,让您选择所需内容。

2

假设以下条件成立:

  • 你的文件足够小
  • 文件中没有分号(或其他指定字符)
  • 你不介意使用多个管道符

那么你可以使用以下方法:

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

-f2,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

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

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