Проверить, является ли строка допустимым целым числом

117

Я пытаюсь сделать что-то достаточно распространенное: анализировать вводимые пользователем данные в сценарии оболочки. Если пользователь предоставил действительное целое число, сценарий выполняет одно действие, а если он недопустим, - другое. Проблема в том, что я не нашел простого (и достаточно элегантного) способа сделать это - я не хочу, чтобы ему приходилось разбирать char на char.

Я знаю, что это должно быть легко, но не знаю как. Я мог бы сделать это на десятке языков, но не на БАШЕ!

В своем исследовании я обнаружил следующее:

Регулярное выражение для проверки того, состоит ли строка из действительного действительного числа в базе 10

И там есть ответ, в котором говорится о регулярном выражении, но, насколько я знаю, эта функция доступна в C (среди прочего). Тем не менее, это выглядело как отличный ответ, поэтому я попробовал его с помощью grep, но grep не знал, что с ним делать. Я попробовал -P, что на моем поле означает рассматривать его как регулярное выражение PERL - нада. Dash E (-E) тоже не работал. И -F тоже.

Чтобы быть ясным, я пробую что-то подобное, ищу какой-либо вывод - оттуда я взламываю сценарий, чтобы использовать все, что я получаю. (IOW, я ожидал, что несоответствующий ввод ничего не вернет, а действительная строка будет повторяться.)

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

Не могли бы вы проиллюстрировать, как это проще всего сделать?

Честно говоря, это, на мой взгляд, недостаток TEST. У него должен быть такой флаг

if [ -I "string" ] ;
then
   echo "String is a valid integer."
else
   echo "String is not a valid integer."
