From bd5f469682d0141b4460330fac2a3c6e1e2d9e06 Mon Sep 17 00:00:00 2001 From: Michael Sippel Date: Tue, 3 Oct 2023 03:37:55 +0200 Subject: [PATCH] pretty print type analysis & add exit code update readme --- Cargo.toml | 1 + README.md | 67 ++++++++++++++++++++----------- src/main.rs | 112 ++++++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 141 insertions(+), 39 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1faaf40..108d83f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,3 +5,4 @@ edition = "2018" [dependencies] laddertypes = { git = "https://github.com/michaelsippel/lib-laddertypes.git" } +tiny-ansi = "0.1.0" diff --git a/README.md b/README.md index b4241d7..221fa0b 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,69 @@ # ltsh -small utility to perform a type-check on shell-pipelines + +**(highly experimental)** + +tiny utility program for type-analysis of shell pipelines based on ladder-typing +
### Example ```sh -[~]$ ltsh <<< 'echo -n $PATH | xargs stat -c %x | sort -n' +[~]$ ltsh <<< 'echo -n $PATH | xargs stat -c %Y | sort -n' ``` ``` --- BEGIN TYPE-ANALYSIS --- -* unknown stdin-type for `echo -n $PATH` +* unknown stdin-type of `echo -n $PATH` -* !====> TYPE MISMATCH !! <====! - —————————— - ....`echo -n $PATH` outputs ->~~>~~ - ——————————— - .... `xargs stat -c %x` expects ->~~>~~ - —————————— +* typecheck error + echo -n $PATH | xargs stat -c %Y + | +> | > +>> | >> +> | > +> | > + | + | -* !====> TYPE MISMATCH !! <====! - —————————— - ....`xargs stat -c %x` outputs ->~~ - ——————————— - .... `sort -n` expects -~~~Char>>~~ - —————————— +* typecheck ok + xargs stat -c %Y | sort -n + | +> | +> | + | +> | > +>> | >> +> | > + | + | --- END TYPE-ANALYSIS --- ``` -### Use as Zsh-extension +### Install + +```sh +git clone https://github.com/michaelsippel/ltsh +cd ltsh +cargo install --path . +``` + +### Use as Zsh-Extension To automatically check every pipeline entered during interactive shell use, add the following hook to your `.zshrc`: ```sh preexec() { - ltsh <<< "$1" + if ! ltsh <<< "${1}"; + then + echo "\e[33;1m" + echo "!! ltsh discoverd a type incompatibility. !!" + echo "!! abort [CTRL-C] or continue regardless [RET] !!" + echo "\e[0m" + read -s keys + fi } ``` @@ -51,5 +74,5 @@ preexec() { * regex-based typedb implementation (slow & incapable) -### License +## License [GPLv3](COPYING) diff --git a/src/main.rs b/src/main.rs index 64c4f7d..5533bc1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ use { laddertypes::*, - std::io::BufRead + std::io::BufRead, + tiny_ansi::TinyAnsi }; //<<<<>>>><<>><><<>><<<*>>><<>><><<>><<<<>>>>\\ @@ -26,17 +27,17 @@ pub fn get_type_str(cmd: &str, item: &str) -> Option { //<<<<>>>><<>><><<>><<<*>>><<>><><<>><<<<>>>>\\ fn main() { + let mut success = true; let mut dict = TypeDict::new(); let stdin = std::io::stdin(); for pipeline in std::io::BufReader::new(stdin).lines() { let mut last_cmd = String::new(); - let mut last_stdout_type_str : Option = None; let mut last_stdout_type : Option = None; let pipeline_str = pipeline.expect(""); - eprintln!("--- BEGIN TYPE-ANALYSIS ---\n"); + eprintln!("{}", "--- BEGIN TYPE-ANALYSIS ---\n".blue()); for cmd in pipeline_str.split("|") { let cmd = cmd.trim() @@ -49,36 +50,113 @@ fn main() { let it = dict.parse(&intype_str).expect("parse error"); if let Some(last) = last_stdout_type { - if last.is_syntactic_subtype_of( &it ).is_ok() { - eprintln!("* typecheck OK!"); - eprintln!(" ——————————\n .... `{}` outputs\n{} ——————————\n .... `{}` expects\n{} ——————————\n", - last_cmd, last_stdout_type_str.unwrap(), cmd, intype_str); - } else { - eprintln!("* !====> TYPE MISMATCH !! <====!"); - eprintln!(" ——————————\n ....`{}` outputs\n{} ———————————\n .... `{}` expects\n{} ——————————\n", - last_cmd, last_stdout_type_str.unwrap(), cmd, intype_str); + match last.is_syntactic_subtype_of( &it ) { + Ok(first_match) => { + eprintln!("{} typecheck {}", "*".bright_blue().bold(), "ok".bold().green()); + + let rl = last.get_lnf_vec().iter().map(|t| dict.unparse(t)).collect::>(); + let rr = it.get_lnf_vec().iter().map(|t| dict.unparse(t)).collect::>(); + + let c1_width = usize::max(rl.iter().map(|s| s.chars().count()).max().unwrap_or(0), last_cmd.chars().count()); + for _ in last_cmd.chars().count() .. c1_width { eprint!(" "); } + eprintln!("{}{}{}", last_cmd.on_black().bright_blue(), " | ".on_black().yellow().bold(), cmd.on_black().bright_blue()); + + for i in 0 .. rl.len() { + if i < first_match { + eprint!("{}", rl[i].bright_black()); + + for _ in rl[i].chars().count() .. c1_width { eprint!(" "); } + eprintln!(" {}", "|".bright_black()); + } else { + eprint!("{}", rl[i].green()); + + if i-first_match < rr.len() { + for _ in rl[i].chars().count() .. c1_width { eprint!(" "); } + eprintln!(" | {}", rr[i-first_match].green()); + } else { + eprintln!(""); + } + } + } + eprintln!(""); + } + Err((first_match, first_mismatch)) => { + success = false; + + eprintln!("{} typecheck {}", "*".bright_blue().bold(), "error".bold().red()); + + let rl = last.get_lnf_vec().iter().map(|t| dict.unparse(t)).collect::>(); + let rr = it.get_lnf_vec().iter().map(|t| dict.unparse(t)).collect::>(); + + let c1_width = usize::max(rl.iter().map(|s| s.chars().count()).max().unwrap_or(0), last_cmd.chars().count()); + for _ in last_cmd.chars().count() .. c1_width { eprint!(" "); } + eprintln!("{}{}{}", last_cmd.on_black().bright_blue(), " | ".on_black().yellow().bold(), cmd.on_black().bright_blue()); + + for i in 0 .. rl.len() { + if i < first_match { + eprint!("{}", &rl[i].bright_black()); + + for _ in rl[i].chars().count() .. c1_width { eprint!(" "); } + eprintln!(" {}", "|".bright_black()); + } else { + if i < first_mismatch { + eprint!("{}", rl[i].green()); + + if i - first_match < rr.len() { + for _ in rl[i].chars().count() .. c1_width { eprint!(" "); } + eprintln!(" | {}", rr[i-first_match].green()); + } else { + eprintln!(""); + } + } else if i > first_mismatch { + eprint!("{}", rl[i].bright_black()); + + if i - first_match < rr.len() { + for _ in rl[i].chars().count() .. c1_width { eprint!(" "); } + eprintln!(" | {}", rr[i-first_match].bright_black()); + } else { + eprintln!(""); + } + } else { + eprint!("{}", rl[i].red()); + + if i - first_match < rr.len() { + for _ in rl[i].chars().count() .. c1_width { eprint!(" "); } + eprintln!(" | {}", rr[i-first_match].red()); + } else { + eprintln!(""); + } + } + } + } + eprintln!(""); + } } } } else { - eprintln!("* unknown stdin-type for `{}`\n", cmd); + eprintln!("{} {} stdin-type of `{}`\n", "*".bold().bright_blue(), "unknown".yellow(), cmd.on_black().bright_blue()); } if let Some(outtype_str) = get_type_str(&cmd, "<1") { let it = dict.parse(&outtype_str).expect("parse error"); - - last_stdout_type_str = Some(outtype_str); last_stdout_type = Some(it); } else { - eprintln!("* unknown stdout-type for `{}`\n", cmd); - last_stdout_type_str = None; + eprintln!("{} {} stdout-type of `{}`\n", "*".bold().bright_blue(), "unknown".yellow(), cmd.on_black().bright_blue()); last_stdout_type = None; } last_cmd = cmd; } + eprintln!("{}", "--- END TYPE-ANALYSIS ---".blue()); } - eprintln!("--- END TYPE-ANALYSIS ---\n"); + std::process::exit( + if success { + 0 + } else { + 1 + } + ); } //<<<<>>>><<>><><<>><<<*>>><<>><><<>><<<<>>>>\\