jqをEmacsから使う

しむどん 2025-02-20

jqはコマンドラインJSONプロセッサだ。JSONから簡単に属性を取得したり、集計したりできる。今回はjqを使う方法について考えていく。

インストール

ここではHomebrewを使ってインストールする。

brew install jq

正しくインストールできると jq というコマンドが使用できる。

属性を得る

jq コマンドを使用してJSON形式の文字列から値を取得してみる。

$ echo '{"foo": 1, "bar": 2}' | jq '[.foo, .bar]'
[
  1,
  2
]

この例では {"foo": 1, "bar": 2} というJSON形式の文字列から、 foo の値と bar の値を取得し表示している。

Emacsに組み込む

jq をEmacsから使用する方法は大きく分けて2つ考えられる。1つ目はサブプロセスとしてjqを起動し、そのプロセスにデータを渡す事で処理を行う。2つ目はEmacsのダイナミックモジュールの機能を使って、jqをEmacsに動的にリンクする。

ダイナミックモジュールでEmacsにjqを組み込む

ここではダイナミックモジュールでEmacsにjqを組み込む方法について考えていく。Emacsにはダイナミックモジュールという仕組みがあり、これを使うと動的リンクライブラリとしてビルドされた共有ライブラリを、Emacsにリンクして利用できる。

この仕組みを利用してjqをEmacsに組み込んだ jq.el が実装され公開されていた。おそらく今回の場合は、これを使用すると良いだろう。

僕はEmacsについてもっと知りたいという事と、この量のコードであれば自分の手元で管理したいという事から、jq.elを参考にしつつEmacsに組み込む事にする。

また可能な限り素の状態を保つようにし、何が必要で何がそうではないかが分かるようにする。

ダイナミックモジュールはCなどの言語で実装し、共有ライブラリを作っておく必要がある。それを実装する。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include <jq.h>
#include <jv.h>

#include <emacs-module.h>

int plugin_is_GPL_compatible;

int emacs_version;

emacs_value jq_libel_execute(emacs_env* env, ptrdiff_t nargs, emacs_value* args, void* data)
{
  emacs_value Qnil = env->intern(env, "nil");
  if (nargs != 4) {
    return Qnil;
  }

  emacs_value arg_input_data = args[0];
  emacs_value arg_input_length = args[1];

  emacs_value arg_program_data = args[2];
  emacs_value arg_program_length = args[3];

  intmax_t input_buffer_length = env->extract_integer(env, arg_input_length) + 1;
  if (input_buffer_length < 0) {
    return Qnil;
  };
  intmax_t program_buffer_length = env->extract_integer(env, arg_program_length) + 1;
  if (program_buffer_length < 0) {
    return Qnil;
  };

  char *input_str = malloc(sizeof(char) * input_buffer_length);
  if (input_str == NULL) {
    return Qnil;
  }

  char *program_str = malloc(sizeof(char) * program_buffer_length);
  if (program_str == NULL) {
    free(input_str);
    return Qnil;
  }

  bool program_copied = env->copy_string_contents(env, arg_program_data, program_str, &program_buffer_length);
  if (!program_copied) {
    free(program_str);
    free(input_str);
    return Qnil;
  }
  bool input_copied = env->copy_string_contents(env, arg_input_data, input_str, &input_buffer_length);
  if (!input_copied) {
    free(program_str);
    free(input_str);
    return Qnil;
  }

  jq_state *jq = jq_init();
  if (jq == NULL) {
    free(program_str);
    free(input_str);
    jq_teardown(&jq);
    return Qnil;
  }

  int compiled = jq_compile_args(jq, program_str, jv_object());
  if (!compiled) {
    free(program_str);
    free(input_str);
    jq_teardown(&jq);
    return Qnil;
  }

  jv_parser *parser = jv_parser_new(0);
  if (parser == NULL) {
    free(program_str);
    free(input_str);
    jq_teardown(&jq);
    jv_parser_free(parser);
    return Qnil;
  }
  jv_parser_set_buf(parser, input_str, (input_buffer_length-1), 0);

  jv value = jv_parser_next(parser);
  if (!jv_is_valid(value)) {
    free(program_str);
    free(input_str);
    jq_teardown(&jq);
    jv_parser_free(parser);
    jv_free(value);
    return Qnil;
  }
  jq_start(jq, value, 0);

  jv result = jq_next(jq);
  if (!jv_is_valid(result)) {
    free(program_str);
    free(input_str);
    jq_teardown(&jq);
    jv_parser_free(parser);
    jv_free(value);
    jv_free(result);
    return Qnil;
  }

  jv dumped = jv_dump_string(jv_copy(result), 0);
  const char* s = jv_string_value(dumped);
  const int s_len = strlen(s);
  if (s == NULL) {
    free(program_str);
    free(input_str);
    jq_teardown(&jq);
    jv_parser_free(parser);
    jv_free(value);
    jv_free(result);
    jv_free(dumped);
    return Qnil;
  }
  emacs_value ret = env->make_string(env, s, s_len);

  free(program_str);
  free(input_str);
  jq_teardown(&jq);
  jv_parser_free(parser);
  jv_free(value);
  jv_free(result);
  jv_free(dumped);
  return ret;
}

