Joshua Jarman
Joshua Jarman

Reputation: 447

bash script: use command output to dynamically create menu and arrays?

I'm trying to create a script to run a command and take that output and use it to create a menu dynamically. I also need to access parts of each output line for specific values.

I am using the command:

lsblk --nodeps -no name,serial,size | grep "sd"

output:

sda   600XXXXXXXXXXXXXXXXXXXXXXXXXX872 512G
sdb   600XXXXXXXXXXXXXXXXXXXXXXXXXXf34 127G

I need to create a menu that looks like:

Available Drives:
1) sda   600XXXXXXXXXXXXXXXXXXXXXXXXXX872 512G
2) sdb   600XXXXXXXXXXXXXXXXXXXXXXXXXXf34 127G
Please select a drive: 

(note: there can be any number of drives, this menu would be constructed dynamically from the available drives array)

When the user selects the menu number I need to be able to access the drive id (sdb) and drive serial number (600XXXXXXXXXXXXXXXXXXXXXXXXXXf34) for the selected drive.

Any assistance would be greatly appreciated. Please let me know if any clarification is needed.

Upvotes: 7

Views: 5492

Answers (3)

mklement0
mklement0

Reputation: 438103

#!/usr/bin/env bash

# Read command output line by line into array ${lines [@]}
# Bash 3.x: use the following instead:
#   IFS=$'\n' read -d '' -ra lines < <(lsblk --nodeps -no name,serial,size | grep "sd")
readarray -t lines < <(lsblk --nodeps -no name,serial,size | grep "sd")

# Prompt the user to select one of the lines.
echo "Please select a drive:"
select choice in "${lines[@]}"; do
  [[ -n $choice ]] || { echo "Invalid choice. Please try again." >&2; continue; }
  break # valid choice was made; exit prompt.
done

# Split the chosen line into ID and serial number.
read -r id sn unused <<<"$choice"

echo "id: [$id]; s/n: [$sn]"

As for what you tried: using an unquoted command substitution ($(...)) inside an array constructor (( ... )) makes the tokens in the command's output subject to word splitting and globbing, which means that, by default, each whitespace-separated token becomes its own array element, and may expand to matching filenames.

Filling arrays in this manner is fragile, and even though you can fix that by setting IFS and turning off globbing (set -f), the better approach is to use readarray -t (Bash v4+) or IFS=$'\n' read -d '' -ra (Bash v3.x) with a process substitution to fill an array with the (unmodified) lines output by a command.

Upvotes: 10

Joshua Jarman
Joshua Jarman

Reputation: 447

I managed to untangle the issue in an elegant way:

#!/bin/bash

# Dynamic Menu Function
createmenu () {
    select selected_option; do # in "$@" is the default
        if [ 1 -le "$REPLY" ] && [ "$REPLY" -le $(($#)) ]; then
            break;
        else
            echo "Please make a vaild selection (1-$#)."
        fi
    done
}

declare -a drives=();
# Load Menu by Line of Returned Command
mapfile -t drives < <(lsblk --nodeps -o name,serial,size | grep "sd");
# Display Menu and Prompt for Input
echo "Available Drives (Please select one):";
createmenu "${drives[@]}"
# Split Selected Option into Array and Display
drive=($(echo "${selected_option}"));
echo "Drive Id: ${drive[0]}";
echo "Serial Number: ${drive[1]}";

Upvotes: 0

jas-
jas-

Reputation: 1801

How about something like the following

#!/bin/bash

# define an array
declare -a obj

# capture the current IFS
cIFS=$IFS

# change IFS to something else
IFS=:

# assign & format output from lsblk 
obj=( $(lsblk --nodeps --no name,serial,size) )

# generate a menu system
select item from ${obj[@]}; do
  if  [ -n ${item} ]; then
    echo "Invalid selection"
    continue
  else
    selection=${item}
    break
  fi
done

# reset the IFS
IFS=${cIFS}

That should be a bit more portable with less dependencies such as readarray which isn't available on some systems

Upvotes: -1

Related Questions