2025/12/03

Using AI Turn My Requirement Into A Spec

LLM 最強大的就是你給予適當的內容,它可以幫你延伸很多你沒注意到的問題,最近常常把需求稍微梳理過後,加上一段 prompt 讓他整理成 spec,再將結果調整一下變可以開 redmine 的票。

widget 設定完成後,若使用者將 widget 的欄位刪除,有機率會造成計算或使用錯誤,經討論後端要先行驗證欄位是否存在,查找有問題後一律回覆 422 給前端

---

你的角色是網路工程師。請協助將我提供的功能簡述,轉換成規格(spec)格式。轉換後的 spec 需包含以下幾個內容:

- 功能標題
- 功能簡述
- 實作建議
- 測試建議
- 其他注意事項

允許使用 emoji 輔助
# 📌 Widget 欄位存在性驗證(後端)

## 📝 功能簡述

當使用者完成 widget 設定後,如果後續手動刪除了 widget 所需的欄位,可能導致運算錯誤或使用體驗異常。後端需在每次相關操作前進行欄位存在性驗證,若必填欄位不存在,統一回傳 HTTP 422 錯誤給前端。

---

## 🛠️ 實作建議

1. **欄位驗證邏輯**

   * 在 widget 運算、讀取前加入一段欄位檢查邏輯。
   * 驗證所有 widget 設定中宣告的欄位是否存在於資料來源(如資料庫欄位、配置檔或動態欄位列表)。
   * 若任一欄位不存在,立即中止流程並回傳 422。

2. **統一錯誤格式**

   * 使用統一格式,例如:
        ```json
        {
          "message": "missing field",
          "code": "CO-ME108",
          "status": false
        }
        ```
        **此 message 以及 code 為示意。**

---

## 🧪 測試建議

1. **正常情境**
   * 所有欄位都存在 → 回傳 200 並正常執行邏輯。

2. **單一欄位缺失**
   * 刪除一個 widget 必要欄位 → 應回傳 422,且錯誤訊息不包含該欄位名稱。

3. **多個欄位缺失**
   * 模擬多個欄位被刪除 → 應回傳 422,且錯誤訊息不包含該欄位名稱。

其實他寫了相當多建議,這是我整理過後的結果,那些建議不一定符合我們需求,但可以當作參考也是相當不錯。

Using AI Turn My Requirement Into A Spec

LLM 最強大的就是你給予適當的內容,它可以幫你延伸很多你沒注意到的問題,最近常常把需求稍微梳理過後,加上一段 prompt 讓他整理成 spec,再將結果調整一下變可以開 redmine 的票。

widget 設定完成後,若使用者將 widget 的欄位刪除,有機率會造成計算或使用錯誤,經討論後端要先行驗證欄位是否存在,查找有問題後一律回覆 422 給前端

---

你的角色是網路工程師。請協助將我提供的功能簡述,轉換成規格(spec)格式。轉換後的 spec 需包含以下幾個內容:

- 功能標題
- 功能簡述
- 實作建議
- 測試建議
- 其他注意事項

允許使用 emoji 輔助
# 📌 Widget 欄位存在性驗證(後端)

## 📝 功能簡述

當使用者完成 widget 設定後,如果後續手動刪除了 widget 所需的欄位,可能導致運算錯誤或使用體驗異常。後端需在每次相關操作前進行欄位存在性驗證,若必填欄位不存在,統一回傳 HTTP 422 錯誤給前端。

---

## 🛠️ 實作建議

1. **欄位驗證邏輯**

   * 在 widget 運算、讀取前加入一段欄位檢查邏輯。
   * 驗證所有 widget 設定中宣告的欄位是否存在於資料來源(如資料庫欄位、配置檔或動態欄位列表)。
   * 若任一欄位不存在,立即中止流程並回傳 422。

2. **統一錯誤格式**

   * 使用統一格式,例如:
        ```json
        {
          "message": "missing field",
          "code": "CO-ME108",
          "status": false
        }
        ```
        **此 message 以及 code 為示意。**

---

## 🧪 測試建議

1. **正常情境**
   * 所有欄位都存在 → 回傳 200 並正常執行邏輯。

2. **單一欄位缺失**
   * 刪除一個 widget 必要欄位 → 應回傳 422,且錯誤訊息不包含該欄位名稱。

3. **多個欄位缺失**
   * 模擬多個欄位被刪除 → 應回傳 422,且錯誤訊息不包含該欄位名稱。

其實他寫了相當多建議,這是我整理過後的結果,那些建議不一定符合我們需求,但可以當作參考也是相當不錯。

2025/10/08

Laravel Sail On WSL

緣由

我開發 PHP,不管是純 PHP 或 Laravel 都很喜歡用 Homestead,他優點是可以起一台 VM 就可以同時使用多個專案,而且透過修改一個簡單的 Homestead.yml 就可以做完全部環境的設定,但官方已經宣布不維護了,身為資訊人,你要嘛 fork 一份自己維護,不然就要跟著時代走。

不維護的原因也很簡單,VM 真的很肥很吃資源,現在 docker 實在是太香了,而且 Windows 引入 WSL 以後,Docker Desktop 也是秒級啟動,Laravel Sail 也是 base on Docker Compose,是時候跟 Homestead 分手了 。°(°¯᷄◠¯᷅°)°。。

SMB

WSL 有一個痛點,你用他預設掛載進去的目錄執行任何程式碼都慢到哭,目前我實作比較好的方式是透過 SMB 把你的工作目錄掛進去,跑起來跟在本機執行相差無幾,首先是在你要分享的目錄上按右鍵選內容,共用 -> 進階共用 -> 共用此資料夾,共用名稱建議字尾加個 $,這樣在網路芳鄰上會看不到這個目錄,像是 code$,這樣我打 \\HOST\\code$ 可以連到,在公網上是看不到的,另外是我在本機多設定了一組 smb/smb 帳號,這樣我在 demo 的時候也不會因為被看到密碼尷尬,SMB 沒設定的話預設是你這台 Windows 的登入帳號密碼。

WSL

