Tester si la chaîne est un entier valide

117

J'essaie de faire quelque chose d'assez commun: analyser l'entrée utilisateur dans un script shell. Si l'utilisateur a fourni un entier valide, le script fait une chose, et s'il n'est pas valide, il fait autre chose. Le problème, c'est que je n'ai pas trouvé de moyen facile (et raisonnablement élégant) de faire cela - je ne veux pas avoir à le séparer caractère par caractère.

Je sais que ça doit être facile mais je ne sais pas comment. Je pourrais le faire dans une douzaine de langues, mais pas BASH!

Dans mes recherches, j'ai trouvé ceci:

Expression régulière pour tester si une chaîne se compose d'un nombre réel valide en base 10

Et il y a une réponse là-dedans qui parle de regex, mais pour autant que je sache, c'est une fonction disponible en C (entre autres). Pourtant, il avait ce qui ressemblait à une bonne réponse, alors je l'ai essayé avec grep, mais grep ne savait pas quoi en faire. J'ai essayé -P qui sur ma boîte signifie le traiter comme une expression rationnelle PERL - nada. Dash E (-E) ne fonctionnait pas non plus. Et -F non plus.

Juste pour être clair, j'essaye quelque chose comme ça, à la recherche de n'importe quelle sortie - à partir de là, je vais pirater le script pour profiter de tout ce que je reçois. (IOW, je m'attendais à ce qu'une entrée non conforme ne renvoie rien pendant qu'une ligne valide se répète.)

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

Quelqu'un pourrait-il illustrer comment cela est le plus facile à faire?

Franchement, c'est une lacune de TEST, à mon avis. Il devrait avoir un drapeau comme celui-ci

if [ -I "string" ] ;
then
   echo "String is a valid integer."
else
   echo "String is not a valid integer."
fi
Richard T
la source
4
FYI: [est ancien compatible test; [[est la nouveauté de Bash, avec plus d'opérations et des règles de cotation différentes. Si vous avez déjà décidé de vous en tenir à Bash, allez-y [[(c'est vraiment beaucoup plus sympa); si vous avez besoin de portabilité vers d'autres coquilles, évitez [[complètement.
éphémère

Réponses:

183
[[ $var =~ ^-?[0-9]+$ ]]
  • Le ^indique le début du modèle d'entrée
  • Le -est un "-" littéral
  • Les ?moyens "0 ou 1 de la précédente ( -)"
  • Les +moyens "1 ou plusieurs des précédents ( [0-9])"
  • Le $indique la fin du modèle d'entrée

Ainsi, l'expression régulière correspond à une option -(pour le cas des nombres négatifs), suivie d'un ou plusieurs chiffres décimaux.

Références :

Ignacio Vazquez-Abrams
la source
3
Merci Ignacio, je vais l'essayer dans une seconde. Pourriez-vous l'expliquer pour que je puisse apprendre un peu? Je suppose qu'il se lit comme suit: "Au début de la chaîne (^), un signe moins (-) est facultatif (?), Suivi d'un nombre quelconque de caractères compris entre zéro et 9, inclusivement" ... et que pourrait alors le + ça veut dire? Merci.
Richard T
10
Les +moyens "1 ou plus des précédents", et le $indique la fin du modèle d'entrée. Ainsi, l'expression régulière correspond à un optionnel -suivi d'un ou plusieurs chiffres décimaux.
Ignacio Vazquez-Abrams
grogne re: le lien ABS
Charles Duffy
C'est une tangente, mais notez que lorsque vous spécifiez des plages de caractères, vous pouvez obtenir des résultats étranges; par exemple, [A-z]ne serait pas seulement vous donner A-Zet a-zmais aussi \ , [, ], ^, _et `.
Doktor J
De plus, sur la base du classement des caractères ( voir cette question / réponse associée ), quelque chose comme d[g-i]{2}pourrait finir non seulement par correspondre, digmais aussi dishdans le classement suggéré par cette réponse (où le shdigraphe est considéré comme un seul caractère, rassemblé après h).
Doktor J
61

Wow ... il y a tellement de bonnes solutions ici !! De toutes les solutions ci-dessus, je suis d'accord avec @nortally que l'utilisation d' -equn seul liner est la plus cool.

J'utilise GNU bash, version 4.1.5(Debian). J'ai également vérifié cela sur ksh (SunSO 5.10).

Voici ma version de vérifier si $1est un entier ou non:

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

Cette approche tient également compte des nombres négatifs, dont certaines des autres solutions auront un résultat négatif erroné, et elle autorisera un préfixe de "+" (par exemple +30) qui est évidemment un entier.

Résultats:

$ 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 solution fournie par Ignacio Vazquez-Abrams était également très soignée (si vous aimez les regex) après avoir été expliquée. Cependant, il ne gère pas les nombres positifs avec le +préfixe, mais il peut facilement être corrigé comme ci-dessous:

[[ $var =~ ^[-+]?[0-9]+$ ]]
Peter Ho
la source
Agréable! Assez similaire à cela , cependant.
devnull
Oui. C'est pareil. Cependant, je cherchais une solution d'une seule ligne pour la déclaration «si». J'ai pensé que je n'ai pas vraiment besoin d'appeler une fonction pour cela. Aussi, je peux voir que la redirection de stderr vers stdout dans la fonction. Quand j'ai essayé, le message stderr "expression entière attendue" s'affichait, ce qui n'était pas souhaitable pour moi.
Peter Ho
Je vous remercie! J'appellerais celui-ci facile et élégant.
Ezra Nugroho
2
Il y a une distinction notable entre votre solution et celle de l'expression régulière: la taille de l'entier est vérifiée par rapport aux limites de bash (sur mon ordinateur, elle est de 64 bits). Cette limite n'atteint pas la solution de l'expression rationnelle. Ainsi, votre solution échouera sur un nombre strictement supérieur à 9223372036854775807 sur les ordinateurs 64 bits.
vaab
2
Comme je l'ai découvert récemment, il y a quelques mises en garde .
Kyle Strand
28

Retard à la fête ici. Je suis extrêmement surpris qu'aucune des réponses ne mentionne la solution la plus simple, la plus rapide et la plus portable; la casedéclaration.

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

Le découpage de tout signe avant la comparaison ressemble à un petit hack, mais cela rend l'expression de l'instruction case tellement plus simple.

tripleee
la source
4
J'aurais aimé pouvoir voter pour cela une fois à chaque fois que je reviens sur cette question à cause des dupes. Cela me brise le fait qu'une solution simple mais conforme à POSIX soit enterrée dans le fond.
Adrian Frühwirth
3
Peut-être devriez-vous vous occuper des chaînes vides:''|*[!0-9]*)
Niklas Peter
2
BTW: Voici cette syntaxe documentée: tldp.org/LDP/abs/html/string-manipulation.html
Niklas Peter
Je ne tolère pas particulièrement l'ABS; ceci est évidemment également documenté dans le manuel de Bash. Quoi qu'il en soit, la section à laquelle vous avez lié ne décrit pas cette construction particulière, mais plutôt la réponse de @ Nortally.
tripleee
@tripleee Le document lié décrit la construction pour supprimer un préfixe de chaîne d'une variable utilisée dans la ligne de cas. Il est juste au bas de la page, mais il n'y a pas d'ancres, donc je ne pouvais pas créer un lien direct vers celui-ci, voir la section "Suppression de la sous-chaîne"
Niklas Peter
10

J'aime la solution utilisant le -eqtest, car il s'agit essentiellement d'un one-liner.

Ma propre solution était d'utiliser l'expansion des paramètres pour jeter tous les chiffres et voir s'il restait quelque chose. (J'utilise toujours la version 3.0, je ne l'ai jamais utilisé [[ou expravant, mais je suis heureux de les rencontrer.)

if [ "${INPUT_STRING//[0-9]}" = "" ]; then
  # yes, natural number
else
  # no, has non-numeral chars
fi
au nord
la source
4
Cela peut être encore amélioré en utilisant une [ -z "${INPUT_STRING//[0-9]}" ]solution vraiment sympa!
ShellFish
qu'en est-il des signes négatifs?
scottysseus
La -eqsolution a quelques problèmes; voir ici: stackoverflow.com/a/808740/1858225
Kyle Strand
Un INPUT_STRING vide est considéré comme un nombre, donc échoue pour mon cas
Manwe
9

Pour la portabilité vers pré-Bash 3.1 (lors de l' =~introduction du test), utilisez 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 : REGEXrecherche REGEX ancré au début de STRING, faisant écho au premier groupe (ou durée de la correspondance, si aucune) et renvoyant le succès / échec. C'est une vieille syntaxe regex, d'où l'excès \. -\?signifie "peut-être -", [0-9]\+signifie "un ou plusieurs chiffres" et $signifie "fin de chaîne".

Bash prend également en charge les globes étendus, bien que je ne me souvienne pas de quelle version.

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])) ]]

