pretty print type analysis & add exit code

update readme
This commit is contained in:
Michael Sippel 2023-10-03 03:37:55 +02:00
parent 7796c6bc82
commit bd5f469682
Signed by: senvas
GPG key ID: F96CF119C34B64A6
3 changed files with 141 additions and 39 deletions

View file

@ -5,3 +5,4 @@ edition = "2018"
[dependencies] [dependencies]
laddertypes = { git = "https://github.com/michaelsippel/lib-laddertypes.git" } laddertypes = { git = "https://github.com/michaelsippel/lib-laddertypes.git" }
tiny-ansi = "0.1.0"

View file

@ -1,46 +1,69 @@
# ltsh # 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
<hr/> <hr/>
### Example ### Example
```sh ```sh
[~]$ ltsh <<< 'echo -n $PATH | xargs stat -c %x | sort -n' [~]$ ltsh <<< 'echo -n $PATH | xargs stat -c %Y | sort -n'
``` ```
``` ```
--- BEGIN TYPE-ANALYSIS --- --- BEGIN TYPE-ANALYSIS ---
* unknown stdin-type for `echo -n $PATH` * unknown stdin-type of `echo -n $PATH`
* !====> TYPE MISMATCH !! <====! * typecheck error
—————————— echo -n $PATH | xargs stat -c %Y
....`echo -n $PATH` outputs <Seq Path> | <Seq Path>
<Seq Path~<Seq PathSegment~<Seq Char>>~<SepSeq Char '/'>~<Seq Char>>~<SepSeq Char ':'>~<Seq Char> <Seq <Seq PathSegment>> | <Seq <Seq PathSegment>>
——————————— <Seq <Seq <Seq Char>>> | <Seq <Seq <Seq Char>>>
.... `xargs stat -c %x` expects <Seq <SepSeq Char '/'>> | <Seq <SepSeq Char '/'>>
<Seq Path~<Seq PathSegment~<Seq Char>>~<SepSeq Char '/'>~<Seq Char>>~<SepSeq Char '\n'>~<Seq Char> <Seq <Seq Char>> | <Seq <Seq Char>>
—————————— <SepSeq Char ':'> | <SepSeq Char '\n'>
<Seq Char> | <Seq Char>
* !====> TYPE MISMATCH !! <====! * typecheck ok
—————————— xargs stat -c %Y | sort -n
....`xargs stat -c %x` outputs <Seq Date> |
<Seq Date~ISO-8601~<Seq Char>>~<SepSeq Char '\n'>~<Seq Char> <Seq <TimeSince UnixEpoch>> |
——————————— <Seq <Duration Seconds>> |
.... `sort -n` expects <Seq > | <Seq >
<Seq >~<Seq <PosInt 10 BigEndian>~<Seq <Digit 10>~Char>>~<SepSeq Char '\n'>~<Seq Char> <Seq <PosInt 10 BigEndian>> | <Seq <PosInt 10 BigEndian>>
—————————— <Seq <Seq <Digit 10>>> | <Seq <Seq <Digit 10>>>
<Seq <Seq Char>> | <Seq <Seq Char>>
<SepSeq Char '\n'> | <SepSeq Char '\n'>
<Seq Char> | <Seq Char>
--- END TYPE-ANALYSIS --- --- 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 To automatically check every pipeline entered during interactive shell
use, add the following hook to your `.zshrc`: use, add the following hook to your `.zshrc`:
```sh ```sh
preexec() { 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) * regex-based typedb implementation (slow & incapable)
### License ## License
[GPLv3](COPYING) [GPLv3](COPYING)

View file

