Created
December 26, 2025 21:09
-
-
Save yorgabr/2ece0b936f676de869c64444441fef3d to your computer and use it in GitHub Desktop.
This script has the purpose of mounting all Windows drives letters inside a WSL section.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # mount-windows_drives.sh | |
| # | |
| # This script has the purpose of mounting all Windows drives letters inside a WSL | |
| # section. If one or more drives are unset, it cares yourself of the task to wake them | |
| # up before trying the mounting. | |
| # 'set -euo pipefail' is a robustness trio in Bash: | |
| # - 'set -e' : makes the script exit immediately if ANY command returns | |
| # a non-zero status (failure), except in certain contexts such as | |
| # inside 'if', 'while', and after '||'. | |
| # - 'set -u' : treats the use of UNDEFINED variables as an error, terminating the script. | |
| # This prevents silent bugs caused by mistyped variable names. | |
| # - 'set -o pipefail' : in pipelines (cmd1 | cmd2 | cmd3), the exit code | |
| # becomes that of the FIRST command that fails (not just the last one). | |
| # This ensures failures at the start of the pipeline are not swallowed. | |
| # Together, these options make the script much safer against unexpected states. | |
| set -euo pipefail | |
| VERSION="1.5.0" | |
| # Default settings | |
| RETRY_COUNT=3 # Number of retries per drive (wake/mount) | |
| RETRY_SLEEP=2 # Delay (seconds) between retries | |
| MOUNT_BASE="/mnt" # Mount base in WSL (must be an absolute path) | |
| MOUNT_OPTIONS="" # Extra drvfs mount options (empty by default; use with care) | |
| DRY_RUN=0 # 0 = perform mounts; 1 = enumerate & wake only | |
| # Optional filter: process only these letters (lowercase). | |
| # Leave empty to process all mapped network drives of the current Windows user. | |
| DRIVES_FILTER=( ) # e.g., DRIVES_FILTER=( "j" "k" ) | |
| # Utility functions | |
| log_error() { printf "%s\n" "$*" >&2; } | |
| log_info() { printf "%s\n" "$*"; } | |
| script_name() { basename -- "$0"; } | |
| print_version() { printf "%s version %s\n" "$(script_name)" "$VERSION" } | |
| print_usage() { | |
| cat <<USAGE | |
| $(script_name) — Mount Windows mapped network drives (UNC) into WSL via drvfs. | |
| Usage: | |
| $(script_name) [options] | |
| Options: | |
| --letters j,k Process only the specified drive letters (comma or space separated). | |
| Examples: --letters j,k | --letters=j,k | --letters j k | |
| --dry-run Enumerate and wake drives, but DO NOT mount. | |
| --retry-count N Number of retries for wake/mount attempts (default: ${RETRY_COUNT}). | |
| --retry-sleep SEC Sleep (seconds) between retries (default: ${RETRY_SLEEP}). | |
| --mount-base PATH Base directory in WSL where drives will be mounted (default: ${MOUNT_BASE}). | |
| Must be an absolute path (starts with '/'). | |
| --mount-options OPTS Extra drvfs mount options (passed to 'mount -o'). | |
| ⚠ Use with care, especially in corporate environments. | |
| Some options (e.g., 'metadata', 'uid', 'gid') can change | |
| permission behavior and may conflict with local policies. | |
| --version Show script version and exit. | |
| -h, --help Show this help and exit. | |
| Examples: | |
| $(script_name) | |
| $(script_name) --letters j,k | |
| $(script_name) --dry-run --letters j,k | |
| $(script_name) --retry-count 5 --retry-sleep 3 | |
| $(script_name) --mount-base /mnt/shared | |
| $(script_name) --mount-options "uid=\$(id -u),gid=\$(id -g),metadata" | |
| USAGE | |
| } | |
| parse_args() { | |
| while (("$#")); do | |
| case "$1" in | |
| -h|--help) | |
| print_usage | |
| exit 0 | |
| ;; | |
| --version) | |
| print_version | |
| exit 0 | |
| ;; | |
| --dry-run) | |
| DRY_RUN=1 | |
| shift | |
| ;; | |
| --letters) | |
| shift | |
| if [[ $# -eq 0 ]]; then | |
| log_error "--letters requires a value (e.g., 'j,k')." | |
| exit 2 | |
| fi | |
| local val="$1"; shift | |
| # Support comma or space as separators | |
| for tok in ${val//,/ }; do | |
| DRIVES_FILTER+=( "$(printf "%s" "$tok" | tr '[:upper:]' '[:lower:]')" ) | |
| done | |
| ;; | |
| --letters=*) | |
| local val="${1#--letters=}"; shift | |
| for tok in ${val//,/ }; do | |
| DRIVES_FILTER+=( "$(printf "%s" "$tok" | tr '[:upper:]' '[:lower:]')" ) | |
| done | |
| ;; | |
| --retry-count) | |
| shift | |
| [[ $# -gt 0 ]] || { log_error "--retry-count requires an integer value."; exit 2; } | |
| RETRY_COUNT="$1"; shift | |
| ;; | |
| --retry-sleep) | |
| shift | |
| [[ $# -gt 0 ]] || { log_error "--retry-sleep requires an integer value (seconds)."; exit 2; } | |
| RETRY_SLEEP="$1"; shift | |
| ;; | |
| --mount-base) | |
| shift | |
| [[ $# -gt 0 ]] || { log_error "--mount-base requires a path."; exit 2; } | |
| MOUNT_BASE="$1"; shift | |
| # Basic validation: must be absolute path | |
| if [[ -z "$MOUNT_BASE" || "${MOUNT_BASE:0:1}" != "/" ]]; then | |
| log_error "--mount-base must be an absolute path starting with '/'. Got: '$MOUNT_BASE'" | |
| exit 2 | |
| fi | |
| ;; | |
| --mount-base=*) | |
| MOUNT_BASE="${1#--mount-base=}"; shift | |
| if [[ -z "$MOUNT_BASE" || "${MOUNT_BASE:0:1}" != "/" ]]; then | |
| log_error "--mount-base must be an absolute path starting with '/'. Got: '$MOUNT_BASE'" | |
| exit 2 | |
| fi | |
| ;; | |
| --mount-options) | |
| shift | |
| [[ $# -gt 0 ]] || { log_error "--mount-options requires a value (e.g., 'uid=1000,gid=1000,metadata')."; exit 2; } | |
| MOUNT_OPTIONS="$1"; shift | |
| ;; | |
| --mount-options=*) | |
| MOUNT_OPTIONS="${1#--mount-options=}"; shift | |
| ;; | |
| *) | |
| log_error "Unknown option: $1" | |
| print_usage | |
| exit 2 | |
| ;; | |
| esac | |
| done | |
| } | |
| # Enumerate mapped network drives (FileSystem PSDrives with DisplayRoot starting with \\). | |
| # Output: lines "Letter<TAB>UNC", e.g., "J<TAB>\\server\share". | |
| # 'tr -d '\r'' removes Windows CR (CRLF) to avoid parsing issues in Bash. | |
| ps_list_network_drives() { | |
| powershell.exe -NoProfile -Command " | |
| Get-PSDrive -PSProvider FileSystem | | |
| Where-Object { \$_.DisplayRoot -and (\$_.DisplayRoot -like '\\\\*') } | | |
| Select-Object Name, DisplayRoot | | |
| ForEach-Object { [Console]::WriteLine(\$_.Name + \"`t\" + \$_.DisplayRoot) } | |
| " | tr -d $'\r' | |
| } | |
| # Wake-up a mapped drive letter (e.g., 'J') via PowerShell. | |
| # Touching the root path forces Windows to reconnect in the current user context. | |
| ps_wake_drive() { | |
| local letter_upper="$1" | |
| if powershell.exe -NoProfile -Command " | |
| try { | |
| \$root = '${letter_upper}:\'; | |
| Get-ChildItem -Force -ErrorAction Stop -LiteralPath \$root | Out-Null; | |
| exit 0 | |
| } catch { | |
| exit 1 | |
| } | |
| " >/dev/null; then | |
| return 0 | |
| else | |
| return 1 | |
| fi | |
| } | |
| # Mount via drvfs using UNC (e.g., '\\server\share') into ${MOUNT_BASE}/<lowercase letter>. | |
| mount_by_unc() { | |
| local letter_lower="$1" | |
| local unc="$2" | |
| local target="${MOUNT_BASE}/${letter_lower}" | |
| sudo mkdir -p "$target" | |
| # If already mounted, just report. | |
| if mountpoint -q "$target"; then | |
| log_info "Already mounted: ${target}" | |
| return 0 | |
| fi | |
| # Build mount arguments safely (handles spaces in options if any). | |
| local -a args | |
| args=( "-t" "drvfs" "$unc" "$target" ) | |
| if [[ -n "$MOUNT_OPTIONS" ]]; then | |
| args+=( "-o" "$MOUNT_OPTIONS" ) | |
| fi | |
| sudo mount "${args[@]}" | |
| log_info "Mounted $unc at $target${MOUNT_OPTIONS:+ (options: $MOUNT_OPTIONS)}" | |
| } | |
| # Process a single drive: wake-up and (optionally) mount with retries. | |
| process_drive() { | |
| local letter="$1" # expected uppercase (e.g., 'J') | |
| local unc="$2" | |
| log_info ">>> Processing ${letter}: ($unc)" | |
| # 1) Wake-up with retries | |
| local ok_wake=1 | |
| for (( i=1; i<=RETRY_COUNT; i++ )); do | |
| if ps_wake_drive "$letter"; then | |
| ok_wake=0 | |
| break | |
| else | |
| log_error "Wake attempt ${i}/${RETRY_COUNT} failed for ${letter}:. Retrying in ${RETRY_SLEEP}s..." | |
| sleep "$RETRY_SLEEP" | |
| fi | |
| done | |
| if (( ok_wake != 0 )); then | |
| log_error "Proceeding without successful wake for ${letter}:. Windows may reconnect lazily on mount." | |
| fi | |
| # 2) Mount via UNC with retries (unless dry-run) | |
| if (( DRY_RUN == 1 )); then | |
| log_info "DRY-RUN: Would mount '$unc' at '${MOUNT_BASE}/${letter,,}'${MOUNT_OPTIONS:+ with options '$MOUNT_OPTIONS'}." | |
| return 0 | |
| fi | |
| local ok_mount=1 | |
| for (( i=1; i<=RETRY_COUNT; i++ )); do | |
| if mount_by_unc "${letter,,}" "$unc"; then | |
| ok_mount=0 | |
| break | |
| else | |
| log_error "Mount attempt ${i}/${RETRY_COUNT} failed for ${letter}:. Retrying in ${RETRY_SLEEP}s..." | |
| sleep "$RETRY_SLEEP" | |
| fi | |
| done | |
| if (( ok_mount == 0 )); then | |
| log_info "SUCCESS: ${letter}: mounted in WSL." | |
| return 0 | |
| else | |
| log_error "FAILURE: ${letter}: could not be mounted in WSL." | |
| return 1 | |
| fi | |
| } | |
| # Ensure 'powershell.exe' is available (WSL<->Windows integration). | |
| check_powershell_available() { | |
| if ! command -v powershell.exe >/dev/null 2>&1; then | |
| log_error "powershell.exe not found. Ensure WSL integration with Windows is enabled." | |
| exit 1 | |
| fi | |
| } | |
| main() { | |
| parse_args "$@" | |
| check_powershell_available | |
| # Collect network drives: lines "Letter<TAB>UNC" | |
| local drives_list | |
| drives_list="$(ps_list_network_drives)" | |
| if [[ -z "$drives_list" ]]; then | |
| log_error "No mapped network drives with DisplayRoot (UNC) were found for the current Windows user." | |
| exit 1 | |
| fi | |
| local failed=0 | |
| # 'IFS=$'\t'' sets TAB as the separator; 'read -r' reads raw (no backslash escapes). | |
| while IFS=$'\t' read -r letter unc; do | |
| # Filter (if configured) | |
| if ((${#DRIVES_FILTER[@]} > 0)); then | |
| local want=0 | |
| for f in "${DRIVES_FILTER[@]}"; do | |
| if [[ "${letter,,}" == "$f" ]]; then | |
| want=1 | |
| break | |
| fi | |
| done | |
| if (( want == 0 )); then | |
| continue | |
| fi | |
| fi | |
| # Basic sanity | |
| if [[ -z "$letter" || -z "$unc" ]]; then | |
| log_error "Skipping entry with empty letter or UNC: '${letter}' / '${unc}'" | |
| continue | |
| fi | |
| # Process the drive | |
| if ! process_drive "$letter" "$unc"; then | |
| failed=$((failed+1)) | |
| fi | |
| done < <(printf "%s\n" "$drives_list") | |
| if (( failed > 0 )); then | |
| log_error "$failed drive(s) failed to mount." | |
| exit 1 | |
| fi | |
| if (( DRY_RUN == 1 )); then | |
| log_info "DRY-RUN: Completed. No mounts performed." | |
| else | |
| log_info "All network drives mounted successfully." | |
| fi | |
| } | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment