Bash Conditionals and Loops: if, case, for, while, until

Every useful Bash script has to make decisions and repeat work. Is the disk full? Does the config file exist? Run this command on all ten servers. Skip the empty log files. That’s what conditionals and loops are for, and honestly, once you’re comfortable with them, about 80% of shell scripting is just gluing commands together with if and for.

This guide covers all of it in one place: if/else, nested ifs, case, the three loop types, break and continue, looping over files and arrays, plus the operators you need to write the conditions themselves (numbers, strings, files, permissions and math).

Every example was run on Ubuntu 26.04.1 LTS (Bash 5.3.9) and RHEL 10.2 (Bash 5.2.26). The output you see is real. Where the two systems behaved differently, I’ve pointed it out.

New to scripting? Start with your first Bash script and Bash fundamentals, then come back here.

Set up a practice folder

Save each example into its own file, make it executable, and run it. Something like this:

mkdir -p ~/flow && cd ~/flow
nano disk.sh          # paste the script, save
chmod +x disk.sh
./disk.sh

Run your scripts with ./script.sh or bash script.sh, not sh script.sh. On Ubuntu, sh points to dash, and dash doesn’t understand [[ ]], arrays or (( )).

Bash if, else and elif statements

The basic shape looks like this:

if CONDITION; then
  commands
elif OTHER_CONDITION; then
  commands
else
  commands
fi

Bash checks each condition from the top and runs the first block that matches. elif and else are both optional. And yes, the block ends with fi, which is just if backwards.

Here’s a practical one. It checks how full the root filesystem is and prints a different message depending on the number:

#!/usr/bin/env bash
usage=$(df --output=pcent / | tail -1 | tr -dc '0-9')

if (( usage >= 90 )); then
  echo "CRITICAL: root is ${usage}% full"
elif (( usage >= 75 )); then
  echo "WARNING: root is ${usage}% full"
else
  echo "OK: root is ${usage}% full"
fi
./disk.sh
OK: root is 1% full

Same result on both Ubuntu and RHEL. The tr -dc '0-9' bit strips the % sign and spaces so we’re left with a plain number we can compare.

One thing worth knowing early: if doesn’t actually care about brackets. It runs a command and checks its exit code. Zero means true, anything else means false. So this is perfectly valid:

if grep -q "^asif:" /etc/passwd; then
  echo "user found"
fi

[[ ]], [ ] and (( )) are just commands (well, keywords) that return 0 or 1. That’s why you need spaces inside them. [[$x == 1]] fails with “command not found” because Bash reads [[$x as one word.

Bash nested if statements

A nested if is just an if inside another if. You use it when the second question only makes sense after the first one passes. For example, there’s no point asking whether a user has sudo rights if the user doesn’t exist.

#!/usr/bin/env bash
user="$1"

if id "$user" &>/dev/null; then
  if [[ $(id -u "$user") -eq 0 ]]; then
    echo "$user exists and is root"
  else
    if id -nG "$user" | grep -qwE 'sudo|wheel'; then
      echo "$user exists and has sudo rights"
    else
      echo "$user exists, regular user"
    fi
  fi
else
  echo "$user does not exist"
fi
./nested.sh root
root exists and is root
./nested.sh asif
asif exists and has sudo rights
./nested.sh nobody
nobody exists, regular user
./nested.sh bob
bob does not exist

Checking for both sudo and wheel is what makes this portable. Ubuntu puts admins in the sudo group, RHEL uses wheel. Same output on both.

A word of warning though. Three levels deep is about the limit before nested ifs get painful to read. If you’re going deeper, you can usually flatten it with elif, combine conditions with &&, or exit early when something fails.

Bash case statement with examples

When you’re comparing one value against a bunch of options, a case statement is much cleaner than a long chain of elifs. The classic use is handling script arguments:

#!/usr/bin/env bash
case "$1" in
  start|up)
    echo "Starting app..." ;;
  stop|down)
    echo "Stopping app..." ;;
  restart)
    echo "Restarting app..." ;;
  status)
    echo "App is running" ;;
  *)
    echo "Usage: $0 {start|stop|restart|status}"
    exit 1 ;;
esac
./svc.sh restart
Restarting app...
./svc.sh down
Stopping app...
./svc.sh reload
Usage: ./svc.sh {start|stop|restart|status}
echo $?
1

