ハイパーマッスルエンジニア

Vim、ShellScriptについてよく書く

Mac専用sipsコマンドで、画像に枠線をサクッとつける

f:id:rasukarusan:20210707141514p:plain
Mac標準搭載のコマンドでsipsコマンドがある。画像情報を取得したり付与したりできる。
画像のコマンドといえばimagemagickだが、imagemagickよりも少ないオプションで、かつ直感的に実行できるのがsipsコマンドの良いところ。brew等で別途インストールしなくて済むのも良い。

sipsコマンドを知る上でめちゃくちゃ参考になるサイト

基本的な使い方はここ。

Macのターミナルで簡単に画像処理できるsipsの使い方 - Qiita

こちらはOCRツールのtesseractとsipsを組み合わせて中々面白いことをしている。

Tesseractでデスクトップの一部にOCRをかける - Qiita

sipsで画像に枠線をつける

sips -p ${height} ${width} --padColor ${color} ${imagePath} -o ${newImagePath}

# 元画像320x320, 枠線の太さを20に設定
sips -p 340 340 --padColor 000000 demo.png -o border_demo.png

f:id:rasukarusan:20210707140627j:plain
元画像

f:id:rasukarusan:20210707140637j:plain
#000000の枠線太さ20の画像

元画像のサイズを知っておく必要があるので、それもsipsコマンドで取得するようにすると汎用的になる。
sipsコマンドで画像の幅、高さを指定するには-gオプションを利用する

# 幅を取得
sips -g pixelWidth demo.jpg

# 高さを取得
sips -g pixelHeight demo.jpg

add_border.sh

#!/usr/bin/env bash

main() {
  local image=$1
  local color=${2:-a0a8a9}
  local borderWeight=${3:-10}
  local width=$(sips -g pixelWidth $1 | awk -F ' ' '{print $2}')
  local height=$(sips -g pixelHeight $1 | awk -F ' ' '{print $2}')
  local borderWidth=$(expr $width + $borderWeight)
  local borderHeight=$(expr $height + $borderWeight) 
  sips -p $borderHeight $borderWidth --padColor $color $image -o border_${image}
}
main

使い方

sh add_border.sh demo.jpg

# 枠線色#000000、太さ20
sh add_border.sh demo.jpg 000000 20

終わり

地味に枠線ほしいときあるから、きっと役に立つ。

Sequel AceをApplescriptで操作する

f:id:rasukarusan:20210627222634p:plain
 

以前Sequel ProをApplescriptで操作するのはやっていたが、昨今はSequel Aceを使うようになった。
Sequel ProとAceではFavorite.plistのPATHやUIの配置などがいくつか異なっていたため、それをまとめたい。

スクリプト

まずどういったスクリプトなのかを載せておく。お気に入りリストを取得してfzfで選択し、接続するというスクリプト。

GIFはSequel Proだが同じことがSequel Aceでも可能

ソース

#!/bin/sh

#
# Sequel Aceで指定した接続を開く
# 引数にはSequelProの「お気に入り」の行番号を示すインデックスが入る
#
function run_sequel_pro() {
local app='Sequel Ace'
osascript -- - "$@" << EOF
on run argv
tell application "${app}"
    activate
    delay 0.5
    tell application "System Events"
        tell process "${app}"
            set frontmost to true
            delay 0.5
            repeat with i from 1 to (count argv)
                keystroke "t" using {command down}
                tell window "${app}"
                    delay 0.5
                    tell outline 1 of scroll area 1 of splitter group 1 of window "${app}" of application process "${app}" of application "System Events"
                        # row 1は「クイック接続」、row 2は「お気に入り」の行なので実質一番上はrow 3となる
                        set _row_index to (item i of argv as number) + 2
                        select row _row_index
                    end tell
                    tell scroll area 2 of splitter group 1 of window "${app}" of application process "${app}" of application "System Events"
                        click button "Connect"
                    end tell
                end tell
            end repeat
        end tell
    end tell
end tell
end run
EOF
}

