xargs -P と export -f を使ってシェルスクリプトで並列処理を実現する

xargs-Pオプションを使うとパイプで渡された値を任意の並列数で並列処理することができます。以下では、普通に実行すると10秒かかる処理が、5並列で並列処理することによって2秒で実行することができます。

f.sh:

#!/bin/bash
echo $*;
sleep 1;
$ seq 10 | xargs -I % -P 5 ./f.sh %
1
2
3
4
5
6
7
8
9
10

そのため、シェルスクリプトで大量の項目をforを使って処理している部分をxargsに置き換えることによって高速化することができます。

しかし、xargsでは関数を使用できないので、このままでは並列処理したい部分を都度別のシェルスクリプトに分割しなくてはなりません。

$ function f { echo $*; sleep 1; }
$ seq 10 | xargs -I % -P 5 f %
xargs: f: No such file or directory
$ echo 'echo $*; sleep 1;' > f.sh
$ seq 10 | xargs -I % -P 5 sh ./f.sh %
1
2
3
4
5
6
7
8
9
10

ですが、bashにはexport -fという関数をエクスポートできる機能があり、これとbash -cを組み合わせることによって、いちいち並列処理をしたい部分をシェルスクリプトに書き出さなくてもよくなります。

$ function f { echo $*; sleep 1; }
$ export -f f
$ seq 10 | xargs -I % -P 5 bash -c 'f %'
1
2
3
4
5
6
7
8
9
10

しかし、xargs-Iオプションを使うとデリミタが改行になってしまうので、以下のような場合は意図した動作になりません。

$ function f { echo $*; sleep 1; }
$ export -f f
$ echo '1 2 3 4 5 6 7 8 9 10' | xargs -I % -P 5 bash -c 'f %'
1 2 3 4 5 6 7 8 9 10

この場合は、xargs-dオプションを使うことでデリミタを指定できるので、上記の場合はスペースを指定すれば意図した動作になります。

$ function f { echo $*; sleep 1; }
$ export -f f
$ echo '1 2 3 4 5 6 7 8 9 10' | xargs -d ' ' -I % -P 5 bash -c 'f %'
1
2
3
4
5
6
7
8
9
10

しかし、このオプションはGNU版のxargsにしかないので、Mac(BSD系)だと動きません。なので、brew install findutilsしてgxargsを入れて、alias xargs=gxargsとする必要があります。またaliasインタラクティブシェルでないと動作しないので、シェルスクリプトの場合は、さらにshopt -s expand_aliasesとする必要があります。

a.sh:

#!/bin/bash

# for Mac(BSD系)
shopt -s expand_aliases
alias xargs=gxargs

function f {
  echo $*;
  sleep 1;
}
export -f f
echo '1 2 3 4 5 6 7 8 9 10' | xargs -d ' ' -I % -P 5 bash -c 'f %'
$ ./a.sh
1
2
3
4
5
6
7
8
9
10

PowerShell で sudo 的なことをしたい

Windows Terminal 1.13 から管理者権限に昇格できるプロファイルを定義できるとはいえ、いちいち管理者権限が付与されたウィンドウを立ち上げたくはない。POSIXのように sudo <command> で同一ウィンドウ(タブ)で実行したい。

これには gsudo というそのまんまなことができるものがあるので導入する。winget を導入しているのであれば、以下でインストールできる。

winget install gerardog.gsudo

導入後は以下のように gsudo <command> で管理者権限で任意のコマンドを実行できる。

gsudo ls

しかし、$profile は読んでくれないようなので、$profile に定義されている関数などを実行したい場合は、以下のようにする必要がある。

gsudo "pwsh.exe -Login -Command { <command> }"

毎回これを入力するのは面倒くさいので、$profile に以下を定義しておく。

function Invoke-As-Admin() {
    if ($args.count -eq 0) {
        gsudo
        return
    }
    $cmd = $args -join ' '
    gsudo "pwsh.exe -Login -Command { $cmd }"
}

