Some Simple Import/Export Scripts

Haven’t posted in awhile. I use these scripts a bunch for my Lynx’s. It is always a pain to port methods from device to device, so I made the dumb powershell script to do. It isn’t well tested, but it gets it close enough so I can open up the sub-methods if anything went wrong. There is an assumption that your methods and scripts have to be contained in their own folders with the same name

\MethodManager4
      |_Workspaces
               |_LM999
                    |_Methods
                    |   |_FOOBAR/*.met
                    |   |...
                    |
                    |_Scripts
                        |_FOOBAR/*.cs
                        |...

import.ps1

param(
    [Parameter(Mandatory=$true)]
    [string]$workspace,
    [Parameter(Mandatory=$true)]
    [string]$method
)

# Ensure that the provided zip file exists
if (-not (Test-Path $method)) {
    Write-Error "The provided zip file path does not exist."
    exit
}

# Create a temporary directory to extract the zip contents
$tempDir = [System.IO.Path]::GetTempFileName()
Remove-Item $tempDir
New-Item -ItemType Directory -Path $tempDir

# Extract zip file to the temporary directory
Expand-Archive -Path $method -DestinationPath $tempDir

# Define base target paths
$baseMethodsPath = "C:\MethodManager4\Workspaces\$workspace\Methods"
$baseScriptsPath = "C:\MethodManager4\Workspaces\$workspace\Scripts"

# Check if Methods directory exists in the archive and copy if it does
if (Test-Path "$tempDir\Methods") {
    Get-ChildItem "$tempDir\Methods" -Directory | ForEach-Object {
        $destPath = Join-Path $baseMethodsPath $_.Name
        Copy-Item -Path $_.FullName -Destination $destPath -Recurse -Force
        
        #Resolve the deck location names in the methods
	Write-Host "Resolving .met files in: $destPath"
        & $env:USERPROFILE\cosmos\lynx\resolve.ps1 -workspace $workspace -method $destPath
    }
}

# Check if Scripts directory exists in the archive and copy if it does
if (Test-Path "$tempDir\Scripts") {
    Get-ChildItem "$tempDir\Scripts" -Directory | ForEach-Object {
        $destPath = Join-Path $baseScriptsPath $_.Name
        Copy-Item -Path $_.FullName -Destination $destPath -Recurse -Force
    }
}

# Cleanup
Remove-Item -Path $tempDir -Recurse -Force

export.ps1

# Export a method from the lynx workspace and 
# package up the folder in Methods/ and Scripts/
# into a zip folder.

param(
    [Parameter(Mandatory=$true)]
    [string]$workspace,
    [Parameter(Mandatory=$true)]
    [string]$method
)

# Define the source directories
$lynxActionsPath = "C:\MethodManager4\Workspaces\$workspace\Methods\$method"
$lynxScriptsPath = "C:\MethodManager4\Workspaces\$workspace\Scripts\$method"

# Check if the Actions directory exists
if (-not (Test-Path $lynxActionsPath)) {
    Write-Error "The provided source path for Actions does not exist."
    exit
}

# Create a temporary directory to organize the contents for zipping
$tempDir = [System.IO.Path]::GetTempFileName()
Remove-Item $tempDir
New-Item -ItemType Directory -Path $tempDir

# Copy the contents of lynxActions to the temp directory
Copy-Item -Path $lynxActionsPath -Destination "$tempDir\Methods\$method" -Recurse -Force

# Copy the contents of lynxScripts to the temp directory only if it exists
if (Test-Path $lynxScriptsPath) {
    Copy-Item -Path $lynxScriptsPath -Destination "$tempDir\Scripts\$method" -Recurse -Force
}

# Create the zip file with the desired naming format
$dateTime = Get-Date -Format "MM-dd-yy--HH-mm-ss"
$zipName = "$method-$dateTime.zip"
$zipPath = "$env:USERPROFILE\Downloads\$zipName"

Compress-Archive -Path $tempDir\* -DestinationPath $zipPath

# Cleanup
Remove-Item -Path $tempDir -Recurse -Force

# Output the path of the created zip file
Write-Output "Zip file created at: $zipPath"

resolve.ps1

This is really the meat of the script that could use the most advancement (like importing to instruments with multiple heads or integrated features) - barebones works for me though.

# Given a target workspace $workspace and a Method source folder
# resolve the lynx deck position naming of the source to match the
# deck location names of the the target workspace
#
# ERRORS
#   1: Target workspace does not have enough locations to fit source method
param(
    [Parameter(Mandatory=$true)]
    [string]$workspace,
    [Parameter(Mandatory=$true)]
    [string]$method
)

function LoadNameValuesFromConfig {
    # Load XML content from the configuration file and return the name values
    Write-Debug "Loading name values from config..."
    [xml]$configXml = Get-Content "C:\MethodManager4\Workspaces\$workspace\Configuration\Lynx.Left.Worktable.config"
    $loadedNames = $configXml.SelectNodes("//MMLocation/Name").InnerText
    Write-Debug "Loaded $($loadedNames.Count) name values."
    return $loadedNames
}

Write-Debug "Loaded Name values from config: $($nameValues -join ', ')"

# Add the Levenshtein distance computation method
Add-Type -TypeDefinition @"
using System;
public static class levenshtein {
    public static int Distance(string s, string t) {
        int n = s.Length;
        int m = t.Length;
        int[,] d = new int[n + 1, m + 1];

        if (n == 0) return m;
        if (m == 0) return n;

        for (int i = 0; i <= n; d[i, 0] = i++) ;
        for (int j = 0; j <= m; d[0, j] = j++) ;

        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                int cost = (t[j - 1] == s[i - 1]) ? 0 : 1;
                d[i, j] = Math.Min(
                    Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1),
                    d[i - 1, j - 1] + cost
                );
            }
        }

        return d[n, m];
    }
}
"@ -Language CSharp

function FindBestMatchingString($str) {
    # Returns the best matching string from $nameValues for a given string $str

    Write-Debug "Finding best match for '$str'."
    
    if (-not $nameValues -or $nameValues.Length -eq 0) {
        Write-Debug "Name values list is empty or null. Unable to find a match."
        return $null
    }

    $bestMatch = $nameValues[0]  # Default to the first string in the list
    $bestScore = [int]::MaxValue

    foreach ($name in $nameValues) {
        $score = [levenshtein]::Distance($str, $name)
        
        if ($score -lt $bestScore) {
            $bestMatch = $name
            $bestScore = $score
        }
    }

    Write-Debug "Best match for '$str' is '$bestMatch' with score $bestScore"
    return $bestMatch
}

Get-ChildItem $method -Recurse -Filter "*.met" | ForEach-Object {
    # Refresh name values from the config before processing the file
    $nameValues = LoadNameValuesFromConfig

    if (-not $nameValues -or $nameValues.Length -eq 0) {
        Write-Error "Failed to load name values from the configuration. Ensure the XML file is present and contains <Name> nodes."
        return
    }

    Write-Debug "Processing file: $_"
    [xml]$xmlContent = Get-Content $_.FullName

    # Select nodes for replacement
    $nodes = $xmlContent.SelectNodes("//Collection[@name='WorktableResourceMaps']//Simple[@name='LocationName']")
    Write-Debug "Found $($nodes.Count) nodes to process in $_.FullName"

    foreach ($node in $nodes) {
        $currentValue = $node.GetAttribute("value")
        $replacement = FindBestMatchingString $currentValue

        # If the best match is an exact match, use it directly
        if ([levenshtein]::Distance($currentValue, $replacement) -eq 0) {
            $node.SetAttribute("value", $replacement)
            Write-Debug "Replaced value '$currentValue' with '$replacement'"
            continue
        }

        # Check if the replacement is already used
        while ($nodes | Where-Object { $_.GetAttribute("value") -eq $replacement }) {
            $nameValues = $nameValues | Where-Object { $_ -ne $replacement }
            $replacement = FindBestMatchingString $currentValue
        }

        $node.SetAttribute("value", $replacement)
        Write-Debug "Replaced value '$currentValue' with '$replacement'"
    }
    $xmlContent.Save($_.FullName)
}

The reality is it is pretty stupid to do this with powershell, an executable would beable to do much more logic with less lines, however powershell is on every windows PC regardless of IT so can be kinda nice for that.

4 Likes

@NvdE