在 bash 的 C 风格 for 循环中模拟“while IFS= read -r line”

在 bash 的 C 风格 for 循环中模拟“while IFS= read -r line”

首先介绍一下这个问题的背景。这while IFS= read -r line; do ... done < input.txt是一个众所周知的结构逐行读取文件在 shell 脚本中。但是在使用C风格的for循环(以防万一这里的一些用户不知道,它是andfor((i=0;i<=$val;i++))do;...done中使用的类型),我记得类C语言中的 while 和 for 循环是可以互换的,并且可以模拟一个其他。所以我想出了 C 风格的循环来模拟上面提到的结构。bashkshwhile IFS= read -r line

for((i=1;;i++))
do 
    IFS= read -r line || break
    # do something with line here
done < input.txt

我已经用多种类型的输入对其进行了测试 - 普通行、带有前导制表符/空格的行、不以 结尾的行(这是无法捕获最后一行的\n情况) - 在所有情况下,这都完全有效read与循环方法相同while。从技术上讲,这还有一个带变量的“内置”行计数器i

所以,问题是:是否有任何理由(除了不可移植到除 和 之外的其他 shell 之外kshbash避免使用这种方法?是否存在可能失败的情况?虽然这种方法效果很好,但我想知道在开始在我自己的脚本中积极使用这种方法之前是否忽略了任何问题。

答案1

有什么理由避免使用这种方法?

清晰度,或者缺乏清晰度。

while像这样的循环也while sometest ; do ...可以写成

while :; do 
    if ! sometest; then
        break
    fi
    ...

但我们不这样做(在 shell 中或在 C 中),因为它将循环条件移离了人们习惯于寻找它的位置。您的构造是相似的:您将构造的中间表达式留空for (( ; ; ))

当然你必须这样做,因为for (( ; ; ))在 shell 中只计算算术表达式,而不是像read.尽管forwhile在 C 中可以很容易地进行转换,但同样的情况在 shell 中并不适用,因为:它们做不同的事情。

(我想说,即使在 C 语言中,for循环的结构也暗示了计数循环,但当然它并不完全明确。)

至于这个:

从技术上讲,这还有一个带有 i 变量的“内置”行计数器。

我不认为其中有任何内置内容。您手动初始化了行计数器,并手动递增它。你可以用更传统的while循环做同样的事情

i=0
while IFS= read -r line; do 
    ... 
    let i++
done < input.txt

(或者i=$((i + 1))更便携)

答案2

我完全同意@ilkkachu 的观点。

但是 FWIW,为了能够将该命令用作条件read的一部分(该语法的来源),您可以使用规则:forksh93for ((...))

function read.get {
  IFS= read -r line
  .sh.value=$(($? == 0))
}

for ((i = 0; read; i++)) {
  printf '%5d: %s\n' "$i" "$line"
}

我们设置变量get的规则$read,以便在展开时read运行该命令,并在成功或失败$read时将其展开为 1 。read0

或者使用的变体类型

typeset -T read_t=(
  typeset value
  function get {
    IFS= read -r _.value
    ((.sh.value = $? == 0))
  }
)

read_t line
for ((i = 0; line; i++)) {
  printf '%5d: %s\n' "$i" "${line.value}"
}

其中read_t类型是一种对象,在扩展时会读入一行theobject.value,如果成功则扩展为 1 read,否则扩展为 0。

或者${ ...; }命令替换的形式:

for ((i = 0; ${ IFS= read -r line; echo "$(($? == 0))";}; i++)) {
  printf '%5d: %s\n' "$i" "$line"
}

zsh,劫持动态命名目录特征

set -o extendedglob
handle_-read:var()
  case $1:$2 in
    (d:-read:[a-zA-Z_][a-zA-Z0-9_]#)
      IFS= read -r ${2#*:} && reply=('' $#2);;
    (*) false;;
  esac

zsh_directory_name_functions+=(handle_-read:var)

for ((i = 0; ${#${(D):--read:line}} == 3; i++)) {
  printf '%5d: %s\n' "$i" "$line"
}

(并不是说我建议这样做)。

这是我知道可以将命令作为算术表达式的一部分执行的唯一方法不是在子外壳中。

普通命令替换 ($(...)`...`) 也可用于在算术表达式中运行命令,但它是在子 shell 中完成的,因此类似于:

for ((i = 0; $(IFS= read -r line; echo "$((!$?))"); i++)) {
  printf '%5d: %s\n' "$i" "$line"
}

虽然这对于 来说是有效的语法bash,但它无法$line在子 shell 之外设置变量。

但是,使用zsh,您可以执行以下操作:

for ((i = 0; ${${line::=$(IFS= read -re)}+$?} == 0; i++)) {
  printf '%5d: %s\n' "$i" "$line"
}

但这效率很低,因为它会为每条管线分出一个子壳,并通过额外的管道为管线供电。

bash没有${var::=value}无条件赋值参数扩展运算符,并且其嵌套参数扩展的能力非常有限。它确实有${var=value}Bourne 运算符(如果之前未设置则赋值),并且有一些运算符允许嵌套${foo#${bar}},例如 ,因此您可以执行以下操作:

unset line
for ((i = 0; 0*${?#"x${line=`IFS= read -r && printf %s "$REPLY"`}"}+$? == 0; i++)); do
  printf '%5d: %s\n' "$i" "$line"
  unset line
done

(这里必须解决的两个错误bash)。

相关内容