[python] Script that (should) update pwsh

Motivation

Updating PowerShell on Windows is a manual process, but it has become dull. Semi-automated.

Constitution

If you run pshupdate.py, which is the main body of the script, in python, it will automatically check the version and update it if necessary (planned).

Contents

I'm not doing that elaborate because I just wrote it according to what I should do, but I will write it.

Storage of Version data

Since the version is a character string of such a form as "v3.2.1", the rest after removing only the first extra character is made into a triad of character strings to make it a Version type. If you do (Get-Host) .Version | Format-List * with pwsh, you will get 6 keys, but I haven't used the latter 3 and 3 is enough! Yoshi! The size comparison is implemented in the latter half. For some flexibility, I decided to treat the values of Minor and Build as None, 0, and -1 all the same, which made the result annoying.

import re
from typing import NamedTuple

class Version(NamedTuple):
  """A triplet (Major, Minor, Build) of strings.
  Note that every element can have the None value.
  """
  Major: str
  Minor: str = '-1'
  Build: str = '-1'

  def formatVersion(self) ->str:
    """Return a string "<Major>.<Minor>.<Build>".\n
    Note that this function returns None if Version is None.
    """
    if (self.Major is None):
      return None
    else:
      ls = [self.Major]
    if (self.Minor != None) and (self.Minor != '-1'):
      ls.append(self.Minor)
    if (self.Build != None) and (self.Build != '-1'):
      ls.append(self.Build)
    return '.'.join(ls)

  def __eq__(self,other) -> bool:
    if(not isinstance(other,Version)):
      raise TypeError("Version data cannot compare to other type data.")
    
    if(self.Major != other.Major):
      return False
    elif(not self.Major):
      return True
    elif(self.Minor != other.Minor):
      if (self.Minor != None) and (self.Minor != '0') and (self.Minor != '-1'):
        return False
      elif (other.Minor != None) and (other.Minor != '0') and (other.Minor != '-1'):
        return False
    elif(self.Build != other.Build):
      if (self.Build != None) and (self.Build != '0') and (self.Build != '-1'):
        return False
      elif (other.Build != None) and (other.Build != '0') and (other.Build != '-1'):
        return False
    else:
      return True
  def __le__(self,other) -> bool:
    if(not isinstance(other,Version)):
      raise TypeError("Version data cannot compare to other type data.")
    
    if(self.Major.isdecimal()) and (other.Major.isdecimal()):
      if(int(self.Major) < int(other.Major)):
        return True
      else:
        pass
    elif(not self.Major.isdecimal()) and (not other.Major.isdecimal()):
      a, b = self.Major, other.Major
      mslf = re.search(r'^\d*',a)
      if mslf:
        anum, atxt = a[:mslf.end()], a[mslf.end():]
      else:
        anum, atxt = None, a

      moth = re.search(r'^\d*',b)
      if moth:
        bnum, btxt = b[:moth.end()], b[moth.end():]
      else:
        bnum, btxt = None, b

      if(int(anum) < int(bnum)):
        return True
      elif(int(anum)==int(bnum)):
        if(atxt < btxt):
          return True
        elif(atxt == btxt):
          pass
        else:
          return False
      else:
        return False
    else:
      raise ValueError("two Version data are not compareable.")
    
    if(self.Minor.isdecimal()) and (other.Minor.isdecimal()):
      if(int(self.Minor) < int(other.Minor)):
        return True
      else:
        pass
    elif(not self.Minor.isdecimal()) and (not other.Minor.isdecimal()):
      a, b = self.Minor, other.Minor
      mslf = re.search(r'^\d*',a)
      if mslf:
        anum, atxt = a[:mslf.end()], a[mslf.end():]
      else:
        anum, atxt = None, a

      moth = re.search(r'^\d*',b)
      if moth:
        bnum, btxt = b[:moth.end()], b[moth.end():]
      else:
        bnum, btxt = None, b

      if(int(anum) < int(bnum)):
        return True
      elif(int(anum)==int(bnum)):
        if(atxt < btxt):
          return True
        elif(atxt == btxt):
          pass
        else:
          return False
      else:
        return False
    else:
      raise ValueError("two Version data are not compareable.")

    if(self.Build.isdecimal()) and (other.Build.isdecimal()):
      if(int(self.Build) < int(other.Build)):
        return True
      else:
        return False
    elif(not self.Build.isdecimal()) and (other.Build.isdecimal()):
      a, b = self.Build, other.Build
      mslf = re.search(r'^\d*',a)
      if mslf:
        anum, atxt = a[:mslf.end()], a[mslf.end():]
      else:
        anum, atxt = None, a

      moth = re.search(r'^\d*',b)
      if moth:
        bnum, btxt = b[:moth.end()], b[moth.end():]
      else:
        bnum, btxt = None, b

      if(int(anum) < int(bnum)):
        return True
      elif(int(anum)==int(bnum)):
        if(atxt < btxt):
          return True
        else:
          return False
      else:
        return False
    else:
      raise ValueError("two Version data are not compareable.")

I also created a function that parses a string into a Version type.

