I need to write a POSIX shell script that will change system configurations. Before doing so I want to ensure there are backups of any file I edit.
A requirement for this script is that is uses dmenu
to prompt the user if installed and read
if not.
I want one function (named communicate
below) that will automatically handle this for me based on a variable that gets set on run, $dmenu.
I'm having issues writing to a variable inside a variable, as shown below:
#!/usr/bin/env sh
[ $(command -v dmenu 2>/dev/null) ] && dmenu='true'
communicate(){
description="$1"; options="$2"; outcome="$3"
if [ $dmenu ]; then
echo "$(printf "$options" | dmenu -i -p "$description")" >&0 | read $outcome
else
printf "$description $options "; read $outcome
fi
}
backup(){
[ $1 ] && file="$1" || communicate 'Enter file: ' '' 'file'
[ ! -f $file ] && backup "$1"
cp "$file" "$file.bak"
}
select_interface(){
[ $1 ] && interface="$1" || communicate 'Select interface:' "$interfaces" 'interface'
}
backup
wants to save user input to a variable called $file, whereas later select_interface
wants to save to a variable called $interface.
if dmenu
is not installed, writing to $outcome works fine with the else statement, whereas if it is installed, I cannot seem to get the read
command to trigger when passing the outcome of dmenu
through with the STDIN redirect into read, which works outside of the script.
Can someone see what I'm doing wrong or how I could do this better? I need it all to be in the one function communicate, acting as the communicating agent with the user.
The statement
echo "$(printf "$options" | dmenu -i -p "$description")" >&0 | read $outcome
being a pipe, causes the shell to implement echo
and read
as 2 separate processes. read
is still a forked shell, and it still sets the variable $outcome
, but it only sets it in the forked shell, not in the forking (parent) shell.
The technically correct way to do it is:
eval $outcome=\$\(printf "$options" \| dmenu -i -p "$description"\)'
BUT I would advise against eval for anything but throwaway code.
I also advise against functions which accept variable names to set, it's pretty hard to get right.
The cleaner way to do it:
#!/usr/bin/env sh
if [ $(command -v dmenu 2>/dev/null) ]; then
communicate() {
description="$1"
options="$2"
# also fixed this bug with the menu selection, each option needs to be in a new line
printf "%s\n" $options | dmenu -i -p "${description}:"
}
else
communicate() {
description="$1"
options="$2"
if [ -n "$options" ]; then
optstring="options: ${options}; "
else
optstring=""
fi
read -p "${optstring}${description}: " outcome
echo $outcome
}
fi
backup() {
if [ -n "$1" ]; then
file="$1"
else
file=$(communicate 'Enter file')
fi
if [ -f "$file" ]; then
cp "$file" "${file}.bak"
else
backup
fi
}
select_interface() {
if [ -n "$1" ]; then
interface="$1"
else
interface=$(communicate "Enter interface" "$interfaces")
fi
}