Skip to content

Instantly share code, notes, and snippets.

@yorgabr
Created December 26, 2025 21:09
Show Gist options
  • Select an option

  • Save yorgabr/2ece0b936f676de869c64444441fef3d to your computer and use it in GitHub Desktop.

Select an option

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.
#!/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