Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clap v4 #61

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
753 changes: 557 additions & 196 deletions Cargo.lock

Large diffs are not rendered by default.

17 changes: 12 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
[package]
authors = ["Vincent Prouillet <hello@vincentprouillet.com>"]
description = "A simple way to get started with a project by scaffolding from a template powered by the Tera engine"
description = "A simple way to get started with a project by scaffolding from a template powered by the tera engine"
edition = "2018"
keywords = ["tera", "scaffolding", "templating", "generator", "boilerplate"]
license = "MIT"
name = "kickstart"
keywords = [
"rust",
"tera",
"scaffolding",
"templating",
"generator",
"boilerplate",
]
version = "0.3.0"

[dependencies]
clap = "2"
clap = { version = "4", features = ["env", "unicode", "cargo"] }
glob = "0.3"
memchr = "2"
regex = "1"
serde = {version = "1", features = ["derive"]}
serde = { version = "1", features = ["derive"] }
tera = "1"
term = "0.7"
toml = "0.5"
Expand All @@ -22,4 +29,4 @@ walkdir = "2"
tempfile = "3"

[badges]
maintenance = {status = "actively-developed"}
maintenance = { status = "actively-developed" }
57 changes: 29 additions & 28 deletions src/bin/kickstart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,53 @@ use std::env;
use std::error::Error;
use std::path::Path;

use clap::{crate_authors, crate_description, crate_version, App, AppSettings, Arg, SubCommand};
use clap::{arg, command, Command};

use kickstart::generation::Template;
use kickstart::terminal;
use kickstart::validate::validate_file;