fi
Ричард Т
источник
4
FYI: [старая совместимая test; [[это новая вещь Bash, с большим количеством операций и другими правилами цитирования. Если вы уже решили придерживаться Bash, сделайте [[это (он намного лучше); если вам нужна переносимость на другие оболочки, [[полностью избегайте .
ephemient 05

Ответы:

183
[[ $var =~ ^-?[0-9]+$ ]]
  • Значок ^указывает на начало входного шаблона.
  • -Является буквальным «-»
  • В ?означает «0 или 1 из предшествующего ( -
  • Эти +средства «1 или более из предшествующих ( [0-9]
  • Значок $указывает на конец входного шаблона

Таким образом, регулярное выражение соответствует необязательному -(в случае отрицательных чисел), за которым следует одна или несколько десятичных цифр.

Ссылки :

Игнасио Васкес-Абрамс
источник
3
Спасибо, Игнасио, через секунду попробую. Не могли бы вы объяснить это, чтобы я немного научился? Я так понимаю, он гласит: «В начале строки (^) знак минус (-) является необязательным (?), За ним следует любое количество символов от нуля до 9 включительно» ... и что тогда может + $ имею в виду? Спасибо.
Ричард Т.
10
Эти +средства «1 или более из предшествующих», и $указывает конец входного образа. Таким образом, регулярное выражение соответствует необязательному, -за которым следует одна или несколько десятичных цифр.
Игнасио Васкес-Абрамс,
ворчит по поводу ссылки на АБС
Чарльз Даффи
Это касательный вопрос, но учтите, что при указании диапазонов символов вы можете получить странные результаты; например, [A-z]не только даст вам A-Zи , a-zно и \ , [, ], ^, _, и `.
Doktor J
Кроме того, на основе сопоставления символов ( см. Этот связанный вопрос / ответ ) что-то вроде d[g-i]{2}может закончиться не только сопоставлением, digно и dishсопоставлением, предложенным этим ответом (где shорграф считается одним символом, сопоставленным после h).
Doktor J
61

Ух ты ... тут столько хороших решений !! Из всех вышеперечисленных решений я согласен с @nortally, что использование -eqодного лайнера - самое крутое.

Я запускаю GNU bash, версия 4.1.5 (Debian). Я также проверял это на ksh (SunSO 5.10).

Вот моя версия проверки, $1является ли целое число или нет:

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

Этот подход также учитывает отрицательные числа, которые в некоторых других решениях будут иметь ошибочный отрицательный результат, и он позволит использовать префикс «+» (например, +30), который, очевидно, является целым числом.

Полученные результаты:

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

Решение, предоставленное Игнасио Васкес-Абрамсом, также было очень аккуратным (если вам нравится регулярное выражение) после того, как оно было объяснено. Однако он не обрабатывает положительные числа с +префиксом, но его легко исправить, как показано ниже:

[[ $var =~ ^[-+]?[0-9]+$ ]]
Питер Хо
источник
Ницца! Хотя довольно похоже на это .
devnull 01
Да. Это похоже. Однако я искал однострочное решение для оператора «if». Я подумал, что мне для этого не нужно вызывать функцию. Кроме того, я вижу, что перенаправление stderr на stdout в функции. Когда я попробовал, отобразилось сообщение stderr «Ожидается целочисленное выражение», что было для меня нежелательно.
Питер Хо
Спасибо! Я бы назвал это простым и элегантным.
Эзра Нугрохо
2
Существует заметное различие между вашим решением и регулярным выражением: размер целого числа проверяется на соответствие ограничениям bash (на моем компьютере это 64 бита). Этот предел не попадает в решение regexp. Таким образом, ваше решение не сработает на количестве, строго превышающем 9223372036854775807 на 64-битных компьютерах.
vaab 06
2
Как я недавно обнаружил, есть некоторые оговорки .
Kyle Strand
28

Опоздавший на вечеринку здесь. Я очень удивлен, что ни в одном из ответов не упоминается самое простое, быстрое и портативное решение; caseзаявление.

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

Обрезка любого знака перед сравнением кажется некоторой уловкой, но это значительно упрощает выражение для оператора case.

tripleee
источник
4
Хотел бы я проголосовать за это один раз каждый раз, когда возвращаюсь к этому вопросу из-за обмана. Меня беспокоит то, что простое, но совместимое с POSIX решение спрятано на дне.
Адриан Фрювирт
3
Может быть, тебе стоит позаботиться о пустых строках:''|*[!0-9]*)
Никлас Питер
2
BTW: Вот этот синтаксис задокументирован: tldp.org/LDP/abs/html/string-manipulation.html
Никлас Питер
Я не особо одобряю АБС; очевидно, это также задокументировано в руководстве по Bash. В любом случае, раздел, на который вы ссылаетесь, не описывает эту конкретную конструкцию, а скорее, например, ответ @Nortally.
tripleee
@tripleee Связанный документ описывает конструкцию для удаления префикса строки из переменной, используемой в строке case. Он находится внизу страницы, но там нет якорей, поэтому я не мог напрямую ссылаться на него, см. Раздел «Удаление подстроки»
Никлас Питер
10

Мне нравится решение, использующее -eqтест, потому что оно в основном однострочное.

Мое собственное решение заключалось в том, чтобы использовать расширение параметров, чтобы выбросить все цифры и посмотреть, осталось ли что-нибудь. (Я все еще использую 3.0, не использовал [[и exprраньше, но рад их встретить.)

if [ "${INPUT_STRING//[0-9]}" = "" ]; then
  # yes, natural number
else
  # no, has non-numeral chars
fi
nortally
источник
4
Это можно улучшить, используя [ -z "${INPUT_STRING//[0-9]}" ]действительно хорошее решение!
ShellFish
как насчет отрицательных знаков?
Скоттиссей
У -eqрешения есть некоторые проблемы; см. здесь: stackoverflow.com/a/808740/1858225
Кайл Стрэнд,
Пустой INPUT_STRING считается числом, поэтому не подходит для моего случая
Manwe
9

Для переносимости на pre-Bash 3.1 (когда =~был представлен тест) используйте 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 : REGEXищет REGEX, закрепленный в начале STRING, повторяя первую группу (или длину совпадения, если нет) и возвращая успех / неудачу. Это старый синтаксис регулярных выражений, отсюда и его избыток \. -\?означает «может быть -», [0-9]\+означает «одну или несколько цифр» и $означает «конец строки».

Bash также поддерживает расширенные глобусы, хотя я не помню, с какой версии и далее.

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

@(-|)означает « -или ничего», [0-9]означает «цифру» и *([0-9])означает «ноль или более цифр».

ephemient
источник
Спасибо, эфемерное, очень признательно. Я никогда раньше не видел синтаксиса = ~ - и до сих пор не понимаю, что он должен означать - примерно равно ?! ... Я никогда не возбуждались к программе в BASH , но это необходимо несколько раз!
Ричард Т.
В awk, ~был «регулярное выражение матч» оператора. В Perl (как скопировано из C) ~уже использовалось «битовое дополнение», поэтому они использовали =~. Позднее это обозначение было скопировано на несколько других языков. (Perl 5.10 и Perl 6 любят ~~больше, но здесь это не влияет.) Я полагаю, вы могли бы рассматривать это как своего рода примерное равенство ...
ephemient
Отличный пост И редактировать! Мне очень приятно объяснить, что это значит. Хотел бы я отметить ваши сообщения и сообщения Игнасио как правильный ответ. - нахмуриться - Вы оба молодцы. Но поскольку у вас вдвое большая репутация, я отдаю ее Игнасио - надеюсь, вы понимаете! -smile-
Ричард Т
4

Вот еще один вариант (только с использованием встроенной команды test и ее кода возврата):

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
Необязательно использовать $()с if. Это работает: if is_int "$input". Кроме того, эта $[]форма устарела. $(())Вместо этого используйте . Внутри любой знак доллара можно опустить: echo "Integer: $((input))"фигурные скобки нигде в вашем скрипте не нужны.
Приостановлено до дальнейшего уведомления.
Я ожидал, что это также будет обрабатывать числа в базовой нотации Bash как действительные целые числа (которые, конечно, по определению таковы; но это может не совпадать с вашим), но test, похоже, не поддерживает это. [[хотя делает. [[ 16#aa -eq 16#aa ]] && echo integerпечатает «целое число».
tripleee
Обратите внимание, что [[этот метод возвращает ложные срабатывания; например, [[ f -eq f ]]успешно. Поэтому он должен использовать testили [.
раскрутка
3

Вы можете удалить нецифровые цифры и провести сравнение. Вот демонстрационный сценарий:

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

Вот как выглядит тестовый результат:

44 44 Целое число
-44 44 Целое число
44-44 Не целое число
4-4 44 Не целое число
a4 4 Не целое число
4a 4 Не целое число
.4 4 Не целое
4.4 44 Не целое число
-4,4 44 Не целое
09 9 Не целое число
Приостановлено до дальнейшего уведомления.
источник
Привет, Деннис! Спасибо, что познакомили меня с синтаксисом справа от match = выше. Я никогда раньше не замечал такого синтаксиса типов. Я узнаю кое-что из синтаксиса tr (утилита, которую я еще не совсем освоил, но иногда ковыряюсь); где я могу прочитать о таком синтаксисе? (т.е. как называется этот тип вещей?) Спасибо.
Ричард Т.
Вы можете посмотреть на странице человека Bash в разделе «Параметры расширения» для получения информации о ${var//string}и ${var#string}и в разделе под названием «Pattern Matching» для [^ [: цифры:]] `(который также покрыт man 7 regex).
Приостановлено до дальнейшего уведомления.
1
match=${match#0*}вовсе не полосы ведущих нулей, то полосы не более одного нуля. Используя расширение, это может быть достигнуто только с extglobпомощью via match=${match##+(0)}.
Адриан Фрювирт
Разве 9 или 09 не целое число?
Mike Q
@MikeQ: 09не является целым числом, если вы считаете, что целое число не имеет ведущих нулей. Проверка заключается в том, соответствует ли input ( 09) очищенной версии ( 9- целому числу), а это не так.
Приостановлено до дальнейшего уведомления.
2

Для меня самым простым решением было использовать переменную внутри (())выражения, например:

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

Конечно, это решение действительно только в том случае, если нулевое значение не имеет смысла для вашего приложения. В моем случае это оказалось правдой, и это намного проще, чем другие решения.

Как указано в комментариях, это может привести к атаке выполнения кода: (( ))оператор VARвыполняет оценку , как указано в Arithmetic Evaluationразделе справочной страницы bash (1) . Следовательно, вы не должны использовать эту технику, когда источник содержимого VARнеизвестен (и, конечно, вы не должны использовать ЛЮБУЮ другую форму расширения переменных).

Trebor Rude
источник
Вы можете пойти даже прощеif (( var )); then echo "$var is an int."; fi
Аарон Р.
2
Но это также вернет истину для отрицательных целых чисел, @aaronr, а не то, что искал OP.
Требор Грубый
2
Это опасно, см .: n = 1; var = "n"; если ((var)); затем echo "$ var is an int."; Fi
Ярно
2
Это очень плохая идея , и при условии произвольного кода: попробуйте сами: VAR='a[$(ls)]'; if ((VAR > 0)); then echo "$VAR is a positive integer"; fi. На данный момент вы рады, что я не ввел какую-то злую команду вместо ls. Поскольку OP упоминает ввод пользователя , я действительно надеюсь, что вы не используете его с вводом пользователя в производственном коде!
gniourf_gniourf
Это не работает, если строка содержит несколько цифр, например:agent007
brablc
1

или с помощью 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
источник
В Bash и некоторых других оболочках "Bourne plus" вы можете избежать подстановки команд и внешней команды с помощью test -z "${string//[0-9]/}" && echo "integer" || echo "no integer"... хотя это в основном дублирует ответ Денниса Уильямсона
tripleee
Спасибо! Единственный ответ, который здесь действительно работает!
пользователь
Тихая альтернатива:if [[ -n "$(printf "%s" "${2}" | sed s/[0-9]//g)" ]]; then
пользователь
0

Дополнение к ответу Игнасио Васкес-Абрамса. Это позволит знаку + предшествовать целому числу и позволит использовать любое количество нулей в качестве десятичных знаков. Например, это позволит считать +45.00000000 целым числом.
Однако $ 1 должен быть отформатирован так, чтобы содержать десятичную точку. 45 здесь не считается целым числом, а 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
источник
Есть ли причина, по которой вы используете два разных регулярных выражения для положительных и отрицательных чисел вместо ^[-+]?[0-9]...?
Tripleee
0

Для смеха я примерно быстро разработал набор функций для этого (is_string, is_int, is_float, is alpha string или другие), но есть более эффективные (меньше кода) способы сделать это:

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

Пройдя здесь несколько тестов, я определил, что -44 - это int, а 44- - нет и т. Д .:

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

Вывод:

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 = 

ПРИМЕЧАНИЕ: ведущие 0 могут означать что-то еще при добавлении чисел, таких как восьмеричное, поэтому было бы лучше удалить их, если вы намерены рассматривать '09' как int (что я делаю) (например, expr 09 + 0или разделить с помощью sed)

Майк Кью
источник