2024-03-15
7 min read

How to Get the Directory Where a Bash Script is Located

How to Get the Directory Where a Bash Script is Located

When writing Bash scripts, you often need to know the directory where your script is located. This becomes essential when your script needs to access configuration files, load other scripts, or work with relative paths. Getting the script's directory isn't as straightforward as it might seem, especially when dealing with symbolic links or different execution contexts.

Prerequisites

You'll need a basic understanding of Bash scripting and familiarity with file system concepts like absolute and relative paths. Access to a Unix-like system (Linux, macOS, or WSL) is required to test the examples.

The Basic Approach

The most common method uses the $0 variable, which contains the path used to invoke the script:

#!/bin/bash

# Get the directory of the current script
SCRIPT_DIR=$(dirname "$0")
echo "Script directory: $SCRIPT_DIR"

# Convert to absolute path
SCRIPT_DIR=$(cd "$SCRIPT_DIR" && pwd)
echo "Absolute script directory: $SCRIPT_DIR"

The dirname command extracts the directory portion from a file path, while $0 represents how the script was called. However, this approach has limitations when dealing with symbolic links or relative paths.

If your script might be called through a symbolic link, you need a more robust approach that resolves the actual location:

#!/bin/bash

# Method 1: Using readlink to resolve symbolic links
SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
echo "Real script directory: $SCRIPT_DIR"

