851

I just can't figure out how do I make sure an argument passed to my script is a number or not.

All I want to do is something like this:

test *isnumber* $1 && VAR=$1 || echo "need a number"

Any help?

codeforester
  • 39,467
  • 16
  • 112
  • 140
Flávio Amieiro
  • 41,644
  • 8
  • 32
  • 24
  • 21
    As an aside -- the `test && echo "foo" && exit 0 || echo "bar" && exit 1` approach you're using may have some unintended side effects -- if the echo fails (perhaps output is to a closed FD), the `exit 0` will be skipped, and the code will then try to `echo "bar"`. If it fails at that too, the `&&` condition will fail, and it won't even execute `exit 1`! Using actual `if` statements rather than `&&`/`||` is less prone to unexpected side effects. – Charles Duffy Aug 24 '11 at 14:12
  • @CharlesDuffy That's the kind of really clever thinking that most people only get to when they have to track down hairy bugs...! I didn't ever think echo could return failure. – Camilo Martin Jun 25 '14 at 17:37
  • 12
    Bit late to the party, but I know about the dangers that Charles wrote about, as I had to go through them quite some time ago too. So here's a 100% fool-proof (and well-readable) line for you: `[[ $1 =~ "^[0-9]+$" ]] && { echo "number"; exit 0; } || { echo "not a number"; exit 1; }` The curly brackets indicate that things should NOT be executed in a subshell (which would definitely be that way with `()` parentheses used instead). Caveat: __Never miss the final semicolon__. Otherwise you might cause `bash` to print out the ugliest (and most pointless) error messages... – syntaxerror Jun 09 '15 at 17:34
  • 8
    It doesn't work in Ubuntu, unless you don't remove the quotes. So it should just be `[[ 12345 =~ ^[0-9]+$ ]] && echo OKKK || echo NOOO` – Treviño Sep 10 '15 at 10:56
  • ...I removed the (faulty) answer from the question -- part of why answers should be separate is so they can be commented on, voted on, corrected, etc. individually. – Charles Duffy Apr 27 '16 at 17:06
  • 7
    You'll need to be **more specific** about what you mean by ***"number"***. An integer? A fixed-point number? Scientific ("e") notation? Is there a required range (e.g. a 64-bit unsigned value), or do you allow any number that can be written? – Toby Speight Nov 15 '16 at 11:45
  • It's unbelievable that bash doesn't provide some reliable, built in way to validate whether or not a value is numeric. The number, variety, and variation in quality of the answers here indicate that this is a serious problem. – Michael Burr Jul 29 '20 at 22:25
  • 2
    Bash does provide a reliable means of determining if a number is an INTEGER. { VAR="asdfas" ; (( VAR )) ; echo $?; } The equation will correctly fail if the answer is '0' because '0' is not an integer. I had the very same problem just a few minutes ago and found this thread with a quick serarch. I hope this helps others. Other people were close though. – That one guy from the movie Jun 26 '21 at 21:08
  • @That one guy from the movie Your code gives ugly messages on stderr. For instance `((5a))` gives `bash: ((: 5a: value too great for base (error token is "5a")` – db-inf Nov 18 '21 at 07:11
  • @db-inf Easily fixed by redirecting stderr to null: `{ VAR="5e" ; (( VAR )) 2>/dev/null; echo $?; }` – pmarreck Jan 04 '23 at 16:48

40 Answers40

1110

One approach is to use a regular expression, like so:

re='^[0-9]+$'
if ! [[ $yournumber =~ $re ]] ; then
   echo "error: Not a number" >&2; exit 1
fi

If the value is not necessarily an integer, consider amending the regex appropriately; for instance:

^[0-9]+([.][0-9]+)?$

...or, to handle numbers with a sign:

^[+-]?[0-9]+([.][0-9]+)?$
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • 9
    +1 for this approach, but take care with decimals, doing this test with, by example, "1.0" or "1,0" prints "error: Not a number". – sourcerebels Apr 30 '09 at 14:30
  • 1
    @lhunath - true 'nuff. I tend to use it in more complex error handlers (ie. much more than just one "echo" following), but the habit leaked out here. – Charles Duffy May 02 '09 at 14:29
  • 1
    `^-*[0-9]+([.][0-9]+)?$` to also test for negative numbers – Ben Jun 23 '11 at 20:03
  • 5
    @Ben do you really want to handle more than one minus sign? I'd make it `^-?` rather than `^-*` unless you're actually doing the work to handle multiple inversions correctly. – Charles Duffy Jun 26 '11 at 22:57
  • 4
    @SandraSchlichting Makes all future output go to stderr. Not really a point to it here, where there's only one echo, but it's a habit I tend to get into for cases where error messages span multiple lines. – Charles Duffy Oct 17 '12 at 14:28
  • 2
    @Charles Duffy One could also consider the case of floats in the form .1234 without leading zero with `^[0-9]*([.][0-9]+)?$` This would require an additional check for a non-empty string tough.. – Legionair May 20 '13 at 20:21
  • With the full check then being `if ! [[ -n "$yournumber" && "$yournumber" =~ ^-?[0-9]*([.][0-9]+)?$ ]]; then echo NAN; fi` – Legionair May 20 '13 at 20:32
  • 55
    I'm not sure why the regular expression has to be saved in a variable, but if it's for the sake of compatibility I don't think it's necessary. You could just apply the expression directly: `[[ $yournumber =~ ^[0-9]+$ ]]`. – konsolebox Aug 31 '13 at 21:48
  • 8
    @konsolebox yes, compatibility. Backslash handling in literal regular expressions on the right-hand side of `=~` changed between 3.1 and 3.2, whereas backslash handling in assignments is constant in all relevant releases of bash. Thus, following the practice of always assigning regular expressions to variables before matching against them using `=~` avoids surprises. I do it here to teach good habits, even though this particular regex has no backslash escapes. – Charles Duffy Sep 01 '13 at 00:54
  • @CharlesDuffy And that's actually the reason why I'd prefer extended globbing over it which has been around since 2.05b or maybe earlier. `=~` started at 3.0 and has had varying implementations since then. What's actually most difficult in it is that you would still need to quote characters in values of variables you intended to provide as literal (e.g. `${VAR//./\\.}`) to prevent them from being parsed as regex characters whereas with `==` you could just place them in `""` e.g. `+(x)"$V"`. And the only reason why I could use it over ext. patterns is when I could make use of `BASH_REMATCH`. – konsolebox Sep 01 '13 at 07:42
  • 1
    @konsolebox With respect to the "most difficult" part, there are other ways to handle that problem. `[[ $s =~ $re_start"$literal"$re_end ]]`, for instance, treats the expanded contents of `"$literal"` as, well, literal, no escaping needed. (I also find `[.]` considerably more readable and otherwise easier to handle than `\.`, but that's admittedly bikeshedding) – Charles Duffy Sep 01 '13 at 14:25
  • @konsolebox ...by the way, I'm not arguing that there's only one right answer here. I've upvoted jilles' answer as well (though it needs the acknowledged bit of extension to be complete). – Charles Duffy Sep 01 '13 at 14:31
  • @CharlesDuffy `[[ $s =~ $re_start"$literal"$re_end ]]` would work just like the way of `==` starting 3.2, but not on 3.0 or 3.1. `[[ $s =~ (x) ]]` without needing to save on a variable would also only work starting 4.0. Extended patterns need only to be enabled once at the start of the script so it wouldn't really matter. It's also compatible to all versions of bash starting 2.05b at least. Well these last comments of mine were already just about why someone would prefer extended patterns over `=~`. The info could also be helpful to other readers as well perhaps if they care compatibility. – konsolebox Sep 01 '13 at 14:52
  • @Hans then you're trying to use it with a shell that isn't bash 3.x or newer. Note that `/bin/sh` is POSIX sh (or, on ancient systems, Bourne shell), not bash, even when it's a symlink to bash. – Charles Duffy Dec 06 '13 at 12:55
  • 5
    I did `if [[ 123 =~ '^[0-9]+$' ]]; then echo good; fi` and got nothing. But `re='^[0-9]+$'; if [[ 123 =~ $re ]]; then echo good; fi` said `good`. Why? Do I have to escape something in the first version? – Frozen Flame Aug 01 '14 at 02:24
  • 5
    @FrozenFlame, putting the right-hand side of `=~` in quotes makes it no longer a regular expression but a regular string (in modern versions of bash but not some ancient ones), whereas the version I gave in this answer works consistently in every release where `=~` is supported at all. – Charles Duffy Aug 01 '14 at 13:50
  • What about when a number starts with a decimal point, e.g. `.5`? I would use `^-?([0-9]*[.])?[0-9]+$` instead. – HelloGoodbye Jun 03 '15 at 08:32
  • Why the "!", "=~" and "[[ ]]"? Can someone explain the if/else components? – LP_640 Oct 07 '15 at 20:12
  • @LP_640, they're not if components -- you could use all those constructs outside the context of an `if`. See BashFAQ #31 (http://mywiki.wooledge.org/BashFAQ/031) for the rest; beyond what's covered there, `!` inverts the exit status of a command (make truthy things falsey, and falsey things true); it's a common way of spelling "not" in many, many languages. – Charles Duffy Oct 11 '15 at 13:49
  • @JMS, see `str="hello world"; re='[[:space:]]'; if [[ $str =~ $re ]]; then echo "matched"; fi` behaving as-expected at https://ideone.com/K7kM6M – Charles Duffy Jul 21 '19 at 20:23
  • @JMS, ...I wonder if your execution environment involves `eval` or some other pre-execution stage. Can you come up with an example that doesn't work without the quotes and post a link to it executing on http://ideone.com/? – Charles Duffy Jul 21 '19 at 20:54
  • @CharlesDuffy dug into it a little more and you are right works either way. not sure what was going on earlier. Thank you for your comments. removed my other comments. btw.. couldn't get ideone.com to accept input. is that a thing? – JMS Jul 21 '19 at 22:09
  • The only thing that's tricky (for me) about ideone's stdin implementation is that they don't implement the trailing newline required for valid UNIX text files, but rather go the DOS/Windows route of only putting newlines *between* lines but not after the last one, so `read` fails for the last line entered into the stdin box (which can be cured by putting an empty line on the end of the stdin box). Other than that, though, I haven't historically had a problem. – Charles Duffy Jul 21 '19 at 22:12
  • Be aware for the decimal starting with a dot like `.3`, or ending with a dot like `3.`, it will NOT work. I would like to suggest using `^[+-]?([0-9]*[.])?([0-9]+)?$`, however, three special cases: `.`, `+.`, `-.` have to be excluded first. – Xiang Sep 11 '19 at 22:24
  • I was trying to accommodate for leading and trailing spaces, as in x=" 123 ". So I tried changing re='^\s*[0-9]+\s*$' . But it never matches a number with lead and/or trailing spaces ... – Gab Jun 09 '20 at 21:04
  • it seems to work to not quote it if you don't want to declare a re variable: if [[ "123a" =~ ^[0-9]+$ ]] ; then echo true ; else echo false ; fi – AmanicA Jun 30 '20 at 10:24
  • @AmanicA, it needs to be unquoted on the right hand side whether or not it comes from a variable. The use of variables as a recommended practice is for backwards compatibility with old versions of bash in cases where your regex contains backslashes, as described in the comment thread above. – Charles Duffy Jun 30 '20 at 14:35
  • As I'm very new to bash I wonder if there is any specific reason why the if statement in the first example can't be like this `if [[ ! $yournumber =~ $re ]]` ? – n1nsa1d00 Jul 02 '20 at 14:18
  • @MarcoDufal, maybe it's just because I haven't had my coffee yet, but how's that different from what _is_ already there? – Charles Duffy Jul 02 '20 at 15:27
  • @CharlesDuffy oh well simply the negation operator `!` is not in the square brackets in your code snippet. – n1nsa1d00 Jul 02 '20 at 17:37
  • 1
    Gotcha. Both perfectly legal; I like keeping the negation in the outer scope where doing so reflects the meaning as a matter of habit so more complex conditions are easier to read. For example, consider `[[ ! $foo && $bar ]]`; a reader needs to think about whether the `!` is applied only to `$foo`, or to the result of ANDing both; put it in the outside, and it's obvious at a glance that the negation applies to the entire test, not just one branch of it. – Charles Duffy Jul 02 '20 at 18:25
  • Or, to handle integers that do not have a lead zero `^[1-9][0-9]*$` – Jesse Chisholm Jul 24 '20 at 22:07
  • This is why i use python when its an option. – jerrylogansquare Dec 11 '20 at 16:48
  • Shouldn't we use `return` instead of `exit 1`, which exits the ssh session as well – alper May 27 '21 at 23:51
  • 1
    @alper, depends on the context. I was assuming this would be used in a script expected to exit if given invalid arguments. If you're using it on a function, yes, use `return`. – Charles Duffy May 28 '21 at 00:12
  • 1
    @CharlesDuffy Please have a look at my [fully edited answer](https://stackoverflow.com/a/61835747/1765658) (There is a more suitable floating version of regex solution) – F. Hauri - Give Up GitHub Jun 24 '21 at 16:01
  • I heard `printf` is MUCH MORE efficient. – B. Shea Dec 07 '21 at 01:54
  • @B.Shea, I'd need more context (what specific use of printf? Compared to what?) to even start to evaluate that. Many of the answers here pipe output of `printf` to something else; setting up a pipeline is not efficient at all. – Charles Duffy Dec 07 '21 at 01:56
392

Without bashisms (works even in the System V sh),

case $string in
    ''|*[!0-9]*) echo bad ;;
    *) echo good ;;
esac

This rejects empty strings and strings containing non-digits, accepting everything else.

Negative or floating-point numbers need some additional work. An idea is to exclude - / . in the first "bad" pattern and add more "bad" patterns containing the inappropriate uses of them (?*-* / *.*.*)

jilles
  • 10,509
  • 2
  • 26
  • 39
  • 27
    +1 -- this is idiomatic, portable way back to the original Bourne shell, and has built-in support for glob-style wildcards. If you come from another programming language, it looks eerie, but it's much more elegant than coping with the brittleness of various quoting issues and endless backwards/sideways compatibility problems with `if test ...` – tripleee Sep 04 '11 at 13:21
  • 6
    You can change the first line to `${string#-}` (which doesn't work in antique Bourne shells, but works in any POSIX shell) to accept negative integers. – Gilles 'SO- stop being evil' Jan 03 '12 at 17:17
  • 4
    Also, this is easy to extend to floats -- just add `'.' | *.*.*` to the disallowed patterns, and add dot to the allowed characters. Similarly, you can allow an optional sign before, although then I would prefer `case ${string#[-+]}` to simply ignore the sign. – tripleee Jun 07 '14 at 12:10
  • See this for handling signed integers: http://stackoverflow.com/a/18620446/2013911 – Niklas Peter Dec 13 '15 at 07:48
  • Shouldn't it be `case "$string" in` instead of `case $string in` ? – Dor Oct 06 '16 at 20:53
  • 3
    @Dor The quotes are not needed, since the case command does not perform word splitting and pathname generation on that word anyway. (However, expansions in case patterns may need quoting since it determines whether pattern matching characters are literal or special.) – jilles Oct 07 '16 at 15:47
  • 1
    Explanation for the pipe(vertical bar) character: https://unix.stackexchange.com/questions/85939/what-is-the-pipe-character-xy-in-the-case-statement – Roland Feb 19 '20 at 14:43
  • 1
    This seem to be the quickest way! Have a look at [my comparison](https://stackoverflow.com/a/61835747/1765658) – F. Hauri - Give Up GitHub Jun 25 '21 at 15:02
259

The following solution can also be used in basic shells such as Bourne without the need for regular expressions. Basically any numeric value evaluation operations using non-numbers will result in an error which will be implicitly considered as false in shell:

"$var" -eq "$var"

as in:

#!/bin/bash

var=a

if [ -n "$var" ] && [ "$var" -eq "$var" ] 2>/dev/null; then
  echo number
else
  echo not a number
fi

You can can also test for $? the return code of the operation which is more explicit:

[ -n "$var" ] && [ "$var" -eq "$var" ] 2>/dev/null
if [ $? -ne 0 ]; then
   echo $var is not number
fi

Redirection of standard error is there to hide the "integer expression expected" message that bash prints out in case we do not have a number.

CAVEATS (thanks to the comments below):

  • Numbers with decimal points are not identified as valid "numbers"
  • Using [[ ]] instead of [ ] will always evaluate to true
  • Most non-Bash shells will always evaluate this expression as true
  • The behavior in Bash is undocumented and may therefore change without warning
  • If the value includes spaces after the number (e.g. "1 a") produces error, like bash: [[: 1 a: syntax error in expression (error token is "a")
  • If the value is the same as var-name (e.g. i="i"), produces error, like bash: [[: i: expression recursion level exceeded (error token is "i")
glenn jackman
  • 238,783
  • 38
  • 220
  • 352
Alberto Zaccagni
  • 30,779
  • 11
  • 72
  • 106
  • 10
    I'd still recommend this (but with the variables quoted to allow for empty strings), since the result is guaranteed to be *usable* as a number in Bash, no matter what. – l0b0 Dec 24 '10 at 08:43
  • 23
    Take care to use single brackets; `[[ a -eq a ]]` evaluates to true (both arguments get converted to zero) – Tgr Aug 28 '12 at 09:30
  • This is indeed very neat and works in `bash` (which was requested) and most other shells, but sadly not in `ksh`. If you want to be portable, use jilles' solution. – Adrian Frühwirth May 04 '13 at 14:12
  • 3
    Very nice! Note this this only works for an integer, not any number. I needed to check for a single argument which must be an integer, so this worked well: `if ! [ $# -eq 1 -o "$1" -eq "$1" ] 2>/dev/null; then` – haridsv Aug 02 '13 at 13:07
  • 6
    I would strongly advise against this method because of the not insignificant number of shells whose `[` builtin will evaluate the arguments as arithmetic. That is true in both ksh93 and mksh. Further, since both of those support arrays, there is easy opportunity for code injection. Use a pattern match instead. – ormaaj Oct 08 '14 at 05:34
  • 3
    @AlbertoZaccagni, in current releases of bash, these values are interpreted with numeric-context rules only for `[[ ]]` but not for `[ ]`. That said, this behavior is unspecified by both the POSIX standard for `test` and in bash's own documentation; future versions of bash could modify behavior to match ksh without breaking any documented behavioral promises, so relying on its current behavior persisting is not guaranteed to be safe. – Charles Duffy Mar 26 '16 at 18:43
  • Ah I see, thanks for explanation. I wasn't really worrying about that 7 years ago when I wrote this :D – Alberto Zaccagni Mar 27 '16 at 23:02
  • Strictly speaking, this is not entirely "undocumented". `man test` shows `-eq` is for ints and `=` is for strings. the man page *implies* but doesn't explicitly say `-eq` will fail for string comparison. – JDS Sep 02 '16 at 16:24
  • How can you negate this? – pfnuesel Feb 24 '18 at 14:36
  • @pfnuesel `! [ "$var" -eq "$var" ]`. Reason: `-ne` returns non-zero for non-integer arguments. – Tom Hale Sep 04 '18 at 07:23
  • You'll need to test that `$var` is non-null. I'll suggest an edit: `[ -n "$var" ] && [ "$var" -eq "$var" ] 2>/dev/null` – Tom Hale Sep 04 '18 at 07:38
  • 1
    @TomHale and Alberto, *${x:-1} -eq ${x:-0}* protects against empty input. – 0andriy Oct 03 '18 at 23:30
  • 1
    @AlbertoZaccagni You really should modify your answer to clearly state that it only works for **integers**, i.e. change all mentions of "number" by "integer". I know you hinted it in your caveat section, but you're using the general word "number" everywhere in the main part of your message which is false from a mathematical (set theory) point of view and misleads the inexperienced user. – Atralb Nov 05 '20 at 14:11
  • @AlbertoZaccagni @CharlesDuffy ... why should this not be POSIX compliant? When using the `[ … ]` form (which is `test`) POSIX quite clearly states that this is: "n1 -eq n2: True if the integers n1 and n2 are algebraically equal; otherwise, false." (https://pubs.opengroup.org/onlinepubs/9699919799/utilities/test.html) Strings cannot be integers or algebraically equal, so the worst than can happen is that it gives an exit status `>1` instead of just `1` which is however not an issue for the above problem. – calestyo Oct 02 '21 at 01:38
  • @calestyo, the standard you quote doesn't say anything about how hard a shell should (or shouldn't) try to coerce a non-integer value into an integer. Some shells, given a string, will try to coerce it to an integer by doing things like looking at whether it is in fact the name of a variable that contains an integer; and those going-to-extra-lengths approaches can sometimes involve code execution. Even those shells that don't process anything but a proper integer will often have side effects when non-integers are present, such as writing an error message to stderr. – Charles Duffy Oct 02 '21 at 03:10
  • @calestyo, ...which is to say: The standard you quote says what the shell _must_ do to be compliant, but it doesn't by any means proscribe the shell from doing other things in addition. That's true for most of POSIX: Unspecified behaviors are room for extensions, as opposed to being prohibited by default. – Charles Duffy Oct 02 '21 at 03:12
80

Nobody suggested bash's extended pattern matching:

[[ $1 == ?(-)+([0-9]) ]] && echo "$1 is an integer"

or using a POSIX character class:

[[ $1 == ?(-)+([[:digit:]]) ]] && echo "$1 is an integer"
glenn jackman
  • 238,783
  • 38
  • 220
  • 352
  • 6
    Glenn, I remove `shopt -s extglob` from your post (that I upvoted, it's one of my favorite answers here), since in [Conditional Constructs](http://www.gnu.org/software/bash/manual/bashref.html#Conditional-Constructs) you can read: _When the `==` and `!=` operators are used, the string to the right of the operator is considered a pattern and matched according to the rules described below in [Pattern Matching](http://www.gnu.org/software/bash/manual/bashref.html#Pattern-Matching), as if the `extglob` shell option were enabled._ I hope you don't mind! – gniourf_gniourf Feb 13 '15 at 19:49
  • In such contexts, you don't need to `shopt extglob`... that's a good thing to know! – gniourf_gniourf Feb 13 '15 at 19:49
  • Works well for simple integers. –  Mar 09 '15 at 17:15
  • Your solution does not work In `3.2.25(1)-release` of bash: `-bash: syntax error in conditional expression: unexpected token `(' -bash: syntax error near `?(-'. In that release, the featured noted by @gniourf_gniourf is not working. – Jdamian Sep 08 '16 at 07:07
  • @gniourf_gniourf, unlike **extended pattern matching** does not work in 3.2.25 release as you quoted, the **simple pattern matching** does work, for instance, `[[ x3x = *[[:digit:]]* ]]`, even with the single `=` operator. In 3.2 bash man pages the string *`as if the extglob shell option were enabled`* is not found. – Jdamian Sep 08 '16 at 07:38
  • 3
    @Jdamian: you're right, this was added in Bash 4.1 (which was released at the end of 2009… Bash 3.2 was released in 2006… it's now an antique software, sorry for those who are stuck in the past). Also, you could argue that extglobs where introduced in version 2.02 (released in 1998), and don't work in <2.02 versions… Now your comment here will serve as a caveat regarding older versions. – gniourf_gniourf Sep 08 '16 at 09:22
  • Answer would be stronger if it gave a little more explanation. – studgeek Jan 06 '17 at 01:53
  • @glennjackman: How to avoid negative numbers using the approach you are using ? I want only positive numbers. – Destructor Nov 15 '17 at 13:45
  • @Destructor, remove the `?(-)` bit that optionally matches the leading hyphen, so you just match digits with `+([0-9])` – glenn jackman Nov 15 '17 at 14:27
  • This worked for me in `bash 5.0.11`, but shouldn't it be `"$1"` instead of `$1`? or doesn't matter in this case? – downtheroad Jan 14 '20 at 15:02
  • 1
    Variables within `[[...]]` are not subject to word splitting or glob expansion. – glenn jackman Jan 14 '20 at 15:25
  • also my favorite answer, I feel that more explaining will be nice; also seems that it is not both hands [[ $1 == ?(-)+([0-9]) ]] --> does work [[ ?(-)+([0-9]) == $1 ]] --> did not work (for me at least, ubuntu 18) – Thiago Conrado Dec 07 '20 at 23:47
  • 1
    @ThiagoConrado, look up [`[[...]]`](https://www.gnu.org/software/bash/manual/bash.html#index-_005b_005b) in the manual (or `help [[` at a bash prompt): only the right-hand side of `==` is a pattern. – glenn jackman Dec 08 '20 at 00:03
  • How would you reverse this to check for non-integer? – Hashim Aziz May 28 '21 at 23:40
  • 1
    Use `[[:digit:]]` instead of `[:digit:]` for POSIX. – bugi Feb 14 '22 at 21:02
  • Just a comment... unfortunately in zsh this doesn't work. – Bernardo Loureiro Jul 01 '22 at 21:14
  • @Bernardo, which "this" is this? Are you commenting on my answer or one of the other comments? Perhaps it's the bug in the answer that bugi pointed out (now fixed) – glenn jackman Jul 01 '22 at 23:18
  • @glennjackman "this is" is the code in your answer. I'm commenting about your answer. I've tested with #bugi approach about POSIX too, also doesn't work. But pay attention, I didn't report a bug, I just write a comment. – Bernardo Loureiro Jul 01 '22 at 23:43
  • I can confirm the answer doesn't work in zsh: `syntax error near unexpected token `('` – Dark Star1 Jan 30 '23 at 08:36
60

Some performance and compatibility hints

There are some strongly different methods regarding different kinds of tests.

I reviewed most relevant methods and built this comparison.

Unsigned Integer is_uint()

These functions implement code to assess whether an expression is an unsigned integer, i.e. consists entirely of digits.

  • Using parameter expansion

    (This was my approach before all this!)

    isuint_Parm() { [ "$1" ] && [ -z "${1//[0-9]}" ] ;}
    
  • Using fork to grep

    isuint_Grep() { grep -qE '^[0-9]+$' <<<"$1"; }
    

    I test this method only once because it's very slow. This is just there to show what not to do.

  • Using integer capabilities

    isuint_Bash() { (( 10#$1 >= 0 )) 2>/dev/null ;}
    

    or better:

    isuint_Bash() { set -- ${1//[+-]/.};(( 10#$1 >= 0 )) 2>/dev/null ;}
    
  • Using case

    isuint_Case() { case $1 in ''|*[!0-9]*) return 1;;esac;}
    
  • Using 's regex

    isuint_Regx() { [[ $1 =~ ^[0-9]+$ ]] ;}
    

Signed integer is_int()

These functions implement code to assess whether an expression is a signed integer, i.e. as above but permitting an optional sign before the number.

  • Using parameter expansion

    isint_Parm() { local chk=${1#[+-]}; [ "$chk" ] && [ -z "${chk//[0-9]}" ] ;}
    
  • Using integer capabilities

    isint_Bash() {  set -- "${1//[!+-]}" ${1#${1//[!+-]}};
                    (( ( 0 ${1:-+} 10#$2 ) ? 1:1 )) 2>/dev/null ;}
    
  • Using case

    isint_Case() { case ${1#[-+]} in ''|*[!0-9]*) return 1;;esac;}
    
  • Using 's regex

    isint_Regx() { [[ $1 =~ ^[+-]?[0-9]+$ ]] ;}
    

Number (unsigned float) is_num()

These functions implement code to assess whether an expression is a floating-point number, i.e. as above but permitting an optional decimal point and additional digits after it. This does not attempt to cover numeric expressions in scientific notation (e.g. 1.0234E-12).

  • Using parameter expansion

    isnum_Parm() { local ck=${1#[+-]};ck=${ck/.};[ "$ck" ]&&[ -z "${ck//[0-9]}" ];}
    
  • Using 's regex

    isnum_Regx() { [[ $1 =~ ^[+-]?([0-9]+([.][0-9]*)?|\.[0-9]+)$ ]] ;}
    
  • Using case

    isnum_Case() { case ${1#[-+]} in ''|.|*[!0-9.]*|*.*.*) return 1;; esac ;}
    

Tests of concepts

(You could copy/paste this test code after previous declared functions.)

testcases=(
    0  1 42 -3 +42 +3. .9 3.14 +3.141 -31.4 '' . 3-3 3.1.4 3a a3 blah 'Good day!'
);printf '%-12s %4s %4s %4s %4s %4s %4s %4s %4s %4s %4s %4s %4s\n' Value\\Func \
    U{Prm,Grp,Bsh,Cse,Rgx} I{Prm,Bsh,Cse,Rgx} N{Prm,Cse,Rgx};\
for var in "${testcases[@]}";do
    outstr='';
    for func in isuint_{Parm,Grep,Bash,Case,Regx} isint_{Parm,Bash,Case,Regx} \
                isnum_{Parm,Case,Regx};do
        if $func "$var"; then
            outstr+='   ##'
        else
            outstr+='   --'
        fi
    done
    printf '%-11s %s\n' "$var" "$outstr"
done

Should output:

Value\Func   UPrm UGrp UBsh UCse URgx IPrm IBsh ICse IRgx NPrm NCse NRgx
0              ##   ##   ##   ##   ##   ##   ##   ##   ##   ##   ##   ##
1              ##   ##   ##   ##   ##   ##   ##   ##   ##   ##   ##   ##
42             ##   ##   ##   ##   ##   ##   ##   ##   ##   ##   ##   ##
-3             --   --   --   --   --   ##   ##   ##   ##   ##   ##   ##
+42            --   --   --   --   --   ##   ##   ##   ##   ##   ##   ##
+3.            --   --   --   --   --   --   --   --   --   ##   ##   ##
.9             --   --   --   --   --   --   --   --   --   ##   ##   ##
3.14           --   --   --   --   --   --   --   --   --   ##   ##   ##
+3.141         --   --   --   --   --   --   --   --   --   ##   ##   ##
-31.4          --   --   --   --   --   --   --   --   --   ##   ##   ##
               --   --   --   --   --   --   --   --   --   --   --   --
.              --   --   --   --   --   --   --   --   --   --   --   --
3-3            --   --   --   --   --   --   ##   --   --   --   --   --
3.1.4          --   --   --   --   --   --   --   --   --   --   --   --
3a             --   --   --   --   --   --   --   --   --   --   --   --
a3             --   --   --   --   --   --   --   --   --   --   --   --
blah           --   --   --   --   --   --   --   --   --   --   --   --
Good day!      --   --   --   --   --   --   --   --   --   --   --   --

I hope! (Note: uint_bash seem not perfect!)

Performance comparison

Then I've built this test function:

testFunc() {
    local tests=1000 start=${EPOCHREALTIME//.}
    for ((;tests--;)) ;do
        "$1" "$3"
    done
    printf -v "$2" %u $((${EPOCHREALTIME//.}-start))
}
percent(){ local p=00$((${1}00000/$2));printf -v "$3" %.2f%% ${p::-3}.${p: -3};}
sortedTests() {
    local func NaNTime NumTime ftyp="$1" nTest="$2" tTest="$3" min i pct line
    local -a order=()
    shift 3
    for func ;do
        testFunc "${ftyp}_$func" NaNTime "$tTest"
        testFunc "${ftyp}_$func" NumTime "$nTest"
        order[NaNTime+NumTime]=${ftyp}_$func\ $NumTime\ $NaNTime
    done
    printf '%-12s %11s %11s %14s\n' Function Number NaN Total
    min="${!order[*]}" min=${min%% *}
    for i in "${!order[@]}";do
        read -ra line <<<"${order[i]}"
        percent "$i" "$min" pct
        printf '%-12s %9d\U00B5s %9d\U00B5s  %12d\U00B5s  %9s\n' \
               "${line[@]}" "$i" "$pct"
    done
}

I could run in this way:

sortedTests isuint "This is not a number." 31415926535897932384 \
            Case Grep Parm Bash Regx ;\
sortedTests isint  "This is not a number." 31415926535897932384 \
            Case Parm Bash Regx ;\
sortedTests isnum "This string is clearly not a number..." \
            3.141592653589793238462643383279502884  Case Parm Regx

On my host, this shows somthing like:

Function          Number         NaN          Total
isuint_Case       6499µs      6566µs         13065µs    100.00%
isuint_Parm      26687µs     31600µs         58287µs    446.13%
isuint_Regx      36511µs     40181µs         76692µs    587.00%
isuint_Bash      43819µs     40311µs         84130µs    643.93%
isuint_Grep    1298265µs   1224112µs       2522377µs  19306.37%

Function          Number         NaN          Total
isint_Case       22687µs     21914µs         44601µs    100.00%
isint_Parm       35765µs     34428µs         70193µs    157.38%
isint_Regx       36949µs     42319µs         79268µs    177.73%
isint_Bash       55368µs     65095µs        120463µs    270.09%

Function          Number         NaN          Total
isnum_Case       23313µs     23446µs         46759µs    100.00%
isnum_Parm       35677µs     42169µs         77846µs    166.48%
isnum_Regx       51864µs     69502µs        121366µs    259.56%

You could download full isnum comparission script here or full isnum comparission script as text here., (with UTF8 and LATIN handling).

Conclusion

  • case way is clearly the quickest! About 3x quicker than regex and 2x quicker than using parameter expansion.
  • forks (to grep or any binaries) are to be avoided when not needed.

case method has become my favored choice:

is_uint() { case $1        in '' | *[!0-9]*              ) return 1;; esac ;}
is_int()  { case ${1#[-+]} in '' | *[!0-9]*              ) return 1;; esac ;}
is_unum() { case $1        in '' | . | *[!0-9.]* | *.*.* ) return 1;; esac ;}
is_num()  { case ${1#[-+]} in '' | . | *[!0-9.]* | *.*.* ) return 1;; esac ;}

About compatibility

For this, I wrote a little test script based on previous tests, with:

for shell in bash dash 'busybox sh' ksh zsh "$@";do
    printf "%-12s  " "${shell%% *}"
    $shell < <(testScript) 2>&1 | xargs
done

This shows:

bash          Success
dash          Success
busybox       Success
ksh           Success
zsh           Success

As I know other based solution like regex and 's integer won't work in many other shells and forks are resource expensive, I would prefer the case way (just before parameter expansion which is mostly compatible too).

Dharman
  • 30,962
  • 25
  • 85
  • 135
F. Hauri - Give Up GitHub
  • 64,122
  • 17
  • 116
  • 137
  • I agree, anyway, I prefer not to use regex, when I could use parameter expansion... Abusing of RE will make bash script slower – F. Hauri - Give Up GitHub May 21 '20 at 09:35
  • @CharlesDuffy, On my raspberry pi, 1000 iteration will take 2,5sec with my version and 4,4sec with your version! – F. Hauri - Give Up GitHub May 21 '20 at 10:01
  • Can't argue with that. – Charles Duffy May 21 '20 at 12:42
  • TIL `set --` can be used to set positional params – fire Jul 24 '20 at 04:08
  • @tripleee The script you point is buggy! `casenum` don't work with floating numbers! And his test don't use his `$fun` variable!! But I agree, I could pay some attention to this way of doing! – F. Hauri - Give Up GitHub Jun 24 '21 at 07:30
  • Thanks for the feedback; updated the snippet. Sorry for the silly typo. The `case` case (sic) is now clearly faster on IdeOne too. (I should have checked ...) Though probably a better test would not only check the success flow. – tripleee Jun 24 '21 at 07:32
  • Already working on: `isnum(){ case ${1#[-+]} in ''|*[!0-9.]*|*.*.*) return -1;;esac ;}`... seem good! – F. Hauri - Give Up GitHub Jun 24 '21 at 07:45
  • This script could be found there: [isnum_comparission.sh](https://f-hauri.ch/vrac/isnum_comparission.sh.txt) – F. Hauri - Give Up GitHub Jun 27 '21 at 17:35
  • Perhaps notice that `isuint_Bash` is slightly faster than `case` in the `NaN` test. If you are in a tight loop where every cycle counts, and you expect mostly invalid input, that could tilt the comparison in favor of the parameter expansion version. But only for the `uint` case. Perhaps the difference is small enough that it's within the margin of measurement error anyway. – tripleee Jun 28 '21 at 05:13
  • @tripleee I don't thik. This look a tiny difference due to the fact tests was done on my personal desk, who's not quiet. Running this test (from [my script](https://f-hauri.ch/vrac/isnum_comparission.sh)) many times will present this difference randomly in the other way.,, – F. Hauri - Give Up GitHub Jun 28 '21 at 12:41
  • 1
    @tripleee redo the test like published there, many time when my desk was relatively quiet, then chosed more relevant output to publish there. (Did you try my script? I've added a `+Numberr` column, I won't try to explain this there;) – F. Hauri - Give Up GitHub Jun 28 '21 at 13:10
  • 1
    Based on these solutions, you could expand to full float comparisions which would include scientific notations: `is_float() { is_num "${1/[eE][-+]/}"; }` – kvantour Jun 05 '22 at 10:16
  • First downgrade! I'm curious knowing why!! – F. Hauri - Give Up GitHub Jun 08 '23 at 17:16
57

This tests if a number is a non-negative integer. It is shell independent (i.e. without bashisms) and uses only shell built-ins:

[ ! -z "${num##*[!0-9]*}" ] && echo "is a number" || echo "is not a number";

A previous version of this answer proposed:

[ -z "${num##[0-9]*}" ] && echo "is a number" || echo "is not a number";

but this is INCORRECT since it accepts any string starting with a digit, as jilles suggested.

mrucci
  • 4,342
  • 3
  • 33
  • 35
  • 6
    This does not work properly, it accepts any string starting with a digit. Note that WORD in ${VAR##WORD} and similar is a shell pattern, not a regular expression. – jilles Oct 16 '10 at 22:46
  • 3
    Can you translate that expression into English, please? I really want to use it, but I don't understand it enough to trust it, even after perusing the bash man page. – CivFan May 25 '16 at 21:56
  • 4
    `*[!0-9]*` is a pattern that matches all strings with at least 1 non-digit character. `${num##*[!0-9]*}` is a "parameter expansion" where we take the content of the `num` variable and remove the longest string that matches the pattern. If the result of the parameter expansion is not empty (`! [ -z ${...} ]`) then it's a number since it does not contain any non-digit character. – mrucci May 26 '16 at 19:00
  • Unfortunately this fails if there any digits in the argument, even if it is not valid number. For example "exam1ple" or "a2b". – studgeek Jan 06 '17 at 00:59
  • Both fail with `122s` `:-(` – Hastur Apr 06 '18 at 14:30
  • 1
    But that's good, because "exam1ple", "a2b" and "122s" are all not numbers. – nooblag Aug 15 '21 at 14:19
27

I'm surprised at the solutions directly parsing number formats in shell. shell is not well suited to this, being a DSL for controlling files and processes. There are ample number parsers a little lower down, for example:

isdecimal() {
  # filter octal/hex/ord()
  num=$(printf '%s' "$1" | sed "s/^0*\([1-9]\)/\1/; s/'/^/")

  test "$num" && printf '%f' "$num" >/dev/null 2>&1
}

Change '%f' to whatever particular format you require.

pixelbeat
  • 30,615
  • 9
  • 51
  • 60
  • 4
    `isnumber(){ printf '%f' "$1" &>/dev/null && echo "this is a number" || echo "not a number"; }` – Gilles Quénot Sep 28 '12 at 18:33
  • Nice solution. printf really works well, even errors with something appropriate-ish. – dpb Feb 26 '13 at 21:57
  • 4
    @sputnick your version breaks the inherent (and useful) return value semantics of the original function. So, instead, simply leave the function as-is, and use it: `isnumber 23 && echo "this is a number" || echo "not a number"` – michael Jul 18 '13 at 23:54
  • 4
    Shouldn't this have also `2>/dev/null`, so that `isnumber "foo"` does not pollute stderr? – gioele Jun 06 '14 at 06:10
  • 4
    To call modern shells like bash "a DSL for controlling files and processes" is ignoring that they're used for much more than that - some distros have built entire package managers and web interfaces on it (as ugly as that might be). Batch files fit your description though, as even setting a variable there is difficult. – Camilo Martin Jun 26 '14 at 10:57
  • 7
    It's funny that you're trying to be smart by copying some idioms from other languages. Unfortunately this doesn't work in shells. Shells are very special, and without solid knowledge about them, you're likely to write broken code. Your code is broken: `isnumber "'a"` will return true. This is documented in the [POSIX spec](http://pubs.opengroup.org/onlinepubs/9699919799/utilities/printf.html) where you'll read: _If the leading character is a single-quote or double-quote, the value shall be the numeric value in the underlying codeset of the character following the single-quote or double-quote._ – gniourf_gniourf Feb 13 '15 at 19:37
  • There are very likely other cases where your code will break. I just gave one. I hope that the 27 guys who upvoted didn't use this in critical production code. `:D`. – gniourf_gniourf Feb 13 '15 at 19:38
  • 2
    Another case where it fails: `isinteger() { [[ $1 ]] && printf '%d' "$1" >/dev/null 2>&1; }` will fail with, e.g., `isinteger 09`: that's because of a silly leading `0`. Now you might argue that `09` shouldn't be validated... whatever, `isnumber "'a"` is an already good proof that all this design is broken. – gniourf_gniourf Feb 13 '15 at 19:40
  • Note the function was isnumber(). If you want a more restrictive isdecimal() then you could filter with `sed "s/^0*//; s/'/^/"` – pixelbeat Feb 14 '15 at 22:40
  • 1
    I know the function was `isnumber` (the `isinteger` function I gave is just a (failed) attempt in generalizing your method). Funny, but now `0` is not a number. Your function now spawns 4 subshells, uses an external command, doesn't work. And you claim that a shell is just a DSL for controlling files and processes? you need to learn better shell techniques, and when you're more knowledgeable you'll see that the only sane way of solving this problem is to indeed parse the string. – gniourf_gniourf Feb 15 '15 at 07:38
  • By the way, you're misusing `printf`. For example, your function validates `42\n\n\n\n\n\n\n\n\n\n\n\n\n` or `%s42%s` or `%d9` – gniourf_gniourf Feb 15 '15 at 07:45
  • 2
    Much better! yet it still validates variables that contain trailing newlines: `$'1\n\n\n'`; that's because `$(...)` trims trailing newlines. Whatever. You're at the point where you criticize methods that parse the string explicitly (and so we expect your method to be really superior) yet: your method spawns 3 subshells, isn't even 100% safe (regarding trailling newlines), somehow parses the string with `sed`, is much less efficient than methods that directly parse the string. Good job. – gniourf_gniourf Feb 15 '15 at 13:02
  • Could you add an example of how to call this and use its result? – Lightness Races in Orbit Jun 17 '15 at 11:26
  • 2
    "There are ample number parsers a little lower down" -- yet you're not using one, but implementing your own regex-based solution. Since bash has native regexes, this buys what exactly? – Charles Duffy Mar 26 '16 at 18:49
  • @pixelbeat I have real trouble understanding the rationale behind and use of `s/'/^/`. Would you mind precising it ? I don't see how replacing `'` with `^` would make any sense (this is what `sed` does when `^` is alone) when considering a decimal input number. – Atralb Nov 05 '20 at 16:10
  • Replacing ' with ^, is replacing something printf interprets as a number with something it does not. I.e. We generally don't want to consider `'blah` as a number, so tweak it to something that printf will reject – pixelbeat Nov 06 '20 at 17:57
  • Why is it correct to treat `'10` as a number either? `'`s can legitimately be present as _syntax_ in code that encodes literals meant to be treated as numbers, but they should never be present in the data. – Charles Duffy Oct 28 '22 at 17:18
22

I was looking at the answers and... realized that nobody thought about FLOAT numbers (with dot)!

Using grep is great too.
-E means extended regexp
-q means quiet (doesn't echo)
-qE is the combination of both.

To test directly in the command line:

$ echo "32" | grep -E ^\-?[0-9]?\.?[0-9]+$  
# answer is: 32

$ echo "3a2" | grep -E ^\-?[0-9]?\.?[0-9]+$  
# answer is empty (false)

$ echo ".5" | grep -E ^\-?[0-9]?\.?[0-9]+$  
# answer .5

$ echo "3.2" | grep -E ^\-?[0-9]?\.?[0-9]+$  
# answer is 3.2

Using in a bash script:

check=`echo "$1" | grep -E ^\-?[0-9]*\.?[0-9]+$`

if [ "$check" != '' ]; then    
  # it IS numeric
  echo "Yeap!"
else
  # it is NOT numeric.
  echo "nooop"
fi

To match JUST integers, use this:

# change check line to:
check=`echo "$1" | grep -E ^\-?[0-9]+$`
Alexander O'Mara
  • 58,688
  • 18
  • 163
  • 171
Sergio Abreu
  • 2,686
  • 25
  • 20
14

Just a follow up to @mary. But because I don't have enough rep, couldn't post this as a comment to that post. Anyways, here is what I used:

isnum() { awk -v a="$1" 'BEGIN {print (a == a + 0)}'; }

The function will return "1" if the argument is a number, otherwise will return "0". This works for integers as well as floats. Usage is something like:

n=-2.05e+07
res=`isnum "$n"`
if [ "$res" == "1" ]; then
     echo "$n is a number"
else
     echo "$n is not a number"
fi
gniourf_gniourf
  • 44,650
  • 9
  • 93
  • 104
triple_r
  • 1,037
  • 1
  • 8
  • 21
  • 5
    Printing a number is less useful than setting an exit code. `'BEGIN { exit(1-(a==a+0)) }'` is slightly hard to grok but can be used in a function which returns true or false just like `[`, `grep -q`, etc. – tripleee Aug 02 '17 at 07:01
11
test -z "${i//[0-9]}" && echo digits || echo no no no

${i//[0-9]} replaces any digit in the value of $i with an empty string, see man -P 'less +/parameter\/' bash. -z checks if resulting string has zero length.

if you also want to exclude the case when $i is empty, you could use one of these constructions:

test -n "$i" && test -z "${i//[0-9]}" && echo digits || echo not a number
[[ -n "$i" && -z "${i//[0-9]}" ]] && echo digits || echo not a number
user2683246
  • 3,399
  • 29
  • 31
11

For my problem, I only needed to ensure that a user doesn't accidentally enter some text thus I tried to keep it simple and readable

isNumber() {
    (( $1 )) 2>/dev/null
}

According to the man page this pretty much does what I want

If the value of the expression is non-zero, the return status is 0

To prevent nasty error messages for strings that "might be numbers" I ignore the error output

$ (( 2s ))
bash: ((: 2s: value too great for base (error token is "2s")
Hachi
  • 3,237
  • 1
  • 21
  • 29
10

This can be achieved by using grep to see if the variable in question matches an extended regular expression.

Test integer 1120:

yournumber=1120
if echo "$yournumber" | grep -qE '^[0-9]+$'; then
    echo "Valid number."
else
    echo "Error: not a number."
fi

Output: Valid number.

Test non-integer 1120a:

yournumber=1120a
if echo "$yournumber" | grep -qE '^[0-9]+$'; then
    echo "Valid number."
else
    echo "Error: not a number."
fi

Output: Error: not a number.


Explanation

  • The grep, the -E switch allows us to use extended regular expression '^[0-9]+$'. This regular expression means the variable should only [] contain the numbers 0-9 zero through nine from the ^ beginning to the $ end of the variable and should have at least + one character.
  • The grep, the -q quiet switch turns off any output whether or not it finds anything.
  • if checks the exit status of grep. Exit status 0 means success and anything greater means an error. The grep command has an exit status of 0 if it finds a match and 1 when it doesn't;

So putting it all together, in the if test, we echo the variable $yournumber and | pipe it to grep which with the -q switch silently matches the -E extended regular expression '^[0-9]+$' expression. The exit status of grep will be 0 if grep successfully found a match and 1 if it didn't. If succeeded to match, we echo "Valid number.". If it failed to match, we echo "Error: not a number.".


For Floats or Doubles

We can just change the regular expression from '^[0-9]+$' to '^[0-9]*\.?[0-9]+$' for floats or doubles.

Test float 1120.01:

yournumber=1120.01
if echo "$yournumber" | grep -qE '^[0-9]*\.?[0-9]+$'; then
    echo "Valid number."
else
    echo "Error: not a number."
fi

Output: Valid number.

Test float 11.20.01:

yournumber=11.20.01
if echo "$yournumber" | grep -qE '^[0-9]*\.?[0-9]+$'; then
    echo "Valid number."
else
    echo "Error: not a number."
fi

Output: Error: not a number.


For Negatives

To allow negative integers, just change the regular expression from '^[0-9]+$' to '^\-?[0-9]+$'.

To allow negative floats or doubles, just change the regular expression from '^[0-9]*\.?[0-9]+$' to '^\-?[0-9]*\.?[0-9]+$'.

Joseph Shih
  • 1,244
  • 13
  • 25
  • 1
    LGTM; answer as-edited has my +1. The only things I'd do differently at this point are just matters of opinion rather than correctness (f/e, using `[-]` instead of `\-` and `[.]` instead of `\.` is a little more verbose, but it means your strings don't have to change if they're used in a context where backslashes get consumed). – Charles Duffy Mar 16 '20 at 15:07
  • I was using a different approach with `if [[ $yournumber =~ ^[0-9]+([.][0-9]+)?$ ]] ; then` in an old Ubuntu 14.04 based system but, somehow, it stopped working after upgrading to Ubuntu 20.04, your first solution for "Test Integer" does the same in 20.04. I can't say if it is related to the upgrade or maybe my script was wrong in first instance and -somehow- yet working in the old system. Thank you very much. – Geppettvs D'Constanzo Jul 07 '20 at 04:02
  • @GeppettvsD'Constanzo, perhaps might the script have been using `#!/bin/sh`? If so, it should still work in modern Ubuntu as long as you use a `#!/bin/bash` shebang, and avoid starting scripts with `sh scriptname` (which ignores the shebang and forces use of `sh` instead of `bash`). – Charles Duffy Aug 11 '20 at 16:00
  • Using an external process for something Bash has built in is always dubious. – tripleee Jun 24 '21 at 03:56
9

Old question, but I just wanted to tack on my solution. This one doesn't require any strange shell tricks, or rely on something that hasn't been around forever.

if [ -n "$(printf '%s\n' "$var" | sed 's/[0-9]//g')" ]; then
    echo 'is not numeric'
else
    echo 'is numeric'
fi

Basically it just removes all digits from the input, and if you're left with a non-zero-length string then it wasn't a number.

gniourf_gniourf
  • 44,650
  • 9
  • 93
  • 104
Sammitch
  • 30,782
  • 7
  • 50
  • 77
7

I would try this:

printf "%g" "$var" &> /dev/null
if [[ $? == 0 ]] ; then
    echo "$var is a number."
else
    echo "$var is not a number."
fi

Note: this recognizes nan and inf as number.

overflowed
  • 95
  • 1
  • 2
  • 2
    either duplicate of, or perhaps better suited as a comment to, pixelbeat's answer (using `%f` is probably better anyway) – michael Jul 19 '13 at 00:01
  • 4
    Instead of checking the previous status code, why not just put it in the `if` itself? That's what `if` does... `if printf "%g" "$var" &> /dev/null; then ...` – Camilo Martin Jun 26 '14 at 11:00
  • 3
    This has other caveats. It will validate the empty string, and strings like `'a`. – gniourf_gniourf Feb 13 '15 at 21:30
  • Best solution, in my book. I tried bc before realising bc doesn't do floats. The interpretation of the empty string as a number is a minor caveat (and "a" is not interpreted as a number). – JPGConnolly Jul 17 '20 at 07:52
  • @JPGConnly, what do you mean "bc doesn't do floats"? – Jdamian Sep 11 '20 at 09:51
  • See [Why is testing “$?” to see if a command succeeded or not, an anti-pattern?](https://stackoverflow.com/questions/36313216/why-is-testing-to-see-if-a-command-succeeded-or-not-an-anti-pattern) – tripleee Jun 28 '21 at 05:14
7

A clear answer has already been given by @charles Dufy and others. A pure bash solution would be using the following :

string="-12,345"
if [[ "$string" =~ ^-?[0-9]+[.,]?[0-9]*$ ]]
then
    echo $string is a number
else
    echo $string is not a number
fi

Although for real numbers it is not mandatory to have a number before the radix point.

To provide a more thorough support of floating numbers and scientific notation (many programs in C/Fortran or else will export float this way), a useful addition to this line would be the following :

string="1.2345E-67"
if [[ "$string" =~ ^-?[0-9]*[.,]?[0-9]*[eE]?-?[0-9]+$ ]]
then
    echo $string is a number
else
    echo $string is not a number
fi

Thus leading to a way to differentiate types of number, if you are looking for any specific type :

string="-12,345"
if [[ "$string" =~ ^-?[0-9]+$ ]]
then
    echo $string is an integer
elif [[ "$string" =~ ^-?[0-9]*[.,]?[0-9]*$ ]]
then
    echo $string is a float
elif [[ "$string" =~ ^-?[0-9]*[.,]?[0-9]*[eE]-?[0-9]+$ ]]
then
    echo $string is a scientific number
else
    echo $string is not a number
fi

Note: We could list the syntactical requirements for decimal and scientific notation, one being to allow comma as radix point, as well as ".". We would then assert that there must be only one such radix point. There can be two +/- signs in an [Ee] float. I have learned a few more rules from Aulu's work, and tested against bad strings such as '' '-' '-E-1' '0-0'. Here are my regex/substring/expr tools that seem to be holding up:

parse_num() {
 local r=`expr "$1" : '.*\([.,]\)' 2>/dev/null | tr -d '\n'` 
 nat='^[+-]?[0-9]+[.,]?$' \
 dot="${1%[.,]*}${r}${1##*[.,]}" \
 float='^[\+\-]?([.,0-9]+[Ee]?[-+]?|)[0-9]+$'
 [[ "$1" == $dot ]] && [[ "$1" =~ $float ]] || [[ "$1" =~ $nat ]]
} # usage: parse_num -123.456
BobDodds
  • 23
  • 3
Aulo
  • 141
  • 2
  • 7
7

Can't comment yet so I'll add my own answer, which is an extension to glenn jackman's answer using bash pattern matching.

My original need was to identify numbers and distinguish integers and floats. The function definitions deducted to:

function isInteger() {
    [[ ${1} == ?(-)+([0-9]) ]]
}

function isFloat() {
    [[ ${1} == ?(-)@(+([0-9]).*([0-9])|*([0-9]).+([0-9]))?(E?(-|+)+([0-9])) ]]
}

I used unit testing (with shUnit2) to validate my patterns worked as intended:

oneTimeSetUp() {
    int_values="0 123 -0 -123"
    float_values="0.0 0. .0 -0.0 -0. -.0 \
        123.456 123. .456 -123.456 -123. -.456
        123.456E08 123.E08 .456E08 -123.456E08 -123.E08 -.456E08 \
        123.456E+08 123.E+08 .456E+08 -123.456E+08 -123.E+08 -.456E+08 \
        123.456E-08 123.E-08 .456E-08 -123.456E-08 -123.E-08 -.456E-08"
}

testIsIntegerIsFloat() {
    local value
    for value in ${int_values}
    do
        assertTrue "${value} should be tested as integer" "isInteger ${value}"
        assertFalse "${value} should not be tested as float" "isFloat ${value}"
    done

    for value in ${float_values}
    do
        assertTrue "${value} should be tested as float" "isFloat ${value}"
        assertFalse "${value} should not be tested as integer" "isInteger ${value}"
    done

}

Notes: The isFloat pattern can be modified to be more tolerant about decimal point (@(.,)) and the E symbol (@(Ee)). My unit tests test only values that are either integer or float, but not any invalid input.

karttu
  • 121
  • 2
  • 6
7
[[ $1 =~ ^-?[0-9]+$ ]] && echo "number"

Don't forget - to include negative numbers!

Cryptjar
  • 1,079
  • 8
  • 13
D_I
  • 87
  • 1
  • 1
6

I use expr. It returns a non-zero if you try to add a zero to a non-numeric value:

if expr -- "$number" + 0 > /dev/null 2>&1
then
    echo "$number is a number"
else
    echo "$number isn't a number"
fi

It might be possible to use bc if you need non-integers, but I don't believe bc has quite the same behavior. Adding zero to a non-number gets you zero and it returns a value of zero too. Maybe you can combine bc and expr. Use bc to add zero to $number. If the answer is 0, then try expr to verify that $number isn't zero.

David W.
  • 105,218
  • 39
  • 216
  • 337
  • 2
    This is rather bad. To make it slightly better you should use `expr -- "$number" + 0`; yet this will still pretend that `0 isn't a number`. From `man expr`: `Exit status is 0 if EXPRESSION is neither null nor 0, 1 if EXPRESSION is null or 0,` – gniourf_gniourf Feb 13 '15 at 21:23
  • With Bash, you really should never need `expr`. If you are confined to a lesser Bourne shell like POSIX `sh`, then maybe. – tripleee Jun 24 '21 at 03:58
  • POSIX sh is guaranteed to have `$(( ))`. You're talking 1970s Bourne to need `expr`. – Charles Duffy Jun 24 '22 at 19:20
6

One simple way is to check whether it contains non-digit characters. You replace all digit characters with nothing and check for length. If there's length it's not a number.

if [[ ! -n ${input//[0-9]/} ]]; then
    echo "Input Is A Number"
fi
roaima
  • 588
  • 7
  • 27
Andrew
  • 3,733
  • 1
  • 35
  • 36
  • 2
    To handle negative numbers would require a more complicated approach. – Andrew May 08 '17 at 14:28
  • ... Or an optional positive sign. – tripleee Aug 02 '17 at 06:52
  • @tripleee i'd like to see your approach if you know how to do it. – Andrew Aug 02 '17 at 15:08
  • 1
    @andrew with this little change, your code work (using zsh) very fine! Even for negative or positive numbers: `[[ ! -n ${1//[+\-0-9]/} ]] && echo "is a number" || echo "is not a number";`. The problem now is that `+-123` will pass too. – Bernardo Loureiro Jul 01 '22 at 21:29
  • 1
    Finally, I achieved expected result using some more changes beginning from your answer. Hope to help someone more. https://gist.github.com/bernardolm/1c9e003a5f68e6e2534458fa758a096d – Bernardo Loureiro Jul 01 '22 at 22:08
  • @BernardoLoureiro this catches one symbol sign at the beginning: `[ $var = ${var//[0-9]/}${var/[+\-]/} ]` – Multifix Nov 21 '22 at 08:08
5

http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_04_03.html

You can also use bash's character classes.

if [[ $VAR = *[[:digit:]]* ]]; then
 echo "$VAR is numeric"
else
 echo "$VAR is not numeric"
fi

Numerics will include space, the decimal point, and "e" or "E" for floating point.

But, if you specify a C-style hex number, i.e. "0xffff" or "0XFFFF", [[:digit:]] returns true. A bit of a trap here, bash allows you do to something like "0xAZ00" and still count it as a digit (isn't this from some weird quirk of GCC compilers that let you use 0x notation for bases other than 16???)

You might want to test for "0x" or "0X" before testing if it's a numeric if your input is completely untrusted, unless you want to accept hex numbers. That would be accomplished by:

if [[ ${VARIABLE:1:2} = "0x" ]] || [[ ${VARIABLE:1:2} = "0X" ]]; then echo "$VAR is not numeric"; fi
ultrasawblade
  • 115
  • 1
  • 1
  • 20
    `[[ $VAR = *[[:digit:]]* ]]` will return true if the variable **contains** a number, not if it **is** an integer. – glenn jackman Oct 26 '12 at 14:48
  • `[[ "z3*&" = *[[:digit:]]* ]] && echo "numeric"` prints `numeric`. Tested in bash version `3.2.25(1)-release`. – Jdamian Sep 08 '16 at 07:13
  • 1
    @ultraswadable, your solution detects those strings containing, at least, one digit surrounded (or not) by any other characters. I downvoted. – Jdamian Sep 08 '16 at 07:28
  • The obviously correct approach is therefore to reverse this, and use `[[ -n $VAR && $VAR != *[^[:digit:]]* ]]` – eschwartz Oct 23 '18 at 15:16
  • 1
    @eschwartz , your solution doesn't work with negative numbers – Angel Mar 21 '19 at 21:57
5

As i had to tamper with this lately and like karttu's appoach with the unit test the most. I revised the code and added some other solutions too, try it out yourself to see the results:

#!/bin/bash

    # N={0,1,2,3,...} by syntaxerror
function isNaturalNumber()
{
 [[ ${1} =~ ^[0-9]+$ ]]
}
    # Z={...,-2,-1,0,1,2,...} by karttu
function isInteger() 
{
 [[ ${1} == ?(-)+([0-9]) ]]
}
    # Q={...,-½,-¼,0.0,¼,½,...} by karttu
function isFloat() 
{
 [[ ${1} == ?(-)@(+([0-9]).*([0-9])|*([0-9]).+([0-9]))?(E?(-|+)+([0-9])) ]]
}
    # R={...,-1,-½,-¼,0.E+n,¼,½,1,...}
function isNumber()
{
 isNaturalNumber $1 || isInteger $1 || isFloat $1
}

bools=("TRUE" "FALSE")
int_values="0 123 -0 -123"
float_values="0.0 0. .0 -0.0 -0. -.0 \
    123.456 123. .456 -123.456 -123. -.456 \
    123.456E08 123.E08 .456E08 -123.456E08 -123.E08 -.456E08 \
    123.456E+08 123.E+08 .456E+08 -123.456E+08 -123.E+08 -.456E+08 \
    123.456E-08 123.E-08 .456E-08 -123.456E-08 -123.E-08 -.456E-08"
false_values="blah meh mooh blah5 67mooh a123bc"

for value in ${int_values} ${float_values} ${false_values}
do
    printf "  %5s=%-30s" $(isNaturalNumber $value) ${bools[$?]} $(printf "isNaturalNumber(%s)" $value)
    printf "%5s=%-24s" $(isInteger $value) ${bools[$?]} $(printf "isInteger(%s)" $value)
    printf "%5s=%-24s" $(isFloat $value) ${bools[$?]} $(printf "isFloat(%s)" $value)
    printf "%5s=%-24s\n" $(isNumber $value) ${bools[$?]} $(printf "isNumber(%s)" $value)
done

So isNumber() includes dashes, commas and exponential notation and therefore returns TRUE on integers & floats where on the other hand isFloat() returns FALSE on integer values and isInteger() likewise returns FALSE on floats. For your convenience all as one liners:

isNaturalNumber() { [[ ${1} =~ ^[0-9]+$ ]]; }
isInteger() { [[ ${1} == ?(-)+([0-9]) ]]; }
isFloat() { [[ ${1} == ?(-)@(+([0-9]).*([0-9])|*([0-9]).+([0-9]))?(E?(-|+)+([0-9])) ]]; }
isNumber() { isNaturalNumber $1 || isInteger $1 || isFloat $1; }
3ronco
  • 552
  • 9
  • 12
  • Personally I would remove the `function` keyword as it doesn't do anything useful. Also, I'm not sure about the usefulness of the return values. Unless otherwise specified, the functions will return the exit status of the last command, so you don't need to `return` anything yourself. – Tom Fenech Sep 09 '16 at 16:39
  • Nice, indeed the `return`s are confusing and make it less readable. Using `function` keywords or not is more a question of personal flavor at least i removed them from the one liners to save some space. thx. – 3ronco Sep 09 '16 at 17:07
  • Don't forget that semicolons are needed after the tests for the one-line versions. – Tom Fenech Sep 09 '16 at 17:09
  • 3
    isNumber will return 'true' on any string that has a number in it. – DrStrangepork Oct 19 '16 at 07:13
  • @DrStrangepork Indeed, my false_values array is missing that case. I will have look into it. Thanks for the hint. – 3ronco Oct 24 '16 at 08:05
  • Tried to create a combined regex expression for all three types. It's possible but this leads to a really complex regex gibberish. A mutex condition looks really simple and readable. – 3ronco Nov 06 '16 at 11:38
4

I use printf as other answers mentioned, if you supply the format string "%f" or "%i" printf will do the checking for you. Easier than reinventing the checks, the syntax is simple and short and printf is ubiquitous. So its a decent choice in my opinion - you can also use the following idea to check for a range of things, its not only useful for checking numbers.

declare  -r CHECK_FLOAT="%f"  
declare  -r CHECK_INTEGER="%i"  

 ## <arg 1> Number - Number to check  
 ## <arg 2> String - Number type to check  
 ## <arg 3> String - Error message  
function check_number() { 
  local NUMBER="${1}" 
  local NUMBER_TYPE="${2}" 
  local ERROR_MESG="${3}"
  local -i PASS=1 
  local -i FAIL=0   
  case "${NUMBER_TYPE}" in 
    "${CHECK_FLOAT}") 
        if ((! $(printf "${CHECK_FLOAT}" "${NUMBER}" &>/dev/random;echo $?))); then 
           echo "${PASS}"
        else 
           echo "${ERROR_MESG}" 1>&2
           echo "${FAIL}"
        fi 
        ;;                 
    "${CHECK_INTEGER}") 
        if ((! $(printf "${CHECK_INTEGER}" "${NUMBER}" &>/dev/random;echo $?))); then 
           echo "${PASS}"
        else 
           echo "${ERROR_MESG}" 1>&2
           echo "${FAIL}"
        fi 
        ;;                 
                     *) 
        echo "Invalid number type format: ${NUMBER_TYPE} to check_number()." 1>&2
        echo "${FAIL}"
        ;;                 
   esac
} 

>$ var=45

>$ (($(check_number $var "${CHECK_INTEGER}" "Error: Found $var - An integer is required."))) && { echo "$var+5" | bc; }

4

I like Alberto Zaccagni's answer.

if [ "$var" -eq "$var" ] 2>/dev/null; then

Important prerequisites: - no subshells spawned - no RE parsers invoked - most shell applications don't use real numbers

But if $var is complex (e.g. an associative array access), and if the number will be a non-negative integer (most use-cases), then this is perhaps more efficient?

if [ "$var" -ge 0 ] 2> /dev/null; then ..
shilovk
  • 11,718
  • 17
  • 75
  • 74
  • This doesn't fail only for complex numbers (those with an imaginary component), but also for floating point numbers (those with a non-integer component). – Charles Duffy May 07 '21 at 22:55
2

To catch negative numbers:

if [[ $1 == ?(-)+([0-9.]) ]]
    then
    echo number
else
    echo not a number
fi
user28490
  • 1,027
  • 8
  • 8
  • Also, this requires extended globbing to be enabled first. This is a Bash-only feature which is disabled by default. – tripleee Aug 02 '17 at 08:38
  • @tripleee extended globbing is activated automatically when using == or != `When the ‘==’ and ‘!=’ operators are used, the string to the right of the operator is considered a pattern and matched according to the rules described below in Pattern Matching, as if the extglob shell option were enabled.` https://www.gnu.org/software/bash/manual/bashref.html#index-_005b_005b – Badr Elmers May 13 '19 at 03:38
  • @BadrElmers Thanks for the update. This seems to be a new behavior which is not true in my Bash 3.2.57 (MacOS Mojave). I see it works as you describe in 4.4. – tripleee May 13 '19 at 05:00
2

You could use "let" too like this :

[ ~]$ var=1
[ ~]$ let $var && echo "It's a number" || echo "It's not a number"
It\'s a number
[ ~]$ var=01
[ ~]$ let $var && echo "It's a number" || echo "It's not a number"
It\'s a number
[ ~]$ var=toto
[ ~]$ let $var && echo "It's a number" || echo "It's not a number"
It\'s not a number
[ ~]$ 

But I prefer use the "=~" Bash 3+ operator like some answers in this thread.

Idriss Neumann
  • 3,760
  • 2
  • 23
  • 32
  • 5
    This is very dangerous. Don't evaluate unvalidated arithmetic in the shell. It must be validated some other way first. – ormaaj Oct 08 '14 at 05:27
  • @ormaaj why is it dangerous? As in malicious numbers, or overflows? Is it dangerous when the input is your own value? – balupton Sep 03 '21 at 08:03
2

Almost as you want in syntax. Just need a function isnumber:

#!/usr/bin/bash

isnumber(){
  num=$1
  if [ -z "${num##*[!0-9]*}" ]; 
    then return 1
  else
    return 0
  fi
}

$(isnumber $1) && VAR=$1 || echo "need a number";
echo "VAR is $VAR"

test:

$ ./isnumtest 10
VAR is 10
$ ./isnumtest abc10
need a number
VAR is 
Daniil Loban
  • 4,165
  • 1
  • 14
  • 20
1
printf '%b' "-123\nABC" | tr '[:space:]' '_' | grep -q '^-\?[[:digit:]]\+$' && echo "Integer." || echo "NOT integer."

Remove the -\? in grep matching pattern if you don't accept negative integer.

  • 3
    Downvote for lack of explanation. How does this work? It looks complex and brittle, and it's not obvious what inputs exactly it will accept. (For example, is removing spaces crucially necessary? Why? It will say a number with embedded spaces is a valid number, which may not be desirable.) – tripleee Aug 02 '17 at 06:51
1

Did the same thing here with a regular expression that test the entire part and decimals part, separated with a dot.

re="^[0-9]*[.]{0,1}[0-9]*$"

if [[ $1 =~ $re ]] 
then
   echo "is numeric"
else
  echo "Naahh, not numeric"
fi
Jerome
  • 103
  • 9
  • Could you explain why your answer is fundamentally different from other old answers, e.g., Charles Duffy's answer? Well, your answer is actually broken since it validates a single period `.` – gniourf_gniourf May 06 '18 at 22:33
  • not sure to understand the single period here... it is one or zero period expected.... But right nothing fundamentally different, just found the regex easier to read. – Jerome May 08 '18 at 21:17
  • also using * should match more real world cases – Jerome May 08 '18 at 21:26
  • 1
    The thing is you're matching the empty string `a=''` and the string that contains a period only `a='.'` so your code is a bit broken... – gniourf_gniourf May 08 '18 at 22:58
1

Easy-to-understand and compatible solution, with test command :

test $myVariable -eq 0 2>/dev/null
if [ $? -le 1 ]; then echo 'ok'; else echo 'KO'; fi

If myVariable = 0, the return code is 0
If myVariable > 0, the return code is 1
If myVariable is not an integer, the return code is 2

fragadass
  • 65
  • 5
0
  • variable to check

    number=12345 or number=-23234 or number=23.167 or number=-345.234

  • check numeric or non-numeric

    echo $number | grep -E '^-?[0-9]*\.?[0-9]*$' > /dev/null

  • decide on further actions based on the exit status of the above

    if [ $? -eq 0 ]; then echo "Numeric"; else echo "Non-Numeric"; fi

Beau Grantham
  • 3,435
  • 5
  • 33
  • 43
Atanu
  • 9
  • 1
0

Following up on David W's answer from Oct '13, if using expr this might be better

test_var=`expr $am_i_numeric \* 0` >/dev/null 2>&1
if [ "$test_var" = "" ]
then
    ......

If numeric, multiplied by 1 gives you the same value, (including negative numbers). Otherwise you get null which you can test for

Paul Roub
  • 36,322
  • 27
  • 84
  • 93
Jon T
  • 41
  • 2
  • 2
  • 6
  • 1
    `expr` is a beast which is hard to tame. I have not tested this solution but I would avoid `expr` in favor of modern shell built-ins unless compatibility back to legacy shells from the early 1980s is an important requirement. – tripleee Aug 02 '17 at 06:46
0

I tried ultrasawblade's recipe as it seemed the most practical to me, and couldn't make it work. In the end i devised another way though, based as others in parameter substitution, this time with regex replacement:

[[ "${var//*([[:digit:]])}" ]]; && echo "$var is not numeric" || echo "$var is numeric"

It removes every :digit: class character in $var and checks if we are left with an empty string, meaning that the original was only numbers.

What i like about this one is its small footprint and flexibility. In this form it only works for non-delimited, base 10 integers, though surely you can use pattern matching to suit it to other needs.

ata
  • 2,045
  • 1
  • 14
  • 19
  • Reading mrucci's solution, it looks almost the same as mine, but using regular string replacement instead of "sed style". Both use the same rules for pattern matching and are, AFAIK, interchangeable solutions. – ata Oct 16 '10 at 22:41
  • `sed` is POSIX, while your solution is `bash`. Both have their uses – v010dya Apr 11 '20 at 04:43
0

I use the following (for integers):

## ##### constants
##
## __TRUE - true (0)
## __FALSE - false (1)
##
typeset -r __TRUE=0
typeset -r __FALSE=1

## --------------------------------------
## isNumber
## check if a value is an integer 
## usage: isNumber testValue 
## returns: ${__TRUE} - testValue is a number else not
##
function isNumber {
  typeset TESTVAR="$(echo "$1" | sed 's/[0-9]*//g' )"
  [ "${TESTVAR}"x = ""x ] && return ${__TRUE} || return ${__FALSE}
}

isNumber $1 
if [ $? -eq ${__TRUE} ] ; then
  print "is a number"
fi
Marnix
  • 294
  • 2
  • 3
0

I found quite a short version:

function isnum()
{
    return `echo "$1" | awk -F"\n" '{print ($0 != $0+0)}'`
}
Matt Fenwick
  • 48,199
  • 22
  • 128
  • 192
mary
  • 9
  • 1
  • um.. doesn't this just return 0 if the string is not a number? Does that means it doesn't work if your string is `"0"`? – naught101 May 30 '12 at 04:43
-1

Quick & Dirty: I know it's not the most elegant way, but I usually just added a zero to it and test the result. like so:

function isInteger {
  [ $(($1+0)) != 0 ] && echo "$1 is a number" || echo "$1 is not a number"
 }

x=1;      isInteger $x
x="1";    isInteger $x
x="joe";  isInteger $x
x=0x16 ;  isInteger $x
x=-32674; isInteger $x   

$(($1+0)) will return 0 or bomb if $1 is NOT an integer. for Example:

function zipIt  { # quick zip - unless the 1st parameter is a number
  ERROR="not a valid number. " 
  if [ $(($1+0)) != 0 ] ; then  # isInteger($1) 
      echo " backing up files changed in the last $1 days."
      OUT="zipIt-$1-day.tgz" 
      find . -mtime -$1 -type f -print0 | xargs -0 tar cvzf $OUT 
      return 1
  fi
    showError $ERROR
}

NOTE: I guess I never thought to check for floats or mixed types that will make the entire script bomb... in my case, I didn't want it go any further. I'm gonna play around with mrucci's solution and Duffy's regex - they seem the most robust within the bash framework...

WWWIZARDS
  • 159
  • 1
  • 3
  • 10
-1

The accepted answer didn't work for me in all cases BASH 4+ so :

# -- is var an integer? --
# trim leading/trailing whitespace, then check for digits return 0 or 1
# Globals: None
# Arguments: string
# Returns: boolean
# --
is_int() {
    str="$(echo -e "${1}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
    case ${str} in ''|*[!0-9]*) return 1 ;; esac
    return 0
}

How to use it ?

Valid (will return 0 = true):

is_int "100" && echo "return 0" || echo "return 1"

Invalid (will return 1 = false) :

is_int "100abc" && echo "returned 0" || echo "returned 1"
is_int ""  && echo "returned 0" || echo "returned 1"
is_int "100 100"  && echo "returned 0" || echo "returned 1"
is_int "      "  && echo "returned 0" || echo "returned 1"
is_int $NOT_SET_VAR  && echo "returned 0" || echo "returned 1"
is_int "3.14"   && echo "returned 0" || echo "returned 1"

Output:

returned 0
returned 1
returned 1
returned 1
returned 1
returned 1
returned 1

note, in Bash, 1 = false, 0 = true. I am simply printing it out where instead something like this would be more likely :

if is_int ${total} ; then
    # perform some action 
fi
Mike Q
  • 6,716
  • 5
  • 55
  • 62
  • 1
    Could I ask you to show a specific case the accepted answer doesn't work for? – Charles Duffy Aug 15 '19 at 23:03
  • ...btw, `echo -e` is going to introduce portability bugs -- on some shells it prints `-e` on output (and those shells where it *doesn't* do so are violating the letter of the [POSIX specification for `echo`](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/echo.html); with the right set of runtime flags set to make it more strictly compliant than usual, this means bash too can print `-e` on output when you run `echo -e`). – Charles Duffy Aug 15 '19 at 23:04
-3

The accepted answer does not work here, I am on MacOS. The following code works:

if [ $(echo "$number" | grep -c '^[0-9]\+$') = 0 ]; then 
    echo "it is a number"
else
    echo "not a number"
fi
neevek
  • 11,760
  • 8
  • 55
  • 73
  • 4
    This has many problems: the most obvious one is that you should compare with `1` and not `0` in the test statement. The other problem is if variable `number` contains newlines: `number=$'42\nthis is not a number, right?'` will be validated. – gniourf_gniourf Feb 15 '15 at 07:52
  • 3
    Eh? The accepted answer does indeed work on MacOS. Please comment on it with details of the issue seen, and (ideally) a reproducer. (I suspect that perhaps it was being run under `/bin/sh` -- either via a `#!/bin/sh` shebang or with a script started with `sh scriptname` -- rather than bash). – Charles Duffy May 24 '16 at 20:17
  • 1
    Using `grep -c` in a command substitution and comparing the result to 0 is a common and extremely convoluted antipattern. See [useless use of `grep`](http://www.iki.fi/era/unix/award.html#grep) and the "pretzel logic" subpage. – tripleee Aug 02 '17 at 07:05
-3

Stack popped a message asked me if I really want to answer after 30+ answers? But of course!!! Use bash new features and here it is: (after the comment I made a change)

function isInt() { ([[ $1 -eq $(( $1 + 0 )) ]] 2>/dev/null && [[ $1 != '' ]] && echo 1) || echo '' }

function isInt() {
   ([[ $1 =~ ^[-+0-9]+$  ]] && [[ $1 -eq $(( $1 + 0 )) ]] 2>/dev/null && [[ $1 != '' ]] && echo 1) || echo ''
}

Supports:

===============out-of-the-box==================
 1. negative integers (true & arithmetic),
 2. positive integers (true & arithmetic),
 3. with quotation (true & arithmetic),
 4. without quotation (true & arithmetic),
 5. all of the above with mixed signs(!!!) (true & arithmetic),
 6. empty string (false & arithmetic),
 7. no value (false & arithmetic),
 8. alphanumeric (false & no arithmetic),
 9. mixed only signs (false & no arithmetic),
================problematic====================
 10. positive/negative floats with 1 decimal (true & NO arithmetic),
 11. positive/negative floats with 2 or more decimals (FALSE & NO arithmetic).

True/false is what you get from the function only when used combined with process substitution like in [[ $( isInt <arg> ) ]] as there is no logical type in bash neither return value of function.

I use capital when the result of the test expression is WRONG whereas, it should be the reverse!

By 'arithmetic' I mean bash can do math like in this expression: $x=$(( $y + 34)).

I use 'arithmetic/no arithmetic' when in mathematical expressions the argument acts as it is expected and 'NO arithmetic' when it misbehaves compared with what it is expected.

As you see, only no 10 and 11 are the problematic ones!

Perfect!

PS: Note that the MOST popular answer fails in case 9!

centurian
  • 1,168
  • 13
  • 25
  • 1
    No, please no!!! this is completely broken! and it is subject to arbitrary code execution. Try it yourself: `isInt 'a[$(ls>&3)]' 3>&1`. You're glad I only used `ls` as a command (which is executed twice). And your code also claims that `a[$(ls>&3)]` is a number. – gniourf_gniourf Sep 25 '16 at 17:27
  • Not to mention that it will claim that any valid variable name (which is unset or expands to a number or empty, or recursively expands to a number or empty) is a number. E.g., `unset a; isInt a`. – gniourf_gniourf Sep 25 '16 at 17:30
  • arbitrary code execution by whom? you take the responsibility to type sudo su but not to execute this or any script? – centurian Sep 27 '16 at 19:43
  • if [[ isInt 'a[$(ls>&3)]' ]]; then ... is safe as it gives ... errors! You mean direct execution isInt 'a[$(ls>&3)]' .... then don't call it direct!! You imply what if an rm was inside ... I would reply 'don't you know what rm does?' I agree, just test it and its OK under conditions! Just like life is! – centurian Sep 27 '16 at 19:55
-5

Below is a Script written by me and used for a script integration with Nagios and it is working properly till now

#!/bin/bash
# Script to test variable is numeric or not
# Shirish Shukla
# Pass arg1 as number
a1=$1
a=$(echo $a1|awk '{if($1 > 0) print $1; else print $1"*-1"}')
b=$(echo "scale=2;$a/$a + 1" | bc -l 2>/dev/null)
if [[ $b > 1 ]]
then
    echo "$1 is Numeric"
else
    echo "$1 is Non Numeric"
fi

EG:

# sh isnumsks.sh   "-22.22"
-22.22 is Numeric

# sh isnumsks.sh   "22.22"
22.22 is Numeric

# sh isnumsks.sh   "shirish22.22"
shirish22.22 is Non  Numeric
Bo Persson
  • 90,663
  • 31
  • 146
  • 203
  • 9
    This is complex and broken. You need double quotes in `echo "$a1"`, otherwise wildcards in the string are expanded and the outcome depends on what files are in the current directory (try `isnumsks.sh "*"`, then try again after creating a file called `42`). You're only looking at the first whitespace-delimited word, so `42 psych` is misclassified as numeric. All kinds of input that are not numeric but valid `bc` syntax will screw this up, e.g. `2012-01-03`. – Gilles 'SO- stop being evil' Jan 03 '12 at 17:07
-5

This is a little rough around the edges but a little more novice friendly.

if [ $number -ge 0 ]
then
echo "Continue with code block"
else
echo "We matched 0 or $number is not a number"
fi

This will cause an error and print "Illegal number:" if $number is not a number but it will not break out of the script. Oddly there is not a test option I could find to just test for an integer. The logic here will match any number that is greater than or equal to 0.

  • 2
    Your test misses 0, not to mention negative numbers. – Gilles 'SO- stop being evil' Jan 03 '12 at 17:14
  • also, this requires `[[ ]]`, instead of `[ ]`, otherwise it fails on non-integer strings. – naught101 May 30 '12 at 04:55
  • 1
    [ "$number" -ge 0 2>/dev/null ] && echo isNumber || echo noNumber – defim Nov 21 '15 at 17:58
  • `-ge` is greater than or equal, so it shouldn't miss 0, but it would miss negative numbers. The above solution also works better, since `[[ ]]` would always result in a number for @defim's answer, but works nicely for `[ ]` since it would generate an error and evaluate to false. – Marcus Dec 07 '19 at 05:04
  • Sorry, forgot the negative above. For all use: ```[ "$number" -eq "$number" 2>/dev/null ] && echo isNumber || echo noNumber``` – defim Dec 09 '19 at 18:01