En_t8
En_t8

Reputation: 8315

Ternary operator (?:) in Bash

Is there a way to do something like this

int a = (b == 5) ? c : d;

using Bash?

Upvotes: 797

Views: 483415

Answers (23)

F. Hauri  - Give Up GitHub
F. Hauri - Give Up GitHub

Reputation: 71007

Two more answers

Here's some ways of thinking about this

1. bash integer variables

In addition to, dutCh, Vladimir and ghostdog74's corrects answers and because this question is regarding integer and tagged :

Is there a way to do something like this

int a = (b == 5) ? c : d;

There is a nice and proper way to work with integers under bash:

declare -i b=' RANDOM % 3 + 4 ' c=100 d=50 a=' b == 5 ? c : d '; echo $b '-->' $a

The output line from this command should by one of:

4 --> 50
5 --> 100
6 --> 50

Of course, declaring integer type of variable is to be done once:

declare -i a b c d
c=100 d=50 b=RANDOM%3+4
a=' b == 5 ? c : d '
echo $a $b
100 5
b=12  a=b==5?c:d
echo $a $b
50 12

Note: This syntax is recommanded. Declaring property of a variable, then assigning a value is better to be done separately.

1.1 Digression: Using a string as a math function:

mathString=' b == 5 ? c : d '
b=5 a=$mathString
echo $a $b
100 5
b=1 a=$mathString 
echo $a $b
50 1

Note: While $mathString do contain spaces, double-quotes are not needed in a=$mathString assignment.

2. Based on parameter expansion and indirection

Following answers from Brad Parks and druid62, here is a way not limited to integer:

c=50 d=100
ar=([5]=c)
read -p 'Enter B: ' b
e=${ar[b]:-d}
echo ${!e}
  • If b==5, then ar[b] is c and indirection do c is 50.
  • Else ar[any value other than 5] is empty, so parameter expansion will default to d, where indirection give 100.

2.1 Same demo using an array instead of an integer

ternArrayDemo(){
    local -a c d e ar
    local b
    c=(foo bar) d=(foo bar baz) e=(empty)
    ar=([5]=c [2]=d)
    b=${ar[$1]:-e}
    b+=[@]      # For array indirection
    printf ' - %s\n' "${!b}"
}
  • In $ar only 2 and 5 are pointing to some variable name.
  • variable e do contain default value.

Then

ternArrayDemo 0
 - empty
ternArrayDemo 2 
 - foo
 - bar
 - baz
ternArrayDemo 4
 - empty
ternArrayDemo 5
 - foo
 - bar
ternArrayDemo 6
 - empty

2.2 Or using associative arrays

ternAssocArrayDemo(){
     local -a c d e
     local -A ar
     local b
     c=( foo bar )  d=( foo bar baz )  e=( empty )
     ar=( [foo]=c[@]  [bar]=d[@]  [baz]=d[*] )
     b=${ar["$1"]:-e[@]}
     printf ' - %s\n' "${!b}"
}

Then

ternAssocArrayDemo hello
 - empty
ternAssocArrayDemo foo
 - foo
 - bar
ternAssocArrayDemo bar
 - foo
 - bar
 - baz
ternAssocArrayDemo baz
 - foo bar baz

2.2b More sophisticated sample:

ternAssocArrayDemo() { 
    local -a _var_c=( Hello\ world. Good\ day! ) _var_d=( foo bar baz ) _var_e=(
        "${_var_d[@]:1}") _var_eMsg=( empty slot )

    local -A "_assocArr=( [foo]=c[@]  [bar]=d[@]  [baz]=d[*]  [fuu]=e[@] )"

    local _bind_var=_var_${_assocArr["$1"]:-eMsg[*]}

    printf ' - %s\n' "${!_bind_var}"
}

Then

for arg in foo bar baz fuu faa; do
    echo $arg
    ternAssocArrayDemo $arg
done
foo
 - Hello world.
 - Good day!
bar
 - foo
 - bar
 - baz
baz
 - foo bar baz
fuu
 - bar
 - baz
faa
 - empty slot

Upvotes: 2

ghostdog74
ghostdog74

Reputation: 342977

