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 /target
/result /result
.direnv # environments
.env* .env*
!.env.example !.env.example
# direnv
.envrc
.direnv

2
Cargo.lock generated
View file

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

View file

@ -13,6 +13,8 @@ categories = ["command-line-interface", "config", "development-tools"]
[dependencies] [dependencies]
clap = { version = "3.2.15", default-features = false, features = ["std", "env", "derive"] } 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] [features]
color = ["clap/color"] color = ["clap/color", "dep:atty", "dep:termcolor"]

View file

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

View file

@ -20,7 +20,7 @@ use std::path::PathBuf;
use clap::Parser; use clap::Parser;
#[derive(Parser)] #[derive(Parser, Debug)]
#[clap( #[clap(
author, author,
version, version,
@ -62,4 +62,63 @@ pub struct Args {
help = "Environment varible sections that will be enabled" help = "Environment varible sections that will be enabled"
)] )]
pub sections: Vec<String>, 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/>. //! 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 crate::{cli::Args, domain};
use std::fs::File; use std::fs::File;
use std::io::{stdout, Write}; use std::io::Write;
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
OpenFile, OpenFile,
Switch(domain::switch::Error), WriteFile,
} }
impl std::fmt::Display for Error { impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Error::OpenFile => f.write_str("Cannot open file"), 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> { pub fn execute(args: &Args) -> Result<(), Error> {
let content = std::fs::read_to_string(&args.file).map_err(|_| Error::OpenFile)?; let content = std::fs::read_to_string(&args.file).map_err(|_| Error::OpenFile)?;
let writer: Box<dyn Write> = if args.dry_run { if args.dry_run {
Box::new(stdout()) println!("Your file will be changed to the following")
} else { }
Box::new(
File::create(args.output.as_ref().unwrap_or(&args.file))
.map_err(|_| Error::OpenFile)?,
)
};
domain::switch::execute(domain::switch::Request { let fs_writer = (!args.dry_run)
content: &content, .then(|| {
writer, 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, sections: &args.sections,
}) on_line: Some(Box::new(move |line| {
.map_err(Error::Switch) #[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, enable_variable: bool,
disable_variable: bool, disable_variable: bool,
} }

View file

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