🍴

手に馴染む開発ツールを揃えて新年を迎える

に公開

概要

今年のうちに開発を快適に進めるために、使っているツールを少しでも使いやすくし、今はあまり使っていないツールを手放すようなことをやりました。

いいなと思ったら同じことでなくていいので、ツールの手入れをしてみてほしいです🦧️

やったこと

  • シンプルなDotfilesマネージャの作成
  • スニペットツールの見直しとコマンドチートシートの作成

シンプルなDotfilesマネージャの作成

詳細はあとでつらつら書きます。

作成したもの

配置したいパス -> 実体ファイルのシンボリックリンクを作成するスクリプトを組みました。
ありがちだと思います。

高機能なものがほしければchezmoiを使うとよいと思いますが、シンプルなもので良かったので簡単なスクリプトを組みました。

操作手順のイメージはだいたいこんな感じ

  1. GhosttyのConfigファイルをGitのリポジトリで管理する
  2. Ghosttyのシンボリックリンク情報を記載したファイルを作成する
  3. make setup でコマンドを動かす
  4. Ghosttyを選択してシンボリックリンクを作成する

スクリプトは次のように動きます。

=== Dotfiles Setup ===
Select applications to setup:

1) cheaty
2) ghostty
3) git
4) gitleaks
5) zsh
Choose an option (number or 'q' to quit): 2

[Plan] Link to be created:
GhosttyのConfigがあるパス -> /Documents/dokodemo-drawer/drawer/ghostty/config

[Before Execution] Existing symlink found:
今、存在するシンボリックリンクが表示される
Overwrite with Plan? (y/N): y

✓ ghostty config: Linked (GhosttyのConfigがあるパス -> /Documents/dokodemo-drawer/drawer/ghostty/config)

ディレクトリ構造

drawer配下に管理したいconfigファイルやdotfileを設置します。

dokodemo-drawer
├── drawer
│   ├── ghostty
│   │   ├── config // 管理対象のConfigファイル
│   │   └── setup.sh // シンボリックリンク作成関数呼び出し(引数指定)
│   ├── git
│   │   ├── .git_aliases
│   │   └── setup.sh
│   ├── gitleaks
│   │   ├── pre-commit
│   │   └── setup.sh
│   └── zsh
│       ├── .zsh_aliases
│       └── setup.sh
├── Makefile
├── README.md
├── setup.sh // シンボリックリンク作成対象選択
└── symlink.sh //シンボリックリンク作成関数

スクリプト

setup.sh(リポジトリルート)
#!/bin/bash
set -euo pipefail

# リポジトリルートの特定
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export REPO_ROOT="$SCRIPT_DIR"

# 共通関数の読み込み
source "$SCRIPT_DIR/symlink.sh"

# APPS+=(ghostty git zsh)のように配列に追加するための準備
declare -a APPS=()

if [ ! -d "$REPO_ROOT/drawer" ]; then
    # drawerディレクトリが存在しない場合は終了
    echo -e "${RED}Error: drawer directory not found${NC}"
    exit 1
fi

