mike-neckのブログ

Java or Groovy or Swift or Golang

gh issue list コマンドでソートする方法

gh コマンドで GitHub のイシューを引っ張ってきてソートしたい場合、ソートというオプションがないようなので困る。

そこでソートする方法としては以下の3つの方法が考えられる

  1. --search オプションによるソート
  2. --jq オプションで jq によるソート
  3. パイプでつないで sort

(1) --search オプションによるソート

一応公式のイシューでソートする方法があると紹介されているのがこの方法。

github.com

ところが以下の通り、少しは頑張ったかなと思われるものの、ソートできているとは言いがたい

(2) --jq オプションで jq によるソート

jq によるソートはデータを引っ張ってきてローカルで jq を呼び出してソートしている感じ。 なお、 --jq オプションを指定するには、 --json オプションが必要になる

この場合、出力が json になるし、元のシンプルな形での表を見たい場合は、それなりの jq のスキルが求められる(ChatGPT に聞いておけばよいかもしれない)。

(3) パイプでつないで sort

で、結局これが一番やりやすかった

ffmpeg を bash ループの中で使うと、 read が正しく標準入力を読めなくなる問題の対処

以前のエントリーにも書いた ffmepg を使うと標準入力が壊れる問題の解決方法は、

/dev/null を読み込ませるということである、

と言いたいだけのエントリー。

mike-neck.hatenadiary.com


次のようなループで read -r filepath が想定してない欠損した値を読み込むのは ffmpeg が標準入力を読み込んでいるのが原因。

while read -r filepath; do
    ffmpeg \
        -i "${filepath}" \
        -c copy \
        "${filepath/.mov/.mp4}"
done < <(find path/to/videos -type f -name '*.mov')

そこで ffmpeg/dev/null から読み込ませるようにすれば、 read -r filepath が想定してないデータを読み込むことはなくなる

while read -r filepath do
    ffmpeg \
        -i "${filepath}" \
        -c copy \
        "${filepath/.mov/.mp4}" \
        < /dev/null
done < <(find path/to/videos -type f -name '*.mov')

よく考えれば当然の方法だが、ちょっと気づかなかったのでメモ

JEP-430 String Template を簡単な範囲で調べた

JEP-430 String Template が気になったので簡単な範囲で調べてみた。

触ってみた系のブログが好きでない人は閉じてもらってよいです。

ざっくり

  • JEP-430 の仕様が詳しいので、これを読むだけでだいたい必要なことは覚えられる
  • processor."String Template" の形式でオブジェクトを作る
  • "String Template" 部分に入れ込む値はそのままでも、式でも可能だが、 forwhile などの Statement はエラーになる
  • StringTemplate.Processor を自作するのも難しくない
  • StringTemplate 部分の中で別の StringTemplate を使える
  • String Template 部分は invokedynamic 命令で StringTemplate のオブジェクトを作るコードになる

JEP-430 の仕様が詳しいので、これを読むだけでだいたい必要なことは覚えられる

JEP-430 String Template の仕様書にString Templateのことが 詳しく書いてあるので、ちゃんと理解したい場合はこのブログを読むよりは仕様書を読んだほうがよい。 最近のJEPは非常に丁寧でわかりやすい。

さっそく試してみる

早速試してみる。なお、試したのは build 21-ea+25-2212 。

public interface HelloWorld {

    static void main(String [] args) {
        String greeting = "Hello";
        String name = "String Template";
        int java = 21;
        String text = STR."\{greeting} \{name}@Java\{java}";
        System.out.println(text);
    }
}

出力は次のようになる

Hello String Template@Java21

String Template の使い方、値の形式

STRStringTemplate.Processor<String, RuntimeException>インスタンスで 実際に値を埋め込んでいる String Template の部分をメソッド(フィールド?)のように呼び出す(アクセスする?)ことで、文字列に出力される。 値を埋め込む時の指定は \{expression} の形式になる。

expression の部分はリテラルでも変数でも式でもメソッド呼び出しでもよい。例えば、次のように switch も埋め込める

public class Switch {

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            System.out.println(STR."item-\{i}\{ switch(i) {
                    case 1 -> "st";
                    case 2 -> "nd";
                    case 3 -> "rd";
                    default -> "th";
                }
            }");
        }
    }
}

しかし、繰り返し処理のようなケースには対応できない

public class For {
    public static void main(String [] args) {
        //コンパイルエラー
        System.out.println(STR." \{ for(int i = 0; i < 5; i++) { } foo \{ } }");
    }
}

ここまでに、取り扱ったのは STR というProcessor<String, RuntimeException> 。 ほかにも java.util.FormatProcessor というクラスには FMT という FormatProcessorインスタンスがある。

import static java.util.FormatProcessor.FMT;

class Example {
    public static void main(String[] args) {
        for (int i = 1; i < 100000; i=i*10) {
            System.out.println(FMT."value = %05d\{i}");
        }
    }
}

出力

value = 00001
value = 00010
value = 00100
value = 01000
value = 10000

自作 StringTemplate$Processor

JEPを読み進めると、自作 StringTemplate$Processor を作る話が出てくる。 StringTemplate$Processor<R, E extends Throwable>R process(StringTemplate st) throws E の単一メソッドを実装するだけのインターフェース。 R の型は自由に決められるので、 String Template 部分で XML を作っておいて、 R はそれを読み込んだ状態の Document を指定することも可能。 SMT."text \{object.getValue()}" のようなテキストと埋め込む式の部分はコンパイル時に StringTemplate という オブジェクトに変換され、 invokedynamic 命令で作られる。