A few things to notice:

  • | lets one branch match several values, so stop and down do the same thing.
  • Each branch ends with ;;. Forget it and you’ll get a syntax error.
  • *) is the catch-all. Put it last, and use it to print usage and exit with a non-zero code.

Case patterns are globs, not plain strings, which makes them great for sorting files by extension:

#!/usr/bin/env bash
for f in report.PDF photo.jpg backup.tar.gz notes; do
  case "$f" in
    *.[pP][dD][fF])  echo "$f -> document" ;;
    *.jpg|*.png)     echo "$f -> image" ;;
    *.tar.gz|*.zip)  echo "$f -> archive" ;;
    *)               echo "$f -> unknown" ;;
  esac
done
./ftype.sh
report.PDF -> document
photo.jpg -> image
backup.tar.gz -> archive
notes -> unknown

The [pP][dD][fF] trick matches the extension in any mix of upper and lower case.

Bash comparison operators explained

This is where a lot of beginners get bitten. Bash has two separate sets of comparison operators: one for numbers and one for strings. Use the wrong one and you won’t get an error. You’ll just get the wrong answer.

Numbers in [[ ]] Numbers in (( )) Strings in [[ ]] Meaning
-eq == == equal
-ne != != not equal
-gt > > greater than (strings: sorts after)
-ge >= n/a greater than or equal
-lt < < less than (strings: sorts before)
-le <= n/a less than or equal

Here’s why the distinction matters:

#!/usr/bin/env bash
a=10 b=9
[[ $a -gt $b ]] && echo "-gt: $a is greater than $b"
(( a > b ))     && echo "(( )): $a > $b"
[[ "$a" > "$b" ]] || echo "[[ > ]]: \"10\" sorts before \"9\" as text"
./cmp.sh
-gt: 10 is greater than 9
(( )): 10 > 9
[[ > ]]: "10" sorts before "9" as text

Inside [[ ]], the > sign compares text alphabetically, and “1” comes before “9”. So 10 is “less than” 9. Not what you wanted.

My rule: use (( )) for anything numeric. It reads like normal math, you don’t need the $ on variable names, and there’s no way to accidentally do a string comparison. Use [[ ]] for strings and files.

Bash string comparison and pattern matching

[[ ]] is where string checks live. It handles exact matches, empty checks, glob patterns and even full regular expressions:

#!/usr/bin/env bash
env="production"
host="web01.example.com"
empty=""

[[ $env == "production" ]] && echo "1. exact match"
[[ $env != "staging" ]]    && echo "2. not staging"
[[ -z $empty ]]            && echo "3. empty is empty"
[[ -n $env ]]              && echo "4. env is set"
[[ $host == web* ]]        && echo "5. glob: starts with web"
[[ $host == *.example.com ]] && echo "6. glob: example.com domain"
if [[ $host =~ ^([a-z]+)([0-9]+)\. ]]; then
  echo "7. regex: role=${BASH_REMATCH[1]} number=${BASH_REMATCH[2]}"
fi
./str.sh
1. exact match
2. not staging
3. empty is empty
4. env is set
5. glob: starts with web
6. glob: example.com domain
7. regex: role=web number=01

The operators at a glance:

  • == and != for equal and not equal (= works too)
  • -z true if the string is empty, -n true if it isn’t
  • == pattern* glob matching, as long as the right side is not quoted
  • =~ regex matching, with captured groups saved in BASH_REMATCH

That quoting rule catches people out. [[ $host == web* ]] is a pattern match. [[ $host == "web*" ]] looks for a literal asterisk. Same goes for =~: keep the regex unquoted, or store it in a variable first.

Want case-insensitive matching? Lower-case the value with ${var,,} before comparing. More expansion tricks like that are in Bash variables explained.

Bash logical operators: AND, OR and NOT

You’ll often need to test more than one thing at once. Bash gives you three operators:

  • && (AND): both sides must be true
  • || (OR): at least one side must be true
  • ! (NOT): flips the result
#!/usr/bin/env bash
port=8080
user=$(whoami)

if [[ $port -ge 1024 && $port -le 65535 ]]; then
  echo "AND: $port is a valid unprivileged port"
fi
if [[ $user == "root" || $user == "admin" ]]; then
  echo "OR: privileged user"
