Showing posts with label Tech. Show all posts
Showing posts with label Tech. Show all posts

Saturday, December 19, 2015

Self-Driving Cars Need To Hurry Up

google car by Trevor is licensed CC BY-SA 2.0
I recently came down with a moderately nasty sinus infection and had to go to urgent care to get diagnosed and get some antibiotics. Before I headed to urgent care, however, I had to choose from the following sub-optimal traveling options:
  • Have my wife drive me there, which would have required our son to be in the car with us. Since I wasn't sure if I had a sinus infection or the flu yet, this wasn't ideal.
  • Hire an Uber or taxi to take me there. I doubt the driver would have appreciated the company, no matter how well I tipped.
  • Call an ambulance. This was overkill and also prohibitively expensive.
  • Drive myself there with a mid-grade (100F) fever.
I opted for the latter option - thankfully, the fever wasn't serious enough to significantly affect my ability to drive safely and I live less than half a mile away from an urgent care facility. However, while driving there, it occurred to me that there were people sharing the road with me that might have been every bit as sick as I was - or even sicker. There could have been a single mother, sick with the flu, sharing a car (and air) with her healthy kid while she tried to get treatment. There could have been someone experiencing a stroke while driving down the road. There could have been someone with a much more serious fever - say, 103F or so - starting to hallucinate while driving down the freeway, or suddenly gripped by fevered paralysis. 

What would protect everyone? Self-driving cars.

Think about it. What if you were really sick - not quite sick enough to require someone to keep your vitals steady, but sick enough where you shouldn't drive (say, if you had dysentery or something), and you could just limp your way into your self-driving car and say, "Go to nearest Urgent Care." Then you could spend the trip focused on keeping warm and hydrated and know that you'll get to your destination safely without potentially losing lucidity and threatening everyone else on the road. The car could even make a loud noise when you got there so you could take a quick nap.

Unfortunately, California's decided to take the slow road on this.

I don't entirely blame California here - Google's cars have been somewhat accident-prone, and identifying who has liability in an accident is still an open question - but still, the sooner this technology takes off, the sooner we can stop worrying about DUIs, strokes, seizures, and other health issues affecting and endangering drivers.

Thursday, December 10, 2015

Odds and ends


Hey everyone - I have a few posts rummaging around in the background (including a follow-up to my last post), but until they're done, here are a couple things to tie everyone over:

I've been posting on the LP Nevada's blog lately:
The first post is a more serious look at an issue raised by Marc Randazza on Popehat, though, for the record, I didn't read his article before I posted mine, which is a shame since he made my point better than I did. Moving forward, though, there's a strong chance that most of my political blogging is going to end up on the LP Nevada blog unless I feel the need to be contrarian and chew certain libertarians a new one.

On a more tech-ish note, we're being encouraged at work to decorate our offices. Here's the Christmas tree I put up on our door:


Though it's not obvious, there are some hidden spaces in there - if you want to run this yourself, copy and paste the following:

@ECHO OFF
CLS
SETLOCAL EnableDelayedExpansion
FOR /L %%G IN (1,1,10) DO (
SET _Str=*
FOR /L %%H IN (1,1,10) DO (
IF %%H LSS %%G (
SET _Str=*!_Str!*
) ELSE (
SET _Str= !_Str!
)
)
ECHO:!_Str!
)

SET _Str=

FOR /L %%I IN (1,1,20) DO (
IF %%I GEQ 10 (
IF %%I LEQ 12 (
SET _Str=!_Str!=
) ELSE (
SET _Str=!_Str! 
)
) ELSE (
SET _Str=!_Str! 
)
)
ECHO:%_Str%

Until next time...

Thursday, May 14, 2015

Stupid PowerShell Tricks: Launching an Elevated CMD Prompt from CMD

A non-elevated prompt creating an elevated prompt
One of the minor annoyances of working in a Windows environment is that, though RunAs is a closer approximation of sudo than it ever was back in the days of Windows XP, it still won't give you UAC-accepted Administrator access (i.e. the Windows equivalent of "root").

PowerShell, on the other hand, labors under no such restrictions, thanks to the Start-Process cmdlet.

As luck would have it, Start-Process includes a -Verb argument, which accepts the following:

File type Verbs 
--------- ------- 
.cmd------Edit, Open, Print, Runas 
.exe------Open, RunAs 
.txt------Open, Print, PrintTo 
.wav------Open, Play

If you feed it the Runas flag, PowerShell will dutifully run whatever process you feed it with an administrative prompt. Consequently, if you feed powershell.exe the following within CMD:

powershell.exe -Command "Start-Process cmd.exe -Verb RunAs"

You'll end up with an administrative CMD prompt.

Wednesday, April 15, 2015

Remotely Rearming Microsoft Office 2010/2013

Tesla Robot Dance by Steve Jurvetson is licenses CC BY 2.0.
When it comes to work, I live and die by the following motto:
If it's worth doing twice, it's worth doing automatically.
With that motto in mind, I ran into an issue. When setting up our computer lab image, I forgot to rearm the source image's Microsoft Office installations - this caused all of our lab PCs to share the same Office activation ID, which in turn led our KMS server to attempt to activate them as if they were all the same computer. Consequently, within a week, I faced some rather confused coworkers who were wondering why Microsoft Office was telling them that it wasn't properly licensed. Meanwhile, whenever I fired up VAMT to perform a volume activation via KMS, each of the client PCs reported the following error message:
0xC004F038 The software Licensing Service reported that the computer could not be activated. The count reported by your Key Management System (KMS) is insufficient. Please contact your system administrator.
When I checked the activation count on our KMS server for Microsoft Office (cscript slmgr.vbs /dlv all), I noticed that the current count was 1 - since we definitely have more than one computer in our computer lab, something clearly wasn't right. This led to a bit of Google sleuthing, which revealed articles that addressed this very issue for Microsoft Office 2010 and Microsoft Office 2013. Trouble was, I was in no position to re-image all of our computer labs, nor was I in a mood to walk up to an innumerable number of PCs and manually run a script, nor was I in a mood to reboot every single computer in the building.

Luckily, I didn't have to. Thanks to psexec, a rather handy part of the Sysinternals Suite, I was able to modify the script for remote execution:

@ECHO OFF
SETLOCAL EnableDelayedExpansion

SET _OSPPreArmPath="C:\Program Files (x86)\Common Files\Microsoft Shared\OfficeSoftwareProtectionPlatform\OSPPREARM.EXE"
SET _OSPP10Path="C:\Program Files (x86)\Microsoft Office\Office14\ospp.vbs"
SET _OSPP13Path="C:\Program Files (x86)\Microsoft Office\Office15\ospp.vbs"

