mlrBinder 소개 — Java에서 Miller를 간단하게 부르기

먼저 밝힙니다. mlrBinder는 제가 직접 만들고 있는 프로젝트입니다. 이 글은 README를 바탕으로 설명은 짧게 줄이고, 샘플은 모두 포함한 버전입니다.


mlrBinder는 Miller(mlr)를 자바에서 읽기 좋은 체인 API로 호출할 수 있게 해 주는 라이브러리입니다. 내부적으로는 표준 mlr 프로세스를 그대로 실행합니다.

  • Java 11 이상
  • Maven Central: io.github.bluedskim:mlr-binder
  • 권장 mlr 버전: mlr 6.17.0
  • 실행 방식: JNI가 아니라 외부 mlr 프로세스를 실행
  • 권장 스타일: 전역 플래그는 Mlr 체인에, 동사는 Miller와 같은 이름의 메서드로 호출 (filter / split.filterVerb() / .splitVerb())

설치 — 이렇게만 추가해 보세요

Java 11 이상이면 됩니다. 주요 타입이 들어 있는 패키지는 **net.shed.mlrbinder**입니다.

Maven (pom.xml):

<dependency>
  <groupId>io.github.bluedskim</groupId>
  <artifactId>mlr-binder</artifactId>
  <version>0.2</version>
</dependency>

Gradle (Kotlin DSL):

dependencies {
    implementation("io.github.bluedskim:mlr-binder:0.2")
}

Gradle (Groovy):

dependencies {
    implementation 'io.github.bluedskim:mlr-binder:0.2'
}

README 샘플 모음

아래는 README의 샘플을 그대로 옮긴 것입니다.

  • 시작: Mlr.inDir(…) 또는 Mlr.withCsvPreset()
  • 전역 플래그: Mlr 체인에 붙이기
  • 동사: Miller와 같은 이름의 메서드 사용
  • 예외: filter / split.filterVerb() / .splitVerb()
  • 연속 동사: 여러 동사를 잇으면 then이 자동으로 들어감

필요한 import만 골라 쓰시면 됩니다.

import static net.shed.mlrbinder.Flag.flag;
import static net.shed.mlrbinder.Objective.objective;
import static net.shed.mlrbinder.SortFlags.f;
import static net.shed.mlrbinder.SortFlags.n;
import static net.shed.mlrbinder.SortFlags.nr;
import static net.shed.mlrbinder.verb.Option.option;

import net.shed.mlrbinder.Mlr;

입출력 플래그와 cat

mlr --csv cat example.csv
Mlr.inDir(workingPath)
	.csv()
	.cat()
	.file("example.csv")
	.run();
mlr --icsv --opprint cat example.csv
Mlr.inDir(workingPath)
	.icsv()
	.opprint()
	.cat()
	.file("example.csv")
	.run();

파일 형식(CSV, JSON, DKVP, TSV)

mlr --csv cat shape.csv
mlr --json cat shape.json
mlr --idkvp --ocsv cat shape.dkvp
mlr --tsv cat shape.tsv
Mlr.inDir(workingPath).csv().cat().file("shape.csv").run();
Mlr.inDir(workingPath).jsonFlag().cat().file("shape.json").run();
Mlr.inDir(workingPath).idkvp().ocsv().cat().file("shape.dkvp").run();
Mlr.inDir(workingPath).tsv().cat().file("shape.tsv").run();

head / tail 옵션

mlr --csv head -n 4 example.csv
Mlr.inDir(workingPath)
	.csv()
	.head(4)
	.file("example.csv")
	.run();
mlr --csv tail -n 4 example.csv
Mlr.inDir(workingPath)
	.csv()
	.tail(4)
	.file("example.csv")
	.run();

sort, cut

mlr --icsv --opprint sort -f shape -nr index example.csv
Mlr.inDir(workingPath)
	.icsv()
	.opprint()
	.sort(f("shape"), nr("index"))
	.file("example.csv")
	.run();
mlr --icsv --opprint cut -o -f flag,shape example.csv
Mlr.inDir(workingPath)
	.icsv()
	.opprint()
	.cut(
		option(flag("-o")),
		option(flag("-f").objective("flag,shape")))
	.file("example.csv")
	.run();

filter / put (DSL은 문자열 그대로)

mlr --icsv --opprint filter '$color == "red"' example.csv
Mlr.inDir(workingPath)
	.icsv()
	.opprint()
	.filterVerb(objective("$color == \"red\""))
	.file("example.csv")
	.run();
mlr --icsv --opprint put '$[[3]] = "NEW"' example.csv
Mlr.inDir(workingPath)
	.icsv()
	.opprint()
	.put(objective("$[[3]] = \"NEW\""))
	.file("example.csv")
	.run();

공백이 있는 필드 이름 (-nr 값은 셸에서 따옴표로 감싸지 않아도 하나의 토큰입니다)

mlr --csv cat spaces.csv
Mlr.withCsvPreset()
	.workDir(workingPath)
	.cat()
	.file("spaces.csv")
	.run();
mlr --c2p sort -nr 'Total MWh' spaces.csv
Mlr.inDir(workingPath)
	.c2p()
	.sort(nr("Total MWh"))
	.file("spaces.csv")
	.run();
mlr --c2p put '${Total KWh} = ${Total MWh} * 1000' spaces.csv
Mlr.inDir(workingPath)
	.c2p()
	.put(objective("${Total KWh} = ${Total MWh} * 1000"))
	.file("spaces.csv")
	.run();

여러 입력 파일

mlr --csv cat data/a.csv data/b.csv
Mlr.inDir(workingPath)
	.csv()
	.cat()
	.file("data/a.csv")
	.file("data/b.csv")
	.run();

then으로 동사 연결

mlr --icsv --opprint sort -nr index then head -n 3 example.csv
Mlr.inDir(workingPath)
	.icsv()
	.opprint()
	.sort(nr("index"))
	.head(3)
	.file("example.csv")
	.run();

셸 파이프(mlr … | mlr …)는 자바에서는 Mlr를 두 번 호출하는 식으로 옮기면 됩니다.

mlr --csv sort -nr index example.csv | mlr --icsv --opprint head -n 3
import java.nio.file.Files;
import java.nio.file.Path;

Path tmp = Files.createTempDirectory("mlr-pipe");
Path sorted = tmp.resolve("sorted.csv");
Mlr.inDir(workingPath)
	.csv()
	.sort(nr("index"))
	.file("example.csv")
	.redirectOutputFile(sorted.toFile())
	.run();

String pprintTop = Mlr.inDir(tmp.toString())
	.icsv()
	.opprint()
	.head(3)
	.file(sorted.getFileName().toString())
	.run();

--from

mlr --icsv --opprint --from example.csv sort -nr index then head -n 3
Mlr.inDir(workingPath)
	.icsv()
	.opprint()
	.from("example.csv")
	.sort(nr("index"))
	.head(3)
	.run();

--mfrom / --mload (--가 가변 인자 뒤에 오는 경우)

mlr --csv --mfrom a.csv b.csv -- cat
Mlr.inDir(workingPath)
	.csv()
	.mfrom("a.csv", "b.csv")
	.cat()
	.run();

stats1처럼 옵션이 많은 동사

mlr --icsv --opprint --from example.csv stats1 -a count,min,mean,max -f quantity -g shape
Mlr.inDir(workingPath)
	.icsv()
	.opprint()
	.from("example.csv")
	.stats1(
		flag("-a").objective("count,min,mean,max"),
		flag("-f").objective("quantity"),
		flag("-g").objective("shape"))
	.run();

JSON 입력·출력

mlr --ijson --ocsv cat example.json
Mlr.inDir(workingPath)
	.ijson()
	.ocsv()
	.cat()
	.file("example.json")
	.run();

제자리 갱신 -I

mlr -I --csv sort -f shape newfile.txt
Mlr.inDir(tmpDir)
	.inPlace()
	.csv()
	.sort(f("shape"))
	.file("newfile.txt")
	.run();

튜토리얼의 put 예제에 나오는 $y2는 문서 오타일 가능성이 있습니다. Miller에서는 제곱을 $y**2로 쓰는 편이 맞습니다.


추가 예제

README의 예제입니다.

import static net.shed.mlrbinder.SortFlags.n;
import static net.shed.mlrbinder.SortFlags.nr;

import net.shed.mlrbinder.Mlr;

// 전역 플래그는 체인에, 동사는 이름 그대로
String runResult = Mlr.inDir(workingPath)
	.csv()
	.sort(n("a"), nr("b"))
	.file("example.csv")
	.run();

// 처음부터 CSV를 전제로 시작할 때
String runResult2 = Mlr.withCsvPreset()
	.workDir(workingPath)
	.sort(n("a"), nr("b"))
	.file("example.csv")
	.run();

짧게만 정리하면, 전역 플래그는 체인에 붙이고 동사는 Miller와 같은 이름의 메서드로 부르면 됩니다. filter / split만 예외입니다.

import static net.shed.mlrbinder.SortFlags.n;
import static net.shed.mlrbinder.SortFlags.nr;

String runResult = Mlr.withCsvPreset()
	.workDir(workingPath)
	.sort(n("a"), nr("b"))
	.file(new File("example.csv"))
	.run();

// 튜토리얼과 같은 흐름
String top3 = Mlr.inDir(workingPath)
	.icsv()
	.opprint()
	.sort(nr("index"))
	.head(3)
	.file("example.csv")
	.run();

filtersplit은 체인에서는 .filterVerb(…), **.splitVerb(…)**를 씁니다.

import static net.shed.mlrbinder.Objective.objective;

Mlr.inDir(workingPath)
	.csv()
	.filterVerb(objective("$index > 1"))
	.file("example.csv")
	.run();

file(File)은 작업 디렉터리가 비어 있으면 자동으로 작업 디렉터리를 채웁니다.


마무리

JVM에서 Miller를 그대로 쓰면서 호출 코드만 정리하고 싶으시다면, mlr-binder를 한 번 살펴봐 주시면 좋겠습니다.