function main() {
  local favorites=$(plutil -convert json ~/Library/Containers/com.sequel-ace.sequel-ace/Data/Library/Application\ Support/Sequel\ Ace/Data/Favorites.plist -o - | jq -r '."Favorites Root".Children[].name')
    local targets=($(echo "${favorites}" | fzf))
    local rows=()
    for target in ${targets[@]}; do
        echo $target
        local row=$(echo "${favorites}" | grep -n ${target} | cut -d ':' -f 1)
        rows=(${rows[@]} $row)
    done
    [ ${#rows[@]} -eq 0 ] && return 130
    run_sequel_pro ${rows[@]} >/dev/null
}

main

Sequel Pro と Ace で変わったこと

  • Favorite.plistのPATH
  • UI Elementの指定方法

Favorite.plistのPATH

お気に入り一覧はFavorite.plistというファイルに載っている。Sequel Proでは

~/Library/Application\ Support/Sequel\ Pro/Data/Favorites.plist

にあったが、Sequel Aceでは下記のPATHに変わっている。

~/Library/Containers/com.sequel-ace.sequel-ace/Data/Library/Application\ Support/Sequel\ Ace/Data/Favorites.plist 

これは「Sequel Ace | MySQL/MariaDB database management for macOS」にも載っている。
ちなみにパスワードが知りたい場合はキーチェーンに保存されている。キーチェーンを開いて検索で「sequel」と打てば出てくるはず。

UI Elements

Sequel Proではお気に入りリストの行を選択するのに下記のようにアクセスしていた。

tell outline 1 of scroll area 1 of splitter group 1 of group 2 of window "Sequel Pro" of application process "Sequel Pro" of application "System Events"

Sequel Aceではgroup 2 ofがなくなっていた。

- tell outline 1 of scroll area 1 of splitter group 1 of group 2 of window "Sequel Pro" of application process "Sequel Pro" of application "System Events"
+ tell outline 1 of scroll area 1 of splitter group 1 of window "Sequel Ace" of application process "Sequel Ace" of application "System Events"

終わり

以上。Sequel AceもApplescriptで操作できてよかった。

gif動画から指定のフレーム(最初・最後・最後から2番目など)を抜き出す

f:id:rasukarusan:20210418171557p:plain

convertコマンドで可能。とりあえずbrewでインストールしましょう。

# imagemagickをインストールするとconvertコマンドが使えるようになる
brew install imagemagick

指定のフレームを抜き出す

最初

convert 'neko.gif[0]' first.png

最後

convert 'neko.gif[-1]' last.png

最後から2番目

convert 'neko.gif[-2]' pre-last.png

終わり

gif動画の始まりと終わりを区別するために、最初と最後のフレームにSTARTENDの文字列を合成しようと思って調べたら出てきた。たぶん文字列の合成もconvertコマンドで出来るから、ImageMagickマジ便利。

今更ながらgit-ftp便利すぎた

f:id:rasukarusan:20210226164209p:plain
とりあえず必要なことをババっと書いておく

インストール

brew install git-ftp

もしくは、リポジトリにgit-ftpの実行ファイルがあるからcloneしてきて使ってもいい。

git clone https://github.com/git-ftp/git-ftp
cd git-ftp
./git-ftp

初期設定

git config git-ftp.url "ftp://FTPホスト/指定ディレクトリ"
git config git-ftp.user "FTPユーザー名"
git config git-ftp.password "FTPパスワード"
git config git-ftp.syncroot pushしたいディレクトリ

ignore設定

.git-ftp-ignoreに書き込んでいく。書き方は.gitignoreと同じ。

touch .git-ftp-ignore

サーバーにPUSH

git ftp init

初回のみinitが必要で、initをしたら自動的にpushされるので注意。
二度目以降はgit ftp pushでpushする。

git ftp push

staging, production環境で分けたい

git-ftp.<environment>で振り分け可能。

staging

git config git-ftp.stg.url "ftp://ステージングのFTPホスト/指定ディレクリ"
git config git-ftp.stg.user "ステージングのFTPユーザー"
git config git-ftp.stg.password "パスワード"
git config git-ftp.stg.syncroot pushしたいディレクトリ

production

git config git-ftp.prd.url "ftp://プロダクションのFTPホスト/指定ディレクリ"
git config git-ftp.prd.user "プロダクションのFTPユーザー"
git config git-ftp.prd.password "パスワード"
git config git-ftp.prd.syncroot pushしたいディレクトリ

確認

cat .git/config

staging, production環境で分けてPUSHする

-sで指定する

staging

git ftp init -s stg
git ftp push -s stg

production

git ftp init -s prd
git ftp push -s prd

ちなみに振り分け設定していない場合に、-sをつけずにinit等をしてもエラーとなる。

$ git ftp init
fatal: Remote host not set.

終わり

急遽Wordpressをすることになり、自前で用意したステージング用サーバーがFTPのみ利用可能で必要になった。
Wordpressのデプロイにはwordmoveが最適っぽいが、設定ファイルを書くのがだるすぎたので却下した。開発用サーバーとかはどうなってもいいから楽なやつがいい。

ターミナル上にwifi接続のQRを表示できるsdushantha/wifi-passwordがおもしろい

f:id:rasukarusan:20210128170048p:plain
github.com

今接続しているwifiのパスワード、またはQRコードをTermianl上に表示できるツール。
使用頻度は少ないかもしれないが、あったら地味に便利。

インストールはpipで可能。

python3 -m pip install --user wifi-password

実行

wifi-password --qrcode

f:id:rasukarusan:20210128154847p:plain:w500
実行するとキーチェーンの許可が求められる

ユーザー名、パスワードを入力するとQRコードがターミナル上に表示される。

f:id:rasukarusan:20210128154904j:plain:w500
なんとなく読み取られるのがアレなためモザイクをかけているが、実際はきれいなQRコードが表示される

このQRをiPhoneなどのカメラで読み取れば接続できる。
オプションで--imageをつければ画像として保存することも可能。ただもしかしたらpipで依存モジュールをインストールする必要があるかも。

元祖rauchg/wifi-passwordの拡張版

rauchg/wifi-passwordが元となっているツール。
Windows, Linux, Macのマルチプラットフォームに対応したのと、QRコードの機能追加されているのが今回のもの。 また、元祖はShellScriptで書かれているが、sdushantha/wifi-passwordはPythonで書かれている。ただ、パスワード取得の処理などはどちらも同じ。

どうやってパスワードを取得しているのか

macのsecurityコマンドを実行して取得している。

security find-generic-password -l ${ssid} -D 'AirPort network password' -w

securityコマンドについては下記2つの記事がわかりやすかった。あとman securityに色々書かれているので見ると楽しい。 yukidarake.hateblo.jp macromates.com

終わり

securityコマンドの存在を初めて知った。
今までトークンなどそれ用のファイルに書き込んで、shellなどから読み取っていたが、キーチェーンに登録すればファイルとして残さなくて済みそう。

Githhub Actionsをローカルで実行するnectos/actでcommand not foundが出たときの対処法

f:id:rasukarusan:20210127224510p:plain:w600
Github Actionsをローカル実行できるツールnektos/actの話。

act -P でimageを指定するも必ずnode:12.6-buster-slimで実行されてしまう

actの実行時の環境は、デフォルトではnode:12.6-buster-slimが選択されるが、このimageは最小限の構成なのでgitコマンド等が入っていない。 なので下記のようにcommand not foundと出てしまい、実行に失敗する。

$ act
WARN[0000] unable to get git repo: section "remote \"origin\"" does not exist
[Test Workflow/Run Git Commands] 🚀  Start image=node:12.6-buster-slim
...略
[Test Workflow/Run Git Commands] ⭐  Run execute command!
| /github/workflow/set_value_for_formula: line 1: git: command not found
[Test Workflow/Run Git Commands]   ❌  Failure - execute command!
Error: exit with `FAILURE`: 127

しかしactにはオプションが用意されていてact -Pで実行するimageが選択できる。

act -P ubuntu-18.04=nektos/act-environments-ubuntu:18.04

ただ上記のコマンドを実行しても選択されるimageがnode:12.6-buster-slimのままになってしまう。

f:id:rasukarusan:20210128010718p:plain
nektos/act-environments-ubuntu:18.04を指定しているのにnode:12.6-buster-slimが選択される

これを解決する。

原因:yamlのruns-on:ubuntu-latestとact -P ubuntu-18.04=...がちぐはぐだったから

.github/workflows/test-workflow.yml

name: Test Workflow
on:
  push:
    tags:
      - 'v*'
jobs:
  build:
    name: Run Git Commands
    runs-on: ubuntu-latest
    steps:
      - name: execute command!
        id: set_value_for_formula
        run: |
          git status

actコマンド

act -P ubuntu-18.04=nektos/act-environments-ubuntu:18.04

yamlファイルはruns-on: ubuntu-latestなのにコマンドがact -P ubuntu-18.04=...になっているとnode:12.6-buster-slimが選択されてしまう。

対処法

ymlファイルとコマンド実行をちゃんと合わせる。

- act -P ubuntu-18.04=nektos/act-environments-ubuntu:18.04
+ act -P ubuntu-latest=nektos/act-environments-ubuntu:18.04

公式のREADMEだとubuntu-18.04で書かれているため、そのままコピペするとハマってしまうかもしれない。

確認

f:id:rasukarusan:20210128011028p:plain

ちゃんと🚀 Start image=nektos/act-environments-ubuntu:18.04になっていますね。

Tips

actの実行前にdocker pull nektos/act-environments-ubuntu:18.04しておいたほうがいい

docker pullをせずにいきなりact -P ubuntu-latest=nektos/act-environments-ubuntu:18.04を実行しても問題ないが、初回実行時はimageをpullしてくるのでめちゃくちゃ時間がかかる上に進捗が見えないので止まったように見えてしまう。
docker pullなら進捗が見えるので、先にやっておいたほうが精神上良い。

docker pull nektos/act-environments-ubuntu:18.04

yamlのruns-onubuntu-18.04だったらgitコマンドが使えるnode:12-busterが選択される

jobs:
  build:
    name: Run Git Commands
    runs-on: ubuntu-18.04

としておけばactで実行するときslimよりちょっとリッチなnode:12-busterで実行される。

f:id:rasukarusan:20210127223032p:plain
gitコマンドが使えるぐらいにはリッチなnode:12-busterが選択される

READMEにはnode:12.6-buster-slimと書かれていたので、自分が間違っているのか更新し忘れなのかわからないが、一応プルリクを出しておいた。結果はまたここに書く。

github.com

Dockerのバージョンを上げないとそもそもactが実行できない

下記のエラーが出る場合、Dockerのバージョンを上げたら実行できるようになる。

$ act
[Test Workflow/printInputs] 🚀  Start image=node:12.6-buster-slim
[Test Workflow/printInputs]   🐳  docker run image=node:12.6-buster-slim entrypoint=["/usr/bin/tail" "-f" "/dev/null"] cmd=[]
[Test Workflow/printInputs]   🐳  docker cp src=/Users/tanakanaoto/Documents/github/gitblamer/. dst=/github/workspace
Error: error during connect: Post "http://%2Fvar%2Frun%2Fdocker.sock/v1.40/exec/dd9940466880af832e8b93ba9d2fbde450785bfac24ae2a73dd2cbd99aec5ce2/start": net/http: HTTP/1.x transport connection broken: unsupported transfer encoding: "identity"

github.com

f:id:rasukarusan:20210127223005p:plain
2.2.0.4→2.5.0.0にアップデートしたら実行できるようになった

はてなブログでgifの代わりにmp4で投稿するために、Github Actionsでgif→mp4変換を自動化した

f:id:rasukarusan:20210125191908p:plain:w500

gifの欠点

  • ファイルサイズがでかい → ページ読み込みが遅くなる
  • シークバーが表示されない → 一時停止、早送りができない

MP4にしてvideoタグで埋め込むといい

<video controls muted autoplay playsinline width="95%">
  <source src="mp4動画のURL">
</video>

gifの圧縮アルゴリズムはそこまで最適化されていないため、MP4などの動画フォーマットのほうがファイルサイズを小さくできるらしい。
またvideoタグで埋め込むのでシークバーが表示されるようになる。

こんな感じで再生できるようになる

gif→mp4の変換

コマンド。ffmpegがない場合brew install ffmpegでインストールできる。

ffmpeg -i org.gif -movflags faststart -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" video.mp4

いちいち変換するのが面倒くさいのでGithub Actionsする

gif撮ってpushしたらmp4動画の作成が終わっている、という感じにしたい。

github.com

workflow

下記を満たすようなstepを書いた。

  • gifが追加されたとき: mp4の作成
  • gifがリネームされたとき: 以前の名前で作られたmp4を削除し、新たな名前でmp4を作成
  • gifが削除されたとき: mp4の削除
  • gif以外のファイル: 対象外にする
# .github/workflows/convert.yml
name: Convert GIF To MP4

on:
  push:
    branches:
      - master
jobs:
  build:
    name: Convert GIF
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
        with:
          fetch-depth: 0

      - name: Setup ffmpeg
        uses: FedericoCarboni/setup-ffmpeg@v1-beta
        id: setup-ffmpeg

      - name: Convert
        run: |
          git show --pretty="format:" --name-status HEAD | grep '.gif$' | while read target; do
            read status gif <<< "$(echo $target | awk '{print $1,$NF}')"
            printf "\e[31m===========${gif}============\e[m\n"
            filename=$(basename $gif | sed 's/\.[^\.]*$//')
            dir=$(dirname $gif)
            mp4=${dir}/${filename}.mp4
            case "$status" in
              'A') # add
                echo 'add'
                [ ! -e $mp4 ] && ffmpeg -i $gif -movflags faststart -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" $mp4 </dev/null
                ;;
              [R]*) # rename
                echo 'rename'
                read pre_mp4 <<< "$(echo $target | awk '{print $2}' | sed 's/\.[^\.]*$/.mp4/' )"
                [ -e $pre_mp4 ] && git rm $pre_mp4
                [ ! -e $mp4 ] && ffmpeg -i $gif -movflags faststart -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" $mp4 </dev/null
                ;;
              'D') # delete
                echo 'delete'
                [ -e $mp4 ] && git rm $mp4
                ;;
            esac
          done

      - name: Commit and push
        run: |
          git status
          if [ -n "$(git status --porcelain 2>/dev/null)" ]; then
            git config --local user.email "action@github.com"
            git config --local user.name "GitHub Action"
            git add -A
            git commit -m "convert gif to mp4"
            git push origin master
          fi

