Teste se a string é um número inteiro válido

117

Estou tentando fazer algo bastante comum: analisar a entrada do usuário em um script de shell. Se o usuário forneceu um número inteiro válido, o script faz uma coisa e, se não for válido, faz outra. O problema é que não encontrei uma maneira fácil (e razoavelmente elegante) de fazer isso - eu não quero ter que separá-lo char por char.

Sei que deve ser fácil, mas não sei como. Eu poderia fazer isso em uma dúzia de idiomas, mas não em BASH!

Em minha pesquisa, descobri o seguinte:

Expressão regular para testar se uma string consiste em um número real válido na base 10

E há uma resposta aqui que fala sobre regex, mas até onde eu sei, essa é uma função disponível em C (entre outras). Ainda assim, ele teve o que parecia ser uma ótima resposta, então tentei com grep, mas grep não sabia o que fazer com ele. Tentei -P, que na minha caixa significa tratá-lo como uma expressão regular PERL - nada. Traço E (-E) também não funcionou. E nem -F.

Só para ficar claro, estou tentando algo assim, procurando qualquer saída - a partir daí, vou hackear o script para aproveitar o que conseguir. (IOW, eu esperava que uma entrada não conforme retornasse nada enquanto uma linha válida fosse repetida.)

snafu=$(echo "$2" | grep -E "/^[-+]?(?:\.[0-9]+|(?:0|[1-9][0-9]*)(?:\.[0-9]*)?)$/")
if [ -z "$snafu" ] ;
then
   echo "Not an integer - nothing back from the grep"
else
   echo "Integer."
fi

Alguém poderia ilustrar como isso é feito mais facilmente?

Francamente, isso é uma lacuna do TEST, na minha opinião. Deve ter uma bandeira como esta

if [ -I "string" ] ;
then
   echo "String is a valid integer."
else
   echo "String is not a valid integer."
