Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

JShellでフォルダ出力するワンライナーを書いてみた。

概要

こんにちは、最近JShellに嵌っているuedaです。
この記事はワンライナー自慢大会 Advent Calendar 2018 - Qiitaの19日目です。
JavaのJShellを使ってテキストに書かれたとおりの構成のフォルダを出力する
ワンライナーを紹介します。

昔はJavaワンライナーなんて考えもしませんでしたが、
Java9から使えるようになったJShell(REPL)で対話的に実行する事で、
ワンライナーっぽく書ける様になりました。
中々使う機会が無いので、勉強がてらワンライナーを作成しました。

f:id:acro-engineer:20181218044827j:plain:w200
duke

環境

Java: openjdk version "11.0.1" 2018-10-16
OS: Windows10 Home

※本稿のワンライナーはJava11のAPIを使用しているため、Java10以前では動きません。

JShellを使ったJavaコマンド実行への道

Java11のダウンロード

JDKはいくつか種類がありますが、今回はAdoptOpenJDKを使いました。
adoptopenjdk.net

パスを通す

展開したAdoptOpenJDKのbinにパスを通し、「JShell」をコマンドプロンプトから実行できるようにします。

コマンドプロンプトからJShellを経由してのJava実行

> echo System.out.plintln("Hello.") | JShell -

このように「echo」+「生のJavaコード」+パイプ~JShell~ハイフンでJavaコードの単発実行ができます。
クラスやmainメソッドを書かなくて済む事を考えると、大分楽です。

注意点として、コマンドプロンプト実行ではechoコマンドが「>」を評価してしまうため、「^」(キャレット)でエスケープする必要があります。
例えば以下のような記述になります。

例 (i -> i * 10) → (i -^^^> i * 10)

本題(フォルダ出力ワンライナー)

さて、本題のワンライナーについて。
大量のディレクトリを作んなきゃ、て事はたまにありますよね。
そんな時、さくっと生成したいと考え、
テキストに書いたとおりにフォルダを出力するワンライナーを書きました。

input.txt

Excelからぺっと貼り付けたタブインデント付のテキストです。
このテキストの構成どおりのフォルダ作成を考えます。

test
	test1
		1-1
		1-2
	test2
		1-1
		1-2
		2-1
		3-1
			images
			procedure
		3-2
			images
			procedure
		3-3
			images
			procedure

本稿の主役のワンライナー

上記input.txtと同じフォルダで下記ワンライナーを実行すると、テキストに記載された構成通りのフォルダが生成されます。

>echo Stream.of(new LinkedHashMap^^^<Integer, String^^^>()).forEach(map -^^^> {try { Files.readAllLines(Path.of("input.txt")).stream().peek(k -^^^> map.put(k.length() - k.stripLeading().length(), k.strip())).map(s -^^^> s.length() - s.stripLeading().length() == 0 ? s : String.join("/", map.values().stream().limit(s.length() - s.stripLeading().length()).collect(Collectors.toList())) + "/" + s.strip()).peek(System.out::println).forEach(r -^^^> {try {Files.createDirectory(Path.of(r));} catch (IOException e) {throw new UncheckedIOException(e);}});} catch (IOException e) {throw new UncheckedIOException(e);}}); | JShell -

解説

ワンライナーのままだと読みづらいので、展開してみます。

Stream.of(new LinkedHashMap<Integer, String>()).forEach(map -> {
	try {
		Files.readAllLines(Path.of("input.txt")).stream()
				.peek(k -> map.put(k.length() - k.stripLeading().length(), k.strip()))
				.map(s -> s.length() - s.stripLeading().length() == 0 ? s
						: String.join("/", map.values().stream().limit(s.length() - s.stripLeading().length())
								.collect(Collectors.toList())) + "/" + s.strip())
				.peek(System.out::println)
				.forEach(r -> {
					try {
						Files.createDirectory(Path.of(r));
					} catch (IOException e) {
						throw new UncheckedIOException(e);
					}
				});
	} catch (IOException e) {
		throw new UncheckedIOException(e);
	}
});
最初のStreamは、処理中で使用するLinkedHashMap生成のためだけに行います。
Stream.of(new LinkedHashMap<Integer, String>()).forEach(map -> 以降実際の出力処理
ファイルを読み込み、ファイル1行ごとの文字列に対しストリーム処理を行います。
Files.readAllLines(Path.of("input.txt")).stream()
peekはストリームに影響を与えない処理を行うため、ここでMapにファイル内容を格納しています。

 Mapはキーがタブインデントの数、バリューがフォルダ名になるよう加工しています。

.peek(k -> map.put(k.length() - k.stripLeading().length(), k.strip()))
相対パスの生成を行っています。

 タブ数が0ならそのまま、0以外なら全親フォルダのパス+自分のフォルダ名がストリームに流れます。

.map(s -> s.length() - s.stripLeading().length() == 0 ? s : String.join("/", map.values().stream().limit(s.length() - s.stripLeading().length()).collect(Collectors.toList())) + "/" + s.strip()).
生成された相対パスのDEBUG出力です。
.peek(System.out::println)
生成された相対パスに対し順次フォルダ生成を行います。
forEach(r -> {try {Files.createDirectory(Path.of(r));}


書いてみて、今回の処理は流れをさかのぼる処理があったことから、
最初のmap生成など強引な処理になっているところがあります。
この辺りワンライナーに向いていなかったのではないかと後になって気がつきました。

ちなみに

Streamを使わずに、展開して記述すると以下のようになります。

	var map = new LinkedHashMap<Integer, String>();
	var resultPathList = new ArrayList<String>();
	List<String> lines = null;
	lines = Files.readAllLines(Path.of("input.txt"));

	for (String line : lines) {
		String pathName = "";
		int tabCount = line.length() - line.stripLeading().length();
		line = line.strip();
		map.put(tabCount, line);
		if (tabCount == 0) {
			pathName = line;
		} else {
			for (int index = 0; index < tabCount; index++) {
				pathName += map.get(index) + "/";
			}
			pathName += line;
		}
		resultPathList.add(pathName);
	}

	for (String resultPath : resultPathList) {
		System.out.println(resultPath);
		Files.createDirectory(Path.of(resultPath));
	}

24Lineぐらいありますね。是に比べるとStreamを使ったコードはスッキリしています。
こちらのほうが読みやすいですが。。

まとめ

以上、JShellで書いたワンライナー、いかがでしたでしょうか。
Javaはあまり向いていないとは思いますが、
ストリームを書く練習にはちょうどよいのではないかと思います。

ではでは。

Acroquest Technologyでは、キャリア採用を行っています。

  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

世界初のElastic認定エンジニアと一緒に働きたい人Wanted! - Acroquest Technology株式会社のエンジニア中途・インターンシップの求人 - Wantedlywww.wantedly.com