PowerShellOSA.psm1
|
<# PowerShellOSA Copyright (C) 2025 Vincent Anso This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. #> #Requires -Version 7.2 if ( -Not $IsMacOS ) { Write-Warning "This module only runs on macOS." exit 0 } else { if (Test-Path -LiteralPath "$PSScriptRoot/PowerShellOSA.zip") { Write-Verbose "Need to unzip PowerShellOSA.app" /usr/bin/unzip -qo $PSScriptRoot/PowerShellOSA.zip -d "$PSScriptRoot/" /bin/rm $PSScriptRoot/PowerShellOSA.zip /bin/rm -R $PSScriptRoot/__MACOSX } if (Test-Path -LiteralPath "$PSScriptRoot/PowerShellOSAUI.zip") { Write-Verbose "Need to unzip PowerShellOSAUI.app" /usr/bin/unzip -qo $PSScriptRoot/PowerShellOSAUI.zip -d "$PSScriptRoot/" /bin/rm $PSScriptRoot/PowerShellOSAUI.zip /bin/rm -R $PSScriptRoot/__MACOSX } } enum PowerShellOSAOutputFormat { PSCustomObject JSON PLIST } function Invoke-OSA { <# .SYNOPSIS Allowing PowerShell to call AppleScript script or file using the PowerShellOSA application through seamless two-way communication. #> [CmdletBinding(DefaultParameterSetName = "Path")] param ( [Parameter(ParameterSetName = 'Script')] [Parameter(Position = 0)] [ValidateNotNullOrEmpty()] [String]$Script, [Parameter(ParameterSetName = 'Path')] [ValidateNotNullOrEmpty()] [ValidateScript({ Test-Path -LiteralPath $_ })] [string]$Path, [Parameter(ParameterSetName = 'Script')] [Parameter(ParameterSetName = 'Path')] [Parameter(ValueFromPipeline = $true)] [Object]$InputObject = $null, [Parameter(ParameterSetName = 'Script')] [Parameter(ParameterSetName = 'Path')] [Object]$Parameters, [Parameter(ParameterSetName = 'Script')] [Parameter(ParameterSetName = 'Path')] [ValidateScript({ $(/usr/bin/osalang) -ccontains $_ })] [String]$Language="AppleScript", [Parameter(ParameterSetName = 'Script')] [Parameter(ParameterSetName = 'Path')] [ValidateSet("PSCustomObject","JSON", "PLIST", IgnoreCase = $false)] [PowerShellOSAOutputFormat]$OutputFormat="PSCustomObject" ) if ($env:POWERSHELLOSA_PATH) { $PowerShellOSA=$env:POWERSHELLOSA_PATH } else { $PowerShellOSA="$PSScriptRoot/PowerShellOSA.app" } if (-Not (Test-Path -LiteralPath $PowerShellOSA)) { Write-Warning "PowerShellOSA application not found at path $PowerShellOSA" return $null } $PowerShellOSA="$PowerShellOSA/Contents/MacOS/PowerShellOSA" Write-Debug $PowerShellOSA if ($PSCmdlet.ParameterSetName -eq "Script") { $source = "--script=`"$Script`"" } else { $source = "--file=`"$Path`"" } $Parameters = $($Parameters | ConvertTo-Json -Compress -WarningAction SilentlyContinue) if ($PSBoundParameters.ContainsKey('InputObject')) { if ($input) { $InputObject = $input } } $InputObject = $($InputObject | ConvertTo-Json -Compress -WarningAction SilentlyContinue) Write-Debug $InputObject Write-Debug $source ($OutputFormat -eq "PSCustomObject") ? ($RawOutput = "JSON") : ($RawOutput = $OutputFormat) | Out-Null # --script=text | --file=filename [--input=JSON-formatted string] [--parameters=JSON-formatted string] [--language=OSALanguageName] [--format=JSON[PLIST]] [--quiet] $command = "$PowerShellOSA $source --input='$InputObject' --parameters='$Parameters' --language=$Language --format=$RawOutput --quiet" Write-Debug $command $result = Invoke-Expression $command if ($result) { switch ($OutputFormat) { {($_ -eq "JSON") -or ($_ -eq "PLIST")} { $result } "PSCustomObject" { $result | ConvertFrom-Json } } } } function Invoke-JavaScript { [CmdletBinding(DefaultParameterSetName = "Path")] param ( [Parameter(ParameterSetName = 'Script')] [Parameter(Position = 0)] [ValidateNotNullOrEmpty()] [String]$Script, [Parameter(ParameterSetName = 'Path')] [ValidateNotNullOrEmpty()] [ValidateScript({ Test-Path -LiteralPath $_ })] [string]$Path, [Parameter(ParameterSetName = 'Script')] [Parameter(ParameterSetName = 'Path')] [Parameter(ValueFromPipeline = $true)] [Object]$InputObject = $null, [Parameter(ParameterSetName = 'Script')] [Parameter(ParameterSetName = 'Path')] [Object]$Parameters, [Parameter(ParameterSetName = 'Script')] [Parameter(ParameterSetName = 'Path')] [ValidateSet("JavaScript")] [String]$Language="JavaScript", [Parameter(ParameterSetName = 'Script')] [Parameter(ParameterSetName = 'Path')] [ValidateSet("PSCustomObject","JSON", "PLIST", IgnoreCase = $false)] [PowerShellOSAOutputFormat]$OutputFormat="PSCustomObject" ) if ($PSCmdlet.ParameterSetName -eq "Script") { Invoke-OSA -Script $Script -InputObject $InputObject -Parameters $Parameters -Language $Language -OutputFormat $OutputFormat } else { Invoke-OSA -Path -$Path $InputObject -Parameters $Parameters -Language $Language -OutputFormat $OutputFormat } } function ConvertTo-Hashtable { <# .SYNOPSIS Convert a PSCustomObject to a Hashtable. #> param( [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [PSCustomObject]$Object ) $hashtable = @{} $Object.PSObject.Properties | ForEach-Object { $hashtable[$_.Name] = $_.Value } $hashtable } function ConvertTo-PSCutomObject { <# .SYNOPSIS Convert a Hashtable to a PSCustomObject. #> param( [hashtable]$Hashtable ) New-Object psobject -Property $Hashtable } function ConvertFrom-PascalCase { <# .SYNOPSIS Convert a Pascal Case word to a string. #> param ( [string]$String ) ($String -creplace '([a-z])([A-Z])', '$1 $2').ToLower() } function ConvertTo-PascalCase { <# .SYNOPSIS Convert a string to a Pascal Case word. #> param ( [string]$String, [string]$Delimiter = " " ) # Remove diacritics $normalized = $String.Normalize([Text.NormalizationForm]::FormKD); $String = $(-join ($normalized.ToCharArray() | Where-Object { [Globalization.CharUnicodeInfo]::GetUnicodeCategory($_) -ne [Globalization.UnicodeCategory]::NonSpacingMark })) $String = $String.Replace("_"," ") $String = $String.Replace("-"," ") $String = $String.Replace("."," ") $String = $String.Replace("’s","") $String = $String.Replace("#","N") $String = $(-join ($String.ToCharArray() | Where-Object { -not [Char]::IsPunctuation($_) })) $String = [Text.RegularExpressions.Regex]::Replace($String, "\s+", " ") $words = $String -split $Delimiter $result = "" foreach ($word in $words) { if ($word.Length -gt 0) { $capitalized = $word.Substring(0,1).ToUpper() + $word.Substring(1) } $result += $capitalized } return $result } function ConvertTo-AppleScriptParameter { <# .SYNOPSIS Convert a PowerShell parameter to an AppleScript parameter. #> param( $Parameter ) $value = $null Write-Debug $Parameter.GetType() Write-Debug "$($Parameter) $($Parameter.Key) $($Parameter.Value) $($Parameter.Value.GetType()) $($Parameter.Value.GetType().BaseType)" if ($Parameter.Value.GetType() -eq [switch]) { $value = $($Parameter.Value) ? "with $(ConvertFrom-PascalCase $Parameter.Key)" : "without $(ConvertFrom-PascalCase $Parameter.Key)" } elseif ($Parameter.Value.GetType() -eq [String]) { $value = "`"`"$($Parameter.Value)`"`"" } elseif ($Parameter.Value.GetType() -eq [securestring]) { $password = $([PSCredential]::new(0, $Parameter.Value).GetNetworkCredential().Password) $value = "`"`"$($password)`"`"" } elseif ($Parameter.Value.GetType().BaseType -eq [Enum]) { $value = "$(ConvertFrom-PascalCase $Parameter.Value)" } elseif ($Parameter.Value.GetType() -eq [Uri]) { $value = "(POSIX file `"`"$($Parameter.Value)`"`" )" } elseif ($Parameter.Value.GetType().BaseType -eq [array]) { $list = @() foreach ($item in $Parameter.Value) { if ($item.GetType() -eq [string]) { $list += "`"`"$item`"`"" } elseif ($item.GetType() -contains @([int], [Double])) { $list += $item } else { $list += "`"$item`"" } } $value = "{ " + $($list -join ", ") + " }" } else { $value = $Parameter.Value } return " $value " } function New-AppleScriptCommand { <# .SYNOPSIS Generate an AppleScript command from a PowerShell function. #> param ( [string]$Command, [hashtable]$Parameters, [array]$IgnoreParameters ) if ($IgnoreParameters.Count -gt 0) { foreach ($ignoreParameter in $IgnoreParameters) { $Parameters.Remove($ignoreParameter) } } if ($Parameters.ContainsKey('DirectParameter')) { $DirectParameter = $Parameters.('DirectParameter') $value = ConvertTo-AppleScriptParameter $([Collections.DictionaryEntry]::new("DirectParameter", $DirectParameter)) $Command += $value $Parameters.Remove('DirectParameter') } foreach ($parameter in $Parameters.GetEnumerator()) { $value = ConvertTo-AppleScriptParameter $parameter if ($parameter.Value.GetType() -eq [switch]) { $Command += $value } else { $Command += " $(ConvertFrom-PascalCase $parameter.Key) $value " } } Write-Debug $Command $Command } Set-Alias -Name Invoke-AppleScript -Value Invoke-OSA Set-Alias -Name Invoke-AppleScriptObjC -Value Invoke-OSA Export-ModuleMember -Alias @( 'Invoke-AppleScript', 'Invoke-AppleScriptObjC' ) Export-ModuleMember -Function @( 'ConvertFrom-PascalCase', 'ConvertTo-AppleScriptParameter', 'ConvertTo-Hashtable', 'ConvertTo-PascalCase', 'ConvertTo-PSCutomObject' 'Invoke-JavaScript' 'Invoke-OSA', 'New-AppleScriptCommand' ) |