Registry / RepositoryTypeScriptRustPython
Rollpie
ReadmeFiles
Versions
Info
Download
0.1.023.4 KB2026-09-14
Version
0.1.0
Copyright
Rollpie, Irohabook
Publisher
math
Published
2026-09-14
Size
23.4 KB
Downloads
1
Checksum
298d198fbc3fc0aecbe6668651a0d3beefb7ff2d1a098483af80cc924d925bdd
Dependencies
None

main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
//! simple_json_formatter のコマンドの入口。
//!
//! ファイルを指さなければ標準入力を読んで標準出力へ出す。
//! 終了コードは 0 が正常、1 が --check で差分あり、2 がエラー。

use simple_json_formatter::{Indent, Options};
use std::env;
use std::ffi::OsString;
use std::fs::{self, OpenOptions};
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use std::process::{self, ExitCode};


/// -i で受け付ける空白の数の上限。大きな数で出力がふくらみ、メモリを使い切るのを防ぐ
const MAX_INDENT: usize = 64;

/// -w の一時ファイルの名前を作り直す回数の上限
const TEMPORARY_ATTEMPT: usize = 100;


const USAGE: &str = "\
使い方: simple_json_formatter [オプション] [ファイル...]

ファイルを指さなければ、標準入力を読んで標準出力へ出す。

  -i, --indent N     インデントを空白 N 個にする(0 から 64、既定は 2)
  -t, --tab          インデントをタブにする
  -c, --compact      空白を落として 1 行にまとめる
      --max-width N  この桁数に収まる配列とオブジェクトを 1 行にまとめる(0 ならまとめない)
  -s, --sort-keys    オブジェクトのキーを辞書順に並べ替える
  -a, --ascii        ASCII 以外の文字を \\uXXXX にする
  -w, --write        入力ファイルを整形した結果で上書きする
      --check        整形済みかどうかだけを調べる。違えば終了コード 1
  -h, --help         この説明を出す
";


/// 引数を読んだ結果
struct Command {
	options: Options,
	write: bool,
	check: bool,
	files: Vec<PathBuf>,
}


fn main() -> ExitCode {
	let argument: Vec<String> = env::args().skip(1).collect();
	match run(&argument) {
		Ok(code) => ExitCode::from(code),
		Err(reason) => {
			eprintln!("simple_json_formatter: {}", reason);
			ExitCode::from(2)
		}
	}
}


fn run(argument: &[String]) -> Result<u8, String> {
	let Some(command) = read_argument(argument)? else {
		print!("{}", USAGE);
		return Ok(0);
	};

	if command.files.is_empty() {
		let mut source = String::new();
		io::stdin()
			.read_to_string(&mut source)
			.map_err(|error| format!("標準入力を読めません: {}", error))?;
		let formatted = format_text(&source, &command.options).map_err(|error| format!("<stdin>: {}", error))?;
		if command.check {
			return Ok(report_check(&source, &formatted, "<stdin>"));
		}
		print_text(&formatted)?;
		return Ok(0);
	}

	let mut code = 0;
	for path in &command.files {
		let label = path.display().to_string();
		let source = fs::read_to_string(path).map_err(|error| format!("{} を読めません: {}", label, error))?;
		let formatted = format_text(&source, &command.options).map_err(|error| format!("{}: {}", label, error))?;

		if command.check {
			if report_check(&source, &formatted, &label) == 1 {
				code = 1;
			}
		} else if command.write {
			if source != formatted {
				replace_file(path, &formatted).map_err(|error| format!("{} に書き込めません: {}", label, error))?;
				eprintln!("整形しました: {}", label);
			}
		} else {
			print_text(&formatted)?;
		}
	}
	Ok(code)
}


