2024年5月18日 星期六

Python 透過 click 仿 PowerShell 自動填充參數

Python 透過 Click 仿 PowerShell 自動填充參數

在 Linux 上很少看到像 PowerShell 那樣依照位置自動填充參數的功能,但透過 Click,我們可以實現大約九成的相似效果。

動態填充的意思是可以給參數設置一個順序,這個順序會自動抓取未加前綴的變數,依序自動填入。舉個例子:

function DemoFunction {
    Param(
        [Parameter(Position = 0, Mandatory)]
        [string]$Id,
        [Parameter(Position = 1, Mandatory)]
        [string]$Name
    )
    return $null
}

你可以通過以下的方式顛倒指定順序:

DemoFunction "CHG" -Id 777

這是一個非常方便的功能。接下來,我們看看如何在 Python 中實現類似的功能。


Python 自動參數填充

我們使用 Click,這是 Python 的一個第三方模組。
官方網站有詳細的文檔:Click Documentation (8.1.x)

以下示例分成兩個檔案:command1.py 和 setup.py

先來看 setup.py,這是 Python 內建的功能,用來啟動代碼。

from setuptools import setup, find_packages

setup(
    name='myapp',
    version='0.1.0',
    # packages=find_packages(),
    py_modules=['command1'],
    install_requires=[
        'click',
    ],
    entry_points={
        'console_scripts': [
            'myappcmd1=command1:pycli1',
        ],
    }
)


接下來是 command1.py

import click

# 自動填入剩餘參數的函數閉包
def auto_fill_argument(required=False):
    def callback(ctx, param, value):
        if 'args' not in ctx.params:
            ctx.params['args'] = ()
        args = list(ctx.params['args'])
        if value is None:
            if args:
                value = args.pop(0)
                ctx.params['args'] = tuple(args)
            elif required:
                raise click.MissingParameter(param_hint=[f"--{param.name}", f"-{param.opts[1]}"])
        ctx.params['args'] = tuple(args)
        return value
    return callback

# 自定義命令類以實現多餘參數檢查
class AddWithExtraArgsCheck(click.Command):
    def invoke(self, ctx):
        if 'args' in ctx.params and ctx.params['args']:
            params_info = ", ".join(f"{param}: {value if value is not None else ''}" for param, value in ctx.params.items() if param != 'args')
            extra_args = " ".join(ctx.params['args'])
            raise click.UsageError(
                f"Got unexpected extra arguments ({extra_args})"
                f", already assigned ({params_info})",
                ctx=ctx
            )
        return super().invoke(ctx)

# 宣告選項群
@click.group()
def pycli1():
    pass

# 選項命令1:: upload
@pycli1.command(cls=AddWithExtraArgsCheck)
@click.option('--identity', '-id', type=str, callback=auto_fill_argument(required=True), help="指定雲端文件夾的 ID")
@click.option('--name', '-n', type=str, callback=auto_fill_argument(required=True), help="指定文件路徑")
@click.option('--type', '-t', type=click.Choice(['any', 'folder', 'file'], case_sensitive=False), default='any', help="指定路徑類型: any (預設), folder, file")
@click.argument('args', nargs=-1)
def upload(identity, name, type, args):
    """上傳文件到指定的雲端文件夾"""
    click.echo(f"雲端文件夾ID:{identity}, 正在上傳文件:{name}, 路徑類型:{type}", err=False)

# 選項命令2:: getmyinfo
@pycli1.command()
def getmyinfo():
    """獲取我的信息"""
    click.echo(f"執行 getmyinfo 動作,目前沒有任何參數", err=False)

if __name__ == '__main__':
    pycli1()


使用範例:

myappcmd1 upload "C:\path\to\your\file.txt" -id 12345

如此一來,就可以實現自動參數填充的功能。


通過指定 callback=auto_fill_argument(required=True) 可以將參數標記為自動填入項目,並且可以進一步標記為必須項目。該參數可以為空,但至少要保留括號。

此外,通過在選項命令處指定 cls=AddWithExtraArgsCheck,可以檢查是否有多餘的參數。這樣做的原因是,啟用 args 後會關閉原生的參數檢查功能,但如果不啟用 args,又無法接收任意參數,所以只能這樣妥協使用。

市集中的 python 版本被優先使用,還無法從環境變數中刪除找不到該位置

市集中的 python 版本被優先使用,還無法從環境變數中刪除找不到該位置

Windows中有個隱藏的環境變數,叫做應用程式別名,從市集安裝的似乎預設會從那裏設定,不知道的話真的很頭痛被搞過一次。

位置可以從設定裡面點出來



找到那個元兇關掉就好



Windows11上也大同小異,位置就別找了直接搜尋 "應用程式執行別名" 就好



-


2024年5月17日 星期五

下載 Bats 並安裝到用戶的暫存目錄 (以免安裝的方式啟動)

下載 Bats 並安裝到用戶的暫存目錄 (以免安裝的方式啟動)

完整的流程,從下載Bats到安裝到用戶的暫存目錄,並臨時設置環境變數來使用它。


1. 下載Bats

首先,從GitHub下載Bats v1.11.0的源碼壓縮包:

wget https://github.com/bats-core/bats-core/archive/refs/tags/v1.11.0.tar.gz -O bats-core-1.11.0.tar.gz

解壓縮下載的檔案:

tar -xzf bats-core-1.11.0.tar.gz

進入解壓縮的目錄:

cd bats-core-1.11.0


2. 安裝腳本

原作寫的安裝腳本 bats-core/install.sh 下面是腳本的解析並追加了提示如何添加到環境變數的信息,你也可以直接執行原裝的就好沒有改動。


在解壓縮的目錄中,創建一個名為 install_bats.sh 的文件,並將以下內容粘貼到文件中:-

#!/usr/bin/env bash
set -e

BATS_ROOT="${0%/*}" # 腳本所在目錄的路徑
PREFIX="${1%/}"     # 安裝前綴路徑 (移除路徑結尾的斜線)
LIBDIR="${2:-lib}"  # 庫文件目錄名,默認為 "lib"

# 如果沒有提供安裝路徑,則輸出使用說明到標準錯誤。
if [[ -z "$PREFIX" ]]; then
  printf '%s\n' \
    "usage: $0 <prefix> [base_libdir]" \
    "  e.g. $0 /usr/local" \
    "       $0 /usr/local lib64" >&2
  exit 1
fi

# 使用 install 命令來創建目錄結構並設置適當的權限。
install -d -m 755 "$PREFIX"/{bin,libexec/bats-core,"${LIBDIR}"/bats-core,share/man/man{1,7}}

# 安裝二進制文件、庫和手冊頁。
install -m 755 "$BATS_ROOT/bin"/* "$PREFIX/bin"
install -m 755 "$BATS_ROOT/libexec/bats-core"/* "$PREFIX/libexec/bats-core"
install -m 755 "$BATS_ROOT/lib/bats-core"/* "$PREFIX/${LIBDIR}/bats-core"
install -m 644 "$BATS_ROOT/man/bats.1" "$PREFIX/share/man/man1"
install -m 644 "$BATS_ROOT/man/bats.7" "$PREFIX/share/man/man7"

# 讀取安裝後的 bats 執行文件,修改庫目錄變量後重新寫入。
read -rd '' BATS_EXE_CONTENTS <"$PREFIX/bin/bats" || true
BATS_EXE_CONTENTS=${BATS_EXE_CONTENTS/"BATS_BASE_LIBDIR=lib"/"BATS_BASE_LIBDIR=${LIBDIR}"}
printf "%s" "$BATS_EXE_CONTENTS" > "$PREFIX/bin/bats"

# 輸出安裝完成的訊息
echo "Installed Bats to $PREFIX/bin/bats"

# 提示用戶如何添加 BATS 到環境變數
echo "To temporarily add Bats to your PATH, run:"
echo "export PATH=\"\$PATH:$PREFIX/bin\""


給安裝腳本賦予執行權限:

chmod +x install_bats.sh

執行安裝腳本,將Bats安裝到 ~/tmp/bats 目錄中:

./install_bats.sh ~/tmp/bats


3. 臨時設置環境變數

按照腳本的提示,設置環境變數以臨時使用Bats:

export PATH="~/tmp/bats/bin:$PATH"

這裡的路徑記得要根據安裝的位置輸入,然後這只是臨時的關閉終端機就沒有了


4. 驗證安裝

確認Bats已成功安裝並可用:

bats --version


總結

以上步驟將Bats安裝到用戶的暫存目錄 /tmp/bats,並通過臨時設置環境變數使其可用。這樣可以在不影響系統的情況下使用Bats進行測試。

2024年5月15日 星期三

PowerShell 複製到剪貼簿歷史中

PowerShell 複製到剪貼簿歷史中

預設的 Set-Clipboard 不會顯示在剪貼簿中,這個是他的擴充函式
直接上代碼,自行取用

function Set-ClipboardHistory {
    param(
        [Parameter(Mandatory=$true)]
        [string]$Value,

        [Parameter()] # 記錄在 Windows 的剪貼板歷史
        [switch]$DenyInHistory,

        [Parameter()] # 在用戶的多個設備之間漫遊
        [switch]$Roamable
    )

    # 加載 Windows Runtime 類型
    Add-Type -AssemblyName 'System.Runtime.WindowsRuntime'

    # 創建 DataPackage 和 ClipboardContentOptions 對象
    $dataPackage = New-Object Windows.ApplicationModel.DataTransfer.DataPackage
    $cOptions = New-Object Windows.ApplicationModel.DataTransfer.ClipboardContentOptions

    # 根據參數設定剪貼板內容選項
    $cOptions.IsAllowedInHistory = -not $DenyInHistory
    $cOptions.IsRoamable = $Roamable

    # 設定剪貼板操作為複製
    [int]$RequestedOperationCopy = 1
    $dataPackage.RequestedOperation = $RequestedOperationCopy

    # 使用選項將內容設置到剪貼板
    $dataPackage.SetText($Value)
    [Windows.ApplicationModel.DataTransfer.Clipboard]::SetContentWithOptions($dataPackage, $cOptions) | Out-Null

} # Set-ClipboardHistory "TestString"

-

確保 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絕對不受環境影響的。