9

The script is:

#!/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]}";

The older system doesn't have mapfile or readarray so I need to convert that line to some alternative that can read each line of the lsblk output into an array.

The line in question that creates the array is:

mapfile -t drives < <(lsblk --nodeps -o name,serial,size | grep "sd");
codeforester
  • 39,467
  • 16
  • 112
  • 140
Joshua Jarman
  • 427
  • 1
  • 6
  • 10
  • Is this possible to use **older command scripts** such as the answer accepted below on **latest version of Bash**? – Bando Sep 13 '21 at 13:33

3 Answers3

15

You can loop over your input and append to the array:

$ while IFS= read -r line; do arr+=("$line"); done < <(printf '%d\n' {0..5})
$ declare -p arr
declare -a arr='([0]="0" [1]="1" [2]="2" [3]="3" [4]="4" [5]="5")'

Or, for your specific case:

while IFS= read -r line; do
    drives+=("$line")
done < <(lsblk --nodeps -o name,serial,size | grep "sd")

See the BashFAQ/001 for an excellent explanation why IFS= read -r is a good idea: it makes sure that whitespace is conserved and backslash sequences not interpreted.

Benjamin W.
  • 46,058
  • 19
  • 106
  • 116
2

Here's the solution I came up with a while back. This is better because it provides a substitute function for older versions of Bash that don't support mapfile/readarray.

if ! type -t readarray >/dev/null; then
  # Very minimal readarray implementation using read. Does NOT work with lines that contain double-quotes due to eval()
  readarray() {
    local cmd opt t v=MAPFILE
    while [ -n "$1" ]; do
      case "$1" in
      -h|--help) echo "minimal substitute readarray for older bash"; exit; ;;
      -r) shift; opt="$opt -r"; ;;
      -t) shift; t=1; ;;
      -u) 
          shift; 
          if [ -n "$1" ]; then
            opt="$opt -u $1"; 
            shift
          fi
          ;;
      *)
          if [[ "$1" =~ ^[A-Za-z_]+$ ]]; then
            v="$1"
            shift
          else
            echo -en "${C_BOLD}${C_RED}Error: ${C_RESET}Unknown option: '$1'\n" 1>&2
            exit
          fi
          ;;
      esac
    done
    cmd="read $opt"
    eval "$v=()"
    while IFS= eval "$cmd line"; do      
      line=$(echo "$line" | sed -e "s#\([\"\`]\)#\\\\\1#g" )
      eval "${v}+=(\"$line\")"
    done
  }
fi

You don't have to change your code one bit. It just works!

readarray -t services -u < <(lsblk --nodeps -o name,serial,size | grep "sd")
parleer
  • 1,220
  • 3
  • 12
  • 22
  • 1
    Hey do you have this up on a github gist somewheres? If so, point me at it and consider it followed ! I haven't tested it yet, but if I get a chance to, I'll vote accordingly -- Looks good in general tho! – David Farrell Jan 18 '21 at 00:57
0

For those playing along at home, this one aims to provide a mapfile that's feature-compliant with Bash 5, but still runs as far back as Bash 3.x:

#!/usr/bin/env bash

if ! (enable | grep -q 'enable mapfile'); then
  function mapfile() {
    local    DELIM="${DELIM-$'\n'}";     opt_d() {    DELIM="$1"; }
    local    COUNT="${COUNT-"0"}";       opt_n() {    COUNT="$1"; }
    local   ORIGIN="${ORIGIN-"0"}";      opt_O() {   ORIGIN="$1"; }
    local     SKIP="${SKIP-"0"}";        opt_s() {     SKIP="$1"; }
    local    STRIP="${STRIP-"0"}";       opt_t() {    STRIP=1;    }
    local  FROM_FD="${FROM_FD-"0"}";     opt_u() {  FROM_FD="$1"; }
    local CALLBACK="${CALLBACK-}";       opt_C() { CALLBACK="$1"; }
    local  QUANTUM="${QUANTUM-"5000"}";  opt_c() {  QUANTUM="$1"; }

    unset OPTIND; local extra_args=()
    while getopts ":d:n:O:s:tu:C:c:" opt; do
      case "$opt" in
        :)  echo "${FUNCNAME[0]}: option '-$OPTARG' requires an argument" >&2; exit 1 ;;
       \?)  echo "${FUNCNAME[0]}: ignoring unknown argument '-$OPTARG'" >&2 ;;
        ?)  "opt_${opt}" "$OPTARG" ;;
      esac
    done

    shift "$((OPTIND - 1))"; set -- ${extra_args[@]+"${extra_args[@]}"} "$@"

    local var="${1:-MAPFILE}"

    ### Bash 3.x doesn't have `declare -g` for "global" scope...
    eval "$(printf "%q" "$var")=()" 2>/dev/null || { echo "${FUNCNAME[0]}: '$var': not a valid identifier" >&2; exit 1; }

    local __skip="${SKIP:-0}" __counter="${ORIGIN:-0}"  __count="${COUNT:-0}"  __read="0"

    ### `while read; do...` has trouble when there's no final newline,
    ### and we use `$REPLY` rather than providing a variable to preserve
    ### leading/trailing whitespace...
    while true; do
      if read -d "$DELIM" -r <&"$FROM_FD"
         then [[ ${STRIP:-0} -ge 1 ]] || REPLY="$REPLY$DELIM"
         elif [[ -z $REPLY ]]; then break
      fi

      (( __skip-- <= 0 )) || continue
      ((  COUNT <= 0 || __count-- > 0 )) || break

      ### Yes, eval'ing untrusted content is insecure, but `mapfile` allows it...
      if [[ -n $CALLBACK ]] && (( QUANTUM > 0 && ++__read % QUANTUM == 0 ))
         then eval "$CALLBACK $__counter $(printf "%q" "$REPLY")"; fi

      ### Bash 3.x doesn't allow `printf -v foo[0]`...
      ### and `read -r foo[0]` mucks with whitespace
      eval "${var}[$((__counter++))]=$(printf "%q" "$REPLY")"
    done
  }

  ### Alias `readarray` as well...
  readarray() { mapfile "$@"; }
fi

if [[ -z ${PS1+YES} ]]; then
   echo "'mapfile' should only be called as a shell function; try \"source ${BASH_SOURCE[0]##*/}\" first..." >&2
   exit 1
fi
DabeDotCom
  • 121
  • 6