2024年5月15日 星期三

確保 PowerShell Function 總是返回數組(即便對象是單一物件)

確保 PowerShell Function 總是返回數組(即便對象是單一物件)

這個問題是來自於 PowerShell 在經過函式的時候自動將單一一個的數組把數組拆掉變成一個物件的特性導致的。

要怎麼應付這個自動拆解有兩個方法



同一層級使用 @() 包覆

如果是在同一層級只要使用 @() 打包起來就好了,除了過函數會拆數組的特性之外還有一個是在任何時候總是會拆掉數組中的數組,就是不會讓你包兩層數組的意思,除非這兩層的Count都大於1,不然就會被拆掉只剩一層

利用這個特性就管他是不是數組,總之只要包起來就對了

$array = @((Get-Array))


不用寫判斷式也可以保證他是正確的,如果要寫判斷式的話會長這個樣子

$array = if ((Get-Array) -isnot [array]) { @((Get-Array)) }




函式的返回

函式返回就麻煩了一共套了兩層規則,一個是雙層數組拆解跟返回時拆解所以兩層都要解


對於返回值而言只能用逗號傳遞數組

function MyFunc() {
  return ,$array
}


這個第一次看應該覺得很問號,我給你講個猜想應該就可以馬上記住了。就把他當作加一個空物件組成 Count 大於二的數組 @($null, $array),然後出去的時候那個空物件會被解掉。

雖然不確定實作是不是這樣做的,但總之就是對於函式返回值只有這樣才能解,你用第一種出去還是變成物件。


第二個問題是萬一你那個 $array 不是數組的話逗號的保護模被函式搓破了出去還是變成物件了,這個就把兩層保護組合起來就好了

function MyFunc() {
  return ,@((Get-Array))
}


這樣一來就可以不寫判斷式的保證返回值一定是個數組了




PowerShell 獲取剪貼簿歷史紀錄 (包含pwsh)

PowerShell 獲取剪貼簿歷史紀錄

參考自這篇文章並做了一些改良
Enumerating Windows clipboard history in PowerShell - The Old New Thing (microsoft.com)

由於載入的物件僅有 PowerShell 能使用,這導致在 Pwsh 中無法使用,一個簡單的解法就是直接呼叫 PowerShell 來做事就好了

這邊附上完整的函式

function Get-ClipboardHistory {

    # 定義在 Windows PowerShell 中執行的腳本
    $scriptBlock = {
        # 導入 Windows Runtime 支持
        Add-Type -AssemblyName System.Runtime.WindowsRuntime
        # 尋找 AsTask 泛型方法,用於處理異步操作
        $asTaskGeneric = ([System.WindowsRuntimeSystemExtensions].GetMethods() | Where-Object {
            $_.Name -eq 'AsTask' -and
            $_.GetParameters().Count -eq 1 -and
            $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1'
        })[0]

        # 定義 Await 函數,用於等待 Windows Runtime 的異步操作結果
        function Await($WinRtTask, $ResultType) {
            $asTask = $asTaskGeneric.MakeGenericMethod($ResultType)
            $netTask = $asTask.Invoke($null, @($WinRtTask))
            $netTask.Wait(-1) | Out-Null
            $netTask.Result
        }

        # 加載剪貼板相關的 Windows Runtime 類型
        [Windows.ApplicationModel.DataTransfer.Clipboard, Windows.ApplicationModel.DataTransfer, ContentType=WindowsRuntime] | Out-Null
        # 獲取剪貼板歷史項目的異步操作
        $op = [Windows.ApplicationModel.DataTransfer.Clipboard]::GetHistoryItemsAsync()
        $result = Await ($op) ([Windows.ApplicationModel.DataTransfer.ClipboardHistoryItemsResult])

        # 獲取每個歷史項目的文本內容
        $textops = $result.Items.Content.GetTextAsync()

        # 迭代處理每個歷史項目的文本
        $textObjects = @()
        foreach ($textop in $textops) {
            $textObjects += Await($textop) ([String])
        }

        # 將對向轉換為 JSON 字符串
        $textObjects | ConvertTo-Json

    }

    # 使用 Base64 編碼 PowerShell 腳本,並通過 PowerShell.exe 執行
    $encodedBlock = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($scriptBlock.ToString()))
    ,@((powershell.exe -EncodedCommand $encodedBlock | ConvertFrom-Json))

} # (Get-ClipboardHistory)[0]



