Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
11 changes: 0 additions & 11 deletions .ctp.example

This file was deleted.

11 changes: 11 additions & 0 deletions .ctp.toml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Location of template (use absolute path)
[templates]
c = "/Users/willlane/dev/ctp/proj-example/"

# Commands that will be run before project dir
[commands-before]
c = ["echo {{__OUT__}}"]

# Commands that will be run in project dir
[commands-after]
c = ["gcc main.c -o {{__NAME__}}", "./{{__NAME__}}"]
3 changes: 3 additions & 0 deletions proj-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# {{__NAME__}}

## Folder name {{__OUT__}}
6 changes: 6 additions & 0 deletions proj-example/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#include <stdio.h>

int main(void) {
printf("Hello from {{__NAME__}} in the folder {{__OUT__}}");
return 0;
}
10 changes: 4 additions & 6 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ use std::process::Command;
use anyhow::{anyhow, Result};
use thiserror::Error;

const REPLACEABLE_NAME: &str = "{{__NAME__}}";
const REPLACEABLE_OUTPUT: &str = "{{__OUT__}}";

#[derive(Error, Debug)]
pub enum ExecError {
#[error("Cannot execute empty command.")]
Expand All @@ -14,8 +11,8 @@ pub enum ExecError {

pub fn exec(s: &str, proj_name: &str, proj_output: &str) -> Result<(), anyhow::Error> {
let replaced = s
.replace(REPLACEABLE_NAME, proj_name)
.replace(REPLACEABLE_OUTPUT, proj_output);
.replace(crate::REPLACEABLE_NAME, proj_name)
.replace(crate::REPLACEABLE_OUTPUT, proj_output);
let split = replaced.split_ascii_whitespace().collect::<Vec<&str>>();

match split.as_slice() {
Expand All @@ -28,7 +25,8 @@ pub fn exec(s: &str, proj_name: &str, proj_output: &str) -> Result<(), anyhow::E

fn execute_command_with_output(command: &str, args: &[&str]) -> Result<(), anyhow::Error> {
println!("[CMD] {} [{}]", command, &args.to_vec().join(", "));
Command::new(command).args(args).output()?;
let cmd = Command::new(command).args(args).output()?;
println!("[STDOUT] {}", std::str::from_utf8(cmd.stdout.as_slice())?);

Ok(())
}
36 changes: 32 additions & 4 deletions src/directory.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,43 @@
use std::fs;
use std::fs::{self, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::Path;

pub fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> anyhow::Result<()> {
fn replace_name_and_out(s: String, proj_name: &str, proj_out: &str) -> String {
s.replace(crate::REPLACEABLE_NAME, proj_name)
.replace(crate::REPLACEABLE_OUTPUT, proj_out)
}

pub fn copy_dir_all(
src: impl AsRef<Path>,
dst: impl AsRef<Path>,
proj_name: &str,
proj_out: &str,
) -> anyhow::Result<()> {
fs::create_dir_all(&dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let ft = entry.file_type()?;
if ft.is_dir() {
copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
copy_dir_all(
entry.path(),
dst.as_ref().join(entry.file_name()),
proj_name,
proj_out,
)?;
} else {
fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
let output = dst.as_ref().join(entry.file_name());
let mut contents = String::new();
fs::File::open(&entry.path())?.read_to_string(&mut contents)?;
let contents = replace_name_and_out(contents, proj_name, proj_out);
fs::copy(entry.path(), &output)?;

let mut f = OpenOptions::new()
.read(false)
.write(true)
.truncate(true)
.open(output)?;
f.seek(SeekFrom::Start(0))?;
f.write_all(contents.as_bytes())?;
}
}

Expand Down
41 changes: 38 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,53 @@
#![allow(dead_code)]

mod commands;
mod directory;
mod opts;
mod shape;

use std::fs::File;
use std::io::Read;

use anyhow::Result;
use opts::Opts;

#[cfg(test)]
pub mod tests;

pub const REPLACEABLE_NAME: &str = "{{__NAME__}}";
pub const REPLACEABLE_OUTPUT: &str = "{{__OUT__}}";

fn main() -> Result<(), anyhow::Error> {
let opts = Opts::opts()?;
println!("{}", opts.project_name);
let mut config_file = String::new();
File::open(&opts.config)?.read_to_string(&mut config_file)?;

let toml_config = toml::from_str(&config_file)?;
let dir_location = shape::get_lang_location(&toml_config, &opts.language)?;
let output_path = &opts.output.as_path().to_str().unwrap();

if let Some(commands) =
shape::get_commands(&toml_config, &opts.language, shape::CommandVariants::Before)?
{
for command in commands {
commands::exec(&command, &opts.project_name, &output_path)?;
}
}

directory::copy_dir_all(
&dir_location,
&opts.output,
&opts.project_name,
&output_path,
)?;

std::env::set_current_dir(&opts.output)?;

if let Some(commands) =
shape::get_commands(&toml_config, &opts.language, shape::CommandVariants::After)?
{
for command in commands {
commands::exec(&command, &opts.project_name, &output_path)?;
}
}

Ok(())
}
47 changes: 10 additions & 37 deletions src/opts.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::path::Path;
use std::path::PathBuf;

use anyhow::Result;
use clap::Parser;
Expand All @@ -11,20 +11,14 @@ pub const DEFAULT_PROJECT_LOCATION: &str = "_";
pub enum OptError {
#[error("No config file found. Please create one at $HOME/.ctp or pass in a config file location with --config.")]
NoConfigFile,

#[error("{0} is not a valid output directory name, please use --output for a valid directory name or use a differnet project name.")]
ProjectNameError(String),

#[error("\"{0}\" is not a valid output directory name, please enter a new output directory.")]
OutputError(String),
}

#[derive(Parser)]
#[clap(version = VERSION, author = "William Lane <[email protected]>")]
pub struct Opts {
#[clap(short, long, default_value = "~/.ctp")]
#[clap(short, long, default_value = "_default_")]
/// Optional custom config file location.
pub config: String,
pub config: PathBuf,

/// Project language name.
pub language: String,
Expand All @@ -33,45 +27,24 @@ pub struct Opts {

#[clap(short, long, default_value = DEFAULT_PROJECT_LOCATION)]
/// Optional custom output directory location.
pub output: String,
pub output: PathBuf,
}

impl Opts {
pub fn valid_name(s: &str) -> bool {
[
'#', '\\', '%', '&', '{', '}', '<', '>', '*', '?', '/', '$', '!', '\'', '"', ':', '+',
'`', '|', '=',
]
.iter()
.any(|c| s.contains(*c))
}

pub fn config_file_exists(path: &str) -> bool {
Path::new(path).exists()
}

#[allow(clippy::self_named_constructors)]
pub fn opts() -> Result<Self, OptError> {
let mut opts: Opts = Opts::parse();

if opts.output == DEFAULT_PROJECT_LOCATION {
opts.output = opts.project_name.to_owned();
if opts.config.as_path().to_str().unwrap() == "_default_" {
opts.config = PathBuf::from(std::env::var("HOME").unwrap()).join(".ctp.toml");
}

let output_invalid = Self::valid_name(&opts.output);
let project_name_invalid = Self::valid_name(&opts.project_name);
opts.output = format!("./{}", opts.output);

if !Self::config_file_exists(&opts.config) {
return Err(OptError::NoConfigFile);
if opts.output.as_path().to_str().unwrap() == DEFAULT_PROJECT_LOCATION {
opts.output = ["./", &opts.project_name].iter().collect();
}

if output_invalid {
return Err(OptError::OutputError(opts.output));
}

if project_name_invalid && output_invalid {
return Err(OptError::ProjectNameError(opts.project_name));
if !opts.config.exists() {
return Err(OptError::NoConfigFile);
}

Ok(opts)
Expand Down
30 changes: 14 additions & 16 deletions src/shape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ pub enum TomlError {

#[error("The value of \"{0}\" is an invalid type, expected String")]
InvalidType(String),

#[error("Parse error")]
ParseError,
}

fn base_toml_checks(toml_value: &toml::Value) -> Result<(), TomlError> {
Expand All @@ -22,14 +19,6 @@ fn base_toml_checks(toml_value: &toml::Value) -> Result<(), TomlError> {
return Err(TomlError::SectionNotFound("templates".into()));
}

if !toml_table_unwraped.contains_key("commands-before") {
return Err(TomlError::SectionNotFound("commands-before".into()));
}

if !toml_table_unwraped.contains_key("commands-after") {
return Err(TomlError::SectionNotFound("commands-after".into()));
}

Ok(())
}

Expand Down Expand Up @@ -57,7 +46,7 @@ where
}

match table[lang_name].to_owned().try_into::<T>() {
Ok(s) => Ok(s),
Ok(val) => Ok(val),
Err(_) => Err(TomlError::InvalidType(lang_name.into())),
}
}
Expand All @@ -81,12 +70,21 @@ pub fn get_commands(
toml_value: &toml::Value,
lang_name: &str,
variant: CommandVariants,
) -> Result<Vec<String>, TomlError> {
) -> Result<Option<Vec<String>>, TomlError> {
base_toml_checks(toml_value)?;
let commands = get_table(toml_value, &variant.to_string())?;
let commands = extract_language_value(&commands, lang_name)?;
if !toml_value
.as_table()
.unwrap()
.contains_key(&variant.to_string())
{
return Ok(None);
}

Ok(commands)
let commands = get_table(toml_value, &variant.to_string())?;
match extract_language_value(&commands, lang_name) {
Ok(commands) => Ok(Some(commands)),
Err(_) => Ok(None),
}
}

pub fn get_lang_location(toml_value: &toml::Value, lang_name: &str) -> Result<String, TomlError> {
Expand Down