ブロックダイアグラム作成御用達のdiagrams(旧称draw.io)

紹介

制御とか処理とかでブロックダイアグラムを作成したい時に利用できる

microsoftvisioみたいなものではあるが,例えば詳細な図面の設計に使えるものではないし,あくまで情報・工業・数学・産業といった分野において,ブロックダイアグラムなどの作成を支援してくれる

環境

ブラウザ

JavaScriptを有効にしておくだけでOK

app.diagrams.net

アプリ

インストールはこちらから,OSに対応してダウンロードしてね

get.diagrams.net

利点

  • 利用開始までの手順が簡単
  • 利用環境のハードルが低い
  • 機能が単純だから扱い易い
  • LaTeXの基礎的なコマンドに対応
  • 論文とかでも使えそうね

欠点

  • 美術的な創作業には不向き
  • レイヤーは扱いづらいかも

導入

ぶっちゃけhomebrewで入れられるのが強み

brew install --cask drawio                                             

ちなドキュメント

www.diagrams.net

faqは役に立つかも

www.diagrams.net

ビギナーズページ

drawio-app.com

drawio-app.com

drawio-app.com

drawio-app.com

drawio-app.com

drawio-app.com

drawio-app.com

drawio-app.com

drawio-app.com

drawio-app.com

作成

基本的には.drawioとそのバックアップ.drawio.bkpが作成される

加えてテンプレートも自由に選べる,ソフトウェア設計・クラウドネットワーク・ハードウェア設計のいずれも網羅している

ページ

下部でページを切り替えて作業できる,イラレっぽい

よく使う図形

各種テンプレートにはよく使う図形が同封されているが,これはライブラリ表示を有効化することでいつでも利用できる 左下の「図形を追加」から入って

適当にチェックを入れれば良い

または上部の「表示」->「図形」からでも設定できる

一応はそれぞれがライブラリという扱いをされている

数式組版

テキストを編集する際に$$で囲むと,その内部の文字列がLaTeXソースとして解釈され,編集後に組版されて表示される
上部の「拡張」->「数式組版」をオンにすれば良い

編集

ページ表示

グリッドが利用できるのがとても強い,ついでに点や線に合わせて自動的にスナップしてくれる

用紙サイズを設定してページビューをオンにすると境界線や中央線が表示される,背景色も一応は入れておこう

カラーパレットが使えるなら変えられる,枠線・テキスト・塗りつぶしで利用できる

グラデーション

4方向+放射状の5種類,かつ2色変化しか使えない,しょうがないね

レイヤー

上部の「表示」->「レイヤー」から展開できる

あとはお好みで作業しよう,やっぱりイラレっぽい

図形

左のブラウザからドラッグ&ドロップ,もしくは後述するインポートを行う

図のコネクタをドラッグで繋ぐとできる
ただし始点と終点の概念があるので方向に注意しよう,一応は右クリックから「逆」を選ぶと反転できる

drawio-app.com

分岐の書き方

接点が発生するのは図形と線の間のみなので,交点には必ず黒点を配置すると良い

交差の書き方

逆に交点を設けない場合は,わかりやすくジャンプさせると良い
線を選択して「スタイル」から「line jumps」で「円弧」を選択する

実際こうなるが,

前面と背面の関係によってどちらがジャンプするか決定される

入れ替えることも可能である

適度に大きい方がわかりやすいね!

矢マークの調整

「スタイル」の中段のリスト群の下列で,始点と終点のそれぞれにアイコンを適用できる
その下の「サイズ」で大きさを決められるが,線の太さに比例する係数であって絶対値ではないので注意

テキスト

選択した図形や線の上でダブルクリックすれば編集画面に入れる

drawio-app.com

フォント

「フォント」->「カスタム」に入れるので,

そこにシステムから入れるなりGoogleフォントを使うなりすれば良い

LaTeXコンパイルにおいて

フォントや自体は影響しないけど,色は上書きできるしレイアウトは変更できるし,サイズも比例して調整できる

インポート

上部の「ファイル」->「次をインポート:」

drawio-app.com

エクスポート

上部の「ファイル」->「形式を指定してエクスポート」->「(拡張子を選択)」

drawio-app.com

解像度について

このままでは弄れないので,「ファイル」->「形式を指定してエクスポート」->「高度な設定」から

DPIを上げれば良い,ついでに背景や余白についても設定しよう

「横幅」と「縦幅」は据え置きで,「ズーム」と「DPI」が変化するだけ
ちなみにここで言う余白は用紙サイズを基準としたものではないらしい,具体的には図形を収められる最小の長方形に勝手にトリミングしてくるので,pdfで出力すると表示やページがおかしくなってしまう,素直にpngで吐き出した方が良いかも

自作ライブラリ

drawio-app.com

作成

「ファイル」->「新規ライブラリ」で.xmlファイルとして作成できる お目当ての画像などを追加して保存する,上書き更新もできるはずだけど何故か動作がフリーズする場合があるかもしれないね(私は手元でよくバグるので)

読込

「ファイル->ライブラリを開く」で該当の.xmlファイルを選択する

自作シェイプ

図形そのものを自作できるが,コーディングが必要になってくるので辛い,そこまでする必要に駆られることはそうないと思う

www.diagrams.net

ググっても思った以上に資料が出ないので,本気でやるとなるとかなり辛そう
ただどこかsvgに似ているので,svgに慣れている人ならできそうな気もする

制御の資料を作るだけならぶっちゃけ不要だし,それくらいデフォルトのライブラリが充実しているので心配しなくても良い

dockの大きさをdefaults writeで限界突破させる

やり方

デスクトップの写真を設定しようとして間違えてdockのところ触っちゃうと,なぜか勝手に限界値に設定されちゃうので

defaults write com.apple.dock largesize -int 256 | killall Dock

する必要がある

デカい方がいいね!

defaultsについて

read [ファイル名]

読み込み
辞書型のうち1つだけを操作する場合のコマンド,という解釈が近そう

特定のアプリ

アプリ名に対応するファイル名を入れれば選択できる
入れない場合は全ての設定を吐き出す

辞書型

.plist<dict></dict>タグが{}で置換されるらしい
オプション名に後付けしていけば抜き出せるかも?

配列型

.plist<array></array>タグが()で置換されるらしい,JSONっぽいがJSONではない
ちなみに配列の番号を指定して読み込むのは不可能っぽい

export

.plistファイルを吐き出す

write [アプリ名] [オプション名] [値]

書き換え
辞書型のうち1つだけを操作する場合のコマンド,という解釈が近そう

addする場合

一般人の需要こそないだろうが,array-adddictionary-addがあるらしい

型指定

.plistではタグで型を指定する,-string-float-int-bool

Node.jsで.csv形式でログを取って.tar.gz形式で圧縮する

soluna-eureka.hatenablog.com

の続き

開発背景

電力が貴重なこの時代!学術研究でも計算機の消費電力は無視できない!そもそもCPU温度が下がらない状況は危ない!
ということで常駐で温度を監視してログを取れるやつにまで昇華しました,やってることは基礎的なことのはずなんだけどな…

ちなみに冷却装置の設定によっては室温との相関があるかもしれない,流体の熱伝導を考えればそれはそうという感じはあるが

機能

  • 1秒毎にCPU温度を監視
    • 設定温度を超過するとSlackにログを送信
  • 毎分10n秒でログを.csvに書き込む
    • excelとかで見るのを前提に
  • 毎時0分でSlackにログを送信
    • 動作確認の意味合いも兼ねて
  • 毎日0時5分に前日の.csv.tar.gzに圧縮し元の.csvを削除
    • もちろん最大圧縮

以下,反省と言い訳

csvの操作がクソ難しい

こちらもオブジェクト型や配列型の酷使に慣れていなかったというのもあるが,単純にnpmcsvの各ライブラリ(csv-parseとかcsv-stringifyとか)の仕様がかなり変わっていて導入が面倒だった,ごく最近のいい感じのブログとかもなかったので公式ドキュメントを見ないとなかなか実装できなかった,あっ営利目的でこの記事を参考にしてもいいけど参考にしたらここのリンクを広めといてね♡

async/await多すぎ問題

これってsyncなライブラリだよな〜と思ったらasyncでした〜みたいなことがちょっと多すぎて頭にきますよ〜,じゃけんasync/awaitかけますね(保険的な意味合いで),async functionで定義した関数をawaitなしで呼び出すとpromiseが返ってくるなんて初めて知りましたね…言われてみればそうなんだけど,本当にそうなんだ…

.csvファイルの成形問題

ヘッダの有無くらいしか実装できなかったが,実際の利用においては問題はないと判断したのでこれで行くことにした,調べた限りではヘッダありで.csvをパースしてjsonライクにオブジェクト型でデータを操作する的な場合もあるらしいが,わざわざ各ループの間で状態を渡し続ける実装なんて面倒だし,もう1行目の完全一致だけで判定した方がはるかに早そうなのでなぁ…

その無駄っぽい変数宣言いる?

いる(確信),書いてるうちにわからなくなりかけたので必要だった(確信),ちなみに文字数と修飾語で用途を分けている

地味に温度の精度が悪い

1℃単位の測定しか許容されていなかったり誤差が無駄に大きかったりという噂がOpen Hardware Monitorにはあるらしいが,まぁ気休め程度だと思って使うことにする,実際に測定結果を見ていても面白いから問題なし

コード

// IO plugin
const fs = require('fs-extra');
const tar = require('tar');
const tryCatch = require('try-catch');
const scheduler = require('node-schedule');

// API plugin
const { getJSON } = require('simple-get-json');
const { stringify } = require('csv-stringify/sync');
const { parse } = require('csv-parse/sync');
const csvStringify = stringify;
const csvParsing = parse;

//Util plugin
const datetime = require('date-and-time');
const { IncomingWebhook } = require("@slack/webhook");

// Parameters
const slackURL = "https://hooks.slack.com/services/xxx/yyy/zzz";
const slackWebhook = new IncomingWebhook(slackURL);
const monitorURL = "http://localhost:8085/data.json";
const coreTempLimit = 60;




// API for Open Hardware Monitor
async function accessJSON() {
    try {
        var res = await getJSON(monitorURL);
        var raw = JSON.stringify(res, null, 4);
        var obj = JSON.parse(raw)['Children']; // for Open Hardware Monitor
        var tgt = obj[0]['Children'][1]['Children'][1]['Children']; // for Open Hardware Monitor
        return tgt;
    } catch (err) {
        console.log("something wrong happened with CPU Monitor!")
        tgt = [
            { Text: "0", Value: "0" } // for open hardware monitor
        ];
        return tgt;
    }
}


// Shaper for Open Hardware Monitor
async function makeMessage(tgt) {
    var cpuParam = "";
    var coreName = "";
    var coreTemp = "";
    var heatFlag = false;

    var i = 0;
    var j = 0;
    tgt.forEach(cpuCore => {
        coreName = cpuCore['Text']; // for Open Hardware Monitor
        coreTemp = cpuCore['Value']; // for Open Hardware Monitor
        cpuParam += (coreName + " is " + coreTemp + "\n");
        floatCoreTemp = parseFloat(coreTemp);
        i = i + 1
        if (floatCoreTemp > coreTempLimit) {
            j = j + 1;
        }
    });
    if (i == j) {
        heatFlag = true; // whether need to notice or not
    }

    var txt = cpuParam + datetime.format(new Date(), 'YYYY/MM/DD HH:mm:ss:SSS') + "\n";
    var flg = heatFlag;
    return [flg, txt];
}


async function makeCSVtext(tgt) {
    var cpuParam = [];
    var coreName = "";
    var coreNames = ["date"];
    var coreTemp = "";
    var coreTemps = [datetime.format(new Date(), 'YYYY/MM/DD HH:mm:ss:SSS')];
    var heatFlag = false;

    var i = 0;
    var j = 0;
    tgt.forEach(cpuCore => {
        coreName = cpuCore['Text']; // for Open Hardware Monitor
        coreNames.push(coreName);
        coreTemp = cpuCore['Value']; // for Open Hardware Monitor
        floatCoreTemp = parseFloat(coreTemp);
        coreTemps.push(floatCoreTemp);
        i = i + 1
        if (floatCoreTemp > coreTempLimit) {
            j = j + 1;
        }
    });
    if (i == j) {
        heatFlag = true; // judge whether to notice or not
    }

    cpuParam = [coreNames, coreTemps];
    var txt = csvStringify(cpuParam, { header: false }, { delimiter: "," });
    var flg = heatFlag;
    return [flg, txt];
}



// csv(array)_txt(string) Parse&Build interface
async function parseCSVdata(txt) {
    var csv = await csvParsing(txt, { columns: false, delimiter: "," });
    return csv;
}

async function parseCSVdataHead(txt) {
    var csv = await csvParsing(txt, { columns: false, delimiter: ",", to_line: 1 }); // get only header line
    return csv;
}

async function buildCSVdata(csv) {
    var txt = await csvStringify(csv, { header: false, delimiter: "," });
    return txt;
}



// API for Slack
async function sendMessage(flg, txt) {
    if (flg) { // grasp whether to notice or not
        try {
            await slackWebhook.send({
                text: `${txt}`
            });
        } catch (err) {
            var text = "temp flag is " + flg + ", but something error happened with slack!";
            console.log(text);
        }
    }
}

async function sendLogging(flg, txt) {
    try {
        await slackWebhook.send({
            text: `${txt}`
        });
    } catch (err) {
        var text = "something error happened with slack!";
        console.log(text);
    }
}



// commonly file system utils
async function createFolder(path) {
    try {
        await fs.ensureDir(path);
    } catch (err) {
        var text = path + " (folder) can not be created!";
        console.log(text);
    };
}

async function createFile(path) {
    try {
        await fs.ensureFile(path);
    } catch (err) {
        var text = path + " (file) can not be created!";
        console.log(text);
    };
}

async function writeupFile(path, txt) {
    await createFile(path);
    try {
        await fs.appendFile(path, txt);
    } catch (err) {
        var text = path + " (file) can not be written!";
        console.log(text);
    };
}

async function writeupCSVdata(path, txt) {
    var preTxt;
    var preCsv;
    var csv;
    try {
        preTxt = await fs.readFile(path);
    } catch (err) {
        var text = path + " (file) can not be read!";
        console.log(text);
        preTxt = "";
    };
    preCsv = await parseCSVdataHead(preTxt);
    csv = await parseCSVdataHead(txt);
    if (JSON.stringify(preCsv) == JSON.stringify(csv)) { // check a header, need to developed
        csv = await parseCSVdata(txt);
        csv.shift();
        txt = await buildCSVdata(csv);
    }
    await writeupFile(path, txt);
}



async function createArchive(filePath, archivePath) {
    try {
        tar.create({
            sync: true,
            file: archivePath,
            gzip: '-k -9'
        }, [
            filePath
        ])
    } catch (err) {
        var text = archivePath + " (archive) can not be created!";
        console.log(text);
    };
}


async function removeFileFolder(path) {
    try {
        fs.removeSync(path);
    } catch (err) {
        var text = path + " (file) can not be removed!";
        console.log(text);
    };
}




// initialize
(async() => {
    createFolder(".", "/Logs");
})();




// routine definition
const scheduleSendMessage = scheduler.scheduleJob("0-59 * * * * *", function() {
    var tgt;
    var txt;
    var flg;
    (async() => {
        tgt = await accessJSON();
        [flg, txt] = await makeMessage(tgt);
        await sendMessage(flg, txt);
    })();
});



const scheduleSendLogging = scheduler.scheduleJob("0 0 */1 * * * ", function() {
    var tgt;
    var txt;
    var flg;
    (async() => {
        tgt = await accessJSON();
        [flg, txt] = await makeMessage(tgt);
        await sendLogging(flg, txt);
    })();
});



const scheduleLogging = scheduler.scheduleJob("0,10,20,30,40,50 * * * * *", function() {
    var date = datetime.format(new Date(), 'YYYY_MM_DD');
    var path = "./Logs/" + date + ".csv"
    var tgt;
    var txt;
    var flg;
    (async() => {
        tgt = await accessJSON();
        [flg, txt] = await makeCSVtext(tgt);
        await writeupCSVdata(path, txt);
    })();
});



const manageLogfiles = scheduler.scheduleJob("0 5 0 */1 * *", function() {
    var cur = new Date();
    var old = datetime.addDays(cur, -1);
    var curDate = datetime.format(cur, 'YYYY_MM_DD');
    var oldDate = datetime.format(old, 'YYYY_MM_DD');
    var curPath = "./Logs/" + curDate + ".csv";
    var oldPath = "./Logs/" + oldDate + ".csv";
    var curGzip = "./Logs/" + curDate + ".tar.gz";
    var oldGzip = "./Logs/" + oldDate + ".tar.gz";
    (async() => {
        await createFile(curPath);
        await createFile(curGzip);
        await createArchive(oldPath, oldGzip);
        await removeFileFolder(oldPath);
    })();
    console.log(datetime.format(new Date(), 'YYYY/MM/DD HH:mm:ss:SSS'));
    var text = "logging files are regularLy updated!";
    console.log(text);
});

CPUをOpen Hardware MonitorとNode.JSで監視してSlackで通知する

何がしたいか

重いシミュレーションを長い時間かけてリモートPCで回し続けるのはやっぱり不安な点があり,特に知らん間に冷却装置が不調を来したらどうしようという不安に苛まれる,まして多少はCPUをチューニングしているような状態だとそれは尚更になる
ということでスクリプトでCPU監視し続けて,温度が一定値を超えたらSlackに通知を投げ,Remote Desktopで非常措置を迅速に行えるようにした(ソフトの関係上でWindows限定だけど)(ネットワークが切れた場合のことは知らんものとする)

大まかな筋書き

  • サーバーモードのOpen Hardware MonitorでPCを監視
  • NodeJSでそのAPIを叩いてJSONを取得・加工
  • CPUの状態を評価して必要な場合はWebhookのAPIを叩く
  • Slackで確認して対応する

内容

Open Hardware Monitorの設定

インストール

openhardwaremonitor.org

サーバーモードで起動

管理者権限でOpenHardwareMonitor.exeを立ち上げた後に「Options」->「Remote Web Server」->「Run」で開始
localhostでアクセス可能,ポートはOptions->Remote Web Server->Portで設定できる,ここに出るリンクのIPアドレスはグローバルアドレスらしいけど,普通にlocalhostのほうが早いし楽なのでオススメ,環境によってはコロコロ変わるし

NodeJSの用意

ハードウェアが絡むせいで色々と面倒だからWSLとか使わずにPowerShellから直にNodeJSを動かせるようにする

管理(nvm)の入手

他のツールで工夫するのも面倒なので公式が推してるnvm-windowsを入れる

openhardwaremonitor.org

github.com

最新版のnvm-setup.zipを落として解凍,適当に回答してインストール

NodeJSの入手

PowerShell管理者権限で起動(じゃないとインストール後に有効化できない)
nvm list availableで出てきたバージョンから好きなものを選んでnvm install x.y.z
最後にnvm use x.y.z,なぜか管理者権限が必要でない場合はstatus:1が返ってくる

VSCodeの用意

あったら楽なので

code.visualstudio.com

入れたらgitの拡張機能も入れておくべし

gitの用意

soluna-eureka.hatenablog.com

実は上の記事でWSL2のUbuntuの中にgitを入れる前にWindowsにもgitを入れていた
特に変なことをしなければインストールに問題は起きないしVSCode拡張機能にも対応してくれている(この場合は絶対にAdjusting your PATH environmentで真ん中を選ぶ必要があるので注意)

git-scm.com

docs.microsoft.com

下の記事がわかりやすいかも

sukkiri.jp

Slackの用意

適当なメアドでサインインしてブラウザ上で適当にワークスペースを作って通知専用のチャンネルを作る
「設定と管理」「ワークスペースの設定」から「管理者設定」の画面が出るので左の「App管理」をクリック
上の「Appディレクトリ」で「webhook」を検索して「Incoming Webhook」をクリック,「Slackに追加」を選択
「チャンネルへの投稿」と「名前をカスタマイズ」「説明ラベル」を適当に編集して「Webhook URL」を確保

なお,複数のWebhook URLを使い分けたい場合は「Slackに追加」で更に作成できる,上限数は知らんけど

プロジェクト作成

良さげなディレクトリを作ったらVSCodeから開いて拡張機能からgit initする
次にVSCode内でPowershell(ターミナル)を開いてnpm init,適当に入力する<br. さらに以下をnpm installする

  • @slack/webhook
  • date-and-time
  • get-json
  • node-schedule
  • post-json
  • simple-get-json
  • simple-post-json

スクリプト作成

  • JSONデータのうちCPU温度がある場所については各自で確認すること
    • シングルCPUならobj[0]['Children'][1]['Children'][1]['Children']が持ってる配列に格納されている
  • 必ずhttp://localhost:[numTCPport]/data.jsonを叩くこと
    • これは実際ブラウザ上でJSONを確認できる
  • coreTempLimitで通知を出したいCPU温度の下限値を設定できる
const scheduler = require('node-schedule');
const { getJSON } = require('simple-get-json');
const datetime = require('date-and-time');
const { IncomingWebhook } = require("@slack/webhook");

const slackURL = "[slack webhook url]";
const slackWebhook = new IncomingWebhook(slackURL);
const monitorURL = "http://localhost:8085/data.json";

const coreTempLimit = 50;

async function accessJSON() {
    var res = await getJSON(monitorURL);
    var raw = JSON.stringify(res, null, 4);
    var obj = JSON.parse(raw)['Children'];
    var tgt = obj[0]['Children'][1]['Children'][1]['Children'];
    return tgt;
}

async function makeMessage(tgt) {
    var cpuParam = "";
    var coreName = "";
    var coreTemp = "";
    var heatFlag = false;

    tgt.forEach(cpuCore => {
        coreName = cpuCore['Text'];
        coreTemp = cpuCore['Value']
        cpuParam += (coreName + " is " + coreTemp + "\n")
        floatCoreTemp = parseFloat(coreTemp);
        if (floatCoreTemp > coreTempLimit) {
            heatFlag = true;
        }
    });
    var txt = cpuParam + datetime.format(new Date(), 'HH:mm:ss:SSS YYYY/MM/DD');
    var flg = heatFlag;
    console.log(flg);
    return [txt, flg];
}

async function sendMessage(txt, flg) {
    if (flg) {
        await slackWebhook.send({
            text: `${txt}`
        });
    }
}


const scheduleBase = scheduler.scheduleJob("*/1 * * * * *", function() {
    var tgt;
    var txt = "";
    var flg = false;
    (async() => {
        tgt = await accessJSON();
        [txt, flg] = await makeMessage(tgt);
        await sendMessage(txt, flg);
    })();
});

稼働

node index.jsで永久に稼働する,Slackにメッセージが飛べば成功
デバッグ代わりにflgを出力している

改善したいところ

  • 検知間隔の設定を改善
    • */1 * * * * *は1秒毎という意味,2なら2秒毎
  • GUI設定の実装
    • NodeJSのお得意技
  • データ化,グラフ化
  • ローカルネットワークで繋がる他の複数のPCと連携

complex modificationsのjson fileを利用したマルチボタンのマウス + macOS + Karabinerの設定

settings for multi button mouse + macOS + Karabiner with complex modifications .json file

何がしたいか

腱鞘炎対策にマルチボタンのついてる縦型マウスを買ったが,macOSは一部のメーカを除いてマウスごとのドライバというものがなく,またデフォルトではmagic mouse(左右ボタン+中ボタン兼スクローラ)と同程度の動作しか利用できない,そのためKarabinerといった入出力の変換ソフトが必要になる
今回はマルチボタンの有効化とその簡単な割り当てを紹介するが,基本的に左・右・中のボタン(1番・2番・3番)はあんまりイジらない,既存の動作に依存や干渉の関係があると面倒なので

注意

Karabinerは仕様の関係で割と動作が強力である(ほぼハードの動作と直結する)ため,下手をすると壊れて操作を受け付けなくなる可能性がある,なので自己責任で丁寧に作業をすること
一応はアプリごとに動作を変えられたりもするが,あくまでウィンドウがどこにあるか程度でしか判別してくれないので,もしそれがソフト側をイジるだけで解決する問題なら,なるべくソフト側だけをイジった方が良い
例えば ↓

soluna-eureka.hatenablog.com

インストール

cliで取得

brew install --cask karabiner-elements

formulae.brew.sh

これに限る
--caskなので/Applications/Karabiner-Elements.app/Applications/Karabiner-EventViewer.appがあればok,起動しよう

起動,システムへのアクセス許可

初回起動の時点で色々と権限を要求されるので,システム環境設定->セキュリティとプライバシーから適宜に承認する

フォルダの確認

~/.config/karabiner/assets/complex_modificationsが勝手に生成されていればOK,その配下に定義に則って作成された任意のjsonファイルを置くことで,自由に機能を追加できるし管理できる
実際なんだかんだでGUIには頼らずテキストで保存した方がいい,GUIだけでは複雑な挙動を検討できないし,ハードウェア設定をも考慮した本体プロファイルと分けないと混乱する

プログラムの確認

表からはKarabiner-ElementsKarabiner-EventViewerを起動できるが,前者は設定用・後者はデバッグ用に過ぎない
実際は初回起動において監視・制御のデーモンを裏で勝手に展開してくれる,次回起動からはログイン時に勝手にそれらが起動する,ログイン項目に登録する必要はない(ただし再読み込みはKarabiner-Elementsからやる必要がある)

f:id:Soluna_Eureka:20220221012821p:plain

設定

バイス登録

有線・無線・Bluetoothは関係なく,使いたいマウスを接続したらKarabiner-Elements->Devices->Basic configurationに移動,hoge Optional Mouse (fuga)が見つかるはずなので,左端チェックボックスから有効化する
ついでにVendor IDProduct IDをメモする,これらは後で条件設定の時に使いたい

f:id:Soluna_Eureka:20220221223804p:plain

Advancedは特に要らない

基本設定

混乱回避のため,Simple Modificationsを空欄にし,Function keysをリセットする(To Keyをそのまま割り振ると良い)

拡張ファイルの製作

以下に一例を示す

  • 共通設定
    • 5番のみでtab
    • 4番のみでshift+tab
  • ブラウザのみ
    • 1番+2番でcommand+r
    • 5番+6番でcommand+w
    • 1番+5番でcommand+click
    • 1番+4番でshift+click
    • 2番+5番でcommand+c
    • 2番+4番でcommand+v
{
    "title": "MouseWithFiveButtons",
    "rules": [{
        "description": "Define how how a mouse with 5 buttons works",
        "manipulators": [{
                "description": "Rcmd+'w',for Browsers (safari, chrome, firefox, vivaldi, ...) and Editor(vscode, ...)",
                "conditions": [{
                        "type": "device_if",
                        "identifiers": [{
                            "vendor_id": 7119,
                            "product_id": 5,
                            "description": "a mouse with 5 buttons"
                        }]
                    },
                    {
                        "type": "frontmost_application_if",
                        "bundle_identifiers": [
                            "^com\\.apple\\.Safari$",
                            "^com\\.google\\.Chrome$",
                            "^org\\.mozilla\\.firefox$",
                            "^com\\.vivaldi\\.Vivaldi$",
                            "^com\\.microsoft\\.VSCode$"
                        ]
                    }
                ],
                "type": "basic",
                "from": {
                    "simultaneous": [{
                            "pointing_button": "button4"
                        },
                        {
                            "pointing_button": "button5"
                        }
                    ]
                },
                "to": [{
                    "key_code": "w",
                    "modifiers": [
                        "right_command"
                    ]
                }]
            },
            {
                "description": "Rcmd+'w',for Browsers (safari, chrome, firefox, vivaldi, ...) and Editor(vscode, ...)",
                "conditions": [{
                        "type": "device_if",
                        "identifiers": [{
                            "vendor_id": 7119,
                            "product_id": 5,
                            "description": "a mouse with 5 buttons"
                        }]
                    },
                    {
                        "type": "frontmost_application_if",
                        "bundle_identifiers": [
                            "^com\\.apple\\.Safari$",
                            "^com\\.google\\.Chrome$",
                            "^org\\.mozilla\\.firefox$",
                            "^com\\.vivaldi\\.Vivaldi$",
                            "^com\\.microsoft\\.VSCode$"
                        ]
                    }
                ],
                "type": "basic",
                "from": {
                    "simultaneous": [{
                            "pointing_button": "button1"
                        },
                        {
                            "pointing_button": "button2"
                        }
                    ]
                },
                "to": [{
                    "key_code": "r",
                    "modifiers": [
                        "right_command"
                    ]
                }]
            },
            {
                "description": "Lsft+Lclk, for Browsers (safari, chrome, firefox, vivaldi, ...) and Editor(vscode, ...)",
                "conditions": [{
                        "type": "device_if",
                        "identifiers": [{
                            "vendor_id": 7119,
                            "product_id": 5,
                            "description": "a mouse with 5 buttons"
                        }]
                    },
                    {
                        "type": "frontmost_application_if",
                        "bundle_identifiers": [
                            "^com\\.apple\\.Safari$",
                            "^com\\.google\\.Chrome$",
                            "^org\\.mozilla\\.firefox$",
                            "^com\\.vivaldi\\.Vivaldi$",
                            "^com\\.microsoft\\.VSCode$"
                        ]
                    }
                ],
                "type": "basic",
                "from": {
                    "simultaneous": [{
                            "pointing_button": "button1"
                        },
                        {
                            "pointing_button": "button4"
                        }
                    ]
                },
                "to": [{
                    "pointing_button": "button1",
                    "modifiers": [
                        "left_shift"
                    ]
                }]
            },
            {
                "description": "Lcmd+Lclk,for Browsers (safari, chrome, firefox, vivaldi, ...) and Editor(vscode, ...)",
                "conditions": [{
                        "type": "device_if",
                        "identifiers": [{
                            "vendor_id": 7119,
                            "product_id": 5,
                            "description": "a mouse with 5 buttons"
                        }]
                    },
                    {
                        "type": "frontmost_application_if",
                        "bundle_identifiers": [
                            "^com\\.apple\\.Safari$",
                            "^com\\.google\\.Chrome$",
                            "^org\\.mozilla\\.firefox$",
                            "^com\\.vivaldi\\.Vivaldi$",
                            "^com\\.microsoft\\.VSCode$"
                        ]
                    }
                ],
                "type": "basic",
                "from": {
                    "simultaneous": [{
                            "pointing_button": "button1"
                        },
                        {
                            "pointing_button": "button5"
                        }
                    ]
                },
                "to": [{
                    "pointing_button": "button1",
                    "modifiers": [
                        "left_command"
                    ]
                }]
            },
            {
                "description": "Rcmd+'v',for Browsers (safari, chrome, firefox, vivaldi, ...) and Editor(vscode, ...)",
                "conditions": [{
                        "type": "device_if",
                        "identifiers": [{
                            "vendor_id": 7119,
                            "product_id": 5,
                            "description": "a mouse with 5 buttons"
                        }]
                    },
                    {
                        "type": "frontmost_application_if",
                        "bundle_identifiers": [
                            "^com\\.apple\\.Safari$",
                            "^com\\.google\\.Chrome$",
                            "^org\\.mozilla\\.firefox$",
                            "^com\\.vivaldi\\.Vivaldi$",
                            "^com\\.microsoft\\.VSCode$"
                        ]
                    }
                ],
                "type": "basic",
                "from": {
                    "simultaneous": [{
                            "pointing_button": "button2"
                        },
                        {
                            "pointing_button": "button4"
                        }
                    ]
                },
                "to": [{
                    "key_code": "v",
                    "modifiers": [
                        "right_command"
                    ]
                }]
            },
            {
                "description": "Rcmd+'c',for Browsers (safari, chrome, firefox, vivaldi, ...) and Editor(vscode, ...)",
                "conditions": [{
                        "type": "device_if",
                        "identifiers": [{
                            "vendor_id": 7119,
                            "product_id": 5,
                            "description": "a mouse with 5 buttons"
                        }]
                    },
                    {
                        "type": "frontmost_application_if",
                        "bundle_identifiers": [
                            "^com\\.apple\\.Safari$",
                            "^com\\.google\\.Chrome$",
                            "^org\\.mozilla\\.firefox$",
                            "^com\\.vivaldi\\.Vivaldi$",
                            "^com\\.microsoft\\.VSCode$"
                        ]
                    }
                ],
                "type": "basic",
                "from": {
                    "simultaneous": [{
                            "pointing_button": "button2"
                        },
                        {
                            "pointing_button": "button5"
                        }
                    ]
                },
                "to": [{
                    "key_code": "c",
                    "modifiers": [
                        "right_command"
                    ]
                }]
            },
            {
                "description": "global settings for mouse button 4",
                "conditions": [{
                    "type": "device_if",
                    "identifiers": [{
                        "vendor_id": 7119,
                        "product_id": 5,
                        "description": "a mouse with 5 buttons"
                    }]
                }],
                "type": "basic",
                "from": {
                    "pointing_button": "button4"
                },
                "to": [{
                    "key_code": "tab",
                    "modifiers": [
                        "right_shift"
                    ]
                }]
            },
            {
                "description": "global settings for mouse button 5",
                "conditions": [{
                    "type": "device_if",
                    "identifiers": [{
                        "vendor_id": 7119,
                        "product_id": 5,
                        "description": "a mouse with 5 buttons"
                    }]
                }],
                "type": "basic",
                "from": {
                    "pointing_button": "button5"
                },
                "to": [{
                    "key_code": "tab"
                }]
            }
        ]
    }]
}

これを~/.config/karabiner/assets/complex_modificationsに作成する

作成したらKarabiner-Elements->Complex Modifications->Rules->Add ruleを見る,構文ミスさえなければ表示されるので右端のEnableボタンでで有効化,ちなみにここにはファイル名は表示されないので注意(jsonオブジェクトのtitlerules.descriptionのみ表示される)

f:id:Soluna_Eureka:20220221225737p:plain

うまく有効化できたらMisc->Restart Karabiner-Elementsで再起動,これで完全に動作が反映されるはず

特徴

  • 同時押しsimultaneousによって修飾キー以外による同時押しに対応
    • 単一押しと使用するボタンやキーが被る場合は,simultaneousさせたい動作についてjsonオブジェクト上に先に記述すること,後に記述すると単一押しの方が優先されて判定を受けてしまい,同時押しの方は問答無用で無視されてしまう
    • それぞれの動作の定義がrules.manipulatorsの配列に格納される形であるため,処理の優先度が前後関係で決定されている可能性がある,そのためなるべく少ないjsonファイルに関連する設定をまとめた方がむしろ安心
    • 実はKarabiner-Elements->Complex Modifications->Parametersから設定できる
      • マウスで使うにはあまりにもシビアなので,猶予時間は長めにとった方が良いかも
      • f:id:Soluna_Eureka:20220221225755p:plain
  • アプリケーション判定frontmost_application_ifによってブラウザ限定動作に対応
    • マルチディスプレイで別々のアプリをトップに表示すると両方とも有効と扱われるので注意,シングルディスプレイを想定した実装しかされていない(意図しない動作が思わず発生する可能性がある)

カスタマイズ

  • rules.manipulators[].conditions[].identifiersには対応するvendor_idproduct_idが必要,各自で確認
  • マウスのボタン配置も確認して使いやすい組み合わせを検討
  • rules.manipulators[].conditions[].bundle_identifiersには対応するアプリのidentifiersが必要各自で確認
    • アプリを起動してlsappinfo info -only bundleid hogeすれば確認できる

qiita.com

解説

何を読めば理解できるか

公式ドキュメント

karabiner-elements.pqrs.org

jsonオブジェクトについて記載されている

判定時間の設定について

基本的には「キーを下げる」がpressの定義であって,一般的にはtoにて処理が起爆される.後述のhaltも使えるとは思うが動作は未確認,ドキュメントには例文どころか説明すら載ってないし使う価値もなさそう.
また「キーを(指定した時間より長く)下げ続ける」と処理が起爆するto_if_held_down,「下げたキーを上げる」と処理が起爆するto_after_key_upがあり,これら2つは両立して存在できる.その場合,to_if_held_downの中にhalt:trueを用いることで後続のto_after_key_upをキャンセルする,すなわちキーを下げ続ける時間で処理を変えることが可能になる.
to_if_aloneは「キーを(指定した時間より短く)単独で下げ続ける」と処理が起爆する,長く押し続けると判定がtoに移行する.同様にhalt:trueto_after_key_upをキャンセルして処理を変えることが可能になる.

もしくはfrom.simultaneousで修飾キー以外の複数入力を受け付けられるが,これはto系のメンバではなくfrom系のメンバであるために判定時間の概念が異なる.先に述べたようにrules.manipulatorsの配列の順番がfrom系のメンバの実行の優先順序になると推定されており,同じキーを利用した同時押しの判定をやりたいのであれば先に記述すべきである.

また包括されるキーの組み合わせを設定するのも全く推奨されない,例えばhoge+hugahogeを待ち受ける2つのfromオブジェクトがある場合,後者の設定をto_if_held_downでも設定しない限りaを下げて上げた瞬間に後者が起爆する.

総合的に考えると,今回の実装はえらく特殊なものである以上は,真似はしない方が良いだろう.
…実際に使うとなればKarabiner-Elements->Complex Modifications->Parametersto_if_alone_timeout_millisecondssimultaneous_threshold_millisecondsをイジるくらいで,これはデバイスの使用感に依存するだろう.
また変数をset_variableで設定してvariable_ifvariable_unlessで利用できるらしいが,使ったことがない…

`zsh`で`eval`する

evalとは

       eval [ arg ... ]
          Read the arguments as input to the shell and execute the resulting
          command(s) in the current shell process.  The return status is the
          same as if the commands had been executed directly by the shell;
          if there are no args or they contain no commands (i.e. are an
          empty string or whitespace) the return status is zero.

要するに文字列を標準入力としてシェルに与えて実行(した上でその返り値を取得)できる

何が嬉しいのか

変数展開の都合によって通常の記法によるシェルスクリプトでは設定できないようなコマンドをそのまま通すことができる
もっと手軽に言えば,変数展開の実行とevalの実行を分けてしまうことでより簡易に明確に文字列をインジェクションできる

soluna-eureka.hatenablog.com

例えば上記で扱ったc++ likeな文字列の取り扱いについても,シェルスクリプトの文面に直接的にコマンドを書かなくとも

#!/bin/zsh -eu
# this is testZsh5

arg="ab\ncd"

echo "for echo():"

cmd1="echo ${arg}"
cmd2="echo '${arg}'"
cmd3="echo \"${arg}\""
cmd4="echo $'${arg}'"
cmd5="echo $\"${arg}\""

eval ${cmd1}
eval ${cmd2}
eval ${cmd3}
eval ${cmd4}
eval ${cmd5}

echo "for printf():"

cmd6="printf \"%s\n\" ${arg}"
cmd7="printf \"%s\n\" '${arg}'"
cmd8="printf \"%s\n\" \"${arg}\""
cmd9="printf \"%s\n\" $'${arg}'"
cmd10="printf \"%s\n\" $\"${arg}\""

eval ${cmd6}
eval ${cmd7}
eval ${cmd8}
eval ${cmd9}
eval ${cmd10}
% testZsh5 
for echo():
abncd
ab
cd
ab
cd
ab
cd
$ab
cd
for printf():
abncd
ab\ncd
ab\ncd
ab
cd
$ab\ncd

とすればより使い分けやすくなるし挙動がより確実に追跡できる,これと同じものを生で記述するのはかなり面倒になるだろう

curl to ffmpeg

悪用厳禁

インスペクタを開いてネットワークを監視しながら動画を見ていると,ほぼ必ずcurl形式のダウンロードリンクがブラウザにエイリアスとして提供されていることが見て取れるが,そのheader要素をそのままffmpegに渡すことでローカルファイルに落とせる,というのもCookieやsessionによって行われる管理を維持したままブラウザ環境とCLI環境に対応させることができる

しかしいくらそれをコピペしようがcurlにはffmpegのような機能はなく(もし単純にmp4が落ちてくるならまだしもhlsの配信が相手だと無力である),またffmpegとはそれ自身のコマンドはもちろん渡されるオプションのフォーマットも違う,それなら以下のように--ffmpeg [動画ファイルの名前]を末尾に追記するだけで自動的にオプションを加工して(-Hを集めて-headersに渡す)ffmpegを呼び出せる(確実に実行ファイルを指定する)ようにすれば,かなり楽だろうと思われる
現状ではちゃんと動作している

#!/bin/zsh -eu
# this is curl with ffmpeg

default=$argv
ffmpeg='--ffmpeg'

zparseopts -D -E -a optionArray -A optionPair -ffmpeg: X+: H+:

if ((${+optionPair[$ffmpeg]})); then
    echo "--ffmpeg option allowed!"
    n=0
    type=""
    head=""
    for i in $optionArray; do
        (( n += 1))
        if [ $i = "-X" ]; then
            echo "type added"
            type=$type$optionArray[n+1]'\r\n'
        elif [ $i = "-H" ]; then
            echo "head added"
            head=$head$optionArray[n+1]'\r\n'
        fi
    done
    command="/usr/local/bin/ffmpeg -loglevel quirt -headers $'${head}' -i '${*}' -c copy ${optionPair[$ffmpeg]}"
    eval "${command}"
else
    /usr/local/opt/bin/curl ${default}
fi

youtubeとかは

ffmpegだけでは無理なサイトもあるが,そういう時はstreamlinkの方が強いのでそっちに任せたい,しかしstreamlinkも決して万能ではなく対応していないサイトの方がむしろ多い,表向きに.m3u8.mp4が出てくるサイトならffmpegで対応できるが…

soluna-eureka.hatenablog.com

youtube-dlとかよりstreamlinkの方が一元的に利用できて良い

formulae.brew.sh

`echo`と`printf`では制御文字の扱いが異なる件

ffmpegで気づいたこと

ffmpegでは-headersオプションを使えば複数のhttp headerを付与しつつ-iで指定したurlに向かってhttpで叩けるが,複数の内容を利用する際には改行するための制御文字を間々で認識させる必要があるらしく,その場合は$'...'で表せる.

note.kiriukun.com

そしてこの$'...'については確かにzshのmanページでも取り扱い方法が記載されていた,以下を参照のこと.

man zshmisc | col -b | expand -t 4 | pcre2grep -Moe ' {0}(QUOTING\n)( {7,}(\S* *)*\S*\n*)*'

見た感じではprintprintfc++の実装そのままで,対してechozsh用に作られた安全なものという印象を受けた.

zshで実験

準備

以下のシェルスクリプトを用いる

#!/bin/zsh -eu
# this is testZsh4

echo "all arguments : ${*}"

zparseopts -D -E -a optionArray -A optionPair test:

if ((${+optionArray})); then
    for i in $optionArray; do
        echo "echo: component: ${i}"
        printf "printf: %s\n" $i
    done
fi

if ((${+optionPair})); then
    for key in ${(@k)optionPair}; do
        echo "echo: flags: ${key} => value: ${optionPair[$key]}"
        printf "printf: flags: %s => value: %s\n" $key $optionPair[$key]
    done
fi

echo "other arguments : ${*}"

-testオプションにつける引数を変えれば処理の比較ができる

soluna-eureka.hatenablog.com

方針

改行文字を含めた文字列ab\ncdを対象に色々と変えてみよう!

ab\ncd

% testZsh4 -test ab\ncd
all arguments : -test abncd
echo: component: -test
printf: -test
echo: component: abncd
printf: abncd
echo: flags: -test => value: abncd
printf: flags: -test => value: abncd
other arguments : 

両者共に\が消えた

'ab\ncd'

% testZsh4 -test 'ab\ncd'
all arguments : -test ab
cd
echo: component: -test
printf: -test
echo: component: ab
cd

printf: ab\ncd
echo: flags: -test => value: ab
cd
printf: flags: -test => value: ab\ncd
other arguments : 

echoだけ改行文字が読まれた,printfでは文字として扱われた

"ab\ncd"

% testZsh4 -test "ab\ncd" 
all arguments : -test ab
cd
echo: component: -test
printf: -test
echo: component: ab
cd
printf: ab\ncd
echo: flags: -test => value: ab
cd
printf: flags: -test => value: ab\ncd
other arguments : 

'ab\ncd'と変わらない

$'ab\ncd'

% testZsh4 -test $'ab\ncd'
all arguments : -test ab
cd
echo: component: -test
printf: -test
echo: component: ab
cd
printf: ab
cd
echo: flags: -test => value: ab
cd
printf: flags: -test => value: ab
cd
other arguments : 

両者とも改行文字が読まれた

$"ab\ncd"

% testZsh4 -test $"ab\ncd"
all arguments : -test $ab
cd
echo: component: -test
printf: -test
echo: component: $ab
cd
printf: $ab\ncd
echo: flags: -test => value: $ab
cd
printf: flags: -test => value: $ab\ncd
other arguments : 

"..."の動作はそのまま先頭に$が入っただけだった

結論

printfをみるにc++系の実装なら$'...'することでほぼ確実に制御文字を反映させられるっぽい,またechoではそれよりも簡単に制御文字を反映させられるっぽい

ffmpegでの使いかた

-headersオプションは多重の呼び出しが不可能である(2回目からは内容が置き換わる)ため,前述の手法で制御文字を有効にすることではじめて複数の内容を利用できる.とは言えその中で頻用される幾つかはffmpeg側でも専用のオプションとして提供されており,本来ならばそういったものを利用するべきではないかとも考えられる(処理がめんどいって?それはそう).

詳細は以下で確認できる,-loglevel traceで動作が全て標準出力される,-loglevel quietで何も表示されない,また-i [url]-headers [header]より前に持ってくると-headers [header]が認識されない(なぜかドキュメントにはのってないけど)

