Prueba si la cadena es un entero válido

117

Estoy tratando de hacer algo lo suficientemente común: analizar la entrada del usuario en un script de shell. Si el usuario proporcionó un número entero válido, el script hace una cosa, y si no es válido, hace otra. El problema es que no he encontrado una manera fácil (y razonablemente elegante) de hacer esto; no quiero tener que separarlo char por char.

Sé que esto debe ser fácil, pero no sé cómo. Podría hacerlo en una docena de idiomas, ¡pero no en BASH!

En mi investigación encontré esto:

Expresión regular para probar si una cadena consta de un número real válido en base 10

Y hay una respuesta allí que habla de expresiones regulares, pero que yo sepa, esa es una función disponible en C (entre otras). Aún así, tenía lo que parecía una gran respuesta, así que lo probé con grep, pero grep no sabía qué hacer con él. Probé -P que en mi caja significa tratarlo como una expresión regular PERL - nada. Dash E (-E) tampoco funcionó. Y tampoco -F.

Para que quede claro, estoy intentando algo como esto, buscando cualquier resultado; a partir de ahí, piratearé el script para aprovechar lo que obtenga. (IOW, esperaba que una entrada no conforme no devolviera nada mientras se repite una línea válida).

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

¿Alguien podría ilustrar cómo se hace esto más fácilmente?

Francamente, esto es un defecto de TEST, en mi opinión. Debería tener una bandera como esta

if [ -I "string" ] ;
then
   echo "String is a valid integer."
else
   echo "String is not a valid integer."
