Linux pwd Command: From System Calls to Symbolic Link Handling#

The pwd command might be one of the simplest yet most frequently used Linux commands. But beneath its simplicity lies interesting implementation details and symbolic link handling logic that can save you from subtle scripting bugs.

Core Implementation: The getcwd() System Call#

At its core, pwd invokes the getcwd() system call, which returns the absolute path of the current process’s working directory. In kernel space, each process’s task_struct maintains a pointer to the current directory’s dentry.

// Directory information in Linux kernel process structure
struct task_struct {
    struct fs_struct *fs;  // Filesystem info
    // ...
};

struct fs_struct {
    struct path pwd;       // Current working directory
    struct path root;      // Root directory
    // ...
};

struct path {
    struct vfsmount *mnt;  // Mount point
    struct dentry *dentry; // Directory entry
};

When you execute pwd, the kernel traverses up the directory tree from fs->pwd.dentry, reconstructing the full path. Time complexity is O(directory depth), space complexity is O(path length).

-L vs -P: Logical vs Physical Paths#

This is where pwd gets confusing. The two flags differ in how they handle symbolic links:

  • pwd -L (default): Shows logical path, preserving symbolic links
  • pwd -P: Shows physical path, resolving all symbolic links

Here’s a practical example:

# Create test environment
mkdir -p /tmp/real_dir
ln -s /tmp/real_dir /tmp/symlink_dir
cd /tmp/symlink_dir

# Now observe the difference
pwd      # Output: /tmp/symlink_dir  (logical path)
pwd -L   # Output: /tmp/symlink_dir  (same, default behavior)
pwd -P   # Output: /tmp/real_dir     (physical path, resolved)

Implementation Details:

  • -L mode reads the $PWD environment variable, maintained by the shell
  • -P mode calls the getcwd() system call, rebuilding the path from kernel dentry structures

You can verify these two sources through /proc:

# Environment variable (logical path)
echo $PWD                    # /tmp/symlink_dir

# Kernel perspective (physical path)
readlink /proc/self/cwd      # /tmp/real_dir

Practical Scenarios and Scripting Techniques#

1. Getting Script Directory (The Reliable Way)#

A common mistake developers make:

# ❌ Wrong: Using pwd directly in scripts
cd /tmp/symlink_dir
./script.sh  # pwd inside script gives execution dir, not script dir

# ✅ Correct: Get the script file's directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"

This approach has two key points:

  1. ${BASH_SOURCE[0]} is the script file’s actual path (even if it’s a symlink)
  2. pwd -P ensures you get the physical path, avoiding symlink confusion

2. Behavior When Directory Is Deleted#

An interesting edge case: what happens if the current directory is deleted by another process?

mkdir /tmp/test_dir
cd /tmp/test_dir
rmdir /tmp/test_dir  # Delete in another terminal

pwd  # Output: pwd: cannot determine current directory: No such file or directory

The shell shows an error but preserves the old path in the prompt. This happens because the shell caches $PWD, but the kernel’s dentry is now invalid.

3. Performance: O(n) Directory Traversal#

pwd’s performance bottleneck is directory depth. Traversing a 1000-level deep directory tree:

# Create deep directory test
mkdir -p /tmp/deep/level{1..1000}
cd /tmp/deep/level1000

time pwd  # ~0.001s, very fast in practice

In practice, even 1000 levels deep, pwd completes within 1ms. The kernel’s dentry cache makes upward traversal an O(1) operation.

Web Implementation: Browser-Side pwd Simulation#

Implementing pwd functionality in a web terminal emulator:

class VirtualFileSystem {
  private currentPath: string = '/';
  private symbolicLinks: Map<string, string> = new Map();

  pwd(physical: boolean = false): string {
    if (physical) {
      // Resolve all symbolic links
      return this.resolvePhysicalPath(this.currentPath);
    }
    // Return logical path (PWD environment variable)
    return this.currentPath;
  }

  private resolvePhysicalPath(path: string): string {
    const parts = path.split('/').filter(Boolean);
    const resolved: string[] = [];

    for (const part of parts) {
      if (part === '..') {
        resolved.pop();
      } else if (part !== '.') {
        resolved.push(part);
      }
    }

    let result = '/' + resolved.join('/');
    
    // Recursively resolve symbolic links
    while (this.symbolicLinks.has(result)) {
      result = this.symbolicLinks.get(result)!;
    }

    return result;
  }
}

// Usage example
const fs = new VirtualFileSystem();
fs.symbolicLinks.set('/tmp/symlink', '/tmp/real');
fs.currentPath = '/tmp/symlink';

console.log(fs.pwd());      // /tmp/symlink (logical)
console.log(fs.pwd(true));  // /tmp/real (physical)

Integration with Other Commands#

pwd commonly works with other commands in scripts:

# Save and restore working directory
OLD_DIR=$(pwd)
cd /some/complex/path
do_work
cd "$OLD_DIR"

# Safer approach: use pushd/popd
pushd /some/complex/path
do_work
popd

# Get absolute path (resolve relative paths)
realpath=$(cd "$(dirname "$file")" && pwd)/$(basename "$file")

Common Pitfalls and Best Practices#

Pitfall Consequence Solution
Using $(pwd) in scripts Gets execution dir, not script dir Use ${BASH_SOURCE[0]}
pwd fails after dir deleted Script crashes cd / to safe directory first
Ignoring -L/-P distinction Unexpected symlink behavior Explicitly specify flag
Mixing $PWD and $(pwd) May get different results Consistently use pwd -P

The pwd command may be small, but it’s everywhere in system programming and script development. Understanding its implementation helps you write more robust automation scripts and avoid the pitfalls of symbolic links and directory management.