【问题标题】:How to write a tail script without the tail command如何编写没有tail命令的tail脚本
【发布时间】:2013-09-03 19:38:46
【问题描述】:

您将如何在 bash 中实现这一点。这是我在一次采访中被问到的一个问题,我可以用高级语言想到答案,但不能用 shell。

据我了解,tail 的真正实现是寻找文件末尾,然后向后读取。

【问题讨论】:

  • "in bash" "in shell" 你的意思是不允许使用任何外部命令吗?像 sed/awk/wc 吗?
  • @t0mmyt 使用标准输入或管道无法实现不使用缓冲区而仅进行搜索的真正实现。至于向后阅读,我不确定,但如果您已经在使用数组,一切皆有可能。请检查我用纯 bash 编写的解决方案。我认为证明这个概念已经绰绰有余了。

标签: bash unix


【解决方案1】:

主要思想是保持一个固定大小的缓冲区并记住最后几行。这是使用 shell 进行尾部操作的快速方法:

#!/bin/bash

SIZE=5
idx=0

while read line
do
    arr[$idx]=$line
    idx=$(( ( idx + 1 ) % SIZE )) 
done < text

for ((i=0; i<SIZE; i++))
do
    echo ${arr[$idx]}
    idx=$(( ( idx + 1 ) % SIZE )) 
done

