0

I'm trying to compare the state of more than two variables. The use-case is a script that, among other options, should select (or auto-select) only one of several "modes" based on available executables. A partial syntax diagram is:

 [ [--fzf,-f]|[--rofi,-r]|[--dmenu,-d] ]

The variables are defined based on presence of command arguments, so, for example, if --fzf or -f are found on the command line arguments, an associated fzf variable is set to "1" indicating that the user wants to run the script in, for lack of better terms, fzf mode.

The && operator statement below addresses what happens if none of the command arguments is found on the command-line. Basically, if no "mode" was selected, then the script will auto-select one, and only one, mode to use in a hierarchical fashion.

The || operator statement below is supposed to address what happens if a mode is selected (by the user), but the underlying executable is not found.

Here is the original version for the && operator:

if [[ $fzf = 0 && $rofi = 0 ]]; then
        if command_exists fzf; then
            fzf=1
        elif command_exists rofi; then
            rofi=1
        fi
    fi

to

if [[ $fzf = 0 && $rofi = 0 && $dmenu = 0 ]]; then
        if command_exists fzf; then
            fzf=1
        elif command_exists rofi; then
            rofi=1
        elif command_exists dmenu; then
            dmenu=1
        fi
    fi

And lastly, here the original for the || operator:

if [[ $rofi = 1 || $fzf = 0 ]]; then
        command_exists rofi || die "Could not find rofi in \$PATH"
        menu="$rofi_cmd"
elif [[ $fzf = 1 || $rofi = 0 ]]; then
        command_exists fzf || die "Could not find fzf in \$PATH"
        menu="$fzf_cmd"
else
        die "Could not find either fzf or rofi in \$PATH"
fi

to

if [[ $rofi = 1 || $fzf = 0 || $dmenu = 0 ]]; then
        command_exists rofi || die "Could not find rofi in \$PATH"
        menu="$rofi_cmd"
    elif [[ $fzf = 1 || $rofi = 0 || $dmenu = 0 ]]; then
        command_exists fzf || die "Could not find fzf in \$PATH"
        menu="$fzf_cmd"
    elif [[ $dmenu = 1 || $rofi = 0 || $fzf = 0 ]]; then
        command_exists dmenu || die "Could not find dmenu in \$PATH"
        menu="$dmenu_cmd"
    else
        die "Could not find either fzf or rofi or dmenu in \$PATH"
    fi

Which doesn't seem to throw any error, but i suspect that this is neither the correct way to do this, and probably doesn't work as expected (as it seems to report wrong values when using -x and seeing it's output).

I'm aware of post such as this one, but i didn't (yet) found examples with more than two variables (like what I tried to do above).

The above original part were taken from this script. I'm basically trying to add support for dmenu since it only support rofi and fzf (as showed above).

Here the full modified script. It need password-store as dependencies.

I'm using Bash 5.0.3.

How do i use the && and || operators with more than two variables correctly?

muru
  • 72,889
  • 3
    What is die supposed to be? If you're thinking of something like Perl's die(), that doesn't exist in bash as far as I know. You can use exit instead. – terdon Sep 23 '20 at 18:23
  • 1
    Also, please [edit] your question and explain what logic you are trying to implement here. We can't really help you get it right if we don't know what you are trying to do. – terdon Sep 23 '20 at 18:25
  • I should have linked the original script, my bad. Will do so real quick. @terdon – Nordine Lotfi Sep 23 '20 at 18:25
  • 1
    For arithmetic comparisons better use Other Comparison Operators – schrodingerscatcuriosity Sep 23 '20 at 18:25
  • done! @terdon added short explanation and link to original script. – Nordine Lotfi Sep 23 '20 at 18:28
  • prefer not to honestly. In this case, i want to stay as close as possible to how the original script look like. @schrodigerscatcuriosity unless this isn't possible of course :) – Nordine Lotfi Sep 23 '20 at 18:29
  • 1
    Why do you think it doesn't work? What shell are you using? The || and && do work with more than two values on BASH: $ if [[ 1 = 1 || 2 = 3 || 4 = 5 ]]; then echo works; fi outputs "works". $ if [[ 1 = 1 && 2 = 2 && 5 = 5 ]]; then echo works; fi outputs "works", but this does not: $ if [[ 1 = 1 && 2 = 2 && 4 = 5 ]]; then echo works; fi I think some other problem is being misinterpreted. – kbulgrien Sep 23 '20 at 18:37
  • that's interesting. I just doubted it doesn't work since it reported wrong value in my script (the modified one i linked in my post, where i used -x to see the value it reported). As for the shell I'm using, i mentioned it as i used the tag bash but i guess I'll add it in my post too. @kbulgrien – Nordine Lotfi Sep 23 '20 at 18:44
  • The example I gave works even if some of those numbers are changed to variables like: one = 1; two=2; four=$4; $ if [[ $one = 1 && $two = 2 && $four = 5 ]]; then echo works; fi The question needs to add more information if someone is supposed to be able to help figure out what you are missing. I.e. Show us a snippet of the debug with -x or other things you have done to troubleshoot. Regardless, the title of the question isn't a good fit. Please add more detail to get better help. Version of BASH conceptually could be relevant too. – kbulgrien Sep 23 '20 at 18:57
  • added version of bash in my post since a couple minutes (before you answered too!) think you missed it :) @kbulgrien As for what you said before that, i don't mind posting my own modified version of the linked script, but i fear it might be too long for my post...do you think it'll be fine to post as it is, or post it on an external link? (pastebin/github). – Nordine Lotfi Sep 23 '20 at 18:59
  • 1
    What happens if you just change all "die" to "echo" (for sake of helping you see what's going on)? – kbulgrien Sep 23 '20 at 19:16
  • that's actually an interesting idea! Will try it real quick :) @kbulgrien – Nordine Lotfi Sep 23 '20 at 19:17
  • 2
    Your question is hard to follow. Please proof read – ctrl-alt-delor Sep 23 '20 at 19:38
  • yeah, it is! @ctrl-alt-delor I'm wondering how to make the question better suited for the post i made...Since the initial problem is (at least think so) from the fact the operators logic i demonstrated doesn't give the right values (see the script i linked, and the modified version too). All I'm trying to do is figure out the correct way if there one for handling this particular operators usage. – Nordine Lotfi Sep 23 '20 at 19:46
  • 3
    I'm answering your comment here, @NordineLotfi so as not to spam poor Glenn (my bad, I should have commented here in the first place). Yes, you said you are adding support for dmenu. What we need to know is what you are expecting from all these if statements. What do you think they are doing? You say they give "wrong values" but we have no idea what the "right" value would be. For instance, if $dmenu is 0, your current version will always default to menu="$rofi_cmd", no matter what value $fzf or $rofi have. Is that what you want? it could be, we have no way of knowing. – terdon Sep 23 '20 at 20:02

3 Answers3

5

If the user is only supposed to choose one of many, I'd just use one variable to hold the choice, dropping the per-option variables. We don't really care about all the combinations anyway, we just want one choice of out of three, and another for "unset/default".

So:

#!/bin/sh
cmd=             # empty value for default
case "$1" in
    --rofi|-r)  cmd=rofi ;;
    --fzf|-f)   cmd=fzf ;;
    --dmenu|-d) cmd=dmenu ;;