else
  echo "OR: $user is not root or admin"
fi
if ! command -v nginx &>/dev/null; then
  echo "NOT: nginx is not installed"
fi
mkdir -p /tmp/demo && echo "chain: /tmp/demo ready"
grep -q "^nobody:" /etc/passwd || echo "chain: no nobody user"

On RHEL, which doesn’t have Nginx installed:

./logic.sh
AND: 8080 is a valid unprivileged port
OR: asif is not root or admin
NOT: nginx is not installed
chain: /tmp/demo ready

On the Ubuntu box, Nginx was already installed from an earlier LEMP setup, so the NOT line didn’t print. Exactly what you’d expect.

The last two lines show && and || used outside of brackets, as a short way to chain commands. cmd1 && cmd2 runs cmd2 only if cmd1 succeeded. cmd1 || cmd2 runs cmd2 only if cmd1 failed. The grep found the nobody user, so nothing printed.

Older scripts use -a and -o inside single brackets for AND and OR. They’re deprecated and have weird edge cases. Stick with && and || inside [[ ]].

How to check if a file or directory exists in Bash

Probably the most common check in any script. Before you read a config, write a log or delete a folder, you should know it’s actually there. These are the test operators you’ll use most:

Operator True if the path…
-e exists (any type)
-f is a regular file
-d is a directory
-L is a symbolic link
-s exists and is not empty
file1 -nt file2 file1 is newer than file2

This script runs a handful of paths through them:

#!/usr/bin/env bash
for p in /etc/hosts /etc /etc/nothing ~/flow/logs/empty.log /etc/localtime; do
  if [[ -L $p ]]; then
    echo "$p -> symlink"
  elif [[ -f $p && -s $p ]]; then
    echo "$p -> regular file, not empty"
  elif [[ -f $p ]]; then
    echo "$p -> regular file, but empty"
  elif [[ -d $p ]]; then
    echo "$p -> directory"
  else
    echo "$p -> does not exist"
  fi
done
./exists.sh
/etc/hosts -> regular file, not empty
/etc -> directory
/etc/nothing -> does not exist
/home/asif/flow/logs/empty.log -> regular file, but empty
/etc/localtime -> symlink

The -L check goes first on purpose. -f and -d follow symlinks, so /etc/localtime would also pass -f because it points to a real file. Check for the link before anything else if the difference matters to you.

To check that something does not exist, add !. This one-liner creates a backup folder only if it’s missing:

[[ ! -d /backup ]] && sudo mkdir -p /backup

Honestly, mkdir -p already does nothing if the folder exists, so you could skip the test there. But the pattern is handy for things like “exit if the config file is missing”.

How to check file permissions using Bash

Existence is one thing. Whether your script can actually use the file is another. These tests check permissions for whoever is running the script:

  • -r readable
  • -w writable
  • -x executable (for a directory, it means you can enter it)
  • -O you own it
#!/usr/bin/env bash
for f in secret.key deploy.sh /etc/shadow; do
  echo -n "$f: "
  [[ -r $f ]] && echo -n "read " || echo -n "no-read "
  [[ -w $f ]] && echo -n "write " || echo -n "no-write "
  [[ -x $f ]] && echo -n "exec " || echo -n "no-exec "
  [[ -O $f ]] && echo "(you own it)" || echo "(owned by $(stat -c %U "$f"))"
done
./perms.sh
secret.key: read write no-exec (you own it)
deploy.sh: read write exec (you own it)
/etc/shadow: no-read no-write no-exec (owned by root)

Identical on both distros. The real permissions on /etc/shadow are different though. Ubuntu uses -rw-r----- owned by root:shadow, while RHEL locks it down completely with ---------- owned by root:root. Your script doesn’t need to care. -r answers the only question that matters: can this user read it right now?

If you need the actual mode, say to enforce 600 on a private key, use stat:

stat -c '%A %a %U:%G %n' secret.key
-rw------- 600 asif:asif secret.key
mode=$(stat -c %a secret.key)
if [[ $mode != "600" ]]; then
  echo "secret.key has mode $mode, fixing"
  chmod 600 secret.key
fi

Keep in mind that root passes -r and -w on almost everything, so these checks are most useful in scripts run by normal users. For permissions beyond owner/group/other, see Linux ACLs with setfacl and getfacl.

Bash arithmetic operations and calculations

For integer math, use $(( )). It’s built in, fast, and supports everything you’d expect:

#!/usr/bin/env bash
a=17 b=5
echo "add:      $(( a + b ))"
echo "subtract: $(( a - b ))"
echo "multiply: $(( a * b ))"
echo "divide:   $(( a / b ))"
echo "modulo:   $(( a % b ))"
echo "power:    $(( 2 ** 10 ))"
(( a++ )); echo "a++ now:  $a"
let "total = a * 2"; echo "let:      $total"
echo "float:    $(echo "scale=2; 17 / 5" | bc)"

On Ubuntu:

./math.sh
add:      22
subtract: 12
multiply: 85
divide:   3
modulo:   2
power:    1024
a++ now:  18
let:      36
float:    3.40

Notice that 17 divided by 5 gives 3, not 3.4. Bash only does whole numbers and just drops the decimal part. For decimals you need an external tool like bc.

And here’s an actual difference between the two systems. On a minimal RHEL 10 install, the last line failed:

./math.sh: line 11: bc: command not found
float:

bc isn’t installed there by default. Either install it, or use awk, which is on pretty much every Linux box:

sudo dnf install bc            # RHEL
sudo apt install bc            # Ubuntu, if it's missing
awk 'BEGIN { printf "%.2f\n", 17/5 }'
3.40

Two more things to remember. let and expr still work, but $(( )) is the modern way and doesn’t need quoting tricks. And dividing by zero kills the expression with an error, so check your divisor first:

echo $(( 10 / 0 ))
bash: 10 / 0 : division by 0 (error token is "0 ")

Bash for loop explained with practical examples

The for loop is the one you’ll write most. It comes in a few flavours:

#!/usr/bin/env bash
# 1. Loop over a list
for svc in nginx mysql redis; do
  echo "Checking $svc"
done

# 2. Number range with brace expansion
for i in {1..3}; do echo -n "$i "; done; echo

# 3. Range with a step
for i in {0..10..5}; do echo -n "$i "; done; echo

# 4. C-style counter
for (( i=1; i<=3; i++ )); do echo "Retry $i of 3"; done

# 5. Loop over command output
for u in $(awk -F: '$3 >= 1000 && $3 < 65000 {print $1}' /etc/passwd); do
  echo "Human user: $u"
done
./forloop.sh
Checking nginx
Checking mysql
Checking redis
1 2 3
0 5 10
Retry 1 of 3
Retry 2 of 3
Retry 3 of 3
Human user: asif

A couple of gotchas. Brace ranges like {1..$n} don’t work with variables because brace expansion happens before variables are filled in. Use the C-style form for that: for (( i=1; i<=n; i++ )).

Also, looping over $(command) splits on spaces, not just new lines. It’s fine for usernames, which never contain spaces, but it’s the wrong tool for reading files line by line. That’s a job for while.

Bash while loop explained

A while loop keeps going as long as its condition is true. Use it when you don’t know in advance how many times you’ll loop.

#!/usr/bin/env bash
count=1
while (( count <= 3 )); do
  echo "Pass $count"
  (( count++ ))
done

while read -r line; do
  echo "line: $line"
done < logs/app.log
./whileloop.sh
Pass 1
Pass 2
Pass 3
line: error one
line: ok

The while read -r line ... done < file pattern is the right way to read a file line by line. The -r stops backslashes from being treated as escape characters, and the < at the end feeds the file into the loop. If redirection is still a bit fuzzy, pipes and redirection in Linux explains it properly.

Forget to increment count in the first loop and you’ve got an infinite loop. Press Ctrl+C to get out. Everyone does it at least once.

Sometimes an infinite loop is what you want, like a simple watcher that only stops when you tell it to:

while true; do
  date +%T
  sleep 5
done

Bash until loop with examples

until is the mirror image of while. It runs as long as the condition is false and stops the moment it becomes true. That makes it read naturally for “wait until something is ready” jobs.

In this example a background job creates a flag file after 3 seconds, and the loop waits for it:

#!/usr/bin/env bash
( sleep 3; touch /tmp/ready.flag ) &
tries=0
until [[ -f /tmp/ready.flag ]]; do
  (( tries++ ))
  echo "Waiting for ready.flag (try $tries)"
  sleep 1
done
echo "Ready after $tries tries"
rm -f /tmp/ready.flag
./untilloop.sh
Waiting for ready.flag (try 1)
Waiting for ready.flag (try 2)
Waiting for ready.flag (try 3)
Ready after 3 tries

That’s the RHEL run. On Ubuntu it took 4 tries, simply because of timing. Which is a good reminder: in a real script, always add a limit so it can’t wait forever. Something like this, for waiting on a web server to come up:

tries=0
until curl -fs http://localhost >/dev/null; do
  (( ++tries >= 30 )) && { echo "gave up after 30s"; exit 1; }
  sleep 1
done
echo "web server is up"

Anything you can write with until can also be written as while !. Pick whichever reads better for the situation.

Break and continue statements in Bash

Two keywords give you control inside any loop:

  • continue skips the rest of the current pass and jumps to the next one
  • break leaves the loop completely
#!/usr/bin/env bash
for n in {1..10}; do
  (( n % 2 == 0 )) && continue
  (( n > 7 )) && break
  echo "odd: $n"
done
./breakcont.sh
odd: 1
odd: 3
odd: 5
odd: 7

Even numbers get skipped by continue. When the loop hits 9, break ends it, so 9 never prints.

In nested loops, plain break only exits the innermost loop. Use break 2 to jump out of two levels at once. continue 2 works the same way.

How to loop through files in Bash

The simplest way is a glob. Bash expands *.log into a list of matching file names, and spaces in names are handled correctly as long as you quote "$f":