對於特殊符號有些顯示不出來變成問號,改成 UTF8 可以正常顯示

[console]::OutputEncoding = [Text.Encoding]::UTF8

這其實不是編碼不匹配的問題單純只是中文 BIG5 的符號沒有 UTF8 齊全而已,根據需求自己實作進去吧。



2024年5月13日 星期一

Box Python SDK 如何設置 proxy

Box Python SDK 如何設置 proxy



方法0 透過環境環境變數設置

只需要從 python 環境中設置環境變數即可

import os
os.environ['http_proxy'] = 'http://Enterprise.co.jp:8080'
os.environ['https_proxy'] = 'https://Enterprise.co.jp:8080'

我也有嘗試著從環境變數設置過無效,下面舊文是我掙扎的痕跡,可能是我這邊環境問題。

但總之最後測試的結果是,BOXSDK不用改直接追加這幾行在開頭就行了。


如果你從這邊設置無效順著我底下舊文直接改SDK絕對不受環境影響的。








如何 Excel 垂直合併儲存格中的文字到第一個

如何 Excel 垂直合併儲存格中的文字到第一個

應該經常遇到說上下文被儲存格分開了,需要合併又只能手動剪下文字,不然直接按合併第二格之後的文字會通通不見。

寫了個腳本來處理這件事件,腳本可以指定到快捷鍵加速操作,平常也不需要貼到目標 xlsx 檔案上只需要儲存在一個屬於自己的 xlsm 上面,當你需要這功能的時候打開這個檔案在背景就可以了。

快捷鍵可能每次都要在目標 xlsx 上重設,不過這幾乎不成問題就是了。



腳本

Sub CombineTextInColumns()
    Dim rng As Range
    Dim cell As Range
    Dim startCell As Range
    Dim combinedText As String
    Dim col As Long
    Dim shouldMerge As Boolean

    ' 控制是否合併儲存格的變數
    shouldMerge = False  ' 如果設為 False 則不合併儲存格,如果設為 True 則合併儲存格

    ' 確認用戶已選擇儲存格
    If Not TypeName(Selection) = "Range" Then
        MsgBox "請選擇儲存格"
        Exit Sub
    End If

    Set rng = Selection
    ' 按列處理選定範圍
    For col = 1 To rng.Columns.Count
        Set startCell = rng.Cells(1, col)
        combinedText = startCell.Value
        ' 合併每一列中的文字到第一個儲存格
        For Each cell In rng.Columns(col).Cells
            If cell.Address <> startCell.Address Then
                If combinedText <> "" And cell.Value <> "" Then
                    combinedText = combinedText & Chr(10) & cell.Value
                ElseIf cell.Value <> "" Then
                    combinedText = cell.Value
                End If
            End If
        Next cell

        ' 更新第一個儲存格的內容
        startCell.Value = combinedText
        startCell.WrapText = True

        ' 清除該列中第一個儲存格以外的其他儲存格內容
        For Each cell In rng.Columns(col).Cells
            If cell.Address <> startCell.Address Then
                cell.ClearContents
            End If
        Next cell

        ' 根據條件合併儲存格
        If shouldMerge Then
            rng.Columns(col).Merge
            rng.Columns(col).VerticalAlignment = xlTop
        End If
    Next col

    ' 自動調整所有選定範圍的行高
    rng.Rows.AutoFit
End Sub






補一個日文註解版本的

Sub CombineTextInColumns()
    Dim rng As Range
    Dim cell As Range
    Dim startCell As Range
    Dim combinedText As String
    Dim col As Long
    Dim shouldMerge As Boolean

    ' セルをマージするかどうかを制御する変数
    shouldMerge = False  ' False の場合はセルをマージしない、 True の場合はセルをマージ

    ' ユーザーがセルを選択していることを確認
    If Not TypeName(Selection) = "Range" Then
        MsgBox "セルを選択してください"
        Exit Sub
    End If

    Set rng = Selection
    ' 選択範囲の各列を処理
    For col = 1 To rng.Columns.Count
        Set startCell = rng.Cells(1, col)
        combinedText = startCell.Value
        ' 各列のセルのテキストを最初のセルに結合
        For Each cell In rng.Columns(col).Cells
            If cell.Address <> startCell.Address Then
                If combinedText <> "" And cell.Value <> "" Then
                    combinedText = combinedText & Chr(10) & cell.Value
                ElseIf cell.Value <> "" Then
                    combinedText = cell.Value
                End If
            End If
        Next cell

        ' 最初のセルの内容を更新
        startCell.Value = combinedText
        startCell.WrapText = True

        ' 最初のセル以外のセルの内容をクリア
        For Each cell In rng.Columns(col).Cells
            If cell.Address <> startCell.Address Then
                cell.ClearContents
            End If
        Next cell

        ' 条件に基づいてセルをマージ
        If shouldMerge Then
            rng.Columns(col).Merge
            rng.Columns(col).VerticalAlignment = xlTop
        End If
    Next col

    ' 選択範囲のすべての行の高さを自動調整
    rng.Rows.AutoFit