我並沒有使用 Docker Desktop,因為他開機即啟動,而且無法手動 terminate,WSL 資源雖然吃不多,但我沒有要開發的時候並不需要你來吃我這份資源,我知道網路上有許多改機碼之類的方式可以達成這個目的,但官方沒給的我認為都是邪門歪道,我很不喜歡用非官方的方式解決問題,所以我自己起一台 Ubuntu wsl --install -d Ubuntu --name develop,之後再進去自己安裝 Docker,自由自在~

進去 WSL 以後我建議一律 sudo su,用 root 可以少遇到很多奇奇怪怪的權限問題,再來就是手動掛載 smb 了,我將指令放到 ~/.bashrc,因為 WSL 沒有固定 IP,所以你沒辦法把掛載寫死,要的話也是要寫一堆 script 去判斷東西,我是覺得沒必要。

~/.bashrc

# ~/.bashrc - SMB Mount Configuration with Dynamic Windows IP

_get_windows_ip() {
    # 方法 1:透過 /proc/net/route (最穩定)
    # 取得預設閘道,就是 Windows host IP
    local gateway_ip
    gateway_ip=$(awk '$1 == "00000000" {print strtonum("0x"substr($3,7,2))"."strtonum("0x"substr($3,5,2))"."strtonum("0x"substr($3,3,2))"."strtonum("0x"substr($3,1,2))}' /proc/net/route | head -1)
    
    if [[ -n "$gateway_ip" ]]; then
        echo "$gateway_ip"
        return 0
    fi
    
    # 方法 2:透過 ip route (備選)
    gateway_ip=$(ip route show | awk '/^default via/ {print $3}' | head -1)
    
    if [[ -n "$gateway_ip" ]]; then
        echo "$gateway_ip"
        return 0
    fi
    
    # 方法 3:透過 resolv.conf (Windows 10 相容)
    gateway_ip=$(awk '/^nameserver/ {print $2; exit}' /etc/resolv.conf)
    
    if [[ -n "$gateway_ip" ]]; then
        echo "$gateway_ip"
        return 0
    fi
    
    echo "" >&2
    return 1
}

_mount_smb() {
    local smb_share="$1"
    local mount_point="$2"
    local smb_username="smb"
    local smb_password="smb"
    local smb_host
    
    # 檢查必要參數
    if [[ -z "$smb_share" ]] || [[ -z "$mount_point" ]]; then
        echo "Usage: _mount_smb <share> <mount_point>" >&2
        return 1
    fi
    
    # 檢查是否已掛載
    if mountpoint -q "$mount_point"; then
        echo "Already mounted at $mount_point"
        return 0
    fi
    
    # 確保掛載點存在
    [[ -d "$mount_point" ]] || mkdir -p "$mount_point"
    
    # 動態取得 Windows host IP
    smb_host=$(_get_windows_ip)
    
    if [[ -z "$smb_host" ]]; then
        echo "Error: Cannot determine Windows host IP" >&2
        return 1
    fi
    
    echo "Detected Windows host IP: $smb_host"
    echo "Attempting to mount //$smb_host/$smb_share to $mount_point"
    
    # 測試連線
    if ! timeout 3 bash -c "</dev/tcp/$smb_host/445" 2>/dev/null; then
        echo "Error: Cannot connect to $smb_host:445 (SMB port)" >&2
        return 1
    fi
    
    echo "SMB port is reachable, mounting..."
    
    # 執行掛載 (優先使用 SMB 3.0,失敗則試 2.1)
    if sudo mount -t cifs "//${smb_host}/${smb_share}" "$mount_point" \
        -o username="$smb_username",password="$smb_password",vers=3.0,uid=$(id -u),gid=$(id -g),file_mode=0777,dir_mode=0777 2>/dev/null; then
        echo "✓ SMB share mounted at $mount_point"
        return 0
    else
        echo "SMB 3.0 failed, trying SMB 2.1..."
        
        if sudo mount -t cifs "//${smb_host}/${smb_share}" "$mount_point" \
            -o username="$smb_username",password="$smb_password",vers=2.1,uid=$(id -u),gid=$(id -g),file_mode=0777,dir_mode=0777 2>/dev/null; then
            echo "✓ SMB share mounted at $mount_point (SMB 2.1)"
            return 0
        fi
        
        echo "✗ Failed to mount SMB share" >&2
        return 1
    fi
}

# 只在互動式 shell 中執行
if [[ $- == *i* ]] && [[ $SHLVL -eq 1 ]]; then
    _mount_smb "code$" ~/mnt/code
fi

這段程式碼簡單講就是把我在本機 share 的 \\HOST\\code$ 掛到 ~/mnt/code,需要多掛幾個多執行幾次 _mount_smb 即可,每次進入 WSL 便抓取 Host Gateway IP 來連結,目錄跟檔案權限都設定成 777 避免一堆怪事發生,反正開發機不怕。

實際測試

這邊獻上幾個環境跑 phpunit 測試結果,測試數量 1364:

Host

real 1m 37.34s
real 1m 37.83s
real 1m 36.08s


平均:1m 37.08s

Vagrant + SMB

real 3m58.906s
real 3m32.119s
real 3m17.965s


平均:3m 36.33s

WSL + SMB

real 1m48.497s
real 1m57.464s
real 1m50.013s


平均:1m51.991s

其實在 Vagrant 裡面即便使用 SMB 執行速度還是悲劇,我已經這樣操作很久了,但因為 Homestead 的便利性一直選擇忽視這個問題,反正也不是整天跑 full test,另外有趣的一件事情,在 WSL 裡面使用 docker build 一個 php 環境跑測試,我預計應該要掉個 3% 的速度,因為它是 Host -> WSL -> Docker Volume,多一層轉換理應要消耗吧,但沒想到跑了幾次的結果它居然跟在 Windows Host 跑的時間差不多,php.ini 除了調整 memory_limit = -1 以外都沒動,不確定是 Docker 預設拉取的 OS 是 Alpine Linux 效能很好,還是 PHP 官方的 container php.ini 的配置有優化,總之蠻意外的。

之後預計會使用 WSL + Docker + Laravel/Sail 開發測試專案啦,謝謝你 Homestead。

2025/09/13

Docker Compose

原本以為 docker composedocker-compose 指令官方的套件整合,後來發現其實是全新的東西。