f:id:rasukarusan:20210125190201p:plain

pushしたらgifがmp4に変換される

f:id:rasukarusan:20210125190144p:plain

pushしたのはgifだけなのにmp4が出来上がっている状態

f:id:rasukarusan:20210125190125p:plain

mp4動画のURLをコピーできる

終わり

Github Actionsこんな使い方もあるんだ。楽しい。

参考

Github Actionsのアクション作ってみる(Typescript編)

f:id:rasukarusan:20210122225003p:plain:w500

Typescriptで独自アクション作る

以前GithubActionsを使ってHomebrewのリリースを自動化し、一部のstepを独自アクションとして切り出した。

www.rasukarusan.com www.rasukarusan.com

前回のDockerで書き出したアクションを、Typescriptで書いてみる。

対象のstep

steps:
  # 1つ前のtagからの差分を取得
  - name: Get commit summary
    id: get_commit_summary
    run: |
      PREVIOUS_TAG=$(git tag --sort=-creatordate | sed -n 2p)
      COMMIT_SUMMARY="$(git log --oneline --pretty=tformat:"%h %s" $PREVIOUS_TAG..${{ github.ref }})"
      COMMIT_SUMMARY="${COMMIT_SUMMARY//$'\n'/'%0A'}"
      echo ::set-output name=COMMIT_SUMMARY::$COMMIT_SUMMARY

1つ前のtagからの差分を取得する、というstep。リリースノートに記載するためのもの。

f:id:rasukarusan:20210122223959p:plain

1つ前のtagからの差分コミットを一覧出力

作り方

基本的に公式のJavascriptでの独自アクションの作り方と、Typescriptで書かれたテンプレートのリポジトリを見ながらやればOK。

docs.github.com github.com

完成形

github.com

~/Documents/github/gitblamer/.github/actions/commit-summary
$ tree
.
├── action.yml
├── package.json
├── src
│   └── index.ts
└── tsconfig.json

諸々不要なものを削った最小構成。npmでインストールするものは下記。

npm init -y
npm install @actions/core
npm install @actions/exec
npm install --save-dev @vercel/ncc
npm install --save-dev @types/node
npm install --save-dev typescript
  • @actions/core: 引数受け取ったりecho ::set-output name=みたいなことをするため。
  • @actions/exec: git等のコマンドを実行するため
  • @vercel/ncc: Node.jsモジュールコンパイル君
  • @types/node: typescript使うため
  • typescript: typescript使うため

action.yml

name: 'Get Commit Summary'
description: 'Get commits from previrous tag to new tag'
inputs:
  ref:
    description: 'new tag'
    required: true
    default: 'please set ${{ github.ref }}'
outputs:
  summary:
    description: 'commit summary'
runs:
  using: 'node12'
  main: 'dist/index.js'

Dockerで作ったときとほぼ一緒。違うところはrunsの箇所。

runs:
-   using: 'docker'
-   image: 'Dockerfile'
-   args:
-     - ${{ inputs.ref }}
+   using: 'node12'
+   main: 'dist/index.js'

package.json

{
  "name": "commit-summary",
  "version": "1.0.0",
  "main": "dist/index.js",
  "description": "",
  "scripts": {
    "build": "ncc build src/index.ts -o dist --license licenses.txt"
  },
  "keywords": [],
  "author": "",
  "license": "MIT",
  "dependencies": {
    "@actions/core": "^1.2.6",
    "@actions/exec": "^1.0.4"
  },
  "devDependencies": {
    "@types/node": "^14.14.22",
    "@vercel/ncc": "^0.27.0",
    "typescript": "^4.1.3"
  }
}