【讨论】:

  • 我认为这需要稍微修正一下:例如,如果您使用 6 行的 text,最后一行将被包装为输出中的第一行。
  • @MannyD 是的,它可能有更多错误
  • @MannyD 我修复了那个(但引入了一个丑陋的 bashism :-( )。感谢您指出。
  • 很高兴这没有被删除,因为它是一个非常好的答案。您也可以只在一个循环中输出,从 1..$SIZE 循环,但增加 IDX 的方式与在 read 中所做的完全相同,并使用它来索引数组。
  • 数学上下文中不需要$for (( i=0; i&lt;idx; i++ )) 不需要 $idx。此外,idx=$((idx + 1 )) 在 POSIX sh 中有效,并且比使用 expr 更容易阅读;同样适用于idx=$(( ( idx + 1 ) % SIZE ))
【解决方案2】:

如果允许所有非尾命令,为什么不异想天开?

#!/bin/sh

[ -r "$1" ] && exec < "$1"

tac | head | tac

【讨论】:

  • exit $? 是多余的——shell 的默认退出状态是最近运行的命令。
  • 你是对的,当然。出于长期的习惯,我通常让我的脚本在他们出门时说“退出”,虽然那是不必要的。
【解决方案3】:

使用wc -l 计算文件中的行数。从中减去你想要的行数,然后加 1,得到起始行号。然后将其与sedawk 一起使用以从该行号开始打印文件,例如

sed -n "$start,\$p"

【讨论】:

  • -1 这根本不是纯 Bash。 (不过,我的似乎是第二次投反对票。)
  • AFAIC,标准 Unix 工具,如 wcsed 计数。 “纯 bash”实际上从未使用过。
  • @tripleee:绝对不是-1。 OP 从未写过pure bash 要求。
  • @Barmar 如果tail 在面试的上下文中不可用,我很确定 sed 或 awk 或任何其他类似工具是不可接受的
  • 我想这取决于问题的措辞。 IE。他们是否说“只有 bash 可用”或“tail 不可用”。
【解决方案4】:

有这个:

#!/bin/bash
readarray file
lines=$(( ${#file[@]} - 1 ))
for (( line=$(($lines-$1)), i=${1:-$lines}; (( line < $lines && i > 0 )); line++, i-- )); do
    echo -ne "${file[$line]}"
done

基于此答案:https://stackoverflow.com/a/8020488/851273

您在文件末尾传入要查看的行数,然后通过标准输入发送文件,将整个文件放入一个数组中,并且只打印数组的最后 # 行。

【讨论】:

  • 您撤消以删除以相反顺序打印它们的答案部分。
  • @JonLin 这不是倒序打印吗?
  • @rici 看到你的简单代码我很惊讶,当我注意到如果L &gt; C (${#file[@]}) in "${file[@]:(-L)}" 不起作用时,我想将它应用到我的代码中;但我们可以使用printf "%s" "${A[@]:(C &gt; L ? C - L : 0)}" 进一步改进它。然而,将所有数据立即用于一个命令给了我一个想法,即我们可以不使用readarray,而只能通过使用读取循环来将缓冲区限制为 N 行。
【解决方案5】:

在“纯”shell 中我能想到的唯一方法是对整个文件执行while read 逐行转换为索引模n 的数组变量,其中n em> 是尾行数(默认为 10)— 即循环缓冲区,然后在 while read 结束时从您离开的地方迭代循环缓冲区。从任何意义上说,它既不高效也不优雅,但它可以工作并且避免将整个文件读入内存。例如:

#!/bin/bash                                                                                 

incmod() {
    let i=$1+1
    n=$2

    if [ $i -ge $2 ]; then
        echo 0
    else
        echo $i
    fi
}

n=10
i=0
buffer=
while read line; do
    buffer[$i]=$line
    i=$(incmod $i $n)
done < $1

j=$i
echo ${buffer[$i]}
i=$(incmod $i $n)
while [ $i -ne $j ]; do
    echo ${buffer[$i]}
    i=$(incmod $i $n)
done

【讨论】:

    【解决方案6】:

    这个脚本以某种方式模仿tail

    #!/bin/bash
    
    shopt -s extglob
    
    LENGTH=10
    
    while [[ $# -gt 0 ]]; do
        case "$1" in
        --)
            FILES+=("${@:2}")
            break
            ;;
        -+([0-9]))
            LENGTH=${1#-}
            ;;
        -n)
            if [[ $2 != +([0-9]) ]]; then
                echo "Invalid argument to '-n': $1"
                exit 1
            fi
            LENGTH=$2
            shift
            ;;
        -*)
            echo "Unknown option: $1"
            exit 1
            ;;
        *)
            FILES+=("$1")
            ;;
        esac
        shift
    done
    
    PRINTHEADER=false
    
    case "${#FILES[@]}" in
    0)
        FILES=("/dev/stdin")
        ;;
    1)
        ;;
    *)
        PRINTHEADER=true
        ;;
    esac
    
    IFS=
    
    for I in "${!FILES[@]}"; do
        F=${FILES[I]}
    
        if [[ $PRINTHEADER == true ]]; then
            [[ I -gt 0 ]] && echo
            echo "==> $F <=="
        fi
    
        if [[ LENGTH -gt 0 ]]; then
            LINES=()
            COUNT=0
    
            while read -r LINE; do
                LINES[COUNT++ % LENGTH]=$LINE
            done < "$F"
    
            for (( I = COUNT >= LENGTH ? LENGTH : COUNT; I; --I )); do
                echo "${LINES[--COUNT % LENGTH]}"
            done
        fi
    done
    

    示例运行:

    > bash script.sh -n 12 <(yes | sed 20q) <(yes | sed 5q)
    ==> /dev/fd/63 <==
    y
    y
    y
    y
    y
    y
    y
    y
    y
    y
    y
    y
    
    ==> /dev/fd/62 <==
    y
    y
    y
    y
    y
    > bash script.sh -4 <(yes | sed 200q)
    y
    y
    y
    y
    

    【讨论】:

      【解决方案7】:

      如果我在面试中被问到这个问题,我会给出以下答案:

      这是什么环境,我有bash 但没有tail?可能是早期启动脚本?我们可以在那里获得busybox 以便我们可以使用完整的shell 实用程序吗?或者也许我们应该看看我们是否可以在其中加入一个精简的 Perl 解释器,即使没有大多数可以让生活变得更轻松的模块。你知道dashbash 小得多,并且非常适合脚本使用,对吧?这也可能有帮助。如果这些都不是一个选项,我们应该检查静态链接的 C mini-tail 需要多少空间,我敢打赌,我可以将它放入与您想要的 shell 脚本相同数量的磁盘块中。

      如果这不能说服面试官这是一个愚蠢的问题,那么我继续观察我不相信使用 bash 扩展,因为现在用 shell 脚本编写任何复杂的东西的唯一充分理由是可移植性是最重要的问题。通过避免任何不可移植的东西,即使是一次性的,我也不会养成坏习惯,并且我不会想在 shell 中做某事,而在现实中做会更好编程语言。

      现在的问题是,在真正可移植的 shell 中,数组可能不可用。 (我实际上不知道 POSIX shell 规范是否有数组,但肯定有没有它们的遗留 Unix shell。)所以,如果你 必须 模拟 tail 只使用shell内置,它必须在任何地方工作,这是你能做的最好的,是的,这很可怕,因为你用错误的语言编写:

      #! /bin/sh
      
      a=""
      b=""
      c=""
      d=""
      e=""
      f=""
      
      while read x; do
          a="$b"
          b="$c"
          c="$d"
          d="$e"
          e="$f"
          f="$x"
      done
      
      printf '%s\n' "$a"
      printf '%s\n' "$b"
      printf '%s\n' "$c"
      printf '%s\n' "$d"
      printf '%s\n' "$e"
      printf '%s\n' "$f"
      

      调整变量的数量以匹配您要打印的行数。

      身经百战的玩家会注意到printf 也不是 100% 可用的。不幸的是,如果你只有echo,你就上岸了:一些版本的echo 不能打印字符串“-n”,而其他版本不能打印字符串“\n”,甚至想找出你有哪个有点痛苦,特别是如果你没有printf(在POSIX中),你可能也没有用户定义的函数。

      (注意,这个答案中的代码,没有理由,最初是由用户“Nirk”发布的,但后来在人们的投票压力下被删除,我假设他们不知道某些 shell 没有数组。)

      【讨论】:

      • 不需要慈善解释——这个问题被明确标记为bash
      • @CharlesDuffy 但这很愚蠢。如果您有bash,那么您没有tail 是不可能的。 (实际上我认为整个问题都很愚蠢。我已经看到了cat 不可用的真实情况 - Solaris 2.5 中非常早期的引导脚本,几乎所有内容都放在了 @987654341 @ - 这是我想到的极其有限的 shell 之一。但现在没有理由不将 busybox 放在那种环境中,为您提供完整的实用程序。)
      • 这是一个面试问题(并在前面明确描述过)。它不应该在现实世界中有意义。
      • @CharlesDuffy 在现实世界中没有意义的面试问题是不好的问题。 (见编辑。)
      猜你喜欢
      • 1970-01-01
      • 2022-10-14
      • 1970-01-01
      • 1970-01-01
      • 2013-06-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多