14

How can I let my script determine the largest number for itself?

I looked through my environment variables, and I found these two that looked promising:

~# declare -p BASH_VERSINFO HOSTTYPE
declare -ar BASH_VERSINFO=([0]="5" [1]="0" [2]="11" [3]="1" [4]="release" [5]="x86_64-slackware-linux-gnu")
declare -- HOSTTYPE="x86_64"

...but could I really trust parsing those, in order to draw a conclusion about what the largest number in Bash arithmetic would be? There must be a better way, programmatically. Any suggestions?

7
  • @Kamil Maciorowski Thanks. But the accepted answer there is telling me how to play around with values like (2^31)-1 etc in order to figure it out interactively for myself... I was more like thinking better leave it to the script. – patilan Apr 3 at 4:33
  • 1
    @patilan: Please take a look at: What should I do when someone answers my question? – Cyrus Apr 3 at 7:24
  • @Cyrus You mean, upvote it? (Did you?) It's a cool answer. I refrained from accepting it just yet, because someone might come up with a brighter idea -- without a loop -- maybe like taking -1 and just masking the most signifficant bit somehow. – patilan Apr 3 at 7:45
  • 1
    @patilan, pretty much. Hence, you get to fall back to the loop. Though I guess for most cases you could assume it's either 32- or 64-bit signed numbers and just test those two. Except... well, ksh uses floating point. Not sure if it automatically switches between integers and floats, and with what rules. But it breaks the loop looking for an overflow. – ilkkachu Apr 3 at 9:50
19

Bash arithmetic uses signed numbers.

So the quick answer would be:

((MAX=(1<<63)-1))

But since you want your script to not know about the bitness of the system it's running on, then let's keep going.

Brute force would be, keep adding 1 in a loop, until you hit the point where it will overflow unto a negative number. But that could take years! :-) A quicker and more elegant way to do it is with a simple bit-shift.

Let's find the sign bit, i.e., let's find the number that has 1 in the most signifficant bit, and zeros in all the other bits, however many they may be. Once we have that number, we'll simply subtract 1 from it, and we'll get the largest signed number.

# MIN -- the smallest signed number 0x8000...00  (it equals MAX+1)
# MAX -- the largest signed number  0x7Fff...FF  <-- what we are looking for

MIN=1; until (( (MIN<<=1) < 0 )) ;do :;done
((MAX=MIN-1))

echo $MAX

Result:
9223372036854775807

Or, here's a one-liner, without a loop. We put the hex representation of a number in a variable, and then mask the sign bit through the variable expantion when passing it to the printf builtin:

printf -v MAX %x -1 && printf -v MAX %d 0x${MAX/f/7}

echo $MAX

Result:
9223372036854775807

On a machine with a different bitness than mine, the result will be a different number.

And just for illustration, in my case:

printf "MAX %X  %d\nMIN %X %d\n" $MAX $MAX $MIN $MIN
MAX 7FFFFFFFFFFFFFFF  9223372036854775807
MIN 8000000000000000 -9223372036854775808

A little side note about MIN: You may want to constrain yourself to using ((MIN=-MAX)), otherwise you will occasionally run into problems with some arithmetic operations.

((MIN=-MAX)) ; printf "MIN %X %d\n" $MIN $MIN
MIN 8000000000000001 -9223372036854775807
3
  • It would take days? A 64-bit count would take decades! Assuming 1 count per clock cycle (e.g. for a compiled language like C on a normal modern CPU), counting up to 2^31 -1 takes about half a second at 4.2GHz, but counting up to 2^63 - 1 would take 69.6 years. Maybe 35 years if you unroll the loop to do 2-per-clock increment / break-if-negative checks :P (Bash arithmetic has vastly more overhead than 1/clock, so a day might be possible for 32-bit.) – Peter Cordes Apr 4 at 5:10
  • 1
    Interestingly, on my 32-bit Bash I get the same result from both methods. Apparently, it uses long long for arithmetic. – Ruslan Apr 4 at 13:23
  • "take years" part reminds me of this. – Vorac Apr 8 at 5:18
5

You can obtain the maximum number in a two-steps text-manipulation operation.

TL; DR: in bash all you need to do is:

printf -v ff %x -1
printf -v max %d "0x${ff/#?/7}"

while the minimum can be obtained with the following additional operation:

# this uses the shell's own arithmetic engine
min="$((max+1))"

To answer an OP's comment, note that bash implements printf as a builtin command, and by default it favors its builtin, hence it does not try to invoke the external printf command that is commonly available as a standalone executable under $PATH.

Also, the bash's own builtin printf used as above does not spawn any additional process. It is instead run in the shell's current execution environment, therein creating the variables $ff and $max as specified with the convenient -v option.


The same as above but in a POSIX-compliant syntax would be:

