2024年5月18日 星期六

Python 虛擬機用法

Python 虛擬機用法

新增虛擬機

python -m venv .venv

激活虛擬機

# Linux
source .venv/bin/activate

# Windows
.venv\Scripts\activate

這個在 Vscode 中不需要輸入只要偵測到有環境打開就自動進去了

備份環境依賴

pip freeze > requirements.txt

安裝環境依賴

pip install -r requirements.txt

如果是當前專案的話生成會變成絕對路徑,可以手動改成 -e . 這樣可以動態獲取

安裝當前專案到環境

# 發布用
pip install .

# 開發用引用當前代碼
pip install -e .

用 -e 的話不會複製代碼到環境中,而是直接引用當前的代碼,所以可以即時修改。但要是有框架改變或更名等較大的變異還是需要重新安裝一次才能跑。

退出虛擬環境

deactivate


 

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

截止制目前我覺得很扯的是,從源代碼觀察似乎不存在任何參數可以設置 Proxy ,官方可能就沒考慮到這事情

或許有什麼隱藏方法可以設置也說不定,只是我沒能從官方說明上找到,只好自己從原始代碼中下手了

最後被我找到是在 default_network.py 這份檔案中的 Session 負責的,由於看不出來怎麼設置他只好強行修改了
box-python-sdk/boxsdk/network/default_network.py#L26 (github.com)


修改的方式就直接硬上了,在你的程式中使用以下代碼動態修改函式庫
(不是直接修改函式庫代碼別搞錯)

# EnterpriseNetwork (Proxy Setting)
import requests
from boxsdk.network.default_network import DefaultNetwork
from logging import getLogger
def EnterpriseNetwork(self):
    super(DefaultNetwork, self).__init__()
    self._session = requests.Session()
    self._session.proxies.update({
        'http' : 'http://Enterprise.co.jp:8080',
        'https': 'https://Enterprise.co.jp:8080'
    })
    self._logger = getLogger(__name__)
DefaultNetwork.__init__ = EnterpriseNetwork

這樣就能做到動態修改了,覺得很醜但至少應該沒啥副作用,奏合著用吧。






這篇好像疑似有方法,未測試
Box APIとの通信をCharles Proxyを通して覗き見る方法 #BOX - Qiita


如何 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