Automating My Dotfile Restoration with a Python Script



I love using GNU Stow to manage my dotfiles. It keeps everything neat and organized in my Git repository, making it easy to sync configurations across different machines. But as my list of dotfile packages grew, restoring them manually became a pain. Having to stow each package one by one felt tedious, especially after setting up a new system.

So, I decided to automate the process with a Python script that restores my dotfiles quickly and interactively. I also wanted the flexibility to choose which packages to restore instead of blindly applying everything.

Initially, I thought about just writing a simple loop to stow everything, but I realized I needed more control. Some dotfiles belong in ~/.config/, while others go directly into ~. A single command to restore everything wouldn't work unless I had a way to handle different target directories. That's where my script comes in. It allows me to:
- List available Stow packages from my repository.
- Select which packages to restore interactively.
- Choose between global and local restore modes to control where files are linked.

How the script works

The script takes three main parameters:
1. stow_dir – The directory where all my Stow packages are stored.
2. target_dir – The directory where the selected packages should be restored.
3. global_target – A mode that determines whether all packages use the same --target-dir or if I should be prompted for each package.

Two restoration modes

Script also support two restoration modes. In the global mode all selected packages are restored in a single --target-dir. In the local mode script asks for the target directory for each restore.

Global Mode - One Target Directory for All Packages

When global mode is enabled using --global-target, all selected packages are restored to --target-dir. This is great when all configurations belong in the same place, like ~/.config/.

Local Mode - Different Target Directories per Package

When global mode is disabled (i.e., not using --global-target), the script asks me where to place each package. If I don’t specify a directory, it defaults to ~.

The final script

Fully functional script is as follows:

import os
import subprocess
import argparse

def list_stow_packages(stow_dir):
    """List available Stow packages in the given directory."""
    return [pkg for pkg in os.listdir(stow_dir) if os.path.isdir(os.path.join(stow_dir, pkg))]

def restore_packages(stow_dir, target_dir, use_global_target):
    """Interactively restore selected Stow packages."""
    packages = list_stow_packages(stow_dir)

    if not packages:
        print("No Stow packages found.")
        return

    print("Available Stow packages:")
    for idx, pkg in enumerate(packages, 1):
        print(f"{idx}. {pkg}")

    selected_indices = input("Enter the numbers of packages to restore (comma-separated): ").strip()

    try:
        selected_indices = [int(i) for i in selected_indices.split(',') if i.strip().isdigit()]
    except ValueError:
        print("Invalid input. Exiting.")
        return

    selected_packages = [packages[i - 1] for i in selected_indices if 0 < i <= len(packages)]

    if not selected_packages:
        print("No valid packages selected.")
        return

    for pkg in selected_packages:
        pkg_target_dir = target_dir if use_global_target else input(f"Enter the target directory for {pkg} (default: {target_dir}): ").strip() or target_dir
        print(f"Restoring {pkg} to {pkg_target_dir}...")
        cmd = ["stow", "-t", pkg_target_dir, pkg]
        subprocess.run(cmd, cwd=stow_dir)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Interactive GNU Stow package restoration.")
    parser.add_argument("stow_dir", nargs="?", default=os.getcwd(), help="Path to the Stow directory (default: current directory)")
    parser.add_argument("--global-target", action="store_true", help="Use a single target directory for all packages")
    parser.add_argument("--target-dir", default=os.path.expanduser("~"), help="Global target directory (default: ~)")

    args = parser.parse_args()

    if not os.path.isdir(args.stow_dir):
        print("Invalid Stow directory.")
    elif args.global_target and not os.path.isdir(args.target_dir):
        print("Invalid global target directory.")
    else:
        restore_packages(args.stow_dir, args.target_dir, args.global_target)

Running the Script

With only 3 arguments ,script is simple to use:

python stow_restore.py <stow_directory> [--global-target] [--target-dir <directory>]

Sample Runs

Example 1: Restore All Packages to One Directory
python stow_restore.py ~/.dotfiles --global-target --target-dir ~/.config

This restores all selected dotfiles to ~/.config/ without asking where to put them.

Example 2: Select a Directory for Each Package
python stow_restore.py ~/.dotfiles

With this, the script asks where to place each selected package. If I just hit Enter, it defaults to ~.

Example 3: Restoring Only Specific Packages
python stow_restore.py ~/.dotfiles --global-target --target-dir ~/dotfiles

I pick only the packages I want, and they all go into ~/dotfiles/.

Conclusion

This script has completely changed how I manage my dotfiles. Now, I don’t have to manually restore each package, and I can easily choose which ones to apply. It’s simple, interactive, and flexible—exactly what I needed!