それマグで!

知識はカップより、マグでゆっくり頂きます。 takuya_1stのブログ

習慣に早くから配慮した者は、 おそらく人生の実りも大きい。

jpeg と png だとどれくらいサイズが変わるのか

jpegpng だとどれくらいサイズが変わるのか

単純に変換だけしたとき。

takuya@DESKTOP-2ALDRO3:/mnt/c/Users/takuya/Desktop$ convert sample.jpg  -quality 100 out.png
takuya@DESKTOP-2ALDRO3:/mnt/c/Users/takuya/Desktop$ convert sample.jpg  -quality 100 out.jpg
takuya@DESKTOP-2ALDRO3:/mnt/c/Users/takuya/Desktop$ ll out*
-rwxrwxrwx 1 takuya takuya 1.2M Aug 21 00:47 out.jpg
-rwxrwxrwx 1 takuya takuya 2.1M Aug 21 00:47 out.png

png は圧倒的に不利なので、「パレット化」する。

convert sample.jpg -strip -define png:compression-level=9 -define png:compression-filter=0 -define png:compression-strategy=0  -type Palette out.png

パレット化するとだいぶ無駄が減る。

ll out.*
-rwxrwxrwx 1 takuya takuya 550K Aug 21 00:49 out.jpg
-rwxrwxrwx 1 takuya takuya 886K Aug 21 01:05 out.png

#!/usr/bin/env ruby

def gen_cmd (level,filter,strategy)
  "convert
  sample.jpg
  -strip -define png:compression-level=#{level}
  -define png:compression-filter=#{filter}
  -define png:compression-strategy=#{strategy}
  -type Palette out.#{level}.#{filter}.#{strategy}.png".gsub(/\n/,'')
  end



(0..9).each{|level|
  (1..5).each{|filter|
    (0..4).each{|strategy|
     cmd = gen_cmd level, filter, strategy
     p cmd
     `#{cmd}`
    }
  }

}

全通り試してみよう

圧縮フィルタを変えていく

同じパレット化でも、これくらいに差が出てくるわけです。

ll out.9.5.0.png out.0.1.0.png
-rwxrwxrwx 1 takuya takuya 2.9M Aug 21 01:33 out.0.1.0.png
-rwxrwxrwx 1 takuya takuya 826K Aug 21 01:38 out.9.5.0.png
2,964,981  out.0.1.0.png
2,964,981  out.0.1.1.png
2,964,981  out.0.1.2.png
2,964,981  out.0.1.3.png
2,964,981  out.0.1.4.png
2,964,981  out.0.2.0.png
2,964,981  out.0.2.1.png
2,964,981  out.0.2.2.png
2,964,981  out.0.2.3.png
2,964,981  out.0.2.4.png
2,964,981  out.0.3.0.png
2,964,981  out.0.3.1.png
2,964,981  out.0.3.2.png
2,964,981  out.0.3.3.png
2,964,981  out.0.3.4.png
2,964,981  out.0.4.0.png
2,964,981  out.0.4.1.png
2,964,981  out.0.4.2.png
2,964,981  out.0.4.3.png
2,964,981  out.0.4.4.png
2,964,981  out.0.5.0.png
2,964,981  out.0.5.1.png
2,964,981  out.0.5.2.png
2,964,981  out.0.5.3.png
2,964,981  out.0.5.4.png
1,328,449  out.1.1.2.png
1,328,449  out.1.2.2.png
1,328,449  out.1.3.2.png
1,328,449  out.1.4.2.png
1,328,449  out.1.5.2.png
1,328,449  out.2.1.2.png
1,328,449  out.2.2.2.png
1,328,449  out.2.3.2.png
1,328,449  out.2.4.2.png
1,328,449  out.2.5.2.png
1,328,449  out.3.1.2.png
1,328,449  out.3.2.2.png
1,328,449  out.3.3.2.png
1,328,449  out.3.4.2.png
1,328,449  out.3.5.2.png
1,328,449  out.4.1.2.png
1,328,449  out.4.2.2.png
1,328,449  out.4.3.2.png
1,328,449  out.4.4.2.png
1,328,449  out.4.5.2.png
1,328,449  out.5.1.2.png
1,328,449  out.5.2.2.png
1,328,449  out.5.3.2.png
1,328,449  out.5.4.2.png
1,328,449  out.5.5.2.png
1,328,449  out.6.1.2.png
1,328,449  out.6.2.2.png
1,328,449  out.6.3.2.png
1,328,449  out.6.4.2.png
1,328,449  out.6.5.2.png
1,328,449  out.7.1.2.png
1,328,449  out.7.2.2.png
1,328,449  out.7.3.2.png
1,328,449  out.7.4.2.png
1,328,449  out.7.5.2.png
1,328,449  out.8.1.2.png
1,328,449  out.8.2.2.png
1,328,449  out.8.3.2.png
1,328,449  out.8.4.2.png
1,328,449  out.8.5.2.png
1,328,449  out.9.1.2.png
1,328,449  out.9.2.2.png
1,328,449  out.9.3.2.png
1,328,449  out.9.4.2.png
1,328,449  out.9.5.2.png
1,128,468  out.1.1.4.png
1,128,468  out.1.2.4.png
1,128,468  out.1.3.4.png
1,128,468  out.1.4.4.png
1,128,468  out.1.5.4.png
1,088,706  out.2.1.4.png
1,088,706  out.2.2.4.png
1,088,706  out.2.3.4.png
1,088,706  out.2.4.4.png
1,088,706  out.2.5.4.png
1,049,337  out.3.1.4.png
1,049,337  out.3.2.4.png
1,049,337  out.3.3.4.png
1,049,337  out.3.4.4.png
1,049,337  out.3.5.4.png
1,016,731  out.4.1.4.png
1,016,731  out.4.2.4.png
1,016,731  out.4.3.4.png
1,016,731  out.4.4.4.png
1,016,731  out.4.5.4.png
1,012,007  out.1.1.3.png
1,012,007  out.1.2.3.png
1,012,007  out.1.3.3.png
1,012,007  out.1.4.3.png
1,012,007  out.1.5.3.png
1,012,007  out.2.1.3.png
1,012,007  out.2.2.3.png
1,012,007  out.2.3.3.png
1,012,007  out.2.4.3.png
1,012,007  out.2.5.3.png
1,012,007  out.3.1.3.png
1,012,007  out.3.2.3.png
1,012,007  out.3.3.3.png
1,012,007  out.3.4.3.png
1,012,007  out.3.5.3.png
1,012,007  out.4.1.3.png
1,012,007  out.4.2.3.png
1,012,007  out.4.3.3.png
1,012,007  out.4.4.3.png
1,012,007  out.4.5.3.png
1,012,007  out.5.1.3.png
1,012,007  out.5.2.3.png
1,012,007  out.5.3.3.png
1,012,007  out.5.4.3.png
1,012,007  out.5.5.3.png
1,012,007  out.6.1.3.png
1,012,007  out.6.2.3.png
1,012,007  out.6.3.3.png
1,012,007  out.6.4.3.png
1,012,007  out.6.5.3.png
1,012,007  out.7.1.3.png
1,012,007  out.7.2.3.png
1,012,007  out.7.3.3.png
1,012,007  out.7.4.3.png
1,012,007  out.7.5.3.png
1,012,007  out.8.1.3.png
1,012,007  out.8.2.3.png
1,012,007  out.8.3.3.png
1,012,007  out.8.4.3.png
1,012,007  out.8.5.3.png
1,012,007  out.9.1.3.png
1,012,007  out.9.2.3.png
1,012,007  out.9.3.3.png
1,012,007  out.9.4.3.png
1,012,007  out.9.5.3.png
988,969    out.5.1.4.png
988,969    out.5.2.4.png
988,969    out.5.3.4.png
988,969    out.5.4.4.png
988,969    out.5.5.4.png
969,534    out.6.1.4.png
969,534    out.6.2.4.png
969,534    out.6.3.4.png
969,534    out.6.4.4.png
969,534    out.6.5.4.png
964,417    out.7.1.4.png
964,417    out.7.2.4.png
964,417    out.7.3.4.png
964,417    out.7.4.4.png
964,417    out.7.5.4.png
958,431    out.8.1.4.png
958,431    out.8.2.4.png
958,431    out.8.3.4.png
958,431    out.8.4.4.png
958,431    out.8.5.4.png
956,071    out.9.1.4.png
956,071    out.9.2.4.png
956,071    out.9.3.4.png
956,071    out.9.4.4.png
956,071    out.9.5.4.png
953,058    out.1.1.0.png
953,058    out.1.1.1.png
953,058    out.1.2.0.png
953,058    out.1.2.1.png
953,058    out.1.3.0.png
953,058    out.1.3.1.png
953,058    out.1.4.0.png
953,058    out.1.4.1.png
953,058    out.1.5.0.png
953,058    out.1.5.1.png
947,009    out.4.1.1.png
947,009    out.4.2.1.png
947,009    out.4.3.1.png
947,009    out.4.4.1.png
947,009    out.4.5.1.png
930,469    out.2.1.0.png
930,469    out.2.1.1.png
930,469    out.2.2.0.png
930,469    out.2.2.1.png
930,469    out.2.3.0.png
930,469    out.2.3.1.png
930,469    out.2.4.0.png
930,469    out.2.4.1.png
930,469    out.2.5.0.png
930,469    out.2.5.1.png
928,669    out.5.1.1.png
928,669    out.5.2.1.png
928,669    out.5.3.1.png
928,669    out.5.4.1.png
928,669    out.5.5.1.png
909,453    out.6.1.1.png
909,453    out.6.2.1.png
909,453    out.6.3.1.png
909,453    out.6.4.1.png
909,453    out.6.5.1.png
905,455    out.7.1.1.png
905,455    out.7.2.1.png
905,455    out.7.3.1.png
905,455    out.7.4.1.png
905,455    out.7.5.1.png
904,646    out.3.1.0.png
904,646    out.3.1.1.png
904,646    out.3.2.0.png
904,646    out.3.2.1.png
904,646    out.3.3.0.png
904,646    out.3.3.1.png
904,646    out.3.4.0.png
904,646    out.3.4.1.png
904,646    out.3.5.0.png
904,646    out.3.5.1.png
897,668    out.8.1.1.png
897,668    out.8.2.1.png
897,668    out.8.3.1.png
897,668    out.8.4.1.png
897,668    out.8.5.1.png
894,594    out.9.1.1.png
894,594    out.9.2.1.png
894,594    out.9.3.1.png
894,594    out.9.4.1.png
894,594    out.9.5.1.png
887,389    out.4.1.0.png
887,389    out.4.2.0.png
887,389    out.4.3.0.png
887,389    out.4.4.0.png
887,389    out.4.5.0.png
873,315    out.5.1.0.png
873,315    out.5.2.0.png
873,315    out.5.3.0.png
873,315    out.5.4.0.png
873,315    out.5.5.0.png
858,641    out.6.1.0.png
858,641    out.6.2.0.png
858,641    out.6.3.0.png
858,641    out.6.4.0.png
858,641    out.6.5.0.png
854,629    out.7.1.0.png
854,629    out.7.2.0.png
854,629    out.7.3.0.png
854,629    out.7.4.0.png
854,629    out.7.5.0.png
848,080    out.8.1.0.png
848,080    out.8.2.0.png
848,080    out.8.3.0.png
848,080    out.8.4.0.png
848,080    out.8.5.0.png
845,454    out.9.1.0.png
845,454    out.9.2.0.png
845,454    out.9.3.0.png
845,454    out.9.4.0.png
845,454    out.9.5.0.png

github にpush してghcr.io に docker push する。

github にpush したらghcr.io にdocker push する。

概要

github に push したタイミングで、github actionsを起動して、docker ビルドをして、docker イメージを github コンテナ・レジストリにpush したい

手順

  1. github のレポジトリを用意する
  2. github で actionsから workflow 用の変数を用意する
  3. workflow の定義を用意する
  4. git push する
  5. 動作チェックする。
  6. docker push のチェック
  7. 公開・非公開の切替え
  8. 注意点

github のレポジトリを用意する

github のレポジトリを用意する。

Dockerfile が含まれていたら良い。

workflow 用の変数を用意する

workflow から github のコンテナレジストリにpushするために、ログイン(アクセス権)が必要。パスワードを書くわけにも行かないので、GitHubアクセストークンを用意して、レポジトリの変数に設定する。

プロジェクト(レポジトリ)→settings→secrets

f:id:takuya_1st:20220404025407p:plain

変数名と中身を設定

f:id:takuya_1st:20210927133955p:plain

設定例

名前: GH_ACCESS_TOKEN
中身:Githubから発行される個人用アクセストークン

f:id:takuya_1st:20220404030459p:plain

アクセストークンの発行は別記事を参照のこと。

workflow を設定する。

ファイルにワークフローを記述する

## git clone 
git clone https://github.com/takuya/$REPO
cd $REPO
mkdir -p .github/workflows/
touch .github/workflows/actions.yml

##
open $REPO/.github/workflows/actions.yml

レポジトリに .github/workflowsディレクトリを作ってyamlをその中に設置する。日本人は workflowsworkflowと書いてしまい複数形を忘れがちなので注意。

docker push 用のキーを書き込み

f:id:takuya_1st:20210927134032p:plain

Dockerfileの場所を指定。

Dockerfile が格納されているフォルダを指定する。フォルダを作らずDockerfileを直接設置したときは ./とカレントディレクトリを指定しておけばいい。

f:id:takuya_1st:20210927134049p:plain

actions.yml の設定例

name: Build and Publish Docker

on:
  push:
    branches:
      - master

jobs:
  build_and_push:
    runs-on: ubuntu-latest
    env:
      IMAGE_NAME: sample-docker-image
    steps:
      - name: checkout
        uses: actions/checkout@v2

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v1
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GH_ACCESS_TOKEN }}
      - name: Build and push
        uses: docker/build-push-action@v2
        with:
          context: ./docker-build
          push: true
          tags: |
            ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}:latest
            ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}:1.0.0

workflow のかんたんな解説。

ビルドとpushをする箇所

docker builddocker pushを行うのはここ。

- name: Build and push
    uses: docker/build-push-action@v2

これは。https://github.com/docker/build-push-action で公開されているスクリプトで、それを取り込んで実行している。

push 先とタグを tags: で指定している。

${{ github.repository_owner }}githubワークフローのcontextと呼ばれるプリセット変数である。

docker push の準備をする箇所

docker push にはログインが必要なので、ログインを行う。

- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
  registry: ghcr.io
  username: ${{ github.repository_owner }}
  password: ${{ secrets.GH_ACCESS_TOKEN }}

これは、 https://github.com/docker/login-actionで公開されているスクリプトで。uses: docker/login-action@v1 を使って利用を指定して、どこにログインするかとパスワードとユーザー名をyaml で与えている。

${{ secrets.GH_ACCESS_TOKEN }}が最初にレポジトリに設定したsecrets変数である。接頭辞をつけてsecrets.MY_NAME でアクセスする。

git push で動作させる

master レポジトリにpush すると actionが動き出す。

git push origin master

master で動くのは、masterへのpush で起動指定してあるから。

ワークフローのyml の冒頭部分(以下抜粋)で指定してある。

on:
  push:
    branches:
      - master

動作チェックする

レポジトリ→Actionsを選ぶと実行結果が見られる。

f:id:takuya_1st:20210927134130p:plain

docker push のチェック

docker build が無事に終わり、docker pushも無事に終わると、githubにパッケージがpushされている。

github で docker push された結果は、「パッケージ」と呼ばれる。

結果は, https://github.com/YOUR_NAME?tab=packages で見ることができる。

公開・非公開の切り替え

docker push されたイメージは、「プライベート」になっているので、「パブリック」にしてあげる。

公開非公開の切り替えは「パッケージ」の「設定」を使って設定する。

f:id:takuya_1st:20220404031519p:plain

