1. Functions and modularity โ
๐ฏ What you'll learn in this chapter:
- Declaring and calling a function, and passing arguments
- Returning a value via
returnandecho(the two are different!)localvariables and scope logic- Writing reusable libraries (loading via
source)- A real example โ a small logging library
โฑ Time: ~25 minutes reading + exercises ๐งช Exercises:
bashlings watchโ 6 interactive exercises ready (exercises/06_functions/)
1.1. Why functions? โ
Imagine your script does the same work in three places: it writes a log, stamps the time, prints a colored line. Writing 4 lines in each place violates the DRY (Don't Repeat Yourself) principle.
A function is a named block of code. You write it once and call it many times.
# BAD โ repeated in three places
echo "[$(date +%T)] Backup boshlandi"
# ... work ...
echo "[$(date +%T)] Database eksport qilindi"
# ... work ...
echo "[$(date +%T)] Yakunlandi"
# GOOD โ via a function
log() {
echo "[$(date +%T)] $*"
}
log "Backup boshlandi"
log "Database eksport qilindi"
log "Yakunlandi"Advantages of functions
- Brevity โ the code shrinks 3ร
- Single source of truth โ change the format in one place
- Easy to test โ each function can be tested separately
- Easy to read โ
log "..."states the intent by itself
1.2. Declaration syntax โ
In Bash, a function can be declared in two forms:
A. With the function keyword โ
function salomlash() {
echo "Salom!"
}B. POSIX form (with parentheses) โ
salomlash() {
echo "Salom!"
}Both work. The POSIX form is recommended โ because:
- It also works in other POSIX shells (sh, dash)
- It's shorter
- The
functionkeyword is unnecessary noise
The parentheses are empty โ they're called arguments, but aren't used
The () inside salomlash() indicates that it's a function. Arguments are not given here; they're used inside via $1, $2, ...
Calling โ
You call a function by its name, without parentheses:
salomlash # โ
correct
salomlash() # โ error โ syntax is broken1.3. The first complete example โ
#!/usr/bin/env bash
# Function declaration
salomlash() {
echo "Salom, Bash dunyosi!"
echo "Bugun: $(date +%F)"
}
# Main logic
echo "=== Skript boshlandi ==="
salomlash
echo "=== Skript yakunlandi ==="Result:
=== Skript boshlandi ===
Salom, Bash dunyosi!
Bugun: 2026-05-16
=== Skript yakunlandi ===Declaration order matters!
You must declare a function before using it. Bash reads scripts from top to bottom.
salomlash # โ ERROR โ not yet declared
salomlash() { echo "Salom"; }
salomlash # โ
correct1.4. Arguments โ
Arguments are passed to a function just like passing arguments to a script โ via $1, $2, ...
salomlash() {
echo "Salom, $1!"
echo "Yoshingiz: $2"
}
salomlash "Ali" 25
# Salom, Ali!
# Yoshingiz: 25The main special variables โ
| Variable | Meaning |
|---|---|
$1, $2, โฆ | Positional arguments |
$# | The number of arguments |
$@ | All arguments (separately โ each one its own element) |
$* | All arguments (a single string) |
$0 | The script name (NOT the function name โ beware!) |
${FUNCNAME[0]} | The current function name |
The difference between $@ and $*
example() {
for arg in "$@"; do echo "@ qoldirdi: $arg"; done
for arg in "$*"; do echo "* qoldirdi: $arg"; done
}
example "salom dunyo" "bash"Result:
@ qoldirdi: salom dunyo
@ qoldirdi: bash
* qoldirdi: salom dunyo bash # joined together!Almost always use "$@".
Example: the sum of two numbers โ
yigindi() {
echo $(($1 + $2))
}
yigindi 5 7 # 12
yigindi 100 200 # 3001.5. Default values and validation โ
Default value โ ${var:-default} โ
salomlash() {
local ism="${1:-Anonim}"
echo "Salom, $ism!"
}
salomlash "Ali" # Salom, Ali!
salomlash # Salom, Anonim!Required argument โ ${var:?error message} โ
backup() {
local src="${1:?manba katalog ko'rsatilmagan}"
echo "Backup boshlandi: $src"
}
backup # ERROR: source directory not specified
backup ~/docs # Backup boshlandi: /Users/mac/docsThe parameter expansion set
| Syntax | Meaning |
|---|---|
${var:-default} | If empty, use default, but doesn't set it |
${var:=default} | If empty, use default and sets it |
${var:?message} | If empty, stops with an error |
${var:+replace} | If not empty, returns the replacement value |
Validating arguments manually โ
backup() {
if [[ $# -lt 2 ]]; then
echo "Foydalanish: backup <manba> <maqsad>" >&2
return 1
fi
local src="$1"
local dst="$2"
if [[ ! -d "$src" ]]; then
echo "โ Manba topilmadi: $src" >&2
return 1
fi
echo "โ
$src โ $dst"
}1.6. Return values โ return vs echo โ
The most confusing aspect of Bash โ returning a value from a function.
return โ only an exit code (0..255) โ
return is not the return of other languages. It only returns an exit code: 0 (success) or 1..255 (error).
fayl_bormi() {
[[ -f "$1" ]] && return 0 || return 1
}
if fayl_bormi "/etc/passwd"; then
echo "Mavjud"
fi256 or more โ ERROR
qaytar() { return 256; }
qaytar
echo $? # 0 โ wrap around!echo โ returning a real value โ
If you want to return a real value (a number, a string), echo it. The caller gets it with $(...).
kvadrat() {
local n="$1"
echo $((n * n))
}
natija=$(kvadrat 5)
echo "5 ning kvadrati: $natija"
# 5 ning kvadrati: 25Using both together โ
parse_yosh() {
local yosh="$1"
if [[ ! "$yosh" =~ ^[0-9]+$ ]]; then
echo "noto'g'ri yosh" >&2
return 1
fi
echo "$yosh"
return 0
}
if y=$(parse_yosh "$1"); then
echo "โ
Yosh: $y"
else
echo "โ Parsing xato bo'ldi"
fiStdout vs stderr
A function should write its error message to stderr (>&2), and the value it returns to stdout. Otherwise $(...) will also swallow the error message.
1.7. Scope: local vs global โ
By default, in Bash all variables are global. This is dangerous.
salomlash() {
ism="Ali" # GLOBAL โ visible outside the function too!
echo "Salom, $ism"
}
ism="Vali"
salomlash
echo "Tashqarida: $ism" # "Ali" โ overwritten!The fix: local โ
salomlash() {
local ism="Ali" # ONLY inside the function
echo "Salom, $ism"
}
ism="Vali"
salomlash
echo "Tashqarida: $ism" # "Vali" โ preservedAlways use local!
Add local for every new variable inside a function. This is safety and isolation.
The subtle sides of local โ
# Adding several locals
fn() {
local a b c
local x="bir" y="ikki"
}
# `local` swallows the exit code โ this can be buggy
fn() {
local result=$(maybe_fail) # โ ๏ธ even if maybe_fail fails, $? = 0
}
# The correct way:
fn() {
local result
result=$(maybe_fail) # now $? is correct
}1.8. readonly โ constants โ
readonly MAX_RETRIES=3
readonly LOG_FILE="/var/log/app.log"
MAX_RETRIES=5 # ERROR: readonly variableProject-level constants
Declare readonly variables at the start of your script โ this defines the flow of the code:
readonly SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly LOG_FILE="${LOG_FILE:-/tmp/app.log}"1.9. A function library โ source โ
You can move functions into another file and reuse them over and over.
lib/log.sh โ a reusable module โ
# lib/log.sh
log_info() { printf '\033[32m[INFO]\033[0m %s\n' "$*"; }
log_warn() { printf '\033[33m[WARN]\033[0m %s\n' "$*" >&2; }
log_error() { printf '\033[31m[ERROR]\033[0m %s\n' "$*" >&2; }
die() {
log_error "$*"
exit 1
}The main script: main.sh โ
#!/usr/bin/env bash
set -euo pipefail
# Loading the module โ there are two ways:
source "$(dirname "$0")/lib/log.sh"
# or shorter:
. "$(dirname "$0")/lib/log.sh"
log_info "Skript ishga tushdi"
log_warn "Disk hajmi past"
log_error "Database ulanishi yo'q"
die "Kritik xato โ to'xtatamiz"When does source differ from bash?
bash script.shโ a new subshell opens. The functions don't escape to the outside.source script.shโ runs in the current shell. The functions get loaded.
For a library, always use source (or .).
Idiomatic โ load a module only once (guard) โ
# at the start of lib/log.sh
[[ -n "${__LOG_LOADED:-}" ]] && return 0
readonly __LOG_LOADED=1
log_info() { ... }
# ...The equivalent of #ifndef GUARD in C/C++.
1.10. Naming conventions and best practices โ
| Recommendation | Example |
|---|---|
Use snake_case | log_info, parse_args |
Internal functions should start with _ | _private_helper |
| Verb + noun | get_user, check_disk |
Boolean โ is_* or has_* | is_root, has_internet |
| Module prefix | db_connect, db_close |
A sample of a fully documented function โ
# Checks whether the user is root.
#
# Arguments:
# none
# Returns:
# 0 โ root
# 1 โ another user
# Example:
# if is_root; then ...
is_root() {
[[ $EUID -eq 0 ]]
}1.11. A real example โ a logging library โ
A fully reusable module:
# lib/logger.sh
#
# Usage:
# source lib/logger.sh
# log_info "Boshlandi"
# log_warn "Disk past"
# log_error "Xato"
# LOG_LEVEL=debug log_debug "Detal"
[[ -n "${__LOGGER_LOADED:-}" ]] && return 0
readonly __LOGGER_LOADED=1
# Configuration (the user can override it)
LOG_LEVEL="${LOG_LEVEL:-info}"
LOG_FILE="${LOG_FILE:-}"
readonly __LOG_RED='\033[31m'
readonly __LOG_YELLOW='\033[33m'
readonly __LOG_GREEN='\033[32m'
readonly __LOG_BLUE='\033[34m'
readonly __LOG_RESET='\033[0m'
# Level numbers
declare -A __LOG_LEVELS=(
[debug]=0 [info]=1 [warn]=2 [error]=3
)
_log() {
local level="$1"; shift
local color="$1"; shift
local msg="$*"
# Level filtering
local req="${__LOG_LEVELS[$level]:-1}"
local cur="${__LOG_LEVELS[$LOG_LEVEL]:-1}"
(( req < cur )) && return 0
local ts
ts=$(date '+%Y-%m-%d %H:%M:%S')
local line
line=$(printf '[%s] [%-5s] %s' "$ts" "${level^^}" "$msg")
printf '%b%s%b\n' "$color" "$line" "$__LOG_RESET" >&2
[[ -n "$LOG_FILE" ]] && printf '%s\n' "$line" >> "$LOG_FILE"
}
log_debug() { _log debug "$__LOG_BLUE" "$@"; }
log_info() { _log info "$__LOG_GREEN" "$@"; }
log_warn() { _log warn "$__LOG_YELLOW" "$@"; }
log_error() { _log error "$__LOG_RED" "$@"; }
die() {
log_error "$@"
exit 1
}Using it:
#!/usr/bin/env bash
set -euo pipefail
source ./lib/logger.sh
LOG_FILE=/tmp/app.log
log_info "Boshlandi"
log_warn "Disk 80% to'lgan"
log_error "Database javob bermayapti"
[[ -f /etc/critical ]] || die "Kritik fayl yo'q"What does this lib do?
- 4 levels: debug/info/warn/error
- Filtering via
LOG_LEVEL - If
LOG_FILEis set โ it also writes to a file - Colors to the terminal, plain text to the file
- A single "guard" โ it won't be sourced multiple times
- Everything is in
local, the scope is clean
1.12. Common mistakes โ
Classic traps in functions
Forgetting
local.bashfn() { x=5; } # x stays global!localswallows the exit code.bashfn() { local r=$(may_fail); echo $?; } # always 0 # Correct: fn() { local r; r=$(may_fail); echo $?; }returnis not a number.bashreturn "xato" # ERROR โ only 0..255A function name and a variable name colliding.
bashlog=anything # now you potentially break the function named log tooCalling a function while it's not yet declared.
bashfn # โ fn() { ...}Using
echoinstead ofprintf.echo -eis not portable. If you need formatting, useprintf.Forgetting to write to stderr. An error message should be in
>&2, not in stdout.
1.13. Exercises โ
๐งช Bashlings โ interactive exercises
This chapter's 6 exercises come with auto-checking via the bashlings CLI:
bashlings watch # start from the first pending exercise
bashlings run func1 # check a single exercise
bashlings hint func1 # step-by-step hintSource: exercises/06_functions/
And try the following conceptual exercises by hand yourself:
- Write an
is_evenfunction โ whether a number is even or odd (return 0/return 1). - A
repeat_wordfunction โ print the word in the first argument as many times as the number in the second argument. max3โechothe largest of three numbers to stdout.prompt_confirmโ ask the user "Davom etamizmi? [y/N]" and return 0 or 1 depending on the answer.- Create a
lib/math.shmodule with the functionssum,mul,pow.sourceit in your main script and try it out.
1.14. Summary โ
| Concept | Key point |
|---|---|
| Declaration | name() { ... } โ POSIX form recommended |
| Arguments | $1, $@, $# โ always "$@" |
| Default value | ${1:-default} |
return | Only 0..255 โ an exit code |
echo | For returning a real value |
local | Mandatory for every variable inside a function |
readonly | For constants |
source / . | Load a library into the current shell |
| Naming convention | snake_case, is_*/has_*, module prefix |
| Stderr | Error messages to >&2 |
๐ The first capstone is approaching โ in the next chapter we'll write even more powerful functions with arrays.
Next page: 2. Arrays and dictionaries โ