为什么我的 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 2 3 4的字符串。整个单行将由第一次调用into读取,并在函数调用中使用(不加引号)。由于是 未加引号的,因此 shell 会将字符串吐出到空格上,并且您的函数将仅使用该首字母,而该首字母不是偶数。这意味着未触发。"${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

这是在函数内使用两个名称引用变量filter。第一个是输入数组,第二个是输出数组。

这会给你输出

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

将值传递给filter函数的另一种方法显然是在函数的命令行上传递它们:

#!/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'sfilter将无法就地过滤数组;如果相同的数组被指定为源和目标,它只会截断它。

通过重写函数可以很容易地解决这个问题:

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]$'

相关内容