Skip to content

Instantly share code, notes, and snippets.

@Nejat
Last active April 6, 2024 04:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Nejat/06c275749b7931857bb4c7604a026877 to your computer and use it in GitHub Desktop.
Save Nejat/06c275749b7931857bb4c7604a026877 to your computer and use it in GitHub Desktop.
Lists and optionally upgrades available WinGet updates. Skips Unknown versions or anything in the skip list
# winget-updates.nu
# GitHub Gists
# this script - https://gist.github.com/Nejat/06c275749b7931857bb4c7604a026877
# example skip config - https://gist.github.com/Nejat/5a41fe09bb27c1bbcf345fb1dea8a234
# powersheel version - https://gist.github.com/Nejat/6cb42f098320cb2fcb57a4e1728ca29e
# output formatting constants
const err_color: string = 'light_red'
const fail_color: string = 'red'
const hdr_color: string = 'light_blue'
const msg_color: string = 'light_yellow'
const new_color: string = 'cyan'
const ok_color: string = 'light_green'
const skp_color: string = 'yellow'
const upg_color: string = 'magenta'
# column format padding
const extra_pad: int = 2
# status indicators
const fail_msg: string = 'Upgrad Failed'
const new_msg: string = 'New Version Available'
const ok_msg: string = 'Successfully Upgraded'
const skp_msg: string = 'Skipped'
const upg_msg: string = 'Installing Upgrade ...'
# random nothing messages
# - have any more witty/whimsical/nonsensical/useless suggestions?
const nada: list<string> = [
'Nothing to upgrade',
'¡Nada Zilch Nichts!',
'🤙 Nah Brah',
'Goose Egg 🥚',
'Zero found',
'Big fat NOTHING!'
'Nothing to see here, keep it moving ...',
'¯\_(ツ)_/¯',
]
# column names for a table of available upgrades
const columns: list<string> = [name version available status]
# upgrade table's column alignments
const alignment: record = {
name: left,
id: left,
version: right,
available: right,
status: left,
}
# minimum widths for output columns
let width_minimums: table = [
{
status: ([$upg_msg $fail_msg $ok_msg] | get-max-width)
}
]
# list all packages that have new versions available
def "main list" [
--skip_file (-s): string = 'wngt-cfg' # skip configuration file
]: string -> nothing {
process-upgrade --skip_file $skip_file
}
# upgrade all packages that have new versions available
def "main upgrade" [
--skip_file (-s): string = 'wngt-cfg' # skip configuration file
]: string -> nothing {
process-upgrade --skip_file $skip_file --upgrade
}
# top level help
def main []: nothing -> nothing {
nu winget-updates.nu --help
}
# primary script logic
def process-upgrade [
# skip file is used to skip upgrades that winget fails to handle properlyy
--skip_file (-s): string,
# flag to process the upgrades, otherwise it will only list packages with new versions available
--upgrade (-u)
] -> nothing {
# cast to upgrade flag to boolean
let upgrade: bool = ($upgrade | into bool)
# get a table of available upgrades
let upgrades: table = (get-updates $skip_file)
# get a record of max column widths for all columns of the upgrades table
let col_widths: record = (get-column-widths $upgrades $width_minimums)
# determine the formatting joiners
let hdr_join: string = if $upgrade { '─┴─' } else { '─┼─' }
let fld_join: string = if $upgrade { ' ' } else { ' │ ' }
print ''
# output table header
let header: string = (
$columns
| each {
|col| ($col | str capitalize) | fill -a ($alignment | get $col) -c ' ' -w ($col_widths | get $col)
}
| str join $'(ansi reset) │ (ansi $hdr_color)'
)
print $"(ansi $hdr_color)($header)(ansi reset)"
# output header bottom border
print (
$columns
| each {
|col| '' | fill -a ($alignment | get $col) -c '─' -w ($col_widths | get $col)
} | str join $hdr_join
)
# process/list all upgrades
for up: record in $upgrades {
# initialize a list of padded columns for output
let base_record: list<string> = [
($up.name | fill -a $alignment.name -c ' ' -w $col_widths.name)
($up.version | fill -a $alignment.version -c ' ' -w $col_widths.version)
($up.available | fill -a $alignment.available -c ' ' -w $col_widths.available)
]
let needs_upgrade: bool = $upgrade and ($up.status | str contains $new_msg)
# pad and append the existing status message if the package is not going to be upgraded
let record: list<string> = if not $needs_upgrade {
(
$base_record
| append ($up.status | fill -a $alignment.status -c ' ' -w $col_widths.status)
)
} else {
(
$base_record
| append ($'(ansi $upg_color)($upg_msg)(ansi reset)' | fill -a $alignment.status -c ' ' -w $col_widths.status)
)
}
# output the record, no carriage return pending upgrade status
let clear_msg: closure = (message-display $'($record | str join $fld_join)')
if not $upgrade {
# complete the new line
print ''
} else {
# requires upgrade if flagged for update and the package is not skipped
if $needs_upgrade {
# update package with winget
let result: record = (do { ^winget upgrade --id $up.id --source winget } | complete)
do $clear_msg
if $result.exit_code == 0 {
# complete record output with success message
let record: list<string> = (
$base_record
| append ($'(ansi $ok_color)($ok_msg)(ansi reset)' | fill -a $alignment.status -c ' ' -w $col_widths.status)
)
print $'($record | str join $fld_join)'
} else {
# otherwise complete output with failure message plus any captured output(s)
let record: list<string> = (
$base_record
| append ($'(ansi $err_color)($fail_msg)(ansi reset)' | fill -a $alignment.status -c ' ' -w $col_widths.status)
)
print $'($record | str join $fld_join)'
let has_fail_message: bool = false
# display captured stdout message if one exists
if not ($result.stdout | is-empty) {
print ''
print $'(ansi $msg_color)($result.stdout)(ansi reset)'
let has_fail_message: bool = true
}
# display captured stderr message if one exists
if not ($result.stderr | is-empty) {
print ''
print $'(ansi $fail_color)($result.stderr)(ansi reset)'
let has_fail_message: bool = true
}
# add trailing line space if additional messages were displayed
if $has_fail_message { print '' }
}
} else {
# otherwise, complete the new line
print ''
}
}
}
}
# determines the max length for each column in table
def get-column-widths [data: table, minimums: table] -> record {
$data | each {
|rec| $rec | into value | transpose key value | each {
|itm| { $itm.key: (([$itm.value $itm.key] | get-max-width ($minimums | get --ignore-errors $itm.key | default 0)) + $extra_pad) }
} | reduce --fold {} { |nxt, acc| {...$acc, ...$nxt } }
} | math max
}
# get max width of all strings in a list
def get-max-width [min_width: int = 0]: list<string> -> int {
$in | each { |itm| $itm | str length} | append $min_width | math max
}
# retrieve and parse available winget updates
def get-updates [skip_file: string] -> table {
let clear_msg: closure = (message-display $'Reading skip configuration file ($skip_file) ...')
let skipped: table = (read-skip-file $skip_file)
do $clear_msg
let clear_msg: closure = (message-display 'Searching for updates ...')
let upgrades: list<string> = (
# query winget for any new versions available
winget upgrade --source winget --include-unknown
# convert output into lines, skipping empties
| lines --skip-empty
# skip any extra heading lines, start at column headers
| skip until { |ln| $ln =~ 'Name.*Id.*Version.*Available' }
# only include header and data lines, skip header border and trailing message
| where not ($it =~ 'upgrades available|-----')
)
do $clear_msg
# check if there is anything to upgrade
if ($upgrades | is-empty) or ($upgrades | any { |ln| $ln | str contains 'No installed package found' }) {
# exit expressively if nothing available to upgrade
print $"\n($nada | get (random int ..(($nada | length) - 1)))\n"
exit 0
}
# get the first record, which should be the titlebar
let title_bar: record = ($upgrades | get 0)
# find the indecies of the columns for parsing
let nm_idx: int = ($title_bar | str index-of 'Name')
let id_idx: int = (($title_bar | str index-of 'Id') - $nm_idx)
let vr_idx: int = (($title_bar | str index-of 'Version') - $nm_idx)
let av_idx: int = (($title_bar | str index-of 'Available') - $nm_idx)
# parse the winget output, skipping the title line
($upgrades | skip 1) | each { |up|
{
name: ($up | str substring ..$id_idx | str trim),
id: ($up | str substring $id_idx..$vr_idx | str trim),
version: ($up | str substring $vr_idx..$av_idx | str trim),
available: ($up | str substring $av_idx.. | str trim),
# deetermine if the upgrade is configured to skip
# if skipped; uses the comments of the skip configuration for a status
# othewrwise; uses the new message for a status
status: (
$skipped | where { |skp| $up | str contains $skp.skip }
| append [[skip comments]; ['' $'(ansi $new_color)($new_msg)(ansi reset)']]
| take 1
).0.comments
}
}
}
# erases text on the current line for width of message
def message-clear [msg: string] {
if ($msg| str length) > 0 {
print -n $"\r('' | fill -w ($msg | str length) -c ' ')\r"
}
}
# displays a message with no new line, returns a clousre to clear the message displayed
def message-display [msg: string] -> closure {
print -n $"($msg)\r"
{ || message-clear $msg }
}
# read and parse skip configuration file
def read-skip-file [file: string] -> table {
if not ($file | path exists) { return [] }
(
open $file
| lines --skip-empty
| split column '#' skip comments
| str trim
| each { |rw|
{
skip: $rw.skip,
comments: (
if (($rw | columns | length) > 1) {
$'(ansi $skp_color)($skp_msg)(ansi reset); ($rw.comments)'
} else {
''
}
)
}
}
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment