web-dev-qa-db-ja.com

bashのCスタイルのforループで「whileIFS = read-rline」をエミュレートする

最初の質問のちょっとした文脈。 _while IFS= read -r line; do ... done < input.txt_は、シェルスクリプトで ファイルを1行ずつ読み取る のよく知られた構造です。しかし、cスタイルのforループを使用してきました(ここのユーザーの一部が知らない場合は、bashkshで使用されるfor((i=0;i<=$val;i++))do;...doneタイプです)、 Cのような言語のwhileループとforループは交換可能であり、相互にエミュレートできることを思い出しました。そこで、上記の_while IFS= read -r line_構造をエミュレートするcスタイルのループを思いつきました。

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

複数のタイプの入力でテストしました-通常の行、先頭のタブ/スペースのある行、_\n_で終わらない行(これはreadがキャッチできない場合です最後の行)-そしてすべての場合において、これはwhileループアプローチとまったく同じように機能します。これには、技術的にはi変数を持つ「組み込み」ラインカウンターもあります。

したがって、問題はこれです:このアプローチの使用を避ける理由はありますか(kshbash以外の他のシェルへの非移植性以外に)?これが失敗する可能性がある場合はありますか?これはうまく機能しますが、自分のスクリプトでこのアプローチを積極的に使用し始める前に、見落としている問題があるかどうかを知りたいと思います。

6

このアプローチの使用を避ける理由はありますか?

明快さ、またはそれの欠如。

_while sometest ; do ..._のようなwhileループは次のように書くこともできます

_while :; do 
    if ! sometest; then
        break
    fi
    ...
_

しかし、ループ条件を人々がそれを探すのに慣れている場所から遠ざけるので、(シェルまたはCでは)それを行いません。構造は似ています。for (( ; ; ))構造の中央の式を空のままにしました。

もちろん、シェルのfor (( ; ; ))は算術式のみを評価し、readのようなコマンドは評価しないため、これを行う必要がありました。 forwhileはCでいくらか簡単に変換できますが、これが原因でシェルでは同じことが実際には当てはまりません。これらは異なることを行います。

(Cでも、その構造によるforループは、カウントループを少し示唆していますが、もちろん、正確には明確ではありません。)

これに関して:

これには、技術的にはi変数を備えた「組み込み」ラインカウンターもあります。

そこには何も組み込まれていないと思います。ラインカウンタを手動で初期化し、手動でインクリメントしました。従来のwhileループでも同じことができます

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

(またはi=$((i + 1))の移植性を高める)

5
ilkkachu

私はここで@ilkkachuに完全に同意します。

しかし、FWIWは、そのreadコマンドをfor条件の一部として、_ksh93_(for ((...))構文の由来)とともに使用できるようにするために、分野を使用できます:

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

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

_$read_変数のget規律を設定して、展開時にreadコマンドが実行され、readの場合は_$read_が1に展開されるようにします。成功したか、それ以外の場合は_0_。

またはtypesを使用するバリアント:

_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_に読み取り、readが成功した場合は1に展開し、それ以外の場合は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"
}
_

(私はそれをすることをお勧めしません)。

これが、サブシェルの算術式notの一部としてコマンドを実行できる場所を私が知っている唯一の方法です。

通常のコマンド置換($(...)または_`...`_)を使用して、算術式でコマンドを実行することもできますが、サブシェルで実行されるため、次のようになります。

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

bashの有効な構文ですが、サブシェルの外部に_$line_変数を設定することはできません。

ただし、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の2つのバグ )。

3