domain: add optional on_line callback #15
9 changed files with 164 additions and 80 deletions
1
.envrc
1
.envrc
|
@ -1 +0,0 @@
|
|||
use flake
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -1,7 +1,11 @@
|
|||
# build
|
||||
/target
|
||||
/result
|
||||
|
||||
.direnv
|
||||
# environments
|
||||
.env*
|
||||
!.env.example
|
||||
|
||||
# direnv
|
||||
.envrc
|
||||
.direnv
|
||||
|
|
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -196,7 +196,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
|||
name = "vnetod"
|
||||
version = "0.3.3"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"clap",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -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"]
|
||||
|
|
29
flake.nix
29
flake.nix
|
@ -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 {
|
||||
|
|
61
src/cli.rs
61
src/cli.rs
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)?,
|
||||
)
|
||||
};
|
||||
if args.dry_run {
|
||||
println!("Your file will be changed to the following")
|
||||
}
|
||||
|
||||
domain::switch::execute(domain::switch::Request {
|
||||
content: &content,
|
||||
writer,
|
||||
sections: &args.sections,
|
||||
let fs_writer = (!args.dry_run)
|
||||
.then(|| {
|
||||
File::create(args.output.as_ref().unwrap_or(&args.file)).map_err(|_| Error::OpenFile)
|
||||
})
|
||||
.map_err(Error::Switch)
|
||||
.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();
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ impl Section {
|
|||
}
|
||||
}
|
||||
|
||||
struct SectionInfo {
|
||||
pub struct SectionInfo {
|
||||
enable_variable: bool,
|
||||
disable_variable: bool,
|
||||
}
|
||||
|
|
|
@ -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, ¤t_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: §ions.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() {
|
||||
|
|
Loading…
Reference in a new issue