Interview questions › Scripting (Bash, Groovy)
Scripting (Bash, Groovy) interview questions & answers
100 Scripting (Bash, Groovy) interview questions, each answered three ways: a concise spoken answer, a technical explanation, and a hands-on example.
Tip: paste the job description + your resume into our free resume checker to see which of these skills the role actually requires.
All questions
- What is the purpose of the shebang line, and what does #!/bin/bash do? [Basic]
- What is the difference between sh and bash? [Basic]
- How do you make a script executable and run it? [Basic]
- What is the difference between running a script with ./script.sh, bash script.sh, and source script.sh? [Basic]
- What does sourcing a script do differently from executing it? [Basic]
- How do you declare a variable in Bash, and why are spaces around = not allowed? [Basic]
- What is the difference between $var and ${var}? [Basic]
- What is the difference between single quotes and double quotes in Bash? [Basic]
- Why should you quote variables, and what bug does unquoting cause? [Basic]
- What is command substitution, and what is the difference between $(...) and backticks? [Basic]
- How do you do arithmetic in Bash? [Basic]
- What are the positional parameters $1, $2, and what are $@, $#, $0, $$, $?, $!? [Basic]
- What is the difference between $@ and $* when quoted? [Basic]
- What is $? used for, and how do you check the exit status of a command? [Basic]
- What does an exit code of 0 versus non-zero mean? [Basic]
- What do set -e, set -u, set -o pipefail, and set -x do? [Basic]
- Why is set -euo pipefail considered a safe default for scripts? [Basic]
- Why does set -e not always behave as expected (e.g., inside pipelines or conditionals)? [Basic]
- What is the difference between [ ], [[ ]], and (( )) in Bash? [Basic]
- Why do the brackets in [ ... ] require spaces? [Basic]
- What are the common file test operators (-f, -d, -e, -r, -w, -x)? [Basic]
- What is the difference between -eq and = in test conditions? [Basic]
- What are string test operators (-z, -n) used for? [Basic]
- How do you write an if/elif/else statement in Bash? [Basic]
- How do you write a for loop over a list and a C-style for loop? [Basic]
- How do you write a while loop and an until loop? [Basic]
- What is the correct way to read a file line by line, and why not use for line in $(cat)? [Basic]
- What does IFS do, and why set it when reading lines? [Basic]
- What does read -r do, and why is the -r flag important? [Basic]
- How do you write and call a function in Bash? [Basic]
- How do you pass arguments to a function and return a value or status? [Basic]
- What is the difference between a function returning a value via echo versus return? [Basic]
- What is local scope in a Bash function? [Basic]
- How do you parse command-line options with getopts? [Intermediate]
- What is OPTARG, and how does getopts handle options with values? [Intermediate]
- What is a here-document, and when would you use one? [Intermediate]
- What is a here-string (<<<), and how does it differ from a here-doc? [Intermediate]
- What is the difference between > , >> , and 2> redirection? [Intermediate]
- What does 2>&1 mean, and how do you redirect both stdout and stderr? [Intermediate]
- What is /dev/null, and why redirect to it? [Intermediate]
- What is a pipe, and how does data flow through a | b | c? [Intermediate]
- What is the difference between && and || in command chaining? [Intermediate]
- What is the difference between a subshell and the current shell? [Intermediate]
- What is process substitution (<(...)), and when is it useful? [Intermediate]
- What is the trap command, and how do you use it for cleanup on exit? [Intermediate]
- How would you run cleanup code whether a script succeeds or fails? [Intermediate]
- How do you create a secure temporary file or directory (mktemp)? [Intermediate]
- How do you handle errors and provide a useful message on failure? [Intermediate]
- What is the difference between exit and return? [Intermediate]
- What are exit codes you should use, and what does a non-zero exit signal to a pipeline? [Intermediate]
- How do you debug a Bash script (set -x, bash -x, shellcheck)? [Intermediate]
- What is shellcheck, and what kinds of issues does it catch? [Intermediate]
- How do you check whether a command exists before using it? [Intermediate]
- How do you loop over files safely when names contain spaces? [Intermediate]
- What is the difference between glob expansion and regex in the shell? [Intermediate]
- What is parameter expansion, and what does ${var:-default} do? [Intermediate]
- What does ${var:?message} do, and when would you use it? [Intermediate]
- What is the difference between ${var%pattern} and ${var#pattern}? [Intermediate]
- How do you get the length of a string or the number of elements in an array? [Intermediate]
- How do you declare and iterate over an array in Bash? [Intermediate]
- What is an associative array, and how do you use one? [Intermediate]
- How do you split a string on a delimiter in Bash? [Intermediate]
- How do you replace text in a variable using parameter expansion? [Intermediate]
- What is the difference between cut, awk, and sed for text processing? [Intermediate]
- How do you extract a specific column from delimited text with cut and with awk? [Intermediate]
- How does awk split lines into fields, and what are $1 and NF? [Intermediate]
- What is the difference between NR and NF in awk? [Advanced]
- How do you use awk to sum or average a column? [Advanced]
- How do you use sed to find and replace text in a file (with a backup)? [Advanced]
- What is the difference between sed -i and sed without -i? [Advanced]
- How do you print specific lines of a file with sed or awk? [Advanced]
- How do you count lines, words, and characters with wc? [Advanced]
- How would you find the top N most frequent values in a log (sort | uniq -c | sort -rn)? [Advanced]
- Why must input be sorted before uniq, and what does uniq -c do? [Advanced]
- How would you write a script to monitor disk usage and alert past a threshold? [Advanced]
- How would you write a script to check if a process is running and restart it? [Advanced]
- How would you write a backup script that rotates and keeps only the newest N files? [Advanced]
- How would you write a script to retry a flaky command with a delay? [Advanced]
- How would you write a health-check script that polls a URL until it returns 200? [Advanced]
- How do you schedule a script with cron, and what do the five cron fields mean? [Advanced]
- What is the difference between a cron job and a systemd timer? [Advanced]
- How would you make a long-running script run safely in the background? [Advanced]
- What is idempotency, and how do you make a script safe to run repeatedly? [Advanced]
- How do you securely handle secrets in a shell script (avoid hardcoding, use env or a vault)? [Advanced]
- Why should you avoid putting secrets on the command line, and what leaks them? [Advanced]
- What is Groovy, and where do you most use it in a DevOps context? [Advanced]
- How is Groovy used to write Jenkins pipelines? [Advanced]
- What is the difference between a declarative and a scripted Jenkins pipeline in Groovy? [Advanced]
- What is a Jenkins shared library, and how is it written in Groovy? [Advanced]
- How do you define and call a function (or step) in a Jenkins Groovy pipeline? [Advanced]
- How do you run shell commands from a Groovy pipeline (sh step) and capture output? [Advanced]
- How do you handle errors and retries in a Groovy pipeline (try/catch, retry, timeout)? [Advanced]
- How do you use environment variables and credentials in a Groovy pipeline? [Advanced]
- How do you run stages in parallel in a Jenkins Groovy pipeline? [Advanced]
- What is the difference between Groovy and Java for scripting purposes? [Advanced]
- When would you choose Bash versus Groovy versus Python for an automation task? [Advanced]
- What recent automation script have you written that saved significant manual effort? [Advanced]
- How do you make a script portable across different shells and systems? [Advanced]
- How would you structure a large script into reusable functions and a main entry point? [Advanced]
- How do you write a dry-run mode into an automation script, and why is it valuable? [Advanced]
What is the purpose of the shebang line, and what does #!/bin/bash do? [Basic]
Answer
The shebang tells the operating system which interpreter should run the script when it is executed directly. #!/bin/bash means: run this file using the Bash interpreter located at /bin/bash.
Technical explanation
The shebang must be the first line in the file; otherwise the kernel will not use it as the interpreter directive.
It matters when running ./script.sh. If I run bash script.sh, I am explicitly choosing Bash and the shebang is mostly ignored.
For portability, #!/usr/bin/env bash is often used when Bash may not live at /bin/bash, but production scripts should still document the required Bash version.
Hands-on example
Create and run a Bash script:
cat > hello.sh <<'EOF'
#!/bin/bash
echo "running with $BASH_VERSION"
EOF
chmod +x hello.sh
./hello.sh
What is the difference between sh and bash? [Basic]
Answer
sh is a POSIX-style shell interface, while bash is GNU Bash with additional features such as arrays, [[ ]], brace expansion, process substitution, and richer parameter expansion. A script using Bash-specific syntax should declare Bash explicitly.
Technical explanation
On many Linux systems /bin/sh is dash, bash in POSIX mode, or another shell, so Bash extensions can fail under sh.
Use sh for maximum POSIX portability and Bash when the script needs Bash-specific features or better ergonomics.
In CI and containers, being explicit avoids surprises: use #!/usr/bin/env bash for Bash scripts and avoid Bashisms in /bin/sh scripts.
Hands-on example
This works in bash but not portable sh:
#!/usr/bin/env bash
arr=(api worker scheduler)
[[ " ${arr[*]} " == *" api "* ]] && echo "api found"
A POSIX sh version would use case statements or loops instead of arrays and [[ ]].
How do you make a script executable and run it? [Basic]
Answer
I make a script executable with chmod +x script.sh and run it as ./script.sh, assuming the file has a valid shebang. I can also run it with an explicit interpreter, such as bash script.sh.
Technical explanation
chmod +x adds execute permission so the OS can invoke the file as a program.
The ./ prefix tells the shell to run the script from the current directory, because current directory is normally not in PATH for security reasons.
If the script is in a directory listed in PATH, it can be run by name after the execute bit is set.
Hands-on example
cat > deploy.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
echo "deploying $1"
EOF
chmod +x deploy.sh
./deploy.sh prod
What is the difference between running a script with ./script.sh, bash script.sh, and source script.sh? [Basic]
Answer
./script.sh executes the file as a new process using its shebang. bash script.sh runs it in a new Bash process regardless of the shebang. source script.sh runs it in the current shell, so variables, functions, aliases, and directory changes remain after it finishes.
Technical explanation
Both ./script.sh and bash script.sh run in child processes; environment changes made inside do not modify the parent shell.
source is commonly used for profile files, virtualenv activation, and scripts that intentionally modify the current shell state.
Using source for general automation is risky because exit, cd, variable assignment, or set options can affect the caller.
Hands-on example
cat > env.sh <<'EOF'
export APP_ENV=dev
cd /tmp
EOF
bash env.sh # parent shell is unchanged
source env.sh # APP_ENV and current directory change in this shell
echo "$APP_ENV"
pwd
What does sourcing a script do differently from executing it? [Basic]
Answer
Sourcing runs the file in the current shell instead of starting a separate shell process. That means changes to variables, functions, shell options, and the working directory persist after the sourced file completes.
Technical explanation
It is useful for loading configuration or helper functions into an interactive session or another script.
It is not isolated, so a bad sourced script can overwrite variables, change set options, or exit the caller unless carefully written.
For libraries, avoid top-level destructive commands and expose functions that the caller can invoke deliberately.
Hands-on example
cat > lib.sh <<'EOF'
log() { printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*"; }
export TOOL_REGION=ap-south-1
EOF
source ./lib.sh
log "region is $TOOL_REGION"
How do you declare a variable in Bash, and why are spaces around = not allowed? [Basic]
Answer
A Bash variable is assigned as name=value with no spaces around the equals sign. Spaces are not allowed because the shell parses spaces as command separators; var = value is interpreted as running a command named var with arguments = and value.
Technical explanation
Use uppercase names for exported environment variables and lowercase or mixed case for local script variables to avoid conflicts with system variables.
Always quote variable expansions unless you intentionally want word splitting or glob expansion.
Use readonly for constants and local inside functions to limit scope.
Hands-on example
#!/usr/bin/env bash
app_name="orders-api"
readonly region="ap-south-1"
echo "Deploying ${app_name} to ${region}"
# Wrong: app_name = orders-api # tries to execute app_name
What is the difference between $var and ${var}? [Basic]
Answer
$var and ${var} both expand a variable, but braces disambiguate the variable name and enable parameter expansion operations. I use ${var} when appending text, using defaults, slicing, or manipulating strings.
Technical explanation
Without braces, Bash reads the longest valid variable name, which can produce wrong expansions when text follows the variable.
Braces are required for array indexes, string length, defaults, substring operations, and pattern removal.
Consistently using braces can improve readability in production scripts.
Hands-on example
service="api"
echo "$service_worker" # expands variable named service_worker, likely empty
echo "${service}_worker" # prints api_worker
echo "${service:-default}" # default only if unset or empty
What is the difference between single quotes and double quotes in Bash? [Basic]
Answer
Single quotes preserve text literally, while double quotes still allow variable expansion, command substitution, and some escape sequences. I use single quotes for fixed strings and double quotes when I need controlled interpolation.
Technical explanation
Inside single quotes, $USER, $(cmd), and backslashes are not interpreted.
Inside double quotes, variable expansion occurs but word splitting and globbing on the result are suppressed.
Quoting is a correctness and security control because it prevents accidental argument splitting and unintended wildcard expansion.
Hands-on example
name="Ritik"
echo 'Hello $name' # Hello $name
echo "Hello $name" # Hello Ritik
file="report final.txt"
cat "$file" # safe for spaces
Why should you quote variables, and what bug does unquoting cause? [Basic]
Answer
Variables should usually be quoted to prevent word splitting and pathname expansion. The common bug is that a value containing spaces, newlines, or wildcard characters becomes multiple arguments or expands into matching files.
Technical explanation
Unquoted variables are split using IFS and then glob-expanded, which can corrupt filenames, delete the wrong files, or break commands.
Quoting preserves the value as one argument while still allowing variable expansion.
The main exceptions are deliberate word splitting with arrays or controlled glob patterns, where the intent should be obvious.
Hands-on example
file="build output/*.log"
rm $file # dangerous: word splitting and globbing can change targets
rm -- "$file" # treats the variable as one literal pathname
args=(--env "prod blue" --replicas 3)
kubectl rollout restart deployment/api "${args[@]}
What is command substitution, and what is the difference between $(...) and backticks? [Basic]
Answer
Command substitution runs a command and replaces the expression with its stdout. $(...) is preferred over backticks because it is easier to read, nest, and quote correctly.
Technical explanation
Command substitution strips trailing newlines from the captured output.
Always quote the substitution unless you deliberately want splitting: value=$(cmd); echo "$value".
Backticks require awkward escaping when nested; $(...) nests naturally.
Hands-on example
today=$(date +%F)commit=$(git rev-parse --short HEAD)image="registry.example.com/api:${today}-${commit}"echo "$image"
nested=$(basename "$(pwd)")echo "current directory name: $nested
How do you do arithmetic in Bash? [Basic]
Answer
Bash arithmetic is normally done with $((expression)) for expansion or ((expression)) for evaluation and condition checks. It supports integer arithmetic, increments, comparisons, and common operators.
Technical explanation
Bash arithmetic is integer-based, not floating point; use awk, bc, or Python when decimals are required.
Variables inside arithmetic contexts do not need a leading dollar sign, although using one is often accepted.
(( expression )) returns exit status 0 when the expression is nonzero and 1 when it is zero.
Hands-on example
replicas=2
replicas=$((replicas + 1))
echo "replicas=$replicas"
if (( replicas >= 3 )); then
echo "enough capacity"
fi
What are the positional parameters $1, $2, and what are $@, $#, $0, $$, $?, $!? [Basic]
Answer
$1, $2, and so on are positional arguments. $@ represents all arguments, $# is the argument count, $0 is the script name, $$ is the current shell PID, $? is the last exit code, and $! is the PID of the most recent background command.
Technical explanation
These special parameters are central to writing reusable command-line scripts.
Use "$@" to forward arguments safely while preserving argument boundaries.
Capture $? immediately after the command you care about, because any later command overwrites it.
Hands-on example
#!/usr/bin/env bash
echo "script: $0"
echo "first arg: ${1:-missing}"
echo "arg count: $#"
for arg in "$@"; do echo "arg=[$arg]"; done
sleep 30 &
echo "background pid: $!
What is the difference between $@ and $* when quoted? [Basic]
Answer
When quoted, "$@" expands to separate quoted arguments, while "$*" expands to one string joined by the first character of IFS. For almost all argument forwarding, "$@" is the correct choice.
Technical explanation
"$@" preserves the exact argument list, including arguments containing spaces or empty strings.
"$*" is useful only when intentionally joining all arguments into one string.
Unquoted $@ and $* both undergo word splitting and are usually unsafe.
Hands-on example
set -- "one arg" "two" ""
printf '<%s>\n' "$@" # prints three arguments safely
printf '<%s>\n' "$*" # prints one joined string
wrapper() { real_command "$@"; } # correct forwarding pattern
What is $? used for, and how do you check the exit status of a command? [Basic]
Answer
$? stores the exit status of the most recently executed foreground command. I check it immediately after a command, or more cleanly use if command; then ... else ... fi.
Technical explanation
An exit status of 0 means success by convention; nonzero means failure or another exceptional condition.
Reading $? after echo, test, or another command gives the status of that later command, not the original one.
Using if command is safer and avoids accidental overwriting of the status.
Hands-on example
if curl -fsS https://example.com/health >/dev/null; then
echo "healthy"
else
rc=$?
echo "health check failed with rc=$rc" >&2
exit "$rc"
fi
What does an exit code of 0 versus non-zero mean? [Basic]
Answer
By convention, exit code 0 means success and a nonzero exit code means failure. Automation tools, CI jobs, cron, systemd, and pipelines use this status to decide whether a step succeeded.
Technical explanation
Exit codes are limited to 0-255 in the shell; choose small meaningful values and document them for complex scripts.
Different tools may use specific nonzero codes, so preserve the original code when wrapping commands unless you intentionally normalize it.
A script should exit nonzero when it fails so upstream automation does not treat a broken run as successful.
Hands-on example
validate_config() { [[ -f config.yaml ]] || return 10 yq e . config.yaml >/dev/null || return 11}
validate_config || exit $?echo "config is valid
What do set -e, set -u, set -o pipefail, and set -x do? [Basic]
Answer
set -e exits on many unhandled command failures, set -u treats unset variables as errors, set -o pipefail makes a pipeline fail if any command in it fails, and set -x prints commands before execution for debugging.
Technical explanation
set -e reduces accidental continuation after failures, but it has exceptions and should not replace explicit error handling.
set -u catches typos and missing required inputs early.
pipefail is important because otherwise a pipeline usually returns the exit code of only the last command.
Hands-on example
#!/usr/bin/env bash
set -euo pipefail
: "${ENVIRONMENT:?ENVIRONMENT is required}"
if [[ "${DEBUG:-false}" == "true" ]]; then
set -x
fi
Why is set -euo pipefail considered a safe default for scripts? [Basic]
Answer
set -euo pipefail is considered a safer default because it stops many scripts from continuing with failed commands, unset variables, or hidden pipeline failures. It makes failures more visible and reduces silent data corruption.
Technical explanation
It is useful for deployment, backup, and maintenance scripts where continuing after an error can be dangerous.
It should be paired with intentional exception handling for commands whose failure is expected.
It makes scripts stricter, so optional variables should use defaults like ${var:-default} and required variables should use ${var:?message}.
Hands-on example
#!/usr/bin/env bash
set -euo pipefail
backup_dir=${BACKUP_DIR:-/var/backups/app}
mkdir -p "$backup_dir"
pg_dump app > "$backup_dir/app.sql"
gzip -f "$backup_dir/app.sql
Why does set -e not always behave as expected (e.g., inside pipelines or conditionals)? [Basic]
Answer
set -e has exceptions: it does not always exit inside condition tests, parts of && or || chains, some subshells, or non-final commands in a pipeline unless pipefail is enabled. I treat it as a safety net, not as complete error handling.
Technical explanation
Commands used in if, while, until, or after ! are expected to sometimes return nonzero, so set -e does not exit there.
Without pipefail, false | true returns success because the pipeline status is the last command's status.
For critical operations, handle failures explicitly with if, ||, or a die function.
Hands-on example
set -e
false | true # script may continue without pipefail
set -o pipefail
if grep -q "needle" file.txt; then
echo "found"
else
echo "not found is expected, not fatal"
fi
cp source target || { echo "copy failed" >&2; exit 1; }
What is the difference between [ ], [[ ]], and (( )) in Bash? [Basic]
Answer
[ ] is the traditional test command, [[ ]] is Bash's safer and more capable conditional expression, and (( )) is for arithmetic evaluation. In Bash scripts, I prefer [[ ]] for strings and files, and (( )) for numeric logic.
Technical explanation
[ is a command, so it requires spaces and has more quoting pitfalls.
[[ ]] supports pattern matching, regex with =~, and safer handling of empty variables.
(( )) treats variables as numbers and is ideal for counters and numeric comparisons.
Hands-on example
file="app.log"
[[ -f "$file" && "$file" == *.log ]] && echo "log file"
count=5
if (( count > 3 )); then
echo "count is high"
fi
Why do the brackets in [ ... ] require spaces? [Basic]
Answer
The brackets in [ ... ] require spaces because [ is actually a command name and ] is its final argument. Without spaces, the shell sees a different token, not the test command syntax.
Technical explanation
[ -f file ] is equivalent to running the test command with arguments.
[ -ffile] or [-f file ] is parsed incorrectly because command tokens are separated by whitespace.
[[ ... ]] is shell syntax rather than an external-style command, but spacing is still required around the keywords and operators.
Hands-on example
# Correct:
if [ -f "config.yaml" ]; then
echo "exists"
fi
# Incorrect:
# if [-f "config.yaml"]; then ...
What are the common file test operators (-f, -d, -e, -r, -w, -x)? [Basic]
Answer
Common file tests are -f for regular file, -d for directory, -e for exists, -r for readable, -w for writable, and -x for executable. They are used to validate preconditions before acting on files or directories.
Technical explanation
File tests avoid destructive errors such as overwriting a directory or trying to execute a non-executable file.
Combine tests with clear error messages so operational failures are easy to diagnose.
Remember permissions are evaluated from the current user's perspective, not just file mode bits.
Hands-on example
target="/var/log/app.log"[[ -e "$target" ]] || { echo "missing: $target" >&2; exit 1; }[[ -f "$target" && -r "$target" ]] || { echo "not a readable file" >&2; exit 1; }[[ -d /tmp ]] && echo "/tmp is a directory
What is the difference between -eq and = in test conditions? [Basic]
Answer
-eq is for numeric equality in test expressions, while = compares strings. For numbers, I normally use (( a == b )); for strings, I use [[ $a == $b ]].
Technical explanation
Using = for numbers performs string comparison, which may not match numeric intent in more complex comparisons.
Operators such as -lt, -le, -gt, and -ge are numeric test operators for [ ] and [[ ]].
Inside (( )), use C-style operators: ==, !=, <, <=, >, >=.
Hands-on example
a=10b=10[[ "$a" = "$b" ]] && echo "same string"[[ "$a" -eq "$b" ]] && echo "same number"(( a == b )) && echo "same number using arithmetic context
What are string test operators (-z, -n) used for? [Basic]
Answer
-z checks whether a string is empty, and -n checks whether a string is non-empty. I use them to validate optional and required inputs.
Technical explanation
Quote variables with [ ] to avoid syntax errors when values are empty or contain spaces.
With [[ ]], empty variables are safer, but quoting still improves clarity.
For required environment variables, ${var:?message} is often even cleaner.
Hands-on example
name="${1:-}"
if [[ -z "$name" ]]; then
echo "name is required" >&2
exit 2
fi
[[ -n "${DEBUG:-}" ]] && echo "debug enabled
How do you write an if/elif/else statement in Bash? [Basic]
Answer
An if statement evaluates a command or condition and branches with then, elif, else, and fi. I usually write conditions with [[ ]] for string/file tests and (( )) for numeric tests.
Technical explanation
The condition is a command; success status 0 selects the then branch.
elif avoids deeply nested if statements for multiple cases.
Use clear error branches and exit codes in scripts that validate inputs or preconditions.
Hands-on example
env="${1:-dev}"
if [[ "$env" == "prod" ]]; then
echo "production checks enabled"
elif [[ "$env" == "stage" ]]; then
echo "staging deployment"
else
echo "development deployment"
fi
How do you write a for loop over a list and a C-style for loop? [Basic]
Answer
A list-style for loop iterates over words or array elements, while a C-style loop uses initialization, condition, and increment expressions. I prefer arrays for safe list iteration.
Technical explanation
Do not iterate over unquoted command output when values may contain spaces.
Use "${array[@]}" to preserve each array element as one item.
C-style loops are useful for counters, retries, and fixed numeric ranges.
Hands-on example
services=(api worker scheduler)
for svc in "${services[@]}"; do
echo "restart $svc"
done
for ((i=1; i<=3; i++)); do
echo "attempt $i"
done
How do you write a while loop and an until loop? [Basic]
Answer
A while loop runs while a condition succeeds; an until loop runs until a condition succeeds. while is more common, and until is useful for waiting for a desired state.
Technical explanation
Both loops evaluate command exit status, not boolean keywords in the abstract.
Use sleep in polling loops to avoid busy-waiting.
Add timeout or retry limits in production scripts so loops cannot hang forever.
Hands-on example
count=1
while (( count <= 3 )); do
echo "count=$count"
((count++))
done
until curl -fsS http://localhost:8080/health >/dev/null; do
echo "waiting for service"
sleep 2
done
What is the correct way to read a file line by line, and why not use for line in $(cat)? [Basic]
Answer
The correct pattern is while IFS= read -r line; do ... done < file. Do not use for line in $(cat file) because command substitution performs word splitting, removes important whitespace, and can break lines into words instead of records.
Technical explanation
IFS= preserves leading and trailing whitespace while reading.
read -r prevents backslashes from being treated as escapes.
Redirecting the file into the loop avoids unnecessary cat and handles lines predictably.
Hands-on example
while IFS= read -r line || [[ -n "$line" ]]; do
printf 'line=[%s]\n' "$line"
done < input.txt
# Bad: for line in $(cat input.txt); do ...; done
What does IFS do, and why set it when reading lines? [Basic]
Answer
IFS is the Internal Field Separator. Bash uses it for word splitting and read field splitting. When reading whole lines, I often set IFS= for that read command so leading and trailing whitespace is preserved.
Technical explanation
The default IFS includes space, tab, and newline, which can unintentionally trim or split input.
Setting IFS locally for one read avoids changing global shell behavior.
IFS can also be used intentionally to split delimited data, such as CSV-like simple fields, but CSV with quoting needs a real parser.
Hands-on example
line=" alpha beta "
while IFS= read -r value; do
printf '<%s>
' "$value"
done <<< "$line"
IFS=: read -r user _ uid _ _ home shell < <(getent passwd "$USER")
echo "$user $uid $home $shell
What does read -r do, and why is the -r flag important? [Basic]
Answer
read -r tells read not to treat backslashes as escape characters. It is important because paths, regexes, Windows-style strings, and log lines can contain backslashes that must be preserved exactly.
Technical explanation
Without -r, read may remove or alter backslashes, corrupting input data.
Use IFS= read -r line for reliable line-oriented file processing.
Use read -r -a array when intentionally splitting a line into a Bash array.
Hands-on example
printf '%s\n' 'C:\temp\app.log' > paths.txt
while IFS= read -r path; do
printf 'path=%s\n' "$path"
done < paths.txt
How do you write and call a function in Bash? [Basic]
Answer
A Bash function is a reusable block defined as name() { commands; } or function name { commands; }. I call it by writing its name followed by arguments, just like a command.
Technical explanation
Functions share the shell process with the caller, so they can read variables, set globals, and return statuses.
Use local variables inside functions to avoid accidental global state changes.
Functions improve readability when a script has validation, logging, cleanup, retries, and separate action steps.
Hands-on example
log() { printf '[%s] %s\n' "$(date +%F_%T)" "$*"}
deploy() { local env="$1" log "deploying to $env"}
deploy prod
How do you pass arguments to a function and return a value or status? [Basic]
Answer
Function arguments are accessed as $1, $2, and so on inside the function. A function returns a status with return, and returns data by printing to stdout, setting a variable by reference pattern, or writing to a file/stream.
Technical explanation
return is only for numeric exit status from 0 to 255; it is not a general data return mechanism.
Use command substitution to capture stdout from a function when returning a string.
For robust scripts, separate data on stdout from logs/errors on stderr.
Hands-on example
get_image_tag() { local branch="$1" local sha="$2" printf '%s-%s\n' "$branch" "$sha"}
tag=$(get_image_tag "main" "abc123")echo "tag=$tag"
validate_env() { [[ "$1" =~ ^(dev|stage|prod)$ ]]; }validate_env prod || exit 2
What is the difference between a function returning a value via echo versus return? [Basic]
Answer
echo outputs data to stdout, while return sets the function's exit status. In Bash, return cannot return arbitrary strings and is limited to numeric status values, so I use echo/printf for data and return for success or failure.
Technical explanation
Command substitution captures stdout, so any logging printed to stdout can pollute returned data.
Use printf instead of echo for predictable formatting.
Send diagnostic messages to stderr and reserve stdout for machine-readable output when a caller captures it.
Hands-on example
lookup_port() { local service="$1" case "$service" in api) printf '8080\n' ;; worker) printf '9090\n' ;; *) echo "unknown service: $service" >&2; return 1 ;; esac}
port=$(lookup_port api) || exit 1echo "port=$port"
What is local scope in a Bash function? [Basic]
Answer
local creates a variable scoped to the current Bash function and its children. It prevents function internals from accidentally overwriting global variables or variables used by the caller.
Technical explanation
Without local, assignments inside a function are global in the script's shell.
Use local for function arguments, temporary paths, counters, and command results.
local is Bash-specific; POSIX sh does not require it, so portability requirements matter.
Hands-on example
env="prod"
set_env_bad() { env="dev"; }
set_env_good() { local env="dev"; echo "inside=$env"; }
set_env_good
echo "outside=$env" # still prod
How do you parse command-line options with getopts? [Intermediate]
Answer
getopts is Bash's built-in way to parse short command-line options. It iterates through options, sets the option name in a variable, and uses OPTARG for option values when the option requires an argument.
Technical explanation
The option string controls which options are valid; a colon after an option means it requires a value.
After parsing, shift $((OPTIND - 1)) removes processed options so positional arguments remain.
getopts handles short options like -e prod -v. For long options like --environment, many teams use a manual case loop or external getopt carefully.
Hands-on example
env="dev"
verbose=false
while getopts ":e:v" opt; do
case "$opt" in
e) env="$OPTARG" ;;
v) verbose=true ;;
:) echo "option -$OPTARG requires a value" >&2; exit 2 ;;
\?) echo "unknown option -$OPTARG" >&2; exit 2 ;;
esac
done
shift $((OPTIND - 1))
echo "env=$env verbose=$verbose remaining=$*
What is OPTARG, and how does getopts handle options with values? [Intermediate]
Answer
OPTARG holds the value for the current option when getopts processes an option that requires an argument. getopts knows an option requires a value when the option character is followed by a colon in the option string.
Technical explanation
For example, in getopts ':f:t:' both -f and -t require values.
A leading colon in the option string lets the script handle missing values and invalid options explicitly.
OPTIND tracks the next argument index and is used for shifting after option parsing.
Hands-on example
file=""
threshold=80
while getopts ":f:t:" opt; do
case "$opt" in
f) file="$OPTARG" ;;
t) threshold="$OPTARG" ;;
:) echo "missing value for -$OPTARG" >&2; exit 2 ;;
\?) echo "invalid option -$OPTARG" >&2; exit 2 ;;
esac
done
: "${file:?file is required with -f}
What is a here-document, and when would you use one? [Intermediate]
Answer
A here-document feeds a block of text to a command's stdin. I use it for generating config files, sending multi-line SQL, templating small files, or passing scripts to remote commands.
Technical explanation
With <<EOF, variables inside the heredoc are expanded by the shell.
With <<'EOF', the content is literal and variables are not expanded, which is safer for templates containing dollar signs.
Use <<-EOF to allow leading tab indentation to be stripped, but spaces are not stripped.
Hands-on example
cat > app.conf <<EOF
app_name=orders
port=8080
env=${ENVIRONMENT:-dev}
EOF
psql "$DATABASE_URL" <<'SQL'
select now();
select count(*) from users;
SQL
What is a here-string (<<<), and how does it differ from a here-doc? [Intermediate]
Answer
A here-string, written command <<< word, passes a single string to a command's stdin. A here-doc is better for multi-line blocks; a here-string is convenient for one variable or one short value.
Technical explanation
Here-strings append a newline to the supplied string.
They are Bash/Ksh/Zsh features, not strictly POSIX sh.
Use process substitution or printf pipelines when you need more control over streaming or portability.
Hands-on example
data="api:8080:active"
IFS=: read -r name port status <<< "$data"
echo "name=$name port=$port status=$status"
# Multi-line input is clearer as a here-doc:
cat <<'EOF'
line one
line two
EOF
What is the difference between > , >> , and 2> redirection? [Intermediate]
Answer
> redirects stdout and overwrites the target file, >> redirects stdout and appends, and 2> redirects stderr. I use them deliberately so output, logs, and errors go to the correct destination.
Technical explanation
stdout is file descriptor 1 and stderr is file descriptor 2.
Overwriting with > can destroy existing files, so use it only when replacement is intended.
For operational scripts, redirect stdout and stderr separately when logs need to distinguish normal output from errors.
Hands-on example
echo "new report" > report.txt
printf 'another line\n' >> report.txt
curl -fsS https://bad.example 2> error.log
command > stdout.log 2> stderr.log
What does 2>&1 mean, and how do you redirect both stdout and stderr? [Intermediate]
Answer
2>&1 means redirect stderr, file descriptor 2, to wherever stdout, file descriptor 1, currently points. To redirect both stdout and stderr to the same file, use command >file 2>&1 or command &>file in Bash.
Technical explanation
Redirection order matters because 2>&1 copies the current destination of stdout at that point.
command >file 2>&1 sends both streams to file; command 2>&1 >file sends stderr to the original stdout and only stdout to file.
For pipelines, use pipefail if you need upstream failures to affect the pipeline status.
Hands-on example
# Correct: both stdout and stderr to deploy.log
./deploy.sh > deploy.log 2>&1
# Bash shorthand:
./deploy.sh &> deploy.log
# Append both:
./deploy.sh >> deploy.log 2>&1
What is /dev/null, and why redirect to it? [Intermediate]
Answer
/dev/null is a special device that discards anything written to it and returns EOF when read. I redirect to it when I intentionally want to suppress output, while still checking the command's exit code.
Technical explanation
It is useful for quiet existence checks, health checks, and commands whose output is irrelevant.
Do not hide errors blindly in production scripts; suppress output only when failures are handled or logged elsewhere.
Redirect stdout only with >/dev/null, stderr only with 2>/dev/null, or both with >/dev/null 2>&1.
Hands-on example
if command -v kubectl >/dev/null 2>&1; then
echo "kubectl is installed"
else
echo "kubectl is missing" >&2
fi
What is a pipe, and how does data flow through a | b | c? [Intermediate]
Answer
A pipe connects the stdout of one command to the stdin of the next command. In a pipeline like a | b | c, data streams left to right through each process.
Technical explanation
Pipelines are ideal for Unix text processing because each command does one job and passes data onward.
By default, the pipeline exit status is usually the last command's exit status; set -o pipefail changes that behavior in Bash.
Each stage runs as a separate process, and shell state changes inside pipeline components may not persist in the parent shell.
Hands-on example
grep 'ERROR' app.log | awk '{print $1}' | sort | uniq -c | sort -rn
set -o pipefail
curl -fsS https://example.com/data | jq '.items[]'
What is the difference between && and || in command chaining? [Intermediate]
Answer
&& runs the next command only if the previous command succeeds. || runs the next command only if the previous command fails. They are useful for simple control flow but should not replace clear if statements for complex logic.
Technical explanation
command1 && command2 means command2 depends on command1 success.
command1 || command2 means command2 is an error handler or fallback.
Be careful with a && b || c as a fake ternary because c can run if b fails, not only if a fails.
Hands-on example
mkdir -p build && go test ./...
kubectl get ns prod >/dev/null 2>&1 || kubectl create ns prod
if deploy; then
notify_success
else
notify_failure
fi
What is the difference between a subshell and the current shell? [Intermediate]
Answer
A subshell is a child shell process, often created with parentheses, pipelines, or command substitution. Changes made in a subshell, such as cd or variable assignments, do not affect the parent shell.
Technical explanation
Use ( commands ) when you want isolation, such as temporarily changing directories.
Use { commands; } when you want grouping in the current shell.
Pipeline behavior differs across shells; do not rely on variable changes inside a pipeline loop persisting unless you understand the shell options involved.
Hands-on example
pwd
(cd /tmp && pwd)
pwd # still original directory
count=0
printf '%s\n' a b | while read -r x; do count=$((count+1)); done
echo "count may still be $count because loop ran in subshell"
What is process substitution (<(...)), and when is it useful? [Intermediate]
Answer
Process substitution, such as <(command), gives a command a file-like path connected to another command's output. It is useful when a tool expects filenames but the data is produced dynamically.
Technical explanation
It avoids temporary files for comparisons, joins, and tools that take file arguments.
It is a Bash feature and may not be available in POSIX sh.
The shell typically implements it with /dev/fd or named pipes depending on the system.
Hands-on example
diff <(sort old.txt) <(sort new.txt)
comm -12 <(sort desired_users.txt) <(cut -d: -f1 /etc/passwd | sort)
kubectl diff -f <(helm template myapp ./chart)
What is the trap command, and how do you use it for cleanup on exit? [Intermediate]
Answer
trap registers commands to run when the shell receives a signal or exits. I use trap for cleanup, such as deleting temporary files, releasing locks, or printing diagnostics on failure.
Technical explanation
trap 'cleanup' EXIT runs cleanup when the script exits for success or failure.
Signals like INT and TERM can be trapped to handle Ctrl+C or termination gracefully.
Keep trap handlers simple and robust because they may run during failure paths.
Hands-on example
tmpdir=$(mktemp -d)
cleanup() {
rm -rf "$tmpdir"
}
trap cleanup EXIT
trap 'echo "interrupted" >&2; exit 130' INT TERM
echo "working in $tmpdir
How would you run cleanup code whether a script succeeds or fails? [Intermediate]
Answer
I run cleanup on both success and failure by registering a cleanup function with trap ... EXIT. The EXIT trap runs whenever the script exits, regardless of whether the exit was caused by success, an error, or an explicit exit.
Technical explanation
Capture the original exit code inside cleanup if the handler needs to log status and preserve the same result.
Use cleanup for temp directories, lock files, background child processes, and partial artifacts.
Avoid masking the original failure by accidentally returning success from cleanup after an error.
Hands-on example
tmpdir=$(mktemp -d)
cleanup() {
rc=$?
rm -rf "$tmpdir"
echo "cleanup complete, rc=$rc" >&2
exit "$rc"
}
trap cleanup EXIT
cp input.txt "$tmpdir/input.txt"
process "$tmpdir/input.txt
How do you create a secure temporary file or directory (mktemp)? [Intermediate]
Answer
I use mktemp to create unpredictable temporary files or directories safely. This avoids race conditions and symlink attacks that can happen with predictable names like /tmp/myfile.$$.
Technical explanation
mktemp creates the file or directory with a unique name and safe permissions.
For multiple files, create a temporary directory with mktemp -d and place files inside it.
Always register cleanup with trap unless the temp artifact must be preserved for debugging.
Hands-on example
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
config="$tmpdir/config.yaml"
cat > "$config" <<'EOF'
name: demo
EOF
run_tool --config "$config
How do you handle errors and provide a useful message on failure? [Intermediate]
Answer
For useful error handling, I fail fast, print clear messages to stderr, include context, preserve exit codes where needed, and validate inputs before doing destructive actions. I often use a die function and explicit checks around critical commands.
Technical explanation
Good errors say what failed, which input was involved, and what the operator can check next.
Logs and errors should go to stderr so stdout remains parseable for callers.
For scripts used in CI, failing with a nonzero code is as important as printing the message.
Hands-on example
die() { echo "ERROR: $*" >&2; exit 1; }
: "${ENVIRONMENT:?ENVIRONMENT is required}"
[[ "$ENVIRONMENT" =~ ^(dev|stage|prod)$ ]] || die "invalid env: $ENVIRONMENT"
kubectl config current-context >/dev/null || die "kubectl is not configured
What is the difference between exit and return? [Intermediate]
Answer
exit terminates the current shell or script process, while return exits only the current function or sourced script. In normal scripts, use return inside functions and exit for final script termination.
Technical explanation
Calling exit inside a sourced file can close the caller's shell, which is dangerous for library files.
return can only be used inside a function or sourced script, not at top level of a normally executed script.
Both accept numeric statuses, and both should preserve meaningful failure codes where possible.
Hands-on example
validate() { [[ -f "$1" ]] || return 2}
main() { validate config.yaml || return $? echo "valid"}
main "$@"exit $?
What are exit codes you should use, and what does a non-zero exit signal to a pipeline? [Intermediate]
Answer
I use 0 for success and nonzero for failure. Common conventions are 1 for general failure, 2 for bad usage, 126 for found but not executable, 127 for command not found, and 128+n for termination by signal n. Pipelines and CI use nonzero status to stop or mark the step as failed.
Technical explanation
Document custom exit codes when scripts are called by automation or other teams.
Do not return arbitrary large numbers because shell statuses wrap to 0-255.
When wrapping a command, preserve its exit code if callers need the exact failure reason.
Hands-on example
usage() { echo "usage: $0 -f file" >&2; exit 2; }
[[ $# -gt 0 ]] || usage
some_command "$@"
rc=$?
if (( rc != 0 )); then
echo "some_command failed with rc=$rc" >&2
exit "$rc"
fi
How do you debug a Bash script (set -x, bash -x, shellcheck)? [Intermediate]
Answer
I debug Bash with bash -x script.sh, set -x around suspicious sections, meaningful logging, ShellCheck, and smaller reproducible inputs. I also customize PS4 to show file, line, and function context in xtrace output.
Technical explanation
set -x prints commands after expansion, which helps reveal quoting and variable issues, but it can leak secrets.
ShellCheck catches many static issues before runtime.
Use trap ERR or explicit error handlers to print context on failures in complex scripts.
Hands-on example
export PS4='+ ${BASH_SOURCE}:${LINENO}:${FUNCNAME[0]:-main}: '
set -x
render_config "$ENVIRONMENT"
set +x
bash -n script.sh # syntax check
shellcheck script.sh # static analysis
What is shellcheck, and what kinds of issues does it catch? [Intermediate]
Answer
ShellCheck is a static analysis tool for shell scripts. It catches quoting bugs, unsafe word splitting, unassigned variables, unreachable code, portability issues, bad test syntax, and many common shell pitfalls.
Technical explanation
It is especially useful because shell scripts often pass simple tests but fail with spaces, empty values, or unusual filenames.
I run it locally, in pre-commit hooks, and as a CI quality gate for automation repositories.
Warnings should be fixed or explicitly disabled with a narrow comment explaining why the code is intentional.
Hands-on example
# Local check
shellcheck scripts/*.sh
# Example CI step
find scripts -name '*.sh' -print0 | xargs -0 shellcheck -x
How do you check whether a command exists before using it? [Intermediate]
Answer
I check whether a command exists with command -v name >/dev/null 2>&1. It is portable and avoids relying on which, which may be external and inconsistent.
Technical explanation
Validate dependencies at script startup so failures are clear and early.
Use a loop for multiple required tools and print installation guidance if helpful.
For optional tools, use command -v to choose a fallback path.
Hands-on example
require() {
command -v "$1" >/dev/null 2>&1 || {
echo "required command not found: $1" >&2
exit 127
}
}
for cmd in kubectl jq curl; do
require "$cmd"
done
How do you loop over files safely when names contain spaces? [Intermediate]
Answer
To loop over files safely, I use globs with quoted variables or find -print0 with read -d ''. This handles spaces, newlines, and special characters in filenames.
Technical explanation
Avoid for f in $(ls) because command substitution and word splitting corrupt filenames.
Use -- before filenames passed to commands so names beginning with - are not treated as options.
Enable nullglob if an unmatched glob should produce no items instead of the literal pattern.
Hands-on example
shopt -s nullglob
for file in ./*.log; do
gzip -- "$file"
done
find /var/log/myapp -type f -name '*.log' -print0 |
while IFS= read -r -d '' file; do
gzip -- "$file"
done
What is the difference between glob expansion and regex in the shell? [Intermediate]
Answer
Glob expansion is shell pathname matching, such as *.log matching files. Regex is pattern matching used by tools or Bash's [[ string =~ regex ]] operator. They have different syntax and different purposes.
Technical explanation
Globs are expanded by the shell before the command runs and match filenames.
Regex is usually interpreted by tools like grep, awk, sed, or by Bash inside [[ =~ ]].
Do not quote a glob when you want the shell to expand it; do quote variables that contain filenames.
Hands-on example
# Glob: files ending in .log
for file in *.log; do echo "$file"; done
# Regex: lines beginning with ERROR and a status code
grep -E '^ERROR [0-9]{3}' app.log
[[ "release-2026" =~ ^release-[0-9]{4}$ ]] && echo match
What is parameter expansion, and what does ${var:-default} do? [Intermediate]
Answer
Parameter expansion transforms or substitutes variable values. ${var:-default} expands to default if var is unset or empty, without assigning the default to var.
Technical explanation
Use ${var-default} if only unset should use the default and empty string should be respected.
Use ${var:=default} to assign the default when unset or empty.
These expansions make scripts safer under set -u and easier to configure through environment variables.
Hands-on example
log_level=${LOG_LEVEL:-info}
timeout_seconds=${TIMEOUT_SECONDS:-30}
echo "log_level=$log_level timeout=$timeout_seconds"
# With set -u, this avoids unbound variable errors for optional inputs.
What does ${var:?message} do, and when would you use it? [Intermediate]
Answer
${var:?message} fails immediately with the message if var is unset or empty. I use it for required inputs such as environment, cluster, database URL, or credentials reference.
Technical explanation
It is a concise precondition check and works well with set -u.
The script exits or returns depending on context, so it prevents continuing with missing critical data.
Use ${var?message} if an empty string is allowed but an unset variable is not.
Hands-on example
#!/usr/bin/env bash
set -euo pipefail
: "${CLUSTER:?CLUSTER is required}"
: "${NAMESPACE:?NAMESPACE is required}"
kubectl --context "$CLUSTER" -n "$NAMESPACE" get pods
What is the difference between ${var%pattern} and ${var#pattern}? [Intermediate]
Answer
${var%pattern} removes the shortest matching suffix, while ${var#pattern} removes the shortest matching prefix. Double versions, %% and ##, remove the longest matching suffix or prefix.
Technical explanation
These patterns are shell globs, not regular expressions.
They are useful for extracting filenames, extensions, directory parts, tags, and prefixes without calling external tools.
Using built-in parameter expansion is faster and avoids quoting issues around command substitution.
Hands-on example
path="/var/log/app/server.log.gz"
echo "dir=${path%/*}" # /var/log/app
echo "file=${path##*/}" # server.log.gz
echo "no_gz=${path%.gz}" # /var/log/app/server.log
echo "ext=${path##*.}" # gz
How do you get the length of a string or the number of elements in an array? [Intermediate]
Answer
${#var} returns the length of a string, and ${#array[@]} returns the number of elements in an array. These are Bash built-ins and do not require external commands.
Technical explanation
For strings, the count is character count in the shell's current handling, not necessarily display width.
For arrays, ${#array[@]} counts elements, while ${#array[index]} gets the length of one element.
Use array length checks before indexing when empty arrays are possible.
Hands-on example
name="orders-api"echo "string length=${#name}"
services=(api worker scheduler)echo "service count=${#services[@]}"echo "first name length=${#services[0]}
How do you declare and iterate over an array in Bash? [Intermediate]
Answer
A Bash indexed array is declared with parentheses, such as arr=(a b c). I iterate with "${arr[@]}" to preserve each element exactly.
Technical explanation
Arrays are Bash-specific and are not portable to plain POSIX sh.
Use "${array[@]}" for elements and ${!array[@]} for indexes.
Quote each element expansion to handle spaces safely.
Hands-on example
services=("orders api" "payments" "worker")
for svc in "${services[@]}"; do
echo "deploying [$svc]"
done
services+=("scheduler")
echo "total=${#services[@]}
What is an associative array, and how do you use one? [Intermediate]
Answer
An associative array is a Bash map from string keys to values. It is declared with declare -A and is useful for lookups such as service-to-port, environment-to-cluster, or counters by key.
Technical explanation
Associative arrays require Bash 4 or newer, so they are not available in old macOS system Bash 3.x unless a newer Bash is installed.
Use ${map[$key]} to read values and ${!map[@]} to iterate keys.
Under set -u, use ${map[$key]:-default} when a key might be absent.
Hands-on example
declare -A port_by_service=(
[api]=8080
[worker]=9090
)
svc="api"
echo "${svc} port is ${port_by_service[$svc]}"
for key in "${!port_by_service[@]}"; do
echo "$key -> ${port_by_service[$key]}"
done
How do you split a string on a delimiter in Bash? [Intermediate]
Answer
For simple delimiters, I split a string by setting IFS and using read -r -a for arrays or read -r field1 field2 for named fields. For complex formats like quoted CSV, I use a real parser instead of Bash splitting.
Technical explanation
IFS splitting is good for simple colon-, comma-, or slash-separated strings without escaping rules.
Use a local IFS assignment on the read command to avoid changing global IFS.
Do not use naive Bash splitting for JSON, YAML, or real CSV with quotes; use jq, yq, Python, or a purpose-built parser.
Hands-on example
csv="api,8080,active"IFS=, read -r name port status <<< "$csv"echo "$name $port $status"
path="a/b/c"IFS=/ read -r -a parts <<< "$path"printf '<%s>\n' "${parts[@]}"
How do you replace text in a variable using parameter expansion? [Intermediate]
Answer
Bash can replace text in a variable using ${var/pattern/replacement} for the first match and ${var//pattern/replacement} for all matches. The pattern is a shell pattern, not a full regex.
Technical explanation
This is useful for lightweight string normalization without spawning sed.
Use # or % anchored variants for prefix or suffix replacements.
For regex-based replacements or file edits, use sed, awk, Perl, or another appropriate tool.
Hands-on example
image="registry.local/orders-api:latest"
echo "${image/latest/$(git rev-parse --short HEAD)}"
name="feature/add login"
safe="${name//[^a-zA-Z0-9._-]/-}"
echo "$safe
What is the difference between cut, awk, and sed for text processing? [Intermediate]
Answer
cut is best for simple fixed delimiters or character/byte ranges, awk is a field-processing language for records, calculations, and conditions, and sed is a stream editor for substitutions and line transformations.
Technical explanation
Use cut when the delimiter and field positions are simple.
Use awk when logic, numeric aggregation, multiple conditions, or formatted output is needed.
Use sed for search/replace, deleting lines, printing ranges, and simple stream edits.
Hands-on example
# cut: get username from passwd
cut -d: -f1 /etc/passwd
# awk: print users with UID >= 1000
awk -F: '$3 >= 1000 {print $1, $3}' /etc/passwd
# sed: replace debug with info
sed 's/debug/info/g' app.conf
How do you extract a specific column from delimited text with cut and with awk? [Intermediate]
Answer
With cut, use -d to choose the delimiter and -f to choose fields. With awk, set the field separator using -F and print the desired field variables like $1 or $3.
Technical explanation
cut is straightforward for single-character delimiters and fixed field positions.
awk handles multiple conditions, output formatting, and more complex parsing.
For CSV with quoted delimiters, neither simple cut nor basic awk is enough; use a CSV-aware parser.
Hands-on example
# /etc/passwd fields are colon-delimited.
cut -d: -f1,3 /etc/passwd
awk -F: '{print $1, $3}' /etc/passwd
# Access log: print IP and status if status is 500.
awk '$9 == 500 {print $1, $9}' access.log
How does awk split lines into fields, and what are $1 and NF? [Intermediate]
Answer
awk reads input as records, normally lines, and splits each record into fields. $1 is the first field, $2 the second, $0 the whole line, and NF is the number of fields in the current record.
Technical explanation
The default field separator is runs of whitespace; use -F to set a custom separator.
awk programs have pattern-action form: condition { action }.
NF is often used to print the last field with $NF or validate record shape.
Hands-on example
echo 'api 200 15ms' | awk '{print $1, $2, $NF}'
awk -F, 'NF != 3 {print "bad row", NR, $0}' data.csv
awk '$9 >= 500 {print $1, $7, $9}' access.log
What is the difference between NR and NF in awk? [Advanced]
Answer
NR is the current input record number, usually the line number across all files. NF is the number of fields in the current record. NR tells where I am; NF tells how many fields the current line has.
Technical explanation
Use NR for skipping headers, printing line numbers, or selecting line ranges.
Use NF for checking column counts or accessing the last field with $NF.
When processing multiple files, FNR is the record number within the current file, while NR is cumulative.
Hands-on example
awk 'NR == 1 {print "header:", $0} NR > 1 {print NR, NF, $NF}' data.txt
# Skip CSV header and print last field.
awk -F, 'NR > 1 {print $NF}' data.csv
How do you use awk to sum or average a column? [Advanced]
Answer
To sum or average a column in awk, accumulate the numeric field in a variable and print the result in the END block. For average, divide by the count of records included.
Technical explanation
awk variables are initialized to zero or empty string automatically.
Use conditions to skip headers, empty rows, or nonnumeric values.
END runs after all records have been processed, making it ideal for totals and summaries.
Hands-on example
# Sum column 3, skipping header.
awk -F, 'NR > 1 {sum += $3} END {print sum}' metrics.csv
# Average column 2 when it is non-empty.
awk -F, 'NR > 1 && $2 != "" {sum += $2; n++} END {if (n) print sum/n}' metrics.csv
How do you use sed to find and replace text in a file (with a backup)? [Advanced]
Answer
I use sed 's/old/new/g' for substitution and sed -i.bak for in-place editing with a backup. The backup makes rollback easy if the replacement is wrong.
Technical explanation
sed without -i writes modified output to stdout and does not change the original file.
sed -i changes the file in place; syntax differs slightly between GNU sed and BSD/macOS sed.
For production edits, test without -i first or write to a temp file and move it into place after validation.
Hands-on example
# Preview first:
sed 's/log_level=debug/log_level=info/g' app.conf
# Edit in place and keep app.conf.bak:
sed -i.bak 's/log_level=debug/log_level=info/g' app.conf
diff -u app.conf.bak app.conf
What is the difference between sed -i and sed without -i? [Advanced]
Answer
sed without -i streams the modified result to stdout and leaves the source file unchanged. sed -i edits the file in place, optionally creating a backup depending on the argument used.
Technical explanation
Without -i is safer for previewing, pipelines, and redirection to a new file.
With -i is convenient but can be risky if the expression is wrong or portability across GNU/BSD sed matters.
In automated scripts, prefer creating a backup or using a temp file plus atomic mv for critical files.
Hands-on example
# Safe preview
sed 's/replicas: 1/replicas: 3/' deploy.yaml > deploy.new.yaml
mv deploy.new.yaml deploy.yaml
# GNU sed in-place with backup
sed -i.bak 's/replicas: 1/replicas: 3/' deploy.yaml
How do you print specific lines of a file with sed or awk? [Advanced]
Answer
With sed, use -n and p to print specific lines or ranges. With awk, use NR conditions. awk is often clearer when line selection depends on numeric conditions or fields.
Technical explanation
sed -n '10,20p' prints lines 10 through 20.
awk 'NR==10' prints a single line; awk 'NR>=10 && NR<=20' prints a range.
For huge files, tools like sed can stop early with q when only early lines are needed.
Hands-on example
sed -n '10,20p' app.log
awk 'NR >= 10 && NR <= 20 {print}' app.log
# Print first match and quit.
sed -n '/ERROR/{p;q;}' app.log
How do you count lines, words, and characters with wc? [Advanced]
Answer
wc counts lines, words, bytes, and characters. wc -l counts lines, wc -w counts words, wc -c counts bytes, and wc -m counts characters.
Technical explanation
wc is commonly used in scripts to validate row counts, estimate file size, and monitor log volume.
For exact line count of a file, use wc -l < file to avoid printing the filename.
Bytes and characters can differ with multibyte encodings such as UTF-8.
Hands-on example
lines=$(wc -l < access.log)words=$(wc -w < notes.txt)bytes=$(wc -c < artifact.tar.gz)echo "lines=$lines words=$words bytes=$bytes"
How would you find the top N most frequent values in a log (sort | uniq -c | sort -rn)? [Advanced]
Answer
To find the top N most frequent values, extract the field, sort it, count adjacent duplicates with uniq -c, sort numerically descending, and take the first N with head.
Technical explanation
uniq only counts adjacent duplicates, which is why sorting before uniq is required.
sort -rn sorts counts numerically in reverse order.
This pattern is very useful for log analysis, such as top IPs, status codes, endpoints, or error messages.
Hands-on example
# Top 10 client IPs in an access log.
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10
# Top HTTP status codes.
awk '{print $9}' access.log | sort | uniq -c | sort -rn
Why must input be sorted before uniq, and what does uniq -c do? [Advanced]
Answer
uniq compares only neighboring lines, so input must be sorted if I want global duplicate counts. uniq -c prefixes each group with the number of repeated adjacent lines.
Technical explanation
Without sorting, the same value appearing in separate parts of a file will be counted as multiple groups.
The typical frequency pipeline is sort | uniq -c | sort -rn.
If preserving original order matters, use awk with a map instead of sorting.
Hands-on example
printf '%s\n' b a b a a | uniq -c
# counts only adjacent duplicates
printf '%s\n' b a b a a | sort | uniq -c | sort -rn
# global frequency counts
How would you write a script to monitor disk usage and alert past a threshold? [Advanced]
Answer
I would collect filesystem usage with df, compare the used percentage to a threshold, and alert through email, Slack, PagerDuty, or a monitoring push endpoint. The script should exclude irrelevant filesystems and exit nonzero if a threshold is breached.
Technical explanation
Use df -P for portable single-line POSIX output and parse the percentage carefully.
Avoid alerting on tmpfs, container overlay, or read-only pseudo-filesystems unless they matter.
In mature environments this should feed Prometheus/node exporter, but a shell script is useful for small systems or emergency checks.
Hands-on example
#!/usr/bin/env bash
set -euo pipefail
threshold=${1:-85}
failed=0
while read -r used mount; do
pct=${used%%%}
if (( pct >= threshold )); then
echo "ALERT: $mount is ${pct}% full" >&2
failed=1
fi
done < <(df -P -x tmpfs -x devtmpfs | awk 'NR>1 {print $5, $6}')
exit "$failed"
How would you write a script to check if a process is running and restart it? [Advanced]
Answer
I would check process state using systemd when available, because it is the service manager and can restart reliably. If systemd is not available, use pgrep with an exact pattern and then start the process with a controlled command.
Technical explanation
Prefer systemctl is-active and systemctl restart for managed services instead of grepping ps output.
If using pgrep, use -f carefully and avoid matching the grep command or unrelated processes.
Log restart attempts and add rate limiting to avoid restart loops.
Hands-on example
#!/usr/bin/env bash
set -euo pipefail
service="nginx"
if ! systemctl is-active --quiet "$service"; then
echo "$service is down; restarting" >&2
systemctl restart "$service"
fi
systemctl is-active --quiet "$service
How would you write a backup script that rotates and keeps only the newest N files? [Advanced]
Answer
A backup rotation script should create a timestamped backup, verify it exists and is non-empty, then delete older backups while keeping the newest N. Sorting by modification time or timestamped names makes retention deterministic.
Technical explanation
Write backups to a temporary file first, then move into place after success to avoid retaining partial backups.
Use find or ls carefully; for robust handling, use null delimiters when filenames can contain spaces.
Retention should be based on business requirements and ideally paired with restore tests.
Hands-on example
#!/usr/bin/env bash
set -euo pipefail
backup_dir=/var/backups/app
keep=7
mkdir -p "$backup_dir"
file="$backup_dir/app-$(date +%Y%m%d-%H%M%S).tar.gz"
tar -czf "$file.tmp" /opt/app
mv "$file.tmp" "$file"
find "$backup_dir" -maxdepth 1 -name 'app-*.tar.gz' -printf '%T@ %p\0' |
sort -z -rn |
tail -z -n +$((keep + 1)) |
cut -z -d' ' -f2- |
xargs -0 -r rm --
How would you write a script to retry a flaky command with a delay? [Advanced]
Answer
I write retries as a loop with a maximum attempt count, delay, and clear logging. The script should return success as soon as the command succeeds and return nonzero after all attempts fail.
Technical explanation
Retries are appropriate for transient failures, not deterministic errors such as invalid credentials or bad syntax.
Use exponential backoff for overloaded remote systems.
Make commands idempotent before retrying them, especially for writes or deployments.
Hands-on example
retry() {
local max=${1}; shift
local delay=${1}; shift
local attempt
for ((attempt=1; attempt<=max; attempt++)); do
"$@" && return 0
echo "attempt $attempt/$max failed: $*" >&2
sleep "$delay"
done
return 1
}
retry 5 3 curl -fsS https://example.com/health
How would you write a health-check script that polls a URL until it returns 200? [Advanced]
Answer
I would poll the URL with curl, require HTTP 200, sleep between attempts, and enforce a timeout or maximum attempts. The script should print diagnostics and exit nonzero if the service never becomes healthy.
Technical explanation
Use curl -fsS to fail on HTTP errors, show errors, and stay quiet on success.
Use --max-time to prevent one request from hanging forever.
For Kubernetes rollouts, prefer native readiness and kubectl rollout status, but URL polling is useful for external checks.
Hands-on example
#!/usr/bin/env bash
set -euo pipefail
url=${1:?url required}
for attempt in {1..30}; do
if code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 3 "$url") && [[ "$code" == "200" ]]; then
echo "healthy"
exit 0
fi
echo "waiting for 200, got ${code:-curl-error}" >&2
sleep 2
done
echo "service did not become healthy: $url" >&2
exit 1
How do you schedule a script with cron, and what do the five cron fields mean? [Advanced]
Answer
I schedule a script with cron by adding an entry to a user's crontab or /etc/cron.d. The five fields are minute, hour, day of month, month, and day of week, followed by the command.
Technical explanation
Cron has a limited environment, so use absolute paths and set required environment variables explicitly.
Redirect output to logs or monitoring; otherwise cron may mail output depending on system configuration.
Use flock or another lock if overlapping runs would be unsafe.
Hands-on example
# Edit current user's crontab
crontab -e
# Run every 5 minutes
*/5 * * * * /usr/local/bin/health-check.sh >> /var/log/health-check.log 2>&1
# Run daily at 02:30
30 2 * * * /usr/local/bin/backup.sh
What is the difference between a cron job and a systemd timer? [Advanced]
Answer
Cron is the traditional time-based scheduler. A systemd timer is integrated with systemd units, logs, dependencies, missed-run handling, randomized delays, and service isolation. On modern Linux servers, I prefer systemd timers for managed operational jobs.
Technical explanation
Cron is simple and widely available, but its environment and logging are minimal.
systemd timers can trigger services, show status with systemctl, log to journald, and run missed jobs with Persistent=true.
For containers and Kubernetes, use the platform's scheduler, such as Kubernetes CronJob, rather than host cron when possible.
Hands-on example
# /etc/systemd/system/backup.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
# /etc/systemd/system/backup.timer
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
[Install]
WantedBy=timers.target
systemctl enable --now backup.timer
How would you make a long-running script run safely in the background? [Advanced]
Answer
For a long-running script, I prefer running it under systemd, supervisord, Kubernetes, or another process manager. If I must run it manually in the background, I use nohup or a terminal multiplexer, redirect logs, store the PID, and handle signals cleanly.
Technical explanation
A process manager provides restart policy, logs, lifecycle control, and dependency ordering.
Manual backgrounding with & is fragile because the process can die when the session closes unless detached correctly.
The script should trap TERM/INT and clean up child processes or lock files.
Hands-on example
# Better: systemd service
systemctl start my-worker.service
journalctl -u my-worker.service -f
# Manual fallback
nohup /usr/local/bin/worker.sh >> /var/log/worker.log 2>&1 &
echo $! > /run/worker.pid
What is idempotency, and how do you make a script safe to run repeatedly? [Advanced]
Answer
Idempotency means a script can be run repeatedly and converge to the same desired state without causing duplicate or destructive side effects. I make scripts idempotent by checking current state before changing it and using declarative or upsert-style operations.
Technical explanation
Idempotency is essential for retries, CI/CD, cron jobs, and incident recovery because commands may run more than once.
Use mkdir -p, install -m, kubectl apply, create-if-missing logic, and atomic file replacement.
Avoid blind append, duplicate creates, and repeated destructive actions without guards.
Hands-on example
ensure_user() {
local user="$1"
if id "$user" >/dev/null 2>&1; then
echo "user exists: $user"
else
useradd --system --no-create-home "$user"
fi
}
ensure_user appsvc
mkdir -p /opt/app/releases
How do you securely handle secrets in a shell script (avoid hardcoding, use env or a vault)? [Advanced]
Answer
I avoid hardcoding secrets in shell scripts. Instead, I pass secrets through a secret manager, short-lived identity, protected environment variables, files with strict permissions, or CI credential binding, and I make sure logs and xtrace do not expose them.
Technical explanation
Secrets should not be committed to Git or printed to stdout/stderr.
Prefer workload identity, OIDC, IAM roles, Vault, AWS Secrets Manager, or Kubernetes Secrets with external secret operators rather than static long-lived tokens.
Disable set -x around secret handling and avoid passing secrets as command-line arguments where they can appear in process listings.
Hands-on example
set +x
password=$(vault kv get -field=password secret/prod/db)
export PGPASSWORD="$password"
psql -h db.example.com -U app -c 'select 1'
unset PGPASSWORD password
set -x # only if debugging is safe afterward
Why should you avoid putting secrets on the command line, and what leaks them? [Advanced]
Answer
Secrets on the command line can leak through process listings, shell history, CI logs, audit logs, error messages, and monitoring agents. I prefer stdin, protected files, environment bindings from a secret manager, or native credential mechanisms.
Technical explanation
Commands like ps can expose arguments to other users on the system depending on permissions and OS settings.
set -x can print expanded command lines containing secrets.
Some tools log full command invocations on failure, so command-line secrets are easy to leak accidentally.
Hands-on example
# Bad: password visible in command arguments
# mysql -u app -pSuperSecret
# Better: protected option file or env/secret manager
cat > "$HOME/.my.cnf" <<EOF
[client]
user=app
password=$MYSQL_PASSWORD
EOF
chmod 600 "$HOME/.my.cnf"
mysql -e 'select 1'
What is Groovy, and where do you most use it in a DevOps context? [Advanced]
Answer
Groovy is a dynamic language for the JVM with Java interoperability and concise scripting features. In DevOps, I most often use it in Jenkins pipelines, Jenkins shared libraries, build automation, and small JVM-based automation tasks.
Technical explanation
Groovy supports closures, maps/lists literals, optional typing, and DSL-style syntax, which is why Jenkins pipelines are readable as code.
Because it runs on the JVM, it can use Java libraries and integrate with Java-based tools.
In Jenkins, Groovy pipeline code is not exactly the same as running normal Groovy because Pipeline uses CPS transformation and sandboxing.
Hands-on example
// Simple Groovy script
def services = ['api', 'worker']
services.each { svc ->
println "deploying ${svc}"
}
// Common DevOps use: Jenkinsfile and shared-library vars/*.groovy files.
How is Groovy used to write Jenkins pipelines? [Advanced]
Answer
Groovy is used in Jenkins to define pipelines as code. Declarative and Scripted Pipeline both use Groovy syntax or Groovy-based DSLs to define stages, steps, agents, environment variables, post actions, and deployment logic.
Technical explanation
A Jenkinsfile is stored with application code so build and deployment behavior is versioned with the repository.
Pipeline steps such as sh, checkout, archiveArtifacts, retry, timeout, and withCredentials are exposed as Groovy-callable DSL steps.
For reusable logic, teams move common code into Jenkins shared libraries written in Groovy.
Hands-on example
pipeline {
agent any
stages {
stage('Test') {
steps {
sh 'npm ci && npm test'
}
}
stage('Build') {
steps {
sh 'docker build -t app:${BUILD_NUMBER} .'
}
}
}
}
What is the difference between a declarative and a scripted Jenkins pipeline in Groovy? [Advanced]
Answer
Declarative Pipeline is a structured, opinionated Jenkins syntax inside a pipeline { } block. Scripted Pipeline is more free-form Groovy using node { } and direct control flow. Declarative is easier to standardize; scripted is more flexible for complex logic.
Technical explanation
Declarative has sections such as agent, environment, stages, steps, post, options, and when.
Scripted Pipeline allows arbitrary Groovy control flow but can become harder to read and govern.
In enterprise CI, I normally use Declarative for consistency and shared libraries for reusable complex behavior.
Hands-on example
// Declarative
pipeline { agent any; stages { stage('Test') { steps { sh 'make test' } } } }
// Scripted
node {
stage('Test') {
sh 'make test'
}
}
How do you define and call a function (or step) in a Jenkins Groovy pipeline? [Advanced]
Answer
In a Jenkinsfile, I can define a Groovy function with def name(args) { ... } or create a reusable shared-library step in vars/name.groovy with def call(...). I call it like any other Pipeline step.
Technical explanation
Small helper functions can live in a Jenkinsfile, but widely reused logic belongs in a shared library.
Pipeline steps inside functions must run in the right Jenkins context, usually inside node/agent execution.
Shared-library steps should accept maps for optional parameters and validate required inputs clearly.
Hands-on example
def notify(String status) {
echo "Build ${env.JOB_NAME} #${env.BUILD_NUMBER}: ${status}"
}
pipeline {
agent any
stages { stage('Test') { steps { sh 'make test' } } }
post {
success { script { notify('SUCCESS') } }
failure { script { notify('FAILURE') } }
}
}
How do you run shell commands from a Groovy pipeline (sh step) and capture output? [Advanced]
Answer
In Jenkins Pipeline, I run shell commands with the sh step. To capture stdout, use returnStdout: true and trim the result. To capture the exit status without failing the build, use returnStatus: true.
Technical explanation
By default, sh fails the stage if the command exits nonzero.
returnStdout captures standard output, but stderr still appears in logs unless redirected.
Always quote shell variables carefully inside Groovy strings because Groovy interpolation and shell expansion are different layers.
Hands-on example
pipeline {
agent any
stages {
stage('Git Info') {
steps {
script {
def sha = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
def rc = sh(script: 'test -f Dockerfile', returnStatus: true)
echo "sha=${sha} dockerfile_rc=${rc}"
}
}
}
}
}
How do you handle errors and retries in a Groovy pipeline (try/catch, retry, timeout)? [Advanced]
Answer
In a Groovy pipeline, I handle errors with try/catch/finally, retry for transient failures, timeout for hung steps, and post blocks for cleanup or notifications. The design should distinguish expected flaky operations from real deterministic failures.
Technical explanation
retry repeats the enclosed block when a step fails, which is useful for network pulls or temporary API failures.
timeout prevents indefinite hangs and frees Jenkins executors.
try/finally or post { always { ... } } ensures cleanup and notifications happen even after failures.
Hands-on example
pipeline {
agent any
stages {
stage('Deploy') {
steps {
script {
timeout(time: 10, unit: 'MINUTES') {
retry(3) {
sh './deploy.sh prod'
}
}
}
}
}
}
post { always { cleanWs() } }
}
How do you use environment variables and credentials in a Groovy pipeline? [Advanced]
Answer
I use the environment block for non-secret environment variables and withCredentials for secrets. Jenkins credentials should be scoped to the smallest required block and never printed or interpolated into logs unnecessarily.
Technical explanation
environment variables in Declarative Pipeline are available to steps in the pipeline or stage scope.
withCredentials binds secrets temporarily and masks them in logs when used correctly.
Avoid Groovy string interpolation of secrets in command strings; prefer single-quoted shell scripts so expansion happens in the shell with masking.
Hands-on example
pipeline {
agent any
environment {
REGISTRY = 'registry.example.com'
}
stages {
stage('Push') {
steps {
withCredentials([usernamePassword(credentialsId: 'registry-creds', usernameVariable: 'USER', passwordVariable: 'PASS')]) {
sh 'echo "$PASS" | docker login "$REGISTRY" -u "$USER" --password-stdin'
}
}
}
}
}
How do you run stages in parallel in a Jenkins Groovy pipeline? [Advanced]
Answer
In Declarative Pipeline, I use a parallel block inside a stage to run independent stages at the same time. In Scripted Pipeline, I use the parallel step with a map of branch names to closures.
Technical explanation
Parallelism reduces feedback time for independent test suites, scans, and platform checks.
Branches should not write to the same workspace paths unless isolated, because that creates race conditions.
Use failFast when it is better to stop all parallel branches after one critical failure.
Hands-on example
pipeline {
agent any
stages {
stage('Parallel Checks') {
parallel {
stage('Unit') { steps { sh 'make test-unit' } }
stage('Lint') { steps { sh 'make lint' } }
stage('Security') { steps { sh 'make scan' } }
}
}
}
}
What is the difference between Groovy and Java for scripting purposes? [Advanced]
Answer
Groovy runs on the JVM and interoperates with Java, but it is more concise and dynamic for scripting. It has optional typing, closures, literal maps/lists, GDK helpers, and DSL-friendly syntax. Java is stricter and better for large compiled applications; Groovy is convenient for automation and pipeline DSLs.
Technical explanation
Groovy can call Java classes directly and can be statically compiled when desired.
Dynamic typing and metaprogramming make scripts shorter but can move some errors from compile time to runtime.
In Jenkins, Groovy is further constrained by Pipeline CPS and sandbox behavior, so not every normal Groovy pattern is safe in Jenkinsfiles.
Hands-on example
// Groovy is concise for collections.
def ports = [api: 8080, worker: 9090]
ports.each { name, port -> println "${name} -> ${port}" }
// Java would require more boilerplate for the same small automation task.
When would you choose Bash versus Groovy versus Python for an automation task? [Advanced]
Answer
I choose Bash for simple OS orchestration and command glue, Groovy for Jenkins pipeline logic and JVM-integrated automation, and Python for larger automation that needs strong libraries, data structures, testing, APIs, or complex parsing.
Technical explanation
Bash is best when most work is external commands and the logic is small.
Groovy is best when the execution environment is Jenkins or Java/JVM tooling.
Python is better for JSON/YAML processing, APIs, SDKs, complex error handling, unit tests, and maintainable larger programs.
Hands-on example
Decision examples:- Rotate simple log files on a host: Bash.- Standardize Jenkins CI stages across teams: Groovy shared library.- Reconcile thousands of cloud resources via APIs: Python with SDKs, tests, and retries.
What recent automation script have you written that saved significant manual effort? [Advanced]
Answer
A strong example is a Bash/Groovy automation that replaced manual deployment validation. It collected the target environment, validated Kubernetes context, checked image tags, ran smoke tests, summarized rollout status, and posted the result back to Jenkins or Slack. That saved repeated manual checks and reduced missed validation steps.
Technical explanation
In an interview, I would explain the original pain, the script design, the safeguards, and measurable impact.
The important technical points are idempotency, clear failure modes, logging, retries, and integration into CI/CD.
Good examples include backup validation, log triage, disk cleanup, certificate expiry checks, pipeline standardization, or automated rollback checks.
Hands-on example
Example implementation outline:
1. Jenkins Groovy stage collects service, namespace, and image tag.
2. Bash script runs kubectl rollout status with timeout.
3. It polls /health until HTTP 200 or fails after 30 attempts.
4. It prints a compact summary and exits nonzero on failure.
5. Jenkins post block sends success/failure notification with logs attached.
How do you make a script portable across different shells and systems? [Advanced]
Answer
To make a script portable, I declare the intended shell, avoid non-portable features when targeting sh, check required commands, use POSIX-compatible options where possible, avoid hardcoded paths, handle GNU versus BSD differences, and test in the target environments.
Technical explanation
If the script requires Bash features, say so with the shebang and version checks instead of pretending it is portable sh.
Use command -v, mktemp, POSIX flags, and defensive quoting to reduce environment assumptions.
Containerized test runs are useful for validating behavior on Ubuntu, Alpine, RHEL-like images, and macOS where relevant.
Hands-on example
#!/usr/bin/env bash
set -euo pipefail
if (( BASH_VERSINFO[0] < 4 )); then
echo "Bash 4+ required" >&2
exit 2
fi
for cmd in awk sed curl; do
command -v "$cmd" >/dev/null || { echo "missing $cmd" >&2; exit 127; }
done
How would you structure a large script into reusable functions and a main entry point? [Advanced]
Answer
I structure a large script with a small main function, focused helper functions, clear global constants, argument parsing, validation, logging, cleanup traps, and a final main "$@" entry point. This makes the script testable and readable.
Technical explanation
Keep functions single-purpose, such as parse_args, validate_inputs, acquire_lock, deploy, verify, and cleanup.
Avoid running substantial logic at import/source time if the file may be reused as a library.
For very large logic, move to Python or another maintainable language with unit tests and packages.
Hands-on example
#!/usr/bin/env bash
set -euo pipefail
log() { printf '[%s] %s\n' "$(date +%FT%T%z)" "$*"; }
die() { log "ERROR: $*" >&2; exit 1; }
parse_args() {
ENVIRONMENT=${1:-}
[[ -n "$ENVIRONMENT" ]] || die "env required"
}
validate() { command -v kubectl >/dev/null || die "kubectl missing"; }
run() { log "deploying to $ENVIRONMENT"; }
main() { parse_args "$@"; validate; run; }
main "$@"
How do you write a dry-run mode into an automation script, and why is it valuable? [Advanced]
Answer
Dry-run mode shows what the script would change without actually changing it. It is valuable because operators can review planned actions, CI can validate behavior safely, and risky automation can be tested before production execution.
Technical explanation
Implement dry-run by wrapping mutating commands in a function that either prints or executes them.
Read-only validation should still run during dry-run so missing prerequisites are caught.
The output should be explicit enough for review, including target environment, resources, and commands or API actions that would run.
Hands-on example
dry_run=false
[[ "${1:-}" == "--dry-run" ]] && dry_run=true
run_cmd() {
if $dry_run; then
printf 'DRY-RUN: '
printf '%q ' "$@"
printf '\n'
else
"$@"
fi
}
run_cmd kubectl -n prod rollout restart deployment/orders-api
run_cmd kubectl -n prod rollout status deployment/orders-api --timeout=5m
Source Notes
GNU Bash Reference Manual - shell expansions: https://www.gnu.org/software/bash/manual/html_node/Shell-Expansions.html
GNU Bash Reference Manual - shell parameters: https://www.gnu.org/software/bash/manual/html_node/Shell-Parameters.html
GNU Bash Reference Manual - top/reference: https://www.gnu.org/software/bash/manual/html_node/index.html
Apache Groovy documentation: https://groovy-lang.org/documentation.html
Apache Groovy closures: https://groovy-lang.org/closures.html
Jenkins Pipeline syntax: https://www.jenkins.io/doc/book/pipeline/syntax/
Jenkins Pipeline getting started: https://www.jenkins.io/doc/book/pipeline/getting-started/
Jenkins Pipeline CPS method mismatches: https://www.jenkins.io/doc/book/pipeline/cps-method-mismatches/
Jenkins Pipeline steps reference - Groovy: https://www.jenkins.io/doc/pipeline/steps/groovy/
Jenkins Pipeline: Groovy plugin steps: https://www.jenkins.io/doc/pipeline/steps/workflow-cps/