Set-Alias -Name: "sudo" -Value: "Invoke-As-Admin"

これでPOSIXライクに管理者権限で任意のコマンドを実行できるようになる。

sudo ls

なお、sudo (gsudo) とすると、POSIX でいう sudo su - のような挙動となる。

環境

  • OS: Windows 11 Pro
    • バージョン: 21H2
    • ビルド番号: 22000.526
  • PowerShell: 7.2.1

参考

WSL2でcodeコマンドやexplorer.exeがハングする

WSL2上でcodeコマンドを使ってVSCodeを開こうとしたらハングしてしまった。WSL2の再起動や高速スタートアップを無効にしてもWindowsの再起動をしても治らないし、codeコマンドだけではなくexplorer.exeもハングしてしまう。

調査していると、Windows上のパス(/mnt/c/)だと問題なく動作することがわかった。そこで、codeexplorer.exe も内部で利用しているであろう wslpath がおかしいのではないかと思い、wslpath -m を実行してみると以下のようになった。

$ wslpath -m /
//wsl.localhost/Ubuntu/
$ wslpath -m /mnt/c
C:/

WSL2上のパス(/など)だと//wsl.localhost/Ubuntu/のようになっていてなにかおかしい。WSL2上のパスは//wsl$/<ディストリビューション名>のはずである。さらに調査していると、どうやら、Build 21354//wsl$/<ディストリビューション名> から //wsl.localhost/<ディストリビューション名>/ に変更になり、今後はこちらが使われるらしい(現状は//wsl$/<ディストリビューション名>でも開ける)。

しかし、//wsl.localhost/Ubuntu でアクセスしてもハングしてしまいアクセスできない。このことから不具合の原因は wslpath でもなく //wsl.localhost/Ubuntu でアクセスできないことによるらしいことがわかった。また、ほかのディストリビューションでは問題なくアクセスできるのでWSL2自体に問題はなく、自分が使用している Ubuntu ディストリビューションが壊れているらしいことがわかった。

具体的にどこがおかしいか調査をしたかったが、いまいちわからなかったので Ubuntuクリーンインストールしたところ治った。

なお、環境は以下のとおりである。

PowerShell で .bashrc 的なことがしたい

PowerShell にも .bashrc 的なファイルがあって、それを編集すればシェルを起動するときに任意の設定を読み込める。

$profile 変数にプロファイルファイルのパスが入っているので、これを編集する。なお、存在しない場合があるので、その場合は $profile のパスにファイルを作成する。

# 存在確認
Test-Path $profile

# なければ作る
New-Item -path $profile -type file -force

# 編集
code $profile

参考

WSL2 の localhost forwarding を代替する PowerShell Script を作った

以前、自分の環境では localhost forwarding で頻繁にハングすることがあるので、netsh interface portproxy add コマンドを使って手動で port forwarding するようにした。

mrk21.hatenablog.com

別に都度このコマンドを叩いてもいいのだが、WSL2 が listen しているポートを自動で port forwarding したいと思い、PowerShell Script の勉強がてら書いてみた。

使い方は、管理者権限で起動したターミナルで、 wsl-forwarding.ps1 を実行する。このスクリプト実行中は、WSL2側で 0.0.0.0 に listen すると、自動的にWindows側に port forwarding する。

f:id:mrk21:20220205143845g:plain

また、Windows側の port forwarding するIPアドレス0.0.0.0 としているので、外部からWSL2にアクセスできる。 WSL2 の localhost forwarding と同様にループバックアドレスからのみWSL2にアクセスできるようにしたい場合は $HostIP$HostIP = [IPAddress]"127.0.0.1" とする。

Terser で minify するときに ascii_only オプションを true にしないと Unicode Escape Sequence が展開されてしまう

Webpack + Babel + core-js な環境でビルドしたJavaScriptPerlサーバーで配信するプロジェクトで、あるときPerlサーバーで配信しようとしたときに Internal Server Error がでてしまった。