FOR /F "tokens=2 delims=,= usebackq" %%G IN (`dsquery computer ou^=Your PC OU^,dc^=YourInternalDomain^,dc^=YourInternalDomainSuffix -limit 0`) DO (
        SET _Target=^\^\%%G
        ECHO:Rearming Office...
        PSEXEC !_Target! %_OSPPreArmPath%
        PSEXEC !_Target! cscript.exe /nologo %_OSPP10Path% /act
        PSEXEC !_Target! cscript.exe /nologo %_OSPP13Path% /act
        PSEXEC !_Target! reg add "HKLM\Software\Microsoft\Office\14.0\Common\OSPPREARM" /f
        PSEXEC !_Target! reg add "HKLM\Software\Microsoft\Office\15.0\Common\OSPPREARM" /f
)


Some caveats:
  • This assumes that you're using a 32-bit installation of Microsoft Office on your PCs, which, unless you're dealing with really big Excel files or something, you probably should be.
  • It assumes that all of your PCs are in the same OU, or at least are all nested inside the same OU. Note that you can run this on a parent OU and it'll work on all PCs in any child OUs.
  • Make sure to fill in YourInternalDomain and YourInternalDomainSuffix with information appropriate for your environment.
  • You'll need to run this script in an administrative command prompt from a directory that contains psexec - or, alternatively, you'll need to copy psexec to some place previously listed in your PATH, or add the location of psexec to your PATH.
  • All of the affected computers will need to be on.
  • I personally found that, even after running this script, it wasn't a bad idea to double-check licensing information in VAMT and reactivate any PCs that were Out of Grace. I will note, though, that the KMS server actually did hand out licenses successfully to those PCs after running this script.
If you need to run the script on a particular computer:

@ECHO OFF
SETLOCAL EnableDelayedExpansion

SET _OSPPreArmPath="C:\Program Files (x86)\Common Files\Microsoft Shared\OfficeSoftwareProtectionPlatform\OSPPREARM.EXE"
SET _OSPP10Path="C:\Program Files (x86)\Microsoft Office\Office14\ospp.vbs"
SET _OSPP13Path="C:\Program Files (x86)\Microsoft Office\Office15\ospp.vbs"

SET _Target=^\^\%1
ECHO:Rearming Office...
PSEXEC %_Target% %_OSPPreArmPath%
PSEXEC %_Target% cscript.exe /nologo %_OSPP10Path% /act
PSEXEC %_Target% cscript.exe /nologo %_OSPP13Path% /act
PSEXEC %_Target% reg add "HKLM\Software\Microsoft\Office\14.0\Common\OSPPREARM" /f
PSEXEC %_Target% reg add "HKLM\Software\Microsoft\Office\15.0\Common\OSPPREARM" /f


Then just save the script and call it with scriptname.cmd computername (e.g. reactivateoffice.cmd LABPC-1). The same caveats as above more or less apply.

Friday, April 3, 2015

10 Best Places in Reno to Play PAC-MAN in Google Maps

This year's April Fool's Day "prank" from Google - a long and storied tradition - was adding PAC-MAN to Google Maps, which, of course, inevitably led to several articles like Wired's The Top 15 Spots To Play PAC-MAN In Google Maps. Of course, all of these lists focus on trendy, hip locations, like New York, San Francisco, Boston, or other trendy, hip locations that house online journalists or that online journalists aspire to live in, which is a shame - there are plenty of great places to play PAC-MAN that don't involve places with $40 parking.

With that in mind, here are the ten best places I could find in Reno (okay, "Reno/Sparks and other nearby areas"), in alphabetical order:

Arrowcreek


I honestly expected more from Arrowcreek, along with other developments of its type. I figured the winding paths would make for some interesting PAC-MAN mazes; however, it turns out that Google's implementation of the game strongly prefers dense, packed neighborhoods compared to sprawling, spread out ones. Even so, Arrowcreek made for a few fun rounds - East Desert Canyon Drive makes for an excellent escape from the ghosts based on High Vista and Indian Ridge.

It gets better from here, though.

California Ave.


Now we're talking. The triangle where Liberty, Arlington and California meet was an absolute hellscape of frantic maneuvering and button mashing. Meanwhile, the old Southwest streets led to some rather interesting escapes.

Caughlin Parkway


Though most of the newer subdivisions didn't lend themselves to effective PAC-MANing due to their relative lack of density, the shopping area around Caughlin Parkway lent itself nicely, especially when McCarran Boulevard was included. Thankfully, ghosts can't change lanes - less thankfully, neither can you if you have ghosts coming at you from both sides of McCarran.

The Legends at Sparks


This was actually the first place I played PAC-MAN on, in no small part because of the roundabouts and because I drive by the place every time I go to work. Between Sparks Boulevard's three lanes on the right, the various roundabouts and exits, and the random little blind corners by the big box retail stores (good luck cleanly navigating near Target), you're in for quite the ride.

Mountain View Cemetery


The fact that Google made it possible for me to run away from ghosts as Pac-Man in a cemetery tickled me to no end. That Mountain View Cemetery actually makes a surprisingly decent PAC-MAN maze was icing on the cake. Be careful, though - all northbound lanes lead to the entrance to the cemetery on the right.

Pyramid & Victorian, Sparks


PAC-MAN really prefers older, denser neighborhoods, so I thought to myself, why not try a game on the oldest, densest neighborhood in Sparks? The results, like Sparks itself, were quite straightforward.

Stead & Silver Lake


The Sierra Shadows Mobile Home Park - the squarish area that the ghosts spawn in on the left - drew my eye, and I'm glad it did. It serves as a nice and dangerous counterpoint to the otherwise sprawling, meandering streets of the newer housing developments in Stead on the right.

University of Nevada, Reno


I don't think it would be an exaggeration to declare it a criminal offense to do something like this without including UNR somehow. I was rather surprised with how detailed Google Maps decided to make the maze out of UNR's footpaths - you can not only clearly see Lawlor (the round circle at the top), but you can also see the better part of UNR's parking and street system.

Verdi


Ah, Verdi... small, simple, straightforward, green. Much like the actual town itself, come to think of it.

Wingfield Park


Most of downtown Reno honestly doesn't map up very well due to the larger casinos, but, the older, tighter area by the river turned out nicely. Have fun navigating around the Truckee.

So, there you have it - the ten best places I could find to play PAC-MAN in or near Reno. If you, like me, are watching the progress bar move listlessly and need to kill some time, you could certainly do worse things than trying to find ten better ones.

Friday, February 27, 2015