tsconfig

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs"
  }
}

公式だともうちょっと色々書いてあったけど、必要最小限にした。

src/index.ts

import * as core from '@actions/core'
import * as exec from '@actions/exec'
import { ExecOptions } from '@actions/exec'

const execute = async (command: string): Promise<string> => {
  let output = ''
  const options: ExecOptions = {}
  options.listeners = {
    stdout: (data: Buffer) => {
      output += data.toString()
    },
    stderr: (data: Buffer) => {
      console.error(data)
    }
  }
  await exec.exec(command, null, options)
  return output

}

const main = async () => {
  try {
    const newTag = core.getInput('ref')
    const preTag = await execute('/bin/bash -c "git tag --sort=-creatordate | sed -n 2p"')
    const summary = await execute(`git log --oneline --pretty=tformat:"%h %s" ${preTag.trim()}..${newTag}`)
    core.setOutput("summary", summary)
  } catch (error) {
    core.setFailed(error.message)
  }
}
main()

アクションの肝。基本的にrunsで実行していたコマンドを、exec.exec()で実行していけばいい。
が、exec.exec()const result = await exec.exec('ls')のようにしても標準出力の内容が受け取れない。返ってくるのは01などの終了コードのみ。標準出力を受け取りたい場合、exec.exec()の第三引数のoptionsに、コールバックを受け取るlistenersを登録するなど、ごにょごにょしないといけない。

// ごにょごにょして標準出力を返すようにする
const execute = async (command: string): Promise<string> => {
  let output = ''
  const options: ExecOptions = {}
  options.listeners = {
    stdout: (data: Buffer) => {
      output += data.toString()
    },
    stderr: (data: Buffer) => {
      console.error(data)
    }
  }
  await exec.exec(command, null, options)
  return output
}

また、exec.exec()はパイプの実行に対応していない。下記の書き方空文字が返ってきてしまう。

const preTag = await execute('git tag --sort=-creatordate | sed -n 2p')

パイプで実行したい場合、/bin/bash -cで実行する必要がある。

const preTag = await execute('/bin/bash -c "git tag --sort=-creatordate | sed -n 2p"')

もしくはせっかくtsで書いているので、パイプ以降でやりたい処理をtsで書いてもいい。今回の場合やりたいことは「上から二行目を取得する」なのでpreTag.split('\n')[1]みたいな感じでもいい。

ビルドしてworkflows/release.ymlを更新したら終了

npm run build

ビルド後のディレクトリ構造

~/Documents/github/gitblamer/.github/actions/commit-summary
$ tree
.
├── action.yml
├── dist
│   ├── index.js
│   └── licenses.txt
├── package-lock.json
├── package.json
├── src
│   └── index.ts
└── tsconfig.json

2 directories, 7 files

workflows/release.yml

       # 1つ前のtagからの差分を取得
       - name: Get commit summary
         id: get_commit_summary
-        run: |
-          PREVIOUS_TAG=$(git tag --sort=-creatordate | sed -n 2p)
-          COMMIT_SUMMARY="$(git log --oneline --pretty=tformat:"%h %s" $PREVIOUS_TAG..${{ github.ref }})"
-          COMMIT_SUMMARY="${COMMIT_SUMMARY//$'\n'/'%0A'}"
-          echo ::set-output name=COMMIT_SUMMARY::$COMMIT_SUMMARY
+        uses: ./.github/actions/commit-summary
+        with:
+          ref: ${{ github.ref }}

すっきり。

f:id:rasukarusan:20210122223756p:plain

tagをpushして動作確認

終わり

Docker、Typescriptの両方でアクションを作成することができた。今回みたいなちょっとしたスクリプトレベルだったらTypescriptじゃなくていい。
もうちょっとヘビーな処理とかチーム開発だったら、絶対Typescript選びたくなるんでしょうね。

Github Actionsのアクションを作ってみる(Docker編)

f:id:rasukarusan:20210122144649p:plain:w500

1つ前のtagからの差分を出すアクションを作ってみる

前回GithubActionsを使ってHomebrewのリリースを自動化した。

www.rasukarusan.com

上記で実行している「1つ前のtagからの差分を取得する」をアクションとして切り出してみる。

イメージとしてはrunでゴリゴリ書いている箇所が、

steps:
  # 1つ前のtagからの差分を取得
  - name: Get commit summary
    id: get_commit_summary
    run: |
      PREVIOUS_TAG=$(git tag --sort=-creatordate | sed -n 2p)
      COMMIT_SUMMARY="$(git log --oneline --pretty=tformat:"%h %s" $PREVIOUS_TAG..${{ github.ref }})"
      COMMIT_SUMMARY="${COMMIT_SUMMARY//$'\n'/'%0A'}"
      echo ::set-output name=COMMIT_SUMMARY::$COMMIT_SUMMARY

usesで独自アクションを指定して、シンプルな形になる予定。

steps:
  # 1つ前のtagからの差分を取得
  - name: Get commit summary
    id: get_commit_summary
    uses: ./.github/actions/get-commit-summary

アクションの作成

docs.github.com

DockerまたはJavascriptでアクションを作れるみたい。

今回はとりあえずDockerでアクションを作ってみる。

完成形

.githubに新たにactionsディレクトリを作成。独自アクションはここにいれていく。

ディレクトリ構造

~/gitblamer
$ tree
.
├── .github
│   ├── actions
│   │   └── commit-summary
│   │       ├── Dockerfile
│   │       ├── action.yml
│   │       └── entrypoint.sh
│   └── workflows
│       └── release.yml
├── README.md
└── gitblamer

独自アクションを特定のリポジトリだけで使うプライベートアクションの場合、.github/actionsに作ればいい。他のリポジトリからも使えるようなパブリックアクションの場合、パブリックなリポジトリを作ってその中に置く必要がある。
プライベートアクションからパブリックアクションにするのは一瞬で出来るため、一旦プライベートアクションとして作る。

Dockerfile

# コードを実行するコンテナイメージ
FROM alpine/git
# アクションのリポジトリからコードファイルをコンテナのファイルシステムパス `/`にコピー
COPY entrypoint.sh /entrypoint.sh
# dockerコンテナが起動する際に実行されるコードファイル (`entrypoint.sh`)
ENTRYPOINT ["/entrypoint.sh"]

今回はgitコマンドを使いたいのでコンテナイメージにalpine/gitを指定。

action.yml

name: 'Get Commit Summary'
description: 'Get commits from previrous tag to new tag'
inputs:
  ref:
    description: 'new tag'
    required: true
    default: 'please set ${{ github.ref }}'
outputs:
  commit_summary:
    description: 'commit summary'
runs:
  using: 'docker'
  image: 'Dockerfile'
  args:
    - ${{ inputs.ref }}

引数の設定などをする。

entrypoint.sh

#!/bin/sh

NEW_TAG=$1

PREVIOUS_TAG=$(git tag --sort=-creatordate | sed -n 2p)
COMMIT_SUMMARY="$(git log --oneline --pretty=tformat:"%h %s" $PREVIOUS_TAG..$NEW_TAG)"
COMMIT_SUMMARY="${COMMIT_SUMMARY//$'\n'/'%0A'}"
echo ::set-output name=COMMIT_SUMMARY::$COMMIT_SUMMARY

