自作の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.って表示されていた,いや確かにそれはそうなのだが…

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

markdown pdfを魔改造した

利点

vscode拡張機能のコアファイルは保存場所が既に決定されており,それが使うパスの書き換えは表からはほぼできない
なので内部のスクリプトを弄ってmarkdown pdf関係の書類をクラウドなどで共有できるようにした,これならコピペ不要
さらにどの場所でmarkdown pdfをやっても必ず同じ結果が得られるようになる,ただしvscodeの環境設定で弄れる範囲を除く

欠点

vscodeの環境設定にパラメータを追加できるのは開発者だけらしく大元ディレクトリのフルパスとプラグインのリストアップはextension.jsに書き込む必要があり面倒

概要

そこそこに改変した
まずmarkdown pdf拡張機能のコアファイルのextension.js(ここの最後の方でいじってたやつ)を以下でコピペして上書きする,だいたい1050行くらい
次にmyGroundPathとmyPluginListをその冒頭箇所に指示通りに書き込む,Windowsではエスケープ処理を忘れないように
更に関係ファイルはmyGroundPathの配下に(例として)以下のように設置する,プラグインは同名のcss・html・jsをそれぞれ必ず配置してmyPluginListとの対応を必ず確保しておく
最後にbase.htmlも間違えずにコピペする,コレで終わり

.
├── base
│   └── base.html
├── plugins
│   └── math
│       ├── math.css
│       ├── math.html
│       └── math.js
├── template
│   ├── template.css
│   ├── template.html
│   └── template.js
└── test
    ├── test.html
    └── test.md

ここのmathとはmarkdown pdf上でkatexを動かすのに必要な設定やマクロなどを集めたもので,導入スクリプトと設定スクリプトで分けてある
そっちの周りのお話はここを参考にしてほしい

ソース

以下はbase.html

<!DOCTYPE html>
<html>
<head>
<title>{{{title}}}</title>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
{{{frame}}}
{{{style}}}
{{{script}}}
{{{mermaid}}}
<script>
    // insert your test script!
    console.log("rendered from base.html!");
</script>
</head>
<body>
  <script>
    mermaid.initialize({
      startOnLoad: true,
      theme: document.body.classList.contains('vscode-dark') || document.body.classList.contains('vscode-high-contrast')
          ? 'dark'
          : 'default'
    });
  </script>
{{{content}}}
</body>
</html>

ここの{{{example}}}などに中身を突っ込むためのテンプレートエンジンはmustache.js,と言うよりextension.jsも含め拡張機能全体がnodeで動作しているっぽい,それはそうか

以下はextension.js

'use strict';
var vscode = require('vscode');
var path = require('path');
var fs = require('fs');
var url = require('url');
var os = require('os');
const { least } = require('d3-array');
var INSTALL_CHECK = false;

// set mother directory path for entire files as String.
const myGroundPath="/hogehoge/hugahuga/markdownPdf";
// set list of extensions as List[String,String...].
// you should place files like
//
// markdownPdf
// ├ foo
// │  ├ foo.html
// │  ├ foo.js
// │  ├ foo.css   
// ├ bar
// │  ├ bar.html
// .  ...
//
//.
const myPluginList=["foo","bar"];



function activate(context) {
  init();

  var commands = [
    vscode.commands.registerCommand('extension.markdown-pdf.settings', async function () { await markdownPdf('settings'); }),
    vscode.commands.registerCommand('extension.markdown-pdf.pdf', async function () { await markdownPdf('pdf'); }),
    vscode.commands.registerCommand('extension.markdown-pdf.html', async function () { await markdownPdf('html'); }),
    vscode.commands.registerCommand('extension.markdown-pdf.png', async function () { await markdownPdf('png'); }),
    vscode.commands.registerCommand('extension.markdown-pdf.jpeg', async function () { await markdownPdf('jpeg'); }),
    vscode.commands.registerCommand('extension.markdown-pdf.all', async function () { await markdownPdf('all'); })
  ];
  commands.forEach(function (command) {
    context.subscriptions.push(command);
  });

  var isConvertOnSave = vscode.workspace.getConfiguration('markdown-pdf')['convertOnSave'];
  if (isConvertOnSave) {
    var disposable_onsave = vscode.workspace.onDidSaveTextDocument(function () { markdownPdfOnSave(); });
    context.subscriptions.push(disposable_onsave);
  }
}
exports.activate = activate;


// this method is called when your extension is deactivated
function deactivate() {
}
exports.deactivate = deactivate;

async function markdownPdf(option_type) {

  try {

    // check active window
    var editor = vscode.window.activeTextEditor;
    if (!editor) {
      vscode.window.showWarningMessage('No active Editor!');
      return;
    }

    // check markdown mode
    var mode = editor.document.languageId;
    if (mode != 'markdown') {
      vscode.window.showWarningMessage('It is not a markdown mode!');
      return;
    }

    var uri = editor.document.uri;
    var mdfilename = uri.fsPath;
    var ext = path.extname(mdfilename);
    if (!isExistsPath(mdfilename)) {
      if (editor.document.isUntitled) {
        vscode.window.showWarningMessage('Please save the file!');
        return;
      }
      vscode.window.showWarningMessage('File name does not get!');
      return;
    }

    var types_format = ['html', 'pdf', 'png', 'jpeg'];
    var filename = '';
    var types = [];
    if (types_format.indexOf(option_type) >= 0) {
      types[0] = option_type;
    } else if (option_type === 'settings') {
      var types_tmp = vscode.workspace.getConfiguration('markdown-pdf')['type'] || 'pdf';
      if (types_tmp && !Array.isArray(types_tmp)) {
          types[0] = types_tmp;
      } else {
        types = vscode.workspace.getConfiguration('markdown-pdf')['type'] || 'pdf';
      }
    } else if (option_type === 'all') {
      types = types_format;
    } else {
      showErrorMessage('markdownPdf().1 Supported formats: html, pdf, png, jpeg.');
      return;
    }

    // convert and export markdown to pdf, html, png, jpeg
    if (types && Array.isArray(types) && types.length > 0) {
      for (var i = 0; i < types.length; i++) {
        var type = types[i];
        if (types_format.indexOf(type) >= 0) {
          filename = mdfilename.replace(ext, '.' + type);
          var text = editor.document.getText();
          var content = convertMarkdownToHtml(mdfilename, type, text);
          var html = makeHtml(content, uri);
          await exportPdf(html, filename, type, uri);
        } else {
          showErrorMessage('markdownPdf().2 Supported formats: html, pdf, png, jpeg.');
          return;
        }
      }
    } else {
      showErrorMessage('markdownPdf().3 Supported formats: html, pdf, png, jpeg.');
      return;
    }
  } catch (error) {
    showErrorMessage('markdownPdf()', error);
  }
}

function markdownPdfOnSave() {
  try {
    var editor = vscode.window.activeTextEditor;
    var mode = editor.document.languageId;
    if (mode != 'markdown') {
      return;
    }
    if (!isMarkdownPdfOnSaveExclude()) {
      markdownPdf('settings');
    }
  } catch (error) {
    showErrorMessage('markdownPdfOnSave()', error);
  }
}


function isMarkdownPdfOnSaveExclude() {
  try{
    var editor = vscode.window.activeTextEditor;
    var filename = path.basename(editor.document.fileName);
    var patterns = vscode.workspace.getConfiguration('markdown-pdf')['convertOnSaveExclude'] || '';
    var pattern;
    var i;
    if (patterns && Array.isArray(patterns) && patterns.length > 0) {
      for (i = 0; i < patterns.length; i++) {
        pattern = patterns[i];
        var re = new RegExp(pattern);
        if (re.test(filename)) {
          return true;
        }
      }
    }
    return false;
  } catch (error) {
    showErrorMessage('isMarkdownPdfOnSaveExclude()', error);
  }
}


/*
 * convert markdown to html (markdown-it)
 */
