Reputation: 447
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
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
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
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