在电脑里看到的最讨厌的事情之一就是有的程序/组件的重复。Chromium 的 CEF 最垃圾,但是惹不起啊,只能忍受,但是别的东东,有时候还是要抗争一下。
例如 git。好些个工具都自己带这个玩意儿,但又有好些个总是要检查系统里有没有现成的。既然这样,给你们合二为一不好吗?于是,俺就把 GitHub Desktop 自带的 git 放到了环境变量里。
好日子没过几天,某次突然发现在命令行里找不到 git 了。再一看,GitHub Desktop 升级了,它的安装路径下,有一个入口可执行程序,与之平级会有不止一个 app-x.y.x 的目录(很显然,x.y.z 是版本号),每个目录下都是一份完整的对应版本。一般来说,会总是保留最近的两个版本,大概是为了出了问题好回退。这样交替更迭几回,那么死守着旧版本的环境变量总归会失效,需要手动去系统设置里修改一下路径,把版本号改到最新的一版。烦。
于是请 WorkBuddy 来帮忙,让它写个批处理(本人一般不会动用 PowerShell 这种重器)。使用它这两天优惠着的 Space-Bunny 模型,好家伙,批处理里面的坑把它折腾的死去活来,整整迭代了九轮,才算出来个能用的,看了下大小,接近 20KB,是我这辈子产出的个头最大的批处理文件了。
思路特简单,就是在 app-x.y.z 平级创建一个名为 app-latest 的 Junction(这个单词把老夫搞住了,不知道怎么翻译,之前以为是软链接,但 AI 说不是),如果在命令行里找不到 git 了,就把批处理执行一下,没有这个 Junction 则创建之,有则更新之,而系统环境变量里永远是 latest 不需要改。
事情就是这么巧。昨天刚刚把批处理搞定,app-latest 创建好,也指向了最新的 app-3.6.6 目录,GitHub Desktop 工作正常。今天,就在刚刚,把窗口切换到 GitHub Desktop 时,突然弹框报错,说 git 找不到了。拿脚趾头想都知道跟俺昨天的移花接木小手术有关。那个弹框是源源不断的,鼠标点击永远点不完。这个时候要上手速,Alt-F4 连续按,症状消失。到安装目录里查看,果然 app-3.6.7 赫然在目。但这个时候,你如果运行安装目录里的入口程序,则没有任何效果,光标转一两秒后没有任何情况发生。但是,如果双进入到任一版本的目录中,再执行其中的可执行文件,GitHub Desktop 就会正常运行起来。
其实昨天写批处理的时候,跟 AI 已经就这个 Junction 的名字讨论过一番了,当时它害怕的是,GitHub Desktop 的升级程序会根据 app- 前缀来过滤出所有匹配的子目录,除去它自己标明为最新版的那个的话,其余的都会一概删除,而这里面一定会包括有 app-latest。老夫当时的逻辑是,app-latest 指向的不管是哪个版本,对于升级器来说一定是指向了一个旧版本,它删就删了。出于洁癖,命名依然定为 app-latest。但今天的铁定事实证明,入口程序也会被 app-latest 影响:因为当俺把 app-latest 的首字母改掉以后,入口程序就也可以加载到正确的最新版本而成功运行起来了。
看来还得退避一舍,把批处理里的 app-latest 改成别的名字,就叫 ghd-latest 吧。
尽管问题因咱而起,但正如题目所说,开发团队实在是也相当粗疏,app- 的前缀之后,好歹也该检查一下版本格式的。为了表彰 WorkBuddy 虽然资质一般但毕竟勤勤恳恳,把它生成的批处理附录于此。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 |
@echo off rem =========================================================================== rem GitHub Desktop - ghd-latest directory-link maintainer (pure batch) rem rem Usage: rem ghd-app-latest.bat create/update the link as needed rem ghd-app-latest.bat -CheckOnly report only, change nothing rem ghd-app-latest.bat latest use a different link name rem ghd-app-latest.bat -Debug extra tracing while diagnosing rem rem Exit code: 0 ok | 2 install not found | 3 refused | 4 link failed | 1 error rem rem NOTE: keep this file pure ASCII with NO byte order mark. rem A BOM-less UTF-8 batch is read by cmd.exe byte by byte, and CJK rem text gets chopped into fragments that are then parsed as commands rem (it produced errors like "'URTGT' is not recognized"). All prompts rem here are ASCII on purpose, so the file works under any code page. rem rem --------------------------------------------------------------------------- rem Implementation notes (all native cmd, no PowerShell): rem find install : reg query on uninstall keys, else %LOCALAPPDATA% rem match names : pure string substitution, no findstr and no regex. A rem caret-anchored /c:"^app-[0-9]$" never matches inside cmd, rem whatever findstr really receives is not what it looks like. rem pick version : split on dots, compare segment by segment numerically. rem Never sort / compare as string - that puts 3.6.10 before rem 3.6.9. Also never take the first entry of dir /o-n: rem that is time-ordered and our own ghd-latest sorts first. rem read target : fsutil reparsepoint query, parse the "Substitute Name" rem line. Do NOT use "Print Name" - for a junction its rem length is always 0. rem create link : mklink /J (directory junction). No mklink /D fallback: rem /D needs admin or developer mode, so it fails anyway. rem delete link : rmdir, never with the recursive switch. rem --------------------------------------------------------------------------- setlocal EnableExtensions EnableDelayedExpansion rem =========================================================================== rem Parse arguments rem =========================================================================== rem The default link name is "ghd-latest", NOT "app-latest". Squirrel removes rem stale version directories with a loose test for names CONTAINING "app-", rem and its recursive delete does not recognise reparse points. A link named rem app-latest therefore gets destroyed on the next GitHub Desktop update, rem and the delete follows the junction into the real version directory and rem removes files from it. That was observed happening on this machine. rem "ghd-latest" contains no "app-" and stays outside that sweep entirely. set "LINKNAME=ghd-latest" set "CHECKONLY=0" set "DEBUG=0" :ParseArgs if "%~1"=="" goto AfterArgs if /i "%~1"=="-CheckOnly" (set "CHECKONLY=1" & shift & goto ParseArgs) if /i "%~1"=="/CheckOnly" (set "CHECKONLY=1" & shift & goto ParseArgs) if /i "%~1"=="-Debug" (set "DEBUG=1" & shift & goto ParseArgs) if /i "%~1"=="/Debug" (set "DEBUG=1" & shift & goto ParseArgs) set "LINKNAME=%~1" shift goto ParseArgs :AfterArgs if "%DEBUG%"=="1" echo (debug mode on) set "RC=0" set "E_NOFIND=2" set "E_REFUSE=3" set "E_LINKFAIL=4" echo. echo GitHub Desktop - ghd-latest link maintainer echo -------------------------------------------- echo Link name: %LINKNAME% rem =========================================================================== rem Guard: the link name must not look like a version directory rem rem Done with substitution, not findstr. A caret-anchored regex on a cmd rem command line is unreliable (see the note at :ConsiderDir), and a guard rem that silently never fires is worse than no guard at all. rem Rule: reject if the name starts with "app-" followed by a digit. rem rem Written as a subroutine that returns E_GUARD=1 when it must refuse, so rem that no goto ever appears inside a nested if/for block. rem =========================================================================== set "E_GUARD=0" call :CheckGuard if "%E_GUARD%"=="1" ( echo x Link name "%LINKNAME%" clashes with the app-VERSION naming rule. echo Refusing to run. Pick another name, e.g. ghd-app-latest.bat latest set "RC=%E_REFUSE%" goto Finish ) rem =========================================================================== rem 1. Locate the install root rem =========================================================================== echo. echo [ Locate install ] set "ROOT=" for /f "tokens=2,*" %%A in ('reg query "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" /s /v InstallLocation 2^>nul ^| findstr /i /c:"REG_SZ"') do ( if exist "%%~B\GitHubDesktop.exe" set "ROOT=%%~B" ) if not defined ROOT if exist "%LOCALAPPDATA%\GitHubDesktop\GitHubDesktop.exe" set "ROOT=%LOCALAPPDATA%\GitHubDesktop" if not defined ROOT ( echo x GitHub Desktop install directory not found. echo Checked: HKCU uninstall InstallLocation and %%LOCALAPPDATA%%\GitHubDesktop set "RC=%E_NOFIND%" goto Finish ) echo + Install root: %ROOT% rem =========================================================================== rem 2. Enumerate version directories rem =========================================================================== echo. echo [ Version directories ] set "CAND=" for /f "delims=" %%D in ('dir /b /ad "%ROOT%" 2^>nul') do call :ConsiderDir "%%D" if not defined CAND ( echo x No complete and usable app-VERSION directory found. set "RC=%E_NOFIND%" goto Finish ) rem =========================================================================== rem 3. Pick the highest version (numeric, segment by segment) rem =========================================================================== echo. set "BEST=" for %%D in (%CAND%) do call :PickBest "%%D" if not defined BEST ( echo x Cannot determine the effective version. set "RC=%E_NOFIND%" goto Finish ) echo + Effective version: !BEST! rem =========================================================================== rem 4. Inspect the existing link rem =========================================================================== echo. echo [ Link state ] set "LINK=%ROOT%\!LINKNAME!" if exist "%LINK%\" ( call :ReadTarget if defined CURTGT ( echo !LINKNAME! points to: !CURTGT! if /i "!CURTGT!"=="%ROOT%\!BEST!" ( echo. echo [ Result ] echo Already up to date. Nothing to do. set "RC=0" goto Finish ) echo * !LINKNAME! points to an older directory, repointing it to !BEST! ... ) else ( rem Target unreadable: if it is a link, it is broken (rebuild it); rem if dir cannot even list it, it is a real directory (refuse). dir /a:l "%LINK%" >nul 2>&1 if errorlevel 1 ( echo x "%LINKNAME%" exists but is NOT a link, it is a real directory. echo Refusing to delete it. Please inspect it manually. set "RC=%E_REFUSE%" goto Finish ) echo * Found a broken link, no readable target, rebuilding. ) ) else ( echo !LINKNAME! does not exist yet. echo Creating it to point to !BEST! ... ) if "%CHECKONLY%"=="1" ( echo. echo [ Result ] echo CheckOnly mode, nothing was changed. echo It should point to: %ROOT%\!BEST! set "RC=0" goto Finish ) rem =========================================================================== rem 5. Risk warning rem =========================================================================== rem WARN=1 when the chosen link name contains app-, which puts the link rem inside Squirrel's stale-version sweep. That is not theoretical: a link rem named app-latest was destroyed by a real GitHub Desktop update, and the rem recursive delete followed the junction and emptied the old version rem directory it pointed at. The default ghd-latest avoids it entirely. rem rem When the chosen name IS out of range, also check whether a leftover link rem from the old default is still sitting there, and say so. set "WARN=0" call :ContainsAppDash if "%WARN%"=="1" ( echo. echo [ Risk warning ] echo * Squirrel removes old versions with a loose "name contains app-" echo * test, not an exact app-DIGIT match, so "%LINKNAME%" is in range. echo * A GitHub Desktop update can delete this link, and the recursive echo * delete does not recognise reparse points, so it follows the link echo * into the real version directory and removes files from it. echo Observed happening with the previous name "app-latest". echo Workaround: ghd-app-latest.bat latest ) rem leftover check: the old default name is dangerous, flag it if still present if not "%LINKNAME%"=="app-latest" ( if exist "%ROOT%\app-latest\" ( echo. echo [ Leftover warning ] echo * %ROOT%\app-latest still exists. echo * It is inside the Squirrel sweep and its target directory may echo * already have been emptied. Delete it, you will not need it. ) ) rem =========================================================================== rem 6. Rebuild the link rem =========================================================================== echo. echo [ Update link ] if exist "%LINK%\" ( rem rmdir removes the link entry itself. rem Never add the recursive switch: it would empty the real target. rmdir "%LINK%" >nul 2>&1 if exist "%LINK%\" ( echo x Failed to remove the old link, permission denied? set "RC=%E_LINKFAIL%" goto Finish ) echo Old link removed, target directory untouched. if not exist "%ROOT%\!BEST!\GitHubDesktop.exe" ( echo x Target vanished during the operation, aborting. set "RC=%E_NOFIND%" goto Finish ) ) echo Creating directory junction with mklink /J ... rem No mklink /D fallback: it needs admin or developer mode, so on a normal rem user session it always fails. A junction is enough for a local NTFS dir. mklink /J "%LINK%" "%ROOT%\!BEST!" >nul 2>&1 if not errorlevel 1 goto Verify echo x mklink /J failed, check free disk space and permissions. set "RC=%E_LINKFAIL%" goto Finish :Verify call :ReadTarget if not defined CURTGT ( echo x Cannot read the target after creating the link. set "RC=%E_LINKFAIL%" goto Finish ) if /i not "%CURTGT%"=="%ROOT%\!BEST!" ( echo x Verification failed, actual target: %CURTGT% set "RC=%E_LINKFAIL%" goto Finish ) if not exist "%LINK%\GitHubDesktop.exe" ( echo x GitHubDesktop.exe is not reachable through the link. set "RC=%E_LINKFAIL%" goto Finish ) echo. echo [ Result ] echo + Path : %LINK% echo + Target: !CURTGT! echo + Verified: GitHubDesktop.exe is reachable through the link set "RC=0" goto Finish rem =========================================================================== rem Finish: single exit point, pause only when double-clicked rem =========================================================================== :Finish echo. if "%RC%"=="0" echo [OK] Done, the link points at the effective version. if "%RC%"=="1" echo [ERR] Script error, see the details above. if "%RC%"=="2" echo [ERR] GitHub Desktop install not found, nothing changed. if "%RC%"=="3" echo [ERR] Refused on safety grounds, see the reason above. if "%RC%"=="4" echo [ERR] Link creation failed, see the reason above. rem CMDCMDLINE holds this file name when double-clicked, and does not when rem invoked from an existing prompt - that is how we tell the two apart. echo %CMDCMDLINE% | find /i "%~nx0" >nul && pause endlocal & exit /b %RC% rem =========================================================================== rem :ContainsAppDash -- set WARN=1 when LINKNAME contains "app-" rem =========================================================================== :ContainsAppDash set "WARN=0" set "WCOPY=!LINKNAME!" set "WFOUND=" :ContainsLoop if "!WCOPY!"=="" goto ContainsDone if not defined WFOUND ( if "!WCOPY:~0,4!"=="app-" set "WFOUND=1" ) set "WCOPY=!WCOPY:~1!" goto ContainsLoop :ContainsDone if defined WFOUND set "WARN=1" exit /b 0 rem =========================================================================== rem :CheckGuard -- set E_GUARD=1 when LINKNAME looks like app-<digit>... rem =========================================================================== :CheckGuard set "E_GUARD=0" set "GHEAD=!LINKNAME:~0,4!" if not "!GHEAD!"=="app-" exit /b 0 set "GTAIL=!LINKNAME:~4!" if "!GTAIL!"=="" exit /b 0 rem the character right after app- must be a digit set "GONE=!GTAIL:~0,1!" if "!GONE!"=="0" set "E_GUARD=1" & exit /b 0 if "!GONE!"=="1" set "E_GUARD=1" & exit /b 0 if "!GONE!"=="2" set "E_GUARD=1" & exit /b 0 if "!GONE!"=="3" set "E_GUARD=1" & exit /b 0 if "!GONE!"=="4" set "E_GUARD=1" & exit /b 0 if "!GONE!"=="5" set "E_GUARD=1" & exit /b 0 if "!GONE!"=="6" set "E_GUARD=1" & exit /b 0 if "!GONE!"=="7" set "E_GUARD=1" & exit /b 0 if "!GONE!"=="8" set "E_GUARD=1" & exit /b 0 if "!GONE!"=="9" set "E_GUARD=1" & exit /b 0 exit /b 0 rem =========================================================================== rem :ConsiderDir "<dirname>" -- keep it in CAND if it is a usable version dir rem rem This is a subroutine on purpose. It used to be a label INSIDE the rem "for /f ... do ( ... )" block, with "goto SkipD" as the skip path. That rem is the classic batch trap: goto into a block label leaves the parser in rem the wrong context, so the block's closing ")" is reported as rem ") was unexpected at this time.". A goto that jumps OUT of a block is rem fine; jumping INTO one is not. Using "call" keeps every goto outside. rem rem Same reason it does not use setlocal: it must append to the caller's rem CAND list. rem rem NOTE: the argument is held in DNAME, never in CD. "%CD%" is a cmd dynamic rem variable that expands to the current working directory. rem rem The name test uses string substitution, NOT findstr and NOT a regex. rem Debug output proved that inside cmd a double quoted "/c:^app-[0-9][0-9.]*$" rem never matches, whatever findstr receives is not what we think it gets rem (caret and dollar are cmd metacharacters on that command line). rem Substitution has no such ambiguity: rem 1. must start with app- rem 2. strip app-, the remainder must not be empty rem 3. the remainder must consist only of digits and dots rem Verified to accept exactly the same set as the old regex on real data, rem plus it additionally rejects mixed case like APP-1.2. rem =========================================================================== :ConsiderDir set "DNAME=%~1" rem --- name test by substitution, see the note above --- rem must begin with app- if "!DNAME:~0,4!" neq "app-" exit /b 0 rem strip the app- prefix, the remainder must not be empty set "TAIL=!DNAME:~4!" if "!TAIL!"=="" exit /b 0 rem delete every digit and dot; whatever survives must be empty. rem Written out longhand instead of a "for %%d in (...)" loop: this routine is rem reached through CALL, and loop-variable expansion inside a called rem subroutine is one of the places where it is easy to get the wrong value. set "REST=!TAIL!" set "REST=!REST:0=!" set "REST=!REST:1=!" set "REST=!REST:2=!" set "REST=!REST:3=!" set "REST=!REST:4=!" set "REST=!REST:5=!" set "REST=!REST:6=!" set "REST=!REST:7=!" set "REST=!REST:8=!" set "REST=!REST:9=!" set "REST=!REST:.=!" if not "!REST!"=="" exit /b 0 if not exist "!ROOT!\!DNAME!\GitHubDesktop.exe" exit /b 0 if exist "!ROOT!\!DNAME!\.dead" exit /b 0 if exist "!ROOT!\!DNAME!\.not-finished" exit /b 0 set "CAND=!CAND! !DNAME!" echo !DNAME! exit /b 0 rem =========================================================================== rem :ReadTarget -- read the reparse point target of %LINK% into CURTGT rem rem fsutil prints: rem Substitute Name: \??\C:\path\to\app-3.6.6 rem tokens=2,* skips the two words "Substitute" and "Name:", so %%B is the rem path. The "Print Name" line is unusable, its length is 0 for junctions. rem Do NOT use setlocal here, CURTGT could not reach the caller. rem =========================================================================== :ReadTarget set "CURTGT=" set "SUB=" for /f "tokens=2,*" %%A in ('fsutil reparsepoint query "%LINK%" 2^>nul ^| findstr /i /c:"Substitute Name:"') do set "SUB=%%B" if not defined SUB exit /b 0 rem strip the \??\ reparse prefix to get a normal drive path. rem Delayed expansion is required here: with "%SUB:...=%" the percent form is rem expanded while the line is read, so it would pick up the value from the rem PREVIOUS call rather than the one just assigned on the line above. set "CURTGT=!SUB:\??\=!" rem %%~fP makes it absolute and drops the trailing backslash, so that the rem caller can compare it with plain string equality if defined CURTGT for %%P in ("!CURTGT!") do set "CURTGT=%%~fP" exit /b 0 rem =========================================================================== rem :PickBest "<dirname>" -- compare version numbers, replace BEST if larger rem rem CALL is used to reuse code, so every routine works on global variables and rem must NOT use setlocal (values would not travel back to the caller). rem Comparison uses set /a and deliberately no zero padding, because set /a rem reads a leading zero as octal and fails on 08. rem rem NOTE: the candidate is held in DNAME, never in CD - "%CD%" is a cmd rem dynamic variable holding the current working directory. rem =========================================================================== :PickBest set "DNAME=%~1" rem String substitution in the delayed-expansion family uses "!" at BOTH ends: rem !VAR:find=replace! rem Writing "%" at the end (as this used to: !DNAME:app-=%) makes cmd treat rem the trailing "%" as the start of a percent expansion, the substitution rem never happens, and CV ends up holding the literal text "DNAME:app-=". rem The debug output showed exactly that, which is why every version compared rem equal and the first candidate always won. set "CV=!DNAME:app-=!" if not defined BEST ( set "BEST=!DNAME!" goto PBEnd ) rem split the current BEST version into B1..B4 rem Each variable is assigned from its own explicit token index. An earlier rem version used "if not defined B1 set "B1=%%x"" and relied on the token rem variables rotating as the for loop iterated; that mixes parse-time rem (%%x) expansion with run-time ("if not defined") state and is fragile rem inside a CALLed routine. rem NOTE: every option of "for /f" must live inside ONE quoted string. Writing rem for /f "tokens=1" delims=. %%x splits it into two parts and cmd reports rem "delims was unexpected at this time." set "BV=!BEST:app-=!" set "B1=0" & set "B2=0" & set "B3=0" & set "B4=0" for /f "tokens=1 delims=." %%x in ("!BV!") do set "B1=%%x" for /f "tokens=2 delims=." %%x in ("!BV!") do set "B2=%%x" for /f "tokens=3 delims=." %%x in ("!BV!") do set "B3=%%x" for /f "tokens=4 delims=." %%x in ("!BV!") do set "B4=%%x" rem split the candidate version into C1..C4 set "C1=0" & set "C2=0" & set "C3=0" & set "C4=0" for /f "tokens=1 delims=." %%x in ("!CV!") do set "C1=%%x" for /f "tokens=2 delims=." %%x in ("!CV!") do set "C2=%%x" for /f "tokens=3 delims=." %%x in ("!CV!") do set "C3=%%x" for /f "tokens=4 delims=." %%x in ("!CV!") do set "C4=%%x" rem Segment by segment compare. Written flat on purpose: a chain of nested rem if-blocks with goto in between is easy to break, and jumping into or rem across block labels is exactly what caused ") was unexpected at this rem time." earlier. Here every goto stays at the top level of the routine. call :Seg !C1! !B1! if "!CMP!"=="LT" goto PBEnd if "!CMP!"=="GT" goto PBSet call :Seg !C2! !B2! if "!CMP!"=="LT" goto PBEnd if "!CMP!"=="GT" goto PBSet call :Seg !C3! !B3! if "!CMP!"=="LT" goto PBEnd if "!CMP!"=="GT" goto PBSet call :Seg !C4! !B4! if "!CMP!"=="GT" goto PBSet rem All four segments are equal: keep the earlier one. Two directories with rem the same version would be abnormal, and not overwriting is the safer rem choice. goto PBEnd :PBSet set "BEST=!DNAME!" :PBEnd exit /b 0 rem =========================================================================== rem :Seg <A> <B> -- numeric compare, result in CMP: GT / LT / EQ rem rem The operands arrive as command line arguments. "%~1" is the correct way rem to read them: the tilde modifier belongs to the percent family only, so rem "!~1!" expands to nothing at all. That silently made both operands 0, rem every comparison returned EQ, and BEST stayed on the first candidate rem (it picked app-3.6.5 over app-3.6.6). rem =========================================================================== :Seg set "SBA=%~1" set "SBB=%~2" if "!SBA!"=="" set "SBA=0" if "!SBB!"=="" set "SBB=0" rem "set /a X=X+0" normalises any stray leading zero, which set /a would rem otherwise read as octal. set /a "SBA=SBA+0" 2>nul set /a "SBB=SBB+0" 2>nul if !SBA! GTR !SBB! (set "CMP=GT") else if !SBA! LSS !SBB! (set "CMP=LT") else (set "CMP=EQ") exit /b 0 |