function convertMarkdownToHtml(filename, type, text) {
  var grayMatter = require("gray-matter");
  var matterParts = grayMatter(text);

  try {
    try {
      var statusbarmessage = vscode.window.setStatusBarMessage('$(markdown) Converting (convertMarkdownToHtml) ...');
      var hljs = require('highlight.js');
      var breaks = setBooleanValue(matterParts.data.breaks, vscode.workspace.getConfiguration('markdown-pdf')['breaks']);
      var md = require('markdown-it')({
        html: true,
        breaks: breaks,
        highlight: function (str, lang) {

          if (lang && lang.match(/\bmermaid\b/i)) {
            return `<div class="mermaid">${str}</div>`;
          }

          if (lang && hljs.getLanguage(lang)) {
            try {
              str = hljs.highlight(lang, str, true).value;
            } catch (error) {
              str = md.utils.escapeHtml(str);

              showErrorMessage('markdown-it:highlight', error);
            }
          } else {
            str = md.utils.escapeHtml(str);
          }
          return '<pre class="hljs"><code><div>' + str + '</div></code></pre>';
        }
      });
    } catch (error) {
      statusbarmessage.dispose();
      showErrorMessage('require(\'markdown-it\')', error);
    }

  // convert the img src of the markdown
  var cheerio = require('cheerio');
  var defaultRender = md.renderer.rules.image;
  md.renderer.rules.image = function (tokens, idx, options, env, self) {
    var token = tokens[idx];
    var href = token.attrs[token.attrIndex('src')][1];
    // console.log("original href: " + href);
    if (type === 'html') {
      href = decodeURIComponent(href).replace(/("|')/g, '');
    } else {
      href = convertImgPath(href, filename);
    }
    // console.log("converted href: " + href);
    token.attrs[token.attrIndex('src')][1] = href;
    // // pass token to default renderer.
    return defaultRender(tokens, idx, options, env, self);
  };

  if (type !== 'html') {
    // convert the img src of the html
    md.renderer.rules.html_block = function (tokens, idx) {
      var html = tokens[idx].content;
      var $ = cheerio.load(html);
      $('img').each(function () {
        var src = $(this).attr('src');
        var href = convertImgPath(src, filename);
        $(this).attr('src', href);
      });
      return $.html();
    };
  }

  // checkbox
  md.use(require('markdown-it-checkbox'));

  // emoji
  var emoji_f = setBooleanValue(matterParts.data.emoji, vscode.workspace.getConfiguration('markdown-pdf')['emoji']);
  if (emoji_f) {
    var emojies_defs = require(path.join(__dirname, 'data', 'emoji.json'));
    try {
      var options = {
        defs: emojies_defs
      };
    } catch (error) {
      statusbarmessage.dispose();
      showErrorMessage('markdown-it-emoji:options', error);
    }
    md.use(require('markdown-it-emoji'), options);
    md.renderer.rules.emoji = function (token, idx) {
      var emoji = token[idx].markup;
      var emojipath = path.join(__dirname, 'node_modules', 'emoji-images', 'pngs', emoji + '.png');
      var emojidata = readFile(emojipath, null).toString('base64');
      if (emojidata) {
        return '<img class="emoji" alt="' + emoji + '" src="data:image/png;base64,' + emojidata + '" />';
      } else {
        return ':' + emoji + ':';
      }
    };
  }

  // toc
  // https://github.com/leff/markdown-it-named-headers
  var options = {
    slugify: Slug
  }
  md.use(require('markdown-it-named-headers'), options);

  // markdown-it-container
  // https://github.com/markdown-it/markdown-it-container
  md.use(require('markdown-it-container'), '', {
    validate: function (name) {
      return name.trim().length;
    },
    render: function (tokens, idx) {
      if (tokens[idx].info.trim() !== '') {
        return `<div class="${tokens[idx].info.trim()}">\n`;
      } else {
        return `</div>\n`;
      }
    }
  });

  // PlantUML
  // https://github.com/gmunguia/markdown-it-plantuml
  var plantumlOptions = {
    openMarker: matterParts.data.plantumlOpenMarker || vscode.workspace.getConfiguration('markdown-pdf')['plantumlOpenMarker'] || '@startuml',
    closeMarker: matterParts.data.plantumlCloseMarker || vscode.workspace.getConfiguration('markdown-pdf')['plantumlCloseMarker'] || '@enduml',
    server: vscode.workspace.getConfiguration('markdown-pdf')['plantumlServer'] || ''
  }
  md.use(require('markdown-it-plantuml'), plantumlOptions);

  // markdown-it-include
  // https://github.com/camelaissani/markdown-it-include
  // the syntax is :[alt-text](relative-path-to-file.md)
  // https://talk.commonmark.org/t/transclusion-or-including-sub-documents-for-reuse/270/13
  if (vscode.workspace.getConfiguration('markdown-pdf')['markdown-it-include']['enable']) {
    md.use(require("markdown-it-include"), {
      root: path.dirname(filename),
      includeRe: /:\[.+\]\((.+\..+)\)/i
    });
  }

  statusbarmessage.dispose();
  return md.render(matterParts.content);

  } catch (error) {
    statusbarmessage.dispose();
    showErrorMessage('convertMarkdownToHtml()', error);
  }
}


/*
 * https://github.com/microsoft/vscode/blob/ca4ceeb87d4ff935c52a7af0671ed9779657e7bd/extensions/markdown-language-features/src/slugify.ts#L26
 */
function Slug(string) {
  try {
    var stg = encodeURI(
      string.trim()
            .toLowerCase()
            .replace(/\s+/g, '-') // Replace whitespace with -
            .replace(/[\]\[\!\'\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~\`。,、;:?!…—·ˉ¨‘’“”々~‖∶"'`|〃〔〕〈〉《》「」『』.〖〗【】()[]{}]/g, '') // Remove known punctuators
            .replace(/^\-+/, '') // Remove leading -
            .replace(/\-+$/, '') // Remove trailing -
    );
    return stg;
  } catch (error) {
    showErrorMessage('Slug()', error);
  }
}


/*
 * make html
 */
function makeHtml(data, uri) {
  try {

    // get title
    let title = path.basename(uri.fsPath);

    // read base
    let filePath = path.join(myGroundPath,'base','base.html')
    var base = readFile(filePath);

    // read frames
    let frame = '';
    frame += readFrames(uri);

    // read styles
    let style = '';
    style += readStyles(uri);

    // read scripts
    let script = '';
    script += readScripts(uri);

    // read mermaid javascripts
    let mermaidServer = vscode.workspace.getConfiguration('markdown-pdf')['mermaidServer'] || '';
    let mermaid = '<script src=\"' + mermaidServer + '\"></script>';

    // compile html
    let mustache = require('mustache');
    let view = {
      title: title,
      frame: frame,
      style: style,
      script: script,
      content: data,
      mermaid: mermaid
    };
    return mustache.render(base, view);
  } catch (error) {
    showErrorMessage('makeHtml()', error);
  }
}


/*
 * export a html to a html file
 */
function exportHtml(data, filename) {
  fs.writeFile(filename, data, 'utf-8', function (error) {
    if (error) {
      showErrorMessage('exportHtml()', error);
      return;
    }
  });
}


/*
 * export a html to a pdf file (html-pdf)
 */
function exportPdf(data, filename, type, uri) {

  if (!INSTALL_CHECK) {
    return;
  }
  if (!checkPuppeteerBinary()) {
    showErrorMessage('Chromium or Chrome does not exist! \
      See https://github.com/yzane/vscode-markdown-pdf#install');
    return;
  }

  var StatusbarMessageTimeout = vscode.workspace.getConfiguration('markdown-pdf')['StatusbarMessageTimeout'];
  vscode.window.setStatusBarMessage('');
  var exportFilename = getOutputDir(filename, uri);

  return vscode.window.withProgress({
    location: vscode.ProgressLocation.Notification,
    title: '[Markdown PDF]: Exporting (' + type + ') ...'
    }, async () => {
      try {
        // export html
        if (type == 'html') {
          exportHtml(data, exportFilename);
          vscode.window.setStatusBarMessage('$(markdown) ' + exportFilename, StatusbarMessageTimeout);
          return;
        }

        const puppeteer = require('puppeteer-core');
        // create temporary file
        var f = path.parse(filename);
        var tmpfilename = path.join(f.dir, f.name + '_tmp.html');
        exportHtml(data, tmpfilename);
        var options = {
          executablePath: vscode.workspace.getConfiguration('markdown-pdf')['executablePath'] || puppeteer.executablePath(),
          args: ['--lang='+vscode.env.language, '--no-sandbox', '--disable-setuid-sandbox']
          // Setting Up Chrome Linux Sandbox
          // https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md#setting-up-chrome-linux-sandbox
      };
        const browser = await puppeteer.launch(options);
        const page = await browser.newPage();
        await page.goto(vscode.Uri.file(tmpfilename).toString(), { waitUntil: 'networkidle0', timeout:0});
        // generate pdf
        // https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagepdfoptions
        if (type == 'pdf') {
          // If width or height option is set, it overrides the format option.
          // In order to set the default value of page size to A4, we changed it from the specification of puppeteer.
          var width_option = vscode.workspace.getConfiguration('markdown-pdf', uri)['width'] || '';
          var height_option = vscode.workspace.getConfiguration('markdown-pdf', uri)['height'] || '';
          var format_option = '';
          if (!width_option && !height_option) {
            format_option = vscode.workspace.getConfiguration('markdown-pdf', uri)['format'] || 'A4';
          }
          var landscape_option;
          if (vscode.workspace.getConfiguration('markdown-pdf', uri)['orientation'] == 'landscape') {
            landscape_option = true;
          } else {
            landscape_option = false;
          }
          var options = {
            path: exportFilename,
            scale: vscode.workspace.getConfiguration('markdown-pdf', uri)['scale'],
            displayHeaderFooter: vscode.workspace.getConfiguration('markdown-pdf', uri)['displayHeaderFooter'],
            headerTemplate: vscode.workspace.getConfiguration('markdown-pdf', uri)['headerTemplate'] || '',
            footerTemplate: vscode.workspace.getConfiguration('markdown-pdf', uri)['footerTemplate'] || '',
            printBackground: vscode.workspace.getConfiguration('markdown-pdf', uri)['printBackground'],
            landscape: landscape_option,
            pageRanges: vscode.workspace.getConfiguration('markdown-pdf', uri)['pageRanges'] || '',
            format: format_option,
            width: vscode.workspace.getConfiguration('markdown-pdf', uri)['width'] || '',
            height: vscode.workspace.getConfiguration('markdown-pdf', uri)['height'] || '',
            margin: {
              top: vscode.workspace.getConfiguration('markdown-pdf', uri)['margin']['top'] || '',
              right: vscode.workspace.getConfiguration('markdown-pdf', uri)['margin']['right'] || '',
              bottom: vscode.workspace.getConfiguration('markdown-pdf', uri)['margin']['bottom'] || '',
              left: vscode.workspace.getConfiguration('markdown-pdf', uri)['margin']['left'] || ''
            }
          }
          await page.pdf(options);
        }

        // generate png and jpeg
        // https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagescreenshotoptions
        if (type == 'png' || type == 'jpeg') {
          // Quality options do not apply to PNG images.
          var quality_option;
          if (type == 'png') {
            quality_option = undefined;
          }
          if (type == 'jpeg') {
            quality_option = vscode.workspace.getConfiguration('markdown-pdf')['quality'] || 100;
          }

          // screenshot size
          var clip_x_option = vscode.workspace.getConfiguration('markdown-pdf')['clip']['x'] || null;
          var clip_y_option = vscode.workspace.getConfiguration('markdown-pdf')['clip']['y'] || null;
          var clip_width_option = vscode.workspace.getConfiguration('markdown-pdf')['clip']['width'] || null;
          var clip_height_option = vscode.workspace.getConfiguration('markdown-pdf')['clip']['height'] || null;
          var options;
          if (clip_x_option !== null && clip_y_option !== null && clip_width_option !== null && clip_height_option !== null) {
            options = {
              path: exportFilename,
              quality: quality_option,
              fullPage: false,
              clip: {
                x: clip_x_option,
                y: clip_y_option,
                width: clip_width_option,
                height: clip_height_option,
              },
              omitBackground: vscode.workspace.getConfiguration('markdown-pdf')['omitBackground'],
            }
          } else {
            options = {
              path: exportFilename,
              quality: quality_option,
              fullPage: true,
              omitBackground: vscode.workspace.getConfiguration('markdown-pdf')['omitBackground'],
            }
          }
          await page.screenshot(options);
        }

        await browser.close();

        // delete temporary file
        var debug = vscode.workspace.getConfiguration('markdown-pdf')['debug'] || false;
        if (!debug) {
          if (isExistsPath(tmpfilename)) {
            deleteFile(tmpfilename);
          }
        }

        vscode.window.setStatusBarMessage('$(markdown) ' + exportFilename, StatusbarMessageTimeout);
      } catch (error) {
        showErrorMessage('exportPdf()', error);
      }
    } // async
  ); // vscode.window.withProgress
}


function isExistsPath(path) {
  if (path.length === 0) {
    return false;
  }
  try {
    fs.accessSync(path);
    return true;
  } catch (error) {
    console.warn(error.message);
    return false;
  }
}

function isExistsDir(dirname) {
  if (dirname.length === 0) {
    return false;
  }
  try {
    if (fs.statSync(dirname).isDirectory()) {
      return true;
    } else {
      console.warn('Directory does not exist!') ;
      return false;
    }
  } catch (error) {
    console.warn(error.message);
    return false;
  }
}

function deleteFile (path) {
  var rimraf = require('rimraf')
  rimraf.sync(path);
}

function getOutputDir(filename, resource) {
  try {
    var outputDir;
    if (resource === undefined) {
      return filename;
    }
    var outputDirectory = vscode.workspace.getConfiguration('markdown-pdf')['outputDirectory'] || '';
    if (outputDirectory.length === 0) {
      return filename;
    }

    // Use a home directory relative path If it starts with ~.
    if (outputDirectory.indexOf('~') === 0) {
      outputDir = outputDirectory.replace(/^~/, os.homedir());
      mkdir(outputDir);
      return path.join(outputDir, path.basename(filename));
    }

    // Use path if it is absolute
    if (path.isAbsolute(outputDirectory)) {
      if (!isExistsDir(outputDirectory)) {
        showErrorMessage(`The output directory specified by the markdown-pdf.outputDirectory option does not exist.\
          Check the markdown-pdf.outputDirectory option. ` + outputDirectory);
        return;
      }
      return path.join(outputDirectory, path.basename(filename));
    }

    // Use a workspace relative path if there is a workspace and markdown-pdf.outputDirectoryRootPath = workspace
    var outputDirectoryRelativePathFile = vscode.workspace.getConfiguration('markdown-pdf')['outputDirectoryRelativePathFile'];
    let root = vscode.workspace.getWorkspaceFolder(resource);
    if (outputDirectoryRelativePathFile === false && root) {
      outputDir = path.join(root.uri.fsPath, outputDirectory);
      mkdir(outputDir);
      return path.join(outputDir, path.basename(filename));
    }

    // Otherwise look relative to the markdown file
    outputDir = path.join(path.dirname(resource.fsPath), outputDirectory);
    mkdir(outputDir);
    return path.join(outputDir, path.basename(filename));
  } catch (error) {
    showErrorMessage('getOutputDir()', error);
  }
}

function mkdir(path) {
  if (isExistsDir(path)) {
    return;
  }
  var mkdirp = require('mkdirp');
  return mkdirp.sync(path);
}

function readFile(filename, encode) {
  if (filename.length === 0) {
    return '';
  }
  if (!encode && encode !== null) {
    encode = 'utf-8';
  }
  if (filename.indexOf('file://') === 0) {
    if (process.platform === 'win32') {
      filename = filename.replace(/^file:\/\/\//, '')
    } else {
      filename = filename.replace(/^file:\/\//, '');
    }
  }
  if (isExistsPath(filename)) {
    return fs.readFileSync(filename, encode);
  } else {
    return '';
  }
}

function convertImgPath(src, filename) {
  try {
    var href = decodeURIComponent(src);
    href = href.replace(/("|')/g, '')
          .replace(/\\/g, '/')
          .replace(/#/g, '%23');
    var protocol = url.parse(href).protocol;
    if (protocol === 'file:' && href.indexOf('file:///') !==0) {
      return href.replace(/^file:\/\//, 'file:///');
    } else if (protocol === 'file:') {
      return href;
    } else if (!protocol || path.isAbsolute(href)) {
      href = path.resolve(path.dirname(filename), href).replace(/\\/g, '/')
                                                      .replace(/#/g, '%23');
      if (href.indexOf('//') === 0) {
        return 'file:' + href;
      } else if (href.indexOf('/') === 0) {
        return 'file://' + href;
      } else {
        return 'file:///' + href;
      }
    } else {
      return src;
    }
  } catch (error) {
    showErrorMessage('convertImgPath()', error);
  }
}



// series of makes.


function makeFrame(filename) {
  try {
    let frame = readFile(filename);
    if (frame) {
      return '\n' + frame + '\n';
    } else {
      return '';
    }
  } catch (error) {
    showErrorMessage("makeFrame()",error);
  }
}


function makeStyle(filename) {
  try {
    let css = readFile(filename);
    if (css) {
      return '\n<style>\n' + css + '\n</style>\n';
    } else {
      return '';
    }
  } catch (error) {
    showErrorMessage('makeStyle()', error);
  }
}


function makeScript(filename) {
  try {
    let script = readFile(filename);
    if (script) {
      return '\n<script>\n' + script + '\n</script>\n';
    } else {
      return '';
    }
  } catch (error) {
    showErrorMessage("makeScript()",error);
  }
}



// series of reads.


function readFrames(uri) {
  try {
    let frame='';
    let filePath='';
    let fileName='';
    let filePlace='';
    let frameList=[];
    let i=0;

    // 0. read the frame of template.
    filePath = path.join(myGroundPath,'template','template.html');
    frame += makeFrame(filePath);
    vscode.window.showInformationMessage("get template.html");

    // 1. read the frame of plugins.
    filePlace = path.join(myGroundPath,'plugins')
    frameList = myPluginList;
    if (frameList && Array.isArray(frameList) && frameList.length > 0) {
      for (i = 0; i < frameList.length; i++) {
        fileName = frameList[i] + ".html";
        filePath = path.join(filePlace,frameList[i],fileName);
        frame += makeFrame(filePath);
        }
        vscode.window.showInformationMessage("get frame plugins");
      }
    
    vscode.window.showInformationMessage("finish readFrames!");
    return frame;
  } catch (error) {
    showErrorMessage('readFrames()', error);
  }
}


function readStyles(uri) {
  try {
    let style='';
    let filePath='';
    let fileName='';
    let filePlace='';
    let styleList=[];
    let i=0;
    let href='';

    var includeDefaultStyles = vscode.workspace.getConfiguration('markdown-pdf')['includeDefaultStyles'];
    var highlightStyle = vscode.workspace.getConfiguration('markdown-pdf')['highlightStyle'] || '';
    var ishighlight = vscode.workspace.getConfiguration('markdown-pdf')['highlight'];

    // 0. read the frame of template.
    filePath = path.join(myGroundPath,'template','template.css');
    style += makeStyle(filePath);
    vscode.window.showInformationMessage("get template.css");

    // 1. read the style of the vscode.
    if (includeDefaultStyles) {
      filePath = path.join(__dirname, 'styles', 'markdown.css');
      style += makeStyle(filePath);
    }

    // 2. read the style of the markdown.styles setting.
    if (includeDefaultStyles) {
      styleList = vscode.workspace.getConfiguration('markdown')['styles'];
      if (styleList && Array.isArray(styleList) && styleList.length > 0) {
        for (i = 0; i < styleList.length; i++) {
          href = fixHref(uri, styleList[i]);
          style += '<link rel=\"stylesheet\" href=\"' + href + '\" type=\"text/css\">';
        }
      }
    }

    // 3. read the style of the highlight.js.
    if (ishighlight) {
      if (highlightStyle) {
        var css = vscode.workspace.getConfiguration('markdown-pdf')['highlightStyle'] || 'github.css';
        filePath = path.join(__dirname, 'node_modules', 'highlight.js', 'styles', css);
        style += makeStyle(filePath);
      } else {
        filePath = path.join(__dirname, 'styles', 'tomorrow.css');
        style += makeStyle(filePath);
      }
    }

    // 4. read the style of the markdown-pdf.
    if (includeDefaultStyles) {
      filePath = path.join(__dirname, 'styles', 'markdown-pdf.css');
      style += makeStyle(filePath);
    }

    // 5. read the style of the markdown-pdf.styles settings.
    styleList = vscode.workspace.getConfiguration('markdown-pdf')['styles'] || '';
    if (styleList && Array.isArray(styleList) && styleList.length > 0) {
      for (i = 0; i < styleList.length; i++) {
        href = fixHref(uri, styleList[i]);
        style += '<link rel=\"stylesheet\" href=\"' + href + '\" type=\"text/css\">';
      }
    }
    // 6. read the frame of plugins.
    filePlace = path.join(myGroundPath,'plugins')
    styleList = myPluginList;
    if (styleList && Array.isArray(styleList) && styleList.length > 0) {
      for (i = 0; i < styleList.length; i++) {
        fileName = styleList[i] + ".css";
        filePath = path.join(filePlace,styleList[i],fileName);
        style += makeStyle(filePath);
        }
        vscode.window.showInformationMessage("get style plugins");
      }
    
    vscode.window.showInformationMessage("finish readFrames!");
    return style;
  } catch (error) {
    showErrorMessage('readStyles()', error);
  }
}


function readScripts(uri) {
  try {
    let script='';
    let filePath='';
    let fileName='';
    let filePlace='';
    let scriptList=[];
    let i=0;

    // 0. read the frame of template.
    filePath = path.join(myGroundPath,'template','template.js');
    script += makeScript(filePath);
    vscode.window.showInformationMessage("get template.js");

    // 1. read the frame of plugins.
    filePlace = path.join(myGroundPath,'plugins')
    scriptList = myPluginList;
    if (scriptList && Array.isArray(scriptList) && scriptList.length > 0) {
      for (i = 0; i < scriptList.length; i++) {
        fileName = scriptList[i] + ".js";
        filePath = path.join(filePlace,scriptList[i],fileName);
        script += makeScript(filePath);
        }
        vscode.window.showInformationMessage("get script plugins");
      }

    vscode.window.showInformationMessage("finish readScripts!");
    return script;
  } catch (error) {
    showErrorMessage('readScripts()', error);
  }
}



/*
 * vscode/extensions/markdown-language-features/src/features/previewContentProvider.ts fixHref()
 * https://github.com/Microsoft/vscode/blob/0c47c04e85bc604288a288422f0a7db69302a323/extensions/markdown-language-features/src/features/previewContentProvider.ts#L95
 *
 * Extension Authoring: Adopting Multi Root Workspace APIs ?E Microsoft/vscode Wiki
 * https://github.com/Microsoft/vscode/wiki/Extension-Authoring:-Adopting-Multi-Root-Workspace-APIs
 */
function fixHref(resource, href) {
  try {
    if (!href) {
      return href;
    }

    // Use href if it is already an URL
    const hrefUri = vscode.Uri.parse(href);
    if (['http', 'https'].indexOf(hrefUri.scheme) >= 0) {
      return hrefUri.toString();
    }

    // Use a home directory relative path If it starts with ^.
    if (href.indexOf('~') === 0) {
      return vscode.Uri.file(href.replace(/^~/, os.homedir())).toString();
    }

    // Use href as file URI if it is absolute
    if (path.isAbsolute(href)) {
      return vscode.Uri.file(href).toString();
    }

    // Use a workspace relative path if there is a workspace and markdown-pdf.stylesRelativePathFile is false
    var stylesRelativePathFile = vscode.workspace.getConfiguration('markdown-pdf')['stylesRelativePathFile'];
    let root = vscode.workspace.getWorkspaceFolder(resource);
    if (stylesRelativePathFile === false && root) {
      return vscode.Uri.file(path.join(root.uri.fsPath, href)).toString();
    }

    // Otherwise look relative to the markdown file
    return vscode.Uri.file(path.join(path.dirname(resource.fsPath), href)).toString();
  } catch (error) {
    showErrorMessage('fixHref()', error);
  }
}

function checkPuppeteerBinary() {
  try {
    // settings.json
    var executablePath = vscode.workspace.getConfiguration('markdown-pdf')['executablePath'] || ''
    if (isExistsPath(executablePath)) {
      INSTALL_CHECK = true;
      return true;
    }

    // bundled Chromium
    const puppeteer = require('puppeteer-core');
    executablePath = puppeteer.executablePath();
    if (isExistsPath(executablePath)) {
      return true;
    } else {
      return false;
    }
  } catch (error) {
    showErrorMessage('checkPuppeteerBinary()', error);
  }
}



/*
 * puppeteer install.js
 * https://github.com/GoogleChrome/puppeteer/blob/master/install.js
 */
function installChromium() {
  try {
    vscode.window.showInformationMessage('[Markdown PDF] Installing Chromium ...');
    var statusbarmessage = vscode.window.setStatusBarMessage('$(markdown) Installing Chromium ...');

    // proxy setting
    setProxy();

    var StatusbarMessageTimeout = vscode.workspace.getConfiguration('markdown-pdf')['StatusbarMessageTimeout'];
    const puppeteer = require('puppeteer-core');
    const browserFetcher = puppeteer.createBrowserFetcher();
    const revision = require(path.join(__dirname, 'node_modules', 'puppeteer-core', 'package.json')).puppeteer.chromium_revision;
    const revisionInfo = browserFetcher.revisionInfo(revision);

    // download Chromium
    browserFetcher.download(revisionInfo.revision, onProgress)
      .then(() => browserFetcher.localRevisions())
      .then(onSuccess)
      .catch(onError);

    function onSuccess(localRevisions) {
      console.log('Chromium downloaded to ' + revisionInfo.folderPath);
      localRevisions = localRevisions.filter(revision => revision !== revisionInfo.revision);
      // Remove previous chromium revisions.
      const cleanupOldVersions = localRevisions.map(revision => browserFetcher.remove(revision));

      if (checkPuppeteerBinary()) {
        INSTALL_CHECK = true;
        statusbarmessage.dispose();
        vscode.window.setStatusBarMessage('$(markdown) Chromium installation succeeded!', StatusbarMessageTimeout);
        vscode.window.showInformationMessage('[Markdown PDF] Chromium installation succeeded.');
        return Promise.all(cleanupOldVersions);
      }
    }

    function onError(error) {
      statusbarmessage.dispose();
      vscode.window.setStatusBarMessage('$(markdown) ERROR: Failed to download Chromium!', StatusbarMessageTimeout);
      showErrorMessage('Failed to download Chromium! \
        If you are behind a proxy, set the http.proxy option to settings.json and restart Visual Studio Code. \
        See https://github.com/yzane/vscode-markdown-pdf#install', error);
    }

    function onProgress(downloadedBytes, totalBytes) {
      var progress = parseInt(downloadedBytes / totalBytes * 100);
      vscode.window.setStatusBarMessage('$(markdown) Installing Chromium ' + progress + '%' , StatusbarMessageTimeout);
    }
  } catch (error) {
    showErrorMessage('installChromium()', error);
  }
}

function showErrorMessage(msg, error) {
  vscode.window.showErrorMessage('ERROR: ' + msg);
  console.log('ERROR: ' + msg);
  if (error) {
    vscode.window.showErrorMessage(error.toString());
    console.log(error);
  }
}

function setProxy() {
  var https_proxy = vscode.workspace.getConfiguration('http')['proxy'] || '';
  if (https_proxy) {
    process.env.HTTPS_PROXY = https_proxy;
    process.env.HTTP_PROXY = https_proxy;
  }
}

function setBooleanValue(a, b) {
  if (a === false) {
    return false
  } else {
    return a || b
  }
}

function init() {
  try {
    if (checkPuppeteerBinary()) {
      INSTALL_CHECK = true;
    } else {
      installChromium();
    }
  } catch (error) {
    showErrorMessage('init()', error);
  }
}

感想

readCssからmakeStyleが出てくるの違和感がありすぎるのでreadStyleにしてやった,それらと同様の流れでframe(htmlファイル)とscript(jsファイル)を扱えるようにした,あと変更する範囲でvarの使用をなるべくやめさせた,変数宣言で型を明示したくなった,なぜかvscode上でコンソールが表示されなかったがvscode.window.showErrorMessageは役立った,ブラウザでコンソール見るのは大事だと思った

vscode上でmarkdown pdf + katexで速攻で数式をpdf化

準備

とりあえずこれを入れよう

最近のvscodeはデフォルトでmath形式びmdファイルもちゃんと表示してくれるらしい

概要

vscode上でmarkdownをプレビューする時に数式を扱えるmarkdown mathやvscode内蔵のビュワーは,内部でkatexを動かしてレンダリングしながら表示しているらしく,またそのkatexはブラウザやサーバーサイドにおいてhtml+cssで完結して動くため,svgを弄ってmathmlを設定するmathjaxよりも更に軽量なアドオンらしく,そしてそのmarkdown pdfはmarkdownからhtml+cssを経由したのちにpdfに変換している…
つまりhtml+cssレンダリング処理に介入して(オプションを使って)katexを動作させれば,markdownを爆速でpdfへと変換できるね(デフォルトでは何も設定されていないため数式はレンダリングされないよ),という流れ

手順

  • コマンドパレットから拡張機能フォルダを開いてお目当てのtemplate.htmlを探す
    • macならば/Users/[ユーザー名]/.vscode/extensions/yzane.markdown-pdf-1.4.4/template/template.htmlに確実にあるはず,Windows?知らん
  • katex.org
    • をよく読む
  • katex.org
    • をよく読む
  • katex.org
    • をよく読む
  • そして以下をコピペ
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.15.1/dist/katex.min.css" integrity="sha384-R4558gYOUz8mP9YWpZJjofhk+zx0AS11p36HnD2ZKj/6JR5z27gSSULCNHIRReVs" crossorigin="anonymous">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.15.1/dist/katex.min.js" integrity="sha384-z1fJDqw8ZApjGO3/unPWUPsIymfsJmyrDVWC8Tv/a1HeOtGmkwNd/7xUS0Xcnvsx" crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.15.1/dist/contrib/auto-render.min.js" integrity="sha384-+XBljXPPiv+OzfbB3cVmLHf4hdUFHlWNZN5spNQ7rmHTXpd7WvJum6fIACpNNfIR" crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.15.1/dist/contrib/mathtex-script-type.min.js" integrity="sha384-jiBVvJ8NGGj5n7kJaiWwWp9AjC+Yh8rhZY3GtAX8yU28azcLgoRo4oukO87g7zDT" crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.15.1/dist/contrib/mhchem.min.js" integrity="sha384-UEY9IRPkV+TTTY7nK1wSrfhWPDJy9wr4PmYg3DLPcN5F4NDlIwGZkWtWveKR/45c"  crossorigin="anonymous"></script>
<link href="https://cdn.jsdelivr.net/npm/katex@0.15.1/dist/contrib/copy-tex.min.css" rel="stylesheet" type="text/css">
<script src="https://cdn.jsdelivr.net/npm/katex@0.15.1/dist/contrib/copy-tex.min.js" integrity="sha384-Ep9Es0VCjVn9dFeaN2uQxgGcGmG+pfZ4eBaHxUpxXDORrrVACZVOpywyzvFRGbmv" crossorigin="anonymous"></script>
<script>
    document.addEventListener("DOMContentLoaded", function() {
        renderMathInElement(document.body, {
          delimiters: [
              {left: '$$', right: '$$', display: true},
              {left: '$', right: '$', display: false}
          ],
          throwOnError : false,
          output:"mathml",
          strict: false,
        });
    });
</script>
  • これらを{{{style}}}{{{mermaid}}}の間に(<head><\head>の中に)挟んでおく
  • {{{style}}}{{{mermaid}}}{{{content}}}レンダリング結果を注入するのに必要な起点なので消すと動かなくなる
  • delimiterは好みに書き換えても良いが困らなければこのままで良い(このままの方が良い)
  • 恐らくkatexのheadバージョンが変われば先のページも書き変わるので(srcに載ってるURLにもSSHのオプション引数が設定されているし),気が向いた際にちゃんと確認しておこう

注意点

ブラウザにレンダリングをさせるための工夫

throwOnError : falseoutput:"mathml"が必要,エラーはなるべくは無視してくれる方が良いし,"html"では後述のcopy-tex extensionが動作しなくなる,現状では"mathml"で困ることもなさそうなので

ディスプレイモードで改行を行うための工夫

strict: falseが必要かつ改行には\\\\が必要,前者はkatexの改行制限を外すため,後者は中間生成の時点で働く謎のエスケープ処理を回避するため
もし挙動が変だと思った時は一旦htmlで出力してデバッグするべし,markdown pdfの処理が自分の想定外である場合が大半

アンダースコア_の扱いの工夫

markdown pdfが強調タグ(<em><\em>)に変換しかねない,発生条件は定かではないが|_{}すると50%の確率で<em><\em>のペアに化かしてくる,必ずエスケープした\|\_{}を使う癖をつけるべし

字体の変化の正当な手順

現在のlatexでは\rm{}が正当なやり方とされているが,katexでは{\rm }を使わないと変な処理が起きて困る,理由は不明だが古い処理系を使っているせいと思われる

その他の環境を使いたい

ディスプレイモードに限定されるが,$$で囲った中身でbegin{xxx}end{xxx}を使う必要がある,そこを直で呼び出せるmathjaxとは訳が違うらしい
例)

$$
  \begin{align}
    x &= y \\\\
    &= z
  \end{align}
$$

もちろん改行には\\\\を使う

拡張機能

恐らくこれで全部のはず

mathJax後方互換

mathtex-script-type.min.jsを使う
mathタグで有効化することでmathjaxをまんま置き換えられるらしい

化学記法互換

mhchem.min.jsを使う
化学に関する記法を追加するらしい

mathmlコピペ互換

copy-tex.min.csscopy-tex.min.jsを使う
mdからhtmlに出力した際にクリップボードからlatex記法のコードをそのまま取り出せるようになる,pdfには対応していないが(ブラウザ上のclipboard apiに相当する機能がないので)htmlで見れる環境なら絶対に採用するべし

vscodeの設定用json + markdown pdf

場所

既定の(デフォルト)設定

所在不明,呼び出しても読み取り専用エディタとして出てくるだけ,どこに実体があるのかはさっぱり…

ユーザー(グローバル)設定

/Users/[ユーザ名]/Library/Application Support/Code/User/settings.json

普段から使い回したい設定は全てこちらに書いておく,latexmk用の起爆ソースとか
デフォルト設定をオーバーライドできる,対象はほぼ全てのオプション

ワークスペース(ウィンドウ)設定

workspace.code-workspace

ワークスペースで限定的に使いたい時に書く,エディタのテーマを切り替えるとか
グローバル設定をオーバーライドできる,対象はほぼ全てのオプション

vscodeワークスペースとは即ちウィンドウ1つのこと,処理のカスタマイズはなるべく書かない方が良い気がする

フォルダー(ローカル)設定

.vscode/settings.json

処理のカスタマイズなど環境を汚染したくない時に書く,今回はココを弄ってみる
ディレクトリに1つだけ作成できて,ワークスペースを含めてそのフォルダを開いたら必ず共有される
なおworkspace.code-workspaceが存在しない場合はなぜかこれがワークスペース設定としてGUIに表示されるし,設定範囲はワークスペース設定と同じになっている,意味不明というか初見のユーザーに優しくないね…

ローカル設定をオーバーライドできる,対象は一部のオプション(ここでしか変えられないものもあるみたいだが…)

使い所

試しにmarkdown pdfの書式を弄ってみる

以下コピペ

書式の一例,.vscode/settings.jsonに以下をコピペして成形すると良い

{
    "markdown-pdf.headerTemplate":
        "<div style=\"font-size: 12px; margin:0 12px 0 12px; width:60%;\"><span style=\"float: left;\">ここにタイトル</span></div> <div style=\"font-size: 12px; margin:0 12px 0 12px; width:30%;\"><span style=\"float: left;\">ここに名前</span></div> <div style=\"font-size: 12px; margin:0 12px 0 12px; width:10%;\"><span style=\"float: right;\" class='date'></span></div>",
    "markdown-pdf.footerTemplate":
        "<div style=\"font-size: 12px; margin:0 12px 0 12px; width:60%;\"><span style=\"float: right;\">ここに署名</span></div> <div style=\"font-size: 9px; margin:0 12px 0 12px; width:40%;\" ><span class='pageNumber'></span> ページ目 / <span class='totalPages'></span> ページ中</div>"
}

どうやら環境変数や引数をブチ込むみたいな芸当はこれだけでは無理らしい(できるの?)

バグ?

<span class='pageNumber'></span><span class='totalPages'></span>は現在ページと合計ページをそれぞれ出力する,しかし内外からその文字サイズを9pxより大きく設定してしまうとなぜか表示されなくなって詰む
また最外の<div>に上下向きのmarginを設定するとレイアウトが崩れてフッタが本文に隠されてしまい詰む,それどころかpaddingしても詰むし文字が大きすぎても詰むので,あまり大きく弄らない方が吉

前者はmarkdown pdfがやりたいhtml+cssにおける動的な注入に対して不都合であり,どうやらmarkdown-pdfのさらに実装元のプラグインであるpuppeteerにまで問題が遡るらしい,"puppeteer"+"pageNumber"+"font size"で検索してみれば「10pxにすると出ますよ」「9pxにすると出ますよ」みたいな内容が見つかる

後者はmarkdown-pdf > margin:bottommarkdown-pdf > margin:topの値を弄れば解決するが,あんまり大きくても見栄えが悪いし何ならタイトルページは別に作れば良いので,なるべくはスマートになるよう心がけよう

LaTeXとの比較

Markdownなので機能が軽いとは言えど学習コストも抑えられて設定も楽でコンパイルも手間や時間がかからずhtmlもodfも生成できると考えるとクッソ良い(個人の感想)

サンプル

htmlの時点でこんな感じ
見出しは6段階までらしい

example
example

# welcome

## to

### my

#### slightly

##### beautiful

###### dream

####### ...?


OK.

````zsh
sudo rm -rf /
````

$$
  e^{i \pi} + 1 = 0
$$

$$
\begin{pmatrix}
  1 & 0 \\\\
  0 & 1
\end{pmatrix} \\\\
against, \\\\
\begin{vmatrix}
  1 & 0 \\\\
  0 & 1
\end{vmatrix}
$$

この世をば 我が世とぞ思ふ 望月の

欠けたることも 無しと思へば
body {
    color: white;
    background-color: black
}
{
    "markdown-pdf.headerTemplate":
        "<div style=\"font-size: 12px; margin:0 12px 0 12px; width:60%;\"><span style=\"float: left;\">this is my report</span></div> <div style=\"font-size: 12px; margin:0 12px 0 12px; width:30%;\"><span style=\"float: left;\">Soluna Eureka</span></div> <div style=\"font-size: 12px; margin:0 12px 0 12px; width:10%;\"><span style=\"float: right;\" class='date'></span></div>",
    "markdown-pdf.footerTemplate":
        "<div style=\"font-size: 12px; margin:0 12px 0 12px; width:60%;\"><span style=\"float: right;\">tuned by S.E.</span></div> <div style=\"font-size: 9px; margin:0 12px 0 12px; width:40%;\" ><span class='pageNumber'></span> page of <span class='totalPages'></span> pages </div>",
    "markdown-pdf.margin.top": "2cm",
    "markdown-pdf.margin.bottom": "2cm",
    "markdown-pdf.highlightStyle": "night-owl.css",
    "markdown-pdf.styles": [
        "./change.css"
    ],
    "markdown-pdf.type": [
        "pdf",
        "html"
    ],
    "markdown-pdf.orientation": "landscape",
    "markdown-pdf.convertOnSave": true,
}

なんと無理矢理に横長にできてしまうし背景色もコードブロックのスタイルも弄れるのだ

だがpdfにすると余白領域が真っ白のまま,ダサ過ぎないか…?

example2
example2

コアのカスタマイズ

タイムアウトの阻止

いくらmdファイルが長くともhtmlレンダリングは一瞬で終わるが,pdfレンダリングは処理時間が30000ms(30秒)をオーバーして強制停止されることでお目当てのブツがエクスポートされないケースがある.だいたいpuppeteerの仕様のせいだが元々は異常動作をさせないためにかけられた防御であって,これをクリアするには手動でtimeout時間を0に設定する必要がある(その代わりセキュリティリスクは高まるらしいが).

github.com

具体的には/Users/[ユーザ名]/.vscode/extensions/yzane.markdown-pdf-[バージョン表記]/extension.jsの407行目あたりで

await page.goto(vscode.Uri.file(tmpfilename).toString(), { waitUntil: 'networkidle0', timeout:0});

要はawaitの呼び出し先の関数page.gotoの引数にtimeout:0を入れてやれば良い.
行数が違くても下の行に

// generate pdf
// https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagepdfoptions

とか書いてあるしわかりやすいと思われる.つーか多分このことを言ってるんじゃないかな…?

github.com

せや!結局は`streamlink`で`ffmpeg`を使えばええ!

準備

ffmpegstreamlinkが依存してる)とstreamlinkbrewで入れる

formulae.brew.sh

formulae.brew.sh

brew install ffmpeg

brew install streamlink

以下の通り環境変数PATHffmpegがないと死なので確認する

man streamlink
...
 FFmpeg options

       --ffmpeg-ffmpeg FILENAME
              FFMPEG is used to access or mux separate video and audio streams. You can specify the location of the
              ffmpeg executable if it is not in your PATH.

              Example: "/usr/local/bin/ffmpeg"
...

使い方

hlsでも非hlsでも大丈夫

  1. 対象のwebサービスに行って動画を再生して
  2. 「webインスベクタ」を表示,「ネットワーク」を表示
  3. 頑張って拡張子が.m3u8.mp4のファイルを検索
  4. 前者がhls,後者が非hls
  5. そのファイルを落とした時のcURLをコピー
  6. cURLから標準urlだけを抽出
  7. streamlink '[url]' best -o temp
  8. ffmpeg -i tmp -c copy file.mp4

何が起きてるか

streaming系サービスでのhttp通信をうまいこと管理して,hls処理ができるffmpegに渡してくれてる
ffmpegに不足してるhttp管理機能とcurlに不足してるhls処理機能を両立してくれてこれは…ありがたい

細かいオプション

ffmpeg.org

streamlink.github.io

ニコ動のオプション機能でログイン可能とかセッションID持ち込みとかあったりするらしいな

利点!

独立してセッションを構築してくれるからクッソ楽
もしそれができない事情があるなら適宜にhttpのヘッダ情報を引数で渡せば良いので問題はない

欠点?

処理をせず.m3u8+.tsの形式に落とすのは想定外の動作
hlsだと.mp4になったりならなかったりするのでもっかいffmpegをかける習慣をつけよう
まぁ変換前の形式で落とす必要性もなさそうだし,なんならhlsへの変換もやれるっぽいし(今回はそこには触れないけど)

現行のmacOSで使えるデータの圧縮・暗号

圧縮と暗号は大事

匿名化に加えてこれら3つは3大PCリテラシーだと思います

パスワード付きPDF

ちょっとした書類ならコレで良いと思う

  • プレビューで何らかをpdfを保存するときに
  • 「アクセス権」→「書類を開くときにパスワードを要求」にチェック
  • 専用パスワードを確実に打ちこむ
  • 「アクセス権」の設定でさらに改竄防止を見込める
    • 例外的な許可を出さずにパスワード設定すればおk

PDFの仕様なんてそうそう変わらんし,深くは追求しません,汎用性も低いでしょうし…

パスワード付きで暗号と圧縮

ちょっとしたデータならコレで良いと思う
基本的にFInderからGUI経由で行うことが無理な(オプションが呼び出せない)上に,macOSにデフォルトで入ってるアプリやコマンドは更新性や追従性に乏しく,これはhomebrew経由で外部のツールを導入した方が良いと思う

準備

とりあえずは基本のziprartarを抑えておきたい

formulae.brew.sh

formulae.brew.sh

formulae.brew.sh

えっunrarってつい最近ライセンス問題で公式から消えたらしいですよ

qiita.com

formulae.brew.shuntar は存在しない,動作は全部コマンドで決め打ちしているらしい

  • 通常のbrew環境が構築されてるintel macが前提
    • apple siliconでのhomebrewは色々とパスが違ってくるらしいけど知らん
      • とりあえずRosetta2でやればいいんじゃないのか?
  • コマンド設定
    • 以下の順番を守って取得
      1. brew install zip
      2. brew install unzip
      3. brew install rar
      4. brew install carlocab/personal/unrar
      5. brew install gnu-tar
  • パスを設定
    • rarunrarはデフォルトで/usr/local/binエイリアスが発生する
    • targtarとして/usr/local/binエイリアスがある
    • zipunzipmacデフォルトと衝突すると厄介なことになるらしくエイリアスが生成されていない
      • gzipgzipとして別にあるせいで名前が被ってるせいかもしれんね
    • なので/usr/local/opt/binを作ってパスに通した上で/usr/local/opt/[cask name]/bin/[cask command]シンボリックリンクを置く
      • ついでに同じようにtarも置いとこう
      • mkdir /usr/local/opt/bin
      • ln -s /usr/local/opt/zip/bin/zip /usr/local/opt/bin/zip
      • ln -s /usr/local/opt/unzip/bin/unzip /usr/local/opt/bin/unzip
      • ln -s /usr/local/opt/gnu-tar/bin/gtar /usr/local/opt/bin/tar
      • echo export PATH='/usr/local/opt/bin:$PATH' >> ~/.bashrc
    • 私はPATH = [application path]/usr/local/bin:/usr/local/opt/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbinみたいにしてる
    • 各自のシェルの設定ファイルに合わせること,zshなら~/zprofile~/.zshrcになる
      • bashzshの間なら複製して名前を変えるだけでも割と行けるっぽい…?

実行

zipの場合

  • 暗号
    • zip -A -o -r -n .zip:.bz2:.7z:.rar:.gz:.tgz -X -y -8 -e [zipped file path] [source file path]
      • 例:zip -A -o -r -n .zip:.bz2:.7z:.rar:.gz:.tgz -X -y -8 -e ~/Documents/Private/data.zip ~/Download/list
    • パスワード設定はダブルチェックされる
    • オプションはそれぞれ,
      • -AWindows用の自己解凍.exeファイルが生成
        • つまりmacOSには関係ねぇ
      • -oで出てくる.zipの日付がディレクトリ内の日付の最新版に
      • -r再帰処理にしてディレクトリに対応
      • -n [.extension 1]:[.extension num]で2重圧縮を回避
      • -Xファイルシステムの余計な情報を削除
      • -yで参照先の外部ファイルの混入を阻止
      • -8で圧縮強度を最大化
      • -eで本体を暗号化
    • [zipped file path]のファイル名はそのまま出てくるので末尾に.zipを忘れないように
  • 分割
    • zip -s [num][kmgt] [source file path] -O [zipped file path]
      • 例:zip -s 64k ~/Documents/Private/data.zip -O ~/Documents/Private/spirit.zip
    • 1発目の.zip生成の段階で分割しようとするとエラーを吐くのでNG
    • [source file path]ファイル名は.zipじゃないと認識してくれないので注意
    • パスワードチェックなし
    • オプションはそれぞれ,
      • -s [num][k,m,g,t]で分割サイズを指定,最低でも64kbyte以上
  • 結合
    • zip -s 0 [source file path] -O [zipped file path]
      • 例:zip -s 0 ~/Documents/Private/spirit.zip -O ~/Documents/Private/data2.zip
    • -s 0で1つの.zipに還元される
      • 分割と結合を繰り返すとデータがバグるかもしれん
    • パスワードチェックなし
    • [source file path]には分割時の代表ファイル(.z[num]じゃないもの)を指定する
      • もちろん例に漏れず.zipじゃないと認識してくれない
  • 復号
    • unzip [source file path]
      • 例:unzip ~/Documents/Private/data2.zip
      • 特定のファイル1つを解凍する場合はunzip -p [source file path] > [unzipped file path]で名前指定も可能
        • -pで出力をstdoutに指定してパイプ化しただけ
      • 別のディレクトリに入れる場合はunzip [source file path] -d [any directory path]で押し込める
        • -dとその引数は必ず最後に置くこと
    • パスワードチェック必須
    • .zipじゃなくても読み込んでくれる(中身がダメならダメと言ってくれる)
    • 名前がダブったファイルの強制上書き許可は-o・強制上書き禁止は-n
      • unzip -B [source file path]すると上書きはせずにバージョン別(拡張子を含むファイル名の末尾に~[num])のバックアップが追加される
        • 解凍ファイルの内部の処理ではなく,解凍された中身が生成される該当のディレクトリに
      • これ自体にバージョン管理機能はなく,やりたいなら番号をワイルドカードで全検索して抽出しろって感じか?
      • -oするとそのバックアップすらも上書きできるので注意
    • 先の通りmacOSでは自己解凍はできない

ここでは単純に圧縮と展開のやり方を示したが,本来はアーカイブ更新機能の側面が強く,元ディレクトリとのファイルの差分をとって更新して保存するやり方が一般的かもしれない.基本的に.zipにはロックをかけずアップデートをかけ続ける,それを自動化して低リソースな環境で実行する…みたいな雰囲気を感じる.
そこらが関係するのか軽い気持ちで調べてたら想像以上に複雑だったし,私が望む「使える・壊れない・明かされない」という観点でもzipよりrarの方が良かったりする,そのためアーカイブ更新機能として使いこなすのはまた別の機会にしたい.

rarの場合

  • 暗号
    • rar a -k -r -ts- -s -ds -sfx -ma5 -m5 -hp [rarred file path] [source file path]
    • rar a -k -r -ts- -s -ds -sfx -ma5 -m5 -hp ~/Documents/Private/data.sfx ~/Download/list
    • パスワード設定はダブルチェックされる
    • オプションはそれぞれ,
      • aアーカイブ生成モードを指定
      • -kでロックして再編集を不可能に
      • -rで再起処理にしてディレクトリに対応
      • -ts-で時間系の情報を削除
      • -sでソリッド圧縮を指定
      • -dsで内部の名前順ソートを無効化
      • -sfxで自己解凍実行ファイルを添付
      • -ma5で新しい.rar規格を使用
      • -m5で圧縮強度を最大化
      • -hpでヘッダごと本体を暗号化
    • -sfxの場合は拡張子.sfxが必ずつくが,ない場合は[rarred file path]がそのまま出てくる
  • 復号
    • unrar x [source file path]
      • 自己解凍させたい場合は./[source file path]すると起動する,手順は変わらない
      • 特定のファイル1つを解凍する場合はunrar p -iunl [source file path] > [unrarred file path]で名前指定も可能
        • -pで出力をstdoutに指定した上で-inulでデフォの出力を消さないとダメ
      • 別のディレクトリに入れる場合はunrar x [source file path] [any directory path+/]で押し込める
        • パスの最後に/が入ってない場合は[source file path]と見なされて処理されないので注意
    • パスワードチェック必須
    • .rarじゃなくても読み込んでくれる(中身がダメならダメと言ってくれる)
    • 名前がダブったファイルの強制上書き許可は-o+・強制上書き禁止は-o-

ロックと暗号化が行えるのが強みで,圧縮性・耐破性・暗号性もzipより上,導入も楽で自爆解凍もできる,しかも文法が簡単で覚えやすい.現実では特に欧州で流行ってるらしく,実際に使っている人間は自分の周りでこそほぼ見たことがないものの,割と積極的に使いたい印象を受けた.もうこれだけでよくね?と言える程度には簡潔に完結していると思う.

zipに関する補記

homebrewで入ってくるzipは「16 June 2008 (v3.0)」かつunzipは「unzip20 April 2009 (v6.0)」が最新版となっている.macOSにデフォルトで入っているzipunzipも全く同じであり,要するにわざわざディレクトリを分けて管理しようとしたのは徒労であったということになる.さらに-Zbzipできるがunzipできない(以下の通りなら.zip規格の対応が0.1だけunzipの方が遅れているせいだと思われる)などのドボン選択肢もある.

% zip -rZb arc.zip test
% unzip arc.zip
... need PK compat. v4.6 (can do v4.5)

またzipにおける「エントリ」とは「.zip内にあるファイルもしくはディレクトリ」を指す.「エントリには65535個の制限があるほか,アーカイブのサイズにも4GBの制限がある」なる昔話があったらしいが.これはかつてのzipが32bit動作をしているからであり,それを改善したzip64はその制限を超えられるようになった.
自分のzipunzipがそれに対応しているかどうかについて,以下のように表示されれば対応の確認が取れる.

% zip -v | grep 64
... ZIP64_SUPPORT
% zip -v | grep 64
... ZIP64_SUPPORT

現在は/usr/bin/unzipもzip64に対応しているようだが,アーカイブユーティリティ.appについてはmacOS Catalinaになるまで実装されなかったという目撃情報がある.

taiyakon.com

そもそもzipunzipの公式サイトを見ても最新版がhomebrew上の最新版やmacOSのデフォルト版と一致しており,

infozip.sourceforge.net

これらはInfo-zipという形で2009年までOSSで開発されていた,今後ここから新しくなることはないと考えて良さそう.
後継としてはzlibgzipか,前者はzipで培われた圧縮技術を各言語・各OSで利用可能にするライブラリで,後者はGNUの開発元でGPLライセンスで提供されるファイル圧縮用のツール,という認識で合っているのだろうか?

準備

rarzipの代わりにbzip2gzip7ziplrzip(・rzip)を利用していく

formulae.brew.sh

formulae.brew.sh

formulae.brew.sh

formulae.brew.sh

formulae.brew.sh

  • コマンド設定
    • 以下を取得
      1. brew install gzip
      2. brew install p7zip
      3. brew install bzip2
      4. brew install lrzip
      5. brew install rzip
  • パスを設定
    • bzip2macデフォルトと衝突すると厄介なことになるらしくエイリアスが生成されていない
      • ln -s /usr/local/opt/bzip2/bin/bzip2 /usr/local/opt/bin/bzip2しましょうね
    • コレ以外は普通に/usr/local/binに入ってくれる
    • rzipは使わないが,archlinux wikiにも載ってたので敬意を表して数字に貢献した
      • lrziprzipの機能を利用した上でさらに色々と工夫したらしいな?

実行

以下は順に追記していきます

gzipの場合

  • 圧縮
    • gzip -k -9 -c [source file path] > [zipped file path]
    • [zipped file path]のファイル名はそのまま出てくるので末尾に.gzを忘れないように
      • -c> [zipped file path]がないと[source file path]のファイル名に勝手に.gzがつくけど
    • オプションはそれぞれ,
      • -kで元ファイルを削除しない
      • -9で圧縮強度を最大化
      • -cstdoutに指定
    • ディレクトリを再帰的に辿る実装はなく,tar(後述)でまとめてからgzipするのが正攻法
      • 引数に複数のファイルを並べると全て個別に.gz化されるのが特徴
      • gzip -k -9 -c [source file path1] > [zipped file path1] [source file path num] > [zipped file path num]すれば良い
        • [source file path1] [source file path num] > [zipped file path]すると,.tar同士だけなら最後に展開すればなんとかなるが,.tar以外のファイルが混ざってると詰む
        • manのADVANCED USAGEは嘘こそ言ってないが,例えば画像ファイルの2つをgzipしてgunzipすると元に戻らない,フォルダ構造を失った挙句それをrescueする方法がないから当然とも言えるが
    • 名前がダブったファイルの強制上書き許可は-f,なければ聞かれるまでもなく上書きされない
  • 解凍
    • gzip2 -k -c [source file path] > [unzipped file path]
    • オプションはそれぞれ,
      • -kで元ファイルを削除しない
      • -cstdoutに指定
    • どうせファイルしか出てこないのでこれで覚えた方が良い
    • 名前がダブったファイルの強制上書き許可は-f,なければ聞かれるまでもなく上書きされない
    • gzipを入れると勝手にgunzipもついてくる
      • というよりgzipを使う上で-zで圧縮するか-dで解凍するかの違いしかない

調べた限りではディレクトリ統合機能もパスワード暗号化機能もなさそう,やはり別ライブラリ(後述)を使うべきだろう.

--rsyncableとは?

tar+gzipで保管されるファイルに対して,生成する際に--rsyncableを用いてgzipしておくことで,そのサーバに対してrsyncした際に得られるデータの差分の量そのものがカットできる(2016年のv1.7で実装されたらしい)

superuser.com

translate.google.com

Rsyncable gzipbeeznest.wordpress.com

beeznest-wordpress-com.translate.goog

rsync -zオプションでも圧縮が可能であるが,こちらは差分を取った後に転送するデータを圧縮するため,要するに2段構えの方が(マシンリソースあるなら)絶対に良いし,zipに比べても大きな利点となる

gzip -rgunzip -rの動作

この-r再帰的という意味を示すが,zip -rは渡されたパスが示すディレクトリを基準として配下のディレクトリ・ファイルをまとめて1つの.zipにする一方,gzip -rでは渡されたパスが示すディレクトリもしくはその配下のディレクトリにファイルがあればその場で1つずつ.gzにしていく,同様に1つずつ.gzから戻していくのがgunzip -rである

p7zipの場合

bzip2の場合

  • 圧縮
    • bzip2 -k -9 -c [source file path] > [zipped file path]
    • [zipped file path]のファイル名はそのまま出てくるので末尾に.bz2を忘れないように
      • -c> [zipped file path]がないと[source file path]のファイル名に勝手に.bz2がつくけど
    • オプションはそれぞれ,
      • -kで元ファイルを削除しない
      • -9で圧縮強度を最大化
      • -cstdoutに指定
    • ディレクトリを再帰的に辿る実装はなく,tar(後述)でまとめてからbzip2するのが正攻法
      • 引数に複数のファイルを並べると全て個別に.bz2化されるのが特徴
        • bzip2 -k -9 -c [source file path1] > [zipped file path1] [source file path num] > [zipped file path num]すれば良い
        • [source file path1] [source file path num] > [zipped file path]すると,.tar同士だけなら最後に展開すればなんとかなるが,.tar以外のファイルが混ざってると詰む
        • gzipとほぼ同じ理由か
    • 名前がダブったファイルの強制上書き許可は-f,なければ聞かれるまでもなく上書きされない
  • 解凍
    • bunzip2 -k -c [source file path] > [unzipped file path]
    • オプションはそれぞれ,
      • -kで元ファイルを削除しない
      • -cstdoutに指定
    • どうせファイルしか出てこないのでこれで覚えた方が良い
    • 名前がダブったファイルの強制上書き許可は-f,なければ聞かれるまでもなく上書きされない
    • bzip2を入れると勝手にbunzip2もついてくる
      • というよりbzip2を使う上で-zで圧縮するか-dで解凍するかの違いしかない

調べた限りではディレクトリ統合機能もパスワード暗号化機能もなさそう,やはり別ライブラリ(後述)を使うべきだろう.

lrzipの場合

そういやunar使わないんすか?

なんか負けた気分になるし手動で詰めた方が個人的にはスッキリするので…

FireVault

Mac起動ディスクだけならコレで良いと思う
Windowsのbitlockerよりも優秀だと感じている

設定したら復旧キーを忘れないように必ずメモしておこう,iCloud連携は正直やりたくないなって…

外部のボリュームを暗号化

外付けストレージならコレで良いと思う
ディスクユーティリティでexFATフォーマットされたドライブの中にも問答無用で設定できる

veracrypt

Windows向けという印象があるがmacOSでも普通に使える
OSS開発でありながらも,これを使えば暗号化はかなり万全と考えて良さそうか…?

adguardのユーザールールで5ちゃんねる用の非表示フィルタを作成

ワッチョイ,IPアドレスコテハン,名前

!wacchoi,ip,name
5ch.net##div:matches-attr("/class/"="/post|post highlightpost|post_hover vis own[0-9]+/") > div > span:matches-attr("/class/"="/name/"):contains(/ここに正規表現で書き込み/):upward(2):remove()

ワッチョイ決め打ち

正規表現,こんな感じで前後に分けて判定

abcd-....|....-efgh

ID

!userID
5ch.net##div:matches-attr("/class/"="/post|post highlightpost|post_hover vis own[0-9]+/") > div > span:matches-attr("/class/"="/uid/"):contains(/ここに正規表現で書き込み/):upward(2):remove()

末尾の決め打ち

正規表現,例として末尾d・末尾a・末尾r・末尾Mを弾く

\b.+[darM]\b

正規表現の仕様

developer.mozilla.org

概ねこれに一致しているかと思います(オプション指定はしなくても良さそう)

鯖毎or板毎に使い分け

ドメインを細かく指定すれば何とかなると思います

iCloud+の「メールを非公開(偽装アドレス)」で実験してみた

以下,偽装アドレスと呼称します

コレに興味がある方は基本的な使い方は大丈夫だと思われまスゥゥゥ…

https://support.apple.com/ja-jp/HT210425

簡単な説明

macOS Monterey(Public Beta版でそこそこ安定しているやつがあるから登録して落としてこよう
設定 → Apple ID → メールを非公開(オプション)に移動
左下の『➕』で自動生成,ラベルとメモを忘れずに書き加える
アドレスをコピペして実際に使う,以上

用語リスト

f:id:Soluna_Eureka:20211008202622p:plain
説明

偽装アドレス

さっきの手順で手に入れたアドレス
xxx@icloud.comとおく

自分アドレス

自分の本来のiCloudのアドレス
XXX@icloud.comとおく

相手アドレス

やり取りをしたい相手のアドレス
YYY@example.ne.jpとでもおくか

串用アドレス

メールの送信先に実際に置かれるアドレス
4つ偽装アドレスで5つくらい取ってから実験してみた
ユーザーの視点では「プロキシが持つアドレス」に見えるのでそう呼びたい…
YYY@example.ne.jpを用いると
YYY_at_example_ne_jp_[aa][nnnnnnnnnn][bb]_[mm][cccccc]@icloud.comみたいに導出される

[aa][bb][cccccc]は同じ偽装アドレスを用いて通信した相手アドレスであれば共通する
[nnnnnnnnnn](10桁)・[mm](2桁)は相手アドレスによって完全にランダムに振られる
なんでこの桁数や構成になったのかは不明…

実際に制限ルールを探ってみた

ルール1.文通するには最初に相手から送られてくる必要がある

相手アドレスから偽装アドレスにメールが飛んでくると,相手アドレスに対応する串用アドレスが発行され,串用アドレスから自分アドレスにメールが飛んでくる(Applesmtpサーバから転送されてくる)
偽装アドレスを用いて相手アドレスにメールを返信するにはその真逆の動作をしなければならず,自分アドレスから串用アドレスに飛ばすことで偽装アドレスから相手アドレスにメールが飛んでいく

すなわち送信アドレスに対応した串側アドレスをユーザーが叩くことで偽装アドレスから相手アドレスへのアクセスが行えるが,串側アドレスの発行は外側から受けたメールを処理するApplesmtpサーバでしかできない以上,こちらから先制して送りつけることは不可能,対応できる串側アドレスがなければユーザーはなにもできない
濫用防止のためと言われれば当然はであるが

ルール2.串側アドレスの乱数文字列がユーザ認証の役目を果たす

試しに1文字だけ弄って送信したらエラーを吐かれたし,偽装アドレスによって串側アドレスのフォーマットの約半分が定まっていることから,自分アドレスと偽装アドレスの権限照合・串側アドレスと相手アドレスの宛先照合が行われているとみて良さそう
予想するに,他のiCloudユーザが他人の串側アドレスを宛先にして送信してもダメだったりするのではないだろうか…?(誰かやってくれ)

ちなみに宛先が間違っていると代替のsmtpサーバに送信され,Undelivered Mail Returned to Serverされる

ルール3.串側アドレスを宛先に含む場合はアドレス1つ分しか対応しない

宛先に2つ以上の串側アドレスを登録して1度に送ろうとすると,

  • Web版iCloudメールでは警告メッセージを吐かれる
    • 「機能的に無理です!」
  • Macデフォルトのメールソフトでは約5分間隔で宛先1つずつに再送される
    • 「too many recipients!」
      • 代替のstmpサーバの指定を要求されるが,何を選んでも結局は再送になる.
    • なお1つ目の宛先にはすぐに送れる
    • 個別送信扱いになるので他の宛先は見えなくなる

または串側アドレスと通常のアドレスを登録して1度に送ろうとすると,

  • Web版iCloudメールでは警告メッセージを吐かれる
    • 「Undelivered Mail Returned to Server」
    • どっちも届かない
  • Macデフォルトのメールソフトでは約5分間隔で宛先1つずつに再送される
    • 代替のstmpサーバの指定を要求される
      • 選ぶとバラバラに(串側アドレスはApplesmtpサーバで,それ以外は代替smtpサーバで)再送される
      • 相手アドレスから見れば宛先の連名は見れないが,通常のアドレスからは丸見え
    • 1つ目の串側アドレスや通常のアドレスの宛先にはすぐに送れる

つまりメールソフトにおける動作としては,

  1. まずはiCloudサーバにまるごと送る
  2. 串側アドレスがある場合は1つしか受け付けられないと返信する
  3. 代替smtpサーバがない場合は全て破棄,ある場合は
    1. 串側アドレス1つをiCloudサーバに送り
    2. その他全てを代替smtpサーバに送る
    3. 串側アドレスが残れば返送されるので
      1. 約5分間隔で1つずつ再送する
  4. エラーが無くなれば完了

という感じ…っぽい

ルール4.串側アドレスはApple側で保持される

以上のルールを満たすには,偽装アドレスのみならず串側アドレスもAppleが管理していると見るべきだろう
というかリンク切れが起きたりしたらこっちも大変なので,そこら辺は頑張ってセーブし続けてほしい月額160円でやっていいサービスか?これが…

感想

今後また仕様が何かしら変わるかも知れません
が,とりあえず串側アドレスには偽装アドレスと一意に結び付けられる成分があることは頭に入れておくべきだと思いました
万が一の場合にはそれが流出することでやり取りが特定される可能性も考えられます
あとはどれだけ串側アドレスが保持されるのかやってみなくちゃわかりません,1年間の未使用でリセットとかありそうですし…

匿名化ヨシ!ご安全に!

全てのきっかけ

https://twitter.com/Soluna_Eureka/status/1446095138808221707?s=20