0

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.

1 Answer 1

1

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
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.