Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ jobs:
run: |
Get-PackageProvider -Name NuGet -ForceBootstrap | Out-Null
Install-Module -Name Pester -RequiredVersion 5.7.1 -Scope CurrentUser -Force -SkipPublisherCheck -AllowClobber
Install-Module -Name PSScriptAnalyzer, DependsOn -Scope CurrentUser -Force -SkipPublisherCheck -AllowClobber
Install-Module -Name PSScriptAnalyzer -RequiredVersion 1.25.0 -Scope CurrentUser -Force -SkipPublisherCheck -AllowClobber
Install-Module -Name DependsOn -Scope CurrentUser -Force -SkipPublisherCheck -AllowClobber

- name: Import module from source
shell: pwsh
Expand All @@ -59,6 +60,7 @@ jobs:
shell: pwsh
run: |
Import-Module Pester -RequiredVersion 5.7.1 -Force
Import-Module PSScriptAnalyzer -RequiredVersion 1.25.0 -Force
Import-Module ./PSGraph/PSGraph.psd1 -Force

$config = New-PesterConfiguration
Expand Down
4 changes: 2 additions & 2 deletions PSGraph/PSGraph.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
# NestedModules = @()

# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = @('Edge','Entity','Export-PSGraph','Graph','Inline','Install-GraphViz','Node','Rank','Record','Row','Set-NodeFormatScript','Show-PSGraph','SubGraph')
FunctionsToExport = @('Edge','Entity','Export-PSGraph','Graph','Inline','Install-GraphViz','New-EdgeAttributeSet','New-NodeAttributeSet','Node','Rank','Record','Row','Set-NodeFormatScript','Show-PSGraph','SubGraph')

# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
CmdletsToExport = @()
Expand All @@ -78,7 +78,7 @@
VariablesToExport = '*'

# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
AliasesToExport = @('digraph')
AliasesToExport = @('digraph', 'NodeAttributes', 'EdgeAttributes')

# DSC resources to export from this module
# DscResourcesToExport = @()
Expand Down
3 changes: 2 additions & 1 deletion PSGraph/PSGraph.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,5 @@ foreach ($folder in $folders)
Write-Verbose -Message 'Exporting Public functions...'
$functions = Get-ChildItem -Path "$PSScriptRoot\Public" -Filter '*.ps1' -Recurse

Export-ModuleMember -Function $functions.BaseName
# Keep in sync with PSGraph.psd1's AliasesToExport
Export-ModuleMember -Function $functions.BaseName -Alias 'DiGraph', 'NodeAttributes', 'EdgeAttributes'
18 changes: 15 additions & 3 deletions PSGraph/Private/Format-Value.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,24 @@ function Format-Value
if ($Edge -and
# is not surounded by explicit quotes
$value -notmatch '^".*"$' -and
# has record notation with a word as a target
$value -match '^(?<node>.+):(?<Record>(\w+))$'
# has record notation with a port/row target - allow hyphens so GUID-style
# row IDs (see issue #65) are recognized as a port, not part of the node name
$value -match '^(?<node>.+):(?<Record>[\w-]+)$'
)
{
# Capture both groups before any further regex ops below, since -notmatch
# re-populates (and would otherwise clobber) $matches
$recordNode = $matches.node
$recordPort = $matches.Record

if ($recordPort -notmatch '^[A-Za-z_]\w*$')
{
# Not a bare GraphViz identifier (e.g. a GUID, or starts with a digit) - quote it
$recordPort = '"{0}"' -f $recordPort
}

# Recursive call to this function to format just the node
"{0}:{1}" -f (Format-Value $matches.node -Node), $matches.record
"{0}:{1}" -f (Format-Value $recordNode -Node), $recordPort
}
else
{
Expand Down
4 changes: 3 additions & 1 deletion PSGraph/Private/Update-DefaultArgument.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ function Update-DefaultArgument
$InputObject['LayoutEngine'] = Get-LayoutEngine -Name $InputObject['LayoutEngine']
}

if ( -Not $InputObject.ContainsKey( 'DestinationPath' ) )
# PassThru intentionally omits DestinationPath so graphviz writes to stdout;
# don't let AutoName's '-O' flag force it to an auto-named file instead.
if ( -Not $InputObject.ContainsKey( 'DestinationPath' ) -and -Not $InputObject.ContainsKey( 'PassThru' ) )
{
$InputObject["AutoName"] = $true;
}
Expand Down
45 changes: 41 additions & 4 deletions PSGraph/Public/Export-PSGraph.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ function Export-PSGraph
Path or paths to the dot graphviz executable. Some sensible defaults are used if nothing is passed.
.PARAMETER ShowGraph
Launches the graph when done
.PARAMETER PassThru
Returns the rendered graph as text instead of writing it to a file. Useful for
piping SVG/DOT output into a notebook workflow (e.g. Jupyter/.NET Interactive).
Only supported when Source is inline DOT text (not a file path), and cannot be
combined with -DestinationPath or -ShowGraph.
.Example
Export-PSGraph -Source graph.dot -OutputFormat png