アクションの肝。stepとして書いていた箇所をほぼそのままコピって貼り付けるだけ。
${{ github.ref }}などstep内でしか参照できない箇所などを修正する。
chmod +x entrypoint.shで実行権限を付与しておく。

workflows/release.ymlを修正したら終了

修正後のworkflows/release.yml

       # 1つ前のtagからの差分を取得
       - name: Get commit summary
+        uses: ./.github/actions/commit-summary
         id: get_commit_summary
-        run: |
-          PREVIOUS_TAG=$(git tag --sort=-creatordate | sed -n 2p)
-          COMMIT_SUMMARY="$(git log --oneline --pretty=tformat:"%h %s" $PREVIOUS_TAG..${{ github.ref }})"
-          COMMIT_SUMMARY="${COMMIT_SUMMARY//$'\n'/'%0A'}"
-          echo ::set-output name=COMMIT_SUMMARY::$COMMIT_SUMMARY
+        with:
+          ref: ${{ github.ref }}

すっきりした。

tagをpushしてActionsを確認。ちゃんと前回のDockerで実行されている。

f:id:rasukarusan:20210122142255p:plain

"Get commit summary"がDockerで実行されている

f:id:rasukarusan:20210122142342p:plain

リリースノートもバッチリ作成されている

独自アクションを他のリポジトリでも使えるようにする

パブリックなリポジトリを作ってその中にactionsディレクトリの中身をいれるだけ。
リポジトリ側で特に設定する箇所もない。

mkdir commit-summary
cd commit-summary
cp -r ~/gitblamer/.github/actions/* ./
git init 
git remote add origin git@github.com:Rasukarusan/commit-summary.git
git add -A
git commit -m 'initial commit'
git tag v1
git push origin v1

github.com

"Publish this Action to Marketplace"というアラートが出るが、別にしなくても他リポジトリから使えるようになるのでしなくてOK。

f:id:rasukarusan:20210122142450p:plain

Marketplaceに出したい人だけ出せばいい

あとはusesの指定を変更したら終了。

      # 1つ前のtagからの差分を取得
      - name: Get commit summary
-       uses: ./.github/actions/commit-summary
+       uses: Rasukarusan/commit-summary@v1
        id: get_commit_summary
        with:
          ref: ${{ github.ref }}

f:id:rasukarusan:20210122142603p:plain
Rasukarusan/commit-summaryが指定されて動いてる

終わり

今回はDockerで作ってみたが、Dockerはコンテナのビルドおよび取得のレイテンシがかかるため、Javascriptより遅いらしい。 次はJavascriptでアクションを書いてみよう。

今回使ったリポジトリ

GithubActionsでリリースとFormulaリポジトリの更新を自動化した

f:id:rasukarusan:20210120214244p:plain:w500

Homebrewの自作CLIツールの配布がとても面倒くさい

以前Homebrew/tapによる配布方法をまとめたが、やることが結構あって面倒くさい。
どうやらGithubActionsを使えばめちゃくちゃ楽にできるみたいなのでやってみた。

今までの流れ

1. tagをpush
2. GithubでReleaseの作成
3. Formulaファイルの作成or更新
5. 完了

今回目指すのはこれ

1. tagをpush
5. 完了
  • Githubのページを開いてReleaseノートを作成
  • Formulaリポジトリの.rbファイルを更新

この2つの作業を撲滅する。

死ぬほど参考にさせていただいたサイト

基本的に下記のサイトを参考にすればいけました、感謝です。
参考にしていて詰まったところと、変更している箇所を書いていきます。

sasa5740.hatenablog.com

対象リポジトリ

FormulaリポジトリにはFormulaディレクトリを設置していて、複数のツールが登録してある状態。ツールごとにFormulaリポジトリを用意せず、1つのリポジトリで管理するスタイルです。

流れ

  1. リリースノートを作成するworkflow(リリース用のリポジトリ)
  2. Formulaファイルを作成/更新するworkflow(Formula用のリポジトリ)

リリースノートを作成するworkflow

リリースノートには

  • 前回のtagからの差分
  • バイナリファイルのアップロード

を載せます。

リリースリポジトリに.github/workflows/release.ymlを作っていきます

name: Release

on:
  push:
    tags:
      - 'v*'
jobs:
  build:
    name: Create Release
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
        with:
          fetch-depth: 0

      # 1つ前のtagからの差分を取得
      - name: Get commit summary
        id: get_commit_summary
        run: |
          PREVIOUS_TAG=$(git tag --sort=-creatordate | sed -n 2p)
          COMMIT_SUMMARY="$(git log --oneline --pretty=tformat:"%h %s" $PREVIOUS_TAG..${{ github.ref }})"
          COMMIT_SUMMARY="${COMMIT_SUMMARY//$'\n'/'%0A'}"
          echo ::set-output name=COMMIT_SUMMARY::$COMMIT_SUMMARY

      # リリースノートの作成
      - name: Create Release
        id: create_release
        uses: actions/create-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: ${{ github.ref }}
          release_name: Release ${{ github.ref }}
          body: |
            ${{ steps.get_commit_summary.outputs.COMMIT_SUMMARY }}
          draft: false
          prerelease: false

      # バイナリファイルのアップロード
      - name: Upload Release Asset
        id: upload-release-asset
        uses: actions/upload-release-asset@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ steps.create_release.outputs.upload_url }}
          asset_path: ./gitblamer
          asset_name: gitblamer
          asset_content_type: application/octet-stream

      # Formulaに値を渡す準備
      - name: Set value for formula
        id: set_value_for_formula
        run: |
          SHA256=$(openssl dgst -sha256 gitblamer | awk '{print $2}')
          echo ::set-output name=SHA256::$SHA256
          echo ::set-output name=BINARY::gitblamer
          echo ::set-output name=CLASS::Gitblamer

      # Formulaリポジトリ更新(Formulaリポジトリのworkflowを発火)
      - name: Update Formula Repository
        uses: peter-evans/repository-dispatch@v1
        with:
          token: ${{ secrets.REPO_ACCESS_TOKEN }}
          repository: Rasukarusan/homebrew-tap
          event-type: released
          client-payload: '
            {
              "ref": "${{ github.ref }}",
              "sha256": "${{ steps.set_value_for_formula.outputs.SHA256 }}",
              "binary": "${{ steps.set_value_for_formula.outputs.BINARY }}",
              "url": "${{ steps.create_release.outputs.upload_url }}",
              "class": "${{ steps.set_value_for_formula.outputs.CLASS }}"
            }
          '

とりあえずこれでtagをpushしたらリリースノートが作られます。

# tagをpush
git tag v1.2.2
git push origin v1.2.2

f:id:rasukarusan:20210120212131p:plain

Formulaファイルを作成/更新するworkflow

本来手動でbrew createコマンドで作るファイルを、step内で作っている感じですね。

name: Update Formula
on:
  repository_dispatch:
    types: [released]
jobs:
  myEvent:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
        with:
          fetch-depth: 0
      - name: Update formula file
        run: |
          VERSION=$(echo ${{ github.event.client_payload.ref  }} | sed -e "s#refs/tags/##g")
          BINARY=${{ github.event.client_payload.binary }}
          URL="https://github.com/Rasukarusan/$BINARY/releases/download/$VERSION/$BINARY"
          data=$(cat <<EOF > Formula/$BINARY.rb
            class ${{ github.event.client_payload.class }} < Formula
              url "$URL"
              sha256 "${{ github.event.client_payload.sha256 }}"
              def install
                bin.install "$BINARY"
              end
            end
          EOF
          )
      - name: Commit version change
        run: |
          git config --local user.email "action@github.com"
          git config --local user.name "GitHub Action"
          git add -A
          git commit -m "update version"
      - name: Push changes
        uses: ad-m/github-push-action@master
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}

この状態でリリースリポジトリにtagがpushされたら新しいformulaファイルがコミットされる。

f:id:rasukarusan:20210120212208p:plain

f:id:rasukarusan:20210120212929p:plain
ちゃんとファイルが更新されてる

ここまで終わったらTerminalでCLIツールを更新できるようになっている。

brew info gitblamer
brew upgrade gitblamer

f:id:rasukarusan:20210120212218p:plain

詰まったところ

前回のtagからの差分を出す

# 1つ前のtagからの差分を取得
- name: Get commit summary
id: get_commit_summary
run: |
  PREVIOUS_TAG=$(git tag --sort=-creatordate | sed -n 2p)
  COMMIT_SUMMARY="$(git log --oneline --pretty=tformat:"%h %s" $PREVIOUS_TAG..${{ github.ref }})"
  COMMIT_SUMMARY="${COMMIT_SUMMARY//$'\n'/'%0A'}"
  echo ::set-output name=COMMIT_SUMMARY::$COMMIT_SUMMARY

こちらは下記サイトを参考にさせていただきました。

zenn.dev

注意点として、最初のstepでCheckoutするときにwith: fetch-depth: 0を設定しないと前回のtagが取得できないので注意。デフォルトでは1らしい。

バイナリファイルのアップロード

# バイナリファイルのアップロード
- name: Upload Release Asset
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
  upload_url: ${{ steps.create_release.outputs.upload_url }}
  asset_path: ./gitblamer
  asset_name: gitblamer
  asset_content_type: application/octet-stream

Homebrewではバイナリを直接指定する方式を取っていたため、zipではなくバイナリをアップロードする必要があります。
つまりasset_content_typeapplication/octet-streamにする。

Github Actionsのusesって何?

誰かが作成したアクション。自分で作ることもできる。Githubが公式でいくつか用意している。
Marketplaceで色んなworkflowが検索できる。

Formulaリポジトリのworkflowを発火する箇所も、本来はcurlでGithubAPIを叩くだけなので別にusesを使わなくてもいいが、使ったほうがcurlで書くよりシンプルに見やすくなる。

# Formulaリポジトリ更新(Formulaリポジトリのworkflowを発火)
- name: Update Formula Repository
uses: peter-evans/repository-dispatch@v1
with:
  token: ${{ secrets.REPO_ACCESS_TOKEN }}
  repository: Rasukarusan/homebrew-tap
  event-type: released
  client-payload: '
    {
      "ref": "${{ github.ref }}",
      "sha256": "${{ steps.set_value_for_formula.outputs.SHA256 }}",
      "binary": "${{ steps.set_value_for_formula.outputs.BINARY }}",
      "url": "${{ steps.create_release.outputs.upload_url }}",
      "class": "${{ steps.set_value_for_formula.outputs.CLASS }}"
    }
  '

${{ github.ref }} の他には何がある?

docs.github.com

終わり

自作ツールのアップデートが最高に楽になった、やったね。

1年を振り返るときに使ったgitコマンド

コミット履歴を保ったままブランチを統合する

ブランチ整理のために。過去のコミット履歴をなかったことにしたくなかった。

下記の記事が神。

qiita.com

# 新規リポジトリを作成
mkdir new_repository
cd new_repository
git init

# リポジトリを統合
git remote add ${repositoryName} ~/Desktop/${repositoryName}
git fetch ${repositoryName}
git read-tree --prefix=${repositoryName} ${repositoryName}/master
git checkout -- .
git add .
git merge -s subtree ${repositoryName}/master --allow-unrelated-histories

2020年に作ったリポジトリ一覧を出す

GithubのAPIを使って出した。APIのパラメータで日時を指定して取得することができなかったので、jqのselect文で2020-01-01~2020-12-31でフィルタして取得することに。
jqのパイプで@tsvとして出力する方法は知らなかった。

#!/usr/bin/env bash
TOKEN=XXXXXXXXXXXXXX
USERNAME=YYYYYYYYY

main() {
  # ページネーションの最後の番号を取得
  local last=$(curl -sI -H "Authorization: token ${TOKEN}" "https://api.github.com/user/repos?per_page=100&page=1" \
    | tr "," "\n" \
    | grep 'last' \
    | grep -oP "(?<=&page\=).*(?=\>)"
  )

  # tsvで出力する用のヘッダー
  echo "name     private     language    url     created_at  updated_at"

  # 一度に100件までしか取得できないので、ページネーション分回す
  for page in $(seq 1 $last); do
    curl -s -H "Authorization: token ${TOKEN}" "https://api.github.com/user/repos?per_page=100&page=${page}" \
      | jq '.[]
      | select(.owner.login == "'${USERNAME}'")
      | select("2020-01-01" <= .created_at and .created_at <= "2020-12-31")
      '
  done | jq -r --slurp 'sort_by(.created_at) | .[] | [.name, .private, .language, .html_url, .created_at, .updated_at] | @tsv'
}
main

上記をgit.shとして保存して実行する。

$ sh git.sh
name     private         language        url     created_at      updated_at
get-twitter-from-qiita  false   JavaScript      https://github.com/Rasukarusan/get-twitter-from-qiita   2020-01-07T12:31:26Z    2021-01-01T08:44:55Z
react-tutorial  true    JavaScript      https://github.com/Rasukarusan/react-tutorial   2020-01-09T00:04:29Z    2020-01-12T07:29:49Z
react-search-author     false   JavaScript      https://github.com/Rasukarusan/react-search-author      2020-01-13T00:42:50Z    2020-02-09T01:16:00Z

出力をNotionに貼り付けるといい感じの表が作成される。

f:id:rasukarusan:20210109230051p:plain
スプレッドシートに一度貼ってからコピペするといい感じにできる

スター数が多い順にリポジトリを出力する関数も書いた。

stars() {
  local last=$(curl -sI -H "Authorization: token ${TOKEN}" "https://api.github.com/user/repos?per_page=100&page=1" \
    | tr "," "\n" \
    | grep 'last' \
    | grep -oP "(?<=&page\=).*(?=\>)"
  )

  echo "name     stargazers_count    language    url     created_at  updated_at"

  for page in $(seq 1 $last); do
    curl -s -H "Authorization: token ${TOKEN}" "https://api.github.com/user/repos?per_page=100&page=${page}" \
      | jq '.[]
      | select(.owner.login == "'${USERNAME}'")
      | select(.stargazers_count > 0)
      '
  done | jq -r --slurp 'sort_by(.stargazers_count) | reverse | .[] | [.name, .stargazers_count, .language, .html_url, .created_at, .updated_at] | @tsv'
}

終わりに

改めて思うけどgitコマンド色んな機能ありすぎてすごい。

tmux popupで遊ぶ

tmux popupとは

tmuxのバージョン3.2-rcより導入された新たな機能。
どのようなものかはGIF見ていていただくのが一番早い。

https://github.com/Rasukarusan/blog-assets/blob/master/tmux-popup/popupdemo.gif?raw=true
fzfの絞り込みをtmux popupで実行したり、popup内で作業ができる

paneやwindowの概念とは別に、新規ウィンドウがtmux上に表示される。

使用用途としてはmanにも書いてあるとおり、一時的な作業をするのに適している。
またはfzfの絞り込みをpopup上で行うというものが挙げられる。ちなみにfzfはすでにtmux popupに対応しており、fzfと書いていたところをfzf-tmuxと書き換えてあげればpopupで絞り込みができるようになっている。無論使用しているtmuxがバージョン3.2以上であることが前提。

popup対応しているtmuxをインストールする

現状(2020/09/27)、brewでインストールできるtmuxは3.1bが最新なので、popupが使えない。
なのでtmuxをgit cloneして手動でインストールするしかない。といってもREADMEに記載してあるとおりにするだけなので簡単だ。

git clone https://github.com/tmux/tmux.git
cd tmux
sh autogen.sh
./configure && make

tmuxという実行ファイルが誕生していればOK。 実行時は今起動しているtmuxは終了して./tmuxで実行できる。すでにインストールしているtmuxのセッションを全て終了していないと、エラーが出たりpopupが使えないので、必ず終了しておく。tmux kill-serverしておけばOK。

# 現在のtmuxを終了しておく
$ tmux kill-server
# 最新版のtmuxを起動
$ ./tmux
# バージョン確認(3.2以上だったらOK)
$ tmux -V
tmux next-3.3

popupの実行方法

popupを実行するのは下記の通りでとても簡単。

tmux popup

f:id:rasukarusan:20200927184056p:plain
ENTERなど適当なキーを押せばpopupは消える

-w-hで幅、高さの指定も可能。

tmux popup -w80% -h10%

f:id:rasukarusan:20200927184024p:plain
-w100, -h50など数字で指定することもできる

-x-yで位置の指定も可能。

tmux popup -x10 -y40

f:id:rasukarusan:20200927184110p:plain

popupでコマンドの実行結果を表示する

popup内でshellコマンドを実行するには-Rオプションを使う
tmuxのバージョンアップで修正されました。現在は-Rオプションがなくなり、第一引数にコマンドを渡すだけで実行されます。

tmux popup "cat README"

f:id:rasukarusan:20200927184142p:plain
popup内でshellコマンドを実行

ShellScript内で実行するときなどで、現在のディレクトリをpopupにも渡したいときは下記

tmux popup -d '#{pane_current_path}' "cat README"

また、popup内でshellを起動して作業をしたいときは-KERをオプションにつける。
tmuxのバージョンアップで修正されました。現在は-EのみでOKです

tmux popup -E "zsh"

f:id:rasukarusan:20200927184205p:plain
popup内でzsh起動

-KEについては、popup内でshellを起動して自由に動き回りたいなら必須だと思ってくれていい。

一応説明

  • -KをつけるとCtrl-CESC以外ではpopupを終了させることができなくなる。デフォルトはどんなキーを押してもpopupは消えてしまうので、popup内でコマンドを打ちたいときは-Kは必須。 ただ、Ctrl-CESCでpopupが消えてしまうので、popup内でCtrl-C(処理をやめたいときなど)を使いたいときは後述の-Eを使う。
  • -Eをつけるとshellのexitコマンドでpopupを終了できるようになる。なので-KRとセットで使われることが前提。

色々遊んで見た感想

  • 1セッションにつき、1popupなので、複数のpopupを同時に起動とかはできない
  • neovimのfloating windowのように、popupと現在のwindowを行ったり来たりはできない
  • どんなにpaneが小さくても、真ん中にドカンと表示することができるので、fzfの絞り込みと相性は抜群

https://github.com/Rasukarusan/blog-assets/blob/master/tmux-popup/lupin.gif?raw=true
何か面白いことできないかと思ったけどこれが限界だった

# toiletコマンドが必要
tmux popup -w30 -h15 -E " toilet -f mono12 ル && sleep 0.1"
tmux popup -w30 -h15 -E " toilet -f mono12 パ && sleep 0.1"
tmux popup -w30 -h15 -E " toilet -f mono12 ン && sleep 0.1"
tmux popup -w30 -h15 -E " toilet -f mono12 ル && sleep 0.1"
tmux popup -w30 -h15 -E " toilet -f mono12 パ && sleep 0.1"
tmux popup -w30 -h15 -E " toilet -f mono12 ー && sleep 0.1"
tmux popup -w30 -h15 -E " toilet -f mono12 ー && sleep 0.1"
tmux popup -w30 -h15 -E " toilet -f mono12 ー && sleep 0.1"
tmux popup -w30 -h15 -E " toilet -f mono12 ー && sleep 0.1"
tmux popup -w30 -h15 -E " toilet -f mono12 ン && sleep 0.1"
tmux popup -w100% -h15 " toilet -w $(tput cols) -f mono12   ルパンルパーーーン"

終わり

複数のpopup起動ができればアニメーションっぽい動きもできて、面白くなりそう。

tmuxでiTermのimgcatを使う

iTermの拡張コマンドであるimgcatはtmux上で実行すると挙動がおかしくなる。 これはどうやらtmuxではpane分割があるため、widthやheightがうまく計算できないので描画がおかしくなってしまうらしい。

https://github.com/Rasukarusan/blog-assets/blob/master/tmux-imgcat/demo1.gif?raw=true
tmuxでimgcatを実行したときの挙動。一瞬だけ表示されて消える。

ただ、どうやらimgcatの実行後、何らかの方法で処理をブロッキングすることで描画がちゃんとされることがわかった。

https://github.com/Rasukarusan/blog-assets/blob/master/tmux-imgcat/demo2.gif?raw=true
imgcat実行後、処理をブロッキングした場合のimgcatの挙動

画像はTerminalに残らず、catではなくlessっぽい挙動になってしまうが、十分使える感じはある。

方法

例えばreadコマンドを使って処理をブロッキングすることができる。

imgcat_for_tmux() {
  imgcat "$1"
  # ENTERで画像表示を終了できる。
  read && clear && exit
}
alias imgcat='imgcat_for_tmux'

もしくはsleepコマンドで処理をブロッキングし、trapコマンドでCtrl-cをフックしてもいい。

imgcat_for_tmux() {
  trap 'clear && exit' SIGINT
  imgcat "$1"
  sleep 100
}

ただいちいちCtrl-cするのも面倒だし、trap仕掛けるのもアレなのでreadが一番シンプルだと思う。

引数やカーソル位置等を諸々考慮した版を一応載せておく。

_imgcat_for_tmux() {
    # @See: https://qastack.jp/unix/88296/get-vertical-cursor-position
    get_cursor_position() {
        old_settings=$(stty -g) || exit
        stty -icanon -echo min 0 time 3 || exit
        printf '\033[6n'
        pos=$(dd count=1 2> /dev/null)
        pos=${pos%R*}
        pos=${pos##*\[}
        x=${pos##*;} y=${pos%%;*}
        stty "$old_settings"
    }
    command imgcat "$1"
    [ $? -ne 0 ] && return
    [ ! "$TMUX" ] && return
    get_cursor_position
    # 2行分画像が残ってしまうためtputで再描画判定させて消す
    read && tput cup `expr $y - 2` 0
}

色々問題はある

Terminalのサイズが小さすぎると画像が表示されないとか、画面の下の方で実行すると描画がおかしくなったりと、色々問題はあるけど全く使えないよりはいいかなっていうレベル。
とりあえずtmuxでもimgcat使えるじゃん!!って嬉しかったのでこの世に残しておく。

zshで関数内で実行したコマンドを履歴に残す

通常、コマンドを実行したら履歴に残り、Ctrl-p/n上/下の矢印キーで実行したコマンドを遡ることができる。再度同じコマンドを実行するときはとても便利。
この履歴に手動で追加するにはどうするか。

結論から言うと

print -s 履歴に残したいコマンド

でいける。

例えばecho hogeというコマンドを履歴に残したいなら下記のようにする。

$ print -s echo hoge

Ctrl-pをするとecho hogeが出てくるはず

# historyを確認してみる
$ history | tail -n 1
 2494  echo hoge

どうしてこれが必要なのか

fzfやpecoでコマンドをまとめているときに必要になるんですよね。
dockerやgitのコマンドをfzfで選択して実行する関数を、.zshrc等に書いている人は結構多いと思います。
それ自体はめちゃくちゃ便利で、長すぎるコマンドとかオプションを覚えずにすむので重宝するのですが、 その関数で実行したものを再度実行しようとしたときにもう一度選び直さないといけないというのが面倒でした。

例えば自分はyarnのコマンドを下記のようにまとめていて、

_fzf_yarn() {
    local gitRoot=$(git rev-parse --show-cdup)
    local packageJson=$(find ${gitRoot}. -maxdepth 2  -name 'package.json')
    [ -z "$packageJson" ] && return
    local action=$(cat ${packageJson} | jq -r '.scripts | keys | .[]' \
        | fzf --preview "cat ${packageJson} | jq -r '.scripts[\"{}\"]'" --preview-window=up:1)
    [ -z "$action" ] && return
    yarn $action
}
alias yy='_fzf_yarn'

yyを実行するだけで、package.jsonに書かれたコマンドを選択して実行できる形にしています。

f:id:rasukarusan:20200728235605p:plain:w500
package.jsonを探索して中のコマンドを引っ張ってくる`yy`コマンド
ただ、この状態で「あっさっきのコマンドもう一回実行したい」となった場合にCtrl-pを押すとyyしか出てきません。
さっき実行したyarn startをもう一回実行したいだけなのに、もう一度yyと打ち、starぐらいまで入力し選択して実行、というステップを取らなければなりません。(yarn startと打つのはもっとダルいので却下)

こんなときに便利なのがprint -sという話。先程の関数の最後に追加するだけでOK。

_fzf_yarn() {
    local gitRoot=$(git rev-parse --show-cdup)
    local packageJson=$(find ${gitRoot}. -maxdepth 2  -name 'package.json')
    [ -z "$packageJson" ] && return
    local action=$(cat ${packageJson} | jq -r '.scripts | keys | .[]' \
        | fzf --preview "cat ${packageJson} | jq -r '.scripts[\"{}\"]'" --preview-window=up:1)
    [ -z "$action" ] && return
    yarn $action
+    print -s "yarn $action"
}
alias yy='_fzf_yarn'

historyに実行したyarnコマンドが残るようになりますやったね。

※ちなみにhistoryファイルに強引に書き込むという方法もあるっちゃあるが、人によってはタイムスタンプを記録するようにしていたりしてフォーマットが違うので汎用性が低くなってしまう。

終わり

仕事中煮詰まったときにコマンドのHelpを見て楽しんでいるのだが,そこで偶然見つけてとても嬉しかったので記事にした。
最初はhistoryコマンドにaddオプションみたいなものがあると思ってずっと探していたが見つからず、できないのかなと諦めていたが、まさかprintにこんなオプションあるなんて思わないよね。

fzfは非表示にした列でフィルタリングすることはできない

--with-nthで特定の列だけを表示した上でフィルタリングがしたかったが、どうもできないらしい。

github.com

上記のIssueによると、できないというよりはそれを実装してしまうと混乱を招きそうだから実装しない、ということらしい。

何がしたかったのか

fzfの絞り込み結果は日本語で表示し、フィルタリングは英語で行う、ということをしたかった。

例えばこんなファイルがあるとする。

Ken.tsv

神奈川    Kanagawa
東京  Tokyo
横浜  Yokohama

このファイルをfzfに食わせると下記のようになる。

f:id:rasukarusan:20200726232403p:plain

上記のように列が2つだけならこのままフィルタリングしていっても特に問題はない。
ただ列が多くなってきたときに、特定の列だけを表示した上でフィルタイングしたいときがある。
今回の場合だと、表示は神奈川県などの日本語だけにして、フィルタリングはkanagawaなどローマ字でやりたいっていうケースですね。

f:id:rasukarusan:20200726232437p:plain
イメージこんな感じ

そしてこれはできないよっていうのが冒頭のIssueになります。以下ポエム。

--nth,--with-nthについて

--nth,--with-nthは列(field)を扱うオプション。 ざっくり違いをまとめると以下。

  • --nthフィルタリングを適用させたい列を指定することができる
  • --with-nth表示したい列を指定することができる

ちなみにnthというのは4th5thの数字の部分をNとした形のこと。いわゆる序数ってやつですね。

nth系の挙動を下記のようなファイルで試してみる。

Ken.tsv

神奈川(kanagawaken)   Kanagawa
東京(tokyoto) Tokyo
横浜(yokohamashi) Yokohama

--nth=1(1列目でフィルタリング)

まずは--nthの挙動から確かめてみる。

cat Ken.tsv | fzf --nth=1

f:id:rasukarusan:20200726232504p:plain
1列目でフィルタリングされる

1列目の(kanagawaken)でフィルタリングされている。

--nth=2(2列目でフィルタリング)

では--nth=2にした場合どうなるか。

cat Ken.tsv | fzf --nth=2

f:id:rasukarusan:20200726232526p:plain
2列目でフィルタリングされる

2列目のKanagawaでフィルタリングされている。

このように--nthはフィルタリングを適用させる列を指定することができる。

--with-nth=1(1列目だけ表示)

cat Ken.tsv | fzf --with-nth=1

f:id:rasukarusan:20200726232543p:plain
1列目だけ表示される

1列目だけ表示されている。もちろん--with-nth=2とすると2列目だけ表示される。

--with-nth=N..(N列目以降表示)

2..のように..を使用することでN列目以降を表現できる。

Ken.tsv

神奈川    Kanagawa    1234km
東京  Tokyo   5678km
横浜  Yokohama    10km
cat Ken.tsv | fzf --with-nth=2..

f:id:rasukarusan:20200726232624p:plain
2列目以降が表示される

--nth,--with-nthを組み合わせれば非表示の列でフィルタリングできるんじゃない!?

下記のように--nth--with-nthを組み合わせればいけると思った。

# フィルタリング対象を2列目、1列目だけを表示する
cat Ken.tsv | fzf --nth=2 --with-nth=1

と思ったけどできなかった。これについては下記。

github.com

終わり

fzfを使い始めてからずっと疑問だったことが解消されたし、--nthについてもちゃんと理解することができたので結果オーライ。
どうしてもやりたかったらForkして修正したものを使えばいいという選択肢があるだけで神。
ああfzf楽しい。