Sprawdź, czy ciąg jest prawidłową liczbą całkowitą

117

Próbuję zrobić coś dość powszechnego: przeanalizować dane wejściowe użytkownika w skrypcie powłoki. Jeśli użytkownik podał prawidłową liczbę całkowitą, skrypt robi jedną rzecz, a jeśli nie jest poprawny, robi coś innego. Kłopot w tym, że nie znalazłem łatwego (i dość eleganckiego) sposobu na zrobienie tego - nie chcę, aby trzeba było go rozdzielać po znaku.

Wiem, że to musi być łatwe, ale nie wiem jak. Mógłbym to zrobić w kilkunastu językach, ale nie w BASH!

W moich badaniach znalazłem to:

Wyrażenie regularne do sprawdzenia, czy ciąg składa się z prawidłowej liczby rzeczywistej o podstawie 10

I jest w tym odpowiedź, która mówi o wyrażeniu regularnym, ale o ile wiem, jest to funkcja dostępna w C (między innymi). Mimo to miał coś, co wyglądało na świetną odpowiedź, więc spróbowałem go z grep, ale grep nie wiedział, co z tym zrobić. Spróbowałem -P, co na moim pudełku oznacza traktowanie go jako wyrażenia regularnego PERL - nada. Dash E (-E) też nie działał. Ani też -F.

Żeby było jasne, próbuję czegoś takiego, szukając jakichkolwiek wyników - stamtąd zhakuję skrypt, aby wykorzystać wszystko, co otrzymam. (IOW, spodziewałem się, że niezgodne dane wejściowe nic nie zwracają, podczas gdy poprawna linia jest powtarzana.)

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

Czy ktoś mógłby zilustrować, jak najłatwiej to zrobić?

Szczerze mówiąc, jest to moim zdaniem niedostatek TESTU. Powinien mieć taką flagę

if [ -I "string" ] ;
then
   echo "String is a valid integer."
else
   echo "String is not a valid integer."