Running Dentrix without Local Admin or UAC Disabled

Yep, this looks safe enough for me to put everyone's Social Security Number, medical history, and dental history into.
I used to support more than a few Dentrix installations back in the day. It was not fun - this particular Electronic Health Record (EHR) package, which is all the rage in dental practices across the US, requires local administrator access (see page 8) for all users. It's also strongly encouraged to disable User Account Control so that users aren't constantly bombarded with UAC prompts whenever they try to use the software. Needless to say, this is a highly less-than-ideal way of running a piece of software that's ostensibly designed for HIPAA compliant medical practices, which is why I was thrilled to no end when I changed jobs and stopped supporting dental IT.

Last Friday, I received an email. In it was a request that sent a chill down my spine:
We're teaching a class on Dentrix and need it installed in a computer lab.
Images of fresh-faced students with local administrator access on computer lab PCs filled my mind. Now, this isn't the first time I've faced this - the usual solution is to set the PC to dual-boot, with the administrator image being off of the student lab domain. It's not a perfect solution, but it at least keeps the damage to a minimum and protects our student lab servers. However, this solution only works with technologically sophisticated instructors and users; the instructors that needed Dentrix are sophisticated about Electronic Health Record systems, but not so much about proper Windows booting protocol.

I needed a better way. With some trial and error, I think I found one.

The first reason Dentrix requires local administrator rights is because it places its data folders in its installation directory, which, by default, is C:\Program Files (x86)\Dentrix [1], and whatever user is running Dentrix must have sufficient access to write to these directories. So, the first step to the solution is to make it possible for standard users to write in the standard installation folder:
cd %programfiles(x86)%
icacls Dentrix /grant "Users:(OI)(CI)(F)" /t
I also, just on the safe side, granted write access to everything else installed with Dentrix:
icacls "GURU LE" /grant "Users:(OI)(CI)(F)" /t
icacls "Guru Limited Edition Server" /grant "Users:(OI)(CI)(F)" /t
icacls "Business Objects" /grant "Users:(OI)(CI)(F)" /t
Next, we need to move the installed desktop shortcuts to places accessible by everyone instead of just the profile that Dentrix was installed under:
move %userprofile%\Desktop\*.lnk C:\Users\Public\Desktop
move "%userprofile%\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Dentrix Learning Edition" "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\"
move "%userprofile%\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\GURU Limited Edition" "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Guru Limited Edition"
Oh, and fix the permissions on them, too:
cd "C:\Users\Public\Desktop\"
icacls *.lnk /reset
cd "c:\ProgramData\Microsoft\Windows\Start Menu\Programs\"
icacls * /reset
That made it possible to run Dentrix as a standard user with UAC disabled. Now it was time to get around UAC. To do that, I followed the spirit of Ghack's guide on doing just that - I installed the Microsoft Application Compatibility Toolkit, then created Application Fixes enabled RunAsInvoker for each of Dentrix's client-centered applications, while explicitly avoiding the database rebuild utilities like _Rebuild or _Rebuild2. Namely:
  • Appt.exe - Appointment List
  • Apptbook.exe - Appointment Book
  • Chart.exe - Patient Chart
  • Document.Center.exe - Document Center
  • Document.Convert.Daemon.exe - Document Convert Daemon
  • Document.UnfiledUtility.exe
  • Dtx.ThreeDDataLoader.exe
  • DtxLink.exe
  • DtxLinkNet.exe
  • DtxRx.exe - Prescription Module Wrapper
  • DXPort.exe
  • Dxprint.exe - Dentrix Print Module
  • DXWeb.exe
  • Famfile.exe - Family File
  • Journal.exe - Patient Journal
  • LabTrack.exe
  • Ledger.exe - Ledger
  • Mailer.exe - Mail Merge Utility
  • Office.exe - Office Manager
  • Patpict.exe - Patient Picture
  • PayerId.exe
  • Perio.exe - Perio Module
  • PEXI.exe
  • Presenter.exe - Dentrix Presenter
  • query.exe - Query engine
  • Questionnaire.exe - Questionnaire engine
  • QuickLabels.exe - Quick Labels
  • RA.exe
  • SearchPayments.exe
  • Snapshot.exe
  • TimeClock.exe - Time Clock
  • Totscr.exe
  • Tpman.exe
  • Trintf.exe
  • TxPlanner.exe
  • Wizard.exe
Then it was time to use it:
sdbinst "\\server\path\to\Dentrix\install\dentrix-fix.sdb"

Note that I largely aimed with a shotgun instead of a scalpel - I basically whitelisted almost every single executable in the Dentrix folder I could find that I knew wasn't a database utility and wasn't something that already ran without UAC whitelisting (e.g. AppLauncher). I also excluded eSync's executables since we don't use eSync in our lab. Also note that, since this was a classroom environment, I didn't have to deal with integrating this with Dexis, Dentrix Image, SUNI, or the other, similarly broken imaging solutions that are used in the dental field - that said, the same solution might work. I'll further note that, at least in my case, data integrity is not a high priority; since this is for a lab environment, it just needs to work well enough to get the point across, not remain stable through several Dentrix upgrades. That last point is important since the Microsoft Application Compatibility Toolkit calculates checksums for each executable you add to your database - if Dentrix upgrades a particular executable, you'll need to re-whitelist it for it to work.

The result? A Dentrix installation that let me, as a standard user, load up every Dentrix module I tried and make changes to patient records without a single UAC prompt.

If anybody is actually brave enough to try this in the field, let me know - I'm genuinely curious.

1. Note that, for the purposes of my lab, we're instructing using Dentrix G4. Dentrix G5 is theoretically 64-bit native, so it might install in C:\Program Files on 64-bit systems.

Friday, January 16, 2015

It is by caffeine alone I set my mind in motion...

“It is by caffeine alone I set my mind in motion. It is by the beans of Java that thoughts acquire speed, the hands acquire shakes, the shakes become a warning. It is by caffeine alone I set my mind in motion.” - The Programmer's Mantra
In my previous post regarding MathLibrary.cmd, I pointed out that division was well-night unusable - dividing a 101-digit number by itself took over 22 minutes. This has led me, in fits and spurts, to find some ways to improve performance and streamline the code since I first started working on this over a month ago; unfortunately, most of my results had only provided incremental improvements at best.

I made a mistake yesterday that changed all that.

While fixing my subtraction code, which actually had a fairly subtle error that produced extra significant digits if the answer had fewer digits than either the minuend or the subtrahend, I accidentally created some code with an O(n^2) algorithm, which produced performance results that looked an awful lot like the results I was getting with my division algorithm. This led me to wonder if perhaps that might be the key to understanding where my problem was - was I iterating through my string, character by character, so often that I was killing my code? Or was something else holding it back?