% ffmpeg \
-loglevel trace \
-headers $'Hoge: hoge\r\nHuga: huga' \
-i 'http://example.com/playlist.m3u8' \
-c copy \
output.mp4
...
User-Agent: Lavf/58.76.100
Accept: */*
Range: bytes=0-
Connection: close
Host: example.com
Icy-MetaData: 1
Hoge: hoge
Huga: huga
...

以上の結果が得られればヘッダが正しく設定されたと言える(該当の動画は存在しないので最終的にはerrorが返される)
本来\r\nms-doswindowsで利用される作法であり,旧MacOSでは\rのみ・現macOSでは\nのみがそれぞれの「標準の改行コード」である,なので\nだけで十分ではある…のだが心配なので\r\nすることを癖にしても良いかもしれない,実際の\rmacOSにおいて「同じ行の先頭まで戻る(そして出力が順に置き換わる)」動作を表すので余計な改行にはならないはず

おまけ

これ読めばわかると思われるが…?

QUOTING
       A character may be quoted (that is, made to stand for itself) by
       preceding it with a `\'.  `\' followed by a newline is ignored.

       A string enclosed between `$'' and `'' is processed the same way as the
       string arguments of the print builtin, and the resulting string is
       considered to be entirely quoted.  A literal `'' character can be
       included in the string by using the `\'' escape.

       All characters enclosed between a pair of single quotes ('') that is not
       preceded by a `$' are quoted.  A single quote cannot appear within single
       quotes unless the option RC_QUOTES is set, in which case a pair of single
       quotes are turned into a single quote.  For example,

          print ''''

       outputs nothing apart from a newline if RC_QUOTES is not set, but one
       single quote if it is set.

       Inside double quotes (""), parameter and command substitution occur, and
       `\' quotes the characters `\', ``', `"', `$', and the first character of
       $histchars (default `!').

自作のzshスクリプトでzparseoptsを用いてコマンドラインオプションを解析したい

目標

  • その都度に設定ファイルを指定するタイプのスクリプトhomebrewで入れたmarp-cliとか)の実行をより便利にしたい
  • コマンドライン(CL)オプション(OPT)を使いたい(marpならば--pdfの有無で出力をhtmlかpdfで切り替えられる)

具体的には

その引数を忠実にパースして対応させたコマンドを呼び出せるようにする

下準備

実行環境の整理

パーミッションについて

これを読んでね

soluna-eureka.hatenablog.com

スクリプト置き場

~/zscripts/bin

にパスを通して諸々のシンボリックリンクを入れる,元のスクリプト

~/zscripts/test

のように分類することにした

シンボリックリンクを生成する際は必ずフルパスを指定して生成すること,ln -sする時は相対パス扱いで通っちゃうけど実際に走らせる時はシェル側が絶対パスとして使ってくる,つまり

pwd
# => ~/zscripts
ln -s test/testZsh1 bin

は動いてしまうのだが(これは何も間違ってはいない,この時のzshから見ればtest/testZsh1は存在するし,bin/testZshはちゃんと生成される),これを実行しようとすると

testZsh1
# => zsh「$PATHを読むぞ,~/zscripts/binを見るぞ,testZsh1があったぞ」
# => zsh「えーとtest/Zsh1に行け…?そんな場所ねぇよ!」
# => zsh「zsh: command not found」

という結果になる,ただしこの状態でも$PATH上にシンボリックリンクは存在するため,zshは可能な限りコマンド補完を提供してくれる,うーんこれはかなり紛らわしいね

現状ではlnに引数対象の絶対パスの補完機能はない,というよりもシンボリックリンクではなくハードリンクならiモード番号をまんま参照するためこんなことは起きない,シンボリックリンクリンクに特有の現象と考えて諦めて受け入れるしかなさそう

パスを通す

export PATH=~/zscripts/bin:$PATH

実行ファイル作成

以下の作業はパターン化しよう

cd ~/zscripts/test
touch testZshS1
chmod 0755 testZsh1 # <-rwxr-xr-x
ln -s ~/zscripts/test/testZsh1 ~/zscripts/bin/ # <-最後にスラッシュつけないとディレクトリと認識されないので注意
vi testZsh1
#!/bin/zsh
# this is testZsh1

echo "this is testZsh1"
% testZsh1
# => this is testZsh1

予約済み変数(特殊変数)

シェバンにzshで実行するよう指示されたシェルスクリプトは,起動時にユーザが操作しているシェルからzshのプロセスに渡される,つまりは「ログイン者と対話するシェル」と「スクリプトを実行するシェル」が必ず同じである必要がない.しかしそれでもzshで環境を統一することをオススメしたい理由として,zshが対話シェルとして渡す特殊変数と実行シェルとして受け取る特殊変数の扱いを共通化することによる利点がある.読み物が一つの同じドキュメントだけで済むために大きく労力を削減できるし,権限管理などもやりやすい上にコーディングも多少は楽になるだろうとと思われるからだ(最近になって「移行しておいてよかった」と思い始めている,それでもGoogle先生はよくzshを無視してbashの記事を出してくるけど).
シェルスクリプトはシェバンの書きようによってはshbashに実行させることも可能である,それ故にシェルを切り替えた時に迂闊なミスが起こらないよう,シェルごとにスクリプトの管理フォルダと対応するパスを切り分けるべきだと考えている.

そしてログインシェルとスクリプトシェルの間でやり取りされる情報を利用するための特殊変数がzshには用意されている,manページから以下のコマンドで抽出できるので確認しよう

# ログインシェルが渡すもの
man zshparam | col -b | expand -t 4 | pcre2grep -Moe ' {0}(PARAMETERS SET BY THE SHELL\n)( {5,}(\S* *)*\S*\n*)*'

ちなみに元から利用できる環境変数などは以下で抽出できる

# ログインシェルが使うもの
man zshparam | col -b | expand -t 4 | pcre2grep -Moe ' {0}(PARAMETERS USED BY THE SHELL\n)( {5,}(\S* *)*\S*\n*)*'

シェルスクリプトでは変数名の前に$をつけて展開できるが,これもzshも例外ではなく(以下で抽出できる)

# zshにおける変数展開について
man zshexpn | col -b | expand -t 4 | pcre2grep -Moe ' {0}(PARAMETER EXPANSION\n)( {5,}(\S* *)*\S*\n*)*'

,多くの場合は既に$がついた形式で覚えられがちなのだが,取り敢えず重要そうなものだけをドキュメント通りに並べると

  • $
  • 0
  • *
    • 呼び出し時に与えられた全ての引数
    • 配列扱い
      • bashでは文字列1個の扱いだったらしいな?
    • 引数がなければ未定義
  • @
    • Same as argv[@]らしい
    • 引数がなければ未定義
  • argv
    • Same as *らしい
    • こいつは変数として簡単に処理できる
      • *@は処理が面倒だぞ(後述)
    • 引数がなくてもargvは空の配列として定義される
  • #
    • 引数の個数
  • ?
    • 1つ前の関数の終了状態を示す
    • つまり関数が評価されるたびに値が入れ替えられる
      • シェバンに-eがあると異常終了で必ず止まるけど
      • -uすると未定義変数を参照した時に必ず止まるよ
  • status
    • Same as ?らしい

ちなみにzsh自身のオプションは以下で確認できる,-euに関してはbashと同じっぽい

man zshoptions | col -b | expand -t 4 | pcre2grep -Moe ' {0}(SINGLE LETTER OPTIONS\n)( {3,}(\S* *)*\S*\n*)*'

仕様などは以下を参考に

qiita.com

実験して確認する

#!/bin/zsh -u
# this is testZsh2
# -eu するとエラーで爆死しにくくなるらしい,みんなは-eをつけよう

true=1
false=0
# これは単なるおまじない

echo "pid: '${$}'"
echo "command: '${0}'"
echo "length: '${#}'"

cat OMMCvsCCCCM.txt
# 当然だけどそんな下品なファイルはないので異常なまま終了する(status: 1)
echo "status: '${?}'"

if ((${*:+true})); then
    echo "* is defined"
    echo "for \$* : '${*}'"
    for ((i=1;i<=${#*};i++));do
        echo "for \$*[${i}]: '${*[i]}'"
    done
fi

if ((${@:+true})); then
    echo "@ is defined"
    echo "for \$@ : '${@}'"
    for ((i=1;i<=${#@};i++));do
        echo "for \$@[${i}]: '${@[i]}'"
    done
fi

if ((${+argv})); then
    echo "argv is defined"
    echo "length of argv: '${#argv}'"
fi

unset argv
echo "after unset of 'argv', *: '${*}'"
echo "after unset of 'argv', @: '${@}'"

引数を入れたり消したりして試してみよう,この時点で半角スペースによる配列化が自動で実行されていることがわかるだろう

% testZsh2 -a b -c
# => pid: '[やる度に値が変わる]'
# => command: '[ここにフルパスが出てくる]'
# => length: '3'
# => cat: OMMCvsCCCCM.txt: No such file or directory
# => status: '1'
# => * is defined!
# => for $* : '-a b -c'
# => for $*[1]: '-a'
# => for $*[2]: 'b'
# => for $*[3]: '-c'
# => @ is defined!
# => for $@ : '-a b -c'
# => for $@[1]: '-a'
# => for $@[2]: 'b'
# => for $@[3]: '-c'
# => argv is defined!
# => length of argv: '3'
# => after unset of 'argv', *: ''
# => after unset of 'argv', @: ''

*@は定義されているか?

変数定義の確認方法として有名なものに((${+xxx}))があるものの,zsh 5.8においては((${+*}))((${+@}))zsh: bad substitutionを吐き出す…変数ではなく演算子として認識されているらしく,これに引数の有無や数は関係ないと思われる
だからtrue=1を用意して((${*:+true}))((${@:+true}))にする必要がある,これなら確実に先に*@が展開されて:で評価して定義されていれば+trueを返すことができる(((${xxx:+yyy}))については先に挙げたリファレンスにも載ってる)

他のやり方などは以下を参考に

unhexium.net

zparseoptsについて

zshをセットで導入すれば元から使えるやつらしい,他にも候補があるらしいけどzsh上の普遍性を考慮してこれにした
扱い上はzshmodulesの一部なのでmanページは以下で抽出できる

zscripts % man zshmodules | col -b | expand -t 4 | pcre2grep -Moe ' {7}(zparseopts.*\n)( {10,}(\S* *)*\S*\n*)*' 

特徴

zparseopts*を設定しなくても良い

どうやら勝手に*を覗いて変更してくるらしい,必要なのは動作の調整と扱うオプションの設定だけである

所用のハイフン数に制限なし

1つや2つに限らずいくらでも増やせる,使う機会があるかどうか?さぁ…

オプション文字数に制限なし

-x-xxも通る,-は1文字・--はそれ以外みたいな風潮はあるけど

楽に連想配列を使える

-Aを入れると-x yから勝手に-x: yみたいな連想配列を作ってくれる
-aなら-x yをくっつけて-xyを生成して配列に代入してくるだけだけど…

1オプションには1設定

-x y z -a -b -cとかを入れるとz以降が無視されz -a -b -cが余剰として扱われる,もちろん用意した連想配列は壊れる
複数の設定を1つのオプションに入れたいとなると,流石に*に対する処理を手動で実装した方が早そう,今回はやらんけど…

テンプレート

zparseopts -D -E -a optionArray -A optionPair a bc: -ddd:: --e:

-Dとは?

元の*からオプションとみなした部分を全て切り取ってくれる,複数のファイルを取り扱う場合はこれが必要になってくる
前述したようにパースが崩壊すると切り取れなかった部分が全て*に残ることになる

-Eとは?

オプション宣言の前に変数を置いたり合間に余剰な変数を置いたりしてもパースを続けてくれる,つまりはさっきの注意書きを無視できる,コマンド・ファイル・オプション・内容みたいにやる時には便利かも

オプションの指定方法について

ハイフンが1つだけ減る(連想配列から取り出すときは減らしちゃダメだよ)

  • -aならa
  • -bcならbc
  • --dddなら-ddd激安の殿堂ZOY
  • ---eなら--e

また:の数にも意味がある,オプション後に引数について

  • 必ずなければa
    • あるとエラー
  • 必ずあればa:
    • ないとエラー
  • 分岐したければa::
    • ないと空白の文字列が連想配列に代入される
    • ただし配列が完全に連結してしまう副作用がある
    • -x a[-x, a]となるところを[-xa]にしてしまう
      • ループ処理を自前でやりたい場合とか逆に辛いかも?
    • 連想配列では{-x :a}で変わらない

さらに++:+:::-::-は同じオプションを何回も呼び出した時に影響がある

  • +は倍プッシュを許可する
    • 配列に影響が出る
    • -x -x[-x]となるところを[-x, -x]にできる
  • +:は引数を連結させる
    • 連想配列に影響が出る,配列は倍プッシュされる
    • -x a -x bなら{-x: ab}[-x, a, -x ,b]になる
  • +::は引数を完全に連結させる
    • 配列に影響が出る
    • -x a -x bなら{-x: ab}[-xa, -xb]になる
  • :-::-は…ほとんど意味ない
    • まず-をつけるだけで::と同じ副作用が必ず出現する
      • +:+::みたいに有用と思われる違いがない
    • 引数の置き換えは配列でも連想配列でも重複の呼び出しで勝手に発生するので…
      • つまり:-::-も使い道がない,無視して良い

実験して確認する

#!/bin/zsh -eu
# this is testZsh3

echo "all arguments : ${*}"

zparseopts -D -E -a optionArray -A optionPair a+ bc+:: -ddd+: --e:

if [[ -n "${optionPair[(i)-a]}" ]]; then
    echo "-a option enabled"
fi

if [[ -n "${optionPair[(i)-bc]}" ]]; then
    echo "-bc option enabled: '${optionPair[-bc]}'"
fi

if [[ -n "${optionPair[(i)--ddd]}" ]]; then
    echo "--ddd option enabled: '${optionPair[--ddd]}'"
fi

if [[ -n "${optionPair[(i)---e]}" ]]; then
    echo "---e option enabled: '${optionPair[---e]}'"
fi

echo "other arguments : $*"

if ((${+optionArray})); then
    for i in $optionArray; do
        echo "component: '${i}'"
    done
fi

if ((${+optionPair})); then
    for key in ${(@k)optionPair}; do
        echo "flags: ${key} => value: ${optionPair[$key]}"
    done
fi

echo "${optionPair}"

key="---e"
echo "${#optionPair[$key]}"
% testZsh3 xyz stst -a -a -bc p -bc q --ddd daiou --ddd heika ---e rrr ---e r tsts
all arguments : xyz stst -a -a -bc p -bc q --ddd daiou --ddd heika ---e rrr ---e r tsts
-a option enabled
-bc option enabled: 'pq'
--ddd option enabled: 'daiouheika'
---e option enabled: 'r'
other arguments : xyz stst tsts
component: '-a'
component: '-a'
component: '-bcp'
component: '-bcq'
component: '--ddd'
component: 'daiou'
component: '--ddd'
component: 'heika'
component: '---e'
component: 'r'
flags: -a => value: 
flags: ---e => value: r
flags: -bc => value: pq
flags: --ddd => value: daiouheika
 r pq daiouheika
1

marpコマンドを改善

github.com

formulae.brew.sh

詳細については今は省く(また別のところでやるかも)が,CLIから叩けるmarpvscode拡張機能版とは異なり設定ファイルを-cオプションで指定できる(その設定ファイルも万能ではない)が,しかしmarp本体に記憶させることができない.そのためシェルスクリプトで事前に定めたパスを必ず指定できるように仕向けようと考えた,あとはvscodeに機能を追加するよりもzshシェルスクリプトを書く練習から始めたかった.
…とりあえず-m pdf-m html-m pptxで使い分けられるようにした.ちなみにmarpはまだ全く使いこなせていない.

#!/bin/zsh -eu
# this is marpit

template='[任意のパス]'

zparseopts -D -E -a optionArray -A optionPair i: o: m::

input='-i'
output='-o'
mode='-m'
pdf="pdf"
html="html"
pptx="pptx"

if ((${+optionPair[$mode]})); then
    type=${optionPair[$mode]}
    case $type in
    ($pdf) marp -c ${template} --pdf ${optionPair[$input]}.md -o ${optionPair[$output]}.pdf;|
    ($html) marp -c ${template} ${optionPair[$input]}.md -o ${optionPair[$output]}.html;|
    ($pptx) marp -c ${template} --pptx ${optionPair[$input]}.md -o ${optionPair[$output]}.pptx;|
    esac
else
    echo marp --template ${template}  ${optionPair[$inout]} -0 ${optionPair[$output]}
fi
% marpit -i test -o test -m html     
[  INFO ] An EXPERIMENTAL transition support for bespoke template is enabled. It is using the shared element transition API proposal and it is not yet stable. Recommend to use
          with --preview option for trying transitions. Track the latest information at https://github.com/marp-team/marp-cli/issues/382.
[  INFO ] Converting 1 markdown...
[  INFO ] test.md => test.html
% marpit -i test -o test -m pdf 
[  INFO ] An EXPERIMENTAL transition support for bespoke template is enabled. It is using the shared element transition API proposal and it is not yet stable. Recommend to use
          with --preview option for trying transitions. Track the latest information at https://github.com/marp-team/marp-cli/issues/382.
[  INFO ] Converting 1 markdown...
[  WARN ] Insecure local file accessing is enabled for conversion from test.md.
[  INFO ] test.md => test.pdf
% marpit -i test -o test -m pptx
[  INFO ] An EXPERIMENTAL transition support for bespoke template is enabled. It is using the shared element transition API proposal and it is not yet stable. Recommend to use
          with --preview option for trying transitions. Track the latest information at https://github.com/marp-team/marp-cli/issues/382.
[  INFO ] Converting 1 markdown...
[  WARN ] Insecure local file accessing is enabled for conversion from test.md.
[  INFO ] test.md => test.pptx

zshの制御構文については以下で抽出できる

man zshmisc | col -b | expand -t 4 | pcre2grep -Moe ' {0}(COMPLEX COMMANDS\n)( {7,}(\S* *)*\S*\n*)*'

感想

少しだけ仲良くなれたんじゃないかなと思います,以上で終わります

おまけ

man zshmoduleszparseoptsの部分を抜き出した

       zparseopts [ -D -E -F -K -M ] [ -a array ] [ -A assoc ] [ - ] spec ...
          This builtin simplifies the parsing of options in positional
          parameters, i.e. the set of arguments given by $*.  Each spec
          describes one option and must be of the form `opt[=array]'.  If an
          option described by opt is found in the positional parameters it
          is copied into the array specified with the -a option; if the
          optional `=array' is given, it is instead copied into that array,
          which should be declared as a normal array and never as an
          associative array.

          Note that it is an error to give any spec without an `=array'
          unless one of the -a or -A options is used.

          Unless the -E option is given, parsing stops at the first string
          that isn't described by one of the specs.  Even with -E, parsing
          always stops at a positional parameter equal to `-' or `--'. See
          also -F.

          The opt description must be one of the following.  Any of the
          special characters can appear in the option name provided it is
          preceded by a backslash.

          name
          name+  The name is the name of the option without the leading `-'.
             To specify a GNU-style long option, one of the usual two
             leading `-' must be included in name; for example, a
             `--file' option is represented by a name of `-file'.

             If a `+' appears after name, the option is appended to
             array each time it is found in the positional parameters;
             without the `+' only the last occurrence of the option is
             preserved.

             If one of these forms is used, the option takes no
             argument, so parsing stops if the next positional parameter
             does not also begin with `-' (unless the -E option is
             used).

          name:
          name:-
          name:: If one or two colons are given, the option takes an
             argument; with one colon, the argument is mandatory and
             with two colons it is optional.  The argument is appended
             to the array after the option itself.

             An optional argument is put into the same array element as
             the option name (note that this makes empty strings as
             arguments indistinguishable).  A mandatory argument is
             added as a separate element unless the `:-' form is used,
             in which case the argument is put into the same element.

             A `+' as described above may appear between the name and
             the first colon.

          In all cases, option-arguments must appear either immediately
          following the option in the same positional parameter or in the
          next one. Even an optional argument may appear in the next
          parameter, unless it begins with a `-'.  There is no special
          handling of `=' as with GNU-style argument parsers; given the spec
          `-foo:', the positional parameter `--foo=bar' is parsed as `--foo'
          with an argument of `=bar'.

          When the names of two options that take no arguments overlap, the
          longest one wins, so that parsing for the specs `-foo -foobar'
          (for example) is unambiguous. However, due to the aforementioned
          handling of option-arguments, ambiguities may arise when at least
          one overlapping spec takes an argument, as in `-foo: -foobar'. In
          that case, the last matching spec wins.

          The options of zparseopts itself cannot be stacked because, for
          example, the stack `-DEK' is indistinguishable from a spec for the
          GNU-style long option `--DEK'.  The options of zparseopts itself
          are:

          -a array
             As described above, this names the default array in which
             to store the recognised options.

          -A assoc
             If this is given, the options and their values are also put
             into an associative array with the option names as keys and
             the arguments (if any) as the values.

          -D     If this option is given, all options found are removed from
             the positional parameters of the calling shell or shell
             function, up to but not including any not described by the
             specs.  If the first such parameter is `-' or `--', it is
             removed as well.  This is similar to using the shift
             builtin.

          -E     This changes the parsing rules to not stop at the first
             string that isn't described by one of the specs.  It can be
             used to test for or (if used together with -D) extract
             options and their arguments, ignoring all other options and
             arguments that may be in the positional parameters.  As
             indicated above, parsing still stops at the first `-' or
             `--' not described by a spec, but it is not removed when
             used with -D.

          -F     If this option is given, zparseopts immediately stops at
             the first option-like parameter not described by one of the
             specs, prints an error message, and returns status 1.
             Removal (-D) and extraction (-E) are not performed, and
             option arrays are not updated.  This provides basic
             validation for the given options.

             Note that the appearance in the positional parameters of an
             option without its required argument always aborts parsing
             and returns an error as described above regardless of
             whether this option is used.

          -K     With this option, the arrays specified with the -a option
             and with the `=array' forms are kept unchanged when none of
             the specs for them is used.  Otherwise the entire array is
             replaced when any of the specs is used.  Individual
             elements of associative arrays specified with the -A option
             are preserved by -K.  This allows assignment of default
             values to arrays before calling zparseopts.

          -M     This changes the assignment rules to implement a map among
             equivalent option names.  If any spec uses the `=array'
             form, the string array is interpreted as the name of
             another spec, which is used to choose where to store the
             values.  If no other spec is found, the values are stored
             as usual.  This changes only the way the values are stored,
             not the way $* is parsed, so results may be unpredictable
             if the `name+' specifier is used inconsistently.

          For example,

             set -- -a -bx -c y -cz baz -cend
             zparseopts a=foo b:=bar c+:=bar

          will have the effect of

             foo=(-a)
             bar=(-b x -c y -c z)

          The arguments from `baz' on will not be used.

          As an example for the -E option, consider:

             set -- -a x -b y -c z arg1 arg2
             zparseopts -E -D b:=bar

          will have the effect of

             bar=(-b y)
             set -- -a x -c z arg1 arg2

          I.e., the option -b and its arguments are taken from the
          positional parameters and put into the array bar.

          The -M option can be used like this:

             set -- -a -bx -c y -cz baz -cend
             zparseopts -A bar -M a=foo b+: c:=b

          to have the effect of

             foo=(-a)
             bar=(-a '' -b xyz)

権限管理とシェルスクリプト

macOSです

諸々の確認

lsでフル表示させると

ls -l@FTOaehips

以上に加えて

  • -nするとownergroupがIDになる
  • -Rするとディレクトリ構造に対して再帰処理を行う
  • -Sするとサイズでソートする
  • -tすると時間でソートする
    • -tUすると作成created時間でソートする
    • -tcすると変更changed時間でソートする
    • -tuすると接触access時間でソートする
  • -rするとソートが逆転する
  • -Lすると最後までシンボリックリンクを追跡する
    • 表示されるファイル名はそのままだがそれ以外の情報は全て元のファイルのものになる
  • -Wするとホワイトアウトファイルも含めて表示する

ファイルの種類

英字10字の1つ目が該当する

soluna-eureka.hatenablog.com

まとめると

1文字目 名称 説明
- 普通のファイル エイリアスもこちらに含まれる,つまりエイリアスを相手にcdはできない /bin/zshとか
b ブロックデバイスブロックスペシャル)ファイル ブロックで1区画ずつ(ブロックずつ)データをやりとりする機器やソフトがosのファイルシステムを利用する際に使うエイリアス /dev/disk0とか,こういうのはdiskutil listで見れそう
c キャラクタデバイス(キャラクスペシャル)ファイル ストリームで1字づつ(キャラクタずつ)データをやりとりする機器やソフトがosのファイルシステムを利用する際に使うエイリアス /dev/nullとか/dev/stdinとか/dev/stdoutとか
d 普通のディレクト 特に考えなくて良い /binとか
l シンボリックリンク ls -Lでは表示されない,最後まで追跡されて解決する homebrew/usr/local/binシンボリックリンクを作りまくる
p FIFO,名前付きパイプ 先入先出(FIFO)でプロセス間の通信を実装できる,あくまで通信の順序によるFIFOでありプロセス立ち上げ順序ではない mkfifo testpipeで試してみると良いかも
s ドメインソケット ファイルシステムを利用してソケットが立ち上がるらしいが…わからん…なにこれ…
w whiteout(ホワイトアウト)ファイル -Wすると表示されるらしいが,恐らくこれは.whファイルではなく.whによって隠されるファイルを指す,やったことはないが事情を鑑みると後者だと思う
ネットで漁った情報を素人考えでまとめると,「互換性のあるファイルシステムを持つ異なるディレクトリ(それを含む別のディスクを新たに外から接続しても良い)を,1つのファイルシステムの特定のディレクトリにユニオンマウントすることで,従来よりも拡張されたストレージが構成できるが,もしそこに(ユニオンマウントする際に)読取のみの設定がされたディレクトリやファイルがある場合は,それを直接的に変更したり削除したりはできない,その代わりに変更や削除を記録できる場所に結果がコピーされるような設定ができて,『変更の場合はそのコピーを作成』・『削除の場合はそれを無視するための.whを作成』する」…ということになる?
BSD系osやLinux系osの一部が使える,UnionFSはその基本の存在でDockerはその改良版のAUFSを経て現在はOverlayFSが推奨されているらしい
osによってはlsが対応していない場合もありそう…

ascii.jp

dayflower.hatenablog.com

www.gcd.org

unix.stackexchange.com

www.techscore.com

ユニオンマウントの利点?

複数のディスク(物理)を用いる際に

  • 特定のディスクだけにRWを設定してアプリケーションに対するキャッシュサーバとして扱える
    • 書き込み負荷をキャッシュサーバに集中させられる
    • 壊れるディスクを敢えて絞り込めるから管理が楽
  • ROを設定したディスクに中身をまんまコピーして増やせる
    • 負荷分散(その性能はユニオンマウントするソフトにもよるだろうが)できる
    • もし壊れればコピペするだけで良いので管理が楽

ということなのだろうか…

権限の種類

英字10字の2つ目〜9つ目が該当する,別名ファイルモード(英語ではそう書かれる)

soluna-eureka.hatenablog.com

まとめると

  • 9つの文字は3つの欄で構成
    • owner,製作者・所有者
    • group,特定の利用者群
      • macOSではstaffadminwheelrootのどれか
      • デフォルトではownerprimary groupが優先される
      • どのユーザもprimary groupstaffになっている,UNIX系ならidと打って出てきたgidprimary groupを指すとか
    • other,それ以外の第三者
      • macOSではeveryoneと表記されているが,ここではotherで統一する
  • 1つの欄は3つの文字で構成
    • readable
      • ファイルなら読込可能
      • ディレクトリなら子要素の一覧が可能
      • rならOK,-ならNG
      • 数字では4
    • writable
      • ファイルなら書込可能
      • ディレクトリなら子要素の作成・削除・属性の編集が可能
      • wならOK,-ならNG
      • 数字では2
    • executable/searchable
      • ファイルなら実行可能
      • ディレクトリなら子要素の中身の利用・移動が可能
      • xならOK,-ならNG
      • 数字では1
  • それぞれの欄は数字の和算で表現することができる +124 はどう足しても重複が起きないので

オプション

上記に加えてsetuidsetgidstickyが各欄の3つ目に出現する時がある,ファイルシステムによってはその表示が異なるらしいのだが,macOSにおいては以下となる

owner group other
setuid s(1) or S(2) - -
setgid - s(3) or S(4) -
sticky - - t(5) or T(6)

これを各種の権限に当てはめて整理するとと以下となる

(1) (2) (3) (4) (5) (6)
file 実行可能+setuid 実行不可能+setuid 実行可能+setgid
Linuxだと効果がないらしい
実行不可能+setgidLinuxだと効果がないらしい - -
directory - - Linuxとは違いこっちは効果がない
あっちは子要素を所有するgroupを同期させられるらしい
Linuxとは違いこっちは効果がない
あっちは子要素の所有するgroupを同期させられるとか
利用可能+sticky 利用不可能+sticky

setuid

owner以外でもowner(の持つuid)の権限でファイルを実行できる,使い方を間違えたり脆弱性があったりすると危険
数字では4

setgid

ownerの所属するgroup以外でもownerの所属するgroup(の持つuid)の権限でファイルを実行できる←よくわからないけどmanページに本当にこう書いてあるからしょうがないもん!怖くて使えないけど(groupの操作に慣れてないだけ)
数字では2

sticky

このディレクトリの子要素であるファイル・ディレクトリは共にowner以外の削除と属性変更が拒否される,/tmpとか
数字では1

拡張属性

英字10字の後に@がある場合はOSによる拡張属性が設定されている,例えば~(ユーザのホームディレクトリ)は

 com.apple.FinderInfo      32B 
 0: group:everyone deny delete

と表示されるはず,このように単にrwxだけでは指定できない特定の操作(この場合は削除)をバインドしてる場合が多そう

拡張ACL

英字10字の後に+がある場合はOSによる拡張セキュリティが設定されている…らしいのだが,未だに見たことがない,これはアクセスコントロールリスト(ACL)とも呼ばれる

その他の情報

以上で基本的な管理については事足りると思うのだが,ls -l@FTOaehipsで全て表示されるものは何かをそれぞれメモしておく

[iノード番号] [ブロックサイズ] [権限+拡張属性+拡張ACL] [リンク数] [owner] [group] [ファイルフラグ] [ファイルサイズ] [最終更新日] [名前]

iノード番号

UNIX系システムは属性情報を管理・追跡するために固定番号を使用している.
該当のオブジェクトがファイルシステム上から消されたりしない限りは変わらない.

ブロックサイズ

UNIX系システムはデータを書き込む際にこの単位を利用している.
macOSにおいては,最低でもオブジェクトに対して1つあたり4KB(4KiByte)=4096B(4096Byte)が確保され,これが8blockに相当することから1blockあたり512Byteであるとわかる.この512Byteこそが一般的なSSD・HDD・RAMで用いられる物理的なブロックに相当するが,macOSのAPFSは4KiByte=8blockごとで使用するため(これをアロケーションと呼ぶ),要は4KiByte(これをアロケーションユニットサイズと呼ぶ)に満たないファイルを大量に生成することは,ストレージ節約の観点から見る限りでは無駄だということがわかる.一応ファイルサイズが0ならブロックサイズも0なので安心できる.(まぁiノード番号を浪費するかもしれないけど今時の64bitならそう困らないはず)

普通のディレクト

ディレクトリエントリの情報を持つのでファイルサイズは0ではない…のだがブロックサイズは0である.どこかしら実体があるだろうと思われるが情報が見つからない,iノード番号と似たような場所にあると見ているのだがAPFSの実装の肝になってそうなので割と社外秘(部外秘)な情報ではないかとも予想している.

名前付きパイプ

ファイルサイズが0,すなわちブロックサイズも0である.

シンボリックリンク

パスの情報を持つのでファイルサイズは0ではない…のだがブロックサイズはなぜか0である,普通のディレクトリと同じ事情があるのではないかと予想している.

ハードリンク

同じiノード番号を参照するので実体としてディスクを消費することはないが,それも考慮した上で丁寧にカウントしないと実際のディスク消費量から外れた値を得る羽目になる.表示されるファイルサイズとブロックサイズも,iノード番号が同じであれば同じである.

qiita.com

okmount.hatenablog.com

リンク数

あるiノード番号を持つオブジェクトに対するハードリンクの数,実は./(自己参照)も../(親参照)もハードリンクの扱いであるため,子ディレクトリを増やすと親ディレクトリのリンク数は必ず1増える.シンボリックリンクはiノード番号が異なるためにリンク数にはカウントされない.
ちなみにシンボリックリンクに対してハードリンクを生成しようとすると,シンボリックリンクが参照した実体のiノード番号を持つハードリンクが生成される,これは何重にシンボリックリンクを重ねても変わらない.つまりシンボリックリンクのiノード番号はシンボリックリンクについて固有であり,シンボリックリンクを重ねる場合はそれぞれにiノード番号が与えられている.

ファイルフラグ

通常の権限の種類とはまた異なるもので,アプリケーションも含めた様々な表示や動作について簡単に縛ることができる.
例えばmacOSSIPが有効な状態ではrestrictedフラグが付いたものは削除や変更ができなくなる,/bin/zshなんかそう.

apple.stackexchange.com

ファイルサイズ

普通なら単位はバイト,-hで単位も表示される.

最終更新日

-tUすると作成created時間,-tcすると変更changed時間,-tuすると接触access時間になる,使い分けよう.

権限の管理

chmod [1桁目][2桁目][3桁目][4桁目] [ファイル名orディレクトリ名]で数字を並べれば良い,前述したリストとパターンを記憶できたら以下を設定する
ちなみにアルファベットを並べての設定はできない,数字で考える習慣を身につけよう

オプション

私は保険のために常に-vhしておきたい,ログも残るしシンボリックリンク先を破壊してしまうのは非常によくない気がする

1桁目

オプション設定ができる
それぞれの欄で実行可能かどうかを設定するのは後の3桁で出来るのでここでは考えなくても良い
特に需要がなければ意図的に0を入れてもいいし入れなくても(省略しても)いい

2桁目

owner欄の設定をする

3桁目

group欄の設定をする

4桁目

other欄の設定をする

所有の管理

chownchgrpは現時点ではここでは扱わない(追記こそするかもしれないが),本題から逸れすぎるし私がmacOSで多用する機会もそうそうなさそうなので許してくれ

シェルスクリプト

1行目で#/bin/zshで実行シェルを指定してから(シェバンと言う)スクリプトを書く,所用のディレクトリにパスを通したら

touch zshTest1
ls -l@FTOaehips testZsh1
# => <inodeID> <BlockSize> -rw-r--r--  1 <UserName>  Staff  -   <FileSize> <Last Updated Time> testZsh1
vi zshTese1
#!/bin/zsh
#testZsh1

echo "this is test script 1"

しかしこの権限ではzshを通して実行できる(自分が権限を持つzshtestZsh1を読み込んで実行しただけなので)ものの単品では実行できない(明らかに-rw-r--r--なので),これは面倒というか不都合である

testZsh1 
# => zsh: permission denied: testZsh1
zsh testZsh1 
# => this is test script 1

なので権限を変更すれば良い,これで単品で実行できるようになる

chmod 0744 testZsh1 
testZsh1           
# => this is test script 1
ls -l@FTOaehips testZsh1
# => <inodeID> <BlockSize> -rwxr--r--  1 <UserName>  Staff  -   <FileSize> <Last Updated Time> testZsh1

なお,owner欄の実行権限の有無によってシェルスクリプトファイルの表示がFinder上で変化する今まで気づかなかった

おまけ

man lsDESCRIPTIONを抜き出した

DESCRIPTION
     For each operand that names a file of a type other than directory, ls
     displays its name as well as any requested, associated information.  For
     each operand that names a file of type directory, ls displays the names of
     files contained within that directory, as well as any requested, associated
     information.

     If no operands are given, the contents of the current directory are
     displayed.  If more than one operand is given, non-directory operands are
     displayed first; directory and non-directory operands are sorted separately
     and in lexicographical order.

     The following options are available:

     -@      Display extended attribute keys and sizes in long (-l) output.

     -A      Include directory entries whose names begin with a dot (‘.’) except
         for . and ...  Automatically set for the super-user unless -I is
         specified.

     -B      Force printing of non-printable characters (as defined by ctype(3)
         and current locale settings) in file names as \xxx, where xxx is
         the numeric value of the character in octal.  This option is not
         defined in IEEE Std 1003.1-2008 (“POSIX.1”).

     -C      Force multi-column output; this is the default when output is to a
         terminal.

     -D format
         When printing in the long (-l) format, use format to format the
         date and time output.  The argument format is a string used by
         strftime(3).  Depending on the choice of format string, this may
         result in a different number of columns in the output.  This option
         overrides the -T option.  This option is not defined in IEEE Std
         1003.1-2008 (“POSIX.1”).

     -F      Display a slash (‘/’) immediately after each pathname that is a
         directory, an asterisk (‘*’) after each that is executable, an at
         sign (‘@’) after each symbolic link, an equals sign (‘=’) after
         each socket, a percent sign (‘%’) after each whiteout, and a
         vertical bar (‘|’) after each that is a FIFO.

     -G      Enable colorized output.  This option is equivalent to defining
         CLICOLOR or COLORTERM in the environment and setting --color=auto.
         (See below.)  This functionality can be compiled out by removing
         the definition of COLORLS.  This option is not defined in IEEE Std
         1003.1-2008 (“POSIX.1”).

     -H      Symbolic links on the command line are followed.  This option is
         assumed if none of the -F, -d, or -l options are specified.

     -I      Prevent -A from being automatically set for the super-user.  This
         option is not defined in IEEE Std 1003.1-2008 (“POSIX.1”).

     -L      Follow all symbolic links to final target and list the file or
         directory the link references rather than the link itself.  This
         option cancels the -P option.

     -O      Include the file flags in a long (-l) output.  This option is
         incompatible with IEEE Std 1003.1-2008 (“POSIX.1”).  See chflags(1)
         for a list of file flags and their meanings.

     -P      If argument is a symbolic link, list the link itself rather than
         the object the link references.  This option cancels the -H and -L
         options.

     -R      Recursively list subdirectories encountered.

     -S      Sort by size (largest file first) before sorting the operands in
         lexicographical order.

     -T      When printing in the long (-l) format, display complete time
         information for the file, including month, day, hour, minute,
         second, and year.  The -D option gives even more control over the
         output format.  This option is not defined in IEEE Std 1003.1-2008
         (“POSIX.1”).

     -U      Use time when file was created for sorting or printing.  This
         option is not defined in IEEE Std 1003.1-2008 (“POSIX.1”).

     -W      Display whiteouts when scanning directories.  This option is not
         defined in IEEE Std 1003.1-2008 (“POSIX.1”).

     -a      Include directory entries whose names begin with a dot (‘.’).

     -b      As -B, but use C escape codes whenever possible.  This option is
         not defined in IEEE Std 1003.1-2008 (“POSIX.1”).

     -c      Use time when file status was last changed for sorting or printing.

     --color=when
         Output colored escape sequences based on when, which may be set to
         either always, auto, or never.

         always will make ls always output color.  If TERM is unset or set
         to an invalid terminal, then ls will fall back to explicit ANSI
         escape sequences without the help of termcap(5).  always is the
         default if --color is specified without an argument.

         auto will make ls output escape sequences based on termcap(5), but
         only if stdout is a tty and either the -G flag is specified or the
         COLORTERM environment variable is set and not empty.

         never will disable color regardless of environment variables.
         never is the default when neither --color nor -G is specified.

         For compatibility with GNU coreutils, ls supports yes or force as
         equivalent to always, no or none as equivalent to never, and tty or
         if-tty as equivalent to auto.

     -d      Directories are listed as plain files (not searched recursively).

     -e      Print the Access Control List (ACL) associated with the file, if
         present, in long (-l) output.

     -f      Output is not sorted.  This option turns on -a.  It also negates
         the effect of the -r, -S and -t options.  As allowed by IEEE Std
         1003.1-2008 (“POSIX.1”), this option has no effect on the -d, -l,
         -R and -s options.

     -g      This option has no effect.  It is only available for compatibility
         with 4.3BSD, where it was used to display the group name in the
         long (-l) format output.  This option is incompatible with IEEE Std
         1003.1-2008 (“POSIX.1”).

     -h      When used with the -l option, use unit suffixes: Byte, Kilobyte,
         Megabyte, Gigabyte, Terabyte and Petabyte in order to reduce the
         number of digits to four or fewer using base 2 for sizes.  This
         option is not defined in IEEE Std 1003.1-2008 (“POSIX.1”).

     -i      For each file, print the file's file serial number (inode number).

     -k      This has the same effect as setting environment variable BLOCKSIZE
         to 1024, except that it also nullifies any -h options to its left.

     -l      (The lowercase letter “ell”.) List files in the long format, as
         described in the The Long Format subsection below.

     -m      Stream output format; list files across the page, separated by
         commas.

     -n      Display user and group IDs numerically rather than converting to a
         user or group name in a long (-l) output.  This option turns on the
         -l option.

     -o      List in long format, but omit the group id.

     -p      Write a slash (‘/’) after each filename if that file is a
         directory.

     -q      Force printing of non-graphic characters in file names as the
         character ‘?’; this is the default when output is to a terminal.

     -r      Reverse the order of the sort.

     -s      Display the number of blocks used in the file system by each file.
         Block sizes and directory totals are handled as described in The
         Long Format subsection below, except (if the long format is not
         also requested) the directory totals are not output when the output
         is in a single column, even if multi-column output is requested.
         (-l) format, display complete time information for the file,
         including month, day, hour, minute, second, and year.  The -D
         option gives even more control over the output format.  This option
         is not defined in IEEE Std 1003.1-2008 (“POSIX.1”).

     -t      Sort by descending time modified (most recently modified first).
         If two files have the same modification timestamp, sort their names
         in ascending lexicographical order.  The -r option reverses both of
         these sort orders.

         Note that these sort orders are contradictory: the time sequence is
         in descending order, the lexicographical sort is in ascending
         order.  This behavior is mandated by IEEE Std 1003.2 (“POSIX.2”).
         This feature can cause problems listing files stored with
         sequential names on FAT file systems, such as from digital cameras,
         where it is possible to have more than one image with the same
         timestamp.  In such a case, the photos cannot be listed in the
         sequence in which they were taken.  To ensure the same sort order
         for time and for lexicographical sorting, set the environment
         variable LS_SAMESORT or use the -y option.  This causes ls to
         reverse the lexicographical sort order when sorting files with the
         same modification timestamp.

     -u      Use time of last access, instead of time of last modification of
         the file for sorting (-t) or long printing (-l).

     -v      Force unedited printing of non-graphic characters; this is the
         default when output is not to a terminal.

     -w      Force raw printing of non-printable characters.  This is the
         default when output is not to a terminal.  This option is not
         defined in IEEE Std 1003.1-2001 (“POSIX.1”).

     -x      The same as -C, except that the multi-column output is produced
         with entries sorted across, rather than down, the columns.

     -y      When the -t option is set, sort the alphabetical output in the same
         order as the time output.  This has the same effect as setting
         LS_SAMESORT.  See the description of the -t option for more
         details.  This option is not defined in IEEE Std 1003.1-2001
         (“POSIX.1”).

     -%      Distinguish dataless files and directories with a '%' character in
         long

     -1      (The numeric digit “one”.) Force output to be one entry per line.
         This is the default when output is not to a terminal.  (-l) output,
         and don't materialize dataless directories when listing them.

     -,      (Comma) When the -l option is set, print file sizes grouped and
         separated by thousands using the non-monetary separator returned by
         localeconv(3), typically a comma or period.  If no locale is set,
         or the locale does not have a non-monetary separator, this option
         has no effect.  This option is not defined in IEEE Std 1003.1-2001
         (“POSIX.1”).

     The -1, -C, -x, and -l options all override each other; the last one
     specified determines the format used.

     The -c, -u, and -U options all override each other; the last one specified
     determines the file time used.

     The -S and -t options override each other; the last one specified
     determines the sort order used.

     The -B, -b, -w, and -q options all override each other; the last one
     specified determines the format used for non-printable characters.

     The -H, -L and -P options all override each other (either partially or
     fully); they are applied in the order specified.

     By default, ls lists one entry per line to standard output; the exceptions
     are to terminals or when the -C or -x options are specified.

     File information is displayed with one or more ⟨blank⟩s separating the
     information associated with the -i, -s, and -l options.

DESCRIPTION
     In legacy mode, the -f option does not turn on the -a option and the -g,
     -n, and -o options do not turn on the -l option.

     Also, the -o option causes the file flags to be included in a long (-l)
     output; there is no -O option.

     When -H is specified (and not overridden by -L or -P) and a file argument
     is a symlink that resolves to a non-directory file, the output will reflect
     the nature of the link, rather than that of the file.  In legacy operation,
     the output will describe the file.

     For more information about legacy mode, see compat(5).


man chmodMODEを抜き出した

MODES
     Modes may be absolute or symbolic.  An absolute mode is an octal number
     constructed from the sum of one or more of the following values:

       4000    (the setuid bit).  Executable files with this bit set will
           run with effective uid set to the uid of the file owner.
           Directories with this bit set will force all files and sub-
           directories created in them to be owned by the directory
           owner and not by the uid of the creating process, if the
           underlying file system supports this feature: see chmod(2)
           and the suiddir option to mount(8).
       2000    (the setgid bit).  Executable files with this bit set will
           run with effective gid set to the gid of the file owner.
       1000    (the sticky bit).  See chmod(2) and sticky(7).
       0400    Allow read by owner.
       0200    Allow write by owner.
       0100    For files, allow execution by owner.  For directories, allow
           the owner to search in the directory.
       0040    Allow read by group members.
       0020    Allow write by group members.
       0010    For files, allow execution by group members.  For
           directories, allow group members to search in the directory.
       0004    Allow read by others.
       0002    Allow write by others.
       0001    For files, allow execution by others.  For directories allow
           others to search in the directory.

     For example, the absolute mode that permits read, write and execute by the
     owner, read and execute by group members, read and execute by others, and
     no set-uid or set-gid behaviour is 755 (400+200+100+040+010+004+001).

     The symbolic mode is described by the following grammar:

       mode     ::= clause [, clause ...]
       clause   ::= [who ...] [action ...] action
       action   ::= op [perm ...]
       who      ::= a | u | g | o
       op       ::= + | - | =
       perm     ::= r | s | t | w | x | X | u | g | o

     The who symbols ``u'', ``g'', and ``o'' specify the user, group, and other
     parts of the mode bits, respectively.  The who symbol ``a'' is equivalent
     to ``ugo''.

     The perm symbols represent the portions of the mode bits as follows:

       r       The read bits.
       s       The set-user-ID-on-execution and set-group-ID-on-execution
           bits.
       t       The sticky bit.
       w       The write bits.
       x       The execute/search bits.
       X       The execute/search bits if the file is a directory or any of
           the execute/search bits are set in the original (unmodified)
           mode.  Operations with the perm symbol ``X'' are only
           meaningful in conjunction with the op symbol ``+'', and are
           ignored in all other cases.
       u       The user permission bits in the original mode of the file.
       g       The group permission bits in the original mode of the file.
       o       The other permission bits in the original mode of the file.

     The op symbols represent the operation performed, as follows:

     +     If no value is supplied for perm, the ``+'' operation has no effect.
       If no value is supplied for who, each permission bit specified in
       perm, for which the corresponding bit in the file mode creation mask
       (see umask(2)) is clear, is set.  Otherwise, the mode bits
       represented by the specified who and perm values are set.

     -     If no value is supplied for perm, the ``-'' operation has no effect.
       If no value is supplied for who, each permission bit specified in
       perm, for which the corresponding bit in the file mode creation mask
       is set, is cleared.  Otherwise, the mode bits represented by the
       specified who and perm values are cleared.

     =     The mode bits specified by the who value are cleared, or, if no who
       value is specified, the owner, group and other mode bits are cleared.
       Then, if no value is supplied for who, each permission bit specified
       in perm, for which the corresponding bit in the file mode creation
       mask is clear, is set.  Otherwise, the mode bits represented by the
       specified who and perm values are set.

     Each clause specifies one or more operations to be performed on the mode
     bits, and each operation is applied to the mode bits in the order
     specified.

     Operations upon the other permissions only (specified by the symbol ``o''
     by itself), in combination with the perm symbols ``s'' or ``t'', are
     ignored.

     The ``w'' permission on directories will permit file creation, relocation,
     and copy into that directory.  Files created within the directory itself
     will inherit its group ID.


MODES
     644       make a file readable by anyone and writable by the owner
           only.

     go-w      deny write permission to group and others.

     =rw,+X    set the read and write permissions to the usual defaults, but
           retain any execute permissions that are currently set.

     +X        make a directory or file searchable/executable by everyone if
           it is already searchable/executable by anyone.

     755
     u=rwx,go=rx
     u=rwx,go=u-w  make a file readable/executable by everyone and writable by
           the owner only.

     go=       clear all mode bits for group and others.

     g=u-w     set the group bits equal to the user bits, but clear the
           group write bit.


manページで特定のサブセクションだけ読みたい…読みたくない?

manページで覚えたいところだけいい感じに.txtに保管しようにもデフォルトだと長すぎて編集がめんどくさいんだよな
macOSです

イデア

man xxx | backspase消去 | tab->space変換 | 正規表現でyyyにマッチ > xxx_yyy.txt

backspase消去にはcoltab->space変換にはexpand正規表現yyyにマッチにはpcre2grep…を使いたい
特に今回はセクションを判定して複数行を抜き取る動作をやりたいので,通常のgrepとは異なるpcre2grepを利用する

why pcre2grep

一応はgrepでもマルチライン処理はできるらしいが,もちろんデフォルトでは対応していない,それをするには

  • PCREPerl Compatible Regular Expressions)を-Pオプションで呼び出す必要がある
    • GNUによって用意されているlibpcreが処理に噛んでくる
  • -PオプションはGNUgrepにのみ実装されている
  • そしてmacOSgrepgnuのものではない
    • つまりmacOSではデフォルトでgrep -Pが使えない
  • homebrewからgnu grepを入れても良いが,
    • ggrepとかいう名前で使うのは嫌だし
    • 開発終了のzipならともかくmacOSでもgnuでも現在進行形でメンテされてるgrepはちょっと怖くて併用できん
  • そもそもPCRE.orgが出してるpcre2homebrewで入れれば良くね?
    • 規格を作ってる組織が出す公式ライブラリなんだから信頼できる
    • homebrewgitを入れている場合はpcre2が依存先になっているので既に入ってたりする
      • pcreは古くて非推奨なので(今もまだffmpegとか依存しているけど)なければpcre2を入れよう
    • そのライブラリにAPIとしてのpcre2grepがついてくる(他にも色々ついてくるっぽい)

…ということでpcre2grepを採用したい

why expand

trはちょっと難しそうだしexpandがあるならこっちの方が楽じゃん
タブをエクスパンドするからそう呼ばれるのか

テンプレート

 man [コマンド名] | col -b | expand -t [1タブあたりの空白数] | pcre2grep -Moe ' {[タイトルの深さ(空白数)]}([タイトル]\n)( {[本文の深さ],}(\S* *)*\S*\n*)*'

コマンドラインオプションの説明は省くがこれで過不足はない

.txtに出力

 man xxx | col -b | expand -t 4 | pcre2grep -Moe ' {3}(yyy\n)( {5,}(\S* *)*\S*\n*)*' > xxx_yyy.txt

リダイレクション演算子のうち>>は追記で>は置換らしい

使用例

% man ls | col -b | expand -t 4 | pcre2grep -Moe ' {3}(The Long Format\n)( {5,}(\S* *)*\S*\n*)*'
   The Long Format
     If the -l option is given, the following information is displayed for each
     file: file mode, number of links, owner name, group name, number of bytes
     in the file, abbreviated month, day-of-month file was last modified, hour
     file last modified, minute file last modified, and the pathname.  If the
     file or directory has extended attributes, the permissions field printed by
     the -l option is followed by a '@' character.  Otherwise, if the file or
     directory has extended security information (such as an access control
     list), the permissions field printed by the -l option is followed by a '+'
     character.  If the -% option is given, a '%' character follows the
     permissions field for dataless files and directories, possibly replacing
     the '@' or '+' character.

     If the modification time of the file is more than 6 months in the past or
     future, and the -D or -T are not specified, then the year of the last
     modification is displayed in place of the hour and minute fields.

     If the owner or group names are not a known user or group name, or the -n
     option is given, the numeric ID's are displayed.

     If the file is a character special or block special file, the device number
     for the file is displayed in the size field.  If the file is a symbolic
     link the pathname of the linked-to file is preceded by “->”.

     The listing of a directory's contents is preceded by a labeled total number
     of blocks used in the file system by the files which are listed as the
     directory's contents (which may or may not include . and .. and other files
     which start with a dot, depending on other options).

     The default block size is 512 bytes.  The block size may be set with option
     -k or environment variable BLOCKSIZE.  Numbers of blocks in the output will
     have been rounded up so the numbers of bytes is at least as many as used by
     the corresponding file system blocks (which might have a different size).

     The file mode printed under the -l option consists of the entry type and
     the permissions.  The entry type character describes the type of file, as
     follows:

       -     Regular file.
       b     Block special file.
       c     Character special file.
       d     Directory.
       l     Symbolic link.
       p     FIFO.
       s     Socket.
       w     Whiteout.

     The next three fields are three characters each: owner permissions, group
     permissions, and other permissions.  Each field has three character
     positions:

       1.   If r, the file is readable; if -, it is not readable.

       2.   If w, the file is writable; if -, it is not writable.

       3.   The first of the following that applies:

              S     If in the owner permissions, the file is not
                executable and set-user-ID mode is set.  If in the
                group permissions, the file is not executable and
                set-group-ID mode is set.

              s     If in the owner permissions, the file is executable
                and set-user-ID mode is set.  If in the group
                permissions, the file is executable and setgroup-ID
                mode is set.

              x     The file is executable or the directory is
                searchable.

              -     The file is neither readable, writable, executable,
                nor set-user-ID nor set-group-ID mode, nor sticky.
                (See below.)

        These next two apply only to the third character in the last
        group (other permissions).

              T     The sticky bit is set (mode 1000), but not execute
                or search permission.  (See chmod(1) or sticky(7).)

              t     The sticky bit is set (mode 1000), and is searchable
                or executable.  (See chmod(1) or sticky(7).)

     The next field contains a plus (‘+’) character if the file has an ACL, or a
     space (‘ ’) if it does not.  The ls utility does not show the actual ACL;
     use getfacl(1) to do this.


おまけ

PCRE2対応の正規表現チェッカーならここが良さそうね

regex101.com

GTM(Google Tag Manager)とGA4の設定・開発・連携・解析

※初見1日目の感想を多分に含みます
※完全にチュートリアルです
※以下の続きです

soluna-eureka.hatenablog.com

概要

従来は単純にWebページの遷移を基準にして観測するだけでも良かったが,フレームワークが提供するイベント駆動が主体となるシングルページアプリケーションを相手に解析する上では力不足であり,しかもプロトコルの発展やスマホの普及やアプリの開発に伴って後者が主流となり始めた.そういう世相に対応すべくしてGA4が2020年後期に新たにリリースされ,今後はサービスを受けるクライアントのブラウザでイベントの発火を観測して解析する時代になると考えられる(GTMのコンテナの設定によってはサーバサイドで観測できる,ブラウザのスクリプトが省けるのでメンテナンスしやすく動作も軽く安全性も高い).
そして「用途ごとにタグをバラバラに設定する」「タグごとに処理をバラバラに定義する」「1つのサービスにバラバラにコードを埋め込む」より,「1つのコードを埋め込む」「そこから用途に応じて柔軟にタグを生成する」「観測をまとめて設定する」「そのまま解析する」方が,開発やメンテナンスの効率を高めることができるだろう,そしてそのためにあるのがGTMである.

簡潔に要約すれば「たかが観測の事情に合わせるためだけに,手動でHTMLやフレームワークを弄るのはやめよう,埋め込んだポイント1つから全て管理して,さらにイベント駆動に合わせて発火させた方が,より現代的で良い」という感じになりそう…?

やる前に

念のために前回と同様CSP設定を解除しておこう

導入

アカウント作成

  • ここに行く
  • 「新しいアカウントの追加」を選択(アカウントがないなら)
    • 権限や管理は大体GA4と同じ雰囲気がある
  • アカウント名を設定
    • GA4のアカウント名と同じようなものか
    • ⚠︎GTMアカウントとGA4アカウントとGmailアカウントは別物です!⚠︎
  • 質問には適当に回答
    • 自分しか見ないならデータ共有の類は全て却下して良さげ
    • 後からでも設定できるので問題なし

コンテナ作成

  • コンテナ名は識別しやすくサービス名を表現する
    • 「HatenaBlog」にした
  • 場所は「Web」を選択

トリガーを設定

クライアントのブラウザの挙動に紐づけたトリガを設定する,その挙動はブラウザ上のGTMのスクリプトが監視している.
今回は最も簡単な「全てに反応する」タイプを選択するが,実際は動作を限定してトリガに設定することもできる(クリック,スクロール,フォーム送信,要素の表示,DOM Ready…)し,観測したいものを限定するためにも設定するべきだと思われる.

  • 「トリガー」「新規」
  • トリガ名を設定
    • 機能や対応するイベントを簡潔に表現する
    • 「ChoHoChoDo_trigger」にした
  • 「ページビュー」を選択
  • 「一部のページビュー」から「Page Hostname」「等しい」でホスト名を指定できる
    • 「soluna-eureka.hatenablog.com」にした

タグを設定

トリガの要求に基づきタグを配信する,その中身はGA4で既に設定したものにする,これによりタグが動的に配信される.
1つのサービスにつき1つのGTMコンテナと対応するGTMスクリプトを与えることで,GA4の都合に柔軟に対応できる.

  • 「タグ」「新規」
  • タグ名を設定
    • GA4のデータストリームの名前に合わせた方が分かりやすそう
    • 「ChoHoChoDo」にした
  • 種類は「Googleアナリティクス:GA4設定」
    • このタグタイプは色々あり,Google謹製に加えてコミュニティ提供もある
    • その気になれば「カスタムHTML(script埋め込み可)」で自作できるとか
  • 「測定ID」には事前にGA4で得た測定IDをコピペ
  • 設定フィールドに「debug_mode:true」を入れるとGA4のデバッグモードを使えるようになる
    • これはGTMのデバッグモードとは別物だが,GTMで配信するタグのプロパティを変えることでGA4に影響を与えられる
  • 詳細設定の呼び出しオプションを「無制限」に設定
  • トリガーには先に設定したトリガを変数として指定

ワークスペース,プレビュー,マージ,デプロイ

GTM上で作業を進めるワークスペースは1つのコンテナでいくつか(無料個人アカウントは3つまで)の運用が可能で,複数人で別々のワークスペースごとに作業をした後は,gitのように代表ワークスペースへマージが行える.コンフリクト?競合?衝突?が発生したら手動で直すらしい,その際にはちゃんと通知が出るそうなので,皆んなで協力して完全なマージを目指そう.
またワークスペースごとにプレビューが行える,通常のデバッグページとは異なりプレビュー用の環境が用意されるので,タグが想定通りの動作をしているかどうかを確認しよう(URLをコピペするだけで共有ので,異なる環境でテストを実行できる).

ちなみにChrome限定の拡張機能であるTag Assistant Companion からは通常のデバッグページが開くけど重いので,少しだけ見たいような時はTag Assistant Legacyで十分だと思う,こちらもエラーメッセージなどはちゃんと表示されるので大丈夫.

そうした作業が終わり次第でコンテナをデプロイ(公開)することができるようになる,これを経てようやくGTMがアクセスを受け入れ始める.ワークスペースの公開を押すかバージョンから選択して公開するかでOK,後はもうサービスに埋め込むだけ…

GA4のデバッグ

こちらはChrome限定の拡張機能であるGoogle Analytics Debuggerを入れた上で対象のページを開きつつGA4のページの「設定」→「Debug View」を押すと利用できる,GTMが動作したらGA4も動作しているかどうか確認しよう.

利用

「管理」→「Googleタグマネージャーをインストール」に移動,指示に従いなるべく規定の場所にそのコードをコピペする
はてブロの場合「設定」→「詳細設定」→「Googleタグマネージャ」にコンテナのIDを貼り付けよう,GA4のIDではない.

解析

GA4で行う,データが手元に出揃い次第で追記する,とりあえずアクセス分類系のグラフ作成はやりたいが1週間は欲しい

放置してたら5日分のデータが集まったのでやってみる
※最低でも2日分以上のデータがないと正常に動作しないっぽい

レポートのスナップショット

  1. 右上のカレンダーから日付を選択
  2. 自由選択が可能
  3. 右上のパッドから項目を編集
  4. 「平均エンゲージメント時間、他 2 個」
    • 「滞在した時の平均滞在時間」「ユーザあたり滞在ページ数」「全ての場合の平均滞在時間」

    • カラムの横幅が足りずに表示を切り替えられない場合がある

  5. 「リアルタイム」
    • 直近30分限定
  6. 「表示回数(ページタイトルとスクリーンクラス)」
    • ページ名が表示される
  7. 「セッション(市区町村)」
    • アクセス元が表示される

f:id:Soluna_Eureka:20211225104935p:plain

「ユーザー」とは?

Cookieかなんかを使ってユーザ数を判定していると思われる(詳細不明),つまり1Cookieにつき1人っぽい

support.google.com

「セッション」とは?

サイトに来てから出る(≡30分経過)までを1セッションとする,つまりユーザ数<セッション数になり得る
↓これはUAの記事だけど大体は同じっぽい

support.google.com

実際は30分から変更することもできるらしい,今回は変更していない

support.google.com

「エンゲージメント」とは?

詳細は不明だが通常はページを読んだと判定されたら1エンゲージメントとなる,GA4の判定機能はそこそこ高性能らしい
ユーザ数<エンゲージメント数もあり得るし,セッション数<エンゲージメント数もあり得る

support.google.com

「イベント」とは?

ユーザが何かする度にイベントは発生する,GA4は端末上でそれを観測してサーバに送信する,それを集計して得られるもの

support.google.com

基本的な(デフォルトで使える)(必ず観測される)イベントの一覧

support.google.com

データストリームの設定を少し弄るだけで追加で使えるイベント一覧

support.google.com

カスタムイベントも実装できるらしいが今回は扱わない,むしろ自力で実装してまで使った方が良い場面がどれほどあるのか…

「表示回数」vs「閲覧開始数」

前者はイベント「page_view(Web向け)」「screen_view(App向け)」が発生した数の合計(ページの読み込みor閲覧履歴の更新の度に発火する),後者は任意のページがセッションのエントリーポイントとなった回数(つまり遷移した先はノーカン)

support.google.com

ちょっと前者の発生条件や計測方法がイマイチよくわからない,ちなみに「scroll(9割スクロールしたら1回だけカウント)」も含めて拡張計測機能を設定しないと収集してくれない,せっかくなので試してみよう

f:id:Soluna_Eureka:20211225104600p:plain

リアルタイム

直近30分の動向を全て確認できる,表示内容はカスタマイズできない
※地図上では普通に市町村のレベルで表示されるので,個人情報を扱うサービスを運営している時は気をつけよう

f:id:Soluna_Eureka:20211225105401p:plain

探索

  • なんでも良いのでとりあえず「空白」から作成してみよう
    • エクセルみたいに1つのワークシートで複数のページを使えるから問題ない
    • 用意されているテンプレートもページごとに使い分けられるから問題ない

ソース別アクセス数

実ははてブロを3つに分けて使っているので,それぞれに個別のデータストリームをGA4で用意して,それに対応するトリガとタグをGTMで用意した,そのおかげでデータストリームで分類すればブログごとの数字が得られるということになる
※実際のところストリームの複数設置はあまり推奨されないらしい

手法は「自由形式」を選択,ビジュアリゼーションを「折れ線グラフ」に設定,指標に「表示回数」を呼び出して値に代入,ディメンションに「ストリーム名」を呼び出して内訳に代入

f:id:Soluna_Eureka:20211225110229p:plain

地域別アクセス数

手法は「自由形式」を選択,ビジュアリゼーションを「ドーナツグラフ」に設定,指標に「表示回数」を呼び出して値に代入,ディメンションに「地域」を呼び出して内訳に代入

f:id:Soluna_Eureka:20211225110735p:plain

ちなみに東京都と千葉県だけでほぼ半分を占めていたし神奈川県を含めれば3分の2に届きそうだった,地域格差…ってコト!?

ソース別エンゲージメント率

手法は「自由形式」を選択,ビジュアリゼーションを「折れ線グラフ」に設定,指標に「エンゲージメント率」を呼び出して値に代入,ディメンションに「ストリーム名」を呼び出して内訳に代入

f:id:Soluna_Eureka:20211225111102p:plain

ちなみにエンゲージメント率とは「エンゲージメントがあったセッション数/セッション数」で定義されている

ページ遷移割合

テンプレート「経路データ探索」を用いるとページごとのアクセスの経路や割合が見えてくる,イベントごとに追跡しているがぶっちゃけページ名だけ表示させることも可能
各テンプレートにおいては解析する内容がほぼ決まっているので,使える変数が向こうから指定されることが多い

手法は「経路データ探索」を選択,指標に「イベント数」を呼び出して値に代入

f:id:Soluna_Eureka:20211225111814p:plain

「session_start」からいきなり「scroll」の飛んでいるのが謎,タイムアウト後にスクロールしつつ画像を読み込んだとか…?

スクロール率

セグメントを使い分類を可視化するのだが,なぜか2日分以上のデータじゃないtpセグメントが生成されないので注意

手法は「セグメントの重複」を選択,指標に「表示回数」を呼び出して値に代入(加えて「利用ユーザー」がデフォルトで設定される),ディメンションに「ページ タイトルとスクリーン名」を呼び出して内訳に代入
およびセグメント「目に入った」「読み進めた」をそれぞれ作成してセグメントの比較に代入

f:id:Soluna_Eureka:20211225112754p:plain

f:id:Soluna_Eureka:20211225112806p:plain

f:id:Soluna_Eureka:20211225112856p:plain

セグメントの重複を図で表現できる基準の値が完全に固定でユーザ数に限られているので完全に自由には扱えない,が利用目的がよくあるヤツならユーザがどう動くかを解析して重複を探るパターンがほとんどだろう,ちなみにセグメントはまだ増やせる

スクロールの有無とエンゲージ時間

ページを見た時間やセッションが継続した時間を表示する,データストリームごとの分類には対応していない

手法は「ユーザーのライフタイム」を選択,指標に「ユーザーの合計数」「全期間のエンゲージ期間(平均)」「全期間のセッション時間(平均)」を呼び出して値に代入,セグメントに「目に入った」「読み進めた」をそれぞれ作成してセグメントの比較に代入,およびピボットを「最初の行」に設定

f:id:Soluna_Eureka:20211225114811p:plain

読み進めるユーザーは5分ほどページを表に表示して,なおかつセッションが切れる(デフォルトの30分が経過する)まで裏にページが残り続けるらしい,そうでない場合は2分ほど表示して10分ほどでセッションが切れる…みんなタブ閉じないっぽいね…

余談

ブラウザの設定やプラグイン(アドオン)によっては,ブラウザ上でGA4がそもそも動作しないケースを確認している,Cookie殺し・キャッシュ殺し・セッション殺し・トラッカー殺しの流行はもはや確実であり,というかはてなブログの閲覧数と明らかに合ってないので,このデータ収集に引っかかった人はコンテンツブロッカを搭載していないスマホのブラウザからのアクセス ではないかと睨んでいる.
そう考えるとやはりサーバーサイドで実装するのが最善かと思われるが,市販サービスに乗合で利用するにはページに埋め込むタイプのものしか(ほぼ)使えないわけで,まぁ無料でこれだけ遊べて興味深い結果が得られるというのなら妥協しよう…

しかしアメリカからのアクセスが本当に謎で,メリーランド州のヘイガーズタウンという場所からのセッションが2回も観測されている.1回ならまだわかるが2回はちょっとわからない,英語のブログなんか書いたことないし縁もゆかりもないはず…

Google Analytics(GA4)の設定とContent Security Policy(CSP)

ページのソースコードにありがちなのコレ(今ではGAよりもGA4が一般的)らしい

<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=<measurment ID(String)>"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', <measurment ID(String)>);
</script>

クライアントブラウザが測定ID付きURLを読み込んでjsを叩くと自動でGoogleに送信される

やる前に

GA4のページに仕込まれたインラインスクリプトChromeのCSP設定で弾かれるので(自社製品同士なんだし上手くデバッグしてくれ)

  • 「環境設定」
  • 「プライバシーとセキュリティ」
  • 「安全でないコンテンツ」(1番下にあるはず)
  • [*.]analytics.google.comをurlに追加

しておこう,CSPが設定されたページでは危険な関数(eval系など)や許可のないurlから得たjsをブラウザが実行せず,前者のせいで操作不能になる
ちなみに実際にGA4を埋め込むと後者が原因で動かない時があるらしい,はてなブログはMathJaxが動くから流石にセーフだと思うが,自分でCSPを設定できるサイト(<meta>タグの中かhttpヘッダの中に入れるらしい)の場合はGA4に対応するurlを許可しないといけない(ここでは扱わない)

導入

アカウント作成

  • ここに行く
  • 「無料で設定」を選択(アカウントないなら)
  • アカウント名を設定
    • とりあえず自分の名前にした
    • ⚠︎GA4アカウントとGmailアカウントは別物です!⚠︎
    • 管理者はちゃんと管理しておこう(激うまギャグ)
    • 複数のアカウントが持てるらしいが今は必要性を感じてない
  • 質問には適当に回答
    • 自分しか見ないならデータ共有の類は全て却下して良さげ
    • 後からでも設定できるので問題なし

プロパティ作成

  • プロパティ名は識別しやすくサービス名を表現する
    • 「HatenaBlog」にした
  • タイムゾーンと通貨は日本で設定
    • 本当は場所に合わせた方が良い
  • 「ユニバーサル アナリティクス(GA)のプロパティのみを作成する」は今回は却下
    • ここ曰くはてブロではデフォでGAを埋め込めるらしい
    • プロパティは後から作れるので何かあればやり直せば良い
    • GA(昔からある)とGA4(まだ1年も経ってないらしい)では機能が違うらしい,まぁどうせ改良されるだろう

ストリーム作成

  • 「管理」→「データストリーム」→「データストリームを追加」→「Web」
    • スマホアプリ埋め込み型もあるらしい
  • ストリームURLに対象ドメインを設定
    • https://blog.hatena.ne.jp/Soluna_Eureka/soluna-eureka.hatenablog.comにした
  • ストリーム名に識別しやすくページ名を設定
    • 「ChoHoChoDo」(調法調度)にした
  • 拡張機能を全てオンにする
  • すると測定IDが生成される

利用

「タグ設定手順」の上のやつを開いてコピーしたら任意のページの<head></head>にコピペ
はてブロなら「デザイン」「🔧」「ヘッダ」に入れれば動くはず(GAとGA4ではID形式が違うので直埋め必須)

Google Tag Manager(GTM)

これを使うと実装や管理や楽になるらしいが,今回は扱わない(長くなりそう)

手元でどう映るか

俺はお前が俺を見たのを見たぞ

f:id:Soluna_Eureka:20211219003434p:plain
test

これがやりたかっただけ ちなみに解析は丸1日経たないとできないらしい

最新のhtml+javascript+cssにおける「真のカスタムエレメント」を考える

やりたかったこと

ここ最近,vscode上でmarkdownをひたすら使い潰すためだけに色々とやってきているのだが(以下はこれまでの記事),

soluna-eureka.hatenablog.com

soluna-eureka.hatenablog.com

soluna-eureka.hatenablog.com

これまでの研究によりmarkdown pdfextension.jsを書き換えることで任意の.html.js.cssファイルをプラグインとして利用できる環境を作れてしまうことが判明し,これにkatexを混ぜることで概ね万能な数式の表示すら可能となった.

しかし大元の発端は「latexが面倒だからmarkdownを使おう」というものであり,つまるところlatexにある機能がkatexさらにはそれが対応できるmarkdownには搭載し得ないもので,これは下位互換にすぎない不完全な環境であったのだ.
そこで私が考えついたのが,「latexに備わる組版用の各種コマンドをhtmlの機能で代替できないか?」というものである.

目標

完全に独立した名前を持ち,余計な指定をする必要がなく,かつidstyleが自在に設定できる「真のカスタムエレメント」を実現する.これによりlatexコマンドに対応するカスタムエレメントを自由に作成,柔軟な組版markdownで実現する.

方針

かつてはdocument.registerElementで好き放題にできていたらしく,その手法を扱った記事がいまだに検索にヒットするが,

developer.mozilla.org

現在では全ブラウザ非対応の憂き目に合っている(リンク先は英語ページのみ).ゆえに代替であるCustomElementRegistry

developer.mozilla.org

インターフェースが持つcustomElements.define(あっちはElementなのにこっちはElementsなの,控えめに言って最悪)

developer.mozilla.org

を使用せざるを得ないが,現在では数多くの記事においてこのページが示す「自律型」「拡張型」の2種類のみを扱っている.

しかし前者はタグの間の文字を扱えない(DOMパースがシカトされる)・後者はis=''の指定が必要(DOMパースしてくれる)ため,私の要求には満たない.他方でwhatwghtml系開発コミュニティ)によるcustom elementsのリファレンスでは,

triple-underscore.github.io

カスタム要素定義を登録するのは、 それに関連する要素が初期時に — 構文解析器などにより — 作成された後の方が,好ましいこともある。 昇格は、 そのような用法を可能化して,[ カスタム要素の内容を漸進的に増強する ]ことを許容する。

という内容もあり,つまりは「名前に-が入っているエレメントはカスタム要素として解析が後回しにされる」程度には動作の許容がある.更には「任意のhtml要素を指す」HTMLElement(任意?あっ…)が現在でも全てのブラウザで実装されている

developer.mozilla.org

ため,この2つを足がかりにして攻略していった結果,なぜだか無事に攻略に成功した.

困難

カスタムエレメントなだけあって処理順の設定が難しく,上記2点の仕様が変わらなくても他の関数の仕様が変わるだけで死ぬ可能性は否定できない.これはあくまで2021年12月現在の情報であることはお許しいただきたい(未来のことは知らない).

やり方

まずは上の3つの記事を読むことこそお勧めする(プラグイン管理が格段に楽になるので)が,実際は読まなくても大丈夫だ.

原理

以下では<div></div>由来のボックスとしての<d-box></d-box>を作成する.

class DBox2 extends HTMLDivElement {
    constructor() {
        super();
        this.id = 'box';
    }
}

customElements.define('d-box2', DBox2, { extends: 'div' });


class DBox extends HTMLElement {
    constructor() {
        super();
        let wrapper = document.createElement('div');
        wrapper.id = 'box';
        let content = this.innerHTML;
        console.log(content);
        this.innerHTML = '';
        wrapper.insertAdjacentHTML("afterbegin", content);
        this.insertAdjacentElement("beforeend", wrapper);
    }
}

document.addEventListener("DOMContentLoaded", function() {
    customElements.define('d-box', DBox);
});
body {
    color: #222;
    background-color: #fff;
}

#box {
    color: #444;
    border: 5px solid #666;
    border-radius: 10px;
    padding: 5px;
    margin: 5px;
}
# これはなんですの?

## `custom element`ですわ!

マニュアル通りの手法では
<div is='d-box2'>
sasa
</div>

わたくしが極めた手法では
<d-box>
二重動作もOKですわ!
<d-box>
$$
    \begin{align}
    e=mc^2 \\\\
    e^{i\pi}+1=0
    \end{align}
$$
</d-box>
</d-box>

f:id:Soluna_Eureka:20211210032740p:plain

何をやったか

なんの情報も持たない<d-box></d-box>の中身を丸ごと文字列で抜き取り<div id='box'></div>を埋め込んで更にそこに元の文字列を入れただけ,要するに作業としては大したことはやっていない…が,処理同期手法の試行錯誤が面倒だったことに加え,そもそも「そういうやり方しかない」という結論に至るまでに半日ほど要してしまったことがあり,少し辛かった.

ちなみに<div is='d-box2'></div>はよく紹介されているやり方である,どちらも同じ結果を得ている以上これは成功だろう.

クラス化

superのリファレンスに目を通した時に「この処理はチェーン化できるのでは」と気づいたので,実際に実装に成功してみた.

developer.mozilla.org

結果は省略するが以下にクラス処理に関する記述を載せる,例としてAncestorOfAllからフォールバックし3段組を実現する.

class AncestorOfAll extends HTMLElement {
    constructor(element, purpose) {
        super();
        let wrapper = document.createElement(element);
        wrapper.id = purpose;
        let content = this.innerHTML;
        console.log(content);
        this.innerHTML = '';
        wrapper.insertAdjacentHTML("afterbegin", content);
        this.insertAdjacentElement("beforeend", wrapper);
    }
}

class DCol3 extends AncestorOfAll {
    constructor() {
        super('div', 'col3');
    }
}

document.addEventListener("DOMContentLoaded", function() {
    customElements.define('d-col3', DCol3);
});
#col3 {
    column-count: 3;
    padding: 20px;
    margin: 10px;
}

要はsuper()super()を呼び出しているに過ぎず,あとは先程と同じように<d-col3></d-col3>で呼び出せば良いだろう.

立つ鳥跡を濁さず

カスタムエレメントを使用した痕跡を全く残さないということも可能ではあり(自分でデバッグできなくなるが),これを活用するとkatexの環境や関数をelement風に呼び出せるため非常に可読性が高まってとても良い.ただし環境と関数の定義の順番を間違えると動作しなくなったり,一部の無駄だと思える要素を省略すると動作しなくなったりするため,注意が必要である.

// define your custom math func
// これを後にすると動かない
class AncestorOfMathFunc extends HTMLElement {
    constructor(purpose) {
        super();
        let mathHead = '\\begin{' + purpose + '}';
        let mathTale = '\\end{' + purpose + '}';
        this.insertAdjacentHTML('afterbegin', mathHead);
        this.insertAdjacentHTML('beforeend', mathTale);
        let content = this.innerHTML;
        this.insertAdjacentHTML('beforebegin', content)
        this.remove();
    }
}

class PMatrix extends AncestorOfMathFunc { //丸括弧の行列表記
    constructor() {
        super('pmatrix');
    }
}
document.addEventListener('DOMContentLoaded', function() {
    customElements.define('g-pmatrix', PMatrix); //<g-pmatrix></g-pmatrix>で呼び出し
});

class Cases extends AncestorOfMathFunc { //場合分け表記
    constructor() {
        super('cases');
    }
}
document.addEventListener('DOMContentLoaded', function() {
    customElements.define('g-cases', Cases); //<g-cases></g-cases>で呼び出し
});


// define your custom math mode
// これを先にすると動かない
class AncestorOfMathMode extends HTMLElement {
    constructor(element, purpose) {
        super();
        let breaking = document.createElement('br');
        let mathHead = '\\begin{' + purpose + '}';
        let mathTale = '\\end{' + purpose + '}';
        let mathWrapper = '$$';
        this.insertAdjacentHTML('afterbegin', mathHead);
        this.insertAdjacentHTML('afterbegin', mathWrapper);
        this.insertAdjacentHTML('beforeend', mathTale);
        this.insertAdjacentHTML('beforeend', mathWrapper);
        this.insertAdjacentElement('beforeend', breaking); //これを無効化すると動かない
        let wrapper = document.createElement(element);
        wrapper.id = purpose;
        let content = this.innerHTML;
        this.innerHTML = '';
        wrapper.insertAdjacentHTML('afterbegin', content);
        this.insertAdjacentElement('beforebegin', wrapper);
        this.remove();
    }
}

class DAlign extends AncestorOfMathMode { //align環境
    constructor() {
        super('div', 'align');
    }
}
document.addEventListener('DOMContentLoaded', function() {
    customElements.define('d-align', DAlign); //<d-align></d-align>で呼び出し
});

class DAlignN extends AncestorOfMathMode { //align*環境
    constructor() {
        super('div', 'align*');
    }
}
document.addEventListener('DOMContentLoaded', function() {
    customElements.define('d-alignn', DAlignN); //<d-alignN></d-alignN>で呼び出し
});

この環境定義の動作手順は

  1. タグの中身の文字列を操作
  2. 数式環境の呼び出しに対応
  3. <div></div>を生成
  4. idを設置し書式設定が可能な状態に
  5. タグの中身をinnerHTMLと解釈
  6. 生成した<div></div>にコピペ
  7. 痕跡を消去

という流れで,関数定義の動作手順とは<div></div>を生成するかどうかに違いがある.
かつdocument.addEventListener('DOMContentLoaded', function() {}は,その定義順に実行される.
そこで何かしらkatexの解釈に不都合が事象が発生している…と考えられるだろう.

欠点

本文や数式の分量増えるほどに動作が遅くなる(DOMを操作する量が多くなるから),markdownプレビューがkatexを含め機能しなくなる(vscode上の機能が解釈できないから)…等がある.正直katexレンダリング速度に割と助けられている.

感想

恐らく初めてjavascriptのクラスのお勉強をしたし,処理の同期や非同期について考えた.template.js<head><\head>に入っているし,恐らくどこに置いても正常に動作すると考えられる(一部のリファレンスは末尾設置を推奨しているけど). ただし念のためにkatexよりも前に読み込む設定にはした方が良い,レンダリングのどこかで不都合が起きるかもしれない.

今後よりlatexライクな環境に近づけようなどと考えているが,もしこれに使い道があれば是非とも使ってもらえると嬉しい.

TeX + vscode + git on WSL2(Windows Subsystem for Linux 2) 設定メモ

なぜWSL2?

Windows10にTeXを直乗せして動かしたくせない理由として,vscode拡張機能であるlatex workshopからtexに備わる一括コンパイル機能であるlatexmkを最優先に使用したいのに,その手法がwindowsと致命的に相性が悪いことが挙げられます.
ご存知の通りにwindowsのパスの区切りはバックスラッシュ(円記号)ですが,macOSLinuxと同じ設定のままlatex workshopからlatexmkを起爆すると,なぜかtex関係ファイルの絶対パスがスラッシュ/とバックスラッシュ\が入り混ざったものとして生成されてしまい,ただちにundefined control sequenceI can't find fileコでンパイルエラーを吐かれてしまいます.
どこに原因があるか詳しくはわかりませんが,vscodeで開いているルートフォルダの直前にだけ\が入っているならまだしも,それ以外の場所にも\が入っていたりするために,個人的にはこの問題の解決のためだけに労力を割くのは面倒だと思いました.

現状ではUNIXLinuxのパス表記が圧倒的に正義である(とても強い主張)以上は,OSも拡張機能もデフォルトでパス表記に/を使う認識が共有されているmacOSもしくはLinuxを常に使用することこそ非常に正しい選択肢であり,マシンリソースが十分にあるならばWSL2の採用を検討すべきでしょう.マルチプラットフォームな作業を目指すなら,尚更にPowerShellを含めたWindowsのデフォルト環境を直ちに捨てることこそが,余計な労力を減らすことに繋がります.ここではそのやり方についてメモをしていきたいと思います.

PowerShellの導入

これがないと話になりません,絶対に必要なので速やかに導入しましょう.

docs.microsoft.com

WSL2の導入

頻繁に更新されているらしいので,バージョン違いは気をつけましょう.これは2021年12月における最新の情報のはずです.

OSアップグレードと導入

現行最新版のWindows10 21H2ならWSL2が簡単に利用できます.WSLと比べて処理性能が格段に向上しているため,直ちにアップグレードして簡単なWSL2の導入環境を整えましょう.幾らか古い版でも導入こそできますが,マニュアルを見つつ行う手動でのインストールは非常にダルい上に下らないミスを誘発しかねませんので,やめた方がお得です.
Window11ならデフォルトでOKです,その後は以下の手順に従います.

docs.microsoft.com

docs.microsoft.com

種類は主にUbuntuDebian・KaliLinuxですが,特に需要がなければ素直にUbuntuで良いでしょう.

お互いのファイルを参照するには

例えばWindowsからWSLもしくはWSLからWindowsでファイルを交換したい場合,Windowsから見て

\\wsl$\<環境の名前>

がWSLのルートディレクトリであり(ネットワークドライブ),その逆にLinuxから見て

/mnt/c

がcドライブになります(他のドライブも同様)(マウントドライブ).

環境(ディストリビューション)の複製

WSL2では動作環境をtarしながらexportもしくはimportすることができます.基本的にパッケージ扱いであるため,C:\Users\<ユーザーの名前>\AppData\Local\Packagesの中に紛れ込んでいるのですが,これを行えばバージョン管理や環境保全ができます.そのためには以下を参考にします.

docs.microsoft.com

docs.microsoft.com

前者ではDockerのimageからLinuxを叩き起こしそれを吸い取るやり方が載ってますが,Windowsから供給されるバージョンでよければ無視して良いです.Dockerの導入は以下を参照してください(必ずwsl2を設定し終わってからやりましょう,すると\\wsl$\にDocker系が入ってきてくれます).

docs.microsoft.com

この記事においては本質ではないのでパスします.より難しいことは以下を読みましょう.

docs.microsoft.com

ディストリビューションの実体

デフォルトでは

C:\Users\<ユーザーの名前>\AppData\Local\Packages\CanonicalGroupLimited.UbuntuWindows_<謎のハッシュ値>\LocalState\ext4.vhdx
C:\Users\<ユーザーの名前>\AppData\Local\Packages\KaliLinux.<謎のハッシュ値>\LocalState\ext4.vhdx
C:\Users\<ユーザーの名前>\AppData\Local\Packages\TheDebianProject.DebianGNULinux_<謎のハッシュ値>\LocalState\ext4.vhdx

にありますが,前述したexportおよびimportを行い丸ごとdドライブに移行することも可能です.

zshの導入

Ubuntuを適切に導入した場合のシェルはおそらくbashですが,より新しく多機能なzshをお勧めします.そのためにはまずsudo aptUbuntuをアップデートしてからzshを入れます.

sudo apt update
sudo apt upgrade
sudo apt install zsh
sudo chsh -s $(which zsh)

必要な依存パッケージがインストールされるかと思いますが,上手くいけば

echo $SHELL #->zsh?

が得られます.この後で~/.zprofile~/.zshrcを作成しても構いません.

それぞれの役割

前者はログインシェルの起動時に読み込まれ,後者はそれに加えてインタラクティブシェルの起動時に読み込まれます.これは後述しますが,パス設定は後者に行う方が良いでしょう.

texをダウンロード

まずはmacOSに倣って~/Downloadsを作成しましょう.そこに移動したら次に

curl -O http://ftp.jaist.ac.jp/pub/CTAN/systems/texlive/tlnet/install-tl-unx.tar.gz

で一式を落とします.

余力があればで良いですが,こういたデフォルトのツールも古めだと思われるので,macOSに倣ってhomebrewを導入し完全なパッケージの管理体制を整えましょう.

docs.brew.sh

sudo apt-get install build-essential procps curl file git
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

texのインストール

先ほどのファイルを

tar -xvf install-tl-unx.tar.gz

で解凍し,install-tlがある位置に移動します.次に

sudo ./install-tl --repository http://ftp.jaist.ac.jp/pub/CTAN/systems/texlive/tlnet

でインストールを実行します.
この際にインストールオプションを聞かれますが,とりあえずはフルセットで導入しても構いません.またインストールには時間がそこそこかかります,もし失敗した際は設定したオプションを使い回す

--profile installation.profile

をコマンドに追加して繰り返し挑戦してみましょう.

GUIは必要か

wslでguiが使えるwslgなるプラグインこそあるそうですが,windows10の21H2のwsl2ですら現状では使えていません(windows11のデベロッパ向けのinsider previewでのみ使えるようです).

github.com

tex組版においてguiを使う場面があるとすれば生成したpdfを見る時くらいですが,それは前述した方法でファイルにアクセスすれば良いので,ぶっちゃけコマンドさえあればOKです.

パスを通す

現状ではzshにパスが通っていない上にmacOSのように便利なシンボリックリンクも作られていませんので,vscode拡張機能は実行ファイルを見つけられません.そのためmacOSに倣って以下の手順でパスを通します.デフォルトでは\usr\local\texlive\2021\bin\x86_64-linuxにあります.

シンボリックリンクの作成

まず/Library/TeX/binを作成し(sudoが必要です),そこに/usr/local/texlive/[]/bin/x86_64-linuxをリンクします.これはわかりやすいようにディレクトリ名を揃えていますが,お好みで構いません.

sudo ln -s /usr/local/texlive/<バージョン年度>/bin/x86_64-linux /Library/TeX/bin/x86_64-linux

もしバージョンが変わった場合は適宜に年度を変更してリンクし直しましょう,ちなみにmac texではデフォルトでこの作業をやってくれます(なのでhomebrewとの食い合わせがよろしくないですね).

パスを作成する

~/.zshrcの最後にこの場所を書き加えます,これよりも上位の設定ファイルは弄らない方が吉です,手動でやらない場合でも以下を行えば結構です.

echo 'export PATH="/Library/TeX/bin/x86_64-linux":$PATH' >> ~/.zshrc

念の為にwhich latexmkで見つかるかどうかを確認しておきましょう.

vscodeを導入そして設定

microsoft謹製のvscodeならデフォルトでwsl2の中を扱えます,以下を参考に導入しましょう.

docs.microsoft.com

この時点でubuntuが立ち上がり次第で自由にファイルの編集が行えるだろうと思われます.

gitとlatexmkを設定

ここから先はwsl2に限らない汎用な設定になりますので,以下を参照してください.

soluna-eureka.hatenablog.com

soluna-eureka.hatenablog.com

私の環境ではmacOSに倣った設定を流用して特に問題はありませんでした,すなわちvscodeの設定ファイルを同期させても問題が起きないため,wsl2を使用する恩恵が受けられるというものです.

感想

まさか私がwin・maclinux・wslの四刀流をするとは思いませんでした…

globalにgitでignoreをsetする手順

やり方

git環境は整っていることが前提

ファイルを作る

touch $HOME/.config/git/ignore

ファイルに書き込む

.DS_Store

拡張子指定で一括

*.code-workspace

試してみる

https://git-scm.com/docs/git-config#Documentation/git-config.txt-coreexcludesFile
https://git-scm.com/docs/gitignore#_description
が正しければ,これだけでパスは通っているはずである,試しに動かそう

...
 Defaults to $XDG_CONFIG_HOME/git/ignore.
If $XDG_CONFIG_HOME is either not set or empty,
$HOME/.config/git/ignore is used instead.
See gitignore[5].
...

上手くいけばOK,ダメなら次の手順へ

パスを通す

git config --global core.excludesfile $HOME/.config/git/ignore

これをやると

[core]
    excludesfile = /Users/SolunaEureka/.config/git/ignore

でパスが通るなぜわざわざこんなことやらにゃならんのだ
流石にコレなら動くだろう

パスを消す

git config --global --unset core.excludesfile      

理由は知らんけど消してもしばらく動く場合がある,つーか私はそうだった

おまけ

初めてgithubでレポジトリ作ったけど,作った時点で案内に乗せられてREADMEを作ると,強制的に「initコミット」が発生してしまい,既に手元に作り溜めしたファイルがある場合は,そのブランチにはプッシュできない・別ブランチにpushできてもプルリクできない,という「詰みの状況」が発生した,There isn’t anything to compare.って表示されていた,いや確かにそれはそうなのだが…

あとパスの挙動が謎すぎた,まぁ今はちゃんと動いてるからコレでよしとするか…