ChatGPT解决这个技术问题 Extra ChatGPT

Ternary operator (?:) in Bash

Is there a way to do something like this

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

using Bash?

@dutCh's answer shows that bash does have something similar to the "ternary operator" however in bash this is called the "conditional operator" expr?expr:expr (see man bash goto section "Arithmetic Evaluation"). Keep in mind the bash "conditional operator" is tricky and has some gotchas.
Bash does have a ternary operator for integers and it works inside the arithmetic expression ((...)). See Shell Arithmetic.
Just as @codeforester mentioned, ternary operator works with arithmetic expansion $(( )) and arithmethic evaluation (( )). See also https://mywiki.wooledge.org/ArithmeticExpression.

X
Xiong Chiamiov

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

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

Or

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

Note that the = operator tests for string equality, not numeric equality (i.e. [[ 05 = 5 ]] is false). If you want numeric comparison, use -eq instead.
It's more of a short form for if/then/else
It's a genius way to utilize the short-circuit behavior to get a ternary operator effect :) :) :)
why the [[ and ]] ? it works just as well like this : [ $b = 5 ] && a="$c" || a="$d"
The cond && op1 || op2 construct has an inherent bug: if op1 has nonzero exit status for whatever reason, the result will silently become op2. if cond; then op1; else op2; fi is one line too and doesn't have that defect.
V
Vladimir

Code:

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

this is better than the others... the point about the tertiary operator is that it's an operator, hence it's proper context is in an expression, hence it must return a value.
This is the most concise way. Be aware that if the part with echo "$c" is an aliased, multi-lined command (like echo 1; echo 2), you should enclose it in parentheses.
This will also capture any output of the tested command, too (yes, in this particular case, we "know" it doesn't produce any).
This chain of operators only behaves like a ternary operator if you are positive that the command after && won't have a non-zero exit status. Otherwise, a && b || c will "unexpectedly" run c if a succeeds but b fails.
w
wjandrea

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".


In the case of passing a string parameter with hyphens in it, I had to use quote marks: a=${1:-'my-hyphenated-text'}
link for the lazy - there are additional operators than just substitute (:-)
@JustinWrobel - unfortunately no syntax like ${VAR:-yes:-no}.
Not what the OP wanted, but just wanted to say thanks for teaching me something new today that fit my scenario perfectly ^_^
@KenWilliams Actually there is a syntax like that, it uses a second type of parameter expansion, :+. You can do ${${VAR:+yes}:-no}... if VAR is set & non-null, this will expand to yes and if VAR is unset or null it expands to no.
i
ivan_pozdeev
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. The first pair will prevent a syntax error if $b is blank or has whitespace. Others will prevent translation of all whitespace into single spaces.


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

This is good for numeric comparisons and assignments, but it will give unpredictable results if you use it for string comparisons and assignments.... (( )) treats any/all strings as 0
This can also be written: a=$(( b==5 ? c : d ))
d
devnull
[ $b == 5 ] && { a=$c; true; } || a=$d

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


This will still not catch the error in -e mode: (set -o errexit; [ 5 == 5 ] && { false; true; echo "success"; } || echo "failure"; echo $?; echo "further on";) -> success 0 further on
Use the : bulit-in instead of true to save exec-ing an external program.
@ivan_pozdeev Is there any way to use && .. || and still catch a failed command in between them?
@TomHale No. && and || by definition apply to the entire command before them. So, if you have two commands before it (regardless of how they are combined), you cannot apply it to only to one of them and not the other. You can emulate if/then/else logic with flag variables, but why bother if there's if/then/else proper?
P
Pavan Kumar

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

This is actually (the first and second specifically, of these three examples) the most appropriate answer to this question IMHO.
Each of the suggested ways has limitations/caveats which are not explained. So using this answer is dangerous.
J
Jasonovich

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. :)


A major downside: it will also capture the stdout of [ test ]. So the construct is only safe to use if you "know" that the command doesn't output anything to stdout.
Also the same as stackoverflow.com/questions/3953645/ternary-operator-in-bash/… plus the need to quote and escape quotes inside. The space-tolerant version will look like VARIABLE="`[ test ] && echo \"VALUE_A\" || echo \"VALUE_B\"`" .
e
emu

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.


it is exactly equivalent to (( ... )), so it is only valid for arithmetic expressions
A
Andre Dias

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

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

Valid only for "Arithmetic Expansion" yes, i.e. not strings or any test results.
That's right, this syntax is unable to compare non-arithmetical values.
B
Brad Parks

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"

Very explanatory. Very complete. Very verbose. Three things I like. ;-)
is tern part of Bash? Mac doesn't seem to have it
hey @太極者無極而生 - that's the name of the bash script - save that "tern" section above to a file named "tern", then run chmod 700 tern in the same folder. Now you'll have a tern command in your terminal
w
wjandrea

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)

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

This solution has the same defect ivan_pozdeev describes above where for this solution, echo "false" may occur when ping is successful, but where, for some reason, however unlikely, the echo "true" part returns a non-zero exit status (see ShellCheck SC2015). To echo "false" only when ping fails, regardless of echo "true"’s exit status, move the first grouping brace { to the head: { (ping -c1 localhost&>/dev/null) && echo "true"; } || { echo "false"; }.
P
Priyesh Patel

You can use this if you want similar syntax

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

This works only with integers. You can write it more simply as a=$(((b==5) ? : c : d)) - $((...)) is needed only when we want to assign the result of the arithmetic to another variable.
j
jasonleonhard

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


See this comment. If the && fails, then the || will be run, which is not quite the desired behavior.
My answer here also has some ternary examples: stackoverflow.com/a/62527825/1783588
d
dx_over_dt

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.


S
Sergio Abreu

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"`

B
Bruno Bronosky

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

S
Sapphire_Brick

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 ))


d
druid62

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

D
Dan Bray

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:

Pass the individual commands unquoted but be sure to quote ;, &&, and || operators.

t ls / ? ls qqq '||' echo aaa : echo bbb '&&' ls qq

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'