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"