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
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
// Use jemalloc for better memory allocation performance on Unix-like systems
#[cfg(not(target_env = "msvc"))]
#[global_allocator]
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
// Use mimalloc on Windows for better performance
#[cfg(target_env = "msvc")]
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
mod cli_types;
pub use cli_types::{CheckArgs, FailOn, FixMode};
mod cli_utils;
pub use cli_utils::{apply_cli_overrides, load_config_with_cli_error_handling_with_dir, read_file_efficiently};
mod commands;
use clap::{Parser, Subcommand, ValueEnum};
use clap_complete::shells::Shell;
use core::error::Error;
use rumdl_lib::exit_codes::exit;
mod cache;
mod check_runner;
mod file_processor;
mod formatter;
mod resolution;
mod stdin_processor;
mod watch;
#[derive(Parser)]
#[command(author, version, about, long_about = None, arg_required_else_help = true)]
struct Cli {
#[command(subcommand)]
command: Commands,
/// Control colored output
#[arg(long, global = true, default_value_t, value_enum)]
color: Color,
/// Path to configuration file
#[arg(
long,
global = true,
help = "Path to configuration file",
conflicts_with_all = ["no_config", "isolated"]
)]
config: Option<String>,
/// Ignore all configuration files and use built-in defaults
#[arg(
long,
global = true,
help = "Ignore all configuration files and use built-in defaults"
)]
no_config: bool,
/// Ignore all configuration files (alias for --no-config, Ruff-compatible)
#[arg(
long,
global = true,
help = "Ignore all configuration files (alias for --no-config)",
conflicts_with = "no_config"
)]
isolated: bool,
}
#[derive(Subcommand)]
pub enum SchemaAction {
/// Generate/update the JSON schema file
Generate,
/// Check if the schema is up-to-date
Check,
/// Print the schema to stdout
Print,
}
#[derive(Subcommand)]
enum Commands {
/// Lint Markdown files and print warnings/errors
Check(CheckArgs),
/// Format Markdown files (alias for check --fix)
Fmt(CheckArgs),
/// Initialize a new configuration file
Init {
/// Generate configuration for pyproject.toml instead of .rumdl.toml
#[arg(long, conflicts_with = "output")]
pyproject: bool,
/// Use a style preset (default, google, relaxed)
#[arg(long, value_enum)]
preset: Option<Preset>,
/// Output file path (default: .rumdl.toml)
#[arg(long, short = 'o')]
output: Option<String>,
},
/// Show information about a rule or list all rules
Rule {
/// Rule name or ID (optional, omit to list all rules)
rule: Option<String>,
/// Output format
#[arg(long, short = 'o', value_name = "FORMAT", default_value_t, value_enum)]
output_format: commands::rule::OutputFormat,
/// Filter to only fixable rules
#[arg(long, short = 'f')]
fixable: bool,
/// Filter by category (use --list-categories to see options)
#[arg(long, short = 'c', value_name = "CATEGORY")]
category: Option<String>,
/// Include full documentation in output (for json/json-lines)
#[arg(long)]
explain: bool,
/// List available categories and exit
#[arg(long)]
list_categories: bool,
},
/// Explain a rule with detailed information and examples
Explain {
/// Rule name or ID to explain
rule: String,
},
/// Show configuration or query a specific key
Config {
#[command(subcommand)]
subcmd: Option<ConfigSubcommand>,
/// Show only the default configuration values
#[arg(long, help = "Show only the default configuration values")]
defaults: bool,
/// Show only non-default configuration values (exclude defaults)
#[arg(long, help = "Show only non-default configuration values (exclude defaults)")]
no_defaults: bool,
#[arg(long, help = "Output format (e.g. toml, json)")]
output: Option<String>,
},
/// Start the Language Server Protocol server
Server {
/// TCP port to listen on (for debugging)
#[arg(long)]
port: Option<u16>,
/// Use stdio for communication (default)
#[arg(long)]
stdio: bool,
/// Enable verbose logging
#[arg(short, long)]
verbose: bool,
/// Path to rumdl configuration file
#[arg(short, long)]
config: Option<String>,
},
/// Generate or check JSON schema for rumdl.toml
Schema {
#[command(subcommand)]
action: SchemaAction,
},
/// Import and convert markdownlint configuration files
Import {
/// Path to markdownlint config file (JSON/YAML)
file: String,
/// Output file path (default: .rumdl.toml)
#[arg(short, long)]
output: Option<String>,
/// Output format
#[arg(long, default_value_t, value_enum)]
format: commands::import::Format,
/// Show converted config without writing to file
#[arg(long)]
dry_run: bool,
},
/// Install the rumdl VS Code extension
Vscode {
/// Force reinstall the current version even if already installed
#[arg(long)]
force: bool,
/// Update to the latest version (only if newer version available)
#[arg(long)]
update: bool,
/// Show installation status without installing
#[arg(long)]
status: bool,
},
/// Generate shell completion scripts
Completions {
/// Shell to generate completions for (detected from $SHELL if omitted)
shell: Option<Shell>,
/// List available shells
#[arg(long, short = 'l')]
list: bool,
},
/// Clear the cache
Clean,
/// Show version information
Version,
}
#[derive(Subcommand, Debug)]
pub enum ConfigSubcommand {
/// Query a specific config key (e.g. global.exclude or MD013.line_length)
Get { key: String },
/// Show the absolute path of the configuration file that was loaded
File,
}
#[derive(Clone, ValueEnum)]
enum Preset {
/// Default rumdl configuration
Default,
/// Google developer documentation style
Google,
/// Relaxed rules for existing projects
Relaxed,
}
#[derive(Clone, Default, ValueEnum)]
enum Color {
#[default]
Auto,
Always,
Never,
}
fn main() -> Result<(), Box<dyn Error>> {
// Reset SIGPIPE to default behavior on Unix so piping to `head` etc. works correctly.
// Without this, Rust ignores SIGPIPE and `println!` panics on broken pipe.
#[cfg(unix)]
{
// SAFETY: Setting SIGPIPE to SIG_DFL is standard practice for CLI tools
// that produce output meant to be piped. This is safe and idiomatic.
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
}
// Initialize logging from RUST_LOG environment variable
// This allows users to debug config discovery with: RUST_LOG=debug rumdl check ...
env_logger::Builder::from_default_env()
.format_timestamp(None)
.format_target(false)
.init();
let cli = Cli::parse();
// Set color override globally based on --color flag
match cli.color {
Color::Always => colored::control::set_override(true),
Color::Never => colored::control::set_override(false),
Color::Auto => colored::control::unset_override(),
}
// Catch panics and print a message, exit 1
let result = std::panic::catch_unwind(|| {
match cli.command {
Commands::Init {
pyproject,
preset,
output,
} => {
commands::init::handle_init(
pyproject,
preset.map(|p| match p {
Preset::Default => "default",
Preset::Google => "google",
Preset::Relaxed => "relaxed",
}),
output,
);
}
Commands::Check(mut args) => {
args.fix_mode = if args.fix { FixMode::CheckFix } else { FixMode::Check };
args.fail_on_mode = args.fail_on;
let config_path = if cli.no_config || cli.isolated {
None
} else {
cli.config.as_deref()
};
commands::check::run_check(&args, config_path, cli.no_config || cli.isolated);
}
Commands::Fmt(mut args) => {
args.fix_mode = FixMode::Format;
args.fail_on_mode = args.fail_on;
// --check mode enables diff (don't write files) and will exit 1 if changes needed
if args.check {
args.diff = true;
}
let config_path = if cli.no_config || cli.isolated {
None
} else {
cli.config.as_deref()
};
commands::check::run_check(&args, config_path, cli.no_config || cli.isolated);
}
Commands::Rule {
rule,
output_format,
fixable,
category,
explain,
list_categories,
} => {
commands::rule::handle_rule(rule, output_format, fixable, category, explain, list_categories);
}
Commands::Explain { rule } => {
commands::explain::handle_explain(&rule);
}
Commands::Config {
subcmd,
defaults,
no_defaults,
output,
} => {
commands::config::handle_config(
subcmd,
defaults,
no_defaults,
output,
cli.config.as_deref(),
cli.no_config,
cli.isolated,
);
}
Commands::Schema { action } => {
commands::schema::handle_schema(action);
}
Commands::Server {
port,
stdio,
verbose,
config,
} => {
commands::server::handle_server(port, stdio, verbose, config);
}
Commands::Import {
file,
output,
format,
dry_run,
} => {
commands::import::handle_import(file, output, format, dry_run);
}
Commands::Vscode { force, update, status } => {
commands::vscode::handle_vscode(force, update, status);
}
Commands::Completions { shell, list } => {
commands::completions::handle_completions(shell, list);
}
Commands::Clean => {
commands::clean::handle_clean(cli.config.as_deref(), cli.no_config, cli.isolated);
}
Commands::Version => {
commands::version::handle_version();
}
}
});
if let Err(e) = result {
eprintln!("[rumdl panic handler] Uncaught panic: {e:?}");
exit::tool_error();
} else {
Ok(())
}
}