https://github.com/USERNAME のページで確認できる。

注意点(上限)

workflowとghcr を使うときはパブリックにしておかないと容量と実行時間の上限がシビアなので、パブリックが無難だと思う。可能な限りパブリックにするといいと思う。

2021-09-26 現在で調べた限りですが。

github コンテナ・レジストリでは、デフォルトで500MBの「プライベート領域」がある。そして、push したら、デフォルトでプライベートになる。 しかし、「パブリック」なら無制限である。

そのため、push後はパブリックに切り替えて容量を節約している。

docker push で初期非公開になっています。デフォルト設定で公開する方法は見つからなかった。

docker buildがコケたとき

workflow のログを見ながらエラーを修正するとよいが、github の workflow で実行した場合、build用の仮想マシンを毎回新規作成するので、時間がかかる。

そのため、手元のローカルマシンで docker build をしてbuildが確認できてからgit push で起動したほうがいい。

docker push がコケたとき

docker push がコケたら、面倒だけど一回手作業で docker push しておく。

push 自体はとても簡単なので、一度手作業でpush を試してからgit push してももいいと思う。

push を手動でやる方法

TODO::別エントリ

docker で ghcr.io にログインする。

docker logoin ghcr.io

docker login ghcr の注意 :アクセストークンを使ってログインすること。

任意のイメージを github に push できる名前をつける。

たとえば、apline を ghcr.io にpush するなら

docker pull alpine
name=ghcr.io/takuya/my-first-docker-image
source_id=$(docker images alpine:latest --format="{{.ID}}")
docker tag $source_id $name
git push $name

alpine:latest に名前をつけて、その名前でpushする

名前は、次のようになる。

ghcr.io/YOUR_GITHUB_NAME/IMAGE_NAME

docker push できればあとは、github のサイトに従って使えばオッケー

ワークフローがコケる場合は、まずちゃんとghcr.io に docker push できることを確認しておく。

サンプルレポジトリ

docker build して push するサンプルのレポジトリ

動作サンプルようのレポジトリを残しておく。

https://github.com/takuya/github-actions-build-and-push-docker-sample

2022-04-04

画面デザインの変更に合わせて画像を若干更新

参考資料

共有メニューは使わなくないですか?消しました。

windows 10 の共有メニュー。

誰が使うんですか。これ。

f:id:takuya_1st:20210912102527p:plain

共有メニューの問題点

Shareメニューは日本語訳で「共有」になるのですが。共有メニューは、Win10からの共有と、WinNTからの「ネットワーク共有」とWin内部ローカルユーザー間の「ユーザー間共有」と、もはやわけがわかりません。共有メニュが何を指すのか、使い方を調べるのも一苦労です。そのため、誰も使いたがりませんし使ってないと思います。iPhoneユーザがWindowsを使い始めたときに癖で押して使うだろうが使いにくくて苦労していると思います。

シェアメニューを押して出てくる画面も、情報多すぎて全然わかりません。iphoneの人は、他アプリが出てくると思いきや「他アプリ」は「Open With(別で開く)」ですから。ユーザーが迷うだけです。ゴミ機能です。

消しましょう。

Computer\HKEY_CLASSES_ROOT\*\shellex\ContextMenuHandlers\ModernSharing

f:id:takuya_1st:20210912102929p:plain

アスタリスク( * ) がちょっとわかりにくいですが、*はすべてのファイル(フォルダ)という意味でレジストリツリーに存在します。

参考資料

https://shellfix.nirsoft.net/context_menu_list.html

f:id:takuya_1st:20210912103018p:plain

Shift押したときだけ右クリックメニューに表示するようにしたい

Shift押したときだけ右クリックメニューに表示するようにしたい

右クリックメニューに余計なものが多すぎる。

Windowsには、「Shiftを押したときだけ右クリックメニューが表示される」のフィルタ機能がある。

右クリックメニューに出現するアプリたち。使わないのに右クリックメニューを占拠して私達を迷わせ無駄時間を使わせる極悪非道な右クリックメニューたち。やつらを一斉解雇したい。

だけど、全部消しちゃうと流石に不便なので契約維持・解雇・2軍に分けたい。

Shift化→スタメン落ち

邪魔なメニューだけど、消すのも困るっていうスタメン落ち。2軍(登録抹消)ほどでもないけど、常に表示されるのは困る。

Extended Mode

それは、レジストリでは、Extended Mode 拡張モードにする。

ExtendedModeのエントリを作成しておく。キーが存在するだけでいい。これでShiftのみで表示されるコンテキストメニューが有効になる。

f:id:takuya_1st:20210912100434p:plain

shellmenuviewで閲覧・編集

Windowsレジストリを直接触るのは大変なので、shellmenuviewを使ってカスタマイズする。

ExtendedModeがYesに設定している。

f:id:takuya_1st:20210912095336p:plain

f:id:takuya_1st:20210912095833p:plain

Backgroundは、なにもないところを右クリックしたとき

エクスプローラでなにもないところをクリックするだけでも大量にメニューが出てくるので困りますよね。

ピン留め系のメニューを消す。

ピン留め系のメニューなんて初期設定したあと殆ど使わなくないですか?

f:id:takuya_1st:20210912101358p:plain

ということでこれもスタメン落ちさせてみます。

Computer\HKEY_CLASSES_ROOT\Folder\shell\pintohome

f:id:takuya_1st:20210912101512p:plain

メニューから消える。

Shiftキーを押せばいつでも出てくる。

f:id:takuya_1st:20210912101524p:plain

右クリックメニューが多すぎて押し間違え起きる。

Windowsは右クリックメニューを増やしすぎて、押し間違えで面倒が起きるので、整理しておくのがおすすめです。

WEBフォーム項目の連動項目でのバリデーションの一般的な解決法(laravelでの実装例)

Selectやラジオボタンのチェック項目よって、バリデーションが異なる場合。

根本問題としてそういうフォームを作るなと思うのですが。

直前の選択項目によって次の入力項目のバリデーションが異なることがある。

例 電話番号

固定電話と携帯電話で番号をバリデーションする場合

例として電話番号のバリデーションを考えてみる

HTMLの例

<select name='tel.type' >
  <option value=''>--選んで--</option>
  <option value='cellular'>携帯</option>
  <option value='home'>自宅</option>
</select>
<input type=tel name='tel.number' >

フォーム項目の連動(選択肢よって入力制限が異なる)場合の例として自宅・携帯によって電話番号のバリデーションを変えたいとする。

バリデーションの例

<?php
$validator = Validator::make(
  ['tel' => [
    'type' => 'cellular',
    'number' => '080-1234-5678',],
  ],
  [
    'tel.type' => ['required'],
    'tel.number' => ['required'],
    'tel' => ['required',
      function( $key, $value ) {
        [$type, $number]=array_values( $value );
        
        if ( $type == 'cellular' ) {
          $exp = '/0[789]0-\d{4}-\d{4}/';
        } else if ( $type == 'home' ) {
          $exp = '/0\d{2,3}-\d{2,4}-\d{4}/';
        }
        return preg_match( $exp, $number );
      }],
  ] );

$ret = $validator->passes();

ポイント:グループ化

ポイントは超単純。フォーム項目を「グループ」に分ける。

今回は、フォーム項目をtel配列に入れている。配列に入れるとバリデーションは格段に扱いやすくなる。

そしてグループ(配列)ごとに、バリデーションすると考える。

バリデーションの項目をrequired_if などであれこれ考えるより、配列で綿たほうが早い。

バリデーションのネスト。

コールバックを用いてバリデーションしている箇所が、if 連鎖で美しくないと思う人もいるだろう。

そういうときは、バリデーションをネストしてあげる。

コールバックで難読化してるのを

<?php
[
  'tel.type' => ['required'],
  'tel.number' => ['required'],
  'tel' => ['required',
    function( $key, $value ) {
      [$type, $number] = array_values( $value );
      if ( $type == 'cellular' ) {
        return preg_match( '/0[789]0-\d{4}-\d{4}/', $number );
      } else if ( $type == 'home' ) {
        return preg_match( '/0\d{2,3}-\d{2,4}-\d{4}/', $number );
      }
    }],
]

スッキリ(ネスト)させる

<?php
'tel' => ['required', function( $key, $value ) {
  $validator = Validator::make( $value, [
    'type' => 'required',
    'number' => ['required', new PhoneRule( $value['type'] )],
  ] );
  return $validator->passes();
}]

相当スッキリするはずである。

まとめ

要は、「配列」(パーツ)にフォーム項目を分けて、ネストしてパーツごとにバリデーションするとスッキリする。

あれこれ、Ruleを考えたり、require_if などを組み合わせるより圧倒的にかんたんで読みやすい。検索しても出てこないのが不思議である。

今回は、laravelにおけるバリデーションであったが、railsでもjavaのspringやpython djangophp のその他のフレームワーク(cakephp)などでも全く同じ考え方で処理できると思うんですよね。バリデーションが複数にまたがるときも「分割統治する」という分割責任原則が通用する。

バリデーションが複数個所で入力値によって変わるときどうするのかという質問は頻発なのになんでみんな複雑なものを複雑にコーディングしてしまうんだろう。

ファイル名の最大長の限界を、ストレージのフォーマットタイプごとに調べる

ファイル名の限界を調べる。

windows の人からもらった zip ファイルが 展開できなくて、ファイル名の長さの問題だったので、限界値(最大のファイル名の長さ)をぱぱぱっと調べた。

ファイルシステムごとに、ファイルの文字長(ファイルの文字サイズ)が違っていて、本当に困るんですよ。

日本語だと文字数ほとんど使えないLinux

Linuxはファイルの文字数が255バイトになっているので、日本語UTF-8のとき文字数が極めて少ない。

フォーマット 日本語の最大可能文字数
ext4 85文字
btfs 85文字
NTFS 255文字
exFAT 255文字

UTF-8が3バイト前後というのはわかるのですが、85文字はマジでやばい。少なすぎませんかね。

これは、旧来のシステムと互換性切ってでも文字数はほしいところです。LinuxのSambaとかでファイル名が長くて保存されないってよくある問題。ずっと解決しないので、これから先も解決することな無いんだと思う。

カーネルコンパイル時にファイル名のサイズの上限を変えてコンパイルさせれば、ワンチャンあるけど、それした場合、アップデートどうすんの感。

ちなみに、Linuxから実験してるのでNTFSexFATLinuxでマウントしてます。マウントしてたら最大長突破できるのなら、ext4もマウントオプションで突破できるのではないかと思うんだけどね。

XFSやzfsは調べなかったけど、ex4と結果は同じです、同じ定数(NAME_MAX )見てるので。Linuxでマウントしてる限りは、255が適用されちゃう。

linux で現在の最大長を調べる

255 バイトと決まっているのだから、別に調べなくてもいいのだが。調べたいときは次のようにする。

getconf -a | grep -P  '^NAME_MAX'
NAME_MAX                           255

実験に使用したコード

Linux環境で UTF8 で実行を前提に。

ファイル名の文字列の長さを調べていく。

    pwd = Dir.pwd.to_s
    (1..256).map{|i| 
        begin 
            str = ""*i
            FileUtils.touch(str)
            FileUtils.remove(str)
        rescue => e 
            printf "最大のファイルサイズは %3d バイトで、日本語だと %d 文字\n", 
            (""*(i-1)).bytesize ,  (""*(i-1)).size
            break
        end
    }

ディレクトリについても調べる

    pwd = Dir.pwd.to_s

    (1..256).map{|i| 
        begin 
            pwd = Dir.pwd.to_s
        str = "/あ"*i
        FileUtils.mkdir_p(pwd+str)
        FileUtils.rm_rf(pwd+str)
        puts pwd+str
        rescue => e 
            printf "最大のファイルサイズは %3d バイトで、日本語だと %d 文字\n", 
            (""*(i-1)).bytesize ,  (""*(i-1)).size
            printf "パス名を含めた場合、最大のファイルサイズは %3d バイトで、日本語だと %d 文字\n", 
            (pwd+'/'+""*(i-1)).bytesize ,  (pwd+'/'+""*(i-1)).size
            break
        end
    }

ext4 は 255バイト(UTF-8で85文字)で上限

ext4

ただしディレクトリは別カウント

ext4ディレクト

btrfs も 85文字で上限

btrfs

NTFS は「文字数」で255文字

NTFS

exFAT は「文字数」で255文字

exFAT

linuxのどこで定義されているのか。

cat /usr/include/linux/limits.h
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
#ifndef _LINUX_LIMITS_H
#define _LINUX_LIMITS_H

#define NR_OPEN         1024

#define NGROUPS_MAX    65536    /* supplemental group IDs are available */
#define ARG_MAX       131072    /* # bytes of args + environ for exec() */
#define LINK_MAX         127    /* # links a file may have */
#define MAX_CANON        255    /* size of the canonical input queue */
#define MAX_INPUT        255    /* size of the type-ahead buffer */
#define NAME_MAX         255    /* # chars in a file name */
#define PATH_MAX        4096    /* # chars in a path name including nul */
#define PIPE_BUF        4096    /* # bytes in atomic write to a pipe */
#define XATTR_NAME_MAX   255    /* # chars in an extended attribute name */
#define XATTR_SIZE_MAX 65536    /* size of an extended attribute value (64k) */
#define XATTR_LIST_MAX 65536    /* size of extended attribute namelist (64k) */

#define RTSIG_MAX         32

#endif

btrfs とかは回避できなくもない。

btrfs でマウントしたボリュームをを、cifs/smb 経由でvfat でマウントし直せば。。。。できなくはないが後で地獄を見ることになる。

参考資料

cat /usr/include/linux/limits.h

https://serverfault.com/questions/9546/filename-length-limits-on-linux

https://superuser.com/questions/1109959/reconstituting-long-file-names-lfn-from-8-3-on-nas-server-after-system-crash

laravelの設定(config) のキャッシュと実行環境(テスト環境testing/local ) の関係

laravel の env / config の関係について。

env と configキャッシュで testing のデータベースが使われずにパニクった。

TL;DR

.env.testing を使ってる場合、テスト実行する前には、cache:clearする。

laravel の実行環境と設定について

実行環境と設定は、.env ファイルと、config によって変わる。

実行環境

実行環境は、 envで決まる。 env は .env ファイルで決める。

.env ファイル

環境変数(OS/シェル)の実行環境の変数 Environment (通称ENV)の値を上書きする。自分で作る。

.env.testing ファイル

テスト実行時に使うファイル。自分で作る。

設定ファイル。

設定が書き込まれたファイル。

config/xxxx.php ファイル。

laravel 内で使うのは主にこれ。xxxx の部分が名前空間になっている。

config/ と ファイル名(ex:config/database.php)の関係

ファイル名が名前空間プレフィックス・カテゴリ)になるように調整される。

たとえば、database.php の場合は、次のようになる

ファイル名 名前空間
config/database.php database
config/queue.php queue

設定は名前空間を使ってアクセスする

config/queue.php を取得

config('queue.myname');

config と env の関係

殆どの場合、config はロード時に .env を参照するように書かれている。

config/session.php

<?php 
return [
 'lifetime' => env('SESSION_LIFETIME', 120),
];

config() を呼び出し、env() は ENV(+.env)を呼び出すのがわかっっている。

config 設定ファイルの中で env() が多用される。

config と env の呼び出し順(キャッシュなし)

config() が実行されるときに、env (名前, デフォルト値)への呼び出しのショートカットになっている。

呼び出しの順序が次のようになっている。

config() 
    -> env() 
        -> ENV(OS/シェル) 
        ->  デフォルト値

config は初期ロード前に変数にまとめられる。

configは一旦変数にキャッシュされる。というか config/ に散らばるArrayをまとめて一つにしている。var_exportしてますね。

config キャッシュ

config は、毎回ロードを避けるためにキャッシュすることが可能。

config のキャッシュ保存。

キャッシュ保存先は次のとおりになる。

bootstrap/cache/config.php

