How do I write a bash script that reads a list from a text file and takes user interaction for each item?

I'm writing a script to perform common tasks I like to do with a fresh install of Linux. It has functions for each phase from updating the system to installing common software.

Right now I'm trying to have the script read a list of software from a text file, ask the user if they would like to install it. If they say "yes" it would run apt to install that software. The current draft of the software has echo statements with the commands to avoid making changes while I test the script. Here is the function I'm trying to set up.

InstallAptSW () {
    file="./apps/apt-apps"

    while read -r line; do
        read -p "Would you like to install $line? [Y/n]" yn
        yn=${yn:-Y}
        case $yn in
            [Yy]* ) echo "sudo apt install -y $line";;
            [Nn]* ) printf "\nSkipping";
                    break;;
                * ) echo 'Please answer yes or no.';;
        esac
    done < $file
}

The apps file is just a list of software such as

code
gparted
snapd
neofetch
etc...

Here is the current result of the function:

$USER@$HOSTNAME:~/Documents/popOS-post-install$ ./PopOS-Post-Install.sh 
Please answer yes or no.
Please answer yes or no.
Please answer yes or no.
Please answer yes or no.
Please answer yes or no.
Please answer yes or no.
Please answer yes or no.

Goodbye


Solution 1:

Because the input for the while; do...done < "$file" code block is handled from the file containing the software names to install; the read -rp "Would you like to install $line? [Y/n]" yn which was not given a specific input handler, just inherits the file input from its outer code block.

Rather than reading user input, it reads (consumes) lines from the software list file.

There need to be distinct input handlers for reading file and reading user input.

Edited with suggestion from John Kugelman

Here it is with a couple other fixes:

#!/usr/bin/env bash

InstallAptSW() {
  file='./apps/apt-apps'

  # Reads from File Handler 3 which gets input from "$file"
  while read -r line <&3; do
    # printf before read -r avoid using the read -p bashism
    printf 'Would you like to install %s [Y/n]? ' "$line"

    # Default file handler is not used by "$file",
    # so it takes user input without interference
    read -r yn
    yn=${yn:-Y}
    case $yn in
      [Yy]*) echo sudo apt install -y "$line" ;;
      [Nn]*)
        # Single-quotes are better
        # when no expansion occurs within string
        printf '\nSkipping'
        break
        ;;
      *) echo 'Please answer yes or no.' ;;
    esac
  # Double quote the "$file" variable to prevent
  # word splitting and globing pattern matching
  # Get "$file" input at the File-Handler 3
  done 3< "$file"
}

InstallAptSW

Solution 2:

I would use a new file descriptor for reading the file; that way you'll be able to get the user input inside the loop:

InstallAptSW () {
    local file="./apps/apt-apps"
    local fd appname yn

    while IFS='' read -r appname <&$fd
    do
        while true
        do
            read -N 1 -p "Would you like to install '$appname'? [Y/n] " yn
            [[ $yn ]] && echo
            yn=${yn:-Y}
            case $yn in
                [Yy]) echo "sudo apt install -y $(printf %q "$appname")"
                      # sudo apt install -y "$appname"
                      break
                      ;;
                [Nn]) echo "skip"
                      break
                      ;;
            esac
        done
    done {fd}< "$file"
}

edit: applied first @JohnKugelman suggestion in the comments.

If your bash version doesn't support file descriptors stored in variables then you will have to hard-code it, i.e. using a number greater or equal to 3 (I don't know the upper limit, 999 should be safe):

    while IFS='' read -r appname <&3
    do
        # ...
    done 3< "$file"