Expand All @@ -24,6 +29,11 @@ function Export-PSGraph
edge (5..2)
} | Export-PSGraph -Destination $env:temp\test.png

.Example
graph g {
edge hello world
} | Export-PSGraph -OutputFormat svg -PassThru

.Notes
The source can either be files or piped graph data.

Expand Down Expand Up @@ -74,20 +84,37 @@ function Export-PSGraph
[string[]]
$GraphVizPath = (
'C:\Program Files\NuGet\Packages\Graphviz*\dot.exe',
"$env:USERPROFILE\AppData\Local\PackageManagement\NuGet\Packages\Graphviz*\dot.exe", # Install-GraphViz -Scope CurrentUser location
'C:\program files*\GraphViz*\bin\dot.exe',
'/usr/local/bin/dot',
'/usr/bin/dot'
),

# launches the graph when done
[switch]
$ShowGraph
$ShowGraph,

# returns the rendered graph as text instead of writing it to a file
[switch]
$PassThru
)

begin
{
try
{
if ( $PassThru )
{
if ( $PSBoundParameters.ContainsKey('DestinationPath') -and -Not [string]::IsNullOrEmpty($DestinationPath) )
{
throw '-PassThru cannot be combined with -DestinationPath; PassThru returns the rendered graph instead of writing a file.'
}
if ( $ShowGraph )
{
throw '-PassThru cannot be combined with -ShowGraph; there is no destination file to show when the graph is returned as text.'
}
}

$graphViz = $null

# Unless the caller explicitly pinned a path, prefer a cross-platform
Expand All @@ -107,7 +134,7 @@ function Export-PSGraph
if ( $null -eq $graphViz )
{
$GraphvizPathString = $GraphVizPath -Join " or "
throw "Could not find GraphViz installed on this system. Please run 'Install-GraphViz' to install the needed binaries and libraries. This module looked for a 'dot' executable on PATH and in the following paths: $($GraphvizPathString). Optionally pass a path to your dot.exe file with the GraphVizPath parameter"
throw "Could not find GraphViz installed on this system. Please run 'Install-GraphViz' (or 'Install-GraphViz -Scope CurrentUser' if you don't have admin rights) to install the needed binaries and libraries. This module looked for a 'dot' executable on PATH and in the following paths: $($GraphvizPathString). Optionally pass a path to your dot.exe file with the GraphVizPath parameter"
}

$useStandardInput = $false
Expand Down Expand Up @@ -149,6 +176,11 @@ function Export-PSGraph

if ( $null -ne $fileList -and $Source.Count -gt 0 )
{
if ( $PassThru )
{
throw '-PassThru is only supported when Source is inline DOT text, not a file path.'
}

foreach ( $file in $fileList )
{
Write-Verbose "Generating graph from '$($file.path)'"
Expand Down Expand Up @@ -182,7 +214,7 @@ function Export-PSGraph
if ( $useStandardInput )
{
Write-Verbose 'Processing standard input'
if ( -Not $PSBoundParameters.ContainsKey( 'DestinationPath' ) )
if ( -Not $PSBoundParameters.ContainsKey( 'DestinationPath' ) -and -Not $PassThru )
{
Write-Verbose ' Creating temporary path to save graph'

Expand All @@ -200,12 +232,17 @@ function Export-PSGraph
$arguments = Get-GraphVizArgument $PSBoundParameters
Write-Verbose " Arguments: $($arguments -join ' ')"

$null = $standardInput.ToString() | & $graphViz @($arguments)
$result = $standardInput.ToString() | & $graphViz @($arguments)
if ($LastExitCode)
{
Write-Error -ErrorAction Stop -Exception ([System.Management.Automation.ParseException]::New())
}

if ( $PassThru )
{
return $result
}

if ( $ShowGraph )
{
# Launches image with default viewer as decided by explorer
Expand Down
36 changes: 31 additions & 5 deletions PSGraph/Public/Install-GraphViz.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,26 @@ function Install-GraphViz
<#
.Description
Installs GraphViz package using online provider
.PARAMETER Scope
Use -Scope CurrentUser to install as a non-admin user to a per-user
location instead of Program Files. Unused on macOS.
.Example
Install-GraphViz
.Example
Install-GraphViz -Scope CurrentUser
#>
[cmdletbinding( SupportsShouldProcess = $true, ConfirmImpact = "High" )]
param()
param(
[ValidateSet('AllUsers', 'CurrentUser')]
[string]
$Scope = 'AllUsers'
)

process
{
try
{
if ( $IsOSX )
if ( $IsMacOS )
{
if ( $PSCmdlet.ShouldProcess( 'Install graphviz' ) )
{
Expand All @@ -24,12 +33,29 @@ function Install-GraphViz
{
if ( $PSCmdlet.ShouldProcess('Register Chocolatey provider and install graphviz' ) )
{
if ( -Not ( Get-PackageProvider | Where-Object ProviderName -eq 'Chocolatey' ) )
if ( -Not ( Get-PackageSource | Where-Object ProviderName -eq 'Chocolatey' ) )
{
Register-PackageSource -Name Chocolatey -ProviderName Chocolatey -Location http://chocolatey.org/api/v2/
try
{
Register-PackageSource -Name Chocolatey -ProviderName Chocolatey -Location http://chocolatey.org/api/v2/ -ErrorAction Stop
}
catch
{
# Registering Chocolatey typically requires admin rights. Fall back to the
# (older, but still functional) GraphViz package on nuget.org instead of failing outright.
$nugetSource = Get-PackageSource | Where-Object { $_.Location -like 'https://api.nuget.org/v*' }
if ( -Not $nugetSource )
{
Write-Warning 'No nuget.org package source found to fall back on. Cannot install GraphViz.'
throw
}

Write-Warning 'Could not register a Chocolatey package provider (this typically requires admin rights). Falling back to the older GraphViz package on nuget.org.'
Write-Warning 'Install Chocolatey and re-run this command to get the latest GraphViz.'
}
}

Find-Package graphviz | Install-Package -Verbose -ForceBootstrap
Find-Package graphviz | Install-Package -Verbose -ForceBootstrap -Scope $Scope
}
}
}
Expand Down
135 changes: 135 additions & 0 deletions PSGraph/Public/New-EdgeAttributeSet.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
function New-EdgeAttributeSet
{
<#
.SYNOPSIS
Builds a GraphViz attribute hashtable for the Edge command.

.DESCRIPTION
Edge takes a hashtable of attributes, but GraphViz attribute names and values are
case-sensitive and easy to get wrong ('blue' works, 'Blue' does not). This command
exposes the common edge attributes as PowerShell parameters - with tab completion for
arrowhead/color/font values - and normalizes casing for the ones GraphViz requires lowercase.

.EXAMPLE
$attrs = New-EdgeAttributeSet -Direction both -ArrowHead crow -ArrowTail lcrow -Color Blue -Style dashed -Label test
edge one two $attrs

This defines a two-way dashed edge, in blue, with a "crow" head and left-half-crow tail.

.NOTES
Ported from upstream PR #105 (jhoneill). The source PR called .ToLower() on every
attribute value including numeric/boolean ones, which throws - this version only
lowercases the string-valued attributes GraphViz actually requires lowercase.
#>
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
[CmdletBinding()]
[Alias('EdgeAttributes')]
[OutputType([hashtable])]
param(
# Style of arrowhead on the head node of an edge. Only shown when Direction is 'forward' or 'both'.
[string]
$ArrowHead,

# Multiplicative scale factor for arrowheads
[double]
$ArrowSize,

# Style of arrowhead on the tail node of an edge. Only shown when Direction is 'back' or 'both'.
[string]
$ArrowTail,

# Basic drawing color for graphics, not text (which requires FontColor to be set)
[string]
$Color,

# If false, the edge is not used when ranking nodes
[bool]
$Constraint,

# Which ends of the edge should be decorated with an arrowhead
[ValidateSet('forward', 'back', 'both', 'none')]
[string]
$Direction,

# Color used for text
[string]
$FontColor,

# Font used for text
[string]
$FontName,

# Font size, in points, used for text
[double]
$FontSize,

# Text label placed near the head of the edge
[string]
$HeadLabel,

# Text label attached to the edge
[string]
$Label,

# Color used for HeadLabel/TailLabel; defaults to the edge's FontColor if unset
[string]
$LabelFontColor,

# Font used for HeadLabel/TailLabel; defaults to the edge's FontName if unset
[string]
$LabelFontName,

# Font size, in points, used for HeadLabel/TailLabel; defaults to the edge's FontSize if unset
[double]
$LabelFontSize,

# Preferred edge length, in inches
[double]
$Length,

# Width of the pen, in points, used to draw lines and curves
[double]
$PenWidth,

# Style for the edge, e.g. dashed, solid
[ValidateSet('dashed', 'dotted', 'solid', 'invis', 'bold', 'tapered')]
[string]
$Style,

# Text label placed near the tail of the edge
[string]
$TailLabel
)

$values = @{}

# Attributes where the GraphViz key is shortened from the parameter name
if ($PSBoundParameters.ContainsKey('Direction'))
{
$values['dir'] = $Direction.ToLower()
}
if ($PSBoundParameters.ContainsKey('Length'))
{
$values['len'] = $Length
}

# GraphViz requires these lowercase; user input may not be
foreach ($param in @('ArrowHead', 'ArrowTail', 'Color', 'FontColor', 'LabelFontColor', 'Style'))
{
if ($PSBoundParameters.ContainsKey($param))
{
$values[$param.ToLower()] = $PSBoundParameters[$param].ToLower()
}
}

# Passed through unchanged - numeric, boolean, or free-form text where case is meaningful
foreach ($param in @('ArrowSize', 'Constraint', 'FontName', 'FontSize', 'HeadLabel', 'Label', 'LabelFontName', 'LabelFontSize', 'PenWidth', 'TailLabel'))
{
if ($PSBoundParameters.ContainsKey($param))
{
$values[$param.ToLower()] = $PSBoundParameters[$param]
}
}

$values
}
Loading
Loading