ログを確認すると、Wide character in subroutine entry at /path/to/Compress/Zlib.pm というエラーがでていた。どうやら、配信しようとしていたJavaScriptUnicodeが含まれていたため、Perlの内部文字列のutf8フラグが立ってしまい、そのままgzip圧縮をする Compress::Zlib モジュールに渡ってしまったのが原因のようだ。そのため、$content = encode("UTF-8", $content); としてUTF-8バイト文字列に変換すればよいのだが、そもそも元のJavaScriptにはUnicodeは含まれていない。しかし、ビルドされたJavaScriptを確認すると、たしかにUnicodeが含まれている。

いろいろ調査したところ、どうやら Terser は ascii_only オプションが false の時(デフォルト)は、"\u2028" といった Unicode Escape Sequence を展開してしまうことがあるらしいということがわかった。そして、core-js によって挿入された whitespaces.js というモジュールには Unicode Escape Sequence が含まれており、Terser がこれを展開してしまったため、Perlで読み込まれたときにutf8フラグが立ってしまい問題が引き起こされたようだ。

そのため、webpack.config.js で以下のように Terser の ascii_only オプションを true とすることで問題を解決した。

const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  ...
  optimization: {
    minimizer: [new TerserPlugin({
      terserOptions: {
        format: {
          ascii_only: true
        }
      }
    })]
  }
}

今回のような状況下でなくても、Unicode Escape Sequence が勝手に展開されるのはバグの温床になるので、ascii_only オプションは常に true としておいたほうがよさそうである。

なお、このときの環境は以下の通り。

  • Node.js: 16.13.2
  • Webpack: 5.51.1
  • Babel: 7.15.0
  • core-js: 3.16.3
  • terser: 5.7.1
  • terser-webpack-plugin: 5.1.4

参考

WSL2 の localhostForwarding 機能がうまくうごかない

WSL2 の localhostForwarding 機能を使うと、WSL2側で listen したポートを自動的にWindows側で port forwarding してくれるので、Windows側からは localhost でWSL2側で listen しているポートにアクセスすることができる。

しかし、自分の環境ではアクセスはできるが、頻繁にハングすることがあり困っていた。

そのため、WSL2の localhostForwarding 機能を無効にして、かわりに netsh interface portproxy add v4tov4 コマンドを使って手動で port forwarding することにした。

まず、WSL2側のIPアドレス(WSL2が使用しているHyper-V仮想スイッチに接続しているアダプタのIPアドレス)を調べる。これは通常はeth0であるので、ip route |grep 'eth0 proto'|cut -d ' ' -f9 で取得することができる。ここで、Windows側からは bash.exe 経由で実行できるので、bash -c "ip route |grep 'eth0 proto'|cut -d ' ' -f9" で取得できる。

次に netsh interface portproxy add v4tov4 listenaddress=<host ip> listenport=<port> connectaddress=<wsl ip> connectport=<port> コマンドを使うことにより port forwarding することができるので、さきほど取得した WSL2 の IP を使って port forwarding する。 PowerShell スクリプトにまとめると以下の通りとなる。

wsl-proxy.ps1:

$WSL2_IPV4=bash -c "ip route |grep 'eth0 proto'|cut -d ' ' -f9"
$HOST_IPV4="0.0.0.0"
$PORT=$Args[0]
netsh interface portproxy delete v4tov4 listenaddress=$HOST_IPV4 listenport=$PORT
netsh interface portproxy add v4tov4 listenaddress=$HOST_IPV4 listenport=$PORT connectaddress=$WSL2_IPV4 connectport=$PORT
netsh interface portproxy show v4tov4

このスクリプトを管理者権限で起動したターミナルから実行する。

> wsl-proxy.ps1 3000

これで、ブラウザ等から localhost:3000 にアクセスすると、WSL2の 3000 ポートに転送されてアクセスできるようになる。

なお、このときの環境は以下のとおりである。

参考