1
\$\begingroup\$

Today, I gave myself a small task of detecting the total amount of RAM installed on a hardware from a Linux system using a POSIX shell script. I ended up using dmidecode utility and a rather complex looking code for such a simple task.

Though I found countless simple pieces of code on various places, they are all doing not just that exactly.

I don't want free RAM or total available RAM to the kernel.

I want the pure number of MiBs, which are installed in the hardware (memory sticks).

Reasons are multiple, to name one, for example, there could be less available RAM to the system due to various things, like integrated graphics shared memory, etc.

Performance is not an issue. I know it is slow. I just want to know if there is a way to perhaps simplify the code.


#!/bin/sh

is_number()
{
    [ "${1}" -eq "${1}" ] 2> /dev/null
}

ram_entries=$(sudo dmidecode --type 17 | grep Size: | awk '{ print $2 }')
ram_entries_count=$(( $(printf '%s\n' "${ram_entries}" | wc -l) - 1 ))

i=0
ram_entry=x
total_ram_size_mb=0

while [ "${i}" -le "${ram_entries_count}" ]
do
    is_number "${ram_entry}" && total_ram_size_mb=$(( total_ram_size_mb + ram_entry ))
    i=$(( i + 1 ))
    ram_entry=$(echo "${ram_entries}" | sed --posix --quiet "${i}{p;q}")
done

if [ "${total_ram_size_mb}" -ge 1024 ]
then
    echo "Total RAM installed in Gigabytes =" $(( total_ram_size_mb / 1024 ))
else
    echo "Total RAM installed in Megabytes = ${total_ram_size_mb}"
fi
\$\endgroup\$
1
\$\begingroup\$

Using sudo can be problematic within scripts. It's really designed to be used interactively, and may request a password (which won't work very well if its standard input isn't a terminal). The alternative is to require the script to be run as a suitably privileged user, and I can see that you want to minimise the amount of privileged code.

I couldn't initially find an unprivileged way to do what dmidecode - it inspects /sys/firmware/dmi/tables/DMI which is unreadable by ordinary users and I don't see the same information anywhere else in sysfs. However, lshw can produce a total RAM summary without privilege (but can't see the individual DIMMs). After a bit of tracing, it seems that it reads from /sys/devices/system/memory - multiply the contents of block_size_bytes (in hexadecimal) by the total of */online to get the total RAM size in bytes:

printf '%c' /sys/devices/system/memory/memory*/online | wc -c |
    dc -e '16i' -f /sys/devices/system/memory/block_size_bytes -e 'Ai ? *p'

If coreutils is available (almost all Linux systems, I think; certainly as likely as sed or awk and much more so than dmidecode), then we can use the numfmt utility to convert that number to a friendly human form:

numfmt --to=iec-i --format='%.1f%f' --suffix B

(see the man page to understand the options, and adjust to taste).


Full working alternative

#!/bin/sh

printf 'Total RAM installed = '
printf '%c' /sys/devices/system/memory/memory*/online | wc -c |
    dc -e '?16i' -f /sys/devices/system/memory/block_size_bytes -e '*p' |
    numfmt --to=iec-i --suffix B 

As a bonus, this is much faster than using dmidecode (which seems to gather all its information even when printing only a small subset).


Other points:

  • I recommend setting -e and -u in all shell scripts (both are specified in POSIX):

    set -eu
    
  • Don't pipe through grep and awk - since the latter does regexp matching, just add a pattern to the awk program:

    dmidecode --type 17 | awk '/^[[:space:]]*Size: [0-9]/ { print $2 }'
    

    Including a digit in the pattern eliminates the empty slots neatly at that stage, so obviating is_number.

  • Counting using sprintf|wc is a little safer if you use %c or %.1s conversion; that uses just the first letter, so you can count using wc -c and avoid double-counting strings with embedded newlines (for example, see my worked code). You won't get the problem lines here, but it's a technique worth knowing.

  • Instead of iterating through the indices of $ram_entries and repeatedly invoking sed to extract individual lines, it's better to check that it's safe to expand without quotes (we know this to be true, since we've ensured it contains only decimal numbers), and then simply for ram_entry in $ram_entries.

    Even better, instead of capturing and assigning to a variable, we could pipe into a while read loop to process each line. Be careful to make the loop be in the main shell, not a sub-shell, though, or make it communicate the result out.

\$\endgroup\$

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.