Skip to content

Instantly share code, notes, and snippets.

@h8rt3rmin8r
Created June 29, 2026 14:42
Show Gist options
  • Select an option

  • Save h8rt3rmin8r/a05e8941432303ae6e8319208f49dae6 to your computer and use it in GitHub Desktop.

Select an option

Save h8rt3rmin8r/a05e8941432303ae6e8319208f49dae6 to your computer and use it in GitHub Desktop.
Convert a WebVTT (.vtt) subtitle/caption file into a single mapped JSON document via Powershell
<#
.SYNOPSIS
Convert a WebVTT (.vtt) subtitle/caption file into a single mapped JSON
document with a fixed, fully-documented schema.
.DESCRIPTION
Reads a WebVTT file, parses every structural element the format can carry
(file header and metadata, STYLE / REGION / NOTE blocks, and cue blocks with
their identifiers, timings, positioning settings, inline markup and
word-level timestamp tags), and emits a JSON file beside the input. The
output file name is identical to the input but with a .json extension; the
output directory is the same as the input.
The conversion is faithful and 1:1. Every cue in the source produces exactly
one cue object in the output, in source order. Nothing is de-duplicated,
re-timed, merged, or dropped. This matters for YouTube auto-caption files,
which intentionally emit overlapping "rolling" cues (each cue repeats the
previous visible line and appends the next line with per-word timing). Those
duplicates are preserved as-is; the per-word timing is surfaced in each
cue's "tokens" array.
FIXED OUTPUT SCHEMA (every key is always present; absent data is rendered as
an empty array, an empty object, or null, so downstream consumers can rely on
a stable shape):
schemaVersion : string - schema contract version emitted by this script.
source : object - provenance of the conversion:
fileName : string - input file name.
filePath : string - absolute input path.
byteOrderMark : bool - true if the input began with a UTF-8 BOM.
lineEnding : string - LF | CRLF | CR | MIXED | NONE (as detected
in the input).
convertedAtUtc : string - ISO-8601 UTC timestamp of the conversion.
converter : string - name of this script.
header : object - the WebVTT file header:
magic : string - always "WEBVTT".
description : string - free text after "WEBVTT" on line 1, or null.
metadata : object - parsed "Key: Value" / "Key=Value" header lines
(for example Kind and Language), key->value.
metadataRaw : array - the raw header metadata lines, verbatim.
regions : array - REGION definition blocks, each:
id : string - region id setting, or null.
settings : object - all region settings, name->value.
raw : string - the region block body, verbatim.
afterCue : int - index of the last cue seen before this block
(-1 if it preceded all cues).
styles : array - STYLE blocks, each:
css : string - the CSS payload of the block, verbatim.
afterCue : int - positional context (see regions.afterCue).
notes : array - standalone NOTE comment blocks, each:
text : string - the note body (the word NOTE stripped).
afterCue : int - positional context (see regions.afterCue).
cues : array - the cue blocks in source order, each:
index : int - 0-based ordinal among cues.
identifier : string - the cue identifier line, or null.
start : string - normalized start time "HH:MM:SS.mmm".
end : string - normalized end time "HH:MM:SS.mmm".
startSeconds : number - start time in seconds.
endSeconds : number - end time in seconds.
durationSeconds : number - endSeconds minus startSeconds.
settings : object - parsed cue settings (align, position, line,
size, vertical, region), name->value.
settingsRaw : string - the raw settings string, or null.
text : string - plain text: all markup tags and inline
timestamp tags removed, HTML entities
decoded, source lines joined with "\n".
lines : array - the plain-text payload split per source
line.
rawPayload : string - the payload exactly as in the file (markup
and inline timestamps intact), lines joined
with "\n".
voices : array - distinct <v> speaker names in the cue.
tokens : array - word/phrase-level timing derived from inline
<timestamp> tags, each { time, timeSeconds,
text }. Empty when the cue carries no inline
timing.
stats : object - summary counters:
cueCount : int - number of cues.
regionCount : int - number of REGION blocks.
styleCount : int - number of STYLE blocks.
noteCount : int - number of standalone NOTE blocks.
hasWordLevelTiming: bool - true if any cue produced tokens.
mediaSpanSeconds : number - max(endSeconds) minus min(startSeconds),
or 0 when there are no cues.
The authoritative, published JSON Schema for this output (matching the
emitted schemaVersion) is the formal contract for the shape above:
https://schemas.shruggie.tech/data/webvtt-json.schema.json
Side effects: writes one .json file to the input's directory. Refuses to
overwrite an existing output unless -Force is supplied. Reads and writes the
local filesystem only; performs no network access. Output is UTF-8 with no
BOM and LF line endings.
Requires PowerShell 7+ for deterministic ConvertTo-Json behavior (stable
key ordering and correct empty/single-element array rendering). The script
asserts the version and exits 2 if it is too low.
Exit codes: 0 success (including a -WhatIf preview); 1 the output already
exists and -Force was not supplied; 2 an environment precondition failed
(input missing, unreadable, or not a WebVTT file).
.PARAMETER Path
Path to the input .vtt file. Handled literally, so names containing wildcard
metacharacters or awkward whitespace are read verbatim. The output path is
derived from this by replacing the extension with .json.
Alias: p
.PARAMETER Force
Overwrite the derived .json output file if it already exists. Without this
switch, an existing output causes the script to stop with exit code 1 and
leave the existing file untouched.
Alias: f
.PARAMETER Quiet
Suppress informational chatter (Info, Success, Debug). Warnings and errors
still emit.
Alias: q
.PARAMETER Silent
Suppress all log output including warnings. Genuine errors still reach the
error stream.
.PARAMETER Help
Print this help text to the terminal.
Alias: h
.EXAMPLE
.\ConvertFrom-WebVtt.ps1 -Path .\captions.vtt
Converts captions.vtt to captions.json in the same directory. Fails if
captions.json already exists.
.EXAMPLE
.\ConvertFrom-WebVtt.ps1 -Path .\captions.vtt -Force
Converts and overwrites captions.json if it is already present.
.EXAMPLE
.\ConvertFrom-WebVtt.ps1 -Path '.\Who Controls [the] Internet.vtt' -Force -Quiet
Converts a file whose name contains spaces and wildcard metacharacters,
overwriting the output, with informational logging suppressed.
.EXAMPLE
.\ConvertFrom-WebVtt.ps1 -Path .\captions.vtt -WhatIf
Parses the input and reports what would be written without creating the
output file.
.NOTES
Schema version 1.0. The schema is a superset designed to round-trip the
structural information of any conformant WebVTT file, not only the YouTube
auto-caption shape: it accommodates an optional BOM and header description,
arbitrary header metadata, STYLE / REGION / NOTE blocks, cue identifiers,
hours-optional timestamps (HH:MM:SS.mmm or MM:SS.mmm), cue positioning
settings, inline markup (<b> <i> <u> <c> <v> <lang> <ruby> and friends),
HTML entities, and chapter or metadata payloads. Shapes that a given file
does not use are represented as empty collections or null, never omitted.
The output validates against the published schema linked below, whose
schemaVersion matches the schemaVersion this script emits.
.LINK
https://schemas.shruggie.tech/data/webvtt-json.schema.json
#>
[CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='Medium',DefaultParameterSetName='Default')]
Param(
[Parameter(Mandatory=$true,ParameterSetName='Default',Position=0)]
[Alias("p")]
[string]$Path,
[Parameter(Mandatory=$false,ParameterSetName='Default')]
[Alias("f")]
[Switch]$Force,
[Parameter(Mandatory=$false,ParameterSetName='Default')]
[Alias("q")]
[Switch]$Quiet,
[Parameter(Mandatory=$false,ParameterSetName='Default')]
[Switch]$Silent,
[Parameter(Mandatory=$true,ParameterSetName='HelpText')]
[Alias("h")]
[Switch]$Help
)
#_______________________________________________________________________________
## Declare Functions
function Assert-PSVersion {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$false)]
[version]$Minimum = '7.0'
)
$current = $PSVersionTable.PSVersion
if ($current -lt $Minimum) {
Write-Host ("ALERT: PowerShell {0}+ required; running {1}. Relaunch with 'pwsh'." -f $Minimum, $current) -ForegroundColor Red
exit 2
}
}
function Write-Log {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true,Position=0)]
[string]$Message,
[Parameter(Mandatory=$false)]
[ValidateSet('Info','Success','Warn','Error','Debug')]
[string]$Level = 'Info',
[Parameter(Mandatory=$false)]
[string]$Source = $null
)
if ($script:LogSilent -and $Level -ne 'Error') { return }
if ($script:LogQuiet -and (@('Info','Success','Debug') -contains $Level)) { return }
$stamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss.fff')
$tag = if ($Source) { "[$Source] " } else { '' }
$label = $Level.ToUpper().PadRight(7)
$color = switch ($Level) {
'Info' { 'Gray' }
'Success' { 'Green' }
'Warn' { 'Yellow' }
'Error' { 'Red' }
'Debug' { 'DarkGray' }
}
Write-Host ("{0} {1}{2} {3}" -f $stamp, $tag, $label, $Message) -ForegroundColor $color
}
# Convert a WebVTT timestamp (HH:MM:SS.mmm or MM:SS.mmm) to total seconds.
function ConvertFrom-VttTimestamp {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[string]$Timestamp
)
$parts = $Timestamp.Split(':')
if ($parts.Count -eq 3) {
$h = [double]$parts[0]
$m = [double]$parts[1]
$s = [double]$parts[2]
} else {
$h = 0.0
$m = [double]$parts[0]
$s = [double]$parts[1]
}
return ($h * 3600.0) + ($m * 60.0) + $s
}
# Render total seconds back to a normalized HH:MM:SS.mmm timestamp.
function ConvertTo-VttTimestamp {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[double]$Seconds
)
$total = [int][math]::Floor($Seconds)
$ms = [int][math]::Round(($Seconds - $total) * 1000.0)
if ($ms -ge 1000) { $total += 1; $ms -= 1000 }
$h = [int][math]::Floor($total / 3600)
$m = [int][math]::Floor(($total % 3600) / 60)
$s = [int]($total % 60)
return ('{0:00}:{1:00}:{2:00}.{3:000}' -f $h, $m, $s, $ms)
}
# Strip every WebVTT/HTML tag and inline timestamp tag, then decode entities.
function Remove-VttMarkup {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$false)]
[AllowEmptyString()]
[string]$Text = ''
)
$stripped = [regex]::Replace($Text, '<[^>]*>', '')
return [System.Net.WebUtility]::HtmlDecode($stripped)
}
# Extract distinct <v ...> speaker names from a raw cue payload.
function Get-VttVoice {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$false)]
[AllowEmptyString()]
[string]$Text = ''
)
$names = [System.Collections.Generic.List[string]]::new()
foreach ($match in [regex]::Matches($Text, '<v([^>]*)>')) {
$inner = $match.Groups[1].Value.Trim()
# Drop leading class tokens (".loud") to leave the speaker label.
$name = ($inner -split '\s+' | Where-Object { $_ -and ($_[0] -ne '.') }) -join ' '
$name = $name.Trim()
if ($name -and -not $names.Contains($name)) { $names.Add($name) }
}
return ,$names.ToArray()
}
# Build word/phrase-level tokens from inline <timestamp> tags in a payload.
# Each text segment is governed by the timestamp that precedes it; the first
# segment (before any inline timestamp) inherits the cue start time.
function Get-VttToken {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[AllowEmptyString()]
[string]$RawPayload,
[Parameter(Mandatory=$true)]
[double]$CueStartSeconds
)
$tokens = [System.Collections.Generic.List[object]]::new()
$tsPattern = '<((?:\d{2,}:)?\d{2}:\d{2}\.\d{3})>'
$tsMatches = [regex]::Matches($RawPayload, $tsPattern)
if ($tsMatches.Count -eq 0) {
return ,$tokens.ToArray()
}
$segmentStart = 0
$governingSeconds = $CueStartSeconds
foreach ($match in $tsMatches) {
$segment = $RawPayload.Substring($segmentStart, $match.Index - $segmentStart)
$text = ((Remove-VttMarkup -Text $segment) -replace '\s+', ' ').Trim()
if ($text.Length -gt 0) {
$tokens.Add([pscustomobject][ordered]@{
time = ConvertTo-VttTimestamp -Seconds $governingSeconds
timeSeconds = [math]::Round($governingSeconds, 3)
text = $text
})
}
$governingSeconds = ConvertFrom-VttTimestamp -Timestamp $match.Groups[1].Value
$segmentStart = $match.Index + $match.Length
}
# Trailing segment after the last timestamp.
$segment = $RawPayload.Substring($segmentStart)
$text = ((Remove-VttMarkup -Text $segment) -replace '\s+', ' ').Trim()
if ($text.Length -gt 0) {
$tokens.Add([pscustomobject][ordered]@{
time = ConvertTo-VttTimestamp -Seconds $governingSeconds
timeSeconds = [math]::Round($governingSeconds, 3)
text = $text
})
}
return ,$tokens.ToArray()
}
# Parse a whitespace-separated "name:value" settings string into an ordered map.
function ConvertFrom-VttSettings {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$false)]
[AllowEmptyString()]
[string]$Text = ''
)
$map = [ordered]@{}
foreach ($item in ($Text -split '\s+')) {
if (-not $item) { continue }
$colon = $item.IndexOf(':')
if ($colon -gt 0) {
$map[$item.Substring(0, $colon)] = $item.Substring($colon + 1)
} else {
$map[$item] = $true
}
}
return $map
}
#_______________________________________________________________________________
## Declare Variables and Arrays
$script:LogQuiet = $false
$script:LogSilent = $false
$ThisScriptPath = $MyInvocation.MyCommand.Path
$SchemaVersion = '1.0'
# A WebVTT timestamp: optional hours, then mm:ss.mmm.
$TimestampCore = '(?:\d{2,}:)?\d{2}:\d{2}\.\d{3}'
#_______________________________________________________________________________
## Execute Operations
# Catch help text requests
if (($Help) -or ($PSCmdlet.ParameterSetName -eq 'HelpText')) {
Get-Help $ThisScriptPath -Detailed
exit 0
}
# Wire suppression flags to the logging state
if ($Quiet) { $script:LogQuiet = $true }
if ($Silent) { $script:LogSilent = $true }
# Environment precondition: require PowerShell 7+
Assert-PSVersion -Minimum '7.0'
# Environment precondition: the input file must exist and be a file
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
Write-Host ("FAIL: input file not found: {0}" -f $Path) -ForegroundColor Red
exit 2
}
$inputItem = Get-Item -LiteralPath $Path
$inputFull = $inputItem.FullName
$outputFull = [System.IO.Path]::ChangeExtension($inputFull, '.json')
# Guard: refuse to clobber an existing output unless -Force was supplied
if ((Test-Path -LiteralPath $outputFull) -and (-not $Force)) {
Write-Host ("FAIL: output already exists (use -Force to overwrite): {0}" -f $outputFull) -ForegroundColor Red
exit 1
}
Write-Log ("Reading {0}" -f $inputFull) -Level Info -Source 'Read'
try {
$bytes = [System.IO.File]::ReadAllBytes($inputFull)
} catch {
Write-Host ("FAIL: could not read input: {0}" -f $_.Exception.Message) -ForegroundColor Red
exit 2
}
# Detect and strip a leading UTF-8 BOM (EF BB BF) from the raw bytes, since
# the higher-level text readers strip it silently and hide its presence.
$hadBom = ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF)
$startIndex = if ($hadBom) { 3 } else { 0 }
$rawText = [System.Text.Encoding]::UTF8.GetString($bytes, $startIndex, $bytes.Length - $startIndex)
# Detect the source line ending before normalizing to LF
$hasCrlf = $rawText.Contains("`r`n")
$bareText = $rawText -replace "`r`n", "`n"
$hasLoneCr = $bareText.Contains("`r")
$hasLf = $bareText.Contains("`n")
$lineEnding =
if ($hasCrlf -and -not $hasLoneCr) { 'CRLF' }
elseif ($hasCrlf -and $hasLoneCr) { 'MIXED' }
elseif ($hasLoneCr -and -not $hasLf) { 'CR' }
elseif ($hasLf) { 'LF' }
else { 'NONE' }
# Normalize all line endings to LF for parsing
$normalized = $bareText -replace "`r", "`n"
# Environment precondition: must be a WebVTT file
if ($normalized -notmatch '^WEBVTT(\s|$)') {
Write-Host "FAIL: input does not start with the WEBVTT signature; not a WebVTT file." -ForegroundColor Red
exit 2
}
Write-Log "Parsing WebVTT structure." -Level Info -Source 'Parse'
# Split the body into blocks on empty lines (two or more consecutive
# newlines). Per the WebVTT spec only a genuinely empty line terminates a
# block; a whitespace-only line is payload content, which matters for
# YouTube auto-captions whose cues carry space-only lines.
$blocks = [regex]::Split($normalized, "\n{2,}")
# Output accumulators
$regions = [System.Collections.Generic.List[object]]::new()
$styles = [System.Collections.Generic.List[object]]::new()
$notes = [System.Collections.Generic.List[object]]::new()
$cues = [System.Collections.Generic.List[object]]::new()
$headerDescription = $null
$headerMetadata = [ordered]@{}
$headerMetadataRaw = [System.Collections.Generic.List[string]]::new()
$cueIndex = 0
$anyTokens = $false
$blockNumber = 0
foreach ($block in $blocks) {
$body = $block.Trim("`n")
if ($body.Trim().Length -eq 0) { continue }
$blockNumber++
$lines = $body -split "\n"
$firstLine = $lines[0]
# The very first non-empty block is the file header
if ($blockNumber -eq 1 -and $firstLine -match '^WEBVTT') {
if ($firstLine -match '^WEBVTT[ \t]+(.*)$') {
$headerDescription = $Matches[1].Trim()
}
foreach ($metaLine in ($lines | Select-Object -Skip 1)) {
$trimmed = $metaLine.Trim()
if ($trimmed.Length -eq 0) { continue }
$headerMetadataRaw.Add($metaLine)
if ($trimmed -match '^([^:=]+)[:=](.*)$') {
$headerMetadata[$Matches[1].Trim()] = $Matches[2].Trim()
}
}
continue
}
# NOTE comment block
if ($firstLine -match '^NOTE(\s|$)') {
$noteText = $body -replace '^NOTE[ \t]*', ''
$notes.Add([pscustomobject][ordered]@{
text = $noteText.Trim()
afterCue = $cueIndex - 1
})
continue
}
# STYLE block
if ($firstLine -match '^STYLE(\s|$)') {
$css = (($lines | Select-Object -Skip 1) -join "`n").Trim()
$styles.Add([pscustomobject][ordered]@{
css = $css
afterCue = $cueIndex - 1
})
continue
}
# REGION block
if ($firstLine -match '^REGION(\s|$)') {
$regionBody = (($lines | Select-Object -Skip 1) -join ' ').Trim()
if ($regionBody.Length -eq 0 -and $firstLine -match '^REGION[ \t]+(.*)$') {
$regionBody = $Matches[1].Trim()
}
$regionSettings = ConvertFrom-VttSettings -Text $regionBody
$regionId = if ($regionSettings.Contains('id')) { $regionSettings['id'] } else { $null }
$regions.Add([pscustomobject][ordered]@{
id = $regionId
settings = $regionSettings
raw = (($lines | Select-Object -Skip 1) -join "`n").Trim()
afterCue = $cueIndex - 1
})
continue
}
# Locate the timing line within the block (cue blocks only)
$timingLineIndex = -1
for ($i = 0; $i -lt $lines.Count; $i++) {
if ($lines[$i] -match ('^\s*(' + $TimestampCore + ')\s*-->\s*(' + $TimestampCore + ')\s*(.*)$')) {
$timingLineIndex = $i
break
}
}
# No timing line and not a known block type: skip defensively
if ($timingLineIndex -lt 0) {
Write-Log ("Skipping unrecognized block: {0}" -f ($firstLine.Substring(0, [math]::Min(40, $firstLine.Length)))) -Level Warn -Source 'Parse'
continue
}
# Parse the timing line
$null = $lines[$timingLineIndex] -match ('^\s*(' + $TimestampCore + ')\s*-->\s*(' + $TimestampCore + ')\s*(.*)$')
$startRaw = $Matches[1]
$endRaw = $Matches[2]
$settingsRaw = $Matches[3].Trim()
# The line(s) above the timing line, if any, form the identifier
$identifier = $null
if ($timingLineIndex -gt 0) {
$identifier = (($lines[0..($timingLineIndex - 1)]) -join "`n").Trim()
if ($identifier.Length -eq 0) { $identifier = $null }
}
# The line(s) below the timing line form the payload
$payloadLines = @()
if ($timingLineIndex -lt ($lines.Count - 1)) {
$payloadLines = $lines[($timingLineIndex + 1)..($lines.Count - 1)]
}
$rawPayload = ($payloadLines -join "`n")
$startSeconds = ConvertFrom-VttTimestamp -Timestamp $startRaw
$endSeconds = ConvertFrom-VttTimestamp -Timestamp $endRaw
$plainLines = @($payloadLines | ForEach-Object { (Remove-VttMarkup -Text $_) })
$plainText = ($plainLines -join "`n")
$voices = Get-VttVoice -Text $rawPayload
$tokens = Get-VttToken -RawPayload $rawPayload -CueStartSeconds $startSeconds
if ($tokens.Count -gt 0) { $anyTokens = $true }
$settingsMap = if ($settingsRaw.Length -gt 0) { ConvertFrom-VttSettings -Text $settingsRaw } else { [ordered]@{} }
$cues.Add([pscustomobject][ordered]@{
index = $cueIndex
identifier = $identifier
start = ConvertTo-VttTimestamp -Seconds $startSeconds
end = ConvertTo-VttTimestamp -Seconds $endSeconds
startSeconds = [math]::Round($startSeconds, 3)
endSeconds = [math]::Round($endSeconds, 3)
durationSeconds = [math]::Round($endSeconds - $startSeconds, 3)
settings = $settingsMap
settingsRaw = if ($settingsRaw.Length -gt 0) { $settingsRaw } else { $null }
text = $plainText
lines = @($plainLines)
rawPayload = $rawPayload
voices = @($voices)
tokens = @($tokens)
})
$cueIndex++
}
# Summary statistics
$mediaSpan = 0.0
if ($cues.Count -gt 0) {
$minStart = ($cues | Measure-Object -Property startSeconds -Minimum).Minimum
$maxEnd = ($cues | Measure-Object -Property endSeconds -Maximum).Maximum
$mediaSpan = [math]::Round($maxEnd - $minStart, 3)
}
# Assemble the fixed-schema document
$document = [pscustomobject][ordered]@{
schemaVersion = $SchemaVersion
source = [pscustomobject][ordered]@{
fileName = $inputItem.Name
filePath = $inputFull
byteOrderMark = $hadBom
lineEnding = $lineEnding
convertedAtUtc = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
converter = 'ConvertFrom-WebVtt.ps1'
}
header = [pscustomobject][ordered]@{
magic = 'WEBVTT'
description = $headerDescription
metadata = $headerMetadata
metadataRaw = @($headerMetadataRaw.ToArray())
}
regions = @($regions.ToArray())
styles = @($styles.ToArray())
notes = @($notes.ToArray())
cues = @($cues.ToArray())
stats = [pscustomobject][ordered]@{
cueCount = $cues.Count
regionCount = $regions.Count
styleCount = $styles.Count
noteCount = $notes.Count
hasWordLevelTiming = $anyTokens
mediaSpanSeconds = $mediaSpan
}
}
Write-Log ("Parsed {0} cue(s), {1} region(s), {2} style(s), {3} note(s)." -f $cues.Count, $regions.Count, $styles.Count, $notes.Count) -Level Info -Source 'Parse'
$json = $document | ConvertTo-Json -Depth 12
# Normalize to LF endings for the on-disk file
$json = $json -replace "`r`n", "`n"
# Gate the only state-changing action behind ShouldProcess
if ($PSCmdlet.ShouldProcess($outputFull, 'Write JSON output')) {
try {
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($outputFull, $json, $utf8NoBom)
} catch {
Write-Host ("FAIL: could not write output: {0}" -f $_.Exception.Message) -ForegroundColor Red
exit 1
}
Write-Log ("Wrote {0}" -f $outputFull) -Level Success -Source 'Write'
}
exit 0
#_______________________________________________________________________________
## End of script
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment