domain: add optional on_line callback #15

Merged
pleshevskiy merged 5 commits from pretty-result into main 2022-11-12 16:38:02 +03:00
9 changed files with 164 additions and 80 deletions

1
.envrc
View file

@ -1 +0,0 @@
use flake

6
.gitignore vendored
View file

@ -1,7 +1,11 @@
# build
/target
/result
.direnv
# environments
.env*
!.env.example
# direnv
.envrc
.direnv

2
Cargo.lock generated
View file

@ -196,7 +196,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
name = "vnetod"
version = "0.3.3"
dependencies = [
"atty",
"clap",
"termcolor",
]
[[package]]

View file

@ -13,6 +13,8 @@ categories = ["command-line-interface", "config", "development-tools"]
[dependencies]
clap = { version = "3.2.15", default-features = false, features = ["std", "env", "derive"] }
atty = { version = "0.2.14", optional = true }
termcolor = { version = "1.1.3", optional = true }
[features]
color = ["clap/color"]
color = ["clap/color", "dep:atty", "dep:termcolor"]

View file

@ -11,22 +11,28 @@
cargoToml = fromTOML (readFile ./Cargo.toml);
version = "${cargoToml.package.version}+${substring 0 8 self.lastModifiedDate}.${self.shortRev or "dirty"}";
mkVnetod = { lib, rustPlatform, ... }:
mkVnetod = { lib, rustPlatform, vnetodFeatures ? [ ], ... }:
rustPlatform.buildRustPackage {
name = "vnetod-${version}";
src = lib.cleanSource ./.;
cargoLock.lockFile = ./Cargo.lock;
buildFeatures = vnetodFeatures;
doCheck = true;
};
in
{
overlays = rec {
vnetod = final: prev: {
overlays = {
default = final: prev: {
vnetod = final.callPackage mkVnetod { };
};
default = vnetod;
colored = final: prev: {
vnetod = final.callPackage mkVnetod {
vnetodFeatures = [ "color" ];
};
};
};
}
// flake-utils.lib.eachDefaultSystem (system:
@ -34,6 +40,7 @@
pkgs = import nixpkgs { inherit system; };
vnetod = pkgs.callPackage mkVnetod { };
coloredVnetod = pkgs.callPackage mkVnetod { vnetodFeatures = [ "color" ]; };
docker = pkgs.dockerTools.buildLayeredImage {
name = "pleshevskiy/vnetod";
@ -41,19 +48,25 @@
config = {
Volumes."/data" = { };
WorkingDir = "/data";
Entrypoint = [ "${vnetod}/bin/vnetod" ];
Entrypoint = [ "${coloredVnetod}/bin/vnetod" ];
};
};
mkApp = prog: {
type = "app";
program = "${vnetod}/bin/vnetod";
};
in
{
apps.default = {
type = "app";
program = "${vnetod}/bin/vnetod";
apps = {
default = mkApp vnetod;
colored = mkApp coloredVnetod;
};
packages = {
inherit docker vnetod;
default = vnetod;
colored = coloredVnetod;
};
devShell = pkgs.mkShell {

View file

@ -20,7 +20,7 @@ use std::path::PathBuf;
use clap::Parser;
#[derive(Parser)]
#[derive(Parser, Debug)]
#[clap(
author,
version,
@ -62,4 +62,63 @@ pub struct Args {
help = "Environment varible sections that will be enabled"
)]
pub sections: Vec<String>,
#[cfg(feature = "color")]
#[clap(
long,
value_enum,
default_value = "auto",
long_help = "
This flag controls when to use colors. The default setting is 'auto', which
means vnetod will try to guess when to use colors. For example, if vnetod is
printing to a terminal, then it will use colors, but if it is redirected to a
file or a pipe, then it will suppress color output. vnetod will suppress color
output in some other circumstances as well. For example, if the TERM
environment variable is not set or set to 'dumb', then vnetod will not use
colors.
The possible values for this flag are:
never Colors will never be used.
auto The default. vnetod tries to be smart.
always Colors will always be used regardless of where output is sent.
ansi Like 'always', but emits ANSI escapes (even in a Windows console).
"
)]
pub color: ColorVariant,
}
#[cfg(feature = "color")]
#[derive(clap::ValueEnum, Clone, Debug)]
pub enum ColorVariant {
Auto,
Always,
Ansi,
Never,
}
#[cfg(feature = "color")]
impl From<ColorVariant> for termcolor::ColorChoice {
fn from(col: ColorVariant) -> Self {
match col {
ColorVariant::Never => Self::Never,
ColorVariant::Always => Self::Always,
ColorVariant::Ansi => Self::AlwaysAnsi,
ColorVariant::Auto => {
if atty::is(atty::Stream::Stdout) {
// Otherwise let's `termcolor` decide by inspecting the environment. From the [doc]:
// - If `NO_COLOR` is set to any value, then colors will be suppressed.
// - If `TERM` is set to dumb, then colors will be suppressed.
// - In non-Windows environments, if `TERM` is not set, then colors will be suppressed.
//
// [doc]: https://github.com/BurntSushi/termcolor#automatic-color-selection
Self::Auto
} else {
// Colors should be deactivated if the terminal is not a tty.
Self::Never
}
}
}
}
}

View file

@ -14,21 +14,24 @@
//! along with vnetod. If not, see <https://www.gnu.org/licenses/>.
//!
#[cfg(feature = "color")]
use termcolor::{Color, ColorSpec, StandardStream, WriteColor};
use crate::{cli::Args, domain};
use std::fs::File;
use std::io::{stdout, Write};
use std::io::Write;
#[derive(Debug)]
pub enum Error {
OpenFile,
Switch(domain::switch::Error),
WriteFile,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::OpenFile => f.write_str("Cannot open file"),
Error::Switch(inner) => write!(f, "Cannot switch between states: {}", inner),
Error::WriteFile => f.write_str("Cannot write file"),
}
}
}
@ -38,19 +41,51 @@ impl std::error::Error for Error {}
pub fn execute(args: &Args) -> Result<(), Error> {
let content = std::fs::read_to_string(&args.file).map_err(|_| Error::OpenFile)?;
let writer: Box<dyn Write> = if args.dry_run {
Box::new(stdout())
} else {
Box::new(
File::create(args.output.as_ref().unwrap_or(&args.file))
.map_err(|_| Error::OpenFile)?,
)
};
domain::switch::execute(domain::switch::Request {
content: &content,
writer,
sections: &args.sections,
})
.map_err(Error::Switch)
if args.dry_run {
println!("Your file will be changed to the following")
}
let fs_writer = (!args.dry_run)
.then(|| {
File::create(args.output.as_ref().unwrap_or(&args.file)).map_err(|_| Error::OpenFile)
})
.transpose()?;
#[cfg(feature = "color")]
let color = args.color.clone();
println!();
let new_content = domain::switch::execute(domain::switch::Request {
content: &content.trim(),
sections: &args.sections,
on_line: Some(Box::new(move |line| {
#[cfg(feature = "color")]
print_line(line, color.clone());
#[cfg(not(feature = "color"))]
print!("{}", line)
})),
});
println!();
if let Some(mut fs_writer) = fs_writer {
fs_writer
.write_all(new_content.as_bytes())
.map_err(|_| Error::WriteFile)?;
}
Ok(())
}
#[cfg(feature = "color")]
fn print_line(line: &String, color: crate::cli::ColorVariant) {
let mut stdout = StandardStream::stdout(color.into());
let color = line
.starts_with("###")
.then_some(Color::Yellow)
.or_else(|| (!line.starts_with("#")).then_some(Color::Green));
stdout.set_color(ColorSpec::new().set_fg(color)).ok();
write!(&mut stdout, "{}", line).unwrap();
stdout.reset().ok();
}