ff="$(printf %x -1)"
max="$(printf %d "0x7${ff#?}")"

# to obtain the minimum number in shells that only
# support integer numbers you can just do like
# said for `bash`
min="$((max+1))"

# else for shells defaulting to floating-point numbers
# (such as ksh93) you might instead do one text-manipulation
# operation on top of the arithmetic addition
min="$(printf -- -%u "$((max+1))")"
# of course such result would only apply to the shell's
# integer capacities, not to its floating-point capacities

Note that using the POSIX-compliant syntax above may spawn one (ephemeral) process for each of those Command Substitutions, and this is dependent on whether the particular shell optimizes simple printfs like those or not. However, even if the shell does spawn a process for each such printf, it does so by simply forking itself, hence it still relies on its own arithmetic capacities, not the ones of other arbitrary shells.

This is assuming that the shell implements printf as a builtin, like bash and many other shells do. A shell not implementing printf as builtin would use the external printf command available in $PATH (if any) which would yield valid results as far as the external printf command, the OS's C libraries, and the CPU's own capacities are concerned, but might not necessarily match the shell's own arithmetic capacity, such as is the case for mksh which uses 32-bit arithmetic even on 64-bit machines. Notably mksh might not implement printf as builtin or might anyway still prefer the external command, and ash from busybox might not implement printf as builtin if it hasn't been compiled in as such.

3
  • 1
    #LL3 I am afraid that if I shell out to an external printf, or if I spawn a whole different shell altogether, then I am not going to get the correct number for Bash arithmetic. – patilan Apr 3 at 12:00
  • 1
    @patilan Indeed. You are probably commenting on the "Posixly" part. Else, if you want a correct answer to your question, which explicitly targets Bash, you have no reason to not use Bash builtins. All in all, this answer is quite good and deserves your upvote. – xhienne Apr 3 at 12:13
  • @patilan Not really. See my edit with expanded explanation (hopefully clear now). – LL3 Apr 3 at 15:50
2

I am sitting at a 64-bit machine, but how can I let my script determine that for itself?

Note that that's a separate question from what the largest numbers in bash arithmetic are. You can't use this to figure out your machine's bitness, and the bitness of your machine doesn't determine Bash integer size.

Bash numbers are always 64-bit, even on a 32-bit machine. (Or possibly wider, on an exotic machine where long long is wider than the ISO C required minimum of 64-bit. It's maybe even possible that they could be one's complement or sign/magnitude (in which case you'd have
min = -max, instead of 2's complement -max - 1), if Bash is portable to C implementations that don't use 2's complement. According to commenters, Bash uses long long and unsigned long long internally. So that's something to keep in mind when designing run-time test methods.)


Just out of curiosity, I tested this on an old 32-bit Debian system kicking around with Bash 3.2.39. LL3's method shows that printf %x -1 prints 16 fs (so 8 bytes, 64 bits).

And I tested bash math with incrementing INT64_MAX wrapping to INT64_MIN

$ uname -a
Linux <non-updated kernel version hidden to protect the guilty> i686 GNU/Linux

$ echo $((9223372036854775807 + 1))
-9223372036854775808

So clearly Bash's source code uses int64_t or long long (or hopefully uint64_t to avoid C undefined behaviour when wrapping, unless they build with gcc -fwrapv to define that behaviour), not a type like long that's 32-bit on 32-bit machines.

Doing 64-bit addition on a 32-bit machine just makes the compiler use 2 instructions, like add + adc (add-with-carry), on machines that have a carry flag, or a 3rd instruction on machines like MIPS that don't. So it's totally normal for languages to provide 64-bit types, and for higher-level languages that only have one type to use a nice wide type.

11
  • No, that's not the question! The point was to let the script figure it out. The script does not know or care how things are done under the hood. – patilan Apr 4 at 5:36
  • @patilan: The point of my answer is that figuring it out won't tell you the bitness of your machine, which seemed to be the motivation or premise for your question. I'm not trying to answer the other part of the question, about how to figure it out programmatically. I'm just showing that that if you used those methods on a 32-bit build of bash, you'd get the same answer. Maybe that should have been a comment, IDK. (If all you really wanted to know was the max number bash math can use, then that's fine, but there's a reason I quoted the part of the question that I'm answering.) – Peter Cordes Apr 4 at 5:39
  • 1
    @PeterCordes. Bash source uses long long and unsigned long long – fpmurphy Apr 4 at 6:05
  • @patilan: Oh, I think I misunderstood your phrasing. You were guessing that a 64-bit machine would imply 64-bit bash math. Not that you actually wanted to detect your machine's bitness. So yes, that's a valid question. But it's still interesting to know that the answer doesn't depend on the machine (except on exotic machines where long long is wider than 64-bit.) – Peter Cordes Apr 4 at 6:54
  • 1
    @ilkkachu ELF32-on-kernel64 is distinct from x32, which is based on ELF64. But both are nowadays mostly a historical curiosity: few distros (if any) support any of these setups officially (Debian does have an x32 port, but it's just a chroot kludge). – Ruslan Apr 4 at 15:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.