@(-|)signifie " -ou rien", [0-9]signifie "chiffre" et *([0-9])signifie "zéro ou plusieurs chiffres".

éphémère
la source
Merci éphémère, très obligé. Je n'avais jamais vu la syntaxe = ~ auparavant - et je n'ai toujours aucune idée de ce que cela signifie - à peu près égale?! ... Je ne l' ai jamais été heureux de programme BASH mais il est nécessaire quelques fois!
Richard T
Dans awk, ~était l'opérateur "regex match". En Perl (tel que copié à partir de C), ~était déjà utilisé pour le "complément de bits", donc ils l'ont utilisé =~. Cette dernière notation a été copiée dans plusieurs autres langues. (Perl 5.10 et Perl 6 comme ~~plus, mais cela n'a aucun impact ici.) Je suppose que vous pourriez le regarder comme une sorte d'égalité approximative ...
éphémère
Excellent post ET éditer! J'apprécie vraiment d'expliquer ce que cela signifie. J'aimerais pouvoir marquer les vôtres et ceux d'Ignacio comme LA bonne réponse. -frown- Vous êtes tous les deux super. Mais comme vous avez le double de sa réputation, je la donne à Ignacio - j'espère que vous comprenez! -smile-
Richard T
4

Voici encore une autre interprétation (en utilisant uniquement la commande intégrée test et son code de retour):

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
la source
1
Il n'est pas nécessaire d'utiliser $()avec if. Cela fonctionne: if is_int "$input". En outre, le $[]formulaire est obsolète. Utilisez $(())plutôt. À l'intérieur de l'un ou l'autre, le signe dollar peut être omis: les echo "Integer: $((input))"accolades ne sont nécessaires nulle part dans votre script.
Suspendu jusqu'à nouvel ordre.
Je me serais attendu à ce que cela gère également les nombres dans la notation de base de Bash comme des entiers valides (ce qui, bien sûr, par définition, ils le sont; mais cela pourrait ne pas être en accord avec la vôtre) mais testne semble pas le soutenir. [[fait, cependant. [[ 16#aa -eq 16#aa ]] && echo integerimprime "entier".
tripleee
Notez que [[renvoie des faux positifs pour cette méthode; par exemple [[ f -eq f ]]réussit. Il faut donc utiliser testou [.
spinup
3

Vous pouvez supprimer les non-chiffres et faire une comparaison. Voici un script de démonstration:

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

Voici à quoi ressemble la sortie du test:

44 44 Entier
-44 44 Entier
44-44 Non entier
4-4 44 Non entier
a4 4 Non entier
4a 4 Non entier
.4 4 Non entier
4.4 44 Non entier
-4,4 44 Non entier
09 9 Non entier
Suspendu jusqu'à nouvel ordre.
la source
Salut Dennis, Merci de m'avoir présenté la syntaxe à droite de match = ci-dessus. Je n'ai jamais remarqué cette syntaxe de type auparavant. Je reconnais une partie de la syntaxe de tr (un utilitaire que je ne maîtrise pas tout à fait, mais que je me fraye parfois un chemin); où puis-je lire une telle syntaxe? (c'est-à-dire comment s'appelle ce type de chose?) Merci.
Richard T
Vous pouvez consulter la page de manuel de Bash dans la section intitulée "Expansion des paramètres" pour des informations sur ${var//string}et ${var#string}et dans la section intitulée "Pattern Matching" pour [^ [: digit:]] `(qui est également traitée dans man 7 regex).
Suspendu jusqu'à nouvel ordre.
1
match=${match#0*}ne supprime pas les zéros non significatifs, il supprime au plus un zéro. En utilisant l'extension, cela ne peut être réalisé qu'en utilisant extglobvia match=${match##+(0)}.
Adrian Frühwirth
N'est-ce pas 9 ou 09 un entier?
Mike Q
@MikeQ: 09n'est pas un entier si vous considérez qu'un entier n'a pas de zéros non significatifs. Le test est de savoir si l'entrée ( 09) est égale à une version filtrée ( 9- un entier) et ce n'est pas le cas.
Suspendu jusqu'à nouvel ordre.
2

Pour moi, la solution la plus simple était d'utiliser la variable à l'intérieur d'une (())expression, comme ceci:

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

Bien entendu, cette solution n'est valable que si une valeur de zéro n'a pas de sens pour votre application. Cela s'est avéré être vrai dans mon cas, et c'est beaucoup plus simple que les autres solutions.

Comme indiqué dans les commentaires, cela peut vous exposer à une attaque d'exécution de code: L' (( ))opérateur évalue VAR, comme indiqué dans la Arithmetic Evaluationsection de la page de manuel bash (1) . Par conséquent, vous ne devez pas utiliser cette technique lorsque la source du contenu de VARest incertaine (et vous ne devez pas non plus utiliser TOUTE autre forme d'expansion variable, bien sûr).

Trebor Rude
la source
Vous pouvez même aller plus simple avecif (( var )); then echo "$var is an int."; fi
Aaron R.
2
Mais cela retournera également vrai pour les entiers négatifs, @aaronr, pas ce que l'OP recherchait.
Trebor Rude
2
Ceci est dangereux, voir: n = 1; var = "n"; if ((var)); then echo "$ var est un entier."; fi
jarno
2
C'est une très mauvaise idée et sous réserve de l' exécution de code arbitraire: essayer vous - même: VAR='a[$(ls)]'; if ((VAR > 0)); then echo "$VAR is a positive integer"; fi. À ce stade, vous êtes heureux de ne pas avoir saisi de commande diabolique à la place ls. Parce que OP mentionne l'entrée utilisateur , j'espère vraiment que vous ne l'utilisez pas avec l'entrée utilisateur dans le code de production!
gniourf_gniourf
Cela ne fonctionne pas si la chaîne contient des chiffres comme:agent007
brablc
1

ou avec 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
la source
Dans Bash et quelques autres shells "Bourne plus", vous pouvez éviter la substitution de commande et la commande externe avec test -z "${string//[0-9]/}" && echo "integer" || echo "no integer"... bien que cela reproduise essentiellement la réponse de Dennis Williamson
tripleee
Merci! La seule réponse qui fonctionne réellement ici!
utilisateur du
Alternative silencieuse:if [[ -n "$(printf "%s" "${2}" | sed s/[0-9]//g)" ]]; then
utilisateur du
0

Ajout à la réponse d'Ignacio Vazquez-Abrams. Cela permettra au signe + de précéder l'entier et autorisera n'importe quel nombre de zéros comme points décimaux. Par exemple, cela permettra à +45,00000000 d'être considéré comme un entier.
Cependant, $ 1 doit être formaté pour contenir un point décimal. 45 n'est pas considéré comme un entier ici, mais 45,0 l'est.

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
la source
Y a-t-il une raison pour laquelle vous utilisez deux expressions régulières différentes pour les nombres positifs et négatifs, au lieu de ^[-+]?[0-9]...?
tripleee
0

Pour rire, j'ai rapidement élaboré un ensemble de fonctions pour le faire (is_string, is_int, is_float, est une chaîne alpha, ou autre) mais il existe des moyens plus efficaces (moins de code) de le faire:

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

Faites quelques tests ici, j'ai défini que -44 est un int mais 44- n'est pas 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

Production:

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 = 

REMARQUE: les 0 en tête pourraient déduire autre chose lors de l'ajout de nombres tels que octal, il serait donc préférable de les supprimer si vous avez l'intention de traiter '09' comme un int (ce que je fais) (par exemple expr 09 + 0ou supprimer avec sed)

Mike Q
la source