esac

if [ -z "$cmd" ]; then echo "no command set, looking for default" if command_exists fzf; then cmd=fzf elif ... fi fi

echo "running with command $cmd"

actually do something

Here, the chosen mode is stored in cmd. I didn't actually even give the user the possibility to give two commands, only the first argument is read for the flag. Now, if you use getopt or getopts, that doesn't really fly, but we could check if the mode has already been set before setting it (again).

#!/bin/sh
error_if_cmd_set() {
    if [ -n "$1" ]; then
        echo "command already set"
        exit 1
    fi
}
cmd=
for arg in "$@"; do
    case "$1" in
        --rofi|-r)  error_if_cmd_set "$cmd"; cmd=rofi ;;
        --fzf|-f)   error_if_cmd_set "$cmd"; cmd=fzf ;;
        --dmenu|-d) error_if_cmd_set "$cmd"; cmd=dmenu ;;
    esac
done
# ...
echo "using command $cmd"
ilkkachu
  • 138,973
4

I honestly don't know what you're trying to do with those checks. If you need to check that at least one of those programs is available:

programs=(fzf rofi dmenu)
available=()

for prog in "${programs[@]}"; do location=$(type -P $prog) && available+=("$location") done

(( ${#available[@]} == 0 )) && die "none of ${programs[*] are available"

what is your logic for choosing one if multiple are available?

menu=${available[0]}


Based on your comments, it'll be cleaner to push more work into the case branches:

    fzf_exists=0
    rofi_exists=0
    dmenu_exists=0
# reorder these if you want: the _last_ one to succeed is the default
if command_exists dmenu; then
    dmenu_exists=1
    menu_default=$dmenu_cmd
fi
if command_exists rofi; then 
    rofi_exists=1
    menu_default=$rofi_cmd
fi
if command_exists fzf; then 
    fzf_exists=1
    menu_default=$fzf_cmd
fi

if [[ -z $menu_default ]]; then
    die "none of fzf, rofi, dmenu are available"
fi

while true; do 
    case "$1" in
        -f|--fzf) 
            (( fzf_exists )) || die "fzf is not available"
            [[ -n $menu ]]   && die "choose only one of fzf/rofi/dmenu"
            menu=$fzf_cmd
            shift 
            ;;
        -r|--rofi)
            (( rofi_exists )) || die "rofi is not available"
            [[ -n $menu ]]    && die "choose only one of fzf/rofi/dmenu"
            menu=$rofi_cmd
            shift 
            ;;
        -d|--dmenu)
            (( dmenu_exists )) || die "dmenu is not available"
            [[ -n $menu ]]     && die "choose only one of fzf/rofi/dmenu"
            menu=$dmenu_cmd
            shift 
            ;;
        # ...
    esac
done

: ${menu:=$menu_default}

Note that the command_exists function should return 1 not exit 1

glenn jackman
  • 85,964
3

The || and && operators are working fine. There are problems in the way that the script is translated, but, despite repeated requests for logic clarification, none has been offered. I have therefore added to the question based on how I understand the copied script is working.

One interesting thing is that a bit of the script not posted in the question has been translated incorrectly:

if [[ $fzf = 1 && $rofi = 1 && $dmenu = 1 ]]; then
    die 'Either --fzf,-f or --rofi,-r or --dmenu,-d must be given, not more than one'
fi

The intent, best as can be told after a quick review, is that the statement should be something more along the lines of:

if [[ $(( $fzf + $rofi + $dmenu )) > 1 ]]; then
    die 'Either --fzf,-f or --rofi,-r or --dmenu,-d must be given, not more than one'
fi

There could be other issues. It is VERY important that you provide all relevant information.

kbulgrien
  • 830