Perlで「 `brew install coreutils` しろ」に抗う

Q: これMacで動かないんだけど？
A: `brew install coreutils`しろ

みたいな身も蓋もない[issue](https://github.com/search?o=desc&q=%22brew+install+coreutils%22&s=created&type=Issues)をよく見かける。

PerlならMacにもLinuxにも普段入っているので、これをPerlでなんとかしてみたい。

base64
--

https://github.com/DataDog/datadog-serverless-functions/pull/68

`base64` の-wオプションで末尾の改行を無くしたい。

```bash
#!/bin/bash

if [ $(uname) == "Darwin" ]; then
    datecmd=gdate
    base64cmd=gbase64
else
    datecmd=date
    base64cmd=base64
fi
ts=$(($(${datecmd} +%s%N)/100000))

# [...]

tmp_CWLogB64=$(echo ${tmp_CWLogRaw/__timestamp__/$ts} | gzip | ${base64cmd} -w0)
```

`Time::HiRes`で1秒未満の現在時刻を、base64には`MIME::Base64`を使う。

```sh
ts=$(perl -MTime::HiRes=time -e 'printf "%.4f\n", time' | tr -d '.')
tmp_CWLogB64=$(echo ${tmp_CWLogRaw/__timestamp__/$ts} | gzip \
    | perl -MMIME::Base64 -0777 -ne 'print encode_base64($_, "");')
```

cut
---

https://github.com/bellecp/fast-p/issues/7

BSDの `cut` に-zオプションが無い。

```bash
p () {
    local open
    open=open   # on OSX, "open" opens a pdf in preview
    ag -U -g ".pdf$" \
    | fast-p \
    | fzf --read0 --reverse -e -d $'\t'  \
        --preview-window down:80% --preview '
            v=$(echo {q} | gtr " " "|"); 
            echo -e {1}"\n"{2} | ggrep -E "^|$v" -i --color=always;
        ' \
    | gcut -z -f 1 -d $'\t' | gtr -d '\n' | gxargs -r --null $open > /dev/null 2> /dev/null
}
```

gプレフィックスのついているものの置き換えをそれぞれ検討する:

```bash
p () {
    local open
    open=open   # on OSX, "open" opens a pdf in preview
    ag -U -g ".pdf$" \
    | fast-p \
    | fzf --read0 --reverse -e -d $'\t'  \
        --preview-window down:80% --preview '
            v=$(echo {q} | tr " " "|"); 
            printf "%s\n%s\n" "{1}" "{2}" | egrep "^|$v" -i --color=always;
        ' \
    | perl -0 -l0 -F'\t' -nae 'print $F[0]' | tr -d '\n' | xargs -0 $open > /dev/null 2> /dev/null
}
```

うーんこれはいまいち分かりづらいかな。

head
--

`head -n -1`ができない。

```sh
 command -v ghead > /dev/null && HEAD="ghead" || HEAD="head"
...
        SNAPSTOREMOVE="`listsnaps \"${TEMPLATE}\" \"${DATASET}\" | $HEAD -n -1`"
```

最後の行だけスキップ:

```sh
        SNAPSTOREMOVE="`listsnaps \"${TEMPLATE}\" \"${DATASET}\" | perl -nle 'print if !eof'`"
```

hostname
---

https://github.com/crr0004/deepracer/issues/11

hostnameの-iオプション。これはもとのコードにも問題ありそう。

```sh
export S3_ENDPOINT_URL=http://$(hostname -i):9000
```

```sh
hostname_i=$(perl -MSys::Hostname -MSocket -le '
    $ip = gethostbyname(hostname); print inet_ntoa($ip) if $ip;')
export S3_ENDPOINT_URL="http://${hostname_i}:9000"
```

readlink
---

https://github.com/area9innovation/flow9/pull/217

readlinkの-f/-eオプション

```sh
if [ `uname` == Darwin ]; then
    READLINK=greadlink
else
    READLINK=readlink
fi

SCRIPT_FN=`$READLINK -f "$0"`
```

Cwd::realpathを使ってcoreutilsに依存しない可搬性のあるコードにする:

```sh
SCRIPT_FN=$(perl -MCwd=realpath -le 'print realpath shift' "$0")
```

これはすっきり。


realpath
---

https://github.com/nvm-sh/nvm/pull/2045

realpathがPOSIXに無い。上と違って厄介なのは-m/--canonicalize-missingオプション。

```sh
node_path=$(realpath --canonicalize-missing "${2-}")
```

realpathが出てくるところまでトラバースして残りをディレクトリ扱いに:

```sh
node_path=$(perl -MFile::Spec::Functions=catdir,splitdir -MCwd=realpath -le '
    @p = splitdir shift;
    push @s, pop @p while !realpath catdir @p;
    print catdir realpath(catdir @p), @s;
' "${2-}")
```

sed
--

https://github.com/RobertsLab/resources/issues/694

```sh
  gsed "1iID\t${gene}" ${file}ID_CpG > ${file}ID_CpG_labelled
```

ファイルにヘッダをつけようとしたもの。BSDの`sed`に`i`コマンドがないので失敗してるパターン。
エスケープが甘いのでそれも考慮する。

```sh
  perl -plse 'print "ID\t$gene" if $. == 1;' -- -gene="$gene" "${file}ID_CpG" > "${file}ID_CpG_labelled"
```

でもこんなことをするぐらいなら普通にcatしたほうがいいと思うけど。

```sh
( printf "ID\t%s\n" "$gene"; cat "${file}ID_CpG"; ) > "${file}ID_CpG_labelled"
```

sha512sum
--

https://github.com/openzipkin-contrib/apache-release-verification/issues/5

```py
@check("SHA512 checksum is correct")
    def check_sha512(state: State) -> R:
    return _check_sh(f"sha512sum -c {state.sha512_path}", workdir=state.release_dir)
```

COREモジュールの `Digest::SHA` が `shasum` コマンドを同梱しているので単純に置き換える:

https://perldoc.perl.org/shasum.html

```py
@check("SHA512 checksum is correct")
    def check_sha512(state: State) -> R:
    return _check_sh(f"shasum -c {state.sha512_path}", workdir=state.release_dir)
```

shuf
--

https://github.com/ngerakines/commitment/issues/192

ファイルからランダムに1行だけピックしたい。

```sh
shuf -n 1 commit_messages.txt
```

これは[perlfaq5](https://perldoc.perl.org/perlfaq5.html#How-do-I-select-a-random-line-from-a-file%3f)にある通りですね。

```sh
perl -e 'srand; rand($.) < 1 && ($line = $_) while <>; print $line;' commit_messages.txt
```

1行だけじゃなくて複数行(N=5)とかになると若干面倒:

```sh
perl -e '
    $N = 5;
    srand;
    while (<>) {
        if ($. <= $N) { push @l, $_; next; }
        $r = rand $.; if ($r < $N) { $l[$r] = $_; }
    }
    print @l;
' commit_messages.txt
```

stat
--

https://github.com/trezor/trezor-firmware/issues/9

`stat` の-cオプションが無いとのこと。[lsで代替するパッチ](https://github.com/trezor/trezor-firmware/commit/9f8ebcf1833e2f56c86df854c915385b0e9f43af)が入ってるけど、lsも環境異存だし。

```sh
docker run -it -v $(pwd):/src:z --user="$(stat -c "%u:%g" .)" "$IMAGE" \
  /src/script/fullbuild "$BOOTLOADER_COMMIT" "$FIRMWARE_COMMIT"
```

これを直接perlでやるとこんな感じ

```sh
docker run -it -v "$(pwd):/src:z" \
  --user="$(perl -le 'print join ":", (stat(shift))[4..5]' .)" "$IMAGE" \
  /src/script/fullbuild "$BOOTLOADER_COMMIT" "$FIRMWARE_COMMIT"
```

もしくは `File::stat`:

```sh
UID_GID=$(perl -MFile::stat=:FIELDS -le \
  'stat shift; print join "$st_uid:$st_gid";' .)

docker run -it -v "$(pwd):/src:z" --user="$UID_GID" "$IMAGE" \
  /src/script/fullbuild "$BOOTLOADER_COMMIT" "$FIRMWARE_COMMIT"
```

timeout
--

https://github.com/OCamlPro/techelson/issues/9

```sh
timeout 2 $fmt_running_cmd &> /dev/null
```

ちょうどいいのが[SOにあった](https://stackoverflow.com/a/35512328/482519)

```sh
perl -e 'alarm shift; exec @ARGV' 2 $fmt_running_cmd &> /dev/null
```

他にもありそう。