2026/03/09

Shell Script 語法比較表

整理常用 Shell (sh, bash, zsh) 在語法、功能上的差異與相容性,方便參考。

類型 sh (POSIX) bash zsh 備註
Shebang #!/bin/sh #!/bin/bash #!/bin/zsh 建議腳本跨平台用 #!/bin/sh
變數宣告 name=value 相同 相同 不能有空格
字串插值 "Hello $name" 相同 相同 都支援
命令替換 `date`$(date) 相同 相同 建議用 $( )
條件判斷 [ "$a" = "$b" ] [ "$a" = "$b" ][[ $a == $b ]] [ "$a" = "$b" ][[ $a = $b ]] [[ ... ]] 不是 POSIX 的寫法
邏輯運算 [ "$a" = 1 ] && [ "$b" = 2 ] [[ $a = 1 && $b = 2 ]] 相同 [[ ... && ... ]] 非 POSIX
數學運算 $((1+2)) ((i++)) / $((1+2)) ((i++)) / $((1+2)) POSIX sh 只能 $(( ))
陣列 ❌ 不支援 arr=(a b c)${arr[0]} arr=(a b c)${arr[1]} Bash 陣列從 0 起算,Zsh 從 1 起算
關聯陣列 key=value declare -A map; map[key]=val typeset -A map; map[key]=val POSIX sh 沒有
brace expansion {1..5} {1..5} POSIX sh 不支援
迴圈 for for i in 1 2 3; do ...; done for i in {1..3}; do ...; done brace expansion 會展開整個序列 {1..3} 不是 POSIX
函數定義 foo() { ... } foo() { ... }function foo { ... } 相同 function foo {} 不是 POSIX
字串長度 ${#var} 相同 相同
字串比較 = == / = == / = (glob) POSIX sh 只能用 =
大小寫轉換 ${var^^} / ${var,,} ${(U)var} / ${(L)var} Bash/Zsh 特殊功能
字串切割 ${var%pattern} / ${var#pattern} 支援更多:${var^^} (大寫) 支援更多::${(U)var} POSIX 只有 % #
printf / echo printf 標準,echo 不一定支援 -e echo -e 可用 echo -e 可能無效,用 print 建議用 printf
測試檔案 [ -f file ] 相同 相同
正則比對 [[ string =~ regex ]] [[ string =~ regex ]] 但 regex 語法不同 POSIX sh 無 regex 功能
展開 (globbing) 基本 * ? [ ] shopt -s globstar Zsh 預設更強大 (e.g. **/*.txt) Zsh glob 功能最強
補全 (tab) bash-completion 內建強大補全 互動環境差異,不影響 script
錯誤處理 set -e 相同,加強版 set -o pipefail 相同 pipefail 不是 POSIX
信號處理 trap 'cmd' INT TERM 相同 相同
source 檔案 . file source file / . file source file / . file POSIX sh 用 .
目錄堆疊 pushd / popd pushd / popd POSIX sh 無目錄堆疊功能
互動功能 readline、history、completion history、completion、prompt customization Zsh 提供最強互動功能
local / typeset local / typeset local / typeset POSIX sh 不支援函數內局部變數

  • sh
    • POSIX,相容性最好,但功能有限。
    • 如果要製作跨平台的 script,就使用標準的 POSIX 語法
  • bash
    • 增強 POSIX,支援陣列、關聯陣列、[[ ]]、brace expansion、Bashisms。
    • 一般在 mac/linux,可使用 bash script
  • zsh
    • 幾乎包含 Bash 功能,互動功能更強(prompt、補全、glob、history)
    • 陣列索引從 1 開始,部分語法行為不同。
    • 使用者互動操作時,可使用 zsh

2026/03/02

zsh

在 RockyLinux 測試 zsh。目前比較常見的,還是使用 bash,如果要測試 zsh,需要另外安裝。

安裝

dnf -y install zsh

修改預設的 shell

chsh -s /bin/zsh

基本的設定檔

可直接跳到下面的 Oh My Zsh

# 1. 基本環境
export LANG=en_US.UTF-8
export EDITOR=vim
export PATH="/usr/local/bin:$PATH"

# 2. Prompt 設定
PROMPT='[%*]%n@%m %~$ '

# 3. Alias 常用指令
alias ll='ls -lh'
alias la='ls -la'
alias ..='cd ..'
alias gs='git status'

# 4. 歷史設定
HISTSIZE=5000
SAVEHIST=5000
HISTFILE=~/.zsh_history
setopt share_history      # 多個 zsh session 共享歷史

# 5. 自動補全 & 修正
autoload -Uz compinit && compinit
setopt correct            # 拼字錯誤自動建議
setopt autocd             # 輸入資料夾名稱自動 cd
setopt nocaseglob         # 補全時忽略大小寫

# 6. 補全行為微調
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}'

功能有

  • 乾淨的 prompt[時間]使用者@主機 當前路徑$

  • 常用 aliasll, la, gs, ..

  • 歷史 → 儲存 5000 筆並共享不同視窗

  • 自動補全 → tab 補全,支援大小寫不敏感

  • 錯字修正 & auto cd → 輸入資料夾名稱會自動進去

Oh My Zsh

Oh My Zsh 是 zsh 常見的 plugin 管理工具,可安裝多個 plugin 擴充 zsh 的功能,但也要注意,載入越多 plugin 會讓 zsh 啟動變慢。

安裝

sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

常見的 theme 工具是 Powerlevel10k,但這邊不使用,直接用基本的 Prompt

修改 ~/.zshrc 增加以下這個部分,然後就能將一些不同用途的設定檔分開

for file in .zshrc_*;
do source $file;
done

增加常用 plugins

安裝

cd $ZSH/custom/plugins
# 安裝 zsh-autosuggestions
git clone https://github.com/zsh-users/zsh-autosuggestions.git

# 安裝 zsh-syntax-highlighting
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git

# zsh-completions
git clone https://github.com/zsh-users/zsh-completions

設定檔

~/.zshrc_basic

# 1. 指定 Oh My Zsh 安裝路徑
export ZSH="$HOME/.oh-my-zsh"

# 3. Plugins
# git           → git alias & 補全
# zsh-autosuggestions → 歷史自動建議
# zsh-syntax-highlighting → 命令語法高亮
# history-substring-search → 部分字串歷史搜索
# extract       → 快速解壓縮
plugins=(
  git
  zsh-autosuggestions
  zsh-syntax-highlighting
  zsh-completions
  history-substring-search
  extract
  colored-man-pages
  z
)

# 4. 啟動 Oh My Zsh
source $ZSH/oh-my-zsh.sh

# 5. PATH & 編輯器
export PATH="/usr/local/bin:$PATH"
export EDITOR=vim
export LANG=en_US.UTF-8

# 6. Alias 常用指令
alias ll='ls -lh'
#alias la='ls -A'
alias la='ls -la'
alias ..='cd ..'
alias gs='git status'
alias gp='git push'
alias gd='git diff'

# 7. 歷史設定
HISTSIZE=5000
SAVEHIST=5000
HISTFILE=~/.zsh_history
setopt share_history      # 多個 zsh session 共享歷史

# 8. 自動補全 & 行為優化
autoload -Uz compinit && compinit
setopt correct            # 拼字錯誤自動建議
setopt autocd             # 輸入資料夾名稱自動 cd
setopt nocaseglob         # 補全大小寫不敏感
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}'

另外做一個 ~/.zshrc_prompt

#PROMPT='%F{green}[%D{%H:%M}]%f%F{yellow}%n@%m%f %F{blue}%~%f$ '
#PROMPT='%F{green}[%D{%H:%M}]%f%F{yellow}%n@%m%f %F{blue}%~%f${git_prompt_info} $ '
autoload -Uz vcs_info
precmd() { vcs_info }
zstyle ':vcs_info:git:*' formats '(%b)'

PROMPT='%F{blue}[%D{%H:%M}]%f%F{green}%n@%m%f %F{magenta}%~%f${vcs_info_msg_0_} $ '

增加功能,區分本地跟遠端的 ssh

Zsh / Bash 都可以靠環境變數來判斷:

  • 本地登入 -> SSH_CONNECTION / SSH_TTY 不存在

  • 遠端登入 (SSH) -> SSH_CONNECTIONSSH_TTY 存在

if [[ -n "$SSH_CONNECTION" ]]; then
  HOST_STYLE="%F{red}@%m%f(ssh)"
else
  HOST_STYLE="%F{yellow}@%m%f"
fi

PROMPT='%F{blue}[%D{%H:%M}]%f%F{green}%n%f'"$HOST_STYLE"' %F{magenta}%~%f${vcs_info_msg_0_} $ '

重新登入後,就可以使用 zsh

extract plugin

extract 定義了一個名為 extract 的函數,用於解壓縮你傳遞給它的檔案,並支援多種檔案類型。

使用方式為 extract 檔案名稱

不需要知道具體的解壓縮命令,只需執行 extract 就可以解壓大部分常見檔案,直接輸入 x 檔案名稱 也可以

z plugin

Usage: z [OPTION]... [ARGUMENT]
Jump to a directory that you have visited frequently or recently, or a bit of both, based on the partial
string ARGUMENT.

With no ARGUMENT, list the directory history in ascending rank.

  --add Add a directory to the database
  -c    Only match subdirectories of the current directory
  -e    Echo the best match without going to it
  -h    Display this help and exit
  -l    List all matches without going to them
  -r    Match by rank
  -t    Match by recent access
  -x    Remove a directory from the database (by default, the current directory)
  -xR   Remove a directory and its subdirectories from the database (by default, the current directory)

autojump

z 是用 shell script 實作,autojump 是 python,z 的速度比較快,且內建於 zsh。一般建議就直接使用 z,如果要使用 autojump,需要做另外的設定。

安裝

cd $ZSH/custom/plugins

# 安裝 autojump
git clone https://github.com/wting/autojump.git

注意 autojump 需要另外安裝套件

dnf -y install autojump

因為 autojump 只有提供 bash 版本的初始化 script

/usr/share/autojump/autojump.bash

現在要使用 zsh,故要另外產生一個 zsh 版本的初始化

到 autojump 原始的 github 網頁取得 autojump.zsh

把 autojump.zsh 放到 /usr/share/autojump/autojump.zsh

修改設定檔

plugins=(
  autojump
)

# Autojump 初始化
# 確認 autojump 安裝後再 source
if [ -f /usr/share/autojump/autojump.zsh ]; then
  source /usr/share/autojump/autojump.zsh
fi
# 任意切換目錄
cd ~/download/dir1
cd ~/download/dir2

# 檢查 autojump
j -s

# autojump
j dir1

2026/02/09

Cytoscape.js

Cytoscape.js 是一個處理資料視覺化的 javascript library,當我們要對資料關係進行可視化顯示時,例如社交網路關係或網路拓樸圖時,Cytoscape.js 是個不錯的選擇。

Cytoscape 和 Cytoscape.js 是兩個完全獨立不同的軟體

  • Cytoscape

    • 使用 Java 語言編寫的用於網絡可視化的桌面應用程序

    • 需要安裝 Java SDK 才能使用

    • 用於大型網絡分析和可視化的高性能應用程序

  • Cytoscape.js

    • 用於網絡可視化的 javascript library,本身不是一個完整的 Web Application

    • 可以在大多數瀏覽器上使用

    • 不需要 plugin 即可運行

    • 需要編寫程式來建構 Web Application

    • 支援 Extensions

    • 基於 CSS 將資料映射到元件屬性

sample1

建立一個 圓形排列 的 4 個節點 (A, B, C, D),節點之間有箭頭連線,點擊節點會有事件

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="UTF-8">
    <title>test1</title>
    <script src="https://unpkg.com/cytoscape/dist/cytoscape.min.js"></script>
    <style>
        #cy {
            width: 800px;
            height: 600px;
            border: 1px solid #ccc;
            display: block;
        }
    </style>
</head>
<body>
    <h2>test1</h2>
    <div id="cy"></div>

    <script>
        const cy = cytoscape({
            container: document.getElementById('cy'),

            elements: [
                { data: { id: 'a', label: '節點 A' } },
                { data: { id: 'b', label: '節點 B' } },
                { data: { id: 'c', label: '節點 C' } },
                { data: { id: 'd', label: '節點 D' } },
                { data: { id: 'ab', source: 'a', target: 'b' } },
                { data: { id: 'bc', source: 'b', target: 'c' } },
                { data: { id: 'cd', source: 'c', target: 'd' } },
                { data: { id: 'da', source: 'd', target: 'a' } }
            ],

            style: [
                {
                    selector: 'node',
                    style: {
                        'background-color': '#0074D9',
                        'label': 'data(label)',
                        'color': '#fff',
                        'text-valign': 'center',
                        'text-outline-width': 2,
                        'text-outline-color': '#0074D9'
                    }
                },
                {
                    selector: 'edge',
                    style: {
                        'width': 3,
                        'line-color': '#AAAAAA',
                        'target-arrow-color': '#AAAAAA',
                        'target-arrow-shape': 'triangle',
                        'curve-style': 'bezier',
                    }
                }
            ],

            layout: {
                name: 'circle'
            }
        });

        // 點擊事件
        cy.on('tap', 'node', function(evt) {
            let node = evt.target;
            console.log('你點了節點: ' + node.id());
        });
    </script>
</body>
</html>

sample2

流程圖,layout 調整為 dagre extension。

使用時要引用 dagre library 及 Cytoscape.js 的 extension

dagre 正是 Cytoscape.js 常用來畫flowchart或 directed graph 的 layout。適合做 flowchart, network topology, workflow

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="UTF-8">
    <title>Cytoscape.js Flowchart</title>
    <script src="https://unpkg.com/cytoscape/dist/cytoscape.min.js"></script>
    <script src="https://unpkg.com/dagre/dist/dagre.min.js"></script>
    <script src="https://unpkg.com/cytoscape-dagre/cytoscape-dagre.js"></script>
    <style>
        #cy {
            width: 800px;
            height: 600px;
            border: 1px solid #ccc;
        }
    </style>
</head>
<body>
    <h2>flowchart</h2>
    <div id="cy"></div>

    <script>
        cytoscape.use(cytoscapeDagre);

        const cy = cytoscape({
            container: document.getElementById('cy'),

            elements: [
                { data: { id: 'start', label: '開始' } },
                { data: { id: 'step1', label: '步驟 1' } },
                { data: { id: 'step2', label: '步驟 2' } },
                { data: { id: 'decision', label: '判斷 ?' } },
                { data: { id: 'end', label: '結束' } },
                { data: { id: 's1', source: 'start', target: 'step1' } },
                { data: { id: 's2', source: 'step1', target: 'step2' } },
                { data: { id: 's3', source: 'step2', target: 'decision' } },
                { data: { id: 's4', source: 'decision', target: 'end' } },
                { data: { id: 's5', source: 'decision', target: 'step1' } }
            ],

            style: [
                {
                    selector: 'node',
                    style: {
                        'shape': 'round-rectangle',
                        'background-color': '#28a745',
                        'label': 'data(label)',
                        'color': '#fff',
                        'text-valign': 'center',
                        'text-outline-width': 2,
                        'text-outline-color': '#28a745'
                    }
                },
                {
                    selector: 'node[id="decision"]',
                    style: {
                        'shape': 'diamond',
                        'background-color': '#ffc107',
                        'text-outline-color': '#ffc107'
                    }
                },
                {
                    selector: 'edge',
                    style: {
                        'width': 2,
                        'line-color': '#555',
                        'target-arrow-color': '#555',
                        'target-arrow-shape': 'triangle',
                        'curve-style': 'bezier',
                    }
                }
            ],

            layout: {
                name: 'dagre',
                // rankDir: 'TB'  // top-to-bottom
                rankDir: 'LR' // 由左到右 排列
            }
        });
    </script>
</body>
</html>

sample3

鐵路模擬,增加火車在鐵軌上移動的動畫

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="UTF-8">
    <title>Cytoscape.js Railway</title>
    <script src="https://unpkg.com/cytoscape/dist/cytoscape.min.js"></script>
    <style>
        #cy {
            width: 800px;
            height: 600px;
            border: 1px solid #ccc;
        }
    </style>
</head>
<body>
    <h2>Railway</h2>
    <div id="cy"></div>

    <script>
        const cy = cytoscape({
            container: document.getElementById('cy'),

            elements: [
                { data: { id: 'station1', label: '車站 1' } },
                { data: { id: 'station2', label: '車站 2' } },
                { data: { id: 'station3', label: '車站 3' } },
                { data: { id: 'checkpoint4', label: '檢查點 4' } },
                { data: { id: 'checkpoint5', label: '檢查點 5' } },
                { data: { id: 'checkpoint6', label: '檢查點 6' } },
                { data: { id: 's1', source: 'station1', target: 'station2' } },
                { data: { id: 's2', source: 'station2', target: 'station3' } },
                { data: { id: 's3', source: 'station2', target: 'checkpoint4' } },
                { data: { id: 's4', source: 'checkpoint4', target: 'checkpoint5' } },
                { data: { id: 's5', source: 'checkpoint5', target: 'station3' } },
                { data: { id: 's6', source: 'checkpoint5', target: 'checkpoint6' } },

                // 列車節點
                { data: { id: 'train1', label: '🚆' }, classes: 'train' }
            ],

            style: [
                {
                    selector: 'node',
                    style: {
                        'shape': 'ellipse',
                        'background-color': '#0074D9',
                        'label': 'data(label)',
                        'color': '#fff',
                        'text-valign': 'center',
                        'text-outline-width': 2,
                        'text-outline-color': '#0074D9'
                    }
                },
                {
                    selector: 'node[id^="station"]',
                    style: {
                        'shape': 'round-rectangle',
                        'background-color': '#17a2b8',
                        'text-outline-color': '#17a2b8'
                    }
                },
                {
                    selector: 'node.train',
                    style: {
                        'background-color': 'red',
                        'shape': 'ellipse',
                        'label': 'data(label)',
                        'font-size': 24,
                        'width': 30,
                        'height': 30
                    }
                },
                {
                    selector: 'edge',
                    style: {
                        'width': 2,
                        'line-color': '#555',
                        'target-arrow-color': '#555',
                        'target-arrow-shape': 'triangle'
                    }
                }
            ],

            layout: {
                name: 'breadthfirst',
                directed: true,
                padding: 20
            }
        });

        function moveAlongEdge(train, fromNode, toNode, duration, callback) {
            const start = fromNode.position();
            const end = toNode.position();
            const startTime = performance.now();

            function animate(now) {
                const elapsed = now - startTime;
                const t = Math.min(elapsed / duration, 1); // 0~1
                const x = start.x + (end.x - start.x) * t;
                const y = start.y + (end.y - start.y) * t;
                train.position({ x, y });

                if (t < 1) {
                    requestAnimationFrame(animate);
                } else if (callback) {
                    callback();
                }
            }

            requestAnimationFrame(animate);
        }

        function moveTrain(path) {
            let i = 0;
            const train = cy.getElementById('train1');

            function step() {
                if (i >= path.length - 1) return;
                const fromNode = cy.getElementById(path[i]);
                const toNode = cy.getElementById(path[i + 1]);

                moveAlongEdge(train, fromNode, toNode, 2000, () => {
                    i++;
                    step();
                });
            }

            step();
        }

        // 定義路徑
        const route = ['station1', 'station2', 'checkpoint4', 'checkpoint5', 'station3'];

        // 初始化列車位置
        cy.getElementById('train1').position(cy.getElementById(route[0]).position());

        // 2 秒後啟動列車
        setTimeout(() => moveTrain(route), 2000);
    </script>
</body>
</html>