bash 例程到 return 文本文件中给定行号的页码

bash routine to return the page number of a given line number from text file

考虑一个包含分页 ASCII 控制字符的纯文本文件 "Form Feed" ($'\f'):

alpha\n
beta\n
gamma\n\f
one\n
two\n
three\n
four\n
five\n\f
earth\n
wind\n
fire\n
water\n\f

请注意,每页的行数是随机的。

需要一个 bash 例程,该例程 return 包含分页 ASCII 控制字符的文本文件中给定行号的页码。

经过长时间研究解决方案,我终于找到了这段代码:

function get_page_from_line
{
    local nline=""
    local input_file=""

    local npag=0
    local ln=0
    local total=0

    while IFS= read -d $'\f' -r page; do

        npag=$(( ++npag ))

        ln=$(echo -n "$page" | wc -l)

        total=$(( total + ln ))

        if [ $total -ge $nline ]; then
            echo "${npag}"
            return
        fi

    done < "$input_file"

    echo "0"

    return
}

但是,不幸的是,这个解决方案在某些情况下被证明非常慢。

有更好的解决方案吗?

谢谢!

awk 救援!

awk -v RS='\f' -v n=09 '[=10=]~"^"n"." || [=10=]~"\n"n"." {print NR}' file

3

更新锚定如下评论。

 $ for i in $(seq -w 12); do awk -v RS='\f' -v n="$i" 
          '[=11=]~"^"n"." || [=11=]~"\n"n"." {print n,"->",NR}' file; done

01 -> 1
02 -> 1
03 -> 1
04 -> 2
05 -> 2
06 -> 2
07 -> 2
08 -> 2
09 -> 3
10 -> 3
11 -> 3
12 -> 3

这个 gnu awk 脚本为作为命令行参数给出的行号打印 "page":

BEGIN   { ffcount=1;
      search = ARGV[2]
      delete ARGV[2]
      if (!search ) {
        print "Please provide linenumber as argument"  
        exit(1);
      }
    }

 ~ search { printf( "line %s is on page %d\n", search, ffcount) }

/[\f]/ { ffcount++ }

awk -f formfeeds.awk formfeeds.txt 05 一样使用它,其中 formfeeds.awk 是脚本,formfeeds.txt 是文件,'05' 是行号。

BEGIN 规则主要处理命令行参数。其他规则为简单规则:

  • ~ search 当第一个字段与存储在 search
  • 中的命令行参数匹配时适用
  • /[\f]/ 在有换页时适用

可以在 bash 本身中编写类似长度的脚本来定位和响应包含在文件中的嵌入式 <form-feed>。 (它也适用于 POSIX shell,替代字符串索引和 expr 数学)例如,

#!/bin/bash

declare -i ln=1     ## line count
declare -i pg=1     ## page count

fname="${1:-/dev/stdin}"            ## read from file or stdin

printf "\nln:pg  text\n"            ## print header

while read -r l; do                 ## read each line
    if [ ${l:0:1} = $'\f' ]; then   ## if form-feed found
        ((pg++))
        printf "<ff>\n%2s:%2s  '%s'\n" "$ln" "$pg" "${l:1}"
    else
        printf "%2s:%2s  '%s'\n" "$ln" "$pg" "$l"
    fi
    ((ln++))
done < "$fname"

示例输入文件

带有嵌入式 <form-feed> 的简单输入文件是用以下方法创建的:

$ echo -e "a\nb\nc\n\fd\ne\nf\ng\nh\n\fi\nj\nk\nl" > dat/affex.txt

当输出给出时:

$ cat dat/affex.txt
a
b
c

d
e
f
g
h

i
j
k
l

例子Use/Output

$ bash affex.sh <dat/affex.txt

ln:pg  text
 1: 1  'a'
 2: 1  'b'
 3: 1  'c'
<ff>
 4: 2  'd'
 5: 2  'e'
 6: 2  'f'
 7: 2  'g'
 8: 2  'h'
<ff>
 9: 3  'i'
10: 3  'j'
11: 3  'k'
12: 3  'l'

使用 read -d $'\f' 然后计算行数的想法很好。

这个版本可能看起来不够优雅:如果 nline 大于或等于文件中的行数,那么文件会被读取两次。

试一试,因为它超级快:

function get_page_from_line ()
{
    local nline=""
    local input_file=""    
    if [[ $(wc -l "${input_file}" | awk '{print }') -lt nline ]] ; then
        printf "0\n"
    else
        printf "%d\n" $(( $(head -n ${nline} "${input_file}" | grep -c "^"$'\f') + 1 ))
    fi
}

awk 的性能优于上面的 bash 版本。 awk 就是为这种文本处理而创建的。

试试这个测试版本:

function get_page_from_line ()
{
  awk -v nline="" '
    BEGIN {
      npag=1;
    }
    {
      if (index([=11=],"\f")>0) {
        npag++;
      }
      if (NR==nline) {
        print npag;
        linefound=1;
        exit;
      }
    }
    END {
      if (!linefound) {
        print 0;
      }
    }' ""
}

遇到\f时,增加页码

NR是当前行号。

----

关于历史,还有另一个 bash 版本。

此版本仅使用内置命令来计算当前页面中的行数。

您在评论中提供的 speedtest.sh 显示它有点超前(大约 20 秒),这与您的版本相同:

function get_page_from_line ()
{
    local nline=""
    local input_file=""

    local npag=0
    local total=0

    while IFS= read -d $'\f' -r page; do
        npag=$(( npag + 1 ))
        IFS=$'\n'
        for line in ${page}
        do
            total=$(( total + 1 ))
            if [[ total -eq nline ]] ; then
                printf "%d\n" ${npag}
                unset IFS
                return
            fi
        done
        unset IFS
    done < "$input_file"
    printf "0\n"
    return
}

使用awk,你可以定义RS(记录分隔符,默认换行符)到换页符(\f)和IFS(输入字段分隔符,默认任意序列水平空白)到换行符(\n)并获得行数作为 "record" 中 "fields" 的行数,即 "page".

在您的数据中放置换页会在页面中产生一些空行,因此发生这种情况的地方计数不对。

awk -F '\n' -v RS='\f' '{ print NF }' file

如果$NF == "",您可以将数字减一,也许可以将所需页面的数字作为变量传递:

awk -F '\n' -v RS='\f' -v p="2" 'NR==p { print NF - ($NF == "") }' file

要获取特定行的页码,只需将 head -n number 提供给脚本,或遍历这些数字,直到计算出行的总和。

line=1
page=1
for count in $(awk -F '\n' -v RS='\f' '{ print NF - ($NF == "") }' file); do
    old=$line
    ((line += count))
    echo "Lines $old through line are on page $page"
    ((page++)
done