#!/bin/bash # Shell functions for most initialization scripts . /etc/ash_functions # Print or depending on whether $1 is empty. Useful to mask an # optional password parameter. mask_param() { if [ -z "$1" ]; then echo "" else echo "" fi } # Pipe input to this to sink it to the debug log, with a name prefix. # If the input is empty, no output is produced, so actual output is # readily visible in logs. # # For example: # ls /boot/vmlinux* | SINK_LOG "/boot kernels" SINK_LOG() { local name="$1" local line haveblank # If the input doesn't end with a line break, read won't give us the # last (unterminated) line. Add a line break with echo to ensure we # don't lose any input. Buffer up to one blank line so we can avoid # emitting a final (or only) blank line. (cat; echo) | while IFS= read -r line; do [[ -n "$haveblank" ]] && DEBUG "$name: " # Emit buffered blank line if [[ -z "$line" ]]; then haveblank=y else haveblank= LOG "$name: $line" fi done } # Trace a command with DEBUG, then execute it. Trace failed exit status, stdout # and stderr, etc. # # DO_WITH_DEBUG is designed so it can be dropped in to most command invocations # without side effects - it adds visibility without actually affecting the # execution of the script. Exit statuses, stdout, and stderr are traced, but # they are still returned/written to the caller. # # A password parameter can be masked by passing --mask-position N before the # command to execute, the debug trace will just indicate whether the password # was empty or nonempty (which is important when use of a password is optional). # N=0 is the name of the command to be executed, N=1 is its first parameter, # etc. # # DO_WITH_DEBUG() can be added in most places where a command is executed to # add visibility in the debug log. For example: # # [DO_WITH_DEBUG] mount "$BLOCK" "$MOUNTPOINT" # ^-- adding DO_WITH_DEBUG will show the block device, mountpoint, and whether # the mount fails # # [DO_WITH_DEBUG --mask-position 7] tpmr seal "$KEY" "$IDX" "$pcrs" "$pcrf" "$size" "$PASSWORD" # ^-- trace the resulting invocation, but mask the password in the log # # if ! [DO_WITH_DEBUG] umount "$MOUNTPOINT"; then [...] # ^-- it can be used when the exit status is checked, like the condition of `if` # # hotp_token_info="$([DO_WITH_DEBUG] hotp_verification info)" # ^-- output of hotp_verification info becomes visible in debug log while # still being captured by script # # [DO_WITH_DEBUG] umount "$MOUNTPOINT" &>/dev/null || true # ^-- if the command's stdout/stderr/failure are ignored, this still works the # same way with DO_WITH_DEBUG DO_WITH_DEBUG() { local exit_status=0 local cmd_output if [[ "$1" == "--mask-position" ]]; then local mask_position="$2" shift shift local show_args=("$@") show_args[$mask_position]="$(mask_param "${show_args[$mask_position]}")" DEBUG "${show_args[@]}" else DEBUG "$@" fi # Execute the command and capture the exit status. Tee stdout/stderr to # debug sinks, so they're visible but still can be used by the caller # # This is tricky when set -e / set -o pipefail may or may not be in # effect. # - Putting the command in an `if` ensures set -e won't terminate us, # and also does not overwrite $? (like `|| true` would). # - We capture PIPESTATUS[0] whether the command succeeds or fails, # since we don't know whether the pipeline status will be that of the # command or 'tee' (depends on set -o pipefail). if ! "$@" 2> >(tee /dev/stderr | SINK_LOG "$1 stderr") | tee >(SINK_LOG "$1 stdout"); then exit_status="${PIPESTATUS[0]}" else exit_status="${PIPESTATUS[0]}" fi if [[ "$exit_status" -ne 0 ]]; then # Trace unsuccessful exit status, but only at DEBUG because this # may be expected. Include the command name in case the command # also invoked a DO_WITH_DEBUG (it could be a script). DEBUG "$1: exited with status $exit_status" fi # If the command was (probably) not found, trace PATH in case it # prevented the command from being found if [[ "$exit_status" -eq 127 ]]; then DEBUG "$1: PATH=$PATH" fi return "$exit_status" } # Trace the current script and function. TRACE_FUNC() { # Index [1] for BASH_SOURCE and FUNCNAME give us the caller location. # FUNCNAME is 'main' if called from a script outside any function. # BASH_LINENO is offset by 1, it provides the line that the # corresponding FUNCNAME was _called from_, so BASH_LINENO[0] is the # location of the caller. TRACE "${BASH_SOURCE[1]}(${BASH_LINENO[0]}): ${FUNCNAME[1]}" } # Show the entire current call stack in debug output - useful if a catastrophic # error or something very unexpected occurs, like totally invalid parameters. DEBUG_STACK() { local FRAMES FRAMES="${#FUNCNAME[@]}" DEBUG "call stack: ($((FRAMES-1)) frames)" # Don't print DEBUG_STACK itself, start from 1 for i in $(seq 1 "$((FRAMES-1))"); do DEBUG "- $((i-1)) - ${BASH_SOURCE[$i]}(${BASH_LINENO[$((i-1))]}): ${FUNCNAME[$i]}" done } pcrs() { if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then tpm2 pcrread sha256 elif [ "$CONFIG_TPM" = "y" ]; then head -8 /sys/class/tpm/tpm0/pcrs fi } confirm_totp() { TRACE_FUNC prompt="$1" last_half=X unset totp_confirm while true; do # update the TOTP code every thirty seconds date=$(date "+%Y-%m-%d %H:%M:%S") seconds=$(date "+%s") half=$(expr \( $seconds % 60 \) / 30) if [ "$CONFIG_TPM" != "y" ]; then TOTP="NO TPM" elif [ "$half" != "$last_half" ]; then last_half=$half TOTP=$(unseal-totp) || recovery "TOTP code generation failed" fi echo -n "$date $TOTP: " # read the first character, non-blocking read \ -t 1 \ -n 1 \ -s \ -p "$prompt" \ totp_confirm && break # nothing typed, redraw the line echo -ne '\r' done # clean up with a newline echo } reseal_tpm_disk_decryption_key() { TRACE_FUNC #For robustness, exit early if LUKS TPM Disk Unlock Key is prohibited in board configs if [ "$CONFIG_TPM_DISK_UNLOCK_KEY" == "n" ]; then DEBUG "LUKS TPM Disk Unlock Key is prohibited in board configs" return else DEBUG "LUKS TPM Disk Unlock Key is allowed in board configs. Continuing" fi if ! grep -q /boot /proc/mounts; then mount -o ro /boot || recovery "Unable to mount /boot" fi if [ -s /boot/kexec_key_devices.txt ] || [ -s /boot/kexec_key_lvm.txt ]; then warn "LUKS TPM sealed Disk Unlock Key secret needs to be resealed alongside TOTP/HOTP secret" echo "Resealing LUKS TPM Disk Unlock Key to be unsealed by LUKS TPM Disk Unlock Key passphrase" while ! kexec-seal-key /boot; do warn "Recovery Disk Encryption key passphrase/TPM Owner Password may be invalid. Please try again" done warn "LUKS header hash changed under /boot/kexec_luks_hdr_hash.txt" echo "Updating checksums and signing all files under /boot/kexec.sig" while ! update_checksums; do warn "Checksums were not signed. Preceding errors should explain possible causes" done warn "Rebooting in 3 seconds to enable booting default boot option" sleep 3 reboot else DEBUG "No TPM disk decryption key to reseal" fi } # Enable USB storage (if not already enabled), and wait for storage devices to # be detected. If USB storage was already enabled, no wait occurs, this would # have happened already when USB storage was enabled. enable_usb_storage() { TRACE_FUNC if ! lsmod | grep -q usb_storage; then timeout=0 echo "Scanning for USB storage devices..." insmod /lib/modules/usb-storage.ko >/dev/null 2>&1 || die "usb_storage: module load failed" while [[ $(list_usb_storage | wc -l) -eq 0 ]]; do [[ $timeout -ge 8 ]] && break sleep 1 timeout=$(($timeout + 1)) done fi } device_has_partitions() { local DEVICE="$1" # fdisk normally says "doesn't contain a valid partition table" for # devices that lack a partition table - except for FAT32. # # FAT32 devices have a volume boot record that looks enough like an MBR # to satisfy fdisk. In that case, fdisk prints a partition table header # but no partitions. # # This check covers that: [ $(fdisk -l "$b" | wc -l) -eq 5 ] # In both cases the output is 5 lines: 3 about device info, 1 empty line # and the 5th will be the table header or the invalid message. local DISK_DATA=$(fdisk -l "$DEVICE") if echo "$DISK_DATA" | grep -q "doesn't contain a valid partition table" || \ [ "$(echo "$DISK_DATA" | wc -l)" -eq 5 ]; then # No partition table return 1 fi # There is a partition table return 0 } list_usb_storage() { TRACE_FUNC # List all USB storage devices, including partitions unless we received argument stating we want drives only # The output is a list of device names, one per line. if [ "$1" = "disks" ]; then DEBUG "Listing USB storage devices (disks only) since list_usb_storage was called with 'disks' argument" else DEBUG "Listing USB storage devices (including partitions)" fi stat -c %N /sys/block/sd* 2>/dev/null | grep usb | cut -f1 -d ' ' | sed "s/[']//g" | while read b; do # Ignore devices of size 0, such as empty SD card # readers on laptops attached via USB. if [ "$(cat "$b/size")" -gt 0 ]; then DEBUG "USB storage device of size greater then 0: $b" echo "$b" fi done | sed "s|/sys/block|/dev|" | while read b; do # If the device has a partition table, ignore it and # include the partitions instead - even if the kernel # hasn't detected the partitions yet. Such a device is # never usable directly, and this allows the "wait for # disks" loop in mount-usb to correctly wait for the # partitions. if ! device_has_partitions "$b"; then # No partition table, include this device DEBUG "USB storage device without partition table: $b" echo "$b" #Bypass the check for partitions if we want only disks elif [ "$1" = "disks" ]; then # disks only were requested, so we don't list partitions DEBUG "USB storage device with partition table: $b" DEBUG "We asked for disks only, so we don't want to list partitions" echo "$b" else # Has a partition table, include partitions DEBUG "USB storage device with partition table: $b" ls -1 "$b"* | awk 'NR!=1 {print $0}' fi done } # Prompt for a TPM Owner Password if it is not already cached in /tmp/secret/tpm_owner_password. # Sets tpm_owner_password variable reused in flow, and cache file used until recovery shell is accessed. # Tools should optionally accept a TPM password on the command line, since some flows need # it multiple times and only one prompt is ideal. prompt_tpm_owner_password() { TRACE_FUNC if [ -s /tmp/secret/tpm_owner_password ]; then DEBUG "/tmp/secret/tpm_owner_password already cached in file. Reusing" tpm_owner_password=$(cat /tmp/secret/tpm_owner_password) return 0 fi read -s -p "TPM Owner Password: " tpm_owner_password echo # new line after password prompt # Cache the password externally to be reused by who needs it DEBUG "Caching TPM Owner Password to /tmp/secret/tpm_owner_password" mkdir -p /tmp/secret || die "Unable to create /tmp/secret" echo -n "$tpm_owner_password" >/tmp/secret/tpm_owner_password || die "Unable to cache TPM owner_password under /tmp/secret/tpm_owner_password" } # Prompt for a new TPM Owner Password when resetting the TPM. # Returned in tpm_owner_passpword and cached under /tpm/secret/tpm_owner_password # The password must be 1-32 characters and must be entered twice, # the script will loop until this is met. prompt_new_owner_password() { TRACE_FUNC local tpm_owner_password2 tpm_owner_password=1 tpm_owner_password2=2 while [ "$tpm_owner_password" != "$tpm_owner_password2" ] || [ "${#tpm_owner_password}" -gt 32 ] || [ -z "$tpm_owner_password" ]; do read -s -p "New TPM Owner Password (2 words suggested, 1-32 characters max): " tpm_owner_password echo read -s -p "Repeat chosen TPM Owner Password: " tpm_owner_password2 echo if [ "$tpm_owner_password" != "$tpm_owner_password2" ]; then echo "Passphrases entered do not match. Try again!" echo fi done # Cache the password externally to be reused by who needs it DEBUG "Caching TPM Owner Password to /tmp/secret/tpm_owner_password" mkdir -p /tmp/secret || die "Unable to create /tmp/secret" echo -n "$tpm_owner_password" >/tmp/secret/tpm_owner_password || die "Unable to cache TPM password under /tmp/secret" } check_tpm_counter() { TRACE_FUNC LABEL=${2:-3135106223} tpm_password="$3" # if the /boot.hashes file already exists, read the TPM counter ID # from it. if [ -r "$1" ]; then TPM_COUNTER=$(grep counter- "$1" | cut -d- -f2) else warn "$1 does not exist; creating new TPM counter" tpmr counter_create \ -pwdc '' \ -la $LABEL | tee /tmp/counter || die "Unable to create TPM counter" TPM_COUNTER=$(cut -d: -f1 ${CONFIG_FILE}.tmp awk "gsub(\"^${CONFIG_OPTION}=.*\",\"${CONFIG_OPTION}=\\\"${NEW_SETTING}\\\"\")" /tmp/config >>${CONFIG_FILE}.tmp # then copy any remaining settings from the existing config file, minus the option you changed grep -v "^export ${CONFIG_OPTION}=" ${CONFIG_FILE} | grep -v "^${CONFIG_OPTION}=" >>${CONFIG_FILE}.tmp || true sort ${CONFIG_FILE}.tmp | uniq >${CONFIG_FILE} rm -f ${CONFIG_FILE}.tmp } # Generate a secret for TPM-less HOTP by reading the ROM. Output is the # sha256sum of the ROM (binary, not printable), which can be truncated to the # supported secret length. secret_from_rom_hash() { local ROM_IMAGE="/tmp/coreboot-notpm.rom" echo -e "\nTPM not detected; measuring ROM directly\n" 1>&2 # Read the ROM if we haven't read it yet if [ ! -f "${ROM_IMAGE}" ]; then flash.sh -r "${ROM_IMAGE}" >/dev/null 2>&1 || return 1 fi sha256sum "${ROM_IMAGE}" | cut -f1 -d ' ' | fromhex_plain } update_checksums() { TRACE_FUNC # ensure /boot mounted if ! grep -q /boot /proc/mounts; then mount -o ro /boot || recovery "Unable to mount /boot" fi # remount RW mount -o rw,remount /boot # sign and auto-roll config counter extparam= if [ "$CONFIG_TPM" = "y" ]; then if [ "$CONFIG_IGNORE_ROLLBACK" != "y" ]; then extparam=-r fi fi if ! kexec-sign-config -p /boot -u $extparam; then rv=1 else rv=0 fi # switch back to ro mode mount -o ro,remount /boot return $rv } print_tree() { TRACE_FUNC find ./ ! -path './kexec*' -print0 | sort -z } # Escape zero-delimited standard input to safely display it to the user in e.g. # `whiptail`, `less`, `echo`, `cat`. Doesn't produce shell-escaped output. # Most printable characters are passed verbatim (exception: \). # These escapes are used to replace their corresponding characters: #n#r#t#v#b # Other characters are rendered as hexadecimal escapes. # escape_zero [prefix] [escape character] # prefix: \0 in the input will result in \n[prefix] # escape character: character to use for escapes (default: #); \ may be interpreted by `whiptail` escape_zero() { local prefix="$1" local echar="${2:-#}" local todo="" local echar_hex="$(echo -n "$echar" | xxd -p -c1)" [ ${#echar_hex} -eq 2 ] || die "Invalid escape character $echar passed to escape_zero(). Programming error?!" echo -e -n "$prefix" xxd -p -c1 | tr -d '\n' | { while IFS= read -r -n2 -d ''; do if [ -n "$todo" ]; then #REPLY == " " is EOF [[ "$REPLY" == " " ]] && echo '' || echo -e -n "$todo" todo="" fi case "$REPLY" in 00) todo="\n$prefix" ;; 08) echo -n "${echar}b" ;; 09) echo -n "${echar}t" ;; 0a) echo -n "${echar}n" ;; 0b) echo -n "${echar}v" ;; 0d) echo -n "${echar}r" ;; "$echar_hex") echo -n "$echar$echar" ;; #interpreted characters: 2[0-9a-f] | 3[0-9a-f] | 4[0-9a-f] | 5[0-9abd-f] | 6[0-9a-f] | 7[0-9a-e]) echo -e -n '\x'"$REPLY" ;; # All others are escaped *) echo -n "${echar}x$REPLY" ;; esac done } } # Currently heads doesn't support signing file names with certain characters # due to https://bugs.busybox.net/show_bug.cgi?id=14226. Also, certain characters # may be intepreted by `whiptail`, `less` et al (e.g. \n, \b, ...). assert_signable() { TRACE_FUNC # ensure /boot mounted if ! grep -q /boot /proc/mounts; then mount -o ro /boot || die "Unable to mount /boot" fi find /boot -print0 >/tmp/signable.ref local del='\001-\037\134\177-\377' LC_ALL=C tr -d "$del" /tmp/signable.del || die "Failed to execute tr." if ! cmp -s "/tmp/signable.ref" "/tmp/signable.del" &>/dev/null; then local user_out="/tmp/hash_output_mismatches" local add="Please investigate!" [ -f "$user_out" ] && add="Please investigate the following relative paths to /boot (where # are sanitized invalid characters):"$'\n'"$(cat "$user_out")" recovery "Some /boot file names contain characters that are currently not supported by heads: $del"$'\n'"$add" fi rm -f /tmp/signable.* } verify_checksums() { TRACE_FUNC local boot_dir="$1" local gui="${2:-y}" ( set +e -o pipefail local ret=0 cd "$boot_dir" || ret=1 sha256sum -c "$TMP_HASH_FILE" >/tmp/hash_output || ret=1 # also make sure that the file & directory structure didn't change # (sha256sum won't detect added files) print_tree >/tmp/tree_output || ret=1 if ! cmp -s "$TMP_TREE_FILE" /tmp/tree_output &>/dev/null; then ret=1 [[ "$gui" != "y" ]] && exit "$ret" # produce a diff that can safely be presented to the user # this is relatively hard as file names may e.g. contain backslashes etc., # which are interpreted by whiptail, less, ... escape_zero "(new) " <"$TMP_TREE_FILE" >"${TMP_TREE_FILE}.user" escape_zero "(new) " /tmp/tree_output.user diff "${TMP_TREE_FILE}.user" /tmp/tree_output.user | grep -E '^\+\(new\).*$' | sed -r 's/^\+\(new\)/(new)/g' >>/tmp/hash_output rm -f "${TMP_TREE_FILE}.user" rm -f /tmp/tree_output.user fi exit $ret ) return $? } # Check if a device is an LVM2 PV, and if so print the VG name find_lvm_vg_name() { TRACE_FUNC local DEVICE VG DEVICE="$1" mkdir -p /tmp/root-hashes-gui if ! lvm pvs "$DEVICE" >/tmp/root-hashes-gui/lvm_vg 2>/dev/null; then # It's not an LVM PV return 1 fi VG="$(tail -n +2 /tmp/root-hashes-gui/lvm_vg | awk '{print $2}')" if [ -z "$VG" ]; then DEBUG "Could not find LVM2 VG from lvm pvs output:" DEBUG "$(cat /tmp/root-hashes-gui/lvm_vg)" return 1 fi echo "$VG" } # If a block device is a partition, check if it is a bios-grub partition on a # GPT-partitioned disk. is_gpt_bios_grub() { TRACE_FUNC local PART_DEV="$1" DEVICE NUMBER # Figure out the partitioned device containing this device (if there is # one) from /sys/class/block. local DEVICE_MATCHES=("/sys/class/block/"*"/$(basename "$PART_DEV")") DEVICE="$(echo "${DEVICE_MATCHES[0]}" | cut -d/ -f5)" if [ "${#DEVICE_MATCHES[@]}" -ne 1 ] || [ "$DEVICE" = "*" ]; then return 0 fi # Extract the partition number if ! [[ $(basename "$PART_DEV") =~ ([0-9]+)$ ]]; then return 0 # Can't figure out the partition number fi NUMBER="${BASH_REMATCH[1]}" # Now we know the device and partition number, get the type. This is # specific to GPT disks, MBR disks are shown differently by fdisk. TRACE "$PART_DEV is partition $NUMBER of $DEVICE" if [ "$(fdisk -l "/dev/$DEVICE" | awk '$1 == '"$NUMBER"' {print $5}')" == grub ]; then return 0 fi return 1 } # Test if a block device could be used as /boot - we can mount it and it # contains /boot/grub* files. (Here, the block device could be a partition or # an unpartitioned device.) # # If the device is a partition, its type is also checked. Some common types # that we definitely can't mount this way are excluded to silence spurious exFAT # errors. # # Any existing /boot is unmounted. If the device is a reasonable boot device, # it's left mounted on /boot. mount_possible_boot_device() { TRACE_FUNC local BOOT_DEV="$1" local PARTITION_TYPE # Unmount anything on /boot. Ignore failure since there might not be # anything. If there is something mounted and we cannot unmount it for # some reason, mount will fail, which is handled. umount /boot 2>/dev/null || true # Skip bios-grub partitions on GPT disks, LUKS partitions, and LVM PVs, # we can't mount these as /boot. if is_gpt_bios_grub "$BOOT_DEV" || cryptsetup isLuks "$BOOT_DEV" || find_lvm_vg_name "$BOOT_DEV" >/dev/null; then TRACE "$BOOT_DEV is not a mountable partition for /boot" return 1 fi # Get the size of BOOT_DEV in 512-byte sectors sectors=$(blockdev --getsz "$BOOT_DEV") # Check if the partition is small (less than 2MB, which is 4096 sectors) if [ "$sectors" -lt 4096 ]; then TRACE_FUNC DEBUG "Partition $BOOT_DEV is very small, likely BIOS boot. Skipping mount." return 1 else TRACE_FUNC DEBUG "Try mounting $BOOT_DEV as /boot" if mount -o ro "$BOOT_DEV" /boot >/dev/null 2>&1; then if ls -d /boot/grub* >/dev/null 2>&1; then # This device is a reasonable boot device return 0 fi umount /boot || true fi fi return 1 } # detect and set /boot device # mount /boot if successful detect_boot_device() { TRACE_FUNC local devname # unmount /boot to be safe cd / && umount /boot 2>/dev/null # check $CONFIG_BOOT_DEV if set/valid if [ -e "$CONFIG_BOOT_DEV" ] && mount_possible_boot_device "$CONFIG_BOOT_DEV"; then # CONFIG_BOOT_DEV is valid device and contains an installed OS return 0 fi # generate list of possible boot devices fdisk -l | grep "Disk /dev/" | cut -f2 -d " " | cut -f1 -d ":" >/tmp/disklist # Check each possible boot device for i in $(cat /tmp/disklist); do # If the device has partitions, check the partitions instead if device_has_partitions "$i"; then devname="$(basename "$i")" partitions=("/sys/class/block/$devname/$devname"?*) else partitions=("$i") # Use the device itself fi for partition in "${partitions[@]}"; do partition_dev=/dev/"$(basename "$partition")" # No sense trying something we already tried above if [ "$partition_dev" = "$CONFIG_BOOT_DEV" ]; then continue fi # If this is a reasonable boot device, select it and finish if mount_possible_boot_device "$partition_dev"; then CONFIG_BOOT_DEV="$partition_dev" return 0 fi done done # no valid boot device found echo "Unable to locate /boot files on any mounted disk" return 1 } scan_boot_options() { TRACE_FUNC local bootdir config option_file bootdir="$1" config="$2" option_file="$3" if [ -r $option_file ]; then rm $option_file; fi for i in $(find $bootdir -name "$config"); do DO_WITH_DEBUG kexec-parse-boot "$bootdir" "$i" >>$option_file done # FC29/30+ may use BLS format grub config files # https://fedoraproject.org/wiki/Changes/BootLoaderSpecByDefault # only parse these if $option_file is still empty if [ ! -s $option_file ] && [ -d "$bootdir/loader/entries" ]; then for i in $(find $bootdir -name "$config"); do kexec-parse-bls "$bootdir" "$i" "$bootdir/loader/entries" >>$option_file done fi } calc() { awk "BEGIN { print "$*" }" } # truncate a file to a size only if it is longer (busybox truncate lacks '<' and # always sets the file size) truncate_max_bytes() { local bytes="$1" local file="$2" if [ "$(stat -c %s "$file")" -gt "$bytes" ]; then truncate -s "$bytes" "$file" fi } # Busybox xxd -p pads the last line with spaces to 60 columns, which not only # trips up many scripts, it's very difficult to diagnose by looking at the # output. Delete line breaks and spaces to really get plain hex output. tohex_plain() { xxd -p | tr -d '\n ' } # Busybox xxd -p -r silently truncates lines longer than 60 hex chars. # Shorter lines are OK, spaces are OK, and even splitting a byte across lines is # allowed, so just fold the text to maximum 60 column lines. # Note that also unlike GNU xxd, non-hex chars in input corrupt the output (GNU # xxd ignores them). fromhex_plain() { fold -w 60 | xxd -p -r } print_battery_health() { if [ -d /sys/class/power_supply/BAT* ]; then battery_health=$(calc $(cat /sys/class/power_supply/BAT*/charge_full)/$(cat /sys/class/power_supply/BAT*/charge_full_design)*100 | awk -F "." {'print $1'}) echo "$battery_health" fi } print_battery_charge() { if [ -d /sys/class/power_supply/BAT* ]; then battery_charge=$(calc $(cat /sys/class/power_supply/BAT*/charge_now)/$(cat /sys/class/power_supply/BAT*/charge_full)*100 | awk -F "." {'print $1'}) echo "$battery_charge" fi } generate_random_mac_address() { #Borrowed from https://stackoverflow.com/questions/42660218/bash-generate-random-mac-address-unicast hexdump -n 6 -ve '1/1 "%.2x "' /dev/urandom | awk -v a="2,6,a,e" -v r="$RANDOM" 'BEGIN{srand(r);}NR==1{split(a,b,",");r=int(rand()*4+1);printf "%s%s:%s:%s:%s:%s:%s\n",substr($1,0,1),b[r],$2,$3,$4,$5,$6}' } # Add a command to be invoked at exit. (Note that trap EXIT replaces any # existing handler.) Commands are invoked in reverse order, so they can be used # to clean up resources, etc. # The parameters are all executed as-is and do _not_ require additional quoting # (unlike trap). E.g.: # at_exit shred "$file" #<-- file is expanded when calling at_exit, no extra quoting needed at_exit() { AT_EXIT_HANDLERS+=("$@") # Command and args AT_EXIT_HANDLERS+=("$#") # Number of elements in this command } # Array of all exit handler command arguments with lengths of each command at # the end. For example: # at_exit echo hello # at_exit echo a b c # results in: # AT_EXIT_HANDLERS=(echo hello 2 echo a b c 4) AT_EXIT_HANDLERS=() # Each handler is an array AT_EXIT_HANDLER_{i} run_at_exit_handlers() { local cmd_pos cmd_len cmd_pos="${#AT_EXIT_HANDLERS[@]}" # Silence trace if there are no handlers, this is common and occurs a lot [ "$cmd_pos" -gt 0 ] && DEBUG "Running at_exit handlers" while [ "$cmd_pos" -gt 0 ]; do cmd_pos="$((cmd_pos - 1))" cmd_len="${AT_EXIT_HANDLERS[$cmd_pos]}" cmd_pos="$((cmd_pos - cmd_len))" "${AT_EXIT_HANDLERS[@]:$cmd_pos:$cmd_len}" done } trap run_at_exit_handlers EXIT