for app_dir in "$REPO_ROOT/drawer"/*; do
    if [ ! -d "$app_dir" ]; then
        # ディレクトリでない場合はスキップ
        continue
    fi

    app_name=$(basename "$app_dir")
    setup_script="$app_dir/setup.sh"

    if [ ! -f "$setup_script" ]; then
        # setup.shが存在しない場合はスキップ
        echo -e "${YELLOW}Warning: setup.sh not found for $app_name${NC}"
        continue
    fi

    source "$setup_script"
    APPS+=("$app_name")
done

# メインメニュー
main() {
    echo -e "${BLUE}=== Dotfiles Setup ===${NC}"
    echo "Select applications to setup:"
    echo

    if [ ${#APPS[@]} -eq 0 ]; then
        echo -e "${RED}No applications found in drawer/${NC}"
        exit 1
    fi

    PS3="Choose an option (number or 'q' to quit): "

    select opt in "${APPS[@]}"; do
        # 'q'または'Q'で終了
        if [[ "$REPLY" =~ ^[qQ]$ ]]; then
            echo "Exiting..."
            break
        fi

        # 選択されたアプリが有効かチェック
        valid_opt=false
        for app in "${APPS[@]}"; do
            if [ "$app" == "$opt" ]; then
                valid_opt=true
                break
            fi
        done

        if [ "$valid_opt" = false ]; then
            echo -e "${RED}Invalid option${NC}"
            continue
        fi

        # setup_<app_name> 関数を動的に呼び出し
        func_name="setup_${opt}"
        if ! type "$func_name" > /dev/null 2>&1; then
            echo -e "${RED}Error: Function $func_name not found${NC}"
            continue
        fi

        $func_name
        echo
    done

    echo -e "${GREEN}Done!${NC}"
}

main
symlink.sh
#!/bin/bash

# Define colors
export GREEN='\033[0;32m'
export YELLOW='\033[1;33m'
export RED='\033[0;31m'
export BLUE='\033[0;34m'
export NC='\033[0m'

# シンボリックリンクを作成
create_symlink() {
    local source=$1
    local dest=$2
    local app_name=$3

    # ソースファイルの存在確認
    if [ ! -f "$source" ]; then
        echo -e "${RED}$app_name: Source file not found at $source${NC}"
        return 1
    fi

    echo
    echo -e "${BLUE}[Plan] Link to be created:${NC}"
    echo -e "$dest -> $source"

    # リンク先が実体ファイルやディレクトリとして存在する場合(シンボリックリンクでない)
    if [ -e "$dest" ] && [ ! -L "$dest" ]; then
        local type="File"
        if [ -d "$dest" ]; then
            type="Directory"
        fi
        echo -e "${YELLOW}$app_name: Skipped. Existing $type found at $dest${NC}"
        ls -ldG "$dest"
        return 0
    fi

    # 既にシンボリックリンクが存在する場合
    if [ -L "$dest" ]; then
        echo
        echo -e "${YELLOW}[Before Execution] Existing symlink found:${NC}"
        ls -ldG "$dest"
        echo
        read -p "Overwrite with Plan? (y/N): " -n 1 -r
        echo
        if [[ ! $REPLY =~ ^[Yy]$ ]]; then
            echo -e "${YELLOW}Skipped $app_name${NC}"
            return 0
        fi
        rm "$dest"
    fi

    # ディレクトリ作成
    local parent_dir="$(dirname "$dest")"
    if [ ! -d "$parent_dir" ]; then
        echo -e "${BLUE}$app_name: Parent directory '$parent_dir' does not exist.${NC}"
        read -p "Create it? (y/N): " -n 1 -r
        echo
        if [[ ! $REPLY =~ ^[Yy]$ ]]; then
            echo -e "${YELLOW}Skipped $app_name${NC}"
            return 0
        fi
        mkdir -p "$parent_dir"
    fi

    # リンク作成
    ln -s "$source" "$dest"
    echo -e "${GREEN}$app_name: Linked ($dest -> $source)${NC}"
}
setup.sh
#!/bin/bash

setup_ghostty() {
    local source="$REPO_ROOT/drawer/ghostty/config"
    local dest="$HOME/Library/Application Support/com.mitchellh.ghostty/config"

    create_symlink "$source" "$dest" "ghostty config"
}

工夫した点

dotfileはすべてを管理しないようにした

特定のエイリアスなどをGit管理したいだけなので、例えば、.gitconfigであれば.git_aliasesを作成してこれを読み込みます。.gitconfig自体は管理しないようにしました。

[include]
    path = ~/.git_aliases

zshも似たようなことをして全体は管理していません。

gitleaksを導入した

gitleaksを導入して、秘匿情報をアップロードしないようにしました。ローカル開発ではbrewを使ってinstallし、Git Hooksを使ってコミットさせないように設定しました。
pre-commitというファイルにコミット前の実行スクリプトを書いて、make setupgitleaksを選択するだけで、.git/hooks配下にリンクできるようにしました。

setup.sh
#!/bin/bash
set -euo pipefail

setup_gitleaks() {
    # Check if gitleaks is installed
    if ! command -v gitleaks &> /dev/null; then
        echo -e "${YELLOW}Warning: gitleaks is not installed.${NC}"
        echo "Please install 'gitleaks' manually to enable pre-commit checks."
        return 0
    fi

    # Link pre-commit hook
    create_symlink "$REPO_ROOT/drawer/gitleaks/pre-commit" "$REPO_ROOT/.git/hooks/pre-commit" "gitleaks"
}
pre-commit
#!/bin/bash
# Simply run gitleaks protection on staged files
# If gitleaks is not installed, this will fail and block the commit.
gitleaks protect --staged -v

省略しますが、workflowも作成しました。

シンボリックリンクの作成と更新以外はしないようにした

コアロジックはシンボリックリンクの作成と更新だけです。それ以上をやりたくなったら工数がかかり、他のことをやれなくなるし、年を越してしまうのでやめました。

今のチームになってから、Simple Made EasyDo One Thing and Do It Wellということをリーダーが何度か口にしていたのでそれをやってみる練習だとも思っています。

スニペットツールの見直しとコマンドチートシートの作成

Clipyの見直し

Clipyには大変お世話になり、これからもMacを導入する人には必ずおすすめすると思います。
しかし、Raycastを使っていることもあり、ClipyとRaycastでスニペットがちらばっている状態でしたので、ClipyをアンインストールしてRaycastに寄せるようにしました。

スニペットはとりあえずで保存をしがちで、使わないものがたくさん放置されている状態でした。
スニペットになんでも保存するのではなく、以下のような選択肢から適切な場所を選択することが重要だと再認識しました。

  • dotfilesマネージャ
  • パスワードマネージャ
  • 辞書ツール
  • スニペットツール

例えば、社員番号をスニペットツールで管理していたのですが、辞書ツールに「社員番号」という読みで登録しており、こちらをよく使っていたので自分にとって適切なのはこちらだったのでスニペットツールからは削除しました。

.envファイルも1passwordを使ってれば、こちらから参照できるようになったので、dotfilesマネージャでの管理よりこちらがいいと思っています。
https://dev.classmethod.jp/articles/1password-environments-env-management/

コマンドのチートシートはスニペットツールで管理するべきか

「コマンドのチートシートはコマンドから呼び出せるべきではないか」
それを検証したくて、以下のようなスクリプトをつくりました。
$HOME/bin/cheatyと配置して、aliasで以下のようにchコマンドで呼び出します。

alias ch='print -z "$(cheaty)"'

※Zsh

  1. カテゴリを選択
  2. コマンドを選択
  3. コマンドが入力される

スクリプト

cheaty.sh
#!/bin/zsh

# 1. パス設定 (スクリプトと同じ場所にJSONがある前提)
local SCRIPT_DIR="${${(%):-%N}:A:h}"
local MAIN_FILE="$SCRIPT_DIR/commands.json"
local LOCAL_FILE="$SCRIPT_DIR/commands.local.json"

# 2. メインファイルが無いと始まらないのでチェック
if [ ! -f "$MAIN_FILE" ]; then
    echo "Error: $MAIN_FILE not found." >&2
    return 1
fi

# 3. 読み込むファイルを決定(ローカルがあればリストに加える)
local targets=("$MAIN_FILE")
[ -f "$LOCAL_FILE" ] && targets+=("$LOCAL_FILE")

# 4. JSONを結合 (後勝ち=上書き)
# 配列 targets を展開して jq に渡すだけ。シンプルです。
local merged_json=$(jq -s 'add' "${targets[@]}")

while true; do
    # カテゴリ選択
    local category=$(echo "$merged_json" | jq -r 'keys[]' | \
        fzf --height 12 --layout=reverse --border --prompt="📂 Category > ")

    [ -z "$category" ] && break

    # コマンド選択
    local cmd=$(echo "$merged_json" | jq -r --arg cat "$category" '.[$cat][]' | \
        fzf --height 12 --layout=reverse --border --prompt="📝 Command ($category) > ")

    [ -z "$cmd" ] && continue

    # 結果を出力して終了
    print "$cmd"
    break
done

設定ファイル Git管理対象

commands.json
{
    "Docker": [
        "docker system prune -a",
        "docker-compose up -d"
    ],
    "Git": [
        "git log --oneline --graph --all",
        "git pull origin main",
        "git status",
        "git reset --soft HEAD~1"
    ],
    "Gitleaks": [
        "gitleaks detect --verbose"
    ],
    "Zsh": [
        "source ~/.zshrc"
    ]
}

ローカル専用設定ファイル Git管理しない

commands.local.json
{
    "System": [
        "brew update && brew upgrade",
        "ifconfig | grep \"inet \""
    ]
}

少しつかってみた感想

使いやすいと思いました。
fzfを使って絞り込めるので、すぐに使いたいものにたどり着けます。
Dockerコンテナに入って操作するときには使えないので、スニペットツールの方が便利だと思いました。

まとめ

Ghosttyをインストールしたのは今年の春で、会社のエンジニアに紹介してもらい、少し触ってみてから放置でした。幽霊部員。設定がシンプルで、AIの補完などがないことが逆によく、シンプルに使えることが魅力的だと思っています。最近は、Ghosttyをメインで使い、他のターミナルエミュレータはアンインストールしました。高機能なものが手に馴染むものとは限らないので、シンプルな機能でも、自分にとって必要な道具を使っていきたいです。

AIを使えばシンプルなものを自作することが容易になったのも嬉しいことです。自分で作るということは車輪の再発明になる可能性もありますが、ツールを作ることはツールをより良く使うための知識になると思いました。扱うツールに対する解像度があがる感じがします。

なによりツールの手入れは楽しいので、年末に限らず定期でやっていこうと思います🔧

NE株式会社の開発ブログ

Discussion