/// 引数を読む。--help のときは None を返す
fn read_argument(argument: &[String]) -> Result<Option<Command>, String> {
	let mut options = Options::default();
	let mut spaces = 2;
	let mut tab = false;
	let mut compact = false;
	let mut write = false;
	let mut check = false;
	let mut files = Vec::new();

	let mut index = 0;
	while index < argument.len() {
		let item = argument[index].as_str();
		match item {
			"-h" | "--help" => return Ok(None),
			"-t" | "--tab" => tab = true,
			"-c" | "--compact" => compact = true,
			"-s" | "--sort-keys" => options.sort_keys = true,
			"-a" | "--ascii" => options.ensure_ascii = true,
			"-w" | "--write" => write = true,
			"--check" => check = true,
			"-i" | "--indent" | "--max-width" => {
				index += 1;
				let Some(text) = argument.get(index) else {
					return Err(format!("{} の後ろに数を置いてください", item));
				};
				let number = text
					.parse()
					.map_err(|_| format!("{} の後ろの {} は数ではありません", item, text))?;
				if item == "--max-width" {
					options.max_width = number;
				} else if number > MAX_INDENT {
					return Err(format!("{} には 0 から {} までの数を置いてください", item, MAX_INDENT));
				} else {
					spaces = number;
				}
			}
			_ if item.starts_with('-') => return Err(format!("{} というオプションはありません", item)),
			_ => files.push(PathBuf::from(item)),
		}
		index += 1;
	}

	if tab && compact {
		return Err("-t と -c は一緒に使えません".to_string());
	}
	if write && files.is_empty() {
		return Err("-w を使うときはファイルを指してください".to_string());
	}
	if write && check {
		return Err("-w と --check は一緒に使えません".to_string());
	}

	options.indent = if compact {
		Indent::Compact
	} else if tab {
		Indent::Tab
	} else {
		Indent::Spaces(spaces)
	};
	Ok(Some(Command { options, write, check, files }))
}


/// 整形して、末尾に改行を 1 つ付ける
fn format_text(source: &str, options: &Options) -> Result<String, simple_json_formatter::Error> {
	let mut text = simple_json_formatter::format(source, options)?;
	text.push('\n');
	Ok(text)
}


/// 同じディレクトリの一時ファイルに書いてから、名前を付け替えて元のファイルと置き換える。
///
/// 書き込みの途中で止まっても、元のファイルは壊れない。権限は元のファイルから引き継ぐ。
/// シンボリックリンクはたどって、リンク先のファイルを置き換える。
/// 失敗したときは、自分で作った一時ファイルを消す。
fn replace_file(path: &Path, text: &str) -> io::Result<()> {
	let target = fs::canonicalize(path)?;
	let permissions = fs::metadata(&target)?.permissions();
	let (Some(directory), Some(name)) = (target.parent(), target.file_name()) else {
		return Err(io::Error::new(io::ErrorKind::InvalidInput, "置き換えるファイルの名前がわかりません"));
	};

	// 同じ名前の一時ファイルがすでにあれば、番号を変えて作り直す
	let mut opened = None;
	for number in 0..TEMPORARY_ATTEMPT {
		let mut temporary_name = OsString::from(".");
		temporary_name.push(name);
		temporary_name.push(format!(".{}.{}.tmp", process::id(), number));
		let temporary = directory.join(temporary_name);
		match OpenOptions::new().write(true).create_new(true).open(&temporary) {
			Ok(file) => {
				opened = Some((temporary, file));
				break;
			}
			Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
			Err(error) => return Err(error),
		}
	}
	let Some((temporary, mut file)) = opened else {
		return Err(io::Error::new(io::ErrorKind::AlreadyExists, "一時ファイルを作れません"));
	};

	// 名前を付け替える前にファイルを閉じる。開いたままだと付け替えられない環境がある
	let written = file.write_all(text.as_bytes()).and_then(|_| file.sync_all());
	drop(file);
	let result = written
		.and_then(|_| fs::set_permissions(&temporary, permissions))
		.and_then(|_| fs::rename(&temporary, &target));
	if result.is_err() {
		let _ = fs::remove_file(&temporary);
	}
	result
}


/// 整形済みなら 0、違えば名前を出して 1 を返す
fn report_check(source: &str, formatted: &str, label: &str) -> u8 {
	if source == formatted {
		0
	} else {
		println!("要整形: {}", label);
		1
	}
}


fn print_text(text: &str) -> Result<(), String> {
	let mut stdout = io::stdout().lock();
	stdout
		.write_all(text.as_bytes())
		.and_then(|_| stdout.flush())
		.map_err(|error| format!("標準出力に書けません: {}", error))
}