use anyhow::{Context, Result};
use clap::{Arg, ArgAction, Command};
use colored::*;
use std::fs;
use std::io::{self, Write};
use std::path::Path;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
fn main() -> Result<()> {
let matches = Command::new("mkdir")
.version(env!("CARGO_PKG_VERSION"))
.author(env!("CARGO_PKG_AUTHORS"))
.about(env!("CARGO_PKG_DESCRIPTION"))
.arg(
Arg::new("directories")
.help("Directories to create")
.required(true)
.num_args(1..)
.value_name("DIRECTORY"),
)
.arg(
Arg::new("parents")
.short('p')
.long("parents")
.help("Create parent directories as needed")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("mode")
.short('m')
.long("mode")
.help("Set file mode (permissions) for created directories")
.value_name("MODE"),
)
.arg(
Arg::new("verbose")
.short('v')
.long("verbose")
.help("Print a message for each created directory")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("interactive")
.short('i')
.long("interactive")
.help("Prompt before creating each directory")
.action(ArgAction::SetTrue),
)
.get_matches();
let directories: Vec<&str> = matches.get_many::<String>("directories")
.unwrap()
.map(|s| s.as_str())
.collect();
let parents = matches.get_flag("parents");
let verbose = matches.get_flag("verbose");
let interactive = matches.get_flag("interactive");
let mode = matches.get_one::<String>("mode");
for dir in directories {
if let Err(e) = create_directory(dir, parents, verbose, interactive, mode) {
eprintln!("{}: {}", "Error".red().bold(), e);
std::process::exit(1);
}
}
Ok(())
}
fn create_directory(
path: &str,
parents: bool,
verbose: bool,
interactive: bool,
mode: Option<&String>,
) -> Result<()> {
let path = Path::new(path);
if path.exists() {
if verbose {
println!("{}: directory '{}' already exists",
"Info".yellow().bold(),
path.display()
);
}
return Ok(());
}
if interactive {
print!("Create directory '{}'? [y/N]: ", path.display());
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
if !input.trim().to_lowercase().starts_with('y') {
if verbose {
println!("{}: skipped '{}'", "Info".yellow().bold(), path.display());
}
return Ok(());
}
}
if parents {
fs::create_dir_all(path)
.with_context(|| format!("Failed to create directory '{}'", path.display()))?;
} else {
fs::create_dir(path)
.with_context(|| format!("Failed to create directory '{}'", path.display()))?;
}
if let Some(mode_str) = mode {
#[cfg(unix)]
{
set_permissions(path, mode_str)?;
}
#[cfg(not(unix))]
{
let _ = mode_str; if verbose {
println!("{}: permission setting not supported on this platform",
"Warning".yellow().bold());
}
}
}
if verbose {
println!("{}: created directory '{}'",
"Success".green().bold(),
path.display()
);
}
Ok(())
}
#[cfg(unix)]
fn set_permissions(path: &Path, mode_str: &str) -> Result<()> {
let mode = parse_mode(mode_str)
.with_context(|| format!("Invalid mode: {}", mode_str))?;
let metadata = fs::metadata(path)?;
let mut permissions = metadata.permissions();
permissions.set_mode(mode);
fs::set_permissions(path, permissions)?;
Ok(())
}
#[cfg(unix)]
fn parse_mode(mode_str: &str) -> Result<u32> {
if mode_str.len() == 3 && mode_str.chars().all(|c| c.is_ascii_digit()) {
u32::from_str_radix(mode_str, 8)
.with_context(|| "Invalid octal mode")
} else {
anyhow::bail!("Mode must be a 3-digit octal number (e.g., 755)")
}
}