def parseVersion(s) ->Version:
  """input: a string or a list of strings
  
  Parse a string like "v1.0.4"
  """
  if(isinstance(s,bytes)):
    s = s.decode()
  if(isinstance(s,str)):
    match = re.search(r'\d*\.\d*\.?',s)
    if match:
      token = s[match.start():].split('.',2)
      if (len(token)==3):
        return Version(token[0],token[1],token[2])
      elif (len(token)==2):
        return Version(token[0],token[1],None)
      else:
        return Version(token[0],None,None)
    else:
      match = re.search(r'[0-9]*',s)
      if match:
        return Version(s[match.start():],None,None)
      else:
        raise ValueError("function parseVersion didn't parse argument.")
  elif(isinstance(s,list)):
    for x in s[0:3]:
      if(not isinstance(x,str)):
        raise TypeError("function parseVersion(s) takes a string or a list of strings as the argument.")
    try:
      (major,minor,build) = s[0:3]
    except ValueError:
      raise
    except:
      print("Unexpected error in function 'parseVersion'")
      raise
    return Version(major,minor,build)
  else:
    raise TypeError("function parseVersion(s) takes a string or a list of strings as the argument.")

Get the latest version

Hit the Github API to get information on the latest release. ``

import ssl, urllib.request
import json

head_accept = "application/vnd.github.v3+json"
host = "api.github.com"
key_release = "repos/PowerShell/PowerShell/releases/latest"


print("Latest version of pwsh ... ", end='', flush=True)

context = ssl.create_default_context()
url = ''.join(["https://",host,"/",key_release])
try:
  q = urllib.request.Request(url,headers={'accept':head_accept},method='GET')
  with urllib.request.urlopen(q, context=context) as res:
    content = json.load(res)
except urllib.error.HTTPError as err:
  print(err.code)
except urllib.error.URLError as err:
  print(err.reason)
except json.JSONDecodeError as err:
  print(err.msg)

v_latest = parseVersion(content['tag_name'])
print(v.formatVersion())

Get local version

Get version information with pwsh -v. Since time is imported only for weight processing, nothing can be said when it is said that it is not actually needed.

import time
import subprocess

print("Current version of pwsh ... ", end='', flush=True)
time.sleep(0.5)

cpl_pwsh = subprocess.run("pwsh -v",capture_output=True)
v_local = parseVersion(cpl_pwsh.stdout.strip())
print(v_local.formatVersion())

Version comparison and latest version installation

I haven't done much elaboration.

import ssl, urllib.request
import json

directory_download = R"path\to\directoryof\installer"

if(v_local < v_latest):
  print("Later version available.")
  print("Please wait...")

  aslist = content['assets']
  vlatest = attr_latest.getstr_version()
  targetname = '-'.join(["PowerShell",vlatest,"win-x64.msi"])
  targeturl = None
  for asset in aslist:
    if(asset['name'] == targetname):
      targeturl = asset['browser_download_url']
      break
  if targeturl:
    try:
      print("Downloading installer... ",end='',flush=True)
      with urllib.request.urlopen(targeturl,context=context) as res:
        dat_pack = res.read()
    except urllib.error.HTTPError as err:
      print(err.code)
    except urllib.error.URLError as err:
      print(err.reason)
    except json.JSONDecodeError as err:
      print(err.msg)
    
    try:
      path = '\\'.join([directory_download,targetname])
      f_installer = open(path,mode='xb')
      f_installer.write(dat_pack)
      f_installer.close()
    except OSError:
      print()
      raise
    except:
      print("Unexpected error occurred.")
      raise

    subprocess.run(path, stderr=subprocess.STDOUT)
  else:
    raise Exception("lost download url.")
elif(attr_pwsh.version == attr_latest.version):
  print("Your pwsh is latest version.\n")
else:
  raise Exception("unidentified exception occurred.")

Execution screen

I added a little decoration at the beginning and the end, so when I run it, it looks like this. Of course, it's the latest version now, so I'll leave it until the next pwsh update to test whether the update can be done properly.

================================================
Powershell updater version 0.5.201114
  by Lat.S (@merliborn)

Latest version of pwsh ... 7.1.0
Current version of pwsh ... 7.1.0
Your pwsh is latest version.

Push return key to quit:
================================================

Impressions

I'm glad that I could use HTTPS, hit the API to get information, make something with python in the first place, and do everything I wanted to do for a long time. The fact that type annotations can be written in expressions because they are decorations was a shock to me as a person who usually lives a typed life.

Recommended Posts

[python] Script that (should) update pwsh
Python update (2.6-> 2.7)
Script python file
python script skeleton
Python script profiling
Import python script
"Python Kit" that calls a Python script from Swift
python memorandum (sequential update)
python chromedriver automatic update
ubuntu package update script
What's in that variable (when running a Python script)
DynamoDB Script Memo (Python)
The attitude that programmers should have (The Zen of Python)
A Python script that saves a clipboard (GTK) image to a file.
Creating a Python script that supports the e-Stat API (ver.2)
A Python script that compares the contents of two directories
POST json with Python3 script
How to update Python Tkinter to 8.6
Bitcoin price monitor python script
Automatic update of Python module
Run illustrator script from python
Packages that should be included
Update python on Mac to 3.7-> 3.8
[Python beginner] Update pip itself
Note that it supports Python 3
[Python] [Long-term] 100 articles that Qiita should read now [Automatically updated weekly]
33 strings that should not be used as variable names in python
A python script that deletes ._DS_Store and ._ * files created on Mac