End Sub



2024年5月5日 星期日

PowerShell獲取檔案權限表格

PowerShell獲取檔案權限表格

原本的物件不是很好使,寫了個函式方便快速查詢

輸出範例

Identity                         FullControl Modify ReadAndExecute Read  Write
--------                         ----------- ------ -------------- ----  -----
NT AUTHORITY\SYSTEM              Allow
NT AUTHORITY\Authenticated Users             Allow
BUILTIN\Administrators           Allow
BUILTIN\Users                                       Allow          Allow Deny




函式代碼

Get-FilePermissions

function Get-FilePermissions {
     [CmdletBinding(DefaultParameterSetName = "")]
     param(
         [Parameter(Position = 0, ParameterSetName = "", Mandatory)]
         [string]$Path,
         [Parameter(Position = 1, ParameterSetName = "")]
         [string]$Identity
     )

     # 確認指定的路徑是否存在
     if (!(Test-Path $Path)) {
         Write-Error "Cannot find path '$Path' because it does not exist." -ErrorAction Stop
     }

     # 取得指定路徑的存取控制清單 (ACL)
     $acl = Get-Acl $Path

     # 定義需要檢查的權限列表
     $permissions = @('FullControl', 'Modify', 'ReadAndExecute', 'Read', 'Write')

     # 初始化一個字典來儲存每個使用者的權限狀態
     $permissionDict = @{}

     # 遍歷存取控制規則
     foreach ($accessRule in $acl.Access) {
         # 取得使用者名稱
         $user = $accessRule.IdentityReference.Value

         # 如果字典尚未包含目前用戶,則新增一個條目
         if (-not $permissionDict.ContainsKey($user)) {
             # 動態建立權限字段
             $userPermissions = [ordered]@{ Identity = $user }
             foreach ($permission in $permissions) {
                 $userPermissions[$permission] = ''
             }
             $permissionDict[$user] = $userPermissions
         }

         # 更新權限狀態,拒絕權限優先
         foreach ($permission in $permissions) {
             if ($accessRule.FileSystemRights -like "*$permission*") {
                 $state = $accessRule.AccessControlType.ToString()
                 if ($permissionDict[$user][$permission] -ne 'Deny') {
                     $permissionDict[$user][$permission] = $state
                 }
             }
         }
     }

     # 將字典中的值轉換為物件數組
     if ($Identity) {
         $permission = [PSCustomObject]($permissionDict[$Identity])
     } else {
         $permission = $permissionDict.Values | ForEach-Object { [PSCustomObject]$_ }
     }

     # 傳回產生的權限表格
     return $permission
}

# Get-FilePermissions "Z:\test.txt"|Format-Table|Out-String
# Get-FilePermissions "Z:\test.txt" -Identity 'BUILTIN\Administrators'


使用範例

# 輸出表格
Get-FilePermissions "Z:\test.txt"|Format-Table|Out-String

# 指定特定使用者或群組名稱
Get-FilePermissions "Z:\test.txt" -Identity 'BUILTIN\Administrators'



2024年5月1日 星期三

Linux Ubuntu 自動更新腳本

Linux Ubuntu 自動更新腳本

更新要打的指令其實就那麼幾個只是經常打覺得煩了,習慣上會弄成一個 update.sh 這樣來做比較快

#!/bin/bash

# 更新軟件包列表
sudo apt update

# 升級已安裝的軟件包
sudo apt upgrade -y

# 清理不再需要的軟件包
sudo apt autoremove -y

# 清理下載的安裝包緩存
sudo apt clean

複製之後使用下面的指令打開編輯器

nano ~/update.sh


然後貼上按下 CTRL+S 儲存,再按 CTRL+X 離開
接著在更改權限讓他變成可執行檔案

chmod +x ~/update.sh


到這邊就大功告成了可以使用下面快捷更新

./update.sh



執行之後還是有可能沒更完全

  1. 保留的軟件包:某些軟件包可能因為依賴關係的問題被保留下來,不進行自動升級。
  2. 需要手動干預:有時候,軟件包需要你手動確認升級,尤其是那些可能影響系統設定的重要軟件包。


查看哪些沒有被更新

sudo apt list --upgradable


1. 強制更新

sudo apt full-upgrade

2. 移除

sudo apt remove YOUR_APP_NAME

3. 移除(連同所有設定檔)

sudo apt purge YOUR_APP_NAME




PowerShell 如何接收外部程式返回的二進制值

PowerShell 如何接收外部程式返回的二進制值

這個超坑人阿 PowerShell 預設會自動把收到的數值編碼,導致二進制數值讀進來的瞬間就因為轉換的關係資料遺失了

舉個例子來說如果要用 OpenSSL 簽名,簽名有點複雜在簡化一點產生一個隨機數值並返回二進位數值

openssl rand -out - 8

拿到的值就是爛掉的因為轉換編碼的過程就發生不可逆的資料丟失了
查了一下還好是有解的

shell - How to get original binary data from external command output in PowerShell? - Stack Overflow

只能說真愛生命不要用 PowerShell 管道傳輸非字串的東西…




完成的函式

代碼我把他弄好成一個函式了

function Invoke-CommandAndGetBinaryOutput {
    [CmdletBinding()]
    Param(
        [Parameter(Position = 0, Mandatory, ValueFromPipeline)]
        [string]$CommandLine
    )
    Process {
        # 用空白分割命令行,並分配文件名與參數
        $parts = $CommandLine -split ' ', 2
        $fileName = $parts[0]
        $arguments = $parts[1]

        # 建立 ProcessStartInfo 對象並配置
        $processInfo = New-Object System.Diagnostics.ProcessStartInfo -Property @{
            FileName = $fileName
            Arguments = $arguments
            RedirectStandardOutput = $true
            RedirectStandardError = $true
            UseShellExecute = $false
            CreateNoWindow = $true
        }

        # 開始進程
        $process = New-Object System.Diagnostics.Process
        $process.StartInfo = $processInfo
        $process.Start() | Out-Null
        $process.WaitForExit()

        # 使用 MemoryStream 讀取二進制數據
        $memoryStream = New-Object System.IO.MemoryStream
        try {
            $process.StandardOutput.BaseStream.CopyTo($memoryStream)
            $binaryOutput = $memoryStream.ToArray()

            # 返回二進制數據
            return $binaryOutput
        } finally {
            $memoryStream.Dispose()
        }
    }
} # Write-Host (Invoke-CommandAndGetBinaryOutput "openssl rand -out - 8")

使用範例就是

Write-Host (Invoke-CommandAndGetBinaryOutput "openssl rand -out - 8")

拿到的是二進位數值,這個不要隨便亂成任意編碼(UTF-8)會丟失信息的。

想要安全的攜帶大概就是走base64的路子了,轉法下面有。




驗證

就能拿到正確的數值了,不過 OpenSSL 只有簽名的時候結果才是固定的這邊有點難驗證就是了,大概寫一下可驗證的辦法

走檔案路線的不會受到影響結果一定是對的

# 調用 OpenSSL 進行簽名 (走檔案路線)
$toSignData | Set-Content $toSignDataPath -NoNewline
openssl dgst -sha512 -sign $privatekeyPath -binary -out $signaturePath $toSignDataPath
$base64Signature = [System.Web.HttpServerUtility]::UrlTokenEncode([System.IO.File]::ReadAllBytes($signaturePath)) -replace('[012]$')

然後是副程式的用法

$toSignData | Set-Content $toSignDataPath -NoNewline
$bytes = Invoke-CommandAndGetBinaryOutput "OpenSSL dgst -sha512 -binary -sign `"$privatekeyPath`" `"$toSignDataPath`""
$base64Signature = [System.Web.HttpServerUtility]::UrlTokenEncode($bytes) -replace('[012]$')

寫個大概而已自己在琢磨一下 OpenSSL 怎麼使用