差異說明

  • docker-compose:是原先獨立安裝的 Compose 工具,使用 Python 實作,指令是用 docker-compose(中間有連字號),這是 Docker Compose v1 的常用形式。

  • docker compose:是 Docker Compose v2 版本,整合進 Docker CLI 中,採用 Go 語言重寫,使得 Compose 命令成為 docker 命令的一部分,指令使用空格隔開,即 docker compose,這是官方現階段推薦的新作法,提升整合度與效能。

Compose 檔案名稱的使用

歷史上預設檔名是 docker-compose.yml,由於 docker-compose 指令而來,仍被廣泛使用以確保向後相容。

新推薦的檔名是 compose.yaml(或 compose.yml 同樣被接受),更符合 Compose 規範,且映射到 docker compose 指令。官方文件中多建議使用 compose.yaml 作為現代 Compose 的標準檔名。

也可以用 -f 選項指定自訂檔名,靈活在不同目錄或專案間使用多個 Compose 配置檔。

建議改動

  1. 版本號 version 已經不需要寫
  2. deploy 只在 Swarm 模式才有用
  3. 官方推薦改成 compose.yaml
  4. 支援 profiles(服務分組)
services:
  db:
    image: mysql:8
    profiles: ["dev"]

  redis:
    image: redis:6
    profiles: ["prod", "staging"]
docker compose --profile dev up # 這樣執行只有 profiles 含 dev 的 container 會啟動
  1. 官方不建議用 links,用 depends_on 取代
  2. 多檔案合併,常用於「開發」跟「生產」環境分離

compose.yml

services:
  app:
    image: nginx:alpine
    ports:
      - "80:80"
    environment:
      - APP_ENV=production

compose.override.yaml

services:
  app:
    environment:
      - APP_ENV=development
    ports:
      - "8080:80" # 開發改用 8080 port
    volumes:
      - ./src:/usr/share/nginx/html:ro
docker compose -f compose.yaml -f compose.override.yaml up
  1. docker compose cp 已支援
  2. docker compose config 支援扁平化配置檔顯示
  3. 支援 Include
# compose.yaml
include:
  - ./database/compose.yaml
  - ./monitoring/compose.yaml
  
services:
  web:
    image: nginx
  1. 支援單一 updown service
docker compose up web fpm -d
docker compose down fpm

但有 depends_on 的會被連帶啟動。

2025/07/22

Gcloud Common Commands

☁️ gcloud 常用指令小抄

🧑‍💼 account:帳號管理

功能 指令
登入 gcloud auth login
登出 gcloud auth revoke
查看目前帳號 gcloud config get-value account
設定預設帳號 gcloud config set account <ACCOUNT>
列出所有帳號 gcloud auth list

🏢 project:專案管理

功能 指令
列出所有可用的專案 gcloud projects list
查看目前使用中的專案 gcloud config get-value project
切換專案 gcloud config set project <PROJECT_ID>
查看目前所有 config gcloud config list

💻 compute:VM 實例管理

一般操作

功能 指令
列出所有 instance gcloud compute instances list
查看 instance 詳細資訊 gcloud compute instances describe <INSTANCE> --zone=<ZONE>
啟動 instance gcloud compute instances start <INSTANCE> --zone=<ZONE>
關閉 instance gcloud compute instances stop <INSTANCE> --zone=<ZONE>
刪除 instance gcloud compute instances delete <INSTANCE> --zone=<ZONE>

SSH & 檔案傳輸

功能 指令
SSH 連線 gcloud compute ssh <INSTANCE> --zone=<ZONE>
上傳檔案 gcloud compute scp <LOCAL> <INSTANCE>:<REMOTE> --zone=<ZONE>
下載檔案 gcloud compute scp <INSTANCE>:<REMOTE> <LOCAL> --zone=<ZONE>
rsync 資料夾 gcloud compute rsync <LOCAL_DIR> <INSTANCE>:<REMOTE_DIR> --zone=<ZONE>

📦 scp 常用參數

參數 說明
--recurse-r 遞迴傳輸資料夾
--compress 傳輸時壓縮(較快)
--scp-flag="-C" 額外傳入 scp flag,例如啟用壓縮

📦 storage:Cloud Storage 操作

功能 指令
列出 bucket gcloud storage buckets list
建立 bucket gcloud storage buckets create <BUCKET> --location=<REGION>
上傳檔案 gcloud storage cp <LOCAL> gs://<BUCKET>/
下載檔案 gcloud storage cp gs://<BUCKET>/<FILE> <LOCAL>
刪除檔案 gcloud storage rm gs://<BUCKET>/<FILE>
刪除 bucket gcloud storage buckets delete <BUCKET>

🔁 Cloud Storage rsync

gcloud storage 支援類似 rsync 的功能來同步本地與 GCS 目錄:

上傳資料夾到 bucket

gcloud storage rsync -r ./local-folder gs://my-bucket/remote-folder

從 bucket 下載資料夾

gcloud storage rsync -r gs://my-bucket/remote-folder ./local-folder

🛠️ 常用參數

參數 說明
-r 遞迴處理目錄(必要)
-d 刪除目標端中不存在的檔案(mirror 同步)
-n 模擬執行(dry run,不實際動作)
-x "<REGEX>" 排除符合正則的檔案

🧩 其他常用指令

設定與服務

功能 指令
查看目前所有 config gcloud config list
清除設定 gcloud config unset <PROPERTY>
啟用服務 gcloud services enable <SERVICE>
停用服務 gcloud services disable <SERVICE>

IAM 權限操作

功能 指令
查看 IAM 成員 gcloud projects get-iam-policy <PROJECT>
新增 IAM 成員與角色 gcloud projects add-iam-policy-binding <PROJECT> --member="user:<EMAIL>" --role="roles/<ROLE>"

🧠 小提醒

  • 使用 get-value 取得設定值比 list 簡潔:
gcloud config get-value project
gcloud config get-value account
gcloud config get-value compute/zone
gcloud config get-value compute/region

2025/06/02

Neovim Copilot Chat Plugin

Github Copilot 已經是我工作上的好幫手,它的好處是你付一次月租他可以支援所有你有支援的軟體,我安裝在 Jetbrains 全套、vscode、neovim,一魚多吃相當方便,這邊介紹一下 vim/neovim 用來跟 copilot 對話的好用插件 CopilotChat.nvim,當然你要使用之前必須先有 copilot 帳號,以及安裝開通 copilot.vim

