多年前學會用 PowerShell 設定 IIS 網站,便逐步捨棄人工,把原本要開管理介面點滑鼠的部署修改操作都寫成 PowerShell 指令檔,需要部署或修改網站時,執行 .ps1,數十上百個步驟瞬間完成,一次要處理十幾台機器也不是問題,也不必擔心操作人員疲勞駕駛,眼花手滑出錯。體驗過 CLI 的美妙,就很難回頭開 GUI 操作過茹毛飲血的生活。

實務作業中部分 Web.config 採個別修改調整而非整檔覆寫,是我手邊少數仍依賴人工開 Notepad 操作的環節,今天就來征服它,拓大 PowerShell 的版圖。

之前玩過用 PowerShell 處理 XML,知道 PowerShell 可以直接用 $doc.nodeName.nodeName 存取節點也支援 XPath,寫起來蠻簡潔也夠彈性。這篇則偏應用實務,著重增刪改 Web.config 內容的小技巧。

以下將用這個 Web.config 簡單示範如何新增、修改、刪除 appSetting,以及透過 DOM 及 XPath 更改 bindingRedirect。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <appSettings>
    <add key="webpages:Version" value="3.0.0.0" />
    <add key="webpages:Enabled" value="false" />
    <add key="ClientValidationEnabled" value="true" />
    <add key="UnobtrusiveJavaScriptEnabled" value="true" />
  </appSettings>
  <system.web>
    <compilation debug="true" targetFramework="4.7.2" />
    <httpRuntime targetFramework="4.7.2" />
  </system.web>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>

不囉嗦,直接看 Code。

param (
    [Parameter(Mandatory = $false)]
    [string]$webConfigPath = "Web.config"
)
$ErrorActionPreference = "Stop"

# 讀入 XML
[xml]$xml = Get-Content -Path $webConfigPath

# 透過 DOM 存取
$node = $xml.configuration.appSettings
$check = $node.add | Where-Object { $_.key -eq "ClientValidationEnabled" }
Write-Host "[DOM] ClientValidationEnabled: $($check.value)"

# 修改值
$check.value = "false"
$xml.Save("Rev1.xml")

# 新增 appSetting
$newSetting = $xml.CreateElement("add")
$newSetting.SetAttribute("key", "EnableSSO")
$newSetting.SetAttribute("value", "true")
$xml.configuration.appSettings.AppendChild($newSetting) | Out-Null
$xml.Save("Rev2.xml")

# 透過 XPath 讀取
$check = $xml.SelectSingleNode("configuration/appSettings/add[@key='EnableSSO']")
Write-Host "[XPath] EnableSSO: $($check.value)"

# 刪除 appSetting
$node = $xml.configuration.appSettings.add | Where-Object { $_.key -eq "EnableSSO" }
if ($node) {
    $node.ParentNode.RemoveChild($node) | Out-Null
}
$xml.Save("Rev3.xml")

# DOM 存取 assemblyBinding 改 bindingRedirect 版本
$node = $xml.configuration.runtime.assemblyBinding
$depAsm = $node.dependentAssembly | Where-Object { $_.assemblyIdentity.name -eq "Newtonsoft.Json" }
if ($depAsm) {
    $depAsm.bindingRedirect.oldVersion = "0.0.0.0-12.0.0.0"
    $depAsm.bindingRedirect.newVersion = "12.0.0.0"
}
$xml.Save("Rev4.xml")

# XPath 存取 assemblyBinding 需注意命名空間
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$nsMgr.AddNamespace("asm", "urn:schemas-microsoft-com:asm.v1")
$node = $xml.SelectSingleNode("configuration/runtime/asm:assemblyBinding/asm:dependentAssembly/asm:assemblyIdentity[@name='Newtonsoft.Json']", $nsMgr)
if ($node) {
    $bindingRedirect = $node.ParentNode.bindingRedirect
    $bindingRedirect.oldVersion = "0.0.0.0-13.0.0.0"
    $bindingRedirect.newVersion = "13.0.0.0"
}
$xml.Save("Rev5.xml")

# 新增 ODP.NET BindingRedirect
$asmBind = $xml.configuration.runtime.assemblyBinding
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$nsMgr.AddNamespace("asm", "urn:schemas-microsoft-com:asm.v1")
$ns = $nsMgr.LookupNamespace("asm")
$depAsm = $xml.CreateElement("dependentAssembly", $ns)
$asmId = $xml.CreateElement("assemblyIdentity", $ns)
$asmId.SetAttribute("name", "Oracle.DataAccess")
$asmId.SetAttribute("publicKeyToken", "89b483f429c47342")
$asmId.SetAttribute("culture", "neutral")
$bindingRedirect = $xml.CreateElement("bindingRedirect", $ns)
$bindingRedirect.SetAttribute("oldVersion", "0.0.0.0-12.1.0.0")
$bindingRedirect.SetAttribute("newVersion", "12.1.0.0")
$depAsm.AppendChild($asmId) | Out-Null
$depAsm.AppendChild($bindingRedirect) | Out-Null
$asmBind.AppendChild($depAsm) | Out-Null
$xml.Save("Rev6.xml")

Write-Host "修改 ClientValidationEnabled" -ForegroundColor Yellow
git.exe diff --no-index -U1 Web.config Rev1.xml
Write-Host "新增 EnableSSO" -ForegroundColor Yellow
git.exe diff --no-index -U1 Rev1.xml Rev2.xml
Write-Host "刪除 EnableSSO" -ForegroundColor Yellow
git.exe diff --no-index -U1 Rev2.xml Rev3.xml
Write-Host "修改 Newtonsoft.Json 版本 12.0.0.0" -ForegroundColor Yellow
git.exe diff --no-index -U1 Rev3.xml Rev4.xml
Write-Host "修改 Newtonsoft.Json 版本 13.0.0.0" -ForegroundColor Yellow
git.exe diff --no-index -U1 Rev4.xml Rev5.xml
Write-Host "新增 Oracle.DataAccess 版本導向" -ForegroundColor Yellow
git.exe diff --no-index -U1 Rev5.xml Rev6.xml

程式不複雜,唯一的眉角是 <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> 有自己的 XML Namespace,XPath 查詢跟 CreateElement 時要額外指定。這一刻,老人想起了當年被 XML 支配的恐懼。(補聲幹)

為驗證修改結果,每次更改都會存檔,再借用 git diff 顯示修改前後的差異( git 真的好好用),實測結如下:

掌握這些技巧,未來要用 PowerShell 修改 Web.config 應該都是小菜一碟囉~

Using PowerShell to automate IIS website configuration and Web.config modifications enhances efficiency by replacing manual GUI actions with scripted commands. This blog explores XML manipulation using PowerShell, including adding, modifying, and deleting nodes in Web.config, and handling XML namespaces for XPath queries.


Comments

# by yoyo

能把Web.config修改自動化,對於導入CI/CD是很重要的一步 XPath在較簡單的XML文件下可用工具直接選擇到,對開發較方便 用DOM比用XPath的code好讀很多,我還是用DOM寫 原來選到特定Attribute可用這寫法 Where-Object { $_.assemblyIdentity.name -eq "Newtonsoft.Json" } 之前我都是直接寫死第n個XD

# by 不囉嗦直接看 Code

import xml.etree.ElementTree as ET import os import shutil import subprocess from datetime import datetime # ===== 設定參數 ===== WEB_CONFIG_PATH = "Web.config" BACKUP_FILE = f"Backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.config" REVISIONS = ["Rev1", "Rev2", "Rev3", "Rev4", "Rev5", "Rev6"] NAMESPACE_ASM_V1 = "{urn:schemas-microsoft-com:asm.v1}" # ===== 函式庫 ===== def setup_xml(): """載入 XML 檔案並註冊命名空間""" tree = ET.parse(WEB_CONFIG_PATH) root = tree.getroot() # 註冊命名空間以正確輸出 xmlns ET.register_namespace('', 'urn:schemas-microsoft-com:asm.v1') return tree, root def save_revision(tree, rev_name): """儲存當前 XML 至階段性檔案""" filename = f"{rev_name}.xml" tree.write(filename, encoding="utf-8", xml_declaration=True) print(f" {filename} 已建立") return filename def run_git_diff(before, after, output_file): """執行 Git diff 並儲存結果""" try: result = subprocess.run( ["git", "diff", "-U1", before, after], capture_output=True, text=True, check=True ) with open(output_file, "w", encoding="utf-8") as f: f.write(result.stdout) print(f" Git diff 已儲存至 {output_file}") except Exception as e: print(f" Git 差異比較失敗: {e}") def modify_app_setting(root, key, value=None): """修改或新增 appSettings 中的項目""" app_settings = root.find("appSettings") for add in app_settings.findall("add"): if add.get("key") == key: if value is None: app_settings.remove(add) # 刪除 else: add.set("value", value) # 修改 break else: if value is not None: new_add = ET.Element("add", {"key": key, "value": value}) app_settings.append(new_add) # 新增 def update_binding_redirect(root, old_new_version): """更新 assemblyBinding 的 bindingRedirect""" asm_v1 = NAMESPACE_ASM_V1 binding_redirects = root.findall(f".//{asm_v1}dependentAssembly/{asm_v1}bindingRedirect") for br in binding_redirects: if br.get("oldVersion").startswith("Newtonsoft.Json"): br.set("newVersion", old_new_version) def add_oracle_data_access_binding(root): """新增 Oracle.DataAccess 的 bindingRedirect""" asm_v1 = NAMESPACE_ASM_V1 dependent_assembly = ET.Element(f"{asm_v1}dependentAssembly") assembly_identity = ET.Element( f"{asm_v1}assemblyIdentity", { "name": "Oracle.DataAccess", "publicKeyToken": "89b483f429c47342", "culture": "neutral" } ) binding_redirect = ET.Element( f"{asm_v1}bindingRedirect", { "oldVersion": "2.111.7.0-2.121.1.0", "newVersion": "2.121.1.0" } ) dependent_assembly.append(assembly_identity) dependent_assembly.append(binding_redirect) root.find(f".//{asm_v1}assemblyBinding").append(dependent_assembly) # ===== 主程式 ===== if __name__ == "__main__": # Step 0: 備份原始檔 shutil.copy(WEB_CONFIG_PATH, BACKUP_FILE) print(f"原始檔已備份為 {BACKUP_FILE}") # Step 1: 讀取 XML tree, root = setup_xml() # Step 2~7: 逐步修改 revisions = [] prev_file = WEB_CONFIG_PATH # Step 2: 修改 ClientValidationEnabled modify_app_setting(root, "ClientValidationEnabled", "false") rev1 = save_revision(tree, REVISIONS[0]) run_git_diff(prev_file, rev1, f"{REVISIONS[0]}_diff.txt") prev_file = rev1 revisions.append(rev1) # Step 3: 新增 EnableSSO modify_app_setting(root, "EnableSSO", "true") rev2 = save_revision(tree, REVISIONS[1]) run_git_diff(prev_file, rev2, f"{REVISIONS[1]}_diff.txt") prev_file = rev2 revisions.append(rev2) # Step 4: 刪除 EnableSSO modify_app_setting(root, "EnableSSO") rev3 = save_revision(tree, REVISIONS[2]) run_git_diff(prev_file, rev3, f"{REVISIONS[2]}_diff.txt") prev_file = rev3 revisions.append(rev3) # Step 5: 更新 Newtonsoft.Json 到 12.0.0.0 update_binding_redirect(root, "12.0.0.0") rev4 = save_revision(tree, REVISIONS[3]) run_git_diff(prev_file, rev4, f"{REVISIONS[3]}_diff.txt") prev_file = rev4 revisions.append(rev4) # Step 6: 更新到 13.0.0.0 update_binding_redirect(root, "13.0.0.0") rev5 = save_revision(tree, REVISIONS[4]) run_git_diff(prev_file, rev5, f"{REVISIONS[4]}_diff.txt") prev_file = rev5 revisions.append(rev5) # Step 7: 新增 Oracle.DataAccess add_oracle_data_access_binding(root) rev6 = save_revision(tree, REVISIONS[5]) run_git_diff(prev_file, rev6, f"{REVISIONS[5]}_diff.txt") print("所有階段性修改已完成!")

Post a comment