View file

@ -44,7 +44,7 @@ impl Section {
}
}
struct SectionInfo {
pub struct SectionInfo {
enable_variable: bool,
disable_variable: bool,
}

View file

@ -14,54 +14,32 @@
//! along with vnetod. If not, see <https://www.gnu.org/licenses/>.
//!
use std::io::{BufWriter, Write};
use super::{Section, SectionInfo};
#[derive(Debug)]
pub enum Error {
WriteData,
}
pub type OnLineFn = Box<dyn Fn(&String)>;
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::WriteData => f.write_str("Cannot write data"),
}
}
}
impl std::error::Error for Error {}
pub struct Request<'args, W>
where
W: Write,
{
pub struct Request<'args> {
pub content: &'args str,
pub writer: W,
pub sections: &'args [String],
pub on_line: Option<OnLineFn>,
}
pub fn execute<W>(req: Request<W>) -> Result<(), Error>
where
W: Write,
{
let mut writer = BufWriter::new(req.writer);
pub fn execute(req: Request) -> String {
let choose_sections = req
.sections
.iter()
.map(|s| Section::parse(s.as_str()))
.collect::<Vec<_>>();
let mut current_sections: Option<SectionInfo> = None;
let mut current_section: Option<SectionInfo> = None;
let mut new_content = String::new();
for line in req.content.split_inclusive('\n') {
let new_line = if is_section_end(line) {
current_sections = None;
current_section = None;
line.to_string()
} else if let Some(section_info) = line.strip_prefix("### ") {
current_sections = section_info.split_whitespace().next().map(|r| {
current_section = section_info.split_whitespace().next().map(|r| {
let current_sections = r.split(',').map(Section::parse).collect::<Vec<_>>();
SectionInfo {
enable_variable: should_enable_variable(&choose_sections, &current_sections),
@ -69,7 +47,7 @@ where
}
});
line.to_string()
} else if let Some(section_info) = current_sections.as_ref() {
} else if let Some(section_info) = current_section.as_ref() {
let trimmed_line = line.trim_start_matches(['#', ' ']);
let is_var = is_variable(trimmed_line);
if is_var && section_info.enable_variable {
@ -83,12 +61,13 @@ where
line.to_string()
};
writer
.write_all(new_line.as_bytes())
.map_err(|_| Error::WriteData)?;
new_content.push_str(&new_line);
if let Some(on_line) = req.on_line.as_ref() {
on_line(&new_line);
}
}
writer.flush().map_err(|_| Error::WriteData)
new_content
}
fn is_variable(trimmed_line: &str) -> bool {
@ -144,29 +123,20 @@ fn should_disable_variable(choose_sections: &[Section], current_sections: &[Sect
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
const BASE_ENV: &str = include_str!("../../test_data/base_env");
fn make_test(input: &str, expected_output: &str, sections: Vec<&str>) {
let mut output_data = vec![];
let writer = Cursor::new(&mut output_data);
match execute(Request {
let output_data = execute(Request {
content: input,
writer,
sections: &sections.into_iter().map(String::from).collect::<Vec<_>>(),
}) {
Ok(()) => {
let output = String::from_utf8(output_data).unwrap();
on_line: None,
});
assert_eq!(
output.lines().collect::<Vec<_>>(),
output_data.lines().collect::<Vec<_>>(),
expected_output.lines().collect::<Vec<_>>()
);
}
_ => unreachable!(),
}
}
#[test]
fn should_disable_all_sections() {