fi
Richard T
fuente
4
FYI: [es viejo compatible test; [[es lo nuevo de Bash, con más operaciones y diferentes reglas de cotización. Si ya ha decidido seguir con Bash, hágalo [[(es mucho mejor); si necesita portabilidad a otras conchas, evítelo por [[completo.
efímero

Respuestas:

183
[[ $var =~ ^-?[0-9]+$ ]]
  • El ^indica el principio del patrón de entrada
  • El -es un literal "-"
  • Los ?medios "0 o 1 del ( -) anterior "
  • Los +medios "1 o más de los ( [0-9]) anteriores "
  • El $indica el final del patrón de entrada

Entonces, la expresión regular coincide con un opcional -(para el caso de números negativos), seguido de uno o más dígitos decimales.

Referencias :

Ignacio Vázquez-Abrams
fuente
3
Gracias Ignacio, lo intentaré en un segundo. ¿Te importaría explicarlo para que pueda aprender un poco? Supongo que dice: "Al comienzo de la cadena (^), un signo menos (-) es opcional (?), Seguido de cualquier número de caracteres entre cero y 9, inclusive" ... y entonces, ¿qué podría ser el + $ significa? Gracias.
Richard T
10
El +significa "1 o más de los anteriores" y el $indica el final del patrón de entrada. Entonces, la expresión regular coincide con un opcional -seguido de uno o más dígitos decimales.
Ignacio Vazquez-Abrams
quejas re: el enlace ABS
Charles Duffy
Es una tangente, pero tenga en cuenta que al especificar rangos de caracteres puede obtener resultados extraños; por ejemplo, [A-z]no sólo le dará A-Zy a-zsino también \ , [, ], ^, _, y `.
Doktor J
Además, en función de la intercalación de caracteres ( consulte esta pregunta / respuesta relacionada ), algo como d[g-i]{2}podría terminar no solo coincidente, digsino también dishen la intercalación sugerida por esa respuesta (donde el shdígrafo se considera un solo carácter, intercalado después h).
Doktor J
61

Vaya ... ¡¡Aquí hay tantas buenas soluciones !! De todas las soluciones anteriores, estoy de acuerdo con @nortally en que usar el -eqdelineador es lo mejor.

Estoy ejecutando GNU bash, versión 4.1.5(Debian). También he comprobado esto en ksh (SunSO 5.10).

Aquí está mi versión de verificar si $1es un número entero o no:

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

Este enfoque también tiene en cuenta los números negativos, que algunas de las otras soluciones tendrán un resultado negativo defectuoso, y permitirá un prefijo de "+" (por ejemplo, +30) que obviamente es un número entero.

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.

La solución proporcionada por Ignacio Vazquez-Abrams también fue muy ordenada (si te gusta la expresión regular) después de que se explicó. Sin embargo, no maneja números positivos con el +prefijo, pero se puede arreglar fácilmente de la siguiente manera:

[[ $var =~ ^[-+]?[0-9]+$ ]]
Peter Ho
fuente
¡Agradable! Aunque bastante similar a esto .
devnull
Si. Es similar. Sin embargo, estaba buscando una solución de una sola línea para la declaración "si". Pensé que realmente no necesito llamar a una función para esto. Además, puedo ver que la redirección de stderr a stdout en la función. Cuando lo intenté, se mostró el mensaje stderr "expresión entera esperada" que no era deseable para mí.
Peter Ho
¡Gracias! Yo llamaría a este sencillo y elegante.
Ezra Nugroho
2
Hay una distinción notable entre su solución y la expresión regular: el tamaño del número entero se verifica hacia los límites de bash (en mi computadora es de 64 bits). Este límite no afecta a la solución de expresiones regulares. Por lo tanto, su solución fallará en un número estrictamente superior a 9223372036854775807 en computadoras de 64 bits.
vaab
2
Como descubrí recientemente, hay algunas advertencias .
Kyle Strand
28

Llegado tarde a la fiesta aquí. Estoy extremadamente sorprendido de que ninguna de las respuestas mencione la solución más simple, rápida y portátil; la casedeclaración.

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

El recorte de cualquier signo antes de la comparación parece un truco, pero eso hace que la expresión de la declaración de caso sea mucho más simple.

triples
fuente
4
Me gustaría poder votar esto una vez cada vez que vuelva a esta pregunta debido a los incautos. Me muere el hecho de que una solución simple pero compatible con POSIX esté enterrada en el fondo.
Adrian Frühwirth
3
Quizás deberías cuidar las cadenas vacías:''|*[!0-9]*)
Niklas Peter
2
Por cierto: Aquí está documentada esta sintaxis: tldp.org/LDP/abs/html/string-manipulation.html
Niklas Peter
No apruebo particularmente el ABS; obviamente esto también está documentado en el manual de Bash. De todos modos, la sección a la que se vinculó no describe esta construcción en particular, sino, por ejemplo, la respuesta de @ Nortally.
tripleee
@tripleee El documento vinculado describe la construcción para eliminar un prefijo de cadena de una variable utilizada en la línea de caso. Está en la parte inferior de la página, pero no hay anclajes, por lo que no pude vincularlo directamente, consulte la sección "Eliminación de subcadenas"
Niklas Peter
10

Me gusta la solución que usa la -eqprueba, porque es básicamente una sola línea.

Mi propia solución fue usar la expansión de parámetros para tirar todos los números y ver si quedaba algo. (Todavía estoy usando 3.0, no lo he usado [[o exprantes, pero me alegra conocerlos).

if [ "${INPUT_STRING//[0-9]}" = "" ]; then
  # yes, natural number
else
  # no, has non-numeral chars
fi
norte
fuente
4
¡Esto se puede mejorar aún más usando [ -z "${INPUT_STRING//[0-9]}" ]una solución realmente agradable!
ShellFish
¿qué pasa con los signos negativos?
scottysseus
La -eqsolución tiene algunos problemas; ver aquí: stackoverflow.com/a/808740/1858225
Kyle Strand
INPUT_STRING vacío se considera un número, por lo que falla en mi caso
Manwe
9

Para la portabilidad a pre-Bash 3.1 (cuando =~se introdujo la prueba), 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 : REGEXbusca REGEX anclado al comienzo de STRING, repitiendo el primer grupo (o la duración de la coincidencia, si no hay ninguno) y devolviendo éxito / fracaso. Esta es la antigua sintaxis de expresiones regulares, de ahí el exceso \. -\?significa "tal vez -", [0-9]\+significa "uno o más dígitos" y $significa "fin de cadena".

Bash también admite globs extendidos, aunque no recuerdo de qué versión en adelante.

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 nada", [0-9]significa "dígito" y *([0-9])significa "cero o más dígitos".

efímero
fuente
Gracias efímero, muy agradecido. Nunca había visto la sintaxis = ~ antes, y todavía no tengo idea de lo que se supone que significa, ¿aproximadamente igual? ... Nunca me ha emocionado programar en BASH, ¡pero algunas veces es necesario!
Richard T
En awk, ~era el operador "regex match". En Perl (como se copió de C), ~ya se usaba para "complemento de bits", por lo que usaron =~. Esta notación posterior se copió a varios otros idiomas. (Perl 5.10 y Perl 6 prefieren ~~más, pero eso no tiene ningún impacto aquí). Supongo que podrías verlo como una especie de igualdad aproximada ...
efímero
Excelente publicación y edición! Realmente aprecio explicar lo que significa. Ojalá pudiera marcar las publicaciones tuyas y de Ignacio como LA respuesta correcta. -frunció el ceño- Los dos son geniales. Pero como tú tienes el doble de reputación que él, se lo doy a Ignacio, ¡espero que lo entiendas! -sonrisa-
Richard T
4

Aquí hay otra versión (solo usando el comando integrado de prueba y su 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
fuente
1
No es necesario usar $()con if. Esto funciona: if is_int "$input". Además, el $[]formulario está obsoleto. Úselo en su $(())lugar. En el interior de ambos, el signo de dólar se puede omitir: las echo "Integer: $((input))"llaves no son necesarias en ninguna parte de su guión.
Pausado hasta nuevo aviso.
Hubiera esperado que esto también manejara los números en la notación base de Bash como enteros válidos (que por supuesto, por alguna definición, lo son; pero puede que no esté de acuerdo con el suyo) pero testno parece apoyar esto. [[aunque lo hace. [[ 16#aa -eq 16#aa ]] && echo integerimprime "entero".
tripleee
Tenga en cuenta que [[devuelve falsos positivos para este método; por ejemplo, [[ f -eq f ]]tiene éxito. Entonces debe usar testo [.
spinup
3

Puede quitar los que no son dígitos y hacer una comparación. Aquí hay un script de demostración:

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

Así es como se ve la salida de prueba:

44 44 Entero
-44 44 Entero
44-44 No entero
4-4 44 No entero
a4 4 No entero
4a 4 No entero
.4 4 No entero
4.4 44 No entero
-4,4 44 No entero
09 9 No entero
Pausado hasta nuevo aviso.
fuente
Hola Dennis, Gracias por presentarme la sintaxis a la derecha de match = arriba. Nunca antes había notado esa sintaxis de tipo. Reconozco parte de la sintaxis de tr (una utilidad que no he dominado del todo, pero a veces busco a tientas); ¿Dónde puedo leer sobre esa sintaxis? (es decir, ¿cómo se llama este tipo de cosas?) Gracias.
Richard T
Puede buscar en la página de manual de Bash en la sección llamada "Expansión de parámetros" para obtener información sobre ${var//string}y ${var#string}y en la sección llamada "Coincidencia de patrones" para [^ [: dígito:]] `(que también se trata en man 7 regex).
Pausado hasta nuevo aviso.
1
match=${match#0*}no no despojar ceros a la izquierda, se despoja a lo más un cero. Usando la expansión, esto solo se puede lograr usando a extglobtravés dematch=${match##+(0)} .
Adrian Frühwirth
¿No es 9 o 09 un número entero?
Mike Q
@MikeQ: 09 no es un número entero si considera que un número entero no tiene ceros a la izquierda. La prueba es si input ( 09) es igual a una versión desinfectada ( 9- un número entero) y no es así.
Pausado hasta nuevo aviso.
2

Para mí, la solución más simple fue usar la variable dentro de una (())expresión, así:

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

Por supuesto, esta solución solo es válida si un valor de cero no tiene sentido para su aplicación. Eso pasó a ser cierto en mi caso, y esto es mucho más simple que las otras soluciones.

Como se señaló en los comentarios, esto puede hacer que esté sujeto a un ataque de ejecución de código: el (( ))operador evalúa VAR, como se indica en la Arithmetic Evaluationsección de la página de manual de bash (1) . Por lo tanto, no debe usar esta técnica cuando la fuente del contenido de VARes incierta (ni debe usar CUALQUIER otra forma de expansión variable, por supuesto).

Trebor Rude
fuente
Incluso puede ir más simple conif (( var )); then echo "$var is an int."; fi
Aaron R.
2
Pero eso también será cierto para los enteros negativos, @aaronr, no lo que estaba buscando el OP.
Trebor Rude
2
Esto es peligroso, ver: n = 1; var = "n"; si ((var)); luego echo "$ var es un int."; fi
jarno
2
Esta es una muy mala idea y está sujeto a la ejecución de código arbitrario: hacerlo por uno mismo: VAR='a[$(ls)]'; if ((VAR > 0)); then echo "$VAR is a positive integer"; fi. En este punto que eres me alegro no introdujo alguna orden mal en vez de ls. Debido a que OP menciona la entrada del usuario , ¡realmente espero que no esté usando esto con la entrada del usuario en el código de producción!
gniourf_gniourf
Esto no funciona si la cadena contiene algunos dígitos como: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
knipwim
fuente
En Bash y algunos otros shells "Bourne plus" puede evitar la sustitución de comandos y el comando externo con test -z "${string//[0-9]/}" && echo "integer" || echo "no integer"... aunque eso básicamente duplica la respuesta de Dennis Williamson
tripleee
¡Gracias! ¡La única respuesta que realmente funciona aquí!
usuario
Alternativa silenciosa:if [[ -n "$(printf "%s" "${2}" | sed s/[0-9]//g)" ]]; then
usuario
0

Agregando a la respuesta de Ignacio Vazquez-Abrams. Esto permitirá que el signo + preceda al número entero, y permitirá cualquier número de ceros como puntos decimales. Por ejemplo, esto permitirá que +45.00000000 se considere un número entero.
Sin embargo, $ 1 debe formatearse para contener un punto decimal. 45 no se considera un número entero aquí, pero 45.0 sí lo es.

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
fuente
¿Hay alguna razón por la que usa dos expresiones regulares diferentes para números positivos y negativos, en lugar de ^[-+]?[0-9]...?
tripleee
0

Para las risas, desarrollé rápidamente un conjunto de funciones para hacer esto (is_string, is_int, is_float, es una cadena alfa u otra) pero hay formas más eficientes (menos código) de hacer esto:

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

Ejecute algunas pruebas aquí, definí que -44 es un int pero 44- no lo es, 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

Salida:

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: Los ceros iniciales podrían inferir algo más al agregar números como octal, por lo que sería mejor eliminarlos si tiene la intención de tratar '09' como un int (lo que estoy haciendo) (por ejemplo, expr 09 + 0o eliminar con sed)

Mike Q
fuente