Verifica se la stringa è un numero intero valido


117

Sto cercando di fare qualcosa di abbastanza comune: analizzare l'input dell'utente in uno script di shell. Se l'utente ha fornito un numero intero valido, lo script fa una cosa e, se non è valido, fa qualcos'altro. Il problema è che non ho trovato un modo semplice (e ragionevolmente elegante) per farlo - non voglio doverlo separare carattere per carattere.

So che dev'essere facile ma non so come. Potrei farlo in una dozzina di lingue, ma non BASH!

Nella mia ricerca ho trovato questo:

Espressione regolare per verificare se una stringa è costituita da un numero reale valido in base 10

E c'è una risposta in essa che parla di regex, ma per quanto ne so, questa è una funzione disponibile in C (tra le altre). Tuttavia, aveva quella che sembrava un'ottima risposta, quindi l'ho provato con grep, ma grep non sapeva cosa farne. Ho provato -P che sulla mia scatola significa trattarlo come una regexp PERL - nada. Anche il trattino E (-E) non ha funzionato. E nemmeno -F.

Giusto per essere chiari, sto provando qualcosa di simile, cercando qualsiasi output - da lì, modificherò lo script per sfruttare tutto ciò che ottengo. (IOW, mi aspettavo che un input non conforme non restituisse nulla mentre una riga valida viene ripetuta.)

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

Qualcuno potrebbe illustrare come è più facile farlo?

Francamente, questo è un difetto del TEST, secondo me. Dovrebbe avere una bandiera come questa

if [ -I "string" ] ;
then
   echo "String is a valid integer."
else
   echo "String is not a valid integer."
fi

