Bashフィルタ機能が機能しないのはなぜですか?

Bashフィルタ機能が機能しないのはなぜですか?

私は書いたフィルター/選ぶ関数とストリームを入力として使用する関数です。新しい配列(2 4)を作成する必要があります。しかし、私の結果は何もありませんでした。私はこれがIFSに関連していると思います。

# func int -> bool
is_even () { (( $1 % 2 == 0 )) ;}

# func func -> int
filter () {
  local function_to_apply=$1
  local arg

  while read -r arg; do
    $function_to_apply $arg && echo $arg
  done;:
}

# int array
integers=( 1 2 3 4 )  
result=$(echo "${integers[*]}" | filter is_even)
declare -p result

出力は文字列です。""

declare -- result=""

期待される出力は配列です。( 2 4 )

declare -a result ='([0]="2" [1]="4")'

クレジットが必要な場所にクレジットを提供する:

http://www.binaryphile.com/bash/2018/07/26/approach-bash-like-a-developer-part-1-intro.html

答え1

1行に関数入力を提供できます。この行は1 2 3 4デフォルト値に拡張された文字列です。最初の呼び出しで単一行全体を読み取り、関数呼び出しで(引用符なしで)使用します。引用符がないため、シェルはスペースに文字列を表示し、関数は偶数ではなく最初の文字のみを使用します。これはトリガーされていないことを意味します。"${integers[*]}"$IFSreadarg$arg1echo $arg

代わりに:

#!/bin/bash

is_even () { (( $1 % 2 == 0 )) ;}

filter () {
  local function_to_apply="$1"
  local arg

  while read -r arg; do
    "$function_to_apply" "$arg" && echo "$arg"
  done
}

integers=( 1 2 3 4 )  
result=$(printf '%s\n' "${integers[@]}" | filter is_even)
declare -p result

ここでの主な作業は、配列の要素を関数filterの個々の行に印刷することです。

これにより、単一の文字列2\n4\n改行文字がある場所)が提供されます。に文字列を割り当てるとき、これは驚くべきことではありませんresult

アレイを再インポートするには、最新バージョンでこれを実行できますbash

#!/bin/bash

is_even () { (( $1 % 2 == 0 )) ;}

filter () {
  local func="$1"
  local -n in_array="$2"
  local -n out_array="$3"

  local element

  out_array=()
  for element in "${in_array[@]}"; do
    if "$func" "$element"; then
        out_array+=( "$element" )
    fi
  done
}

integers=( 1 2 3 4 )
even_ints=()

filter is_even integers even_ints
declare -p even_ints

これは、関数内で変数を参照するために2つの名前を使用することですfilter。 1つ目は入力配列、2つ目は出力配列です。

これはあなたに出力を与えるでしょう

declare -a even_ints=([0]="2" [1]="4")

関数に値を渡すもうfilter1つの方法は、明らかに関数のコマンドラインから値を渡すことです。

#!/bin/bash

is_even () { (( $1 % 2 == 0 )) ;}

filter () {
  local func="$1"
  local -n out_array="$2"
  shift 2

  local element

  out_array=()
  for element do
    if "$func" "$element"; then
        out_array+=( "$element" )
    fi
  done
}

integers=( 1 2 3 4 )
even_ints=()

filter is_even even_ints "${integers[@]}"
declare -p even_ints

答え2

1.@Kusalanandaのfilter配列をその場でフィルタリングする方法はありません。同じ配列がソースとターゲットとして指定されている場合、配列は切り捨てられます。

この問題は、関数を書き換えることで簡単に解決できます。

filter() {
        local cb=$1 i j a; local -n src=$2 dst=$3
        for a in "${src[@]}"; do
                "$cb" "$a" && dst[i++]=$a
        done
        for((j=${#dst[@]}; j>=i; j--)); do
                unset dst[j]
        done
}

2.しかし、これらのデモ/チャレンジの課題(例:「bashで関数型プログラミングを実行する方法」)完全に無意味。 Perlやjavascriptのような他の非常に高いレベルの言語と比較して、bashは非常に遅いです。他のユーティリティを呼び出すのではなく、bashを使用してデータを解析/フィルタリングすると、速度がはるかにgrep遅くなる可能性があります。

質問の質問を使用すると、1000要素のマイナーな配列とフィルタリング時にもgrepソリューションが3倍速くなります。bash自身の配列、外部ファイルではありません。ただし、3つのプロセスをフォークして外部バイナリを実行する必要があります。以下の例を参照してください(100000要素の配列を使用)。

bash filter.sh 100000
=== bash_filter ===

real    0m1.037s
user    0m1.036s
sys     0m0.001s
=== grep_filter ===

real    0m0.302s
user    0m0.226s
sys     0m0.189s

フィルタ.sh:

is_even () { (( $1 % 2 == 0 )) ;}
bash_filter() {
    local cb=$1 i j a; local -n src=$2 dst=$3
    for a in "${src[@]}"; do
        "$cb" "$a" && dst[i++]=$a
    done
    for((j=${#dst[@]}; j>=i; j--)); do
        unset dst[j]
    done
}
grep_filter() {
    local flt=$1; local -n src=$2 dst=$3
    dst=($(printf '%d\n' "${src[@]}" | grep "$flt"))

}
timeit(){
    echo "=== $1 ==="
    time "$@" inlist outlist
    [ "${#outlist[@]}" -lt 20 ] && echo "${outlist[@]}"
}

inlist=($(seq "${1-100000}"))
timeit bash_filter is_even
timeit grep_filter '[02468]$'

関連情報