網站中有許多 plugin 安裝的方式,選自己常用的安裝即可,在你的 vim/neovim 配置檔案要加入啟動的語法:

lua << EOF
    require("CopilotChat").setup { }
EOF

" 用 cc 叫出視窗
nnoremap <silent> cc :CopilotChatToggle<CR>

我習慣是盡量不太修改插件預設值,避免有轉換環境的痛苦,套用 cc 叫出視窗是我針對 CopilotChat 唯一的設定了,我們用撰寫一個 docker-compose.yml 當作範例。

nvim docker-compose.yml 建立一個檔案,進入 vim/neovim 按下 cc,可以看到 CopilotChat 視窗在左邊出現。

以下是常用的快捷鍵以及指令:

插入模式 普通模式 功能說明
<Tab> - 觸發或接受補全選單(tokens 自動完成)
<C-c> q 關閉聊天視窗
<C-l> <C-l> 重設並清空聊天視窗
<C-s> <CR> 提交當前的提示內容
- grr 切換目前游標所在行是否為「固定提示」
- grx 清除所有「固定提示」
<C-y> <C-y> 接受最近的一個差異(diff)
- gj 跳轉到最近差異的區塊
- gqa 將所有回覆加入 quickfix 清單
- gqd 將所有差異加入 quickfix 清單
- gy 將最近的差異複製(yank)到暫存器
- gd 顯示原始內容與最近差異的比對
- gi 顯示目前聊天的資訊
- gc 顯示目前聊天的上下文
- gh 顯示說明訊息
指令 說明
:CopilotChat <輸入內容>? 開啟聊天視窗,並可選擇性地輸入問題或指令
:CopilotChatOpen 開啟聊天視窗
:CopilotChatClose 關閉聊天視窗
:CopilotChatToggle 切換聊天視窗(開啟與關閉間切換)
:CopilotChatStop 停止目前的輸出(中斷正在生成的內容)
:CopilotChatReset 重設聊天視窗(清除目前對話)
:CopilotChatSave <名稱>? 儲存聊天記錄,可選擇性指定一個名稱
:CopilotChatLoad <名稱>? 載入先前儲存的聊天記錄
:CopilotChatPrompts 查看並選擇提示範本(例如:解釋代碼、重構等用途)
:CopilotChatModels 查看並選擇可使用的 AI 模型
:CopilotChatAgents 查看並選擇可用的 AI 助手代理(不同風格或角色)
:CopilotChat<PromptName> 使用特定提示範本開啟對話(例如 :CopilotChatExplain

現在位於左邊的 CC 視窗,輸入寫一份 docker-compose.yml,需要安裝 nginx 以及 php-fpm,先按 esc 回到 visual mode 以後再按下 enter

我們可以看到他幫我們產生了許多內容,下面包含了基礎的 nginx 配置,更下方還有測試用的 php 程式碼,但我需要的只有 docker-compose 的部分,所以我將 cursor 移到我需要的區塊,然後按下 ctrl + y,此時他便將該區塊的程式碼移到了右邊去。

我接著輸入 prompt,現在的 docker-compose 不需要使用 version,他會依照上文幫我重產內容。

我們可以使用 gd 快速看出差異。

按下 q 回來以後,一樣使用 ctrl + y 將改變套用,這樣就可以套用新的格式,假設你已經脫離上下文,需要透過 cc 給你的檔案建議呢,這時就要用 vim/neovim 的選取功能,將需要他幫忙的地方選取起來,再去問一下 cc。

一樣用 ctrl + y 套用結果,如果你是要他參考整個文件就得使用全選了,另外一種方式是在命令列輸入 :CopilotChat 將 nginx 改為 ubuntu 版本 #buffer,這樣他就會把該檔案的內容當作緩衝使用,我自己是覺得用選的比較快。

另外一個主動參考整個文件的方法就是在第一次打開視窗問問題時,加上 #buffer,例如:

#buffer 將 $name 取代為 $userName

這樣他便會去直接參考整個檔案。

當我們使用 :CopilotChatModels 時,他會列出所有你可以使用的 model,可以記一下編號,移動到最下面後可以填入該編號,便可以切換模型。

但這個變動只有在這次有效,要永久使用該模型可以設定 config。

lua << EOF
require("CopilotChat").setup {
    model = 'claude-3.7-sonnet'
}
EOF

2025/05/26

Homestead SSL Issue

Laravel Homestead 在使用 nginx 創建網頁 config 時是有幫他配置 ssl key 的,但為什麼我們使用 https 還是會被警告呢,因為我們的瀏覽器是不認這個憑證的,在 Windows 上可以手動安裝該憑證讓你測試的時候可以透過 https 連到你的測試機,有時候開發會遇到跨網站連結 https 的狀況,如果你是 http 的話可能會被擋下來。

配置 Homestead.yml

folders:
  - map: ./ssl
    to: /ssl
  - map: ./code
    to: /home/vagrant/code

sites:
  - map: ssl.test
    to: /home/vagrant/code/
    php: "8.2"

這是我添加的測試網站,將根目錄開一個 ssl 目錄,在 code 目錄下放一個有 echo hello world 的 PHP 檔案,修改本機的 hosts 將 ssl.test 指到該 ip。

vm 起來後,vagrant ssh 進去該 vm,執行 sudo cp /etc/ssl/certs/ca.homestead.homestead.pem /ssl,將憑證複製到 /ssl 下,exit 退出 vm。

前往該 ssl 目錄,執行以下步驟:

  1. 右鍵 點擊憑證檔,選擇 「安裝憑證」
  2. 點選 「下一步」
  3. 選擇 「將所有憑證放入以下的存放區」
  4. 點選 「瀏覽」
  5. 選擇 「受信任的根憑證授權單位」
  6. 點選 「下一步」
  7. 點選 「完成」

此時瀏覽 https://ssl.test 應該就不會有警告了,如果還是有的話可能是 nginx cache,重開 server 或瀏覽器應該就解決了,可以先用無痕驗證一下。

該憑證過期時間為十年,應該是蠻夠用的,過期的話要重產一個新的,但我想應該在過期之前就會重置測試機了。

Homestad PHP PPA Issue

最近執行 Homesteadvagrant reload --provision 會遇到錯誤。

E: Repository 'https://ppa.launchpadcontent.net/ondrej/php/ubuntu jammy InRelease' changed its 'Label' value from '***** The main PPA for supported PHP versions with many PECL extensions *****' to 'PPA for PHP'

這是因為該 PPA 的 Label 產生了變化,解決方法也挺簡單。

vagrant ssh 進去 vm 後,執行下面指令:

sudo rm -rf /var/lib/apt/lists/*
sudo apt-get clean
sudo apt-get update --allow-releaseinfo-change

記得遇到這個問題處理過後要退出 vm 執行 vagrant reload --provision,才會有變動。

2025/05/13

Tmux Auto Split Window Syntax

tmux 是很強大的視窗分割工具,他可以透過語法建立初始化的視窗結構,今天的案例是我在我的 vagrant 虛機裡面有許多 laravel 專案,每個專案都有使用 horizon queue,每次要工作的時候我先 vagrant ssh 進去虛機以後,再手動割四個視窗,進入四個專案目錄後執行 php artisan horizon 啟動 queue server,這個過程可以透過語法讓 tmux 自己幫你完成。

horizon.sh

#!/bin/bash

# API 專案目錄
PROJECTS=(
    "/project1"
    "/project2"
    "/project3"
    "/project4"
)

# 取得目前 tmux Session 名稱
SESSION=$(tmux display-message -p '#S')

# 開啟新視窗並處理第一個 pane
FIRST_DIR="${PROJECTS[0]}"
tmux new-window -n "horizon" -c "$FIRST_DIR"
tmux select-window -t "${SESSION}:horizon"

# 設定第一個 pane 並執行命令
tmux select-pane -t 0
tmux send-keys -t 0 "php artisan horizon" C-m

# 處理其餘 panes
for i in "${!PROJECTS[@]}"; do
    if [ "$i" -eq 0 ]; then continue; fi

    DIR="${PROJECTS[$i]}"

    # 分割方向
    if [ "$i" -eq 1 ]; then
        tmux split-window -h -c "$DIR"
    else
        tmux select-pane -t $((i - 1))
        tmux split-window -v -c "$DIR"
    fi

    # 執行命令
    tmux select-pane -t "$i"
    tmux send-keys -t "$i" "php artisan horizon" C-m
done

# 整理 pane 排列
tmux select-layout tiled

該 script 需要在開啟了一個 tmux window 後執行。

2025/05/02

Log Handle In Docker

Docker 現在已經是可以當 production server 用的工具了,來聊一下他對 logs 有哪些配置方式。

根據 docker 官方文件,目前有以下幾個配置方法:

Driver 說明
none 容器沒有可用的日誌,且 docker logs 不會返回任何輸出。
local 日誌以一種為了最小化開銷而設計的自定義格式儲存。
json-file 日誌格式為 JSON。Docker 的預設日誌驅動程式。
syslog 將日誌消息寫入 syslog 設施。主機上必須運行 syslog 守護程序。
journald 將日誌消息寫入 journald。主機上必須運行 journald 守護程序。
gelf 將日誌消息寫入 Graylog 擴展日誌格式 (GELF) 端點,如 Graylog 或 Logstash。
fluentd 將日誌消息寫入 fluentd(轉發輸入)。主機上必須運行 fluentd 守護程序。
awslogs 將日誌消息寫入 Amazon CloudWatch Logs。
splunk 使用 HTTP 事件收集器將日誌消息寫入 Splunk。
etwlogs 將日誌消息寫入 Windows 事件追蹤 (ETW) 事件。僅適用於 Windows 平台。
gcplogs 將日誌消息寫入 Google Cloud Platform (GCP) Logging。

如果你能擁有自己的 log server,會建議把 log 打進 log api server,擁有權限的人就可以在 web tool 上做所有的查詢以及決定資料怎麼保留跟處理,畢竟如果 log 停留在各 server,以現在很多服務走 HA 或是 k8s 情況下,遇到狀況你到底要進哪台服務查詢都會成為問題。

不過現實生活中很多人都只有一台 server,而且也沒有 api log server 這種資源,只是要有 log 即可的話,我們一般會用 json-file 這個選項,這也是 docker 預設的 log driver。

docker-compose json-file 範例

services:
    nginx:
        image: nginx
        container_name: nginx
        ports:
            - "80:80"
        volumes:
            - ./nginx.conf:/etc/nginx/nginx.conf
        logging:
            driver: json-file
            options:
                max-size: "10m"
                max-file: "3"

這個範例會把 nginx 的 log 儲存到 /var/lib/docker/containers/<container-id>/<container-id>-json.log,當 log 檔案大小超過 10MB 時,docker 會自動將舊的 log 檔案重新命名為 <container-id>-json.log.1,並開始寫入新的 log 檔案,最多會保留 3 個 log 檔案,當你使用指令 docker logs 時,docker 會自動讀取最新的 log 檔案,並顯示在終端機上。

一般 image 如 nginx,他會把 log 設定成 stdout,這樣 docker 才能夠讀取到 log,然後寫入到你設定的 log driver 裡面,選擇 json-file 時 docker 會自動幫你把 stdout 或 stderr 的 log 轉成 json 格式,然後寫入到你設定的 log driver 裡面。

nginx 的話,他的配置會是這樣:

access_log /dev/stdout;
error_log /dev/stderr;

這樣的缺點是,access log 會跟 error log 都寫在同一個檔案裡面,會造成你在查詢 log 的時候不方便,假設我們想要使用 docker,又想要產生跟傳統 web server 一樣的 log,我們可以修改 nginx.conf,讓他把 log 寫到 /var/log/nginx/access.log/var/log/nginx/error.log

Nginx Config 範例

nginx.conf

http {
    server {
        listen       80;
        server_name  localhost;

        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;
    }
}

然後把 log 目錄掛載出來。

docker-compose.yml

services:
    nginx:
        image: nginx
        container_name: nginx
        ports:
            - "80:80"
        volumes:
            - ./nginx.conf:/etc/nginx/nginx.conf
            - ./logs:/var/log/nginx

我們在 /etc/logrotate.d/nginx 新增一個檔案,然後把以下的內容貼上去:

logrotate conf

/var/log/nginx/*.log {
    daily
    missingok
    rotate 7
    compress
    notifempty
    sharedscripts
    lastaction
        docker exec nginx nginx -s reopen
    endlaction
}

docker exec nginx nginx -s reopen 的用意是 logrotate 在 rotate 之後需要呼叫 docker 重新開啟 nginx 的 log,不這樣的話 nginx in docker 會使用你 rename 後的 log 檔案繼續寫 log。

當我們使用 compress,並且會針對壓縮過後的檔案做後續處理的話,一定要有 sharedscripts 以及使用 lastaction,如果使用 postrotate 的話,他不會等所有的檔案都執行完壓縮就直接作動了。

這邊有幾個 logrotate 的指令可以使用:

指令 說明
prerotate/endscript 在進行日誌輪轉之前執行的指令,每個匹配的日誌檔案都會執行一次
postrotate/endscript 在進行日誌輪轉之後執行的指令,每個匹配的日誌檔案都會執行一次
firstaction/endscript 在所有匹配日誌檔案進行輪轉之前執行的指令,只執行一次
lastaction/endscript 在所有匹配日誌檔案進行輪轉之後執行的指令,只執行一次
preremove/endscript 在舊的日誌檔案被移除之前執行的指令

另外有一種不用重新開啟檔案的配置參數 copytruncate,這個參數會在 logrotate 開始 rotate 的時候複製原本的 log 檔案,然後 truncate 原本的 log 檔案,這樣 nginx 就不需要重新開啟檔案了,但這樣有機會漏寫紀錄,所以不建議使用這個參數。

Apache2 Config 範例

apache2 的原始 log 配置會是:

ErrorLog /proc/self/fd/2
CustomLog /proc/self/fd/1 common

我們要修改成:

ErrorLog /var/log/apache2/error.log
CustomLog /var/log/apache2/access.log combined

然後把 log 目錄掛載出來。

docker-compose.yml

services:
    apache2:
        image: httpd
        container_name: httpd
        ports:
            - "80:80"
        volumes:
            - ./httpd.conf:/usr/local/apache2/conf/httpd.conf
            - ./logs:/var/log/apache2

httpd.conf

<VirtualHost *:80>
    ServerName localhost
    DocumentRoot /var/www/html

    ErrorLog /var/log/apache2/error.log
    CustomLog /var/log/apache2/access.log combined
</VirtualHost>

logrotate conf

/var/log/apache2/*.log {
    daily
    missingok
    rotate 7
    compress
    notifempty
    sharedscripts
    lastaction
        docker exec apache2 apachectl -k graceful
    endscript
}

這個範例跟 nginx 的範例一樣,只是把 nginx -s reopen 改成 apachectl -k graceful

假設你 logrotate 後要執行其他 bash 做其他處理的話。

/var/log/apache2/*.log {
    daily
    missingok
    rotate 7
    compress
    notifempty
    sharedscripts
    lastaction
        docker exec httpd apachectl -k graceful
        /usr/bin/bash /usr/local/bin/other_script.sh
    endscript
}

2025/04/02

Neovim Installation

neovim 是我即便是在 windows 下也會開來使用的 command line editor,來 memo 一下 Windows 跟 Linux 怎麼安裝。

Windows

我在 Windows 上都是使用 scoop 來管理套件,所以 neovim 也是用 scoop 來安裝,這樣好處是我用 scoop update 時都可以保持拿到最新的 neovim

scoop install neovim

powershell 可以執行指令安裝 vim-plug

iwr -useb https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim | ni $HOME\AppData\Local\nvim\autoload\plug.vim -Force

接下來就是配置你的個人檔案,位置在 C:\Users\你的使用者名稱\AppData\Local\nvim\init.vim,可以使用任何編輯軟體包括 neovim 開啟他,把 config 都配好以後,執行 neovim,首次執行會有很多錯誤是正常的,因為你的 plugin 並沒有真的安裝,開啟後執行 :PluginInstall,關閉再開啟 neovim 應該就正常了。

Linux

Linux 的部分有兩種方法,一個是透過你自己的 OS 去找最新的 repo 安裝,另一個是可以去 https://github.com/neovim/neovim/releases/,neovim 的 github 官網找最新版本的 appimage 檔案,只要你的 OS 有 FUSE 就可以直接使用,以 ubuntu 22 為例,此文章撰寫時最新的穩定版本是 NVIM v0.11.0

curl -LO https://github.com/neovim/neovim/releases/download/v0.11.0/nvim-linux-x86_64.appimage
chmod +x nvim-linux-x86_64.appimage
sudo mv nvim-linux-x86_64.appimage /usr/local/bin/nvim

執行完這段就有 nvim 可以執行了,接著裝套件。

sh -c 'curl -fLo "${XDG_DATA_HOME:-$HOME/.local/share}"/nvim/site/autoload/plug.vim --create-dirs \
       https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim'

mkdir -p ~/.config/nvim
touch ~/.config/nvim/init.vim
nvim ~/.config/nvim/init.vim

接著就跟 Windows 一樣安裝套件後就可以用了,之後要更新 neovim 版本只要抓最新版本的 appimage 替換即可。

2025/03/03

Pyinstaller With Tobix Pywine

Python 可以透過 PyInstaller 將執行檔編譯成 Windows 可以使用的 exe,很好用,不同的 python 版本編譯出來的 exe 對目前的 Windows 有不同的支援度:

Python 版本 Windows 7 Windows 8 Windows 10 Windows 11
Python 3.6 ✅ (最後支援)
Python 3.7
Python 3.8
Python 3.9
Python 3.10 ⚠️ (可能有問題) ⚠️ (可能有問題)
Python 3.11 ❌ (不支援) ❌ (不支援)
Python 3.12 ❌ (不支援) ❌ (不支援)

所以編譯的時候要考量 python 的版本,Pyinstaller 只有支援 Windows,在 Linux like 的環境要執行必須透過 Wine 這種指令驅動,如果你要跑 CI/CD 沒那麼方便,後來我選擇了 tobix/pywine - Docker Image | Docker Hub 這個 docker 專案,只要選擇你要的 python 版本,如 tobix/pywine:3.8 ,將外部的 python 檔案 mount 進去 container,執行 docker exec -it builder wine pyinstaller.exe -F xxx.py 便可以編譯了,如果有相關要安裝的套件,也可以事先執行 docker exec -it builder wine python.exe -m pip install -r /tmp/requirements.txt

Docker PHP Extension Installer

我是一位 PHP 開發者,目前許多的 production 專案也會用到 docker 了,所以套件安裝經常會在 Dockerfile 出現,在 php - Official Image | Docker Hub 上你要安裝 extension,他的範例是:

FROM php:8.2-fpm
RUN apt-get update && apt-get install -y \
		libfreetype-dev \
		libjpeg62-turbo-dev \
		libpng-dev \
	&& docker-php-ext-configure gd --with-freetype --with-jpeg \
	&& docker-php-ext-install -j$(nproc) gd

這樣寫蠻醜的也不好維護,後來找到了好用的 mlocati/docker-php-extension-installer: Easily install PHP extensions in Docker containers,安裝方式就變得很乾淨。

FROM php:7.2-cli

RUN curl -sSLf \
        -o /usr/local/bin/install-php-extensions \
        https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions && \
    chmod +x /usr/local/bin/install-php-extensions && \
    install-php-extensions gd xdebug

install-php-extensions 一直增加你要的東西就可以,下方還有各 PHP 有哪些支援的 extension 表格對應,可以安裝 composer 版本也很貼心。

# Install the latest version
install-php-extensions @composer
# Install the latest 1.x version
install-php-extensions @composer-1
# Install a specific version
install-php-extensions @composer-2.0.2

Force LF EOL In Git

Windows 以及 MacOS 或 Linux 的換行符號是不太一樣的,Windows 是 CRLF,Linux like 是 LF,在 Windows 使用 git 如果沒有有設定參數,pullcommit 時 git 有可能會幫你自己轉換行符號,這樣會導致 git 在你沒有修改任何內容的情況下判定你有更改檔案,為了避免這樣的狀況最好是設定以下的內容:

$ git config core.eol lf
$ git config core.autocrlf input

Reference

github - Force LF eol in git repo and working copy - Stack Overflow

Scoop Update Command

我在 Windows 上使用 PowerShell 搭配 scoop 來 maintain 我的常用工具已經一陣子了,由於我是更新控,所有的軟體只要出了新版本我都要安裝,scoop 可以用指令來自動偵測更新,以下是我使用的更新命令。

scoop update && scoop update * && scoop cleanup * && scoop cache rm *

指令說明

  • scoop update:更新軟體庫
  • scoop update *:更新所有可以更新的軟體
  • scoop cleanup *:將舊版本刪除
  • scoop cache rm *:將 temp 的安裝檔案清除

2025/02/27

Git Export Modified Files From Commit

我們會遇到某些案子只能用 ftp deploy,會遇到只需要 upload 你修改內容的檔案這件事情,git 有可以做到這件事的指令,git archieve,該指令讓你指定 commit 後匯出從該 commit 到最後 commit 的修改檔案。

git archive -o changes.zip HEAD $(git diff --name-only 91455ec0^ HEAD)

只要將後面的 commit hash 改成你這波修改的開始就可以將修改的內容包含原始目錄結構匯出成 changes.zip,省去每次都要大量覆蓋的問題,如果你是使用 JetBrains 的軟體,並且使用他的 git 管理介面以及 ftp 功能,你可以在一整排你要上傳的使用 Ctrl + Shift + Alt + x 叫出 ftp 上傳介面,他很聰明的就會上傳你選擇的所有 commit 的修改檔案上去,很方便,我真的覺得 JetBrains 可以考慮把他的 git 管理工具獨立成一套軟體,像是 Sourcetree 那樣可以單獨使用,真的很好用。

2025/02/12

Linux Find

之前客戶 server 遇到單一目錄檔案過大問題,使用 rm -f * 或者是砍掉整個目錄都遇到 arguments 過多問題,最終使用 find 指令解決,有在使用 Linux 的人一定知道 find 的強大,在這邊作把常用的案例濃縮筆記一下。

範例

一般

find . -type f -name "lebron*" # 搜尋 lebron 為首的檔案
find . -type f -iname "lebron*" # 搜尋 lebron 為首的檔案(不區分大小寫)
find . -type d -name "lebron*" # 搜尋 lebron 為首的目錄

時間類

參數 說明 單位 範例 說明
atime 檔案最後存取的時間 find /path -atime -7 找出 7 天內被存取過的檔案
ctime 檔案的 inode 最後變更時間 find /path -ctime +30 找出 30 天前 inode 變更的檔案(如權限修改)
mtime 檔案內容的最後修改時間 find /path -mtime 0 找出今天內修改過內容的檔案
amin 檔案最後存取的時間 分鐘 find /path -amin -60 找出 60 分鐘內被存取過的檔案
cmin 檔案的 inode 最後變更時間 分鐘 find /path -cmin +120 找出 120 分鐘前 inode 變更的檔案
mmin 檔案內容的最後修改時間 分鐘 find /path -mmin -30 找出 30 分鐘內修改過內容的檔案

-time

指令 查詢範圍 檔案舉例(假設今天是 2024/02/21)
-mtime +3 4 天前或更早 ✅ 2024/02/17、✅ 2024/02/16
-mtime 3 剛好 3 天前 2024/02/18
-mtime -3 最近 3 天內(包含今天) ✅ 2024/02/21、✅ 2024/02/20、✅ 2024/02/19
-mtime 0 今天(00:00 之後) 2024/02/21(今天)

min

指令 查詢範圍 檔案舉例(假設現在是 12:00 PM)
-mmin +30 31 分鐘前或更早 ✅ 11:29 AM、✅ 11:00 AM
-mmin 30 剛好 30 分鐘前 11:30 AM
-mmin -30 最近 30 分鐘內(含現在) ✅ 12:00 PM、✅ 11:50 AM、✅ 11:45 AM
-mmin 0 最近 1 分鐘內(剛改變) 12:00 PM

空間類

參數 說明 單位 範例 說明
-size 指定檔案大小 512-byte blocks (預設) find /path -size +100M 找出大於 100MB 的檔案
-empty 找出空檔案或空目錄 N/A find /path -empty 找出空檔案或空目錄
-maxdepth 限制搜尋深度 N/A find /path -maxdepth 2 -size +1G 限制搜尋到最多 2 層目錄內,找出大於 1GB 的檔案
-type f 限制搜尋結果為檔案 N/A find /path -type f -size -50k 找出小於 50KB 的檔案
-type d 限制搜尋結果為目錄 N/A find /path -type d -size +10M 找出超過 10MB 的目錄(通常為不常見情況)

-size 常見用法:

  • find /path -size +1G → 找出 大於 1GB 的檔案
  • find /path -size -500k → 找出 小於 500KB 的檔案
  • find /path -size 100M → 找出 剛好 100MB 的檔案
  • find /path -size +100M -size -500M找出大小介於 100MB ~ 500MB 之間的檔案

-size 可用單位:

  • cbytes(例如 -size +1024c 找出大於 1024 bytes 的檔案)
  • kKB(例如 -size -500k 找出小於 500KB 的檔案)
  • MMB(例如 -size +10M 找出大於 10MB 的檔案)
  • GGB(例如 -size +1G 找出大於 1GB 的檔案)

顯示

格式 說明 範例
%p 檔案的完整路徑 ./test.txt
%f 檔案的名稱(不包括路徑) test.txt
%s 檔案的大小(以 byte 為單位) 1024
%M 檔案的權限(如 ls -l 顯示) -rw-r--r--
%u 檔案的擁有者 user1
%g 檔案的群組 group1
%T@ 檔案的最後修改時間,Unix 時間戳 1592390505.000000000
%TY-%Tm-%Td 檔案的修改日期(年-月-日) 2024-02-12
%TH:%TM:%TS 檔案的修改時間(時:分:秒) 14:30:15
%p 檔案的路徑 ./folder/test.txt
%k 檔案的大小(以 1KB 為單位,四捨五入) 1
%l 檔案是符號連結時,顯示其指向的檔案路徑 ./link_to_file.txt
%c 檔案的創建時間(僅支援某些檔案系統) 2024-02-10 12:00:00

顯示檔案名稱與大小(以 KB 為單位)

find . -type f -printf "%s KB\t%p\n"
1024 KB    ./file1.txt
2048 KB    ./file2.log

顯示檔案的修改日期與名稱

find . -type f -printf "%TY-%Tm-%Td %p\n"
2024-02-12 ./file1.txt
2024-02-12 ./file2.log

顯示檔案權限、擁有者、群組、大小和名稱

find . -type f -printf "%M %u %g %8s %p\n"
-rw-r--r-- user1 group1    1024 ./file1.txt
-rw-r--r-- user2 group2    2048 ./file2.txt

執行類

-printf

指令 說明 範例 解釋
-exec 對符合條件的檔案執行指令 find /path -type f -name "*.log" -exec rm {} \; 刪除所有 .log 檔案
-exec (多個參數) 傳遞多個參數到指令 find /path -type f -name "*.txt" -exec mv {} /backup/ \; 移動 .txt 檔案到 /backup/
-exec (多檔案批次處理) + 來一次傳遞多個檔案 find /path -type f -name "*.jpg" -exec ls -lh {} + 列出 .jpg 檔案詳細資訊(批次傳遞參數)
-execdir 在檔案所在目錄執行指令 find /path -type f -name "*.bak" -execdir rm {} \; 在檔案所在目錄刪除 .bak 檔案

刪除所有 .log 檔案:

find /var/log -type f -name "*.log" -exec rm {} \;

批次刪除 .tmp 檔案(避免多次執行 rm):

find /tmp -type f -name "*.tmp" -exec rm {} +

找出 .sh 檔案並修改權限:

find /scripts -type f -name "*.sh" -exec chmod +x {} \;

計算 .log 檔案總大小:

find /var/log -type f -name "*.log" -exec du -ch {} + | tail -n 1

-delete

指令 說明 範例 解釋
-delete 刪除符合條件的檔案 find /path -type f -name "*.log" -delete 刪除所有 .log 檔案
-delete (空目錄) 刪除空目錄 find /path -type d -empty -delete 刪除所有空目錄
-delete (限制層級) 限制搜尋深度刪除 find /path -maxdepth 1 -type f -name "*.tmp" -delete 只刪除 /path 下的 .tmp 檔案,不影響子目錄

其他

參數 說明 範例 解釋
-user 搜尋指定使用者擁有的檔案 find /path -user username 找出屬於指定使用者的檔案
-group 搜尋指定群組擁有的檔案 find /path -group groupname 找出屬於指定群組的檔案
-perm 根據檔案權限搜尋檔案 find /path -perm 755 找出擁有 755 權限的檔案
-newer 搜尋修改時間在指定檔案之後的檔案 find /path -newer file1.txt 找出在 file1.txt 之後被修改過的檔案
! -newer 搜尋修改時間在指定檔案之前的檔案 find /path ! -newer file1.txt 找出在 file1.txt 之前被修改過的檔案
-maxdepth 限制搜尋的最大目錄層級 find /path -maxdepth 2 只搜尋最多 2 層目錄深度的檔案
-mindepth 限制搜尋的最小目錄層級 find /path -mindepth 2 只搜尋從第 2 層開始的檔案
-print0 輸出檔案名稱並以 null 字元結尾,用於處理空格 find /path -print0 方便與 xargs -0 搭配使用,避免檔名有空格
-0 接受 null 字元分隔的檔案名稱 find /path -print0 | xargs -0 rm 用於 xargs,確保檔名包含空格的情況下也能正確處理
-iname 不區分大小寫的檔案名稱搜尋 find /path -iname "*.jpg" 搜尋所有 .jpg 檔案,不區分大小寫
-empty 搜尋空的檔案或目錄 find /path -empty 找出所有空的檔案或目錄
-exec 執行指定的命令來處理找到的檔案 find /path -name "*.log" -exec ls -lh {} \; 對每個找到的檔案執行 ls -lh
-exec + 批次處理所有符合條件的檔案,提高效率 find /path -name "*.log" -exec ls -lh {} + 批次執行命令,將多個檔案一次傳遞給命令處理
-prune 排除特定目錄,使其不被搜尋 find /path -path "/path/exclude" -prune -o -name "*.txt" -print 排除 /path/exclude 目錄,只搜尋其他 .txt 檔案
-follow 跟隨符號連結 find /path -follow -name "*.txt" 跟隨符號連結,搜尋 .txt 檔案