bootstrapを見ればわかるが、env() の結果で置き換わっているのがわかるでしょう。var_exportしてますね。

.env と .env.testing とキャッシュの関係

config が env() から取ってきて、env() が .env から取ってくるのはわかった。 だったら、.env.testing はいつ使うんだよ。.env.testingはどうなるの。

今使ってるのはどっちなんだよ問題。

キャッシュがあればキャッシュを使います。

env()の結果をconfigとしてキャッシュしてるんですから。testing/dev環境ではキャッシュを使ってはいけません。その理由をコードを動かして見ていきます。

キャッシュがあるときの優先度

先程の優先度の関係は、キャッシュが優先です。

config
   ->キャッシュある
   -> キャッシュ無い
    -> config() 
            -> env() 
                -> ENV(OS/シェル) 
                    ->  デフォルト値

であったとわかりますよね。

実際に試してみるとよくわかりますよね

testing 環境とキャッシュを試してみる、

config をダンプするコマンドを作って試してみます。

ファイルを作ります。

php artisan make:command MySample

コードを書きます。

namespace App\Console\Commands;
use Illuminate\Console\Command;

class MySample extends Command {
  
  protected $signature = 'my:sample';
  
  public function handle () {
    dd(config('my.name'));
  }
}

設定を作成します。

config/my.php

<?php

return ['name' => env("MY_NAME"),];

.envを2つ書きます。

開発用と、テスト用の2つのenv ファイルを用意します。

.env

MY_NAME=takuya-prod

.env.testing ファイルに書きます。

.env.testing

MY_NAME=takuya-testing

キャッシュをクリアして動作を確認します。

確認のために、まずキャッシュをクリアします。

$ php8.0 artisan config:clear
Configuration cache cleared!

通常のconfig 確認

通常環境でコマンドを実行してconfigを見ます。

$ php8.0 artisan my:sample
"takuya-prod"

テスト環境でconfig 確認

testing 環境でコマンドを実行してconfigを見ます。

$ APP_ENV=testing php8.0 artisan my:sample
"takuya-testing"

キャッシュ有りで テスト環境のconfig

通常環境でキャッシュを作成してconfigを見ます。

んーキャッシュ優先ですね。

config を確認してみる。

$ php8.0 artisan my:sample
"takuya-prod"
$ APP_ENV=testing php8.0 artisan my:sample
"takuya-prod"
$ APP_ENV=testing php8.0 artisan my:sample
"takuya-prod"

テスティング環境でキャッシュを作ってconfigを見ます。

testingでもキャッシュ優先ですね。

$ APP_ENV=testing php8.0 artisan config:cache
Configuration cache cleared!
Configuration cached successfully!
$ php8.0 artisan my:sample
"takuya-testing"
$ APP_ENV=testing php8.0 artisan my:sample
"takuya-testing"

ということで、artisan config:cacheは .env.testingと併せて使うと、落とし穴にハマりますよ。ってことで。

まとめ

.env.testing を使ってる場合、テスト実行する前には、cache:clear しましょう。

今回実験に使用したバージョン

$ cat composer.json | grep laravel/framework
    "laravel/framework": "^8.54",
$ php8.0  -v
PHP 8.0.9 (cli) (built: Jul 30 2021 13:09:07) ( NTS )
Copyright (c) The PHP Group
Zend Engine v4.0.9, Copyright (c) Zend Technologies
    with Zend OPcache v8.0.9, Copyright (c), by Zend Technologies

phpでは、config に ini/xml/jsonファイルを使わないの?

設定といえば、json や ini ファイルを想像するかもしれませんが。

parse_ini_file という便利な関数があるのに config を php の配列ので書くのは、config 内で、ソースコードを実行したいからですね。設定生成用にコンソールを作るのがめんどくさいからですね。ini ファイルで設定書くとかしません。ましてやJavaのアプリ見たく、XMLyaml を使った静的設定ファイルをパースするとか、利用者もフレームワーク製作者もパーサー製作者も、全員への時間の無駄でしか無いと思います。

awk がちゃんと動かない→gnu awk を入れる。

WEBのawk サンプルが動かないときの対応

一般的に使われているawk には色々あります。ネットに書かれている「テクニック」は gnu awk が多いです。

あれれ動かんぞ、とおもったら gawk を入れましょう。

sudo apt install gawk

awk のサンプルが動かない例

たとえば、次は、数字を桁区切りでprintf しようとしているところです。

takuya $ awk '{printf"%\047d",$1}' <<< "1000000"
awk: run time error: improper conversion(number 1) in printf("%'d")

gawk なら動きます。

takuya $ gawk '{printf"%\047d",$1}' <<< "1000000"
1,000,000

gawk をデフォルトawk

gawk が気に入ったらデフォルトにしておきましょう。

sudo update-alternatives --config awk
There are 2 choices for the alternative awk (providing /usr/bin/awk).

  Selection    Path            Priority   Status
------------------------------------------------------------
* 0            /usr/bin/gawk    10        auto mode
  1            /usr/bin/gawk    10        manual mode
  2            /usr/bin/mawk    5         manual mode

Press <enter> to keep the current choice[*], or type selection number:

参考資料

https://pooh.gr.jp/?p=9768

https://orebibou.com/ja/home/201707/20170717_001/

debian に p7z-rar を入れる。

debian に p7z-rar を入れる。

rar ファイルを扱いたいなと思ってんけど。

rar ファイルは zip と違って、日本語が化けないし、使ってもそんなに困るアーカイブ形式じゃない。

$  sudo apt install p7zip-rar
Reading package lists... Done
Building dependency tree
Reading state information... Done
Package p7zip-rar is not available, but is referred to by another package.

non-free を有効にする。

cat  /etc/apt/sources.list
deb http://debian-mirror.sakura.ne.jp/debian/ buster main  contrib non-free
deb http://debian-mirror.sakura.ne.jp/debian/ buster-updates main
deb http://security.debian.org/debian-security/ buster/updates main
deb http://ftp.debian.org/debian buster-backports main contrib non-free
$ sudo apt list p7zip*
Listing... Done
p7zip-full/oldstable 16.02+dfsg-6 amd64
p7zip-rar/oldstable 16.02-3 amd64
p7zip/oldstable 16.02+dfsg-6 amd64

インストール

$ sudo apt install p7zip-full --install-suggest

7z がunar より有能

unar で展開出来ない rar ファイルでも、7z なら展開できる。

参考資料

https://mag.osdn.jp/07/02/07/0137212

7-zipコマンドの一覧オプション ba を使って再利用しやすくる

7z にはドキュメント化されてないオプションがあるんだ・・

罠でしょ。

7z でアーカイブファイルをパイプで渡しやすい形式にする。

takuya$ 7z l -ba sample.zip
2021-08-23 15:59:54 .....            2            2  a.txt

7z でアーカイブファイルの中身を見る

通常であれば、 7z l archive.zip の形式を使います。次のような出力結果になりますが、むちゃくちゃ再利用しにくいんですよね

takuya $ 7z l sample.zip