fi
Richard T.
źródło
4
FYI: [jest stary kompatybilny test; [[to nowa rzecz Bash, z większą liczbą operacji i innymi zasadami cytowania. Jeśli już zdecydowałeś się pozostać przy Bashu, idź [[(jest o wiele ładniejszy); jeśli potrzebujesz możliwości przenoszenia na inne muszle, [[całkowicie unikaj .
ephemient

Odpowiedzi:

183
[[ $var =~ ^-?[0-9]+$ ]]
  • Symbol ^wskazuje początek wzorca wejściowego
  • To -jest dosłowne „-”
  • Te ?środki „0 lub 1 z poprzedzającego ( -)”
  • Te +środki „1 lub więcej z powyższych ( [0-9])”
  • Symbol $wskazuje koniec wzorca wejściowego

Zatem wyrażenie regularne dopasowuje opcjonalne -(w przypadku liczb ujemnych), po którym następuje jedna lub więcej cyfr dziesiętnych.

Referencje :

Ignacio Vazquez-Abrams
źródło
3
Dzięki Ignacio, spróbuję za sekundę. Czy mógłbyś to wyjaśnić, abym mógł się trochę nauczyć? Rozumiem, że brzmi: „Na początku ciągu (^) znak minus (-) jest opcjonalny (?), Po którym następuje dowolna liczba znaków od zera do 9 włącznie”… i co w takim razie może spowodować + znaczy? Dzięki.
Richard T.
10
W +oznacza „jeden lub więcej z poprzednich” i $wskazuje na koniec wzorca sygnału. Zatem wyrażenie regularne dopasowuje opcjonalne, -po którym następuje jedna lub więcej cyfr dziesiętnych.
Ignacio Vazquez-Abrams,
narzeka na: link ABS
Charles Duffy
Jest to styczna, ale pamiętaj, że określając zakresy znaków, możesz uzyskać dziwne wyniki; na przykład, [A-z]nie tylko daje A-Z, a a-zjednak również \ , [, ], ^, _, i `.
Doktor J,
Dodatkowo, w oparciu o porównywanie znaków ( zobacz to powiązane pytanie / odpowiedź ), coś takiego d[g-i]{2}może zakończyć się nie tylko dopasowaniem, digale także porównaniem dishsugerowanym przez tę odpowiedź (gdzie shdwuznak jest traktowany jako pojedynczy znak, zestawiony po h).
Doktor J,
61

Wow ... jest tu tak wiele dobrych rozwiązań !! Ze wszystkich powyższych rozwiązań zgadzam się z @nortally, że używanie -eqone linera jest najfajniejsze.

Używam GNU bash, wersja 4.1.5(Debian). Sprawdziłem to również na ksh (SunSO 5.10).

Oto moja wersja sprawdzania, czy $1jest liczbą całkowitą, czy nie:

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

Podejście to uwzględnia również liczby ujemne, które w przypadku niektórych innych rozwiązań będą miały błędny wynik ujemny i pozwoli na przedrostek „+” (np. +30), który oczywiście jest liczbą całkowitą.

Wyniki:

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

Rozwiązanie dostarczone przez Ignacio Vazquez-Abrams było również bardzo zgrabne (jeśli lubisz regex) po tym, jak zostało wyjaśnione. Jednak nie obsługuje liczb dodatnich z +prefiksem, ale można go łatwo naprawić, jak poniżej:

[[ $var =~ ^[-+]?[0-9]+$ ]]
Peter Ho
źródło
Miły! Jednak bardzo podobny do tego .
devnull
Tak. To jest podobne. Jednak szukałem rozwiązania z jedną linijką dla stwierdzenia „jeśli”. Pomyślałem, że tak naprawdę nie muszę w tym celu wywoływać funkcji. Widzę również, że przekierowanie stderr na stdout w funkcji. Kiedy próbowałem, wyświetlił się komunikat stderr „oczekiwano wyrażenia liczby całkowitej”, co nie było dla mnie pożądane.
Peter Ho
Dziękuję Ci! Nazwałbym to łatwym i eleganckim.
Ezra Nugroho
2
Istnieje wyraźna różnica między rozwiązaniem twojego rozwiązania a wyrażeniem regularnym: rozmiar liczby całkowitej jest sprawdzany pod kątem limitów basha (na moim komputerze jest to 64 bity). Ten limit nie dotyczy rozwiązania regexp. Więc rozwiązanie nie powiedzie się przy liczbie ściśle większej niż 9223372036854775807 na komputerach 64-bitowych.
vaab
2
Jak niedawno odkryłem, są pewne zastrzeżenia .
Kyle Strand
28

Spóźniony na przyjęcie tutaj. Jestem niezwykle zaskoczony, że żadna z odpowiedzi nie wspomina o najprostszym, najszybszym i najbardziej przenośnym rozwiązaniu; casestwierdzenie.

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

Przycinanie dowolnego znaku przed porównaniem wydaje się trochę hackem, ale to sprawia, że ​​wyrażenie w instrukcji case jest o wiele prostsze.

tripleee
źródło
4
Chciałbym móc głosować za tym raz za każdym razem, gdy wracam do tego pytania z powodu oszustw. Miażdży moje koła zębate, że proste, ale zgodne z POSIX rozwiązanie jest zakopane w dnie.
Adrian Frühwirth
3
Może powinieneś zająć się pustymi strunami:''|*[!0-9]*)
Niklas Peter
2
BTW: Oto udokumentowana składnia: tldp.org/LDP/abs/html/string-manipulation.html
Niklas Peter,
Nie akceptuję szczególnie ABS; jest to oczywiście udokumentowane w podręczniku Bash. W każdym razie sekcja, do której utworzyłeś łącze, nie opisuje tej konkretnej konstrukcji, ale raczej np. Odpowiedź @ Nortally.
tripleee
@tripleee Połączony dokument opisuje konstrukcję służącą do usuwania przedrostka ciągu ze zmiennej używanej w wierszu przypadku. Znajduje się tylko na dole strony, ale nie ma żadnych kotwic, więc nie mogłem bezpośrednio do niego linku, zobacz sekcję „Usuwanie podciągów”
Niklas Peter
10

Podoba mi się rozwiązanie wykorzystujące -eqtest, ponieważ jest to w zasadzie jednolinijkowy.

Moje własne rozwiązanie polegało na zastosowaniu rozszerzenia parametrów, aby wyrzucić wszystkie cyfry i sprawdzić, czy coś zostało. (Nadal używam 3.0, nie używałem [[ani exprwcześniej, ale cieszę się, że je spotykam.)

if [ "${INPUT_STRING//[0-9]}" = "" ]; then
  # yes, natural number
else
  # no, has non-numeral chars
fi
północnej
źródło
4
Można to jeszcze ulepszyć za pomocą, [ -z "${INPUT_STRING//[0-9]}" ]ale naprawdę fajnego rozwiązania!
ShellFish
a co z negatywnymi znakami?
scottysseus,
-eqRozwiązanie ma pewne problemy; patrz tutaj: stackoverflow.com/a/808740/1858225
Kyle Strand,
Puste INPUT_STRING jest traktowane jako liczba, więc w moim przypadku nie powiedzie się
Manwe,
9

Aby zapewnić przenośność do wersji pre-Bash 3.1 (gdy =~test został wprowadzony), użyj 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 : REGEXwyszukuje REGEX zakotwiczony na początku ŁAŃCUCHA, powtarzając pierwszą grupę (lub długość dopasowania, jeśli nie ma) i zwracając sukces / niepowodzenie. To jest stara składnia wyrażeń regularnych, stąd nadmiar \. -\?oznacza „być może -”, [0-9]\+oznacza „jedną lub więcej cyfr” i $oznacza „koniec łańcucha”.

Bash obsługuje również rozszerzone globi, chociaż nie pamiętam, od której wersji.

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

@(-|)oznacza „ -albo nic”, [0-9]oznacza „cyfrę” i *([0-9])oznacza „zero lub więcej cyfr”.

ephemient
źródło
Dziękuję Ephemient, bardzo zobowiązany. Nigdy wcześniej nie widziałem składni = ~ - i nadal nie mam pojęcia, co to ma znaczyć - w przybliżeniu równa ?! ... Nigdy nie byłem podekscytowany programowaniem w BASH, ale czasami jest to konieczne!
Richard T.
W awk, ~był operator „regex mecz”. W Perlu (skopiowanym z C) ~był już używany do „bitowego uzupełnienia”, więc używali =~. Ten późniejszy zapis został skopiowany do kilku innych języków. (Perl 5.10 i Perl 6 lubią ~~więcej, ale to nie ma tu żadnego wpływu.) Przypuszczam, że można to potraktować jako pewnego rodzaju przybliżoną równość ...
ephemient
Doskonały post i edycja! Naprawdę doceniam wyjaśnienie, co to znaczy. Chciałbym móc oznaczyć posty zarówno twoje, jak i Ignacio jako poprawną odpowiedź. -frown- Oboje jesteście wspaniali. Ale ponieważ masz podwójną reputację, którą on ma, daję ją Ignacio - mam nadzieję, że rozumiesz! -smile-
Richard T
4

Oto jeszcze jedno podejście (tylko przy użyciu wbudowanego polecenia test i jego kodu powrotu):

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
źródło
1
Nie jest konieczne używanie $()z if. Działa to: if is_int "$input". Ponadto, $[]forma jest przestarzała. Użyj $(())zamiast tego. Wewnątrz również znak dolara można pominąć: echo "Integer: $((input))"nawiasy klamrowe nie są potrzebne nigdzie w skrypcie.
Wstrzymano do odwołania.
Spodziewałbym się, że będzie to również obsługiwać liczby w notacji bazowej Basha jako prawidłowe liczby całkowite (które oczywiście z pewnej definicji są; ale może nie zgadzać się z twoją), ale testwydaje się, że to nie obsługuje. [[jednak robi. [[ 16#aa -eq 16#aa ]] && echo integerwypisuje "integer".
tripleee
Zauważ, że [[zwraca fałszywe alarmy dla tej metody; np [[ f -eq f ]]. sukces. Musi więc użyć testlub [.
spinup
3

Możesz usunąć niecyfry i zrobić porównanie. Oto skrypt demonstracyjny:

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

Oto jak wygląda wynik testu:

44 44 Integer
-44 44 Integer
44-44 Nie jest liczbą całkowitą
4-4 44 Nie jest liczbą całkowitą
a4 4 Nie jest liczbą całkowitą
4a 4 Nie jest liczbą całkowitą
.4 4 Nie jest liczbą całkowitą
4,4 44 Nie jest liczbą całkowitą
-4,4 44 Nie jest liczbą całkowitą
09 9 Nie jest liczbą całkowitą
Wstrzymano do odwołania.
źródło
Cześć Dennis, Dziękuję za wprowadzenie mnie do składni po prawej stronie match = powyżej. Nigdy wcześniej nie zauważyłem składni tego typu. Rozpoznaję część składni z tr (narzędzie, którego nie do końca opanowałem, ale czasami grzebię w sobie); gdzie mogę poczytać o takiej składni? (tj. jak to się nazywa?) Dzięki.
Richard T.
Możesz zajrzeć na stronę podręcznika Bash w sekcji „Rozwijanie parametrów”, aby uzyskać informacje na temat ${var//string}oraz ${var#string}w sekcji „Dopasowywanie wzorców” dla [^ [: digit:]] `(co jest również omówione w man 7 regex).
Wstrzymano do odwołania.
1
match=${match#0*}sposób nie taśmy zer, to taśmy co najwyżej jedno zero. Korzystając z rozszerzenia, można to osiągnąć tylko za extglobpomocą match=${match##+(0)}.
Adrian Frühwirth
Czy 9 lub 09 nie jest liczbą całkowitą?
Mike Q
@MikeQ: 09nie jest liczbą całkowitą, jeśli uważasz, że liczba całkowita nie ma zer wiodących. Test sprawdza, czy input ( 09) jest równy wersji oczyszczonej ( 9- liczba całkowita), a tak nie jest.
Wstrzymano do odwołania.
2

Dla mnie najprostszym rozwiązaniem było użycie zmiennej wewnątrz (())wyrażenia, tak jak:

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

Oczywiście to rozwiązanie jest poprawne tylko wtedy, gdy wartość zero nie ma sensu dla twojej aplikacji. Tak się stało w moim przypadku i jest to znacznie prostsze niż inne rozwiązania.

Jak wskazano w komentarzach, może to narazić Cię na atak polegający na wykonaniu kodu: (( ))Operator ocenia VAR, jak stwierdzono w Arithmetic Evaluationsekcji strony podręcznika bash (1) . Dlatego nie powinieneś używać tej techniki, gdy źródło treści VARjest niepewne (ani oczywiście nie powinieneś używać ŻADNEJ innej formy rozwijania zmiennych).

Trebor Rude
źródło
Możesz nawet pójść prościej zif (( var )); then echo "$var is an int."; fi
Aaron R.
2
Ale to również zwróci prawdę dla ujemnych liczb całkowitych, @aaronr, a nie tego, czego szukał OP.
Trebor Rude
2
Jest to niebezpieczne, zobacz: n = 1; var = "n"; if ((var)); następnie echo "$ zmienna jest int."; fi
jarno
2
Jest to bardzo zły pomysł i podlega wykonanie dowolnego kodu: try to sam: VAR='a[$(ls)]'; if ((VAR > 0)); then echo "$VAR is a positive integer"; fi. W tym momencie cieszysz się, że zamiast tego nie wydałem złego polecenia ls. Ponieważ OP wspomina o danych wejściowych użytkownika , mam nadzieję, że nie używasz tego z danymi wejściowymi użytkownika w kodzie produkcyjnym!
gniourf_gniourf
To nie działa, jeśli ciąg zawiera kilka cyfr, takich jak:agent007
brablc
1

lub z 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
źródło
W Bash i niektórych innych powłokach „Bourne plus” można uniknąć zastępowania poleceń i poleceń zewnętrznych za pomocą test -z "${string//[0-9]/}" && echo "integer" || echo "no integer" … chociaż to w zasadzie powiela odpowiedź Dennisa Williamsona
tripleee
Dzięki! Jedyna odpowiedź, która faktycznie działa tutaj!
użytkownik
Cicha alternatywa:if [[ -n "$(printf "%s" "${2}" | sed s/[0-9]//g)" ]]; then
użytkownik
0

Dodając do odpowiedzi Ignacio Vazquez-Abrams. Umożliwi to znak + poprzedzający liczbę całkowitą i dowolną liczbę zer jako miejsc dziesiętnych. Na przykład pozwoli to na uznanie +45,00000000 za liczbę całkowitą.
Jednak $ 1 musi być sformatowany tak, aby zawierał kropkę dziesiętną. 45 nie jest tutaj uważane za liczbę całkowitą, ale 45.0 jest.

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
źródło
Czy jest jakiś powód, dla którego używasz dwóch różnych wyrażeń regularnych dla liczb dodatnich i ujemnych zamiast ^[-+]?[0-9]...?
tripleee
0

Dla śmiechu z grubsza po prostu szybko opracowałem zestaw funkcji do tego (is_string, is_int, is_float, is alpha string lub other), ale są bardziej wydajne (mniej kodu) sposoby na zrobienie tego:

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

Przeprowadziłem tutaj kilka testów, zdefiniowałem, że -44 to int, ale 44- nie jest itd.:

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

Wynik:

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 = 

UWAGA: Wiodące zera mogą wywnioskować coś innego podczas dodawania liczb, takich jak ósemkowe, więc lepiej byłoby je usunąć, jeśli zamierzasz traktować '09' jako int (co robię) (np. expr 09 + 0Lub usuń sed)

Mike Q
źródło