pub fn build_cli() -> App<'static, 'static> {
App::new("kickstart")
.version(crate_version!())
.author(crate_authors!())
.about(crate_description!())
.setting(AppSettings::SubcommandsNegateReqs)
pub fn build_cli() -> Command {
command!()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we switch to using the struct way instead of the macro?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, there are also a few other issues I am addressing.

.arg(arg!("kickstart"))
// .about(crate_description!())
// .setting(AppSettings::SubcommandsNegateReqs)
.arg(
Arg::with_name("template")
arg!([name] "template")
.required(true)
.help("Template to use: a local path or a HTTP url pointing to a Git repository"),
)
.arg(
Arg::with_name("output-dir")
.short("o")
arg!([name] "output-dir")
.short('o')
.long("output-dir")
.takes_value(true)
.num_args(1)
.help("Where to output the project: defaults to the current directory"),
)
.arg(
Arg::with_name("sub-dir")
.short("s")
arg!([name] "sub-dir")
.short('s')
.long("sub-dir")
.takes_value(true)
.num_args(1)
.help("A subdirectory of the chosen template to use, to allow nested templates."),
)
.arg(
Arg::with_name("no-input")
arg!([name] "no-input")
.long("no-input")
.help("Do not prompt for parameters and only use the defaults from template.toml"),
)
.subcommands(vec![SubCommand::with_name("validate")
.about("Validates that a template.toml is valid")
.arg(Arg::with_name("path").required(true).help("The path to the template.toml"))])
.subcommand(
Command::new("validate")
.about("Validates that a template.toml is valid")
.arg(arg!([name] "path").required(true).help("The path to the template.toml")),
)
}

fn bail(e: &dyn Error) -> ! {
terminal::error(&format!("Error: {}", e));
terminal::error(&format!("Error: {e}"));
let mut cause = e.source();
while let Some(e) = cause {
terminal::error(&format!("Reason: {}", e));
terminal::error(&format!("Reason: {e}"));
cause = e.source();
}
::std::process::exit(1)
Expand All @@ -57,16 +58,16 @@ fn main() {
let matches = build_cli().get_matches();

match matches.subcommand() {
("validate", Some(matches)) => {
let errs = match validate_file(matches.value_of("path").unwrap()) {
Some(("validate", sub_matches)) => {
let errs = match validate_file(sub_matches.get_one::<String>("path").unwrap()) {
Ok(e) => e,
Err(e) => bail(&e),
};

if !errs.is_empty() {
terminal::error("The template.toml is invalid:\n");
for err in errs {
terminal::error(&format!("- {}\n", err));
terminal::error(&format!("- {err}\n"));
}
::std::process::exit(1);
} else {
Expand All @@ -75,15 +76,15 @@ fn main() {
}
_ => {
// The actual generation call
let template_path = matches.value_of("template").unwrap();
let template_path = matches.get_one::<String>("template").unwrap();
let output_dir = matches
.value_of("output-dir")
.get_one::<String>("output-dir")
.map(|p| Path::new(p).to_path_buf())
.unwrap_or_else(|| env::current_dir().unwrap());
let no_input = matches.is_present("no-input");
let sub_dir = matches.value_of("sub-dir");
let no_input = matches.get_one::<String>("no-input").is_some();
let sub_dir = matches.get_one::<String>("sub-dir");

let template = match Template::from_input(template_path, sub_dir) {
let template = match Template::from_input(template_path, sub_dir.map(|x| &**x)) {
Ok(t) => t,
Err(e) => bail(&e),
};
Expand Down
17 changes: 7 additions & 10 deletions src/definition.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
use std::collections::{HashMap};

use regex::{Regex, Match};
use serde::Deserialize;
use tera::{Context};
use std::collections::HashMap;
use tera::Context;
use toml::Value;

use crate::errors::{new_error, ErrorKind, Result};
use crate::prompt::{ask_bool, ask_choices, ask_integer, ask_string};
use crate::utils::{render_one_off_template};
use crate::utils::render_one_off_template;

/// A condition for a question to be asked
#[derive(Debug, Clone, PartialEq, Deserialize)]
Expand Down Expand Up @@ -122,10 +120,10 @@ impl TemplateDefinition {
context.insert(key, val);
}

let rendered_default = render_one_off_template(&s, &context, None);
let rendered_default = render_one_off_template(s, &context, None);
match rendered_default {
Err(e) => return Err(e),
Ok(v ) => v,
Ok(v) => v,
}
} else {
s.clone()
Expand Down Expand Up @@ -155,7 +153,6 @@ impl TemplateDefinition {

#[cfg(test)]
mod tests {
use toml;

use super::*;

Expand Down Expand Up @@ -303,7 +300,7 @@ mod tests {
assert_eq!(tpl.variables.len(), 3);

let res = tpl.ask_questions(true);

assert!(res.is_ok());
let res = res.unwrap();

Expand All @@ -314,6 +311,6 @@ mod tests {
let got_value = res.get("manifest").unwrap();
let expected_value: String = String::from("my_project-other_project-manifest.md");

assert_eq!(got_value, &Value::String(expected_value))
assert_eq!(got_value, &Value::String(expected_value))
}
}
10 changes: 5 additions & 5 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,16 @@ impl StdError for Error {
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.kind {
ErrorKind::Io { ref err, ref path } => write!(f, "{}: {:?}", err, path),
ErrorKind::Io { ref err, ref path } => write!(f, "{err}: {path:?}"),
ErrorKind::Tera { ref err, ref path } => {
if let Some(p) = path {
write!(f, "{}: {:?}", err, p)
write!(f, "{err}: {p:?}")
} else {
write!(f, "{}: rendering a one-off template", err)
write!(f, "{err}: rendering a one-off template")
}
}
ErrorKind::Git { ref err } => write!(f, "Could not clone the repository: {}", err),
ErrorKind::Toml { ref err } => write!(f, "Invalid TOML: {}", err),
ErrorKind::Git { ref err } => write!(f, "Could not clone the repository: {err}"),
ErrorKind::Toml { ref err } => write!(f, "Invalid TOML: {err}"),
ErrorKind::MissingTemplateDefinition => write!(f, "The template.toml is missing"),
ErrorKind::UnreadableStdin => write!(f, "Unable to read from stdin"),
ErrorKind::InvalidTemplate => write!(f, "The template.toml is invalid"),
Expand Down
53 changes: 34 additions & 19 deletions src/generation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ use std::process::Command;
use std::str;

use glob::Pattern;
use tera::{Context};
use tera::{Context, Tera};
use walkdir::WalkDir;

use crate::definition::TemplateDefinition;
use crate::errors::{map_io_err, new_error, ErrorKind, Result};
use crate::utils::{render_one_off_template, create_directory, get_source, is_binary, read_file, write_file, Source};
use crate::utils::{
create_directory, get_source, is_binary, read_file, render_one_off_template, write_file, Source,
};

/// The current template being generated
#[derive(Debug, PartialEq)]
Expand All @@ -36,7 +38,7 @@ impl Template {
pub fn from_git(remote: &str, sub_dir: Option<&str>) -> Result<Template> {
// Clone the remote in git first in /tmp
let mut tmp = env::temp_dir();
println!("Tmp dir: {:?}", tmp);
println!("Tmp dir: {tmp:?}");
tmp.push(remote.split('/').last().unwrap_or("kickstart"));
if tmp.exists() {
fs::remove_dir_all(&tmp)?;
Expand All @@ -46,7 +48,7 @@ impl Template {
// on some platforms:
// https://www.reddit.com/r/rust/comments/92mbk5/kickstart_a_scaffolding_tool_to_get_new_projects/e3ahegw
Command::new("git")
.args(&["clone", "--recurse-submodules", remote, &format!("{}", tmp.display())])
.args(["clone", "--recurse-submodules", remote, &format!("{}", tmp.display())])
.output()
.map_err(|err| new_error(ErrorKind::Git { err }))?;
Ok(Template::from_local(&tmp, sub_dir))
Expand All @@ -60,6 +62,19 @@ impl Template {
Template { path: buf }
}

fn _render_template(
&self,
content: &str,
context: &Context,
path: Option<PathBuf>,
) -> Result<String> {
let mut tera = Tera::default();

tera.add_raw_template("one_off", content)
.and_then(|_| tera.render("one_off", context))
.map_err(|err| new_error(ErrorKind::Tera { err, path }))
}

/// Generate the template at the given output directory
pub fn generate(&self, output_dir: &Path, no_input: bool) -> Result<()> {
// Get the variables from the user first
Expand All @@ -78,16 +93,16 @@ impl Template {
}

if !output_dir.exists() {
println!("Creating {:?}", output_dir);
create_directory(&output_dir)?;
println!("Creating {output_dir:?}");
create_directory(output_dir)?;
}

// Create the glob patterns of files to copy without rendering first, only once
let patterns: Vec<Pattern> =
definition.copy_without_render.iter().map(|s| Pattern::new(s).unwrap()).collect();

let start_path = if let Some(ref directory) = definition.directory {
self.path.join(&directory)
self.path.join(directory)
} else {
self.path.clone()
};
Expand Down Expand Up @@ -134,19 +149,19 @@ impl Template {
}

// Only pass non-binary files or the files not matching the copy_without_render patterns through Tera
let mut f = File::open(&entry.path())?;
let mut f = File::open(entry.path())?;
let mut buffer = Vec::new();
f.read_to_end(&mut buffer)?;

let no_render = patterns.iter().map(|p| p.matches_path(&real_path)).any(|x| x);

if no_render || is_binary(&buffer) {
map_io_err(fs::copy(&entry.path(), &real_path), entry.path())?;
map_io_err(fs::copy(entry.path(), &real_path), entry.path())?;
continue;
}

let contents = render_one_off_template(
&str::from_utf8(&buffer).unwrap(),
str::from_utf8(&buffer).unwrap(),
&context,
Some(entry.path().to_path_buf()),
)?;
Expand All @@ -158,7 +173,7 @@ impl Template {
if let Some(val) = variables.get(&cleanup.name) {
if *val == cleanup.value {
for p in &cleanup.paths {
let actual_path = render_one_off_template(&p, &context, None)?;
let actual_path = render_one_off_template(p, &context, None)?;
let path_to_delete = output_dir.join(actual_path);
if !path_to_delete.exists() {
continue;
Expand Down Expand Up @@ -187,7 +202,7 @@ mod tests {
fn can_generate_from_local_path() {
let dir = tempdir().unwrap();
let tpl = Template::from_input("examples/complex", None).unwrap();
let res = tpl.generate(&dir.path().to_path_buf(), true);
let res = tpl.generate(dir.path(), true);
assert!(res.is_ok());
assert!(!dir.path().join("some-project").join("template.toml").exists());
assert!(dir.path().join("some-project").join("logo.png").exists());
Expand All @@ -197,7 +212,7 @@ mod tests {
fn can_generate_from_local_path_with_directory() {
let dir = tempdir().unwrap();
let tpl = Template::from_input("examples/with-directory", None).unwrap();
let res = tpl.generate(&dir.path().to_path_buf(), true);
let res = tpl.generate(dir.path(), true);
assert!(res.is_ok());
assert!(dir.path().join("Hello").join("Howdy.py").exists());
}
Expand All @@ -206,7 +221,7 @@ mod tests {
fn can_generate_from_local_path_with_subdir() {
let dir = tempdir().unwrap();
let tpl = Template::from_input("./", Some("examples/complex")).unwrap();
let res = tpl.generate(&dir.path().to_path_buf(), true);
let res = tpl.generate(dir.path(), true);
assert!(res.is_ok());
assert!(!dir.path().join("some-project").join("template.toml").exists());
assert!(dir.path().join("some-project").join("logo.png").exists());
Expand All @@ -216,8 +231,8 @@ mod tests {
fn can_generate_from_remote_repo() {
let dir = tempdir().unwrap();
let tpl = Template::from_input("https://github.com/Keats/rust-cli-template", None).unwrap();
let res = tpl.generate(&dir.path().to_path_buf(), true);
println!("{:?}", res);
let res = tpl.generate(dir.path(), true);
println!("{res:?}");
assert!(res.is_ok());
assert!(!dir.path().join("My-CLI").join("template.toml").exists());
assert!(dir.path().join("My-CLI").join(".travis.yml").exists());
Expand All @@ -229,8 +244,8 @@ mod tests {
let tpl =
Template::from_input("https://github.com/Keats/kickstart", Some("examples/complex"))
.unwrap();
let res = tpl.generate(&dir.path().to_path_buf(), true);
println!("{:?}", res);
let res = tpl.generate(dir.path(), true);
println!("{res:?}");
assert!(res.is_ok());
assert!(!dir.path().join("some-project").join("template.toml").exists());
assert!(dir.path().join("some-project").join("logo.png").exists());
Expand All @@ -240,7 +255,7 @@ mod tests {
fn can_generate_handling_slugify() {
let dir = tempdir().unwrap();
let tpl = Template::from_input("examples/slugify", None).unwrap();
let res = tpl.generate(&dir.path().to_path_buf(), true);
let res = tpl.generate(dir.path(), true);
assert!(res.is_ok());
assert!(!dir.path().join("template.toml").exists());
assert!(dir.path().join("hello.md").exists());
Expand Down
Loading