JEPの例ではSQLPreparedStatement を作っていたので、ここでもそれにあわせて Processor<PreparedStaement, SQLException> を 実装してみる。

import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.List;
import java.time.Instant;

import org.jetbrains.annotations.NotNull;

record SqlProcessor(
        @NotNull Connection connection
) implements StringTemplate.Processor<PreparedStatement, SQLException> {
    @Override
    public PreparedStatement process(@NotNull StringTemplate st) throws SQLException {
        List<String> fragments = st.fragments(); // 1
        String query = String.join("?", fragments);
        PreparedStatement statement = connection.prepareStatement(query);

        List<Object> values = st.values();   // 2
        for (int i = 0; i < values.size(); i++) {
            Object value = values.get(i);
            int index = i + 1;
            switch (value) {
                case Integer v -> statement.setInt(index, v);
                case Instant v -> statement.setDate(index, new Date(v.toEpochMilli()));
                default -> statement.setString(index, String.valueOf(value));
            }
        }
        return statement;
    }
}

fragments() というメソッドでは、 String Template の文字の部分が取得される。

String titleLike = "助成団体決定%";
Instant created = OffsetDateTime
        .of(2020, 1, 2, 15, 4, 5, 6, ZoneOffset.ofHours(9))
        .toInstant();

SqlProcessor sql = new SqlProcessor();
//language=sql
PreparedStatement statement = sql."""
select *
from documents d
where d.title like \{titleLike}
  and d.created_at < \{created}
"""

例えば、上記のString Template に対しては次の3つが fragment() メソッドで返される。

  • select *d.title like の部分
  • \n and d.created_at < の部分
  • ``(empty な文字列)の部分

よって、これを ? で結合すると、 PreparedStatement のクエリが作られる。

values() には埋め込んだ部分 \{...} の値が入ってくる。上記の例では String Instant の値があり、 一つの型には決められないので List<Object> となっており、大体の場合は switch で処理を分けることになると思われる。

作成した StringTemplate.Processor<PreparedStatement, SQLException> については、 具体例にもあるように . を挟んで String Template を記述するようになっている。 switch 式も埋め込めることから、次のように動的な SQL も記述できる。

String titleLike = null;
Instant created = OffsetDateTime
        .of(2020, 1, 2, 15, 4, 5, 6, ZoneOffset.ofHours(9))
        .toInstant();

SqlProcessor sql = new SqlProcessor();
//language=sql
PreparedStatement statement = sql."""
select *
from documents d
where d.created_at < \{created}
\{ switch (titleLike) {
    case null -> "";
    default -> STR."and d.title like \{titleLike}";
} }
"""

入れ子

上記のように、 String Template 中に String Template を使用することももちろん可能である。

コンパイラーによる処理

String Template は実行中に String を解析するのではなく、コンパイラーで解析して、 invokedynamic 命令によって String Template のオブジェクトを作っている。

javap で解析してみると、このよう invokedynamic 命令と断片化されたSQLがBootstrapMethodsにあるのがわかる。

       147: invokedynamic #104,  0            // InvokeDynamic #6:process:(ILcom/example/string/ext/ConditionalSQLFragment;Lcom/example/string/ext/SQLFragment;)Ljava/lang/StringTemplate;
       152: invokevirtual #107                // Method com/example/string/ext/SqlProcessor.process:(Ljava/lang/StringTemplate;)Ljava/lang/Object;
       155: checkcast     #108                // class java/sql/PreparedStatement
BootstrapMethods:

  6: #231 REF_invokeStatic java/lang/runtime/TemplateRuntime.newStringTemplate:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;[Ljava/lang/String;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #209 select\n  s.id,\n  s.name,\n  p.position,\n  d.id,\n  d.name,\n  m.since\nfrom staff s\n  join positions p on s.id = p.staff_id\n  join member m on s.id = m.staff_id\n  join department d on d.id = m.depart_id\nwhere\n  d.id =
      #211 \n
      #211 \n
      #211 \n

まとめ

  • String Template 便利だし、自作 Processor も楽しい、万能感ある
  • 自作 Processor はちゃんと使えるものを作ろうとすると結構沼かもしれない
  • したがって、SQL周りとかはフレームワークやライブラリーの作者が作ってくれると思う

Mac OS の bash(zsh) の while read ループの中で ffmpeg を呼ぶと、read が正しく実行できなくなる

大量に ffmpeg でファイルを処理する必要があったので、 find コマンドから while read -r につないで ffmpeg を実行していたら、2 回に 1 回ファイルが見つからずにエラーになった。

set -e
readonly fromDir="images/original"
readonly toDir="images/dest"
declare filepath

while read -r filepath; do
  echo "${filepath}"
  ffmpeg \
      -i "${filepath}" \
      -vf scale=240:180 \
      "${toDir}/${filepath##*/}" 2>/dev/null
done < <(find "${fromDir}" -type f -name '*.png')

これを以下のルートディレクトリーで実行すると…

2 番目のファイルのパスの先頭一文字消えている


find を記述する位置を変更してもファイル名が正しく読み取れなかったので、多分そういうものなのだろう…

上記のコマンドについては、 -i "${filepath}" の部分を -i "${fromDir}/${filepath##*/}" に変更して事なきを得た