[python] Script qui (devrait) mettre à jour pwsh

Motivation

La mise à jour de PowerShell sous Windows est un processus manuel, mais il est devenu ennuyeux. Semi-automatisé.

Constitution

Si vous exécutez pshupdate.py, qui est le corps principal du script, en python, il vérifiera automatiquement la version et la mettra à jour si nécessaire (prévu).

Contenu

Je ne fais pas cela de manière élaborée parce que je l'ai simplement écrit selon ce que je devrais faire, mais je vais l'écrire.

Stockage des données de version

Puisque la version est une chaîne de caractères de la forme "v3.2.1", le reste après avoir supprimé seulement le premier caractère supplémentaire est transformé en un triplet de chaînes de caractères pour en faire un type Version. Si vous faites (Get-Host) .Version | Format-List * avec pwsh, vous obtiendrez 6 clés, mais je n'ai pas utilisé les dernières 3 et 3 suffisent! Yoshi! Une comparaison grande et petite est mise en œuvre dans la seconde moitié. Pour une certaine flexibilité, j'ai décidé de traiter None, 0 et -1 comme les valeurs de Minor et Build, ce qui a rendu le résultat ennuyeux.

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.")

J'ai également créé une fonction qui analyse une chaîne dans un type Version.

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.")

Obtenez la dernière version

Accédez à l'API Github pour obtenir des informations sur la dernière version. ''

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())

Obtenir la version locale

Obtenez les informations de version avec pwsh -v. Puisque le «temps» n'est importé que pour le traitement du poids, rien ne peut être dit quand on dit qu'il n'est pas réellement nécessaire.

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())

Comparaison des versions et installation de la dernière version

Je n'ai pas fait beaucoup d'élaboration.

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.")

Écran d'exécution

J'ai ajouté un peu de décoration au début et à la fin, donc quand je le lance, ça ressemble à ça. Bien sûr, c'est la dernière version maintenant, donc je vais la laisser jusqu'à la prochaine mise à jour de pwsh pour tester si la mise à jour peut être effectuée correctement.

================================================
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

Je suis content de pouvoir faire tout ce que je voulais faire depuis longtemps, comme utiliser HTTPS, accéder à l'API pour obtenir des informations et créer quelque chose avec python en premier lieu. Le fait que les annotations de type puissent être écrites dans des expressions à cause des décorations était très choquant pour moi en tant que personne qui vit habituellement une vie typée.

Recommended Posts

[python] Script qui (devrait) mettre à jour pwsh
Mise à jour Python (2.6-> 2.7)
Fichier python de script
squelette de script python
Profilage de script Python
Importer un script python
"Kit Python" qui appelle des scripts Python depuis Swift
mémorandum python (mise à jour séquentielle)
script de mise à jour du package ubuntu
Que contient cette variable (lorsque le script Python est en cours d'exécution)
Mémo de script DynamoDB (Python)
L'attitude que les programmeurs devraient avoir (Le Zen de Python)
Un script Python qui enregistre une image de presse-papiers (GTK) dans un fichier.
Création d'un script Python prenant en charge l'API e-Stat (ver.2)
Script Python qui compare le contenu de deux répertoires
POST json avec le script Python 3
Comment mettre à jour Tkinter de Python vers la version 8.6
Mise à jour automatique du module Python
Exécutez le script illustrator à partir de python
Paquets qui devraient être inclus
Mettez à jour le python que vous aviez sur votre Mac à 3.7-> 3.8
[Python débutant] Mettre à jour pip lui-même
Notez qu'il prend en charge Python 3
[Python] [Long terme] 100 articles que Qiita devrait lire maintenant [Mise à jour automatique chaque semaine]
33 chaînes à ne pas utiliser comme noms de variables en python
Un script python qui supprime les fichiers ._DS_Store et ._ * créés sur Mac