You wrote a Bash function that returned the value you were trying to see. Then, you started searching through variables, only to find that none of them contained anything. This can happen to anyone. The problem is not with your function (it works exactly as designed) or with Bash itself (it behaves the way it is supposed to). What many people do not realize at first is that return in Bash does not return strings like it does in many other programming languages; instead, it returns an exit status code. That single misunderstanding is probably behind most of the Bash-function-related issues people continue to run into.
If you have written more than a few Bash scripts and keep getting stuck whenever a function needs to accept input or return something useful, this article will help. By the end, you will understand how to pass values into a function, what return actually means in Bash, and how to properly retrieve real values from a Bash function.
The Bug That Looks Like Bash Is Broken:
You have a function that calculates a number or builds a string. You call it, store it in a variable, and the variable is either empty or holding a stubborn 0 or 1. Here is what people usually see:
- The variable is empty even though the function clearly printed the answer to the screen
return 42shows up in$?but never lands in your variable- Returning a word throws
numeric argument requiredand stops everything
If any of that feels familiar, you are not missing a comma somewhere. You are fighting how bash treats functions, so let us fix the mental model first.
Most of this clicks once you stop thinking of a bash function as a Python function and start thinking of it as a tiny command. If you want the groundwork on structuring functions cleanly, the guide on writing reusable bash functions pairs well with everything below.
Bash Function Arguments and Return Values
Why Bash Functions Trip Up Everyone at First
A bash function is not like a function in most languages. It behaves like a small command you defined yourself. That single shift explains nearly all the confusion.
It takes input the way a script does, through positional arguments such as $1 and $2. And it gives feedback the way a command does, through an exit status. There is no typed value sitting in a return slot waiting for you. A function really talks to the rest of your script over two completely separate channels, and mixing them up is where things fall apart.
Note:
A function communicates through two channels. The first is the exit status, a number from 0 to 255 you read with $?, where 0 means success. The second is whatever it prints to standard output, which you grab with command substitution like var=$(myfunc). Keep those two apart and most function trouble just disappears.
How Arguments Actually Get Into a Function
You do not declare parameters inside the parentheses. The () after the name stays empty, always. It is just syntax that tells bash this is a function.
Inside the function, the arguments show up as $1, $2, and so on. $# gives you how many were passed, and $@ is all of them together. You call the function like a command, with the arguments simply listed after the name.
LinuxTeck.com
# A function that uses the arguments handed to it
greet() {
echo "Hello, $1"
echo "You passed $# argument(s)"
}
greet "Ravi"
greet "Ravi" "Kumar"
You passed 1 argument(s)
Hello, Ravi
You passed 2 argument(s)
One thing that catches people: $0 inside a function is still the script name, not the function name. So do not reach for $0 expecting greet. The numbered arguments start at $1 and that is what you work with.
What return Really Does (It Is Not What You Think)
return sets the exit status and nothing else. The number you give it has to land between 0 and 255, and 0 means success by convention. Use it to say whether the function worked, not to hand back data.
LinuxTeck.com
# return sets the exit status, it does not hand back data
is_even() {
if [ $(( ${1:-0} % 2 )) -eq 0 ]; then
return 0
else
return 1
fi
}
is_even 4
echo "Exit status: $?"
is_even 7
echo "Exit status: $?"
Exit status: 1
Notice you read the result through $? right after the call, not by capturing the function into a variable. This is the same model the shell uses for every command, which is exactly why understanding exit codes and error handling makes functions feel natural.
Getting Real Values Out the Right Way
If return is only for status, how do you get an actual value back? You print it with echo and capture it with command substitution. That is the whole pattern. Once it clicks you will use it constantly.
LinuxTeck.com
# Capture what a function prints with command substitution
get_date() {
echo "$(date +%Y-%m-%d)"
}
today=$(get_date)
echo "Today is $today"
The same approach works for calculated values. Do the math inside, echo the result, capture it outside. Using local here keeps the variable scoped to the function so it does not leak into the rest of your script.
LinuxTeck.com
# Hand back a calculated value the correct way
add() {
local sum=$(( $1 + $2 ))
echo "$sum"
}
result=$(add 12 30)
echo "12 + 30 = $result"
Sometimes you do not want a value at all, you want a yes or no answer to drive an if. That is where return shines, because the exit status plugs straight into a condition with no $? needed.
LinuxTeck.com
# Use the exit status straight inside a condition
file_exists() {
if [ -f "$1" ]; then
return 0
fi
return 1
}
if file_exists "/etc/hostname"; then
echo "File is there"
else
echo "File is missing"
fi
Here is a more realistic one. A function that builds a backup filename and hands it back as a string so the rest of the script can use it.
LinuxTeck.com
# Build a timestamped backup name and use it later
make_backup_name() {
local prefix="$1"
echo "${prefix}_$(date +%Y%m%d_%H%M%S).tar.gz"
}
name=$(make_backup_name "webroot")
echo "Creating archive: $name"
Now the mistake I made years ago, the one that sends people into a half hour of confusion.
LinuxTeck.com
# WRONG: return cannot hand back a string
get_name() {
return "LinuxTeck"
}
name=$(get_name)
echo "Name is $name"
Name is
Common Mistake:
Writing return "LinuxTeck" to pass a string back. It fails with numeric argument required because return only accepts a number from 0 to 255, and your variable ends up empty.
Fix: print the value instead and capture it, echo "LinuxTeck" inside the function and name=$(get_name) outside. Command substitution grabs whatever the function wrote to standard output, which is how strings actually travel back.
Tip:
If you need debug messages from a function you are capturing, send them to standard error with >&2, like echo "checking input" >&2. Command substitution only grabs standard output, so your debug lines stay visible on screen without poisoning the value you are trying to capture.
Passing Many Arguments and Looping Over Them
When a function takes a list, you loop over "$@". Keep the quotes. Quoted, it keeps each argument intact even when one has spaces in it. Unquoted, bash splits everything on spaces and a filename like my report.txt becomes two separate arguments, which is a classic silent bug.
LinuxTeck.com
# Loop over every argument the function received
list_files() {
for f in "$@"; do
echo "Checking: $f"
done
echo "Total: $# files"
}
list_files report.txt data.csv notes.md
Checking: data.csv
Checking: notes.md
Total: 3 files
$# still tracks the count, so you can validate before doing any work. If you ever need to walk through arguments one at a time and drop them as you go, shift removes the first argument and renumbers the rest.
Where This Bites You in Real Scripts
Real scripts use both channels at once. The function returns a status that says whether it worked, and it prints a payload you capture if it did. Here is a small deploy helper doing exactly that.
LinuxTeck.com
# A deploy helper that returns a STATUS and prints a RESULT
deploy() {
local app="$1"
local env="$2"
if [ -z "$app" ] || [ -z "$env" ]; then
echo "missing-args"
return 1
fi
echo "${app}-deployed-to-${env}"
return 0
}
if result=$(deploy "billing" "staging"); then
echo "OK: $result"
else
echo "Failed: $result"
fi
Look at the if result=$(deploy ...) line. It captures the printed result into a variable and checks the exit status in the same breath. That pattern is everywhere in production scripts because it lets one function report success and carry data without you juggling $? by hand.
Once this is solid, a lot opens up. You can write functions that validate input and slot straight into conditionals, build helpers that hand back generated filenames or paths, and put together an automated backup script where each function reports what it actually created instead of leaving you guessing.
What breaks without it is quieter and meaner. People capture an exit code thinking it is data, so a variable that should hold a filename holds 0. Functions echo stray debug lines that get swallowed into the captured value. Cron jobs fail without anyone noticing because nobody checked the status the function was trying to report. None of these throw loud errors, which is exactly why they waste hours.
Questions I Get Asked About This All the Time
Why is my variable empty when the function clearly printed the value?
Almost always one of three things. You used return instead of echo to send the value back, or you forgot the command substitution syntax $( ) around the call so nothing got captured, or the function printed an extra blank line that pushed your value somewhere you did not expect. Check that the function echoes the value and that you wrote var=$(func).
Can a bash function return a string at all?
Not with return. That keyword is for a numeric exit status only, 0 to 255. To send a string back you echo it and capture it with command substitution. It feels odd coming from other languages, but it is the standard bash way and it works perfectly once you expect it.
Do I have to list the arguments inside the parentheses like other languages?
No, and bash will ignore you if you try. The () stays empty. Inside the function you reach the arguments through $1, $2, and the rest, in the order they were passed.
Why does return 300 give me 44 in $?
Because exit status wraps around at 256. 300 minus 256 is 44, so that is what you get. Keep return values inside 0 to 255, and honestly keep them to 0 for success and a small number for specific failures.
How do I get more than one value out of a function?
A few ways. Echo them separated by spaces or newlines and read them into separate variables, or echo a delimited string and split it afterward. For anything beyond two or three values, returning them through a delimited string keeps it readable.
My debug echo keeps ending up inside my captured variable. How do I stop that?
Send the debug line to standard error instead of standard output. Add >&2 to the echo, like echo "starting" >&2. Command substitution only captures standard output, so the message shows on screen but stays out of your variable.
Summary
Now that you have this working, the rest of your scripts get a lot cleaner. Once bash function arguments and return values stop being a mystery, you stop fighting empty variables and start writing functions that actually report back. Remember the split: return for status, echo plus capture for data, and "$@" when you are handling a list.
From here it is worth getting comfortable with how functions sit inside conditional statements, since that is where the exit status really earns its keep. If you want the precise rules straight from the source, the GNU Bash Manual covers function behavior in full detail.
Related Articles
- What Is Bash Scripting in Linux (Part 1 of 34)
- Bash For Loop Examples in Linux (Part 18 / 34)
- Using the read Command for User Input (Part 7 of 34)
- Bash While and Until Loops (Part 19 / 34)
- Bash Scripting for Automation in 2026
Learn step-by-step how to automate Linux tasks with real-world scripts and practical examples.