void jq_libel_register_func(emacs_env *env, char *symbol, void *funcptr, int amin, int amax)
{
  emacs_value QSym = env->intern(env, symbol);
  emacs_value QFun = env->make_function(env, amin, amax, funcptr, NULL, NULL);
  emacs_value args[] = { QSym, QFun };

  emacs_value Qfset = env->intern(env, "fset");
  env->funcall(env, Qfset, 2, args);
}

int emacs_module_init (struct emacs_runtime *ert)
{
  /* verify emacs version compatibility */
  if (ert->size < sizeof (*ert)) {
    return 1;
  }
  emacs_env *env = ert->get_environment(ert);
  /* verify dynamic module api compatibility */
  if (env->size >= sizeof (struct emacs_env_26)) {
    emacs_version = 26;  /* Emacs 26 or later */
  } else if (env->size >= sizeof (struct emacs_env_25)) {
    emacs_version = 25;  /* Emacs 25 */
  } else {
    return 2;
  }

  jq_libel_register_func(env, "jq-libel-execute", jq_libel_execute, 4, 4);

  emacs_value Qfeat = env->intern(env, "jq-libel");
  emacs_value Qprovide = env->intern(env, "provide");
  emacs_value args[] = { Qfeat };
  env->funcall(env, Qprovide, 1, args);
  return 0;
}

jq-libel.c

今回は jq-libel-execute という関数を一つだけ用意した。

このファイルをビルドし共有ライブラリを作る。今回はmacOSで作業を行っているため共有ライブラリの拡張子は .dylib となる。ビルドにはEmacsのソースコードのヘッダファイル、jqのヘッダファイルとライブラリが必要となる。

具体的なパスは環境によって異なるため、以下の変数を使う。必要に応じてパスを置き換えて欲しい。

EMACS_SRC
Emacsのソースコードのディレクトリへのパス。
JQ_INCLUDED_DIR
jqのヘッダファイルのディレクトリへのパス。
JQ_LIB_DIR
jqのライブラリのディレクトリへのパス。
gcc-14 --shared -I${EMACS_SRC} -I${JQ_INCLUDED_DIR} -L${JQ_LIB_DIR} -ljq -o jq-libel.dylib jq-libel.c

ビルドが成功すると jq-libel.dylib が作られる。これはEmacsのダイナミックモジュールの形式をしているため、 load-path の配下にファイルを配置する事で requireload を使ってEmacsに読み込む事ができる。

このライブラリを使ってEmacs Lispから操作しやすいようにしたLispを jq.el に記述した。

;;; jq.el --- Yet another jq library. -*- lexical-binding: t; -*-
(require 'jq-libel)

;;;###autoload
(defun jq (jsonstr querystr)
  (jq-libel-execute jsonstr
      (string-bytes jsonstr)
      querystr
      (string-bytes querystr)))

(provide 'jq)
;;; jq.el ends here

まとめ

今回は jq の使い方について調べた。またEmacsから利用するためにダイナミックモジュールとして、jqをEmacsに動的リンクする方法を調べ、それを実装した。