The answer to both questions turned out to be yes.

First, a bit of technical background. When I first started working on MathLibrary.cmd, I explicitly wrote it in mind with the idea that I'd copy and paste blocks of it as needed for my Project Euler solutions. For example, if I was working on a problem that only needed addition, I'd just copy the part of the script that dealt with adding large numbers (:ExtAdd), along with whatever dependencies (:ExtMatchPad, :ExtDim). Consequently, I specifically built each portion of the script with the assumption that it would run independently. Meanwhile, in order to facilitate the elementary school style math (e.g. iterate through each digit of a large number, do some math against it, carry it to the next one, etc. - we're talking pre-Common Core algorithms here), I had to make sure that both numbers were more or less identically sized - otherwise I'd be adding, say, 9 to absolute emptiness, which would lead to all sorts of unpleasantness. So, to accommodate that, I'd get the length of each number string, then append 0's in either direction as needed.

The mistake I made yesterday was that I got the length of my number string over and over and over again.

Well shoot - why do that? What if I could get it once, store it, pass it to each subroutine, and have it arithmetically manipulated as needed instead of manually checking the length of the string each time? So, last night, that's what I tried - and it worked! I got about a 50% performance improvement from my script by doing that.

Not bad - but still not good enough.

Trouble is, 50% of 22 minutes is still over 10 minutes, which is just not practical if I need to run repeated division operations. I didn't need to drop my division runtime by 50% - I needed to drop it by at least an order of magnitude. What else could I try?

This morning, I had a rather large, rather strong cup of gas station coffee. A bit later, I then had a flash of inspiration. Since multiplication is slow, I smartly decided when I first wrote the division algorithm to do it as rarely as possible - I'd multiply the divisor from 1 to 10, store the values in an array, then compare against those pre-calculated values to find the answer.

What if I didn't multiply at all?

What I was really trying to do was add the divisor to itself ten times, and addition is way faster than multiplication. So, what if I just did that instead?
Testing division operations...
/01 OK 14:48:57.25 14:48:56.64 0:0:0.61
/02 OK 14:48:58.00 14:48:57.26 0:0:0.74
/03 OK 14:48:58.94 14:48:58.01 0:0:0.93
/04 OK 14:49:00.08 14:48:58.94 0:0:1.14
/05 OK 14:49:01.40 14:49:00.09 0:0:1.31
/06 OK 14:49:03.71 14:49:01.41 0:0:2.30
/07 OK 14:49:07.30 14:49:03.72 0:0:3.58
/08 OK 14:49:17.23 14:49:07.32 0:0:9.91
/09 OK 14:49:36.92 14:49:17.24 0:0:19.68
/10 OK 14:49:56.90 14:49:36.94 0:0:19.96
Ah - much better. Now, instead of taking over 20 minutes, division takes less than 20 seconds. Much more livable, especially since this means I can now meaningfully check and troubleshoot it and find little issues like this:
MathLibrary.cmd 1 / 0.5
10.000000000000
Oops - well, looks like I still have some work to do.

Thursday, January 15, 2015

Experimenting with string manipulation

Division using my MathLibrary.cmd script has always been slow. Really, really slow:
/01 OK 11:21:39.08 11:21:37.99 0:0:1.9
/02 OK 11:21:40.89 11:21:39.09 0:0:1.80
/03 OK 11:21:43.92 11:21:40.90 0:0:3.2
/04 OK 11:21:48.57 11:21:43.93 0:0:4.64
/05 OK 11:21:54.78 11:21:48.58 0:0:6.20
/06 OK 11:22:12.96 11:21:54.79 0:0:18.17
/07 OK 11:22:53.36 11:22:12.98 0:0:40.38
/08 OK 11:28:33.39 11:22:53.37 0:5:40.2
/09 OK 11:50:13.04 11:28:33.40 0:21:39.64
/10 OK 12:12:14.42 11:50:13.05 0:22:1.37
The bolded numbers are how long each unit test (/01, /02, etc.) took to complete in HH:MM:SS.mS format. Each test is a series of identical numbers (meaning the answer should always be "1") in increasing size - test /01, for example, is 2/2, while test /10 - which took over 22 minutes - is dividing the same 101-digit number by itself.

This is a problem.

It's a problem because, at the end of the day, the goal of this script is to solve the third Project Euler problem - find the largest prime factor of a large enough number for CMD to choke on it - and in order to do that, I need to be able to calculate the square root of that number so I have an upper bound of prime factors to consider. In order to determine a square root by scratch, however, the most straightforward method that I can wrap my mind around uses iterative floating point division to get closer and closer to the square root until it's found. Obviously, an algorithm that takes over 20 minutes to divide two numbers isn't going to work if I have to divide two numbers over and over again.

While running my unit tests, however, I discovered something curious:
 Testing subtraction operations...
-01 OK 10:55:41.34 10:55:41.16 0:0:0.18
-02 OK 10:55:41.82 10:55:41.35 0:0:0.47
-03 OK 10:55:43.21 10:55:41.82 0:0:1.39
-04 OK 10:55:46.00 10:55:43.22 0:0:2.78
-05 OK 10:55:59.50 10:55:46.02 0:0:13.48
-06 OK 10:55:59.61 10:55:59.52 0:0:0.9
-07 OK 10:55:59.88 10:55:59.63 0:0:0.25
-08 OK 10:56:00.93 10:55:59.88 0:0:1.5
-09 OK 10:56:02.84 10:56:00.94 0:0:1.90
-10 OK 10:56:05.72 10:56:02.84 0:0:2.88
-11 OK 10:56:05.82 10:56:05.72 0:0:0.10
-12 OK 10:56:06.07 10:56:05.82 0:0:0.25
-13 OK 10:56:07.13 10:56:06.09 0:0:1.4
-14 OK 10:56:09.04 10:56:07.13 0:0:1.91
-15 OK 10:56:11.93 10:56:09.04 0:0:2.89
-16 NO 10:56:12.05 10:56:11.93 0:0:0.12
-17 NO 10:56:12.24 10:56:12.05 0:0:0.19
-18 NO 10:56:13.56 10:56:12.25 0:0:1.31
-19 NO 10:56:16.14 10:56:13.58 0:0:2.56
-20 NO 10:56:27.52 10:56:16.16 0:0:11.36
Note the "NO" results on the last five tests - these are tests that subtract two identical floating point numbers, which should return 0. Well, it turns out it wasn't quite doing that...

C:\Users\ \Documents\GitHub\MathLibrary.cmd>MathLibrary.cmd 0.2 - 0.2
0.0

C:\Users\ \Documents\GitHub\MathLibrary.cmd>MathLibrary.cmd 14.320 - 14.320
0.000


Notice that the results - bolded for readability - aren't actually "0" - they're 0.however many significant digits were on the right of the number. This led me to put together a quick "strip trailing zeroes" routine, which is near-identical to the one used by the division subroutine:

    :ExtSubtractStripTrailingZeroes
        CALL :ExtDim %_SubtractResult% _SubtractResLen _SubtractResDec
        IF %_SubtractResDec% GTR %_SubtractResLen% GOTO ExtSubtractDoneStrippingTrailingZeroes
        SET _SubtractRInt=%_SubtractResult:~-1%
        IF %_SubtractRInt%==. (
            SET _SubtractResult=%_SubtractResult:~0,-1%
            GOTO ExtSubtractDoneStrippingTrailingZeroes
        )
        IF NOT %_SubtractRInt%==0 GOTO ExtSubtractDoneStrippingTrailingZeroes
        SET _SubtractResult=%_SubtractResult:~0,-1%
        GOTO ExtSubtractStripTrailingZeroes
    :ExtSubtractDoneStrippingTrailingZeroes


Then I re-ran my tests:
-16 OK 13:45:15.31 13:45:15.15 0:0:0.16
-17 OK 13:45:15.57 13:45:15.31 0:0:0.26
-18 OK 13:45:18.60 13:45:15.59 0:0:3.1
-19 OK 13:45:25.90 13:45:18.61 0:0:7.29
-20 OK 13:51:18.70 13:45:25.90 0:5:52.80
The good news is they all passed. The bad news is they started taking longer - much longer in the case of the last test, which went from taking an already slow 11 seconds to taking close to 6 minutes. Something needed to be done. So, instead of looping via GOTO and truncating the string one digit at a time, I decided I'd try finding the position of the first trailing zero and then cut all of them at once:

    :ExtSubtractStripTrailingZeroes
        CALL :ExtDim %_SubtractResult% _SubtractResLen _SubtractResDec
        IF %_SubtractResDec% GTR %_SubtractResLen% GOTO ExtSubtractDoneStrippingTrailingZeroes
        SET /A _SubStripMax=_SubtractResLen-_SubtractResDec+1
        SET _SubStripOff=0
        FOR /L %%G IN (1,1,%_SubStripMax%) DO (
            SET _SubStripPos=%%G
            CALL SET _SubZChk=%%_SubtractResult:~-!_SubStripPos!,%_One%%%
            IF NOT !_SubZChk!==0 (
                GOTO ExtSubtractFoundTrailingZeroes
            ) ELSE SET _SubStripOff=%%G
        )
    :ExtSubtractFoundTrailingZeroes
        IF %_SubZChk%==. SET /A _SubStripOff+=1
        CALL SET _SubtractResult=%%_SubtractResult:~%_Zero%,-%_SubStripOff%%%
    :ExtSubtractDoneStrippingTrailingZeroes

The result?
-16 OK 15:12:51.19 15:12:51.05 0:0:0.14
-17 OK 15:12:51.41 15:12:51.19 0:0:0.22
-18 OK 15:12:52.83 15:12:51.42 0:0:1.41
-19 OK 15:12:55.59 15:12:52.84 0:0:2.75
-20 OK 15:13:08.87 15:12:55.61 0:0:13.26
Much better.  Granted, it's a little slower than it was when I wasn't cutting the zeroes out at all, but this was at least manageable. Then I took a second look at the first line of code from the original algorithm:

    :ExtSubtractStripTrailingZeroes
        CALL :ExtDim %_SubtractResult% _SubtractResLen _SubtractResDec

ExtDim iterates through each character of the string, manually counting its length and decimal position along the way. Realistically, we only need to run that once, not once per character. Moving things around a bit and reverting to the original algorithms gave me this:
-16 OK 16:55:20.62 16:55:20.47 0:0:0.15
-17 OK 16:55:20.85 16:55:20.62 0:0:0.23
-18 OK 16:55:22.49 16:55:20.87 0:0:1.62
-19 OK 16:55:25.67 16:55:22.51 0:0:3.16
-20 OK 16:55:41.43 16:55:25.68 0:0:15.75

So, even with my original algorithm fixed, the For loop method still remained faster, though nowhere near as much faster as I was hoping since the division zero stripping algorithm isn't as broken as my original subtraction zero stripping algorithm.

I have some other ideas to move division along, though. Stay tuned...

Wednesday, January 14, 2015

Performance Testing CMD


I've remarked a couple of times in the past about the inconvenience of CMD's insistence that all numbers that begin with a zero shall be treated as octal numbers. Since my MathLibrary.cmd project uses string manipulation to break down large and floating point numbers into chunks that CMD can actually handle, it'd be nice if I could break strings down into larger chunks and handle them "natively" instead of one digit at a time like I'm currently doing. However, every attempt I've made at doing so has actually led to a performance penalty since the overhead of "de-octalifying" - checking if a digit starts with a 0 and converting it to a decimal number - consumes any time I might otherwise save.

This led me to consider what the most efficient method of "de-octalifying" a number in CMD might be. Three approaches that immediately came to mind were:
  1. Append a "1" to the beginning of every number and apply the modulus operator against a known power of 10 to extract the desired number. For example, if I'm trying to de-octalify 0003, I'd append a 1 (10003), then modulus* that number against 10000. This is the traditional method seen in most Windows batch script examples that deal with this sort of problem.
  2. Apply a series of If/Then statements that checks for the presence of leading 0's, then return the truncated result.
  3. Similar logic as #2, only using a For loop to iterate through the number string.
I then decided to write a script to test these algorithms, which can be found here. A sample of the algorithms themselves:

***

:TestModulus
    SETLOCAL EnableDelayedExpansion
    :: Accepts one parameter:
    :: %1 - The digit to be de-octalized (passed by reference)
   
    SET _TestMod=1!%1!
    SET /A _TestMod=_TestMod %% 1000000000

    ENDLOCAL & SET %1=%_TestMod%
GOTO :EOF

:TestIf
    SETLOCAL EnableDelayedExpansion
    :: Accepts one parameter:
    :: %1 - The digit to be de-octalized (passed by reference)
   
    SET _TestIf=!%1!
   
    IF %_TestIf:~0,1%==0 (
        IF %_TestIf:~1,1%==0 (
            IF %_TestIf:~2,1%==0 (
                IF %_TestIf:~3,1%==0 (
                    IF %_TestIf:~4,1%==0 (
                        IF %_TestIf:~5,1%==0 (
                            IF %_TestIf:~6,1%==0 (
                                IF %_TestIf:~7,1%==0 (
                                    SET _TestIf=%_TestIf:~-1%
                                ) ELSE SET _TestIf=%_TestIf:~-2%
                            ) ELSE SET _TestIf=%_TestIf:~-3%
                        ) ELSE SET _TestIf=%_TestIf:~-4%
                    ) ELSE SET _TestIf=%_TestIf:~-5%
                ) ELSE SET _TestIf=%_TestIf:~-6%
            ) ELSE SET _TestIf=%_TestIf:~-7%
        ) ELSE SET _TestIf=%_TestIf:~-8%
    )
   
    ENDLOCAL & SET %1=%_TestIf%
GOTO :EOF

:TestFor
    SETLOCAL EnableDelayedExpansion
    :: Accepts one parameter
    :: %1 - The digit to be deoctalized (passed by reference)
   
    SET _TestFor=!%1!
    FOR /L %%G IN (1,1,8) DO (
        SET _TestZero=!_TestFor:~0,1!
        IF !_TestZero!==0 (
            SET _TestFor=!_TestFor:~1!
        ) ELSE GOTO TestForReturn
    )
   
    :TestForReturn
    ENDLOCAL & SET %1=%_TestFor%
GOTO :EOF


***

Note that, of the three, the modulus approach is certainly the tersest of the bunch, which is arguably why it's the most popular. After testing it, there's another advantage:

It's faster.

My test script went through and truncated eight sets of numbers, plus processed a set that didn't need to be truncated at all. I chose this set because, owing to the Windows Batch Script environment's 32-bit signed integer limit, the largest number that can be processed by CMD is a 10-digit number (2147483647, to be more specific) - consequently, the largest block that I could subdivide addition and subtraction operations into would be a nine digit block (999999999 + 999999999 = 1,999,999,998, which is less than CMD's limit of 2,147,483,647), so that would be the largest block I would ever process. I then iterated through each digit in that set a thousand times, which I figured would be long enough to highlight any performance differences, and viewed the results:



Mod If For
1 3.1 3.15 3.66
2 2.85 3.7 3.53
3 2.8 2.99 3.39
4 2.68 2.9 3.23
5 2.59 2.82 3.1
6 2.5 2.72 2.92
7 2.42 2.63 2.77
8 2.31 2.54 2.61
9 2.22 2.41 2.45
The y-axis in that table is how many digits would be returned - for example, "9" corresponds to 123456789, which wouldn't have any 0s removed at all, while "1" corresponds to 000000001, which would have eight 0s removed.  The x-axis is the algorithm chosen for each iteration - either using the modulo operator, cascading If/Then statements, or a For loop. Time was measured in seconds. Notice that the modulus method of 0 truncation was consistently the fastest in each example, with If/Then being faster in most use cases than the For loop method.

The takeaway from all of this? Sometimes the simplest methods really are the best, and sometimes traditions happen for a reason.

*****
* The modulo operator, usually expressed as "%" divides a numerator by a denominator, then returns the remainder. For example, 11 % 2 would give 1, which is the remainder after 11 / 2.

Saturday, January 3, 2015

Extending CMD's Math Abilities

Sample output from MathLibrary.cmd
A while back, I started working on writing Project Euler scripts using Windows Batch Script. I didn't get very far before I ran into some pretty clear limitations with my chosen development environment:
  • Windows Batch Script only supports 32-bit signed integers. This bounds its native math functionality from -2,147,483,648 to 2,147,483,647, inclusively.
  • Windows Batch Script does not handle decimal points at all.
  • Windows Batch Script's native arithmetic functions are extremely limited.
  • Windows Batch Script has almost no error handling capabilities at all. They're so bad, in fact, that you can oftentimes run afoul of each of the above limitations and never receive an error for doing so - you'll just get undesired results.
To that end, I started working on a way to broaden its capabilities a bit, and in the spirit of the original goal of implementing Project Euler solutions using nothing but Windows Batch Scripting, I decided to keep my solution confined to Windows Batch Scripting as well. At first I did what any good sysadmin would do and tried to Google the problem away, which led to some fruitful results. Someone found a way to calculate Pi using Batch Script, which was encouraging - that proved floating point math was possible. I also found solutions that allowed arbitrary length integers to be calculated, which were also promising. Unfortunately, when I attempted to apply those solutions to my particular problem, I found that the code wasn't particularly portable (the division subroutine in the Pi script, for example, only works as long as one of the digits is within the 32-bit signed limit) or otherwise useful for my needs. Consequently, it looked like I would have to write my own.


Now, before anyone gets excited, it's not done yet. I'm publishing it now because 2015 is here, I've been fiddling with this on and off for the past two months, and I decided it was high time to just get it out the door for the rest of the world. With that in mind, here are the current limitations of MathLibrary.cmd:
  • Negative numbers are only supported for addition, subtraction, and comparison. Multiplication and division are coming soon.
  • The algorithms are "conceptual", meaning that, if you were a 10-year-old elementary school student that could read Windows Batch Script, you would understand what was going on perfectly. I'm not taking advantages of quads and octs yet (I'll explain why below). 
  • It's slow. Two 500 digit numbers take about 10 seconds to add on my laptop (AMD Phenom II N660 3.0 GHz) and about 25 seconds to subtract. Two 50 digit numbers take about a minute and a half to multiply together. Division is unspeakably atrocious.
That said, it does successfully add, subtract, multiply, divide, and compare arbitrary-length floating point numbers, which is pretty cool. It can be done.

So, let's talk about speed for a bit. While working on the script, I decided to experiment with refactoring the addition subroutined (:ExtAdd) to use eight digit blocks instead of single integer blocks like what you see on GitHub. What I discovered was that, in order to do so, I had to check for blocks that started with a 0 - or several 0's - and filter them out, otherwise Windows Batch Script would assume I wanted to add two octal numbers together. Consequently, whatever performance gains I got out of adding in 8-digit blocks instead of single digits was devoured by the time it took to clear out the leading 0's out of the blocks so they would add together cleanly.

Another odd note - take a look at the code used in the Pi script for adding two digits together:

set /a Add_Digit = Add_Carry + %1_%%i + %2_%%i
set /a %1_%%i = Add_Digit %% 10000
set /a Add_Carry = Add_Digit / 10000

Now, let's compare to my equivalent code:

SET /A _AddByPos=_AddInt1+_AddInt2+_AddCarry
IF !_AddByPos! GEQ 10 (
SET _AddCarry=1
SET /A _AddByPos-=10
) ELSE (
SET _AddCarry=0
)

What's the difference? The Pi script algorithm is conceptually pretty clever - it takes advantage of the fact that Windows Batch Script's division operator will only return the integer value of a divide operation (i.e. everything to the left of the decimal point in the result). Consequently, if you divide a number between 10-19, inclusively, by 10, you will get 1 instead of 1.1, 1.2, 1.3, and so on. Then, you can use the modulus operator to get the remainder, which, when dividing by 10, would be the lone digit to the right of the decimal point (e.g. 11 %% 10 = 1, 12 %% 10 = 2, and so on).

It's also slower.

Using my UnitTest.cmd script, which I use to make sure that improvements to the script don't fundamentally break the script, here's the times I got for performing the first 10 addition tests using my method (time elapsed in bold):

01 OK 21:01:50.42 21:01:50.32 0:0:0.10
02 OK 21:01:50.51 21:01:50.44 0:0:0.7
03 OK 21:01:50.61 21:01:50.52 0:0:0.9
04 OK 21:01:50.73 21:01:50.62 0:0:0.11
05 OK 21:01:50.87 21:01:50.74 0:0:0.13
06 OK 21:01:51.19 21:01:50.88 0:0:0.31
07 OK 21:01:51.57 21:01:51.21 0:0:0.36
08 OK 21:01:52.91 21:01:51.59 0:0:1.32
09 OK 21:01:55.01 21:01:52.92 0:0:2.9
10 OK 21:02:05.95 21:01:55.08 0:0:10.87

And here's what I got when I implemented the Pi script algorithm:

01 OK 21:48:31.13 21:48:31.02 0:0:0.11
02 OK 21:48:31.24 21:48:31.15 0:0:0.9
03 OK 21:48:31.37 21:48:31.26 0:0:0.11
04 OK 21:48:31.58 21:48:31.40 0:0:0.18
05 OK 21:48:31.77 21:48:31.60 0:0:0.17
06 OK 21:48:32.11 21:48:31.79 0:0:0.32
07 OK 21:48:32.63 21:48:32.13 0:0:0.50
08 OK 21:48:34.21 21:48:32.65 0:0:1.56
09 OK 21:48:37.11 21:48:34.22 0:0:2.89
10 OK 21:48:50.02 21:48:37.12 0:0:12.90

Note that most of the tests are considerably slower - about 10-20% slower, give or take. Why? Part of the problem is that Windows Batch Script's internal math functions are unevenly implemented - addition and subtraction, which my method uses, is faster than division or the modulus operator. Part of it is that the Pi script algorithm applies the same three steps to all addition operations - add, divide, modulus - while mine oftentimes (especially in the first 10 unit tests, which are solely large number addition with no carry) only uses two operations (add, set), with a third set (subtract by 10) used only when there's a carry operation in play.

Ah... but what about that IF statement? Doesn't that count as an operation?

It does, actually, which is why my method is only 10-20% faster instead of 33% faster under ideal conditions, which the first ten unit tests simulate nicely. However, every time the Windows Batch Script arithmetic engine is called, it noticeably kills performance. How noticeably? When I first started this script, I intentionally set all of my "integers" using SET /A to help me differentiate in my code between the strings and substrings I was manipulating against actual numbers that Windows Batch Script could manipulate. While working on it, however, I read somewhere that SET /A added a step in the interpreter over just using SET, so I put it to the test and converted all of my SET /A declarations to SET. The result was a 5-10% performance improvement.

So there you have it. Like I said, I'm not done fiddling with this - negative number support will be coming to multiplication and division over the next week or two, depending on time and motivation. After that, I'm probably going to mix up the UnitTest script a bit so I can test individual components in a hurry instead of testing ALL THE THINGS all the time - the multiplication tests in particular really slow me down - and then I'm going to start playing with performance tuning the script as much as possible. Assuming I can get division into something resembling "usable condition" (it takes over 10 seconds to divide 1 by 3), I'll then start implementing a square root subroutine, which will then clear the way for me to get back to work on Project Euler solutions once more.

It'll get there, one line at a time. I just have to make this script suck a little bit less each week, that's all. It's a good metaphor for life.

Sunday, December 7, 2014

Thoughts on Windows Scripting


For the past few weeks, I haven't been posting much because my attention has been diverted elsewhere. The picture above is a little sneak preview of what I've been working on, which is related to my previous efforts in solving Project Euler problems using Windows batch script - it's almost ready for me to share with the world (though, if you look around, you'll find it since I have it hosted publicly in the cloud), though there are a few i's I'd like to dot and a few t's I'd like to cross before I really promote this thing.