4
FYI: [è vecchio compatibile test; [[è la novità di Bash, con più operazioni e diverse regole di citazione. Se hai già deciso di restare con Bash, prova [[(è davvero molto più bello); se hai bisogno di portabilità ad altri gusci, evita [[completamente.
effimero

Risposte:


183
[[ $var =~ ^-?[0-9]+$ ]]
  • Il ^indica l'inizio del pattern di input
  • Il -è un letterale "-"
  • Il ?mezzo "0 o 1 del precedente ( -)"
  • Il +mezzo "1 o più dei precedenti ( [0-9])"
  • Il $indica la fine del pattern di input

Quindi la regex corrisponde a un opzionale -(per il caso di numeri negativi), seguito da una o più cifre decimali.

Riferimenti :


3
Grazie Ignacio, lo proverò tra un secondo. Ti dispiacerebbe spiegarlo così posso imparare un po '? Capisco che si legge: "All'inizio della stringa (^), un segno meno (-) è opzionale (?), Seguito da un numero qualsiasi di caratteri compreso tra zero e 9, inclusi" ... e allora cosa potrebbe fare il segno + $ significa? Grazie.
Richard T,

10
I +mezzi 1 "o più delle precedenti", e $indica la fine del pattern di input. Quindi la regex corrisponde a un opzionale -seguito da una o più cifre decimali.
Ignacio Vazquez-Abrams,

brontola riguardo: il collegamento ABS
Charles Duffy

È una tangente, ma nota che quando specifichi intervalli di caratteri puoi ottenere risultati dispari; per esempio, [A-z]non sarebbe solo dare A-Ze a-zma anche \ , [, ], ^, _, e `.
Doktor J

Inoltre, in base alle regole di confronto dei caratteri ( vedi questa domanda / risposta correlata ) qualcosa di simile d[g-i]{2}potrebbe finire non solo per corrispondere digma anche dishnella raccolta suggerita da quella risposta (dove il shdigrafo è considerato un singolo carattere, raccolto dopo h).
Doktor J

61

Wow ... ci sono così tante buone soluzioni qui !! Di tutte le soluzioni di cui sopra, sono d'accordo con @nortally sul fatto che l'uso di -equn rivestimento sia il più interessante.

Sto eseguendo GNU bash, versione 4.1.5(Debian). L'ho verificato anche su ksh (SunSO 5.10).

Ecco la mia versione per verificare se $1è un numero intero o meno:

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

Questo approccio tiene conto anche dei numeri negativi, che alcune delle altre soluzioni avranno un risultato negativo errato, e consentirà un prefisso "+" (es. +30) che ovviamente è un numero intero.

risultati:

$ 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.

La soluzione fornita da Ignacio Vazquez-Abrams è stata anche molto chiara (se ti piacciono le espressioni regolari) dopo che è stata spiegata. Tuttavia, non gestisce numeri positivi con il +prefisso, ma può essere facilmente risolto come di seguito:

[[ $var =~ ^[-+]?[0-9]+$ ]]

Bello! Piuttosto simile a questo , però.
devnull

Sì. È simile. Tuttavia, stavo cercando una soluzione di una riga per l'istruzione "if". Ho pensato che non ho davvero bisogno di chiamare una funzione per questo. Inoltre, posso vedere che il reindirizzamento dello stderr a stdout nella funzione. Quando ho provato, è stato visualizzato il messaggio stderr "espressione intera prevista" che non era desiderabile per me.
Peter Ho

Grazie! Lo definirei facile ed elegante.
Ezra Nugroho

2
C'è una notevole distinzione tra la tua soluzione e quella regex: la dimensione dell'intero viene controllata verso i limiti di bash (sul mio computer è 64 bit). Questo limite non colpisce la soluzione regexp. Quindi la tua soluzione fallirà su un numero strettamente maggiore di 9223372036854775807 sui computer a 64 bit.
vaab

2
Come ho scoperto di recente, ci sono alcuni avvertimenti .
Kyle Strand

28

Ritardatario alla festa qui. Sono estremamente sorpreso che nessuna delle risposte menziona la soluzione più semplice, veloce e portatile; la casedichiarazione.

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

Il taglio di qualsiasi segno prima del confronto sembra un po 'un trucco, ma ciò rende l'espressione per la dichiarazione del caso molto più semplice.


4
Vorrei poter votare a favore una volta ogni volta che torno su questa domanda a causa degli stupidi. Mi spaventa il fatto che una soluzione semplice ma conforme a POSIX sia sepolta nel fondo.
Adrian Frühwirth

3
Forse dovresti occuparti delle stringhe vuote:''|*[!0-9]*)
Niklas Peter

2
BTW: ecco questa sintassi documentata: tldp.org/LDP/abs/html/string-manipulation.html
Niklas Peter

Non perdono particolarmente l'ABS; questo è ovviamente documentato anche nel manuale di Bash. Ad ogni modo, la sezione a cui ti sei collegato non descrive questo particolare costrutto, ma piuttosto, ad esempio, la risposta di @ Nortally.
tripleee

@tripleee Il documento collegato descrive il costrutto per rimuovere un prefisso di stringa da una variabile utilizzata nella riga del caso. È solo in fondo alla pagina, ma non ci sono ancore, quindi non posso collegarmi direttamente ad esso, vedere la sezione "Rimozione di sottostringa"
Niklas Peter

10

Mi piace la soluzione che utilizza il -eqtest, perché è fondamentalmente un one-liner.

La mia soluzione è stata quella di utilizzare l'espansione dei parametri per buttare via tutti i numeri e vedere se era rimasto qualcosa. (Sto ancora usando 3.0, non ho usato [[oexpr prima, ma sono contento di incontrarli.)

if [ "${INPUT_STRING//[0-9]}" = "" ]; then
  # yes, natural number
else
  # no, has non-numeral chars
fi

4
Questo può essere ulteriormente migliorato usando [ -z "${INPUT_STRING//[0-9]}" ]una soluzione davvero carina!
ShellFish

che dire dei segni negativi?
scottysseus

La -eqsoluzione presenta alcuni problemi; vedi qui: stackoverflow.com/a/808740/1858225
Kyle Strand

INPUT_STRING vuoto è considerato come numero, quindi non funziona nel mio caso
Manwe

9

Per la portabilità a pre-Bash 3.1 (quando =~è stato introdotto il test), utilizzare 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 : REGEXcerca REGEX ancorato all'inizio di STRING, facendo eco al primo gruppo (o alla lunghezza della corrispondenza, se nessuna) e restituendo successo / fallimento. Questa è la vecchia sintassi regex, da cui l'eccesso \. -\?significa "forse -", [0-9]\+significa "una o più cifre" e $significa "fine della stringa".

Bash supporta anche i glob estesi, anche se non ricordo da quale versione in poi.

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 " -o niente", [0-9]significa "cifra" e *([0-9])significa "zero o più cifre".


Grazie effimero, molto grato. Non avevo mai visto la sintassi = ~ prima - e ancora non ho idea di cosa dovrebbe significare - approssimativamente uguale ?! ... non sono mai stato entusiasta di programmare in BASH ma a volte è necessario!
Richard T

In awk, ~era l'operatore "corrispondenza regex". In Perl (come copiato da C), ~era già usato per "bit complement", quindi usavano =~. Questa notazione successiva è stata copiata in molte altre lingue. (Perl 5.10 e Perl 6 piace di ~~più, ma questo non ha alcun impatto qui.) Suppongo che potresti vederlo come una sorta di uguaglianza approssimativa ...
effimero

Post eccellente e modifica! Apprezzo molto spiegare cosa significa. Vorrei poter contrassegnare sia il tuo post che quello di Ignacio come LA risposta corretta. -frown- Siete entrambi fantastici. Ma dato che hai il doppio della reputazione che ha lui, la sto dando a Ignacio - spero tu capisca! -smile-
Richard T

4

Ecco un'altra versione (usando solo il comando integrato test e il suo codice di ritorno):

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

1
Non è necessario utilizzare $()con if. Questo funziona: if is_int "$input". Inoltre, il $[]modulo è deprecato. Usa $(())invece. All'interno di entrambi, il segno del dollaro può essere omesso: le echo "Integer: $((input))"parentesi graffe non sono necessarie da nessuna parte nel tuo script.
In pausa fino a nuovo avviso.

Mi sarei aspettato che questo gestisse anche i numeri nella notazione di base di Bash come interi validi (che ovviamente per qualche definizione sono; ma potrebbe non essere d'accordo con il tuo) ma testnon sembra supportarlo. [[sì, però. [[ 16#aa -eq 16#aa ]] && echo integerstampa "intero".
tripleee

Nota che [[restituisce falsi positivi per questo metodo; ad esempio, [[ f -eq f ]]riesce. Quindi deve usare testo [.
spinup

3

Puoi rimuovere le non cifre e fare un confronto. Ecco uno script demo:

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

Questo è l'aspetto dell'output di prova:

44 44 Intero
-44 44 Intero
44- 44 Non intero
4-4 44 Non intero
a4 4 Non intero
4a 4 Non intero
.4 4 Non intero
4.4 44 Non intero
-4,4 44 Non intero
09 9 Non intero

Ciao Dennis, grazie per avermi fatto conoscere la sintassi a destra di match = sopra. Non ho mai notato prima quella sintassi di tipo. Riconosco parte della sintassi da tr (un'utilità che non ho imparato a padroneggiare, ma a volte mi faccio strada a tentoni); dove posso leggere su tale sintassi? (cioè, come si chiama questo tipo di cose?) Grazie.
Richard T

Puoi guardare nella pagina man di Bash nella sezione chiamata "Parameter Expansion" per informazioni su ${var//string}e ${var#string}e nella sezione chiamata "Pattern Matching" per [^ [: digit:]] `(che è anche trattato in man 7 regex).
In pausa fino a nuovo avviso.

1
match=${match#0*}non non striscia zeri, le strisce al più uno zero. Usando l'espansione questo può essere ottenuto solo usando extglobvia match=${match##+(0)}.
Adrian Frühwirth

9 o 09 non è un numero intero?
Mike Q

@ MikeQ: 09non è un numero intero se si considera che un numero intero non ha zeri iniziali. Il test è se input ( 09) è uguale a una versione disinfettata ( 9- un numero intero) e non lo è.
In pausa fino a nuovo avviso.

2

Per me, la soluzione più semplice era usare la variabile all'interno di (())un'espressione, in questo modo:

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

Naturalmente, questa soluzione è valida solo se un valore zero non ha senso per la tua applicazione. Questo è successo nel mio caso, e questo è molto più semplice delle altre soluzioni.

Come sottolineato nei commenti, questo può renderti soggetto ad un attacco di esecuzione di codice: L' (( ))operatore valuta VAR, come indicato nella Arithmetic Evaluationsezione della pagina man bash (1) . Pertanto, non dovresti usare questa tecnica quando l'origine dei contenuti di VARè incerta (né dovresti usare QUALSIASI altra forma di espansione variabile, ovviamente).


Puoi anche andare più semplice conif (( var )); then echo "$var is an int."; fi
Aaron R.

2
Ma ciò restituirà vero anche per numeri interi negativi, @aaronr, non ciò che l'OP stava cercando.
Trebor Rude

2
Questo è pericoloso, vedi: n = 1; var = "n"; if ((var)); then echo "$ var è un int."; fi
jarno

2
Questa è una pessima idea e oggetto di esecuzione di codice arbitrario: provate voi stessi: VAR='a[$(ls)]'; if ((VAR > 0)); then echo "$VAR is a positive integer"; fi. A questo punto sei contento che non abbia inserito un comando malvagio invece di ls. Poiché OP menziona l'input dell'utente , spero davvero che tu non lo utilizzi con l'input dell'utente nel codice di produzione!
gniourf_gniourf

Questo non funziona se la stringa contiene alcune cifre come:agent007
brablc

1

o con 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

In Bash e in altre shell "Bourne plus" puoi evitare la sostituzione del comando e il comando esterno con test -z "${string//[0-9]/}" && echo "integer" || echo "no integer"... anche se questo fondamentalmente duplica la risposta di Dennis Williamson
tripleee

Grazie! L'unica risposta che funziona davvero qui intorno!
utente

Alternativa silenziosa:if [[ -n "$(printf "%s" "${2}" | sed s/[0-9]//g)" ]]; then
utente

0

In aggiunta alla risposta di Ignacio Vazquez-Abrams. Ciò consentirà al segno + di precedere il numero intero e consentirà qualsiasi numero di zeri come punti decimali. Ad esempio, questo consentirà a +45.00000000 di essere considerato un numero intero.
Tuttavia, $ 1 deve essere formattato per contenere un punto decimale. 45 non è considerato un numero intero qui, ma 45,0 lo è.

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

C'è una ragione per cui usi due diverse espressioni regolari per numeri positivi e negativi, invece di ^[-+]?[0-9]...?
tripleee

0

Per le risate ho elaborato più o meno rapidamente una serie di funzioni per farlo (is_string, is_int, is_float, è una stringa alfa o altro) ma ci sono modi più efficienti (meno codice) per farlo:

#!/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
}

Esegui alcuni test qui, ho definito che -44 è un int ma 44- non è ecc ..:

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

Produzione:

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: gli 0 iniziali potrebbero dedurre qualcos'altro quando si aggiungono numeri come ottale, quindi sarebbe meglio rimuoverli se intendi trattare '09' come un int (cosa che sto facendo) (ad esempio expr 09 + 0o strip con sed)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.