# Method 2: More portable approach for systems without readlink -f
SOURCE="${BASH_SOURCE[0]}"
while [ -h "$SOURCE" ]; do
    DIR=$(cd -P "$(dirname "$SOURCE")" && pwd)
    SOURCE=$(readlink "$SOURCE")
    [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE"
done
SCRIPT_DIR=$(cd -P "$(dirname "$SOURCE")" && pwd)

echo "Resolved script directory: $SCRIPT_DIR"

The readlink -f command follows symbolic links to their final destination. The second method provides better portability across different Unix systems that might not support the -f flag.

Cross-Platform Compatible Solution

For maximum portability across different systems, use this approach that works on Linux, macOS, and other Unix variants:

#!/bin/bash

get_script_dir() {
    local source="${BASH_SOURCE[0]}"
    local dir=""

    # Resolve $source until the file is no longer a symlink
    while [ -h "$source" ]; do
        dir=$(cd -P "$(dirname "$source")" >/dev/null 2>&1 && pwd)
        source=$(readlink "$source")
        # If $source was a relative symlink, resolve it relative to the path where the symlink file was located
        [[ $source != /* ]] && source=$dir/$source
    done

    dir=$(cd -P "$(dirname "$source")" >/dev/null 2>&1 && pwd)
    echo "$dir"
}

SCRIPT_DIR=$(get_script_dir)
echo "Script located in: $SCRIPT_DIR"

This function handles symbolic links correctly and suppresses error messages while maintaining compatibility across different shells and systems.

Practical Usage Examples

Here's how you might use the script directory in real-world scenarios:

#!/bin/bash

# Get script directory
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)

# Load configuration file from same directory
CONFIG_FILE="$SCRIPT_DIR/config.conf"
if [ -f "$CONFIG_FILE" ]; then
    source "$CONFIG_FILE"
    echo "Configuration loaded from: $CONFIG_FILE"
else
    echo "Warning: Configuration file not found at $CONFIG_FILE"
fi

# Include other scripts from relative paths
HELPERS_DIR="$SCRIPT_DIR/helpers"
if [ -d "$HELPERS_DIR" ]; then
    for helper in "$HELPERS_DIR"/*.sh; do
        if [ -f "$helper" ]; then
            source "$helper"
            echo "Loaded helper: $(basename "$helper")"
        fi
    done
fi

# Work with data files relative to script location
DATA_DIR="$SCRIPT_DIR/data"
OUTPUT_DIR="$SCRIPT_DIR/output"

# Create output directory if it doesn't exist
mkdir -p "$OUTPUT_DIR"

# Process files from data directory
if [ -d "$DATA_DIR" ]; then
    for data_file in "$DATA_DIR"/*.txt; do
        if [ -f "$data_file" ]; then
            filename=$(basename "$data_file")
            processed_file="$OUTPUT_DIR/processed_$filename"
            # Process the file (example: convert to uppercase)
            tr '[:lower:]' '[:upper:]' < "$data_file" > "$processed_file"
            echo "Processed: $filename -> processed_$filename"
        fi
    done
fi

This example demonstrates loading configuration files, including helper scripts, and processing data files all relative to the script's location.

Handling Different Execution Contexts

Your script might be executed in various ways, each affecting how paths are resolved:

#!/bin/bash

# Function to demonstrate different execution contexts
show_execution_context() {
    echo "=== Execution Context ==="
    echo "\$0 (script name): $0"
    echo "\$PWD (current directory): $PWD"
    echo "dirname \$0: $(dirname "$0")"
    echo "BASH_SOURCE[0]: ${BASH_SOURCE[0]}"

    # Get absolute script directory
    SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
    echo "Resolved script directory: $SCRIPT_DIR"
    echo
}

show_execution_context

# Test with different file operations
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
echo "Working with files relative to: $SCRIPT_DIR"

# Example: Create a log file in the script's directory
LOG_FILE="$SCRIPT_DIR/script.log"
echo "$(date): Script executed from $PWD" >> "$LOG_FILE"
echo "Log entry written to: $LOG_FILE"

Test this script by running it from different directories to see how the paths behave:

# Run from script directory
./myscript.sh

# Run from parent directory
subdirectory/myscript.sh

# Run from anywhere with absolute path
/full/path/to/myscript.sh

Error Handling and Validation

Always include proper error handling when working with script directories:

#!/bin/bash

get_script_directory() {
    local source="${BASH_SOURCE[0]}"
    local dir=""

    # Handle case where BASH_SOURCE is not available
    if [ -z "$source" ]; then
        source="$0"
    fi

    # Resolve symbolic links
    while [ -h "$source" ]; do
        dir=$(cd -P "$(dirname "$source")" 2>/dev/null && pwd)
        if [ $? -ne 0 ]; then
            echo "Error: Cannot determine script directory" >&2
            return 1
        fi
        source=$(readlink "$source")
        [[ $source != /* ]] && source="$dir/$source"
    done

    # Get final directory
    dir=$(cd -P "$(dirname "$source")" 2>/dev/null && pwd)
    if [ $? -ne 0 ] || [ -z "$dir" ]; then
        echo "Error: Cannot determine script directory" >&2
        return 1
    fi

    echo "$dir"
}

# Use the function with error checking
SCRIPT_DIR=$(get_script_directory)
if [ $? -eq 0 ]; then
    echo "Script directory: $SCRIPT_DIR"

    # Verify the directory exists and is readable
    if [ -d "$SCRIPT_DIR" ] && [ -r "$SCRIPT_DIR" ]; then
        echo "Directory is accessible: $SCRIPT_DIR"
    else
        echo "Warning: Script directory is not accessible" >&2
    fi
else
    echo "Failed to determine script directory, using current directory" >&2
    SCRIPT_DIR="$PWD"
fi

This approach includes comprehensive error checking and fallback strategies.

Creating a Reusable Library

You can create a reusable function that handles all edge cases:

#!/bin/bash

# Library function for getting script directory
# Usage: script_dir=$(get_script_dir)
get_script_dir() {
    local source="${BASH_SOURCE[1]:-${BASH_SOURCE[0]}}"
    local dir=""

    # Handle different shell environments
    if [ -z "$source" ]; then
        if [ -n "$0" ]; then
            source="$0"
        else
            echo "." # fallback to current directory
            return
        fi
    fi

    # Resolve all symbolic links
    while [ -h "$source" ]; do
        dir=$(cd -P "$(dirname "$source")" 2>/dev/null && pwd)
        [ $? -ne 0 ] && { echo "."; return; }
        source=$(readlink "$source" 2>/dev/null)
        [ $? -ne 0 ] && { echo "$dir"; return; }
        [[ $source != /* ]] && source="$dir/$source"
    done

    # Get the final directory
    dir=$(cd -P "$(dirname "$source")" 2>/dev/null && pwd)
    echo "${dir:-.}"
}

# Example usage
SCRIPT_DIR=$(get_script_dir)
echo "This script is located in: $SCRIPT_DIR"

# Use it to source other files
CONFIG_FILE="$SCRIPT_DIR/app.conf"
[ -f "$CONFIG_FILE" ] && source "$CONFIG_FILE"

# Or include other scripts
UTILS_SCRIPT="$SCRIPT_DIR/utils.sh"
[ -f "$UTILS_SCRIPT" ] && source "$UTILS_SCRIPT"

Save this function in a utilities file that you can source in other scripts.

Testing Your Implementation

Create a test script to verify your directory detection works correctly:

#!/bin/bash

# Test script for directory detection
test_script_dir() {
    local expected_dir="$1"

    # Your directory detection code here
    DETECTED_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)

    echo "Expected: $expected_dir"
    echo "Detected: $DETECTED_DIR"

    if [ "$DETECTED_DIR" = "$expected_dir" ]; then
        echo "✓ Test passed"
        return 0
    else
        echo "✗ Test failed"
        return 1
    fi
}

# Run the test
ACTUAL_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
test_script_dir "$ACTUAL_DIR"

Test your implementation with symbolic links and different execution paths to ensure it works reliably.

Next Steps

Now you know how to reliably determine your script's directory location. Consider exploring advanced Bash scripting patterns like creating modular script architectures, implementing proper logging systems, or building configuration management for your scripts. You might also want to look into using these techniques with deployment scripts or automation tools.

Good luck with your scripting projects!

Published: 2024-03-15|Last updated: 2024-03-15T10:00:00Z

Found an issue?