@ -1,6 +1,7 @@
use { use {
laddertypes::*, laddertypes::*,
std::io::BufRead std::io::BufRead,
tiny_ansi::TinyAnsi
}; };
//<<<<>>>><<>><><<>><<<*>>><<>><><<>><<<<>>>>\\ //<<<<>>>><<>><><<>><<<*>>><<>><><<>><<<<>>>>\\
@ -26,17 +27,17 @@ pub fn get_type_str(cmd: &str, item: &str) -> Option<String> {
//<<<<>>>><<>><><<>><<<*>>><<>><><<>><<<<>>>>\\ //<<<<>>>><<>><><<>><<<*>>><<>><><<>><<<<>>>>\\
fn main() { fn main() {
let mut success = true;
let mut dict = TypeDict::new(); let mut dict = TypeDict::new();
let stdin = std::io::stdin(); let stdin = std::io::stdin();
for pipeline in std::io::BufReader::new(stdin).lines() { for pipeline in std::io::BufReader::new(stdin).lines() {
let mut last_cmd = String::new(); let mut last_cmd = String::new();
let mut last_stdout_type_str : Option<String> = None;
let mut last_stdout_type : Option<TypeTerm> = None; let mut last_stdout_type : Option<TypeTerm> = None;
let pipeline_str = pipeline.expect(""); let pipeline_str = pipeline.expect("");
eprintln!("--- BEGIN TYPE-ANALYSIS ---\n"); eprintln!("{}", "--- BEGIN TYPE-ANALYSIS ---\n".blue());
for cmd in pipeline_str.split("|") { for cmd in pipeline_str.split("|") {
let cmd = cmd.trim() let cmd = cmd.trim()
@ -49,36 +50,113 @@ fn main() {
let it = dict.parse(&intype_str).expect("parse error"); let it = dict.parse(&intype_str).expect("parse error");
if let Some(last) = last_stdout_type { if let Some(last) = last_stdout_type {
if last.is_syntactic_subtype_of( &it ).is_ok() { match last.is_syntactic_subtype_of( &it ) {
eprintln!("* typecheck OK!"); Ok(first_match) => {
eprintln!(" ——————————\n .... `{}` outputs\n{} ——————————\n .... `{}` expects\n{} ——————————\n", eprintln!("{} typecheck {}", "*".bright_blue().bold(), "ok".bold().green());
last_cmd, last_stdout_type_str.unwrap(), cmd, intype_str);
} else { let rl = last.get_lnf_vec().iter().map(|t| dict.unparse(t)).collect::<Vec<_>>();
eprintln!("* !====> TYPE MISMATCH !! <====!"); let rr = it.get_lnf_vec().iter().map(|t| dict.unparse(t)).collect::<Vec<_>>();
eprintln!(" ——————————\n ....`{}` outputs\n{} ———————————\n .... `{}` expects\n{} ——————————\n",
last_cmd, last_stdout_type_str.unwrap(), cmd, intype_str); 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::<Vec<_>>();
let rr = it.get_lnf_vec().iter().map(|t| dict.unparse(t)).collect::<Vec<_>>();
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 { } 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") { if let Some(outtype_str) = get_type_str(&cmd, "<1") {
let it = dict.parse(&outtype_str).expect("parse error"); let it = dict.parse(&outtype_str).expect("parse error");
last_stdout_type_str = Some(outtype_str);
last_stdout_type = Some(it); last_stdout_type = Some(it);
} else { } else {
eprintln!("* unknown stdout-type for `{}`\n", cmd); eprintln!("{} {} stdout-type of `{}`\n", "*".bold().bright_blue(), "unknown".yellow(), cmd.on_black().bright_blue());
last_stdout_type_str = None;
last_stdout_type = None; last_stdout_type = None;
} }
last_cmd = cmd; last_cmd = cmd;
} }
eprintln!("{}", "--- END TYPE-ANALYSIS ---".blue());
} }
eprintln!("--- END TYPE-ANALYSIS ---\n"); std::process::exit(
if success {
0
} else {
1
}
);
} }
//<<<<>>>><<>><><<>><<<*>>><<>><><<>><<<<>>>>\\ //<<<<>>>><<>><><<>><<<*>>><<>><><<>><<<<>>>>\\