fi
Richard T
fonte
4
FYI: [é compatível com o antigo test; [[é a novidade do Bash, com mais operações e diferentes regras de cotação. Se você já decidiu ficar com o Bash, vá em frente [[(é realmente muito melhor); se precisar de portabilidade para outros shells, evite [[completamente.
efemiente

Respostas:

183
[[ $var =~ ^-?[0-9]+$ ]]
  • O ^indica o início do padrão de entrada
  • O -é um "-" literal
  • Os ?meios "0 ou 1 do anterior ( -)"
  • Os +meios "1 ou mais das formas anteriores ( [0-9])"
  • O $indica o fim do padrão de entrada

Portanto, a regex corresponde a um opcional -(no caso de números negativos), seguido por um ou mais dígitos decimais.

Referências :

Ignacio Vazquez-Abrams
fonte
3
Obrigado Ignacio, vou tentar em um segundo. Você se importaria de explicar para que eu possa aprender um pouco? Percebi que diz: "No início da string (^), um sinal de menos (-) é opcional (?), Seguido por qualquer número de caracteres entre zero e 9, inclusive" ... e o que então o + $ significa? Obrigado.
Richard T
10
O +significa "1 ou mais do precedente" e $indica o fim do padrão de entrada. Portanto, a regex corresponde a um opcional -seguido por um ou mais dígitos decimais.
Ignacio Vazquez-Abrams
resmunga sobre o link ABS
Charles Duffy
É uma tangente, mas observe que, ao especificar intervalos de caracteres, você pode obter resultados estranhos; por exemplo, [A-z]não só dar-lhe A-Ze a-z, mas também \ , [, ], ^, _, e `.
Doktor J
Além disso, com base no agrupamento de caracteres ( consulte esta pergunta / resposta relacionada ), algo como d[g-i]{2}pode terminar não apenas em correspondência, digmas também dishno agrupamento sugerido por essa resposta (onde o shdígrafo é considerado um único caractere, agrupado depois h).
Doktor J
61

Uau ... existem tantas soluções boas aqui !! De todas as soluções acima, concordo com @nortally que usar -equm liner é o mais legal.

Estou executando o GNU bash, versão 4.1.5(Debian). Eu também verifiquei isso no ksh (SunSO 5.10).

Aqui está minha versão para verificar se $1é um número inteiro ou não:

if [ "$1" -eq "$1" ] 2>/dev/null
then
    echo "$1 is an integer !!"
else
    echo "ERROR: first parameter must be an integer."
    echo $USAGE
    exit 1
fi

Esta abordagem também leva em conta os números negativos, que algumas das outras soluções terão um resultado negativo defeituoso e permitirá um prefixo de "+" (por exemplo, +30) que obviamente é um número inteiro.

Resultados:

$ int_check.sh 123
123 is an integer !!

$ int_check.sh 123+
ERROR: first parameter must be an integer.

$ int_check.sh -123
-123 is an integer !!

$ int_check.sh +30
+30 is an integer !!

$ int_check.sh -123c
ERROR: first parameter must be an integer.

$ int_check.sh 123c
ERROR: first parameter must be an integer.

$ int_check.sh c123
ERROR: first parameter must be an integer.

A solução fornecida por Ignacio Vazquez-Abrams também foi muito legal (se você gosta de regex) depois que foi explicada. No entanto, ele não lida com números positivos com o +prefixo, mas pode ser facilmente corrigido como a seguir:

[[ $var =~ ^[-+]?[0-9]+$ ]]
Peter Ho
fonte
Agradável! Bem parecido com isso , no entanto.
devnull 01 de
Sim. É semelhante. No entanto, eu estava procurando uma solução de uma linha para a instrução "if". Eu pensei que realmente não preciso chamar uma função para isso. Além disso, posso ver que o redirecionamento do stderr para stdout na função. Quando tentei, a mensagem stderr "expressão inteira esperada" foi exibida, o que não era desejável para mim.
Peter Ho
Obrigado! Eu diria que este é fácil e elegante.
Ezra Nugroho
2
Há uma distinção notável entre a sua solução e a regex: o tamanho do inteiro é verificado em relação aos limites do bash (no meu computador é de 64 bits). Este limite não atinge a solução regexp. Portanto, sua solução falhará em um número estritamente maior que 9223372036854775807 em computadores de 64 bits.
vaab
2
Como descobri recentemente, existem algumas ressalvas .
Kyle Strand
28

Um retardatário para a festa aqui. Estou extremamente surpreso por nenhuma das respostas mencionar a solução mais simples, rápida e portátil; a casedeclaração.

case ${variable#[-+]} in
  *[!0-9]* | '') echo Not a number ;;
  * ) echo Valid number ;;
esac

O corte de qualquer sinal antes da comparação parece um pouco um hack, mas isso torna a expressão para a declaração de caso muito mais simples.

triplo
fonte
4
Eu gostaria de poder votar a favor disso uma vez toda vez que volto a esta questão por causa de tolos. É difícil para mim que uma solução simples, mas compatível com POSIX, esteja enterrada no fundo.
Adrian Frühwirth
3
Talvez você deve cuidar de cadeias vazias:''|*[!0-9]*)
Niklas Peter
2
BTW: Aqui está esta sintaxe documentada: tldp.org/LDP/abs/html/string-manipulation.html
Niklas Peter
Eu particularmente não tolero o ABS; isso obviamente também está documentado no manual do Bash. De qualquer forma, a seção que você vinculou não descreve esta construção em particular, mas sim a resposta de @Nortally.
tripleee
@tripleee O documento vinculado descreve a construção para remover um prefixo de string de uma variável usada na linha do caso. Ele está bem no final da página, mas não há âncoras, então não pude criar um link direto para ele, consulte a seção "Remoção de substring"
Niklas Peter
10

Eu gosto da solução usando o -eqteste, porque é basicamente um one-liner.

Minha própria solução foi usar a expansão dos parâmetros para jogar fora todos os numerais e ver se restava algo. (Ainda estou usando 3.0, não usei [[nem exprantes, mas fico feliz em conhecê-los.)

if [ "${INPUT_STRING//[0-9]}" = "" ]; then
  # yes, natural number
else
  # no, has non-numeral chars
fi
no norte
fonte
4
Isso pode ser melhorado ainda mais usando, [ -z "${INPUT_STRING//[0-9]}" ]mas uma solução muito boa!
ShellFish
e os sinais negativos?
scottysseus
A -eqsolução tem alguns problemas; veja aqui: stackoverflow.com/a/808740/1858225
Kyle Strand
INPUT_STRING vazio é considerado um número, portanto, falha no meu caso
Manwe
9

Para portabilidade para pré-Bash 3.1 (quando o =~ teste foi introduzido), use expr.

if expr "$string" : '-\?[0-9]\+$' >/dev/null
then
  echo "String is a valid integer."
else
  echo "String is not a valid integer."
fi

expr STRING : REGEXprocura REGEX ancorado no início de STRING, ecoando o primeiro grupo (ou duração da correspondência, se nenhuma) e retornando sucesso / falha. Esta é uma sintaxe regex antiga, daí o excesso \. -\?significa "talvez -", [0-9]\+significa "um ou mais dígitos" e $significa "fim da string".

O Bash também oferece suporte a globs estendidos, embora não me lembre de qual versão em diante.

shopt -s extglob
case "$string" of
    @(-|)[0-9]*([0-9]))
        echo "String is a valid integer." ;;
    *)
        echo "String is not a valid integer." ;;
esac

# equivalently, [[ $string = @(-|)[0-9]*([0-9])) ]]

@(-|)significa " -ou nada", [0-9]significa "dígito" e *([0-9])significa "zero ou mais dígitos".

efêmero
fonte
Obrigado efêmero, muito obrigado. Eu nunca tinha visto a sintaxe = ~ antes - e ainda não tenho ideia do que ela significa - aproximadamente igual ?! ... Nunca fiquei animado para programar em BASH mas às vezes é necessário!
Richard T
Em awk, ~foi o operador "correspondência de regex". Em Perl (como copiado de C), ~já era usado para "complemento de bits", então eles usaram =~. Esta notação posterior foi copiada para vários outros idiomas. (Perl 5.10 e Perl 6 gostam ~~mais, mas isso não tem impacto aqui.) Suponho que você possa olhar para isso como uma espécie de igualdade aproximada ...
efêmero
Excelente postagem e edição! Eu realmente aprecio explicar o que isso significa. Eu gostaria de poder marcar as suas postagens e as de Ignacio como a resposta correta. -Frown- Vocês são ótimos. Mas como você tem o dobro da reputação dele, estou passando para Ignacio - espero que você entenda! -smile-
Richard T
4

Aqui está mais uma abordagem sobre ele (usando apenas o comando test builtin e seu código de retorno):

function is_int() { return $(test "$@" -eq "$@" > /dev/null 2>&1); } 

input="-123"

if $(is_int "${input}");
then
   echo "Input: ${input}"
   echo "Integer: $[${input}]"
else
   echo "Not an integer: ${input}"
fi
Hans
fonte
1
Não é necessário usar $()com if. Isso funciona: if is_int "$input". Além disso, o $[]formulário está obsoleto. Use em seu $(())lugar. Dentro de qualquer um deles, o cifrão pode ser omitido: echo "Integer: $((input))"colchetes não são necessários em nenhum lugar do seu script.
Pausado até novo aviso.
Eu esperava que isso também manipulasse números na notação de base do Bash como inteiros válidos (o que é claro por alguma definição; mas pode não concordar com a sua), mas testnão parece apoiar isso. [[faz, no entanto. [[ 16#aa -eq 16#aa ]] && echo integerimprime "inteiro".
tripleee
Observe que [[retorna falsos positivos para este método; por exemplo, [[ f -eq f ]]é bem-sucedido. Portanto, deve usar testou [.
spinup
3

Você pode retirar os não dígitos e fazer uma comparação. Aqui está um script de demonstração:

for num in "44" "-44" "44-" "4-4" "a4" "4a" ".4" "4.4" "-4.4" "09"
do
    match=${num//[^[:digit:]]}    # strip non-digits
    match=${match#0*}             # strip leading zeros
    echo -en "$num\t$match\t"
    case $num in
        $match|-$match)    echo "Integer";;
                     *)    echo "Not integer";;
    esac
done

Esta é a aparência da saída do teste:

44 44 Inteiro
-44 44 Inteiro
44- 44 Não inteiro
4-4 44 Não é inteiro
a4 4 Não é inteiro
4a 4 Não é inteiro
.4 4 Não é inteiro
4,4 44 Não inteiro
-4,4 44 Não inteiro
09 9 Não inteiro
Pausado até novo aviso.
fonte
Olá, Dennis, Obrigado por me apresentar à sintaxe à direita de match = acima. Eu nunca havia percebido essa sintaxe de tipo antes. Eu reconheço parte da sintaxe de tr (um utilitário que ainda não domino, mas às vezes me atrapalho); onde posso ler sobre essa sintaxe? (ou seja, como é chamado esse tipo de coisa?) Obrigado.
Richard T de
Você pode procurar na página de manual do Bash na seção chamada "Expansão de parâmetro" para obter informações sobre ${var//string}e ${var#string}e na seção chamada "Correspondência de padrões" para [^ [: digit:]] `(que também é abordado em man 7 regex).
Pausado até novo aviso.
1
match=${match#0*}que não tira zeros à esquerda, ele retira no máximo um zero. Usando a expansão, isso só pode ser feito usando extglobvia match=${match##+(0)}.
Adrian Frühwirth
9 ou 09 não é um número inteiro?
Mike Q
@MikeQ: 09não é um número inteiro se você considerar que um número inteiro não tem zeros à esquerda. O teste é se o input ( 09) é igual a uma versão higienizada ( 9- um inteiro) e não.
Pausado até novo aviso.
2

Para mim, a solução mais simples era usar a variável dentro de uma (())expressão, assim:

if ((VAR > 0))
then
  echo "$VAR is a positive integer."
fi

Obviamente, essa solução só é válida se um valor zero não fizer sentido para o seu aplicativo. Aconteceu que isso era verdade no meu caso, e isso é muito mais simples do que as outras soluções.

Conforme apontado nos comentários, isso pode torná-lo sujeito a um ataque de execução de código: O (( ))operador avalia VAR, conforme declarado na Arithmetic Evaluationseção de página do manual bash (1) . Portanto, você não deve usar esta técnica quando a fonte do conteúdo VARé incerta (nem deve usar QUALQUER outra forma de expansão de variável, é claro).

Trebor Rude
fonte
Você pode ainda ir mais simples comif (( var )); then echo "$var is an int."; fi
Aaron R.
2
Mas isso também retornará verdadeiro para inteiros negativos, @aaronr, não o que o OP estava procurando.
Trebor Rude,
2
Isso é perigoso, consulte: n = 1; var = "n"; if ((var)); então echo "$ var é um int."; fi
jarno
2
Esta é uma idéia muito ruim e sujeito a execução de código arbitrário: experimentar por si mesmo: VAR='a[$(ls)]'; if ((VAR > 0)); then echo "$VAR is a positive integer"; fi. Neste ponto, você está feliz por eu não ter inserido algum comando maligno em vez de ls. Como o OP menciona a entrada do usuário , eu realmente espero que você não esteja usando isso com a entrada do usuário no código de produção!
gniourf_gniourf
Isso não funciona se a string contém alguns dígitos como:agent007
brablc
1

ou com sed:

   test -z $(echo "2000" | sed s/[0-9]//g) && echo "integer" || echo "no integer"
   # integer

   test -z $(echo "ab12" | sed s/[0-9]//g) && echo "integer" || echo "no integer"
   # no integer
Knipwim
fonte
No Bash e em alguns outros shells "Bourne plus", você pode evitar a substituição do comando e o comando externo com test -z "${string//[0-9]/}" && echo "integer" || echo "no integer"... embora isso basicamente duplique a resposta de Dennis Williamson
tripleee
Obrigado! A única resposta que realmente funciona por aqui!
usuário
Alternativa silenciosa:if [[ -n "$(printf "%s" "${2}" | sed s/[0-9]//g)" ]]; then
usuário
0

Somando-se à resposta de Ignacio Vazquez-Abrams. Isso permitirá que o sinal + preceda o número inteiro e permitirá qualquer número de zeros como casas decimais. Por exemplo, isso permitirá que +45,00000000 seja considerado um número inteiro.
No entanto, $ 1 deve ser formatado para conter um ponto decimal. 45 não é considerado um número inteiro aqui, mas 45,0 é.

if [[ $1 =~ ^-?[0-9]+.?[0]+$ ]]; then
    echo "yes, this is an integer"
elif [[ $1 =~ ^\+?[0-9]+.?[0]+$ ]]; then
    echo "yes, this is an integer"
else
    echo "no, this is not an integer"
fi
JustinMT
fonte
Existe uma razão para você usar duas expressões regulares diferentes para números positivos e negativos, em vez de ^[-+]?[0-9]...?
tripleee
0

Para rir, eu desenvolvi quase que rapidamente um conjunto de funções para fazer isso (is_string, is_int, is_float, é string alfa ou outro), mas existem maneiras mais eficientes (menos código) de fazer isso:

#!/bin/bash

function strindex() {
    x="${1%%$2*}"
    if [[ "$x" = "$1" ]] ;then
        true
    else
        if [ "${#x}" -gt 0 ] ;then
            false
        else
            true
        fi
    fi
}

function is_int() {
    if is_empty "${1}" ;then
        false
        return
    fi
    tmp=$(echo "${1}" | sed 's/[^0-9]*//g')
    if [[ $tmp == "${1}" ]] || [[ "-${tmp}" == "${1}" ]] ; then
        #echo "INT (${1}) tmp=$tmp"
        true
    else
        #echo "NOT INT (${1}) tmp=$tmp"
        false
    fi
}

function is_float() {
    if is_empty "${1}" ;then
        false
        return
    fi
    if ! strindex "${1}" "-" ; then
        false
        return
    fi
    tmp=$(echo "${1}" | sed 's/[^a-z. ]*//g')
    if [[ $tmp =~ "." ]] ; then
        #echo "FLOAT  (${1}) tmp=$tmp"
        true
    else
        #echo "NOT FLOAT  (${1}) tmp=$tmp"
        false
    fi
}

function is_strict_string() {
    if is_empty "${1}" ;then
        false
        return
    fi
    if [[ "${1}" =~ ^[A-Za-z]+$ ]]; then
        #echo "STRICT STRING (${1})"
        true
    else
        #echo "NOT STRICT STRING (${1})"
        false
    fi
}

function is_string() {
    if is_empty "${1}" || is_int "${1}" || is_float "${1}" || is_strict_string "${1}" ;then
        false
        return
    fi
    if [ ! -z "${1}" ] ;then
        true
        return
    fi
    false
}
function is_empty() {
    if [ -z "${1// }" ] ;then
        true
    else
        false
    fi
}

Executei alguns testes aqui, eu defini que -44 é um int, mas 44- não é etc.:

for num in "44" "-44" "44-" "4-4" "a4" "4a" ".4" "4.4" "-4.4" "09" "hello" "h3llo!" "!!" " " "" ; do
    if is_int "$num" ;then
        echo "INT = $num"

    elif is_float "$num" ;then
        echo "FLOAT = $num"

    elif is_string "$num" ; then
        echo "STRING = $num"

    elif is_strict_string "$num" ; then
        echo "STRICT STRING = $num"
    else
        echo "OTHER = $num"
    fi
done

Resultado:

INT = 44
INT = -44
STRING = 44-
STRING = 4-4
STRING = a4
STRING = 4a
FLOAT = .4
FLOAT = 4.4
FLOAT = -4.4
INT = 09
STRICT STRING = hello
STRING = h3llo!
STRING = !!
OTHER =  
OTHER = 

NOTA: Os zeros iniciais podem inferir outra coisa ao adicionar números, como octal, então seria melhor removê-los se você pretende tratar '09' como um int (o que estou fazendo) (por exemplo, expr 09 + 0ou tira com sed)

Mike Q
fonte