Skip to main content
The filesystem module provides functions for path manipulation, file operations, directory management, and file monitoring.

Path manipulation

Pure string operations for working with file paths — no filesystem access required.
Join path components.
fs.sh:12
fs::path::join() {
    local result="$1"; shift
    for part in "$@"; do
        part="${part#/}"   # strip leading slash from each part
        result="${result%/}/$part"
    done
    echo "$result"
}
Usage:
path=$(fs::path::join /home user documents file.txt)
echo "$path"  # /home/user/documents/file.txt
Get filename from path (like basename).
fs.sh:22
fs::path::basename() {
    echo "${1##*/}"
}
Usage:
fs::path::basename /path/to/file.txt  # file.txt
Get directory from path (like dirname).
fs.sh:27
fs::path::dirname() {
    local p="${1%/*}"
    [[ "$p" == "$1" ]] && echo "." || echo "$p"
}
Usage:
fs::path::dirname /path/to/file.txt  # /path/to
Get file extension (without dot).
fs.sh:34
fs::path::extension() {
    local base="${1##*/}"
    [[ "$base" == *.* ]] && echo "${base##*.}" || echo ""
}
Usage:
fs::path::extension file.tar.gz  # gz
Get all extensions for multi-part extensions.
fs.sh:41
fs::path::extensions() {
    local base="${1##*/}"
    [[ "$base" == *.* ]] && echo "${base#*.}" || echo ""
}
Usage:
fs::path::extensions file.tar.gz  # tar.gz
Strip extension from filename.
fs.sh:47
fs::path::stem() {
    local base="${1##*/}"
    [[ "$base" == *.* ]] && echo "${base%.*}" || echo "$base"
}
Usage:
fs::path::stem file.tar.gz  # file.tar
Get absolute path (resolves . and .. without requiring the path to exist).
fs.sh:53
fs::path::absolute() {
    local p="$1"
    if [[ "$p" != /* ]]; then
        p="$(pwd)/$p"
    fi
    # Resolve . and .. manually
    local -a parts=() result=()
    IFS='/' read -ra parts <<< "$p"
    for part in "${parts[@]}"; do
        case "$part" in
            ""|.) ;;
            ..)   [[ ${#result[@]} -gt 0 ]] && unset 'result[-1]' ;;
            *)    result+=("$part") ;;
        esac
    done
    echo "/${result[*]// //}"
}
Get path relative to a base.
fs.sh:73
fs::path::relative() {
    local target="$1" base="$2"
    # Strip common prefix
    while [[ "$target" == "$base"* && "$base" != "/" ]]; do
        target="${target#$base}"
        target="${target#/}"
        break
    done
    echo "$target"
}
Usage:
fs::path::relative /a/b/c /a  # b/c
Check if a path is absolute.
fs.sh:85
fs::path::is_absolute() {
    [[ "$1" == /* ]]
}
Check if a path is relative.
fs.sh:90
fs::path::is_relative() {
    [[ "$1" != /* ]]
}

File and directory checks

Functions to test file and directory properties.
Check if a path exists.
fs.sh:98
fs::exists() { [[ -e "$1" ]]; }
Check if path is a regular file.
fs.sh:99
fs::is_file() { [[ -f "$1" ]]; }
Check if path is a directory.
fs.sh:100
fs::is_dir() { [[ -d "$1" ]]; }
Check if path is readable.
fs.sh:102
fs::is_readable() { [[ -r "$1" ]]; }
Check if path is writable.
fs.sh:103
fs::is_writable() { [[ -w "$1" ]]; }
Check if path is executable.
fs.sh:104
fs::is_executable() { [[ -x "$1" ]]; }
Check if file or directory is empty.
fs.sh:105
fs::is_empty() { [[ -f "$1" && ! -s "$1" ]] || [[ -d "$1" && -z "$(ls -A "$1" 2>/dev/null)" ]]; }
Check if two paths resolve to the same file (inode comparison).
fs.sh:108
fs::is_same() {
    [[ "$(stat -c '%d:%i' "$1" 2>/dev/null)" == "$(stat -c '%d:%i' "$2" 2>/dev/null)" ]]
}

File information

Functions to retrieve file metadata and properties.
File size in bytes.
fs.sh:117
fs::size() {
    stat -c '%s' "$1" 2>/dev/null || wc -c < "$1" 2>/dev/null
}
Human-readable file size.
fs.sh:122
fs::size::human() {
    local size
    size=$(fs::size "$1")
    if runtime::has_command numfmt; then
        numfmt --to=iec-i --suffix=B "$size"
    else
        awk -v s="$size" 'BEGIN {
            split("B KiB MiB GiB TiB", u)
            i=1; while(s>=1024 && i<5){s/=1024; i++}
            printf "%.1f%s\n", s, u[i]
        }'
    fi
}
Usage:
fs::size::human large-file.bin  # 1.5GiB
Last modified time (unix timestamp).
fs.sh:137
fs::modified() {
    stat -c '%Y' "$1" 2>/dev/null
}
Last modified time (human readable).
fs.sh:142
fs::modified::human() {
    stat -c '%y' "$1" 2>/dev/null
}
Creation time (unix timestamp) — not available on all filesystems.
fs.sh:147
fs::created() {
    stat -c '%W' "$1" 2>/dev/null
}
Octal permissions.
fs.sh:152
fs::permissions() {
    stat -c '%a' "$1" 2>/dev/null
}
Usage:
fs::permissions script.sh  # 755
Symbolic permissions (e.g. -rwxr-xr-x).
fs.sh:157
fs::permissions::symbolic() {
    stat -c '%A' "$1" 2>/dev/null
}
Owner username.
fs.sh:162
fs::owner() {
    stat -c '%U' "$1" 2>/dev/null
}
Owner group.
fs.sh:167
fs::owner::group() {
    stat -c '%G' "$1" 2>/dev/null
}
Inode number.
fs.sh:172
fs::inode() {
    stat -c '%i' "$1" 2>/dev/null
}
MIME type.
fs.sh:177
fs::mime_type() {
    if runtime::has_command file; then
        file --mime-type -b "$1" 2>/dev/null
    else
        echo "unknown"
    fi
}
Usage:
fs::mime_type image.png  # image/png

File operations

Functions to create, copy, move, and delete files.
Copy file or directory.
fs.sh:206
fs::copy() {
    cp -r "$1" "$2"
}
Move/rename.
fs.sh:211
fs::move() {
    mv "$1" "$2"
}
Delete file or directory.
fs.sh:216
fs::delete() {
    rm -rf "$1"
}
Create directory (including parents).
fs.sh:221
fs::mkdir() {
    mkdir -p "$1"
}
Touch a file (create or update timestamp).
fs.sh:226
fs::touch() {
    touch "$1"
}
Rename just the filename, keeping directory.
fs.sh:243
fs::rename() {
    local dir
    dir="$(fs::path::dirname "$1")"
    mv "$1" "$dir/$2"
}
Usage:
fs::rename /path/to/oldname.txt newname.txt
Safely delete to a trash dir instead of permanent delete.
fs.sh:251
fs::trash() {
    local trash_dir="${HOME}/.local/share/Trash/files"
    mkdir -p "$trash_dir"
    mv "$1" "$trash_dir/$(fs::path::basename "$1").$(date +%s)"
}

Temporary files

Functions to create temporary files and directories.
Create a temporary file, print its path.
fs.sh:263
fs::temp::file() {
    local prefix="${1:-fsbshf}"
    mktemp "/tmp/${prefix}.XXXXXX"
}
Usage:
tmpfile=$(fs::temp::file)
echo "data" > "$tmpfile"
Create a temporary directory, print its path.
fs.sh:270
fs::temp::dir() {
    local prefix="${1:-fsbshf}"
    mktemp -d "/tmp/${prefix}.XXXXXX"
}
Create a temp file and register cleanup on EXIT.
fs.sh:277
fs::temp::file::auto() {
    local tmp
    tmp=$(fs::temp::file "$1")
    trap "rm -f '$tmp'" EXIT
    echo "$tmp"
}
Create a temp dir and register cleanup on EXIT.
fs.sh:285
fs::temp::dir::auto() {
    local tmp
    tmp=$(fs::temp::dir "$1")
    trap "rm -rf '$tmp'" EXIT
    echo "$tmp"
}

Reading and writing

Functions to read from and write to files.
Read entire file contents.
fs.sh:297
fs::read() {
    cat "$1"
}
Write content to file (overwrites).
fs.sh:303
fs::write() {
    printf '%s' "$2" > "$1"
}
Usage:
fs::write file.txt "Hello, world!"
Write with newline.
fs.sh:308
fs::writeln() {
    printf '%s\n' "$2" > "$1"
}
Append content to file.
fs.sh:313
fs::append() {
    printf '%s' "$2" >> "$1"
}
Append with newline.
fs.sh:318
fs::appendln() {
    printf '%s\n' "$2" >> "$1"
}
Read a specific line number (1-indexed).
fs.sh:324
fs::line() {
    sed -n "${2}p" "$1"
}
Usage:
fs::line file.txt 5  # Read line 5
Read a range of lines.
fs.sh:330
fs::lines() {
    sed -n "${2},${3}p" "$1"
}
Usage:
fs::lines file.txt 10 20  # Read lines 10-20
Count lines in a file.
fs.sh:335
fs::line_count() {
    wc -l < "$1"
}
Count words in a file.
fs.sh:340
fs::word_count() {
    wc -w < "$1"
}
Count characters in a file.
fs.sh:345
fs::char_count() {
    wc -c < "$1"
}
Check if file contains a string.
fs.sh:351
fs::contains() {
    grep -qF "$2" "$1" 2>/dev/null
}
Usage:
if fs::contains config.txt "debug=true"; then
  echo "Debug mode enabled"
fi
Check if file matches a regex.
fs.sh:356
fs::matches() {
    grep -qE "$2" "$1" 2>/dev/null
}
Replace string in file (in-place).
fs.sh:362
fs::replace() {
    sed -i "s|${2}|${3}|g" "$1"
}
Usage:
fs::replace config.txt "old_value" "new_value"
Prepend content to file.
fs.sh:367
fs::prepend() {
    local tmp
    tmp=$(fs::temp::file)
    printf '%s\n' "$2" | cat - "$1" > "$tmp"
    mv "$tmp" "$1"
}

Directory operations

Functions to list and search directory contents.
List directory contents (one per line).
fs.sh:379
fs::ls() {
    ls -1 "${1:-.}"
}
List with hidden files.
fs.sh:384
fs::ls::all() {
    ls -1A "${1:-.}"
}
List only files.
fs.sh:389
fs::ls::files() {
    find "${1:-.}" -maxdepth 1 -type f -printf '%f\n' 2>/dev/null || \
    ls -1p "${1:-.}" | grep -v '/$'
}
List only directories.
fs.sh:395
fs::ls::dirs() {
    find "${1:-.}" -maxdepth 1 -type d -not -path "${1:-.}" -printf '%f\n' 2>/dev/null || \
    ls -1p "${1:-.}" | grep '/$' | tr -d '/'
}
Recursive find by name pattern.
fs.sh:402
fs::find() {
    find "${1:-.}" -name "$2" 2>/dev/null
}
Usage:
fs::find /home "*.txt"
Recursive find by type (f=file, d=dir, l=symlink).
fs.sh:407
fs::find::type() {
    find "${1:-.}" -type "$2" 2>/dev/null
}
Usage:
fs::find::type /var/log f  # Find all files
Find files modified within n minutes.
fs.sh:412
fs::find::recent() {
    find "${1:-.}" -type f -mmin "-${2:-60}" 2>/dev/null
}
Usage:
fs::find::recent . 30  # Files modified in last 30 minutes
Find files larger than n bytes.
fs.sh:417
fs::find::larger_than() {
    find "${1:-.}" -type f -size "+${2}c" 2>/dev/null
}
Find files smaller than n bytes.
fs.sh:422
fs::find::smaller_than() {
    find "${1:-.}" -type f -size "-${2}c" 2>/dev/null
}
Get total size of directory.
fs.sh:427
fs::dir::size() {
    du -sb "${1:-.}" 2>/dev/null | awk '{print $1}'
}
Get total size of directory, human readable.
fs.sh:432
fs::dir::size::human() {
    du -sh "${1:-.}" 2>/dev/null | awk '{print $1}'
}
Count items in directory.
fs.sh:437
fs::dir::count() {
    find "${1:-.}" -maxdepth 1 -not -path "${1:-.}" 2>/dev/null | wc -l
}
Check if directory is empty.
fs.sh:442
fs::dir::is_empty() {
    [[ -z "$(ls -A "${1:-.}" 2>/dev/null)" ]]
}

File watching

Functions to monitor files for changes.
Watch a file for changes, run callback on change.
fs.sh:453
fs::watch() {
    local path="$1" callback="$2" interval="${3:-1}"
    local last_modified
    last_modified=$(fs::modified "$path")

    while true; do
        sleep "$interval"
        local current
        current=$(fs::modified "$path")
        if [[ "$current" != "$last_modified" ]]; then
            last_modified="$current"
            "$callback" "$path"
        fi
    done
}
Usage:
on_change() {
  echo "File $1 changed"
}
fs::watch config.txt on_change 2
Watch with a timeout (seconds).
fs.sh:471
fs::watch::timeout() {
    local path="$1" callback="$2" timeout="$3" interval="${4:-1}"
    local elapsed=0
    local last_modified
    last_modified=$(fs::modified "$path")

    while (( elapsed < timeout )); do
        sleep "$interval"
        (( elapsed += interval ))
        local current
        current=$(fs::modified "$path")
        if [[ "$current" != "$last_modified" ]]; then
            last_modified="$current"
            "$callback" "$path"
        fi
    done
}

Checksums

Functions to compute file checksums and verify integrity.
Compute MD5 checksum.
fs.sh:493
fs::checksum::md5() {
    if runtime::has_command md5sum; then
        md5sum "$1" | awk '{print $1}'
    elif runtime::has_command md5; then
        md5 -q "$1"
    fi
}
Compute SHA1 checksum.
fs.sh:501
fs::checksum::sha1() {
    if runtime::has_command sha1sum; then
        sha1sum "$1" | awk '{print $1}'
    elif runtime::has_command shasum; then
        shasum -a 1 "$1" | awk '{print $1}'
    fi
}
Compute SHA256 checksum.
fs.sh:509
fs::checksum::sha256() {
    if runtime::has_command sha256sum; then
        sha256sum "$1" | awk '{print $1}'
    elif runtime::has_command shasum; then
        shasum -a 256 "$1" | awk '{print $1}'
    fi
}
Check if two files are identical (by content).
fs.sh:518
fs::is_identical() {
    local sum1 sum2
    sum1=$(fs::checksum::sha256 "$1")
    sum2=$(fs::checksum::sha256 "$2")
    [[ "$sum1" == "$sum2" ]]
}

Build docs developers (and LLMs) love