7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
p7zip Version 16.02 (locale=en_US.UTF-8,Utf16=on・・・

Scanning the drive for archives:
1 file, 146 bytes (1 KiB)

Listing archive: sample.zip

--
Path = sample.zip
Type = zip
Physical Size = 146

   Date      Time    Attr         Size   Compressed  Name
------------------- ----- ------------ ------------  ------------------------
2021-08-23 15:59:54 .....            2            2  a.txt
------------------- ----- ------------ ------------  ------------------------
2021-08-23 15:59:54                  2            2  1 files

baはドキュメント化されてない

フォーラムによると、このオプションは使えるけど、ドキュメント化されてないって。 まじか。嘘でしょって感じですね。知ってたら lsar/unar より、7z をメインで使ってましたよ。

-ba - suppress headers; undocumented.

参考資料

https://7ziphelp.com/7zip-command-line https://superuser.com/questions/1020232/list-zip-files-contents-using-7zip-command-line-with-non-verbose-machine-friend

phpに於ける StringIO

phpに於ける StringIO

php にはStringIOが無いと存じます。無いのではなく、必要がないのです。

stringio を実現する

$string_io = new \SplFileObject('php://memory','w+');

これだけです。SplFileObjectが最強です。

もしくは、fopen

$fp = fopen('php://memory','w+');

読み込み書き込み

SplFileObject がストリームのように扱えます。

$string_io = new \SplFileObject('php://memory','w+');
$string_io->fwrite("aaa\n");
$string_io->fwrite("bbb\n");
$string_io->fwrite("ccc\n");
$string_io->rewind();
foreach( $string_io as $line){
   var_dump( trim($line));
}

ただし、trim が必要な点。 fwrite と f-XX が冗長な点。また SplFileObject 自身が大量に継承メソッドを持ち、自分自身がIterable であり、ちょっとわかりにくいんじゃないかと思います。

組み込みで使えるし require 不要でいつでも使えるので便利ではあるのですが。

SplFileObjectというクラス名は、ファイルを連想させコードリーディングの障害になりかねない。そのため変数名を$string_io と意図を明確に書くべきだとおもう。

SplFileObject よりも fopen で php://memory を使ってみたらどうなるか、実装例も作ってみたが、SplFileObjectで十分だと思う。

参考資料

https://www.php.net/manual/en/class.splfileobject.php

sudo時の$HOME/$USER の環境変数の継承について

はじめに

-E環境変数を引き継げるのだけれど、WSLでsudo したときに、Windowsアプリにroot を使ってほしくなく、単にファイルを書き換えたいだけという欲求が出てきたので調べた。結論としてはむやみにやるんじゃない。ってことですけどね。

Home環境変数を維持する。-E

次のように、特に明示しない限り、sudo時に $HOMEをそのままにする。

takuya $ sudo -E  sh -c 'echo $HOME'
/home/takuya
takuya $ sudo -H -E  sh -c 'echo $HOME'
/root

$HOMEが書き換わる仕組み

デフォルトの挙動を見ておく。 素の状態では、sudo 時に 環境変数はリセットされ、HOMEは rootが使われる。

takuya $ sudo sh -c 'echo $HOME'
/root

環境変数を持ち込む

環境変数を維持したまま、sudo を実行する。

sudo -E sh -c 'echo $HOME'
/home/takuya

$HOME以外の環境変数を書き換える -H -E

$HOMEは除外。他の環境変数は維持する。

sudo -E -H sh -c 'echo $HOME'
/root

sudoersとの関係

sudoersのデフォルト設定は次のとおりです。

Defaults        env_reset

env_reset 設定は、sudo時の無継承、環境変数を最小限*1で新規作成し利用する。

このenv_reset を無視した実行が -E

sudo -E

ただし、$HOMEは強制する。 -Eで変数を継承するが、HOME変数は持ち込ませない。

sudo  -E -H  # または -H -E (順不問)

sudoers と -E の関係

-Eを常に有効する 。設定で常に有効にする。

## !が否定。偽になる
Defaults       !env_reset

すると、-Eをつけなくても、環境変数は維持される。

takuya $ export A=123; sudo sh -c 'echo A=$A;echo $HOME'
A=123
/home/takuya

sudoers と -E -Hの関係

sudoers では -Eを常に有効が!env_reset-Halways_set_home に相当する。

環境変数を維持して、ただしHOME変数は持ち込ませない。

# 環境変数を維持
Defaults   !env_reset
# ただし、HOME変数は持ち込ませない。
Defaults   always_set_home 

すると、HOME変数が、持込拒否される。

takuya $ export A=123; sudo sh -c 'echo A=$A;echo $HOME'
A=123
/root

-u オプションと set_logname

ちなみに、-uでユーザー名の書き換えができます。

takuya $ sudo sh -c 'echo $USER:$HOME'
root:/root
takuya $ sudo  -u takuya sh -c 'echo $USER:$HOME'
takuya:/home/takuya

しかし、-uで書き換えたところで、管理者コマンドは実行できません。

takuya$ sudo -E -u takuya apt remove dnsutils
open (13: Permission denied) are you root?

-uset_logname相当なのですが。

sudoersに次のように、設定し、sudo時に$USERを無変更にさせます。

Defaults !set_logname

すると、sudoしても$USERはそのままです。

sudo -E sh -c 'echo $USER:$HOME'
takuya:/home/takuya

そして、-u と同じようにapt install はできませ・・・えええ・・・そのまま apt install はできちゃう・・・

takuya$ sudo -E sh -c 'echo $USER:$HOME'
takuya:/home/takuya
takuya$ sudo -E apt install dnsutils
Setting up dnsutils (1:9.11.5.P4+dfsg-5.1+deb10u5) ...

えええ・・・どうなってるんだ。sudoers怖いね。

sudoers 書き換えは自分の開発環境だけにすること

むやみに書き換えないこと。環境変数をむやみに変えてたりすると、思わぬバグを踏んで、セキュリティを台無しににするので、env_resetなど書き換えはできる限りやらず、

開発環境でも sudo -Eで使ったほうがいいです。

https://www.sudo.ws/alerts/env_add.html

http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-0106

Sudo 1.6.9 before 1.8.5, when env_reset is disabled, does not properly check environment variables for the env_delete restriction, which allows local users with sudo permissions to bypass intended command restrictions via a crafted environment variable.

今回実験に使ったsudoについて

takuya $ wsl --list --verbose
  NAME      STATE           VERSION
* Debian    Running         1
takuya $ cat /etc/debian_version
10.10
takuya $sudo --version
Sudo version 1.8.27
Sudoers policy plugin version 1.8.27
Sudoers file grammar version 46
Sudoers I/O plugin version 1.8.27

参考資料

*1:If set, sudo will run the command in a minimal environment containing the TERM, PATH, HOME, MAIL, SHELL, LOGNAME, USER, USERNAME and SUDO_* variables.

rubyのpopenでエラー出力や終了コードを取得する話。

ruby の popen に関して。

ruby からIO.popenでプロセスを呼び出すときに、「〇〇」が取れない。というブログがいっぱい見つかって、そうじゃないだろ。という気分になったなったのでメモを残すことにする。

そして、Qiitaなどの記事を丸コピした「いかがでしたでしょうか」も溢れているので、検索でちょっと困った。

私は、プロセス起動を頻繁にやっているので、IO.popen を使っているが、本当にわかりにくいので、面倒だけどいまさらだけど、まとめておく。

IO.popen でプロセス起動

IO.popen はプロセスを起動して結果を取得できる。

Rubyは新しく「プロセス」を起動する。文字列で指定された場合は、シェル経由で、配列で指定された場合は子プロセスで起動する。(ただし、文字列で渡してた場合でも単純なコマンドは子プロセスで起動する。リダイレクトやパイプが含まれるコマンド文字列はシェル経由になる)

プロセスを起動するときは、「STDOUTとSTDIN」を指定して起動する。

IO.popenは プロセスとIOをひらく

IOクラスにあることを忘れないで覚えておく。

IOクラスは、出入力を扱う。popenでプロセスと通信経路を開く。

IO(クラス)--->-->in-->子プロセス--out--> IO(クラス)

少しわかりにくいのがIOクラスです。入力も出力もまとめてIOクラスで扱う。入出力をまとめたのがIOクラス。

子プロセスの実行結果を取得する

io = IO.popen(['echo','Hello IO.popen'] ,'r' )
pp io.read
io.close

実行結果

"Hello IO.popen\n"

子プロセスに入力する

子プロセスの標準入力に書き込む場合

io = IO.popen(['cat', '-'] ,'r+' )
io.puts "Hello IO.popen"
io.close_write
pp io.read
io.close

実行結果

"Hello IO.popen\n"

子プロセスへの出入力について。

ここまでの「結果を取得」「プロセスへ入力」をよく見比べてみる。

## 結果を読み込むとき
io = IO.popen(['echo','Hello IO.popen'] ,'r' )
## プロセスにデータを渡し、結果を読み込む
io = IO.popen(['cat', '-'] ,'r+' )

繰り返しになるが、IO.popenはIOクラスである。READもWRITEもどちらも可能です。それがIOクラスである。

IO.popenは ファイルのopen(オープン)と同じ

IO.popenでやってることは、簡単に言えばプロセスへの出入力を開いている。

ファイルの場合をもいだしてみると、ファイルを開くとき open はファイルへの出入力を開いている。

これらは同じことである。同じこととして扱える。プロセスはファイルみたいなものである。

ruby でファイルを開く例

io = open( '/dev/null', 'r+' )
io.puts "Hello"
io.close

ruby のIOでプロセスを開く例

io = IO.popen( 'cat -', 'r+' )
io.puts "Hello"
io.close

これらのコード例を比較する。コードを見ればわかると思うが、ほぼ同じである。

ファイルへの書込読込と、プロセスへの出入力はほぼ同じように扱える。

終了コードの取得

IO.popen で開いたプロセスの終了コードを取るには。$? を使う。

ただし、$?は子プロセスが終了しないと取れない。当たり前だが、終了しないと$?は出現しない。プロセスが終了したら$?が現れる。

では、いつ終了するのか?

それは、子プロセスへの通信経路として確保しているIOインスタンスclose したときです。ソースコードIO#closeしたときです。

IO(クラス)--->-->in-->子プロセス--out--> IO(クラス)

プロセスと、RubyのIOは接続しています。なのでruby側で子プロセスのoutに接続されたIOをCloseしない限り取得できません。

実行例

io = IO.popen( 'cat -', 'r+' )
io.puts "Hello"
pp  $?
io.close 
pp $?

実行結果

$ bundle exec bin/sample.rb
nil
#<Process::Status: pid 6574 exit 0>

ファイルなら、ファイルをcloseしない限りファイルは開いています。

同じように、closeしない限り popenしたプロセスは開いています。生きています。

子プロセスとRubyの間にパイプがある。

子プロセスとruby の間に接続が確立しているので、システム(Linux)が待っていてくれます。ruby 側がCloseして、子プロセスの出力結果を捨てるまで、読み込み可能なデータとしてOSのシステムコールが出力結果を持っててくれています。rubyからプロセスへの書き込みパイプが開いてるときも同じようにruby側の書き込みを待ってていてくれます。そのためにプロセスは終了しません。

popen引数でオートクローズ

read/write の両方がopenして起動するが、オプション指定で起動時にcloseしています。

'r' の場合は、書き込みの用のパイプは null になって、close状態で起動しています。

'w' の場合は、読み込み用のパイプは null になって、close状態で起動しています。

読み込み専用で、子プロセスを開く

io = IO.popen( 'echo hello', 'r' )
pp io.read
pp  $?
io.close 
pp $?
"hello\n"
nil
#<Process::Status: pid 6637 exit 0>
読み込み専用に書き込むと。。。
io = IO.popen( 'echo hello', 'r' )
io.puts "Hello"

子プロセスが起動するときに、システムコールを使うが、そのシステムコールに読み込み用のパイプだけを渡しているので書き込みができない。

IOError: not opened for writing
  bin/sample.rb:34:in `write'
  bin/sample.rb:34:in `puts'
  bin/sample.rb:34:in `<top (required)>'

書き込み専用で子プロセスを開く(暗黙の出力)

では、書き込み専用にプロセスを開いた場合はどうでしょうか。

この場合は、読み込みをしていないのでcat の結果は無くなりそうです。

io = IO.popen( 'cat -', 'w' )
io.puts "Hello"
pp  $?
io.close 
pp $?

実行結果

あれ、、、catの結果が出てきます。

bundle exec bin/sample.rb
Hello
nil
#<Process::Status: pid 6679 exit 0>
takuya@DESKTOP-2ALDRO3:/mnt/c/Users/takuya/Desktop/zip-checker$

暗黙の接続 $stdout

popenで開いたプロセスの出力先を指定しない場合、rubyの標準出力である $stdout へ接続されています。指定しない場合は$stdoutです。

書き込み専用にプロセスを開く。

それでは、暗黙の$stdout 接続をしたくないとき、次のようなコマンドを実行することを思いつくかもしれません。

popen('cat - > /dev/null' )

でも、ちょっとまってください。これはシェル呼び出しでsh コマンドが自分を/dev/nullにつないでいます。ruby 側にありません。

そこで、ruby の popen はIOをつないでいることを思い出してみましょう。

IOをつないでいるのですから、/dev/nullを開くのもruby側でやってみましょう。

io = IO.popen( 'cat -', 'w', :out=>open('/dev/null','w') )
io.puts "Hello"
pp  $?
io.close 
pp $?

実行結果です。

nil
#<Process::Status: pid 6699 exit 0>

どうでしょうか、popenの扱いが少し見えてきた気がしませんか?

暗黙の接続 $stderr

実は、子プロセスのエラー出力も暗黙の了解として、$stderrに接続されています。

ここが、本当にわかりにくい。

暗黙的に popenのエラー出力をは ruby$stderrに接続されています。ここに気づくか気づかないかで、大きく変わってくる。

この暗黙のIO接続に気づくために、先に$stdoutのお話をしました。

エラー出力を捨てる。

子プロセスのエラー出力をを捨ててみましょう。

これも、コマンドでシェル経由にして、次のようなコードが思いつくかもしれません。

popen('ls /no/exits 2> /dev/null' )

でも、これではシェル呼び出しです。

先程のopenを使ったものを応用してみると次のようになります。

io = IO.popen( 'ls /no/exists', 'r', :err=>open('/dev/null','w') )
pp io.read
pp  $?
io.close 
pp $?

ここまでのまとめ

プロセスの起動には、IN/OUTを指定する必要がある。

-->-->(in)子プロセス(out)----> 

IO.popen('cmd')すると、IOクラスに接続される。

IOクラス--->(in)子プロセス(out)---> IOクラス

IO.popen('cmd','r')すると、IOクラスに接続される。 ただし、入力はclosedされた状態で接続される。

IO(closed)--->(in)子プロセス(out)--->IOクラス

IO.popen( 'w' )すると、$stdoutに接続される。

IOクラス--->-->(in)子プロセス(out)--->$stdout

おなじことが、エラー出力にも起きている。

エラー出力を特に指定しない限り、$stderrに接続される

IO(クラス)--->─(in)子プロセス(out)─>$stdout
                  └-──err───> $stderr

ターミナルで起動時との比較

popenは、ターミナルからコマンドを起動したときと同様にプロセスを起動するのだが。popenを使っているとよくわからなくなる。

ターミナルから起動した場合、STDOUT/STDIN/STDERRはどこへ行くのを考える。popenやファイルとよく似ているため、理解の補助になるかもしれない。

ターミナルから echo を起動したとき

$ echo hello world

この場合、パイプは次のようになる。

  • STDIN 使わないので即Close
  • STDOUT ターミナルの出力へ
  • STDERR ターミナルの出力へ

echo を起動したとき、echo の結果は、ターミナルの出力と接続されている。

ターミナル --> in -> echo -> out -> ターミナル

ターミナル(sshなど)はttyでIOを扱うので、こんな感じかな。

tty --> in -> echo -> out -> tty 

popenの場合、これが、rubyになるだけである。

ruby--> in -> echo -> out -> ruby($stdout or IO)

もう、書きませんが、stderr についても同じである。

ruby--> in -> echo -> err -> ruby($stderr)

接続先のはOS(ファイルディスクリプタ)である。

ここで出てきた接続先はファイルディスクリプタです。

プロセスを起動したとき、プロセスを管理しているのはOSです。OSの管理下の接続先のみプロセスは接続(リダイレクト)が可能です。

IOの接続先はOSの管理するファイルディスクリプタなので、IO.popenが接続できるのは、OSの管理下にあるものです。

だから、OS管理下にないRuby内部の文字列(StringIO)などには接続できません

io = IO.popen( 'ls /no/exists', 'r', :err=>StringIO.new('','w') )
ArgumentError: wrong exec redirect action
  bin/sample.rb:33:in `popen'
  bin/sample.rb:33:in `<top (required)>'

次の例では、エラー出力を /dev/nullに接続していますが、これは/dev/nullを開いた、ファイルディスクリプタに接続しているわけです。ここもいくつかrubyが呼び出したときに、OSとの暗黙的な接続が動いてます。このあたりはC言語知らないと、少し辛いかもしれません。

io = IO.popen( 'ls /no/exists', 'r', :err=>open('/dev/null','w') )

簡単に言えば、popenで接続ができるモノは、プロセスから見える範囲にあるモノに限られます。

IO.pipeでつなぐ。

popenは子プロセスが扱える接続先にしか接続できません。それは「ファイル・STDOUT・STDERR」であり、そしてパイプです。

ここまで分かれば、システムコール(OS)管理下にまかせておけば、何でも接続できるとわかる。

その接続先として任意に作れるのがOSの「パイプ」です。OSにパイプを作成してもらうのは、ruby の、IO.pipeが対応している。このメソッドで、OSにパイプを作ってもらうシステムコールを呼び出すことができる。

pp IO.pipe
pp IO.pipe

pipeすると、StringIOとは違い、システムコールによるOS管理下にあるパイプがもらえる。

$ bundle exec bin/sample.rb
[#<IO:fd 9>, #<IO:fd 10>]
[#<IO:fd 11>, #<IO:fd 12>]

fd はファイルディスクリプタを示し、OSの管理するものである。番号は管理番号で、プロセスごとに割り当てられる。

StrngIOは接続できないが「パイプ」なら、popenのエラー出力や標準出力と接続できるのである。IO.pipeの結果は、ファイルディスクリプタであり、open()と同じようにOSファイルとつながっているわけです。IOは奥が深いですね。

子プロセスの出力先を変える

pooen された子プロセスに、IN/OUTがあるとわかった。それらはパイプであり、OSが管理するファイルディスクリプタである。

これらが、よくわかったところで、その出力先を変えてみようと思う。

rubyからファイルディスクリプタを作るには IO.pipe である。

io_r,io_w = IO.pipe
io = IO.popen(['ls', '/a'] , :err => io_w  )
pp io.read
io.close
pp $?

これを、実行すると、エラー出力は消えてしまう。

bundle exec bin/sample.rb
""
#<Process::Status: pid 6850 exit 2>

消えたけど、捨てられたわけはなく、別PIPEに送信されている。

io_r,io_w = IO.pipe
io = IO.popen(['ls', '/a'] , :err => io_w  )
pp io.read
io_w.close
pp io_r.read
io.close
pp $?

closeが増えて見にくいです。

IO.pipeの結果は、それぞれ(r,w) 専用になっている。それぞれつながっているので、 w側をloseしないと、r側で読めないのが面倒ですね。

popenしたIOはclose前に読み込み、一方で、pipeはcloseしないと読めないんでですね。ややこしくなってきた。*1

ファイルとプロセスでの違い

IO.popenは、プロセスを開くとき、プロセス特有の制限を受ける。プロセスはシステムコールで起動する。そのため、プロセスにわたたせるのは、パイプだけである。

エラー出力を取りたいために、知ることが多すぎる。と思います。

popenでは標準エラー出力を取れないと書かれちゃうのも仕方が無いのかもしれません。

でもちゃんとIO使えば取れることがわかりました。良かったね。

まとめ

popenでエラー出力をとる

IO.pipeする。

popenで終了コード(exit status)をとる。

IO.popenをcloseする。

サンプル

err_r,err_w = IO.pipe()
io = IO.popen(['ls','/', '/a'] , :err => err_w  )
##
err_w.close
pp err_r.read
##
pp io.read
io.close

pp $?

出力先を変えるだけなら

io = IO.popen(['ls','/', '/a'] , :err => '/dev/null'  )

もっと詳しく知りたいとき

次の本を読んでください。

関連資料

https://qiita.com/riocampos/items/3d3f540722f91589e00a

https://riocampos-tech.hatenablog.com/entry/20151225/how_to_capture_stderr_by_io_popen_method

https://qiita.com/tyabe/items/56c9fa81ca89088c5627

*1: 正確には、IO#read はEOFまで読み込みを試みるが、#close しないかぎりEOFが来ないので、EOFまちになって止まる。IO#read以外ならclose前でも読める。

改行を維持して 変数をechoする。

改行を維持して echoする。

改行を保持したまま、変数を出力したい ダブルクォーテーションすることで改行が出る

変数定義

STR="
aaa 
"

出力例

echo $STR ## 改行が出ない
echo "$STR" # 改行がでる

変数をダブルクォーテーションでクォートすると、変数内に含まれた改行がそのまま出てきます。ちょっと不思議な感じします。

join コマンドでLEFT OUTER JOIN のNULL 付きで出して、会議ごとの出席者一覧データを作る。

join コマンドでLEFT OUTER JOIN のNULL 付き的なことをやる

会議の出席の記録や、商品の売上のようなデータを横展開して表にしたい時がある。

このようなとき、join コマンドをぱぱっと実行すれば、データのOUTER JOIN的な事ができる。

出席と欠席を join コマンドで

join 2021-08-11.txt   2021-07-11.txt
A100 出席 出席
A101 出席 出席
A102 出席 欠席
A103 出席 欠席
A104 出席 欠席
A105 出席 欠席

しかし、整理されてない場合や、存在しないデータ(NULL・EMPTY)はいちいち書かない。

次のように、出席だけを書く場合が多いと思う。

出席だけを書いている

cat  2021-07-11-出席者のみ.txt
A100 出席
A101 出席

会議の出席者をメモったテキストがあるとする。

これをJOINするときに、NULLに相当する空文字を埋めてあげれば、データとして処理することができる。

出席や提出は出した人だけをメモっていることが多いですよね。まさか未提出を未提出、未払いを未払いとわざわざメモる人は少ないでしょう。

また、フォルダやファイルを探して見つかったものを集計することがおおく、未出現のIDはデータとして出てきません。

先に、欠席を補完する

そこで、未出現をEMPTYやNULLなど、データ欠損を埋めてデータ処理に適した形式にする。

join -a 1 -o auto -e '欠席' id.txt 2021-07-11-出席者のみ.txt
A100 出席
A101 出席
A102 欠席
A103 欠席
A104 欠席
A105 欠席

または、わかりやすいように NULL の文字で埋めてあげる。

join -a 1 -o auto -e 'NULL' id.txt 2021-07-11-出席者のみ.txt
A100 出席
A101 出席
A102 NULL
A103 NULL
A104 NULL
A105 NULL

この処理を冒頭処理と同等に一回で行う

join -a 1  2021-08-11.txt   <(join -a 1 -o auto -e '欠席' id.txt 2021-07-11-出席者のみ.txt)

join コマンドは、 join <( サブコマンド)のカタチで複数回のネストをするこができる。

複数の処理

しかし実際には、サブコマンドでは不便で。

JOINを join *.txt のような、引数を複数ファイルで行うことが出来ない。

事前処理をして、さらに関数を作ってあげれば、join *.txtのような処理で複数ファイルを使うことで実現する。

function multijoin() {
    out=$1
    shift 1
    cat $1 | awk '{print $1}' > $out
    for f in $*; do join $out $f > tmp; mv tmp $out; done
}
multijoin 出席者一覧 *.txt
cat 出席者一覧

NULLを入れてあげるちょっとした事前処理

このような、ちょっとした事前処理をしてあげるのがBashなどシェルコマンドを使うコツだと思うんですが。

失われた世界の知識になりつつありますよね。

この知識を使えば、繰り返しが出てくるデータを上手く集計することができそうな気がす。

たとえば、

  • 授業の提出物ファイル(フォルダ)から、生徒が毎回提出しているか。
  • チャットルームのログから、毎週の出席者を取り出したい
  • 商品の売上データから日々の売上の有無の計算
  • 会議室の、使用履歴とかを展開したり

フォルダごとやファイルごとに同じデータを繰り返し記録していたり、出現を数えてたりそのような古の知識はUNIX哲学だったのだろうが、たぶんもう忘れられた世界。