#!/usr/bin/env bash
for f in logs/*.log; do
  lines=$(wc -l < "$f")
  echo "$f has $lines lines"
done
logs/app.log has 2 lines
logs/empty.log has 0 lines
logs/web server.log has 1 lines

See web server.log? One file, one loop pass, even with the space in its name. Don’t ever do for f in $(ls). That would split it into “web” and “server.log”.

Now the gotcha that bites everyone eventually. If a glob matches nothing, Bash leaves the pattern as it is:

for f in empty/*; do echo "got: $f"; done
got: empty/*

Your script then tries to process a file literally called empty/*. Fix it with nullglob, which makes an unmatched glob expand to nothing, so the loop simply doesn’t run:

shopt -s nullglob
for f in empty/*; do echo "got: $f"; done
shopt -u nullglob

(No output. That’s the point.)

For recursive searches, or when you need to filter by size or age, pair find with a while read loop. Using -print0 and read -d '' makes it safe for any file name, even ones with new lines:

find logs -type f -name '*.log' -print0 | while IFS= read -r -d '' f; do
  echo "found: $f"
done
found: logs/app.log
found: logs/empty.log
found: logs/web server.log

The order of find results isn’t guaranteed, so pipe through sort -z if you need it. There’s a lot more find can do in 30 practical find examples.

How to loop through arrays in Bash

When you have a list of servers, users or packages, put them in an array and loop over "${array[@]}". The quotes are not optional. Look what happens without them:

#!/usr/bin/env bash
servers=("web01" "web02" "db server")
for s in "${servers[@]}"; do echo "ping $s"; done
echo "--- unquoted (wrong)"
for s in ${servers[@]}; do echo "ping $s"; done
ping web01
ping web02
ping db server
--- unquoted (wrong)
ping web01
ping web02
ping db
ping server

Three servers turned into four. Quote it every time.

Need the index as well as the value? "${!servers[@]}" gives you the indexes. And for key/value pairs, use an associative array with declare -A, then loop over the keys the same way:

for i in "${!servers[@]}"; do echo "$i: ${servers[$i]}"; done

declare -A port=([ssh]=22 [http]=80 [https]=443)
for name in "${!port[@]}"; do echo "$name -> ${port[$name]}"; done
0: web01
1: web02
2: db server
ssh -> 22
https -> 443
http -> 80

Notice the associative array came back in a different order than we defined it. Bash doesn’t keep insertion order for those. If order matters, keep a separate indexed array of keys, or pipe the output through sort.

Putting it together: a small log checker

Here’s a short script that uses most of what we’ve covered. It checks every log in a folder, skips empty ones, counts errors, and exits with a non-zero code if it found any:

#!/usr/bin/env bash
dir="${1:-logs}"

if [[ ! -d $dir ]]; then
  echo "No such directory: $dir"
  exit 2
fi

shopt -s nullglob
total=0
for f in "$dir"/*.log; do
  [[ -r $f ]] || { echo "skip (unreadable): $f"; continue; }
  [[ -s $f ]] || { echo "skip (empty): $f"; continue; }

  errors=$(grep -ci "error" "$f")
  case $errors in
    0) echo "OK    $f" ;;
    *) echo "ERROR $f ($errors found)"; (( total += errors )) ;;
  esac
done

(( total == 0 )) && echo "All clean" || { echo "Total errors: $total"; exit 1; }
./logcheck.sh
ERROR logs/app.log (1 found)
skip (empty): logs/empty.log
OK    logs/web server.log
Total errors: 1
echo $?
1

./logcheck.sh /nope
No such directory: /nope

Directory check, permission check, file-size check, a for loop over files, continue, case, arithmetic, and a meaningful exit code. Drop it into cron and it’ll tell you when something’s wrong. To turn output like this into a proper report, have a look at building a report script with sort, uniq, tee and xargs.

Common mistakes to avoid

  • Missing spaces in brackets. [[ $a == 1 ]] works, [[$a==1]] doesn’t.
  • Using > for numbers inside [[ ]]. It compares text. Use -gt or (( )).
  • Unquoted variables. [ -f $file ] breaks on empty values and spaces. Use [[ ]] and quote "$f" in commands.
  • Quoting the pattern in [[ == ]]. "web*" matches a literal star, not “anything starting with web”.
  • Parsing ls in a loop. Use globs or find -print0.
  • Forgetting ;; in case statements or the fi, done and esac that close each block.
  • Loops with no exit. Every until or while that waits on something external needs a retry limit.

Install shellcheck (sudo apt install shellcheck on Ubuntu, from EPEL on RHEL) and run it on your scripts. It catches nearly all of these before they cause trouble.

Frequently asked questions

What is the difference between [ ] and [[ ]] in Bash?

[ ] is the POSIX test command and works in any shell. [[ ]] is a Bash keyword that’s safer with empty variables and spaces, supports && and || inside the brackets, and adds glob and regex matching. In Bash scripts, use [[ ]] unless you specifically need POSIX sh compatibility.

How do I write an if else statement on one line in Bash?

Separate the parts with semicolons: if [[ -f app.log ]]; then echo yes; else echo no; fi. For short checks, [[ -f app.log ]] && echo yes || echo no also works, but be careful: if the command after && fails, the || part runs too.

When should I use case instead of if elif?

Use case when you’re checking one variable against several fixed values or patterns, like script arguments, file extensions or distro names. It’s shorter and easier to read. Use if when each branch tests something different, like a file in one branch and a number in another.

What is the difference between while and until loops in Bash?

A while loop runs as long as its condition is true. An until loop runs as long as its condition is false. They’re interchangeable, so until [[ -f x ]] does the same thing as while [[ ! -f x ]]. Pick the one that reads more naturally.

Can Bash do floating point math?

No. $(( )) only handles integers and throws away the decimal part, so $(( 17 / 5 )) gives 3. For decimals, use bc (echo "scale=2; 17/5" | bc) or awk. Note that bc isn’t installed by default on minimal RHEL systems.

How do I check if a file exists and is not empty?

Use -s, which is true only when the file exists and its size is greater than zero: if [[ -s report.txt ]]; then ... fi. Combine it with -f if you also want to make sure it’s a regular file and not a directory.

How do I loop through files with spaces in their names?

Use a glob such as for f in *.log and always quote "$f" when you use it. For recursive searches, use find ... -print0 | while IFS= read -r -d '' f. Avoid for f in $(ls) or $(find), because they split names on spaces.

Asif Khan

Asif Khan

DevOps, Linux, and Cloud engineer with 10+ years building infrastructure that stays up, and automation so I do not have to fix it at 2am. My focus is automation, security, and systems built to survive failure, using Infrastructure as Code, CI/CD, containers, monitoring, and cloud platforms instead of manual fixes. This blog is where I write down what worked, and just as often what didn't the first time around. Real tutorials you can run yourself, troubleshooting notes pulled from actual outages, and the kind of detail you only get once you've hit the problem firsthand. If you're trying to get Linux, DevOps, and cloud infrastructure to behave, that's what you'll find here: reproducible solutions you can understand, test, and apply in your own environment.