The ternary operator ? : is just a short form of if/then/else:

case "$b" in
 5) a=$c ;;
 *) a=$d ;;
esac

Or:

[[ $b = 5 ]] && a="$c" || a="$d"

Upvotes: 810

ivan_pozdeev
ivan_pozdeev

Reputation: 36096

if [[ $b -eq 5 ]]; then a="$c"; else a="$d"; fi

The cond && op1 || op2 expression suggested in other answers has an inherent bug: if op1 has a nonzero exit status, op2 silently becomes the result; the error will also not be caught in -e mode. So, that expression is only safe to use if op1 can never fail (e.g., :, true if a builtin, or variable assignment without any operations that can fail (like division and OS calls)).

Note the "" quotes. They will prevent translation of all whitespace into single spaces.

Double square brackets as opposed to single ones prevent incorrect operation if $b is equal to a test operator (e.g. "-z"; a workaround with [ is [ "x$b" == "xyes" ] and it only works for string comparison); they also lift the requirement for quoting.

Upvotes: 154

Anton Komarov
Anton Komarov

Reputation: 163

What about such approach:

# any your function
function check () {
    echo 'checking...';

    # Change the following to 'true' to emulate a successful execution.
    # Note: You can replace check function with any function you wish.
    # Be aware in linux false and true are funcitons themselves. see 'help false' for instance.
    false; 
}

# double check pattern
check && echo 'update' \
    || check || echo 'create'; 

See how conditional statements works in the RxJs (i.e. filter pipe). Yes, it is code duplication but more functional approach from my point of view.

Upvotes: 1

Brad Parks
Brad Parks

Reputation: 72271

The following seems to work for my use cases:

Examples

$ tern 1 YES NO                                                                             
YES
    
$ tern 0 YES NO                                                                             
NO
    
$ tern 52 YES NO                                                                            
YES
    
$ tern 52 YES NO 52                                                                         
NO

and can be used in a script like so:

RESULT=$(tern 1 YES NO)
echo "The result is $RESULT"

tern

#!/usr/bin/env bash

function show_help()
{
  ME=$(basename "$0")
  IT=$(cat <<EOF

  Returns a ternary result

  usage: BOOLEAN VALUE_IF_TRUE VALUE_IF_FALSE
  
  e.g. 
  
  # YES
  $ME 1 YES NO                                

  # NO
  $ME 0 YES NO

  # NO
  $ME "" YES NO

  # YES
  $ME "STRING THAT ISNT BLANK OR 0" YES NO

  # INFO contains NO
  INFO=\$($ME 0 YES NO)
EOF
)
  echo "$IT"
  echo
  exit
}

if [ "$1" = "help" ] || [ "$1" = '?' ] || [ "$1" = "--help" ] || [ "$1" = "h" ]; then
  show_help
fi
if [ -z "$3" ]
then
  show_help
fi

# Set a default value for what is "false" -> 0
FALSE_VALUE=${4:-0}

function main
{
  if [ "$1" == "$FALSE_VALUE" ] || [ "$1" = '' ]; then
    echo $3
    exit;
  fi;

  echo $2
}

main "$1" "$2" "$3"

Upvotes: 11

Sujay U N
Sujay U N

Reputation: 5350

We can use following three ways in Shell Scripting for ternary operator :

    [ $numVar == numVal ] && resVar="Yop" || resVar="Nop"

Or

    resVar=$([ $numVar == numVal ] && echo "Yop" || echo "Nop")

Or

    (( numVar == numVal ? (resVar=1) : (resVar=0) ))

Update: Extending the answer for string computations with below ready-to-run example. This is making use of second format mentioned above.

$ strVar='abc';resVar=$([[ $strVar == 'abc' ]] && echo "Yop" || echo "Nop");echo $resVar
Yop
$ strVar='aaa';resVar=$([[ $strVar == 'abc' ]] && echo "Yop" || echo "Nop");echo $resVar
Nop

Upvotes: 37

dx_over_dt
dx_over_dt

Reputation: 14328

Some people have already presented some nice alternatives. I wanted to get the syntax as close as possible, so I wrote a function named ?.

This allows for the syntax:

[[ $x -eq 1 ]]; ? ./script1 : ./script2
# or
? '[[ $x -eq 1 ]]' ./script1 : ./script2

In both cases, the : is optional. All arguments that have spaces, the values must be quoted since it runs them with eval.

If the <then> or <else> clauses aren't commands, the function echos the proper value.

./script; ? Success! : "Failure :("

The function

?() {
  local lastRet=$?
  if [[ $1 == --help || $1 == -? ]]; then
    echo $'\e[37;1mUsage:\e[0m
  ? [<condition>] <then> [:] <else>

If \e[37;1m<then>\e[0m and/or \e[37;1m<else>\e[0m are not valid commands, then their values are
printed to stdOut, otherwise they are executed.  If \e[37;1m<condition>\e[0m is not
specified, evaluates the return code ($?) of the previous statement.

\e[37;1mExamples:\e[0m
  myVar=$(? "[[ $x -eq 1 ]] foo bar)
  \e[32;2m# myVar is set to "foo" if x is 1, else it is set to "bar"\e[0m

  ? "[[ $x = *foo* ]] "cat hello.txt" : "cat goodbye.txt"
  \e[32;2m# runs cat on "hello.txt" if x contains the word "foo", else runs cat on
  # "goodbye.txt"\e[0m

  ? "[[ $x -eq 1 ]] "./script1" "./script2"; ? "Succeeded!" "Failed :("
  \e[32;2m# If x = 1, runs script1, else script2.  If the run script succeeds, prints
  # "Succeeded!", else prints "failed".\e[0m'
    return
  elif ! [[ $# -eq 2 || $# -eq 3 || $# -eq 4 && $3 == ':' ]]; then
    1>&2 echo $'\e[37;1m?\e[0m requires 2 to 4 arguments

\e[37;1mUsage\e[0m: ? [<condition>] <then> [:] <else>
Run \e[37;1m? --help\e[0m for more details'
    return 1
  fi

  local cmd

  if [[ $# -eq 2 || $# -eq 3 && $2 == ':' ]]; then
    cmd="[[ $lastRet -eq 0 ]]"
  else
    cmd="$1"
    shift
  fi

  if [[ $2 == ':' ]]; then
    eval "set -- '$1' '$3'"
  fi

  local result=$(eval "$cmd" && echo "$1" || echo "$2")
  if command -v ${result[0]} &> /dev/null; then
    eval "${result[@]}"
  else
    echo "${result[@]}"
  fi
}

Obviously if you want the script to be shorter, you can remove the help text.

EDIT: I was unaware that ? acts as a placeholder character in a file name. Rather than matching any number of characters like *, it matches exactly one character. So, if you have a one-character file in your working directory, bash will try to run the filename as a command. I'm not sure how to get around this. I thought using command "?" ...args might work but, no dice.

Upvotes: 4

jasonleonhard
jasonleonhard

Reputation: 13927

Simplest ternary

brew list | grep -q bat && echo 'yes' || echo 'no'

This example will determine if you used homebrew to install bat or not yet

If true you will see "yes"

If false you will see "no"

I added the -q to suppress the grepped string output here, so you only see "yes" or "no"

Really the pattern you seek is this

doSomethingAndCheckTruth && echo 'yes' || echo 'no'

Tested with bash and zsh

Upvotes: 3

Dan Bray
Dan Bray

Reputation: 7832

The top answer [[ $b = 5 ]] && a="$c" || a="$d" should only be used if you are certain there will be no error after the &&, otherwise it will incorrectly excute the part after the ||.

To solve that problem I wrote a ternary function that behaves as it should and it even uses the ? and : operators:

Edit - new solution

Here is my new solution that does not use $IFS nor ev(a/i)l.

function executeCmds()
{
    declare s s1 s2 i j k
    declare -A cmdParts
    declare pIFS=$IFS
    IFS=$'\n'
    declare results=($(echo "$1" | grep -oP '{ .*? }'))
    IFS=$pIFS
    s="$1"
    for ((i=0; i < ${#results[@]}; i++)); do
        s="${s/${results[$i]}/'\0'}"
        results[$i]="${results[$i]:2:${#results[$i]}-3}"
        results[$i]=$(echo ${results[$i]%%";"*})
    done
    s="$s&&"
    let cmdParts[t]=0
    while :; do
        i=${cmdParts[t]}
        let cmdParts[$i,t]=0
        s1="${s%%"&&"*}||"
        while :; do
            j=${cmdParts[$i,t]}
            let cmdParts[$i,$j,t]=0
            s2="${s1%%"||"*};"
            while :; do
                cmdParts[$i,$j,${cmdParts[$i,$j,t]}]=$(echo ${s2%%";"*})
                s2=${s2#*";"}
                let cmdParts[$i,$j,t]++
                [[ $s2 ]] && continue
                break
            done
            s1=${s1#*"||"}
            let cmdParts[$i,t]++
            [[ $s1 ]] && continue
            break
        done
        let cmdParts[t]++
        s=${s#*"&&"}
        [[ $s ]] && continue
        break
    done
    declare lastError=0
    declare skipNext=false
    for ((i=0; i < ${cmdParts[t]}; i++ )) ; do
        let j=0
        while :; do
            let k=0
            while :; do
                if $skipNext; then
                    skipNext=false
                else
                    if [[ "${cmdParts[$i,$j,$k]}" == "\0" ]]; then
                         executeCmds "${results[0]}" && lastError=0 || lastError=1
                         results=("${results[@]:1}")
                    elif [[ "${cmdParts[$i,$j,$k]:0:1}" == "!" || "${cmdParts[$i,$j,$k]:0:1}" == "-" ]]; then
                        [ ${cmdParts[$i,$j,$k]} ] && lastError=0 || lastError=1
                    else
                        ${cmdParts[$i,$j,$k]}
                        lastError=$?
                    fi
                    if (( k+1 < cmdParts[$i,$j,t] )); then
                        skipNext=false
                    elif (( j+1 < cmdParts[$i,t] )); then
                        (( lastError==0 )) && skipNext=true || skipNext=false
                    elif (( i+1 < cmdParts[t] )); then
                        (( lastError==0 )) && skipNext=false || skipNext=true
                    fi
                fi
                let k++
                [[ $k<${cmdParts[$i,$j,t]} ]] || break
            done
            let j++
            [[ $j<${cmdParts[$i,t]} ]] || break
        done
    done
    return $lastError
}

function t()
{
    declare commands="$@"
    find="$(echo ?)"
    replace='?'
    commands="${commands/$find/$replace}"
    readarray -d '?' -t statement <<< "$commands"
    condition=${statement[0]}
    readarray -d ':' -t statement <<< "${statement[1]}"
    success="${statement[0]}"
    failure="${statement[1]}"
    executeCmds "$condition" || { executeCmds "$failure"; return; }
    executeCmds "$success"
}

executeCmds separates each command individually, apart from the ones that should be skipped due to the && and || operators. It uses [] whenever a command starts with ! or a flag.

There are two ways to pass commands to it:

  1. Pass the individual commands unquoted but be sure to quote ;, &&, and || operators.
t ls / ? ls qqq '||' echo aaa : echo bbb '&&' ls qq
  1. Pass all the commands quoted:
t 'ls /a ? ls qqq || echo aaa : echo bbb && ls qq'

NB I found no way to pass in && and || operators as parameters unquoted, as they are illegal characters for function names and aliases, and I found no way to override bash operators.

Old solution - uses ev(a/i)l

function t()
{
    pIFS=$IFS
    IFS="?"
    read condition success <<< "$@"
    IFS=":"
    read success failure <<< "$success"
    IFS=$pIFS
    eval "$condition" || { eval "$failure" ; return; }
    eval "$success"
}
t ls / ? ls qqq '||' echo aaa : echo bbb '&&' ls qq
t 'ls /a ? ls qqq || echo aaa : echo bbb && ls qq'

Upvotes: 0

Andre Dias
Andre Dias

Reputation: 145

There's also a very similar but simpler syntax for ternary conditionals in bash:

a=$(( b == 5 ? 123 : 321  ))

Upvotes: 12

Stefan Haberl
Stefan Haberl

Reputation: 10559

Here's a general solution, that

  • works with string tests as well
  • feels rather like an expression
  • avoids any subtle side effects when the condition fails

Test with numerical comparison

a=$(if [ "$b" -eq 5 ]; then echo "$c"; else echo "$d"; fi)

Test with String comparison

a=$(if [ "$b" = "5" ]; then echo "$c"; else echo "$d"; fi)

Upvotes: 8

JWL
JWL

Reputation: 14221

If the condition is merely checking if a variable is set, there's even a shorter form:

a=${VAR:-20}

will assign to a the value of VAR if VAR is set, otherwise it will assign it the default value 20 -- this can also be a result of an expression.

This approach is technically called "Parameter Expansion".

Upvotes: 272

Andr&#233; Hillaire
Andr&#233; Hillaire

Reputation: 29

to answer to : int a = (b == 5) ? c : d;

just write:

b=5
c=1
d=2
let a="(b==5)?c:d"

echo $a # 1

b=6;
c=1;
d=2;
let a="(b==5)?c:d"

echo $a # 2

remember that " expression " is equivalent to $(( expression ))

Upvotes: 2

druid62
druid62

Reputation: 129

A string-oriented alternative, that uses an array:

spec=(IGNORE REPLACE)
for p in {13..15}; do
  echo "$p: ${spec[p==14]}";
done

which outputs:

13: IGNORE
14: REPLACE
15: IGNORE

Upvotes: 1

Bruno Bronosky
Bruno Bronosky

Reputation: 70479

This is much like Vladimir's fine answer. If your "ternary" is a case of "if true, string, if false, empty", then you can simply do:

$ c="it was five"
$ b=3
$ a="$([[ $b -eq 5 ]] && echo "$c")"
$ echo $a

$ b=5
$ a="$([[ $b -eq 5 ]] && echo "$c")"
$ echo $a
it was five

Upvotes: 1

Priyesh Patel
Priyesh Patel

Reputation: 74

You can use this if you want similar syntax

a=$(( $((b==5)) ? c : d ))

Upvotes: 4

Sergio Abreu
Sergio Abreu

Reputation: 2909

Here are some options:

1- Use if then else in one line, it is possible.

if [[ "$2" == "raiz" ]] || [[ "$2" == '.' ]]; then pasta=''; else pasta="$2"; fi

2- Write a function like this:

 # Once upon a time, there was an 'iif' function in MS VB ...

function iif(){
  # Echoes $2 if 1,banana,true,etc and $3 if false,null,0,''
  case $1 in ''|false|FALSE|null|NULL|0) echo $3;;*) echo $2;;esac
}

use inside script like this

result=`iif "$expr" 'yes' 'no'`

# or even interpolating:
result=`iif "$expr" "positive" "negative, because $1 is not true"` 

3- Inspired in the case answer, a more flexible and one line use is:

 case "$expr" in ''|false|FALSE|null|NULL|0) echo "no...$expr";;*) echo "yep $expr";;esac

 # Expression can be something like:     
   expr=`expr "$var1" '>' "$var2"`

Upvotes: 2

Jasonovich
Jasonovich

Reputation: 659

Here is another option where you only have to specify the variable you're assigning once, and it doesn't matter whether what your assigning is a string or a number:

VARIABLE=`[ test ] && echo VALUE_A || echo VALUE_B`

Just a thought. :)

Upvotes: 14

Sir Athos
Sir Athos

Reputation: 9877

[ $b == 5 ] && { a=$c; true; } || a=$d

This will avoid executing the part after || by accident when the code between && and || fails.

Upvotes: 44

emu
emu

Reputation: 1683

The let command supports most of the basic operators one would need:

let a=b==5?c:d;

Naturally, this works only for assigning variables; it cannot execute other commands.

Upvotes: 14

wibble
wibble

Reputation: 675

(ping -c1 localhost&>/dev/null) && { echo "true"; } || {  echo "false"; }

Upvotes: 6

dutCh
dutCh

Reputation: 637

(( a = b==5 ? c : d )) # string + numeric

Upvotes: 62

Vladimir
Vladimir

Reputation: 10513

Code:

a=$([ "$b" == 5 ] && echo "$c" || echo "$d")

Upvotes: 635

Related Questions