Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Bash & Python Automation By James Joyner IV · · 8 min read Last reviewed Jul 2026

Bash Error Guide: 'unary operator expected' — Fix Empty Test Operands

Quick answer

Fix Bash 'unary operator expected': quote test operands so an empty or unset variable can't collapse '[ $x = y ]' into a broken comparison.

  • #bash
  • #automation
  • #troubleshooting
  • #errors
Free toolkit

Stuck on this Bash & Python Automation error? Get the free incident triage checklist

A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.

Overview

Bash prints unary operator expected when a test / [ ... ] comparison lost one of its operands before [ ran. The classic trigger is an unquoted variable that expanded to nothing:

./check.sh: line 12: [: =: unary operator expected

test is an ordinary command, and [ runs word-splitting on its arguments before it evaluates them. If you write [ $x = "prod" ] and $x is empty, the shell hands [ the arguments = and prod — only two words instead of three. [ sees a binary operator (=) sitting where it expected a single value and a unary operator, so it aborts. The fix is almost always to quote the operand.

Symptoms

  • A comparison that works for most inputs fails only when a variable happens to be empty or unset.
  • The error names the operator (=, !=, -eq) as the thing that appeared where an operand should be.
  • Conditionals that “sometimes” take the wrong branch, or scripts that break only in a fresh environment where a variable hasn’t been exported.
  • [[ ... ]] versions of the same test do not fail — a strong hint the problem is word-splitting inside old-style [ ].

Common Root Causes

  • Unquoted empty/unset variable[ $x = y ] becomes [ = y ] when $x is empty.
  • Left operand is a command substitution that printed nothing[ $(get_state) = up ] with no output.
  • A value that itself starts with -[ $flag = on ] where flag="-n" makes [ try to parse -n as a unary operator.
  • Comparing an array element or positional parameter that isn’t set[ $1 = deploy ] with no arguments passed.
  • A trailing/leading part of the value stripped by word-splitting so the operator lands in the wrong argument slot.

Diagnostic Workflow

Reproduce with argument counting so you can see how many words [ actually receives:

x=""
set -- $x = prod
printf 'argc=%d args=[%s]\n' "$#" "$*"   # argc=2 args=[= prod]  -> operand missing

Trace the failing line and watch the expansion:

bash -x ./check.sh 2>&1 | grep -n 'unary\|\[ '

Confirm the suspect variable is empty at the point of comparison:

printf 'x=[%q]\n' "$x"    # [] means empty; a value shows escaped

Example Root Cause Analysis

A health-check script decided whether to page:

STATE=$(curl -s "$URL/health" | jq -r .state)
if [ $STATE = "healthy" ]; then
  echo "ok"
else
  page_oncall
fi

On a flaky endpoint it died with:

healthcheck.sh: line 4: [: =: unary operator expected

bash -x showed the cause:

+ STATE=
+ '[' = healthy ']'
healthcheck.sh: line 4: [: =: unary operator expected

curl had failed, jq printed nothing, and $STATE was empty. Unquoted, the test collapsed to [ = healthy ]. Worse, the error aborted the if in a way that skipped paging entirely. The robust fix quotes both operands and defaults the value:

STATE=$(curl -s "$URL/health" | jq -r .state)
STATE=${STATE:-unknown}
if [ "$STATE" = "healthy" ]; then
  echo "ok"
else
  page_oncall
fi

Quoting "$STATE" guarantees a single argument even when empty, so [ "" = healthy ] evaluates cleanly to false instead of erroring.

Prevention Best Practices

  • Always quote both operands[ "$x" = "y" ], never [ $x = y ]. This one habit removes almost every occurrence.
  • Prefer [[ ... ]] in Bash — it doesn’t word-split unquoted variables, so an empty operand stays a single empty string.
  • Default risky values${x:-} or ${x:-unknown} so a comparison always has something to compare.
  • Enable set -u to catch truly unset variables by name before they reach a test.
  • Guard command-substitution results — capture into a variable, check it’s non-empty, then compare.
  • Run ShellCheck — it flags unquoted test operands (SC2086) and suggests [[ ]].

Quick Command Reference

[ "$x" = "prod" ]            # quoted: safe even when $x is empty
[[ $x == prod ]]            # [[ ]] never word-splits the operand
x=${x:-unknown}             # default so the operand is never missing
set -- $x = y; echo "$#"    # count words handed to [ (debugging)
bash -x ./script.sh         # trace the real expansion
shellcheck ./script.sh      # SC2086 flags the unquoted operand

Conclusion

unary operator expected means [ lost an operand to word-splitting — usually an unquoted variable that expanded to nothing. Quote both sides of every comparison, prefer [[ ... ]] in Bash, and default risky values with ${x:-}. Do that and the test evaluates to a clean true/false even in the edge case where the value is empty, instead of aborting your conditional at the worst possible moment.

Free download · 368-page PDF

Fixed it? Get 500 Bash & Python Automation & DevOps AI prompts — free

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.

Did this fix your issue?

Free download · 368-page PDF

Get 500 Battle-Tested DevOps AI Prompts — Free

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.