And what a thing it is! But I'm not ready to talk about it just yet.

Instead, I would like to talk about the general topic of Windows scripting. For years, my attention has been focused on small business IT - in that environment, there's a limited amount of automation available to the IT professional, at least as long as the customer is hosting their own equipment. Though a clever IT shop can probably find some way to leverage tools like Puppet and Chef to make on-site deployments more consistent, it's a little difficult to avoid treating servers like pets instead of cattle when each "farm" only has one or two "cattle" and the "farmers" don't like to share (to stretch the already overwrought metaphor a bit). In that environment, the limited nature of old-school Windows batch scripting - a language that traces its lineage all the way back to the early days of DOS - wasn't immediately apparent. Oh sure, there was the occasional heated discussion over whether it was more intuitive to suffer through RUNDLL32's myriad flags and byzantine syntax, or through VBScript's ridiculously verbose but at least sort of intelligible at a glance AddPrinterConnection method, or to just give up entirely and use KiXtart, but the results usually didn't matter all that much in the end. By the time you were done writing a script, testing it, and rolling it out, most scripts just weren't worth the time. So, every client would have a script or two with some "NET USE" commands, some clients with more dedicated IT professionals might have a slightly more complicated batch script with a control structure in it (I was notorious about this, though not near as notorious as some of my coworkers) and that was pretty much it. In our context, PowerShell was observed, tried out, and then promptly ignored as much as humanly possible - it took more typing than CMD, we couldn't count on it being on every machine we wanted to automate (it wasn't installed by default until Windows 7/Server 2008 R2), and it didn't do anything we weren't already doing for ourselves.

These days, though, I'm dealing with a somewhat larger environment. It's still not large enough to really see the benefits of PowerShell firsthand - we're talking about less than ten servers, virtual or otherwise, and approximately 250 PCs, more or less - but it's big enough where I can certainly imagine what a larger environment would look like and some of the challenges I would have keeping things halfway consistent and manageable. That's part of the reason I've been doing the Project Euler exercises in the first place - at some point, my scripting chops will need to be ready for a larger environment. While working on my exercises, though, I've run into some pretty serious difficulties - difficulties I've been able to overcome, mind you, but ones that definitely show that CMD is, shall we say, a product of its time:

  • CMD can't math. No, seriously, ask it what 2/3 is:

    set /a _test=2/3
    0


    That's right - no floating point support. Decimals aren't a thing in CMD, and neither is rounding.  
  • CMD's roots as a scripting language cutting its teeth in the heyday of BASIC really show. Functions are kind of a thing, if you're sort of creative about it, but it's pretty clear that they were bolted on well after the fact. There's exactly one loop structure - if you want a do/while loop, you're going to have to get creative with GOTOs and labels. 
  • Delayed Expansion makes a lot of things possible - without it, I don't think I could functionally use a FOR loop or an IF statement in my Project Euler code. That doesn't mean it makes things easy, though. Divining the pattern between "the interpreter will interpret this as it's happening by default" and "the interpreter will just read ahead and only parrot back what the results will be after that loop is done" (General rule - if what you're doing happens between parenthesis, you probably want Delayed Expansion if you're used to any other programming language ever) is a royal pain.
  • You know what's awesome? When you can do things like this:

    set /a _test=2000000000^2

    And you get a result like this:
    Invalid number. Numbers are limited to 32-bits precision.

    But if you do something like this:

    set /a _test=2000000000*2

    You end up with this:
    -294967296

    Neat, huh? I agree, Microsoft - consistent error checking is for chumps.
  • Arrays? They don't exist. No, really - they don't. Except... if you're really creative... and abuse Delayed Expansion a bit... you can kind of fake them, as long as you're only querying their values in a FOR loop. See, if you try to trick the interpreter out by doing something like:

    SET _test0=Foo
    SET _zero=0

    ECHO:%_test%%_zero%


    You'll just get 0 because it'll attempt to extract the value of _test (which doesn't exist), then the value of _zero (which is 0). However, if you do something like this:

    FOR /L %%G IN (0,1,0) DO (
    ECHO:!_test%%G!
    )


    It'll actually work, resolve %%G first, then resolve everything between the exclamation points as one variable. Spooky, eh? If it helps, the rest of CMD's syntax is every bit this consistent.
  • I don't expect a scripting language to have a firm type casting system - quite the contrary, in fact. However, CMD's is particularly... interesting. For example, let's say you want to calculate the elapsed time between the start and end of a script. Easy enough:
SET _TimeStart=%time%
CALL RandomScript.bat
SET _TimeEnd=%time%

FOR /F "tokens=1-4 delims=:." %%G IN ("%_TimeStart%") DO (
SET _HStart=%%G
SET _MStart=%%H
SET _SStart=%%I
SET _mSStart=%%J
)

FOR /F "tokens=1-4 delims=:." %% K IN ("%_TimeEnd%") DO (
SET _HEnd=%%K
SET _MEnd=%%L
SET _SEnd=%%M
SET _mSEnd=%%N
)

SET /A _mSElapsed=_mSEnd-_mSStart


:: Insert conditional to handle occasions where _mSElapsed is negative because the script took longer than a second or it started near the end of a second.


SET /A _SElapsed=_SEnd-_SStart
:: Insert conditional... etc.
...


And so on. Well, if you tried to write the script like that and ran it, two things would happen:
  1. Your failure to use Delayed Expansion when you called the %time% variable would result in the script always returning 0 since it would only query the value of %time% once - at the end of the script.
  2. If any the time blocks (HH:MM:SS.MS) have a zero at the start (say, 12:09:14.54 - _MWhatever would equal "09"), CMD won't interpret the result as "9" when you ask it to do some math against it. Oh no. Instead... well, I'll just let Microsoft explain this one:

    Numeric values are decimal numbers, unless prefixed by 0x for hexadecimal numbers, and 0 for octal numbers. So 0x12 is the same as 18 is the same as 022. Please note that the octal notation can be confusing: 08 and 09 are not valid numbers because 8 and 9 are not valid octal digits.

    That's right - "09" suddenly becomes 9-base-8, which seriously doesn't exist and will lead to some fascinating results.
So, you end up writing a bunch of code like this to convince SET to, no, seriously, save the damned number as a base-10 number, please and thank you very much:

IF %_HEnd:~0,1% EQU 0 (
SET _HEnd=%_HEnd:~1,1%
)
IF %_MEnd:~0,1% EQU 0 (
SET _MEnd=%_MEnd:~1,1%
)
IF %_SEnd:~0,1% EQU 0 (
SET _SEnd=%_SEnd:~1,1%
)
IF %_mSEnd:~0,1% EQU 0 (
SET _mSEnd=%_mSEnd:~1,1%
)
IF %_HStart:~0,1% EQU 0 (
SET _HStart=%_HStart:~1,1%
)
IF %_MStart:~0,1% EQU 0 (
SET _MStart=%_MStart:~1,1%
)
IF %_SStart:~0,1% EQU 0 (
SET _SStart=%_SStart:~1,1%
)
IF %_mSStart:~0,1% EQU 0 (
SET _mSStart=%_mSStart:~1,1%
)

For those playing along at home, that's just a series of statements that say, if the first character in the variable is a zero, throw that zero in the trash can and just keep the last number. 

Yeah. Now imagine having a thousand or so servers to manage and putting up with this. Given the utter lack of responsible error handling and the head-scratching syntax parsing, it's only a matter of time before somebody writes a script that they think checks a system's MAC address in a text file somewhere and uses netsh to assign an IP address and computer name based on that MAC address, only to have it assign the same IP address to every system or just refuse to properly parse a MAC address' delimiters in some weird corner case that magically converts it into octal or hexadecimal or base 23.7 or Roman numerals or the numbering system of Mayan moisture evaporators or something. Compared to this, bash and its ilk must have seemed like a revelation to anyone even slightly seriously interested in server automation. This doesn't even get into how there are several corners of Windows that are virtually impossible to get to via CMD unless you feel like directly editing registry values with REG (given everything covered so far, what could possibly go wrong?) or you feel like stepping into the object-oriented nightmare that is VBScript[1]. Something needed to be done. Windows needed a proper, honest-to-goodness scripting language that acknowledged some progress in interpreted languages had been made since the first term of the Reagan administration.

I'm glad Microsoft took care of that, even if it's still something that remains mostly tangential to my day-to-day administration experience. For now.

*****
1. That five line example right there in CMD turns into:

NET USE H: \\myserver\users /PERSISTENT:NO

And my former coworkers wondered why I preferred CMD, even with its flaws.