Erro na função shell para contar números pares

12

Para uma tarefa, tenho que escrever uma função que imprima o número de números pares quando fornecida com uma sequência de números.

Usei o código que usei para uma tarefa anterior (para imprimir 1quando um número era par e 0quando o número era ímpar)

Meu problema agora é que minha função continua imprimindo 0. O que estou fazendo de errado?

Aqui está o meu script:

#!/usr/bin/bash
# File: nevens.sh

# Write a function called nevens which prints the number of even numbers when provided with a sequence of numbers.
# Check: input nevens 42 6 7 9 33 = output 2

function nevens {

        local sum=0

        for element in $@
        do
                let evencheck=$(( $# % 2 ))
                if [[ $evencheck -eq 0 ]]
                then
                        let sum=$sum+1
                fi
        done

        echo $sum
}
Jedidja
fonte
2
escreva no seu shebang '#! / usr / bin / bash -x' e verá o que exatamente acontece.
Ziazis
+1 por nos dizer que é lição de casa - e por ter trabalhado duro o suficiente para merecer ajuda.
Joe

Respostas:

20

Você esqueceu de substituir $#por ( $) elementno forloop:

function nevens {
  local sum=0
  for element in $@; do
    let evencheck=$(( element % 2 ))
    if [[ $evencheck -eq 0 ]]; then
      let sum=sum+1
    fi
  done
  echo $sum
}

Agora, para testar a função:

$ nevens 42 6 7 9 33
2
$ nevens 42 6 7 9 33 22
3
$ nevens {1..10..2} # 1 to 10 step 2 → odd numbers only
0
$ nevens {2..10..2} # 2 to 10 step 2 → five even numbers
5
sobremesa
fonte
17

A @dessert encontrou o problema principal, darei uma revisão de código:

  1. O shebang: Não existe /usr/bin/bashno Ubuntu. É /bin/bash.
  2. É bom que você tenha declarado sum locale evitado poluir o espaço para nome da variável fora da função. Além disso, você pode declarar uma variável inteira usando a -iopção:

    local -i sum=0
  3. Sempre cite suas variáveis ​​(e parâmetros)! Não é necessário neste script, mas um hábito muito bom de entrar:

    for element in "$@"
    do

    Dito isto, você pode omitir o in "$@"aqui:

    for element
    do

    Quando in <something>não é fornecido, o forloop envolve implicitamente os argumentos. Isso pode evitar erros como esquecer as aspas.

  4. Não há necessidade de calcular e verificar o resultado. Você pode fazer o cálculo diretamente no if:

    if (( (element % 2) == 0 ))
    then
        ((sum = sum + 1))
    fi

    (( ... ))é o contexto aritmético . É mais útil do que [[ ... ]]para executar verificações aritméticas e, além disso, você pode omitir as $variáveis ​​anteriores (o que facilita a leitura, IMHO).

  5. Se você moveu a parte de verificação uniforme para uma função separada, isso pode melhorar a legibilidade e a reutilização:

    function evencheck
    {
        return $(( $1 % 2 ))
    }
    function nevens
    {
        local -i sum=0
        for element
        do
            # `if` implicitly checks that the returned value/exit status is 0
            if evencheck "$element"
            then
                (( sum++ ))
            fi
        done
        echo "$sum"
    }
muru
fonte
1
Se você estiver procurando por um uso do corpo do loop terser sum=$((sum + 1 - element % 2)).
David Foerster
1
@DavidFoerster Terser e menos legível. ;-)
Konrad Rudolph
@ DavidFoerster: Eu pensei que você deveria usar o resultado do mod-2 diretamente. (Ou melhor, &1para verificar a parte mais baixa, se for mais legível para você). Mas podemos torná-lo mais legível: sum=$((sum + !(element&1) ))usar um inverso booleano em vez de +1 - condition. Ou apenas conte os elementos ímpares com ((odd += element&1)), e no final imprima com echo $(($# - element)), porque even = total - odd.
Peter Cordes
Bom trabalho e bom apontar pequenas coisas que os iniciantes sentem falta, por exemplo, local -ie sum++.
Paddy Landau
4

Não tenho certeza se você está aberto a outras soluções. Também não sei se você pode usar utilitários externos ou se está puramente limitado a bash de componentes internos. Se você pode usar grep, por exemplo, sua função pode ser muito mais simples:

function nevens {
    printf "%s\n" "$@" | grep -c '[02468]$'
}

Isso coloca cada número inteiro de entrada em sua própria linha e depois grepconta as linhas que terminam em um dígito par.


Atualização - @PeterCordes apontou que podemos fazer isso sem grep - apenas bash puro, desde que a lista de entrada contenha apenas números inteiros bem formados (sem pontos decimais):

function nevens{
    evens=( ${@/%*[13579]/} )
    echo "${#evens[@]}"
}

Isso funciona criando uma lista chamada evens , filtrando todas as probabilidades e retornando o comprimento dessa lista.

Trauma Digital
fonte
Abordagem interessante. Claro que isso contará 3.0como um número par; a pergunta não era precisa sobre qual seria a forma dos números.
G-Man diz 'Reinstate Monica'
@ G-Man desde o bash não suporta aritmética de ponto flutuante, é seguro assumir inteiros
Muru
Como a pergunta menciona números "pares" (e, implicitamente, "ímpares"), é um pouco seguro assumir que se trata de números inteiros. Não vejo como é válido tirar conclusões sobre a questão a partir dos recursos e limitações da ferramenta que se espera que o usuário use. Lembre-se: a pergunta diz que essa é uma tarefa - acho que para a escola, porque isso é muito trivial para ser de um emprego. Os professores escolhem algumas coisas loucas.
G-Man diz 'Reinstate Monica'
2
O Bash pode filtrar uma lista por si só, se pudermos assumir que nenhum elemento contém espaço em branco: expanda a lista de argumentos com números ímpares substituídos pela sequência vazia usando ${/%/}a @matriz para exigir uma correspondência no final da sequência, dentro de um inicializador de matriz. Imprima a contagem. Como um one-liner para definir e executá-lo: foo(){ evens=( ${@/%*[13579]/} ); echo "${#evens[@]} even numbers"; printf "%s\n" "${evens[@]}"; }; foo 135 212 325 3 6 3 4 5 9 7 2 12310. Inclui realmente imprimir a lista para depuração. Prints 5 even numbers 212 6 4 2 12310(nas linhas de setembro)
Peter Cordes
1
Provavelmente o ZSH tem algo para filtrar adequadamente uma lista, em vez de depender da divisão de palavras para reconstruir uma nova matriz (fiquei um pouco surpreso com o bash não; apenas aplicando coisas de expansão de cadeia escalar a todos os elementos). Para listas grandes, eu não ficaria surpreso se grepfosse realmente mais rápido do que bash. Hmm, eu me pergunto se há uma pergunta codegolf onde eu poderia postar essa função bash: P
Peter Cordes