Skip to content

Instantly share code, notes, and snippets.

@morvy
Created February 11, 2026 10:40
Show Gist options
  • Select an option

  • Save morvy/02df87d8de6ffeff3c5adebb29c790d0 to your computer and use it in GitHub Desktop.

Select an option

Save morvy/02df87d8de6ffeff3c5adebb29c790d0 to your computer and use it in GitHub Desktop.
WinSCP Compare Files — Wine/Linux Meld alternative to WinMerge
#!/bin/bash
#
# WinSCP Compare Files — Wine/Linux Fix
# ======================================
#
# Fix for WinSCP's broken "Compare Files" feature when running under Wine on Linux.
#
# PROBLEM:
# WinSCP's built-in Compare Files extension is PowerShell-based. On native
# Windows this works perfectly — WinMerge opens with the temp file vs local
# file, edits are detected and uploaded. Under Wine, it fails because
# PowerShell is not available. The extension either does nothing or falls
# back to prompting for a new session.
#
# WHY IS THIS SO COMPLICATED:
# Simply replacing PowerShell with a batch script isn't enough. Wine's
# virtual filesystem creates a subtle problem: when WinSCP downloads
# a remote file to a temp location, the file exists inside Wine's VFS
# but is NOT visible to native Linux processes. A native diff tool
# launched via Wine's `start /unix` simply cannot see the temp file.
# The workaround: a batch script running inside Wine (where the file IS
# visible) copies it to /tmp on the real Linux filesystem, then launches
# the native diff tool. When the tool closes, modifications are copied
# back to Wine's temp location so WinSCP detects the change and uploads.
#
# WHAT THIS INSTALLER DOES:
# - Detects WinSCP installation path and Wine prefix automatically
# - Checks for Meld, offers to install it (pacman/apt/dnf/zypper/flatpak)
# - Creates a .WinSCPextension.bat that replaces the broken built-in
# - Creates a Wine batch wrapper that bridges Wine VFS → Linux filesystem
# - Creates a native Linux helper script for launching Meld
# - Updates WinSCP.ini (disables broken extension, adds Meld custom command)
# - Backs up WinSCP.ini before any changes
#
# AFTER INSTALLATION:
# - Compare Files: Commands menu > Compare Files (Shift+Ctrl+Alt+C)
# - Meld command: Commands menu > Custom Commands > Meld
# - WinSCP blocks while Meld is open (same as native Windows behavior)
# - Edits to the remote file in Meld are detected and uploaded by WinSCP
#
# REQUIREMENTS:
# - WinSCP running under Wine (portable mode with WinSCP.ini)
# - Meld (installed automatically if missing)
# - Bash 4+ (for associative arrays)
#
# TESTED WITH:
# - WinSCP 6.x, Wine 9.x/10.x
# - Arch Linux, Ubuntu, Fedora
#
# KEYWORDS (for searchability):
# WinSCP Wine Linux, WinSCP compare files not working Wine,
# WinSCP extension Wine fix, WinSCP PowerShell Wine replacement,
# WinSCP Meld Linux, WinSCP diff tool Wine, WinSCP compare Wine broken,
# WinSCP new session error compare, WinSCP WinSCPnet.dll Wine
#
# Usage: ./winscp-meld-compare.sh [--winscp-path /path/to/WinSCP]
# [--wine-prefix ~/.wine]
# [--uninstall]
#
# License: MIT
#
set -euo pipefail
# --- Colors ---
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
BOLD='\033[1m'
NC='\033[0m'
info() { echo -e "${BLUE}::${NC} $*"; }
ok() { echo -e "${GREEN}::${NC} $*"; }
warn() { echo -e "${YELLOW}:: WARNING:${NC} $*"; }
err() { echo -e "${RED}:: ERROR:${NC} $*" >&2; }
die() { err "$*"; exit 1; }
# --- Defaults ---
WINSCP_PATH=""
WINE_PREFIX="${WINEPREFIX:-$HOME/.wine}"
UNINSTALL=0
# --- Parse args ---
while [[ $# -gt 0 ]]; do
case "$1" in
--winscp-path) WINSCP_PATH="$2"; shift 2 ;;
--wine-prefix) WINE_PREFIX="$2"; shift 2 ;;
--uninstall) UNINSTALL=1; shift ;;
--help|-h)
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " --winscp-path PATH Path to WinSCP installation directory"
echo " --wine-prefix PATH Wine prefix (default: \$WINEPREFIX or ~/.wine)"
echo " --uninstall Remove the Compare Files extension"
echo " --help Show this help"
exit 0
;;
*) die "Unknown option: $1" ;;
esac
done
# --- Detect WinSCP ---
find_winscp() {
local candidates=(
"/opt/WinSCP"
"$HOME/WinSCP"
"$WINE_PREFIX/drive_c/Program Files/WinSCP"
"$WINE_PREFIX/drive_c/Program Files (x86)/WinSCP"
)
for dir in "${candidates[@]}"; do
if [[ -f "$dir/WinSCP.exe" ]]; then
echo "$dir"
return 0
fi
done
# Search Wine prefix
local found
found=$(find "$WINE_PREFIX/drive_c" -maxdepth 3 -name "WinSCP.exe" -type f 2>/dev/null | head -1)
if [[ -n "$found" ]]; then
dirname "$found"
return 0
fi
# Search common system paths
found=$(find /opt /usr/local -maxdepth 2 -name "WinSCP.exe" -type f 2>/dev/null | head -1)
if [[ -n "$found" ]]; then
dirname "$found"
return 0
fi
return 1
}
if [[ -z "$WINSCP_PATH" ]]; then
info "Searching for WinSCP installation..."
WINSCP_PATH=$(find_winscp) || die "WinSCP not found. Use --winscp-path to specify the location."
fi
[[ -f "$WINSCP_PATH/WinSCP.exe" ]] || die "WinSCP.exe not found in $WINSCP_PATH"
[[ -f "$WINSCP_PATH/WinSCP.ini" ]] || die "WinSCP.ini not found in $WINSCP_PATH (portable mode required)"
[[ -d "$WINE_PREFIX/drive_c" ]] || die "Wine prefix not found at $WINE_PREFIX"
ok "WinSCP found at: ${BOLD}$WINSCP_PATH${NC}"
ok "Wine prefix: ${BOLD}$WINE_PREFIX${NC}"
INI="$WINSCP_PATH/WinSCP.ini"
TOOLS_DIR="$WINE_PREFIX/drive_c/winscp-tools"
BIN_DIR="$HOME/.local/bin"
EXTENSION="$WINSCP_PATH/CompareFiles.WinSCPextension.bat"
WAIT_SCRIPT="$BIN_DIR/winscp-meld-wait"
BAT_FILE="$TOOLS_DIR/meld-ext.bat"
# --- Uninstall ---
if [[ "$UNINSTALL" -eq 1 ]]; then
info "Uninstalling Compare Files extension..."
rm -f "$EXTENSION" && ok "Removed extension file" || true
rm -f "$WAIT_SCRIPT" && ok "Removed wait script" || true
rm -f "$BAT_FILE" && ok "Removed bat wrapper" || true
if grep -q 'commonext\\CompareFiles' "$INI" 2>/dev/null; then
sed -i 's/ExtensionsDeleted=commonext\\CompareFiles/ExtensionsDeleted=/' "$INI"
ok "Re-enabled built-in CompareFiles extension in WinSCP.ini"
fi
ok "Uninstall complete. Restart WinSCP to apply changes."
exit 0
fi
# --- Check/Install Meld ---
install_meld() {
local pm=""
if command -v pacman &>/dev/null; then
pm="pacman"
elif command -v apt &>/dev/null; then
pm="apt"
elif command -v dnf &>/dev/null; then
pm="dnf"
elif command -v zypper &>/dev/null; then
pm="zypper"
fi
if [[ -n "$pm" ]]; then
echo ""
read -rp "$(echo -e "${YELLOW}Install Meld using $pm? [Y/n]${NC} ")" answer
answer="${answer:-y}"
if [[ "$answer" =~ ^[Yy] ]]; then
case "$pm" in
pacman) sudo pacman -S --needed meld ;;
apt) sudo apt install -y meld ;;
dnf) sudo dnf install -y meld ;;
zypper) sudo zypper install -y meld ;;
esac
else
die "Meld is required. Install it manually and re-run this script."
fi
elif command -v flatpak &>/dev/null; then
echo ""
read -rp "$(echo -e "${YELLOW}Install Meld via Flatpak? [Y/n]${NC} ")" answer
answer="${answer:-y}"
if [[ "$answer" =~ ^[Yy] ]]; then
flatpak install -y flathub org.gnome.meld
else
die "Meld is required. Install it manually and re-run this script."
fi
else
die "No supported package manager found. Install Meld manually and re-run this script."
fi
}
MELD_BIN=""
if command -v meld &>/dev/null; then
MELD_BIN=$(command -v meld)
ok "Meld found: ${BOLD}$MELD_BIN${NC}"
elif flatpak list 2>/dev/null | grep -q org.gnome.meld; then
MELD_BIN="flatpak run org.gnome.meld"
ok "Meld found (Flatpak)"
else
warn "Meld is not installed."
install_meld
if command -v meld &>/dev/null; then
MELD_BIN=$(command -v meld)
elif flatpak list 2>/dev/null | grep -q org.gnome.meld; then
MELD_BIN="flatpak run org.gnome.meld"
else
die "Meld installation failed."
fi
ok "Meld installed: ${BOLD}$MELD_BIN${NC}"
fi
# --- Backup WinSCP.ini ---
BACKUP="$INI.bak.$(date +%Y%m%d%H%M%S)"
cp "$INI" "$BACKUP"
ok "Backed up WinSCP.ini to ${BOLD}$(basename "$BACKUP")${NC}"
# --- Create directories ---
mkdir -p "$TOOLS_DIR" "$BIN_DIR"
# --- Create bat wrapper ---
# This runs inside Wine: copies the Wine-only temp file to /tmp,
# launches Meld, waits for it to close, copies changes back.
info "Creating bat wrapper..."
cat > "$BAT_FILE" << 'BATEOF'
@echo off
setlocal
set "REMOTE=%~1"
set "LOCAL=%~2"
rem Create temp dir on the Linux filesystem via Z: drive
set "TMPDIR=Z:\tmp\winscp-compare-%RANDOM%"
mkdir "%TMPDIR%"
rem Copy the remote temp file (only visible in Wine) to Linux filesystem
set "REMOTENAME=%~nx1"
copy "%REMOTE%" "%TMPDIR%\%REMOTENAME%" >nul
rem Launch meld (non-blocking) - wrapper creates .done marker when it closes
start "" /unix @WAIT_SCRIPT@ "%TMPDIR%" "%TMPDIR%\%REMOTENAME%" "%LOCAL%"
rem Wait for meld to close by polling for the .done marker
:waitloop
if exist "%TMPDIR%\.done" goto cleanup
ping -n 2 127.0.0.1 >nul
goto waitloop
:cleanup
rem Copy potentially modified file back to WinSCP temp so it detects the change and uploads
copy /y "%TMPDIR%\%REMOTENAME%" "%REMOTE%" >nul 2>&1
rmdir /s /q "%TMPDIR%" >nul 2>&1
endlocal
BATEOF
# Replace placeholder with actual path
sed -i "s|@WAIT_SCRIPT@|$WAIT_SCRIPT|g" "$BAT_FILE"
ok "Created: ${BOLD}$BAT_FILE${NC}"
# --- Create wait script ---
# Native Linux script that runs Meld and signals completion.
info "Creating wait script..."
cat > "$WAIT_SCRIPT" << WAITEOF
#!/bin/bash
# Wrapper: runs meld, creates .done marker when it exits
MARKER_DIR="\$1"; shift
convert_zpath() {
local p="\$1"
[[ "\$p" == [Zz]:* ]] && p="\${p:2}"
echo "\${p//\\\\//}"
}
FILE1=\$(convert_zpath "\$1")
FILE2=\$(convert_zpath "\$2")
$MELD_BIN "\$FILE2" "\$FILE1" || true
MARKER_DIR=\$(convert_zpath "\$MARKER_DIR")
touch "\$MARKER_DIR/.done"
WAITEOF
chmod +x "$WAIT_SCRIPT"
ok "Created: ${BOLD}$WAIT_SCRIPT${NC}"
# --- Create extension file ---
# Convert WinSCP path to Wine path for the @command
# If WinSCP is under Wine's C: drive, use C: path
# If it's on the Linux filesystem, use Z: path
info "Creating extension file..."
TOOLS_WINPATH="C:\\\\winscp-tools"
cat > "$EXTENSION" << EXTEOF
rem @name &Compare Files...
rem @command ${TOOLS_WINPATH}\\meld-ext.bat ! !^!
rem @side Local
rem @flag ApplyToDirectories
rem @description Compare local and remote file using Meld (Wine-compatible)
rem @version 3
rem @author WinSCP Compare Installer
rem @require WinSCP 5.13.4
rem @shortcut Shift+Ctrl+Alt+C
EXTEOF
ok "Created: ${BOLD}$EXTENSION${NC}"
# --- Update WinSCP.ini ---
info "Updating WinSCP.ini..."
# Disable built-in CompareFiles extension
if grep -q '^ExtensionsDeleted=$' "$INI"; then
sed -i '0,/^ExtensionsDeleted=$/s/^ExtensionsDeleted=$/ExtensionsDeleted=commonext\\CompareFiles/' "$INI"
ok "Disabled built-in CompareFiles extension"
elif grep -q 'ExtensionsDeleted=commonext\\CompareFiles' "$INI"; then
ok "Built-in CompareFiles already disabled"
else
# ExtensionsDeleted has other values, append
current=$(grep '^ExtensionsDeleted=' "$INI" | head -1 | cut -d= -f2-)
if [[ -n "$current" && "$current" != "0" ]]; then
sed -i "0,/^ExtensionsDeleted=/s/^ExtensionsDeleted=.*/ExtensionsDeleted=${current};commonext\\\\CompareFiles/" "$INI"
else
sed -i '0,/^ExtensionsDeleted=/s/^ExtensionsDeleted=.*/ExtensionsDeleted=commonext\\CompareFiles/' "$INI"
fi
ok "Disabled built-in CompareFiles extension"
fi
# Update ExtensionsPortableCount
if grep -q 'ExtensionsPortableCount=0' "$INI"; then
sed -i 's/ExtensionsPortableCount=0/ExtensionsPortableCount=1/' "$INI"
ok "Set ExtensionsPortableCount=1"
fi
# Add Meld custom command if not present
MELD_CMD="C:%5Cwinscp-tools%5Cmeld-ext.bat%20!%20!^!"
if ! grep -q '^\[Configuration\\CustomCommands\]' "$INI"; then
warn "CustomCommands section not found in WinSCP.ini — skipping custom command setup"
elif ! grep -q "^Meld=" "$INI"; then
sed -i "/^\[Configuration\\\\CustomCommands\]/a Meld=$MELD_CMD" "$INI"
# Add params if section exists
if grep -q '^\[Configuration\\CustomCommandsParams\]' "$INI"; then
sed -i '/^\[Configuration\\CustomCommandsParams\]/a Meld=257' "$INI"
fi
ok "Added Meld custom command"
else
ok "Meld custom command already present"
fi
# --- Summary ---
echo ""
echo -e "${GREEN}${BOLD}Installation complete!${NC}"
echo ""
echo " Extension: $EXTENSION"
echo " Bat wrapper: $BAT_FILE"
echo " Wait script: $WAIT_SCRIPT"
echo " INI backup: $BACKUP"
echo ""
echo -e " ${BOLD}Restart WinSCP to activate.${NC}"
echo ""
echo " Compare Files: Commands > Compare Files (Shift+Ctrl+Alt+C)"
echo " Meld command: Commands > Custom Commands > Meld"
echo ""
echo " To uninstall: $0 --uninstall"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment