From 85688238c253f0d2935ddb53ccac9e69a94bd71a Mon Sep 17 00:00:00 2001 From: Mark Oborne Date: Fri, 18 Jul 2025 21:42:26 +0900 Subject: [PATCH 1/5] fix: ensure summary only replaces whole numbers Before if 44 came before 447 in a fix message, and the summary template included 447, then we would replace 447 with <44 value>7. --- src/command.rs | 4 ++-- src/prefix/mod.rs | 43 +++++++++++++++++++++++++++++++------------ 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/command.rs b/src/command.rs index ea474cc..9c913f6 100644 --- a/src/command.rs +++ b/src/command.rs @@ -22,11 +22,11 @@ pub fn make_command() -> Command { .action(ArgAction::SetTrue) ) .arg( - arg!(-f --strict "Only consider full FIX messages containing both BeginString and Checksum") + arg!(-r --repeating "Combine any repeating groups into a single field with a comma delimited value") .action(ArgAction::SetTrue) ) .arg( - arg!(-r --repeating "Combine any repeating groups into a single field with a comma delimited value") + arg!(-f --strict "Only consider full FIX messages containing both BeginString and Checksum") .action(ArgAction::SetTrue) ) .arg( diff --git a/src/prefix/mod.rs b/src/prefix/mod.rs index 0238943..0aa4b31 100644 --- a/src/prefix/mod.rs +++ b/src/prefix/mod.rs @@ -13,13 +13,13 @@ struct Field { pub struct Options { delimiter: String, colour: bool, + only_fix: bool, repeating: bool, strict: bool, strip: bool, summary: Option, tag: bool, value: bool, - only_fix: bool, } #[derive(Debug, PartialEq)] @@ -35,17 +35,19 @@ pub fn matches_to_flags(matches: &ArgMatches) -> Options { Options { delimiter: matches.get_one::("delimiter").unwrap().to_string(), colour: use_colour, + only_fix: matches.get_flag("only-fix"), repeating: matches.get_flag("repeating"), strict: matches.get_flag("strict"), strip: matches.get_flag("strip"), summary: matches.get_one::("summary").cloned(), tag: matches.get_flag("tag"), value: matches.get_flag("value"), - only_fix: matches.get_flag("only-fix"), } } fn get_msg_regex() -> Regex { + // This regex will only match valid fields and any malformed fields will be ignored. + // This means its very unlikely for prefix to fail to parse a FIX message. Regex::new(r"(?P[0-9]+)=(?P[^\^\|\x01]+)").unwrap() } @@ -58,14 +60,24 @@ pub fn run(input: &[String], flags: &Options) { let fix_tag_regex = get_tag_regex(); let mut stdout = io::stdout(); + let mut regex_by_tag = HashMap::<&str, Regex>::new(); + if flags.summary.is_some() { + let template = flags.summary.as_ref().unwrap(); + let re = Regex::new(r"\d+").unwrap(); + for number in re.find_iter(template) { + let number = number.as_str(); + regex_by_tag.insert(number, Regex::new(&format!(r"\b{}\b", number)).unwrap()); + } + } + for (i, line) in input.iter().enumerate() { match parse_fix_msg(line, &fix_msg_regex) { FixMsg::Full(parsed) => { - print_fix_msg(&mut stdout, i, input.len(), &parsed, flags); + print_fix_msg(&mut stdout, i, input.len(), &parsed, ®ex_by_tag, flags); } FixMsg::Partial(parsed) => { if !flags.strict { - print_fix_msg(&mut stdout, i, input.len(), &parsed, flags); + print_fix_msg(&mut stdout, i, input.len(), &parsed, ®ex_by_tag, flags); } else if !flags.only_fix { print_non_fix_msg(&mut stdout, line, &fix_tag_regex, flags); } @@ -102,10 +114,11 @@ fn print_fix_msg( line_number: usize, last_line: usize, fix_msg: &[Field], + regex_by_tag: &HashMap<&str, Regex>, flags: &Options, ) { let result = if flags.summary.is_some() { - writeln!(stdout, "{}", format_to_summary(fix_msg, flags)) + writeln!(stdout, "{}", format_to_summary(fix_msg, regex_by_tag, flags)) } else { // Avoid adding an empty new line at the bottom of the output. if line_number + 1 == last_line && flags.delimiter == "\n" { @@ -195,7 +208,7 @@ fn format_to_string(input: &[Field], flags: &Options) -> String { }) } -fn format_to_summary(input: &[Field], flags: &Options) -> String { +fn format_to_summary(input: &[Field], regex_by_tag: &HashMap::<&str, Regex>, flags: &Options) -> String { let template = flags.summary.as_ref().unwrap(); let mut result = String::from(template); for field in input { @@ -209,8 +222,12 @@ fn format_to_summary(input: &[Field], flags: &Options) -> String { &field.value }; if !template.is_empty() { - // Replace tag numbers in template to tag name. - result = result.replace(&field.tag.to_string(), value); + let tag = field.tag.to_string(); + if template.contains(&tag) { + // Use a regex with line boundaries to ensure we don't overwrite partial numbers. + // Replace tag numbers in template to tag name. + result = regex_by_tag[tag.as_str()].replace_all(&result, value).to_string(); + } } if field.tag == 35 { let msg_type = tags::MSG_TYPES @@ -310,13 +327,13 @@ mod tests { let flags = Options { delimiter: String::from("\n"), colour: false, + only_fix: false, repeating: false, strict: false, strip: false, summary: None, tag: false, value: false, - only_fix: false, }; let result = format_to_string(&parsed, &flags); let expected = String::from( @@ -334,13 +351,13 @@ mod tests { let flags = Options { delimiter: String::from("|"), colour: true, + only_fix: true, repeating: true, strict: true, strip: true, summary: None, tag: true, value: true, - only_fix: true, }; let result = format_to_string(&parsed, &flags); let expected = String::from( @@ -362,15 +379,17 @@ mod tests { let flags = Options { delimiter: String::from("\n"), colour: false, + only_fix: false, repeating: false, strict: false, strip: false, summary: Some(String::from("for 55")), tag: false, value: false, - only_fix: false, }; - let result = format_to_summary(&input, &flags); + + let regex_by_tag = HashMap::<&str, Regex>::from([("55", Regex::new(r"\b55\b").unwrap())]); + let result = format_to_summary(&input, ®ex_by_tag, &flags); let expected = String::from("NewOrderSingle for EUR/USD"); assert_eq!(result, expected); } From ecd2859dde94ba4c8fcde064df48512d4283e8d3 Mon Sep 17 00:00:00 2001 From: Mark Oborne Date: Sat, 19 Jul 2025 15:39:49 +0900 Subject: [PATCH 2/5] feat: handle stdin line by line We handle two key scenarios better now: * We receive an infinite input (e.g. kubectl logs xxx -f | prefix) We will now handle each stdin line as soon as it arrives. * We pipe into a program that stops after a set number of lines. (e.g. prefix | head) We will now terminate prefix on broken pipe (head finishes) --- src/main.rs | 26 +++++++++++---- src/prefix/mod.rs | 83 ++++++++++++++++++++++++---------------------- test/brokenpipe.sh | 6 ++++ 3 files changed, 70 insertions(+), 45 deletions(-) create mode 100644 test/brokenpipe.sh diff --git a/src/main.rs b/src/main.rs index bfb345d..ff94777 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,12 +5,26 @@ use std::io; fn main() { let matches = command::make_command().get_matches(); + let flags = prefix::matches_to_flags(&matches); - let fix_message: Vec = match matches.get_many::("message") { - Some(msg) => msg.map(|elem| elem.to_owned()).collect(), - None => io::stdin().lines().map(|line| line.unwrap()).collect(), - }; + // Avoid compiling regexes multiple times. + let msg_regex = prefix::get_msg_regex(); + let tag_regex = prefix::get_tag_regex(); + let summary_regexes = prefix::get_summary_regexes(&flags); + + if let Some(msgs) = matches.get_many::("message") { + let n_msgs = msgs.len(); + for (i, msg) in msgs.enumerate() { + let is_last_line = i + 1 == n_msgs; + prefix::run(msg, is_last_line, &msg_regex, &tag_regex, &summary_regexes, &flags); + } + } else { + let mut lines = io::stdin().lines().peekable(); + while let Some(line) = lines.next() { + let line = line.unwrap(); + let is_last_line = lines.peek().is_none(); + prefix::run(&line, is_last_line, &msg_regex, &tag_regex, &summary_regexes, &flags); + } + } - let flags = prefix::matches_to_flags(&matches); - prefix::run(&fix_message, &flags); } diff --git a/src/prefix/mod.rs b/src/prefix/mod.rs index 0aa4b31..aaa3f6d 100644 --- a/src/prefix/mod.rs +++ b/src/prefix/mod.rs @@ -2,7 +2,9 @@ mod tags; use clap::ArgMatches; use regex::Regex; -use std::io::{self, IsTerminal, Write}; +use std::{ + collections::HashMap, io::{self, IsTerminal, Write}, process +}; #[derive(Debug, PartialEq, Clone)] struct Field { @@ -45,89 +47,92 @@ pub fn matches_to_flags(matches: &ArgMatches) -> Options { } } -fn get_msg_regex() -> Regex { +pub fn get_msg_regex() -> Regex { // This regex will only match valid fields and any malformed fields will be ignored. // This means its very unlikely for prefix to fail to parse a FIX message. Regex::new(r"(?P[0-9]+)=(?P[^\^\|\x01]+)").unwrap() } -fn get_tag_regex() -> Regex { +pub fn get_tag_regex() -> Regex { Regex::new(r"[0-9]+").unwrap() } -pub fn run(input: &[String], flags: &Options) { - let fix_msg_regex = get_msg_regex(); - let fix_tag_regex = get_tag_regex(); - let mut stdout = io::stdout(); - - let mut regex_by_tag = HashMap::<&str, Regex>::new(); +pub fn get_summary_regexes(flags: &Options) -> HashMap:: { + let mut summary_regexes = HashMap::::new(); if flags.summary.is_some() { let template = flags.summary.as_ref().unwrap(); let re = Regex::new(r"\d+").unwrap(); for number in re.find_iter(template) { let number = number.as_str(); - regex_by_tag.insert(number, Regex::new(&format!(r"\b{}\b", number)).unwrap()); + summary_regexes.insert(number.to_string(), Regex::new(&format!(r"\b{}\b", number)).unwrap()); } } + summary_regexes +} - for (i, line) in input.iter().enumerate() { - match parse_fix_msg(line, &fix_msg_regex) { - FixMsg::Full(parsed) => { - print_fix_msg(&mut stdout, i, input.len(), &parsed, ®ex_by_tag, flags); - } - FixMsg::Partial(parsed) => { - if !flags.strict { - print_fix_msg(&mut stdout, i, input.len(), &parsed, ®ex_by_tag, flags); - } else if !flags.only_fix { - print_non_fix_msg(&mut stdout, line, &fix_tag_regex, flags); - } + +pub fn run(input: &str, last_line: bool, msg_regex: &Regex, tag_regex: &Regex, summary_regexes: &HashMap::, flags: &Options) { + let mut stdout = io::stdout(); + + match parse_fix_msg(input, &msg_regex) { + FixMsg::Full(parsed) => { + print_fix_msg(&mut stdout, last_line, &parsed, &summary_regexes, flags); + } + FixMsg::Partial(parsed) => { + if !flags.strict { + print_fix_msg(&mut stdout, last_line, &parsed, &summary_regexes, flags); + } else if !flags.only_fix { + print_non_fix_msg(&mut stdout, input, &tag_regex, flags); } - FixMsg::None => { - if !flags.only_fix { - print_non_fix_msg(&mut stdout, line, &fix_tag_regex, flags); - } + } + FixMsg::None => { + if !flags.only_fix { + print_non_fix_msg(&mut stdout, input, &tag_regex, flags); } } } } -fn ignore_broken_pipe(result: io::Result<()>) { +fn handle_broken_pipe(result: io::Result<()>) { if let Err(e) = result { - // When piping into certain programs like head, printing to stdout can fail. - if e.kind() != io::ErrorKind::BrokenPipe { - panic!("Error writing to stdout: {}", e); + // When piping into certain programs like head, printing to stdout can fail. This is + // expected and we do not want to panic, instead we terminate cleanly. Prefix is not + // designed to be used for anything besides printing. And this keeps the behaviour closer + // to other unix tools that will terminate upon receiving the SIGPIPE (which rust programs ignore by default) + if e.kind() == io::ErrorKind::BrokenPipe { + process::exit(0); } + panic!("Error writing to stdout: {}", e); } } -fn print_non_fix_msg(stdout: &mut io::Stdout, line: &str, fix_tag_regex: &Regex, flags: &Options) { +fn print_non_fix_msg(stdout: &mut io::Stdout, line: &str, tag_regex: &Regex, flags: &Options) { let result = if flags.tag { - writeln!(stdout, "{}", parse_tags(line, fix_tag_regex)) + writeln!(stdout, "{}", parse_tags(line, tag_regex)) } else { writeln!(stdout, "{}", line) }; - ignore_broken_pipe(result); + handle_broken_pipe(result); } fn print_fix_msg( stdout: &mut io::Stdout, - line_number: usize, - last_line: usize, + last_line: bool, fix_msg: &[Field], - regex_by_tag: &HashMap<&str, Regex>, + regex_by_tag: &HashMap, flags: &Options, ) { let result = if flags.summary.is_some() { writeln!(stdout, "{}", format_to_summary(fix_msg, regex_by_tag, flags)) } else { // Avoid adding an empty new line at the bottom of the output. - if line_number + 1 == last_line && flags.delimiter == "\n" { + if last_line && flags.delimiter == "\n" { write!(stdout, "{}", format_to_string(fix_msg, flags)) } else { writeln!(stdout, "{}", format_to_string(fix_msg, flags)) } }; - ignore_broken_pipe(result); + handle_broken_pipe(result); } fn parse_fix_msg(input: &str, regex: &Regex) -> FixMsg { @@ -208,7 +213,7 @@ fn format_to_string(input: &[Field], flags: &Options) -> String { }) } -fn format_to_summary(input: &[Field], regex_by_tag: &HashMap::<&str, Regex>, flags: &Options) -> String { +fn format_to_summary(input: &[Field], regex_by_tag: &HashMap::, flags: &Options) -> String { let template = flags.summary.as_ref().unwrap(); let mut result = String::from(template); for field in input { @@ -388,7 +393,7 @@ mod tests { value: false, }; - let regex_by_tag = HashMap::<&str, Regex>::from([("55", Regex::new(r"\b55\b").unwrap())]); + let regex_by_tag = HashMap::::from([(String::from("55"), Regex::new(r"\b55\b").unwrap())]); let result = format_to_summary(&input, ®ex_by_tag, &flags); let expected = String::from("NewOrderSingle for EUR/USD"); assert_eq!(result, expected); diff --git a/test/brokenpipe.sh b/test/brokenpipe.sh new file mode 100644 index 0000000..9bf7b88 --- /dev/null +++ b/test/brokenpipe.sh @@ -0,0 +1,6 @@ +while true +do + echo "8=FIX|35=D|55=EURUSD|" + sleep 1 +done + From 760710f4c80338b5b7e990a68aab79cc7a7823a7 Mon Sep 17 00:00:00 2001 From: Mark Oborne Date: Sat, 19 Jul 2025 20:05:37 +0900 Subject: [PATCH 3/5] feat: Add --porcelain Prints the FIX messages closer to true FIX Format, same as with --delimiter --strip true. Useful if you only want the tag/ value translating --- src/command.rs | 4 ++++ src/prefix/mod.rs | 13 +++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/command.rs b/src/command.rs index 9c913f6..cf61dfa 100644 --- a/src/command.rs +++ b/src/command.rs @@ -21,6 +21,10 @@ pub fn make_command() -> Command { arg!(-o --"only-fix" "Only print FIX messages") .action(ArgAction::SetTrue) ) + .arg( + arg!(--porcelain "print FIX messages closer to standard format, same as --delimiter \\x01 --strip") + .action(ArgAction::SetTrue) + ) .arg( arg!(-r --repeating "Combine any repeating groups into a single field with a comma delimited value") .action(ArgAction::SetTrue) diff --git a/src/prefix/mod.rs b/src/prefix/mod.rs index aaa3f6d..7f1ed6c 100644 --- a/src/prefix/mod.rs +++ b/src/prefix/mod.rs @@ -34,13 +34,22 @@ enum FixMsg { pub fn matches_to_flags(matches: &ArgMatches) -> Options { let when = matches.get_one::("color").unwrap(); let use_colour = (io::stdout().is_terminal() && when == "auto") || when == "always"; + let delimiter; + let strip; + if matches.get_flag("porcelain") { + delimiter = String::from("\x01"); + strip = true; + } else { + delimiter = matches.get_one::("delimiter").unwrap().to_string(); + strip = matches.get_flag("strip"); + } Options { - delimiter: matches.get_one::("delimiter").unwrap().to_string(), + delimiter, colour: use_colour, only_fix: matches.get_flag("only-fix"), repeating: matches.get_flag("repeating"), strict: matches.get_flag("strict"), - strip: matches.get_flag("strip"), + strip, summary: matches.get_one::("summary").cloned(), tag: matches.get_flag("tag"), value: matches.get_flag("value"), From ca3dad9f51adb4839e2fa26fe52d8158bd5a9586 Mon Sep 17 00:00:00 2001 From: Mark Oborne Date: Sat, 19 Jul 2025 20:15:23 +0900 Subject: [PATCH 4/5] chore: update to version 1.2.0 Generates updated completion and man pages Runs cargo fmt --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- completion/_prefix | 5 +++-- completion/_prefix.ps1 | 5 +++-- completion/prefix.bash | 2 +- completion/prefix.fish | 3 ++- man/prefix.1 | 13 ++++++++----- src/main.rs | 19 ++++++++++++++++--- src/prefix/mod.rs | 40 +++++++++++++++++++++++++++++++--------- 9 files changed, 67 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f94a72a..486ec4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "aho-corasick" @@ -168,7 +168,7 @@ dependencies = [ [[package]] name = "prefix" -version = "1.1.5" +version = "1.2.0" dependencies = [ "clap", "clap_complete", diff --git a/Cargo.toml b/Cargo.toml index 30fd4e7..153f0aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "prefix" -version = "1.1.5" +version = "1.2.0" authors = ["Shivix"] edition = "2021" description = "A customizable pretty printer for FIX messages" diff --git a/completion/_prefix b/completion/_prefix index 7ba74bc..0a7da05 100644 --- a/completion/_prefix +++ b/completion/_prefix @@ -23,10 +23,11 @@ _prefix() { '--summary=[Summarise each fix message based on an optional template]' \ '-o[Only print FIX messages]' \ '--only-fix[Only print FIX messages]' \ -'-f[Only consider full FIX messages containing both BeginString and Checksum]' \ -'--strict[Only consider full FIX messages containing both BeginString and Checksum]' \ +'--porcelain[print FIX messages closer to standard format, same as --delimiter \\x01 --strip]' \ '-r[Combine any repeating groups into a single field with a comma delimited value]' \ '--repeating[Combine any repeating groups into a single field with a comma delimited value]' \ +'-f[Only consider full FIX messages containing both BeginString and Checksum]' \ +'--strict[Only consider full FIX messages containing both BeginString and Checksum]' \ '-s[Strip the whitespace around the = in each field]' \ '--strip[Strip the whitespace around the = in each field]' \ '-t[Translate tag numbers on non FIX message lines]' \ diff --git a/completion/_prefix.ps1 b/completion/_prefix.ps1 index 136b051..e32a507 100644 --- a/completion/_prefix.ps1 +++ b/completion/_prefix.ps1 @@ -29,10 +29,11 @@ Register-ArgumentCompleter -Native -CommandName 'prefix' -ScriptBlock { [CompletionResult]::new('--summary', 'summary', [CompletionResultType]::ParameterName, 'Summarise each fix message based on an optional template') [CompletionResult]::new('-o', 'o', [CompletionResultType]::ParameterName, 'Only print FIX messages') [CompletionResult]::new('--only-fix', 'only-fix', [CompletionResultType]::ParameterName, 'Only print FIX messages') - [CompletionResult]::new('-f', 'f', [CompletionResultType]::ParameterName, 'Only consider full FIX messages containing both BeginString and Checksum') - [CompletionResult]::new('--strict', 'strict', [CompletionResultType]::ParameterName, 'Only consider full FIX messages containing both BeginString and Checksum') + [CompletionResult]::new('--porcelain', 'porcelain', [CompletionResultType]::ParameterName, 'print FIX messages closer to standard format, same as --delimiter \x01 --strip') [CompletionResult]::new('-r', 'r', [CompletionResultType]::ParameterName, 'Combine any repeating groups into a single field with a comma delimited value') [CompletionResult]::new('--repeating', 'repeating', [CompletionResultType]::ParameterName, 'Combine any repeating groups into a single field with a comma delimited value') + [CompletionResult]::new('-f', 'f', [CompletionResultType]::ParameterName, 'Only consider full FIX messages containing both BeginString and Checksum') + [CompletionResult]::new('--strict', 'strict', [CompletionResultType]::ParameterName, 'Only consider full FIX messages containing both BeginString and Checksum') [CompletionResult]::new('-s', 's', [CompletionResultType]::ParameterName, 'Strip the whitespace around the = in each field') [CompletionResult]::new('--strip', 'strip', [CompletionResultType]::ParameterName, 'Strip the whitespace around the = in each field') [CompletionResult]::new('-t', 't', [CompletionResultType]::ParameterName, 'Translate tag numbers on non FIX message lines') diff --git a/completion/prefix.bash b/completion/prefix.bash index 381e811..a5ec6e9 100644 --- a/completion/prefix.bash +++ b/completion/prefix.bash @@ -19,7 +19,7 @@ _prefix() { case "${cmd}" in prefix) - opts="-c -d -o -f -r -s -S -t -v -h -V --color --delimiter --only-fix --strict --repeating --strip --summary --tag --value --help --version [message]..." + opts="-c -d -o -r -f -s -S -t -v -h -V --color --delimiter --only-fix --porcelain --repeating --strict --strip --summary --tag --value --help --version [message]..." if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 diff --git a/completion/prefix.fish b/completion/prefix.fish index 65c7b2d..975c7de 100644 --- a/completion/prefix.fish +++ b/completion/prefix.fish @@ -2,8 +2,9 @@ complete -c prefix -s c -l color -d 'Adds colour to the delimiter and = in for F complete -c prefix -s d -l delimiter -d 'Set delimiter string to print after each FIX field' -r complete -c prefix -s S -l summary -d 'Summarise each fix message based on an optional template' -r complete -c prefix -s o -l only-fix -d 'Only print FIX messages' -complete -c prefix -s f -l strict -d 'Only consider full FIX messages containing both BeginString and Checksum' +complete -c prefix -l porcelain -d 'print FIX messages closer to standard format, same as --delimiter \\x01 --strip' complete -c prefix -s r -l repeating -d 'Combine any repeating groups into a single field with a comma delimited value' +complete -c prefix -s f -l strict -d 'Only consider full FIX messages containing both BeginString and Checksum' complete -c prefix -s s -l strip -d 'Strip the whitespace around the = in each field' complete -c prefix -s t -l tag -d 'Translate tag numbers on non FIX message lines' complete -c prefix -s v -l value -d 'Translate the values of some tags (for Side: 1 -> Buy)' diff --git a/man/prefix.1 b/man/prefix.1 index 81ef1ed..d13626c 100644 --- a/man/prefix.1 +++ b/man/prefix.1 @@ -1,10 +1,10 @@ .ie \n(.g .ds Aq \(aq .el .ds Aq ' -.TH prefix 1 "prefix 1.1.5" +.TH prefix 1 "prefix 1.2.0" .SH NAME prefix \- A customizable pretty printer for FIX messages .SH SYNOPSIS -\fBprefix\fR [\fB\-c\fR|\fB\-\-color\fR] [\fB\-d\fR|\fB\-\-delimiter\fR] [\fB\-o\fR|\fB\-\-only\-fix\fR] [\fB\-f\fR|\fB\-\-strict\fR] [\fB\-r\fR|\fB\-\-repeating\fR] [\fB\-s\fR|\fB\-\-strip\fR] [\fB\-S\fR|\fB\-\-summary\fR] [\fB\-t\fR|\fB\-\-tag\fR] [\fB\-v\fR|\fB\-\-value\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fImessage\fR] +\fBprefix\fR [\fB\-c\fR|\fB\-\-color\fR] [\fB\-d\fR|\fB\-\-delimiter\fR] [\fB\-o\fR|\fB\-\-only\-fix\fR] [\fB\-\-porcelain\fR] [\fB\-r\fR|\fB\-\-repeating\fR] [\fB\-f\fR|\fB\-\-strict\fR] [\fB\-s\fR|\fB\-\-strip\fR] [\fB\-S\fR|\fB\-\-summary\fR] [\fB\-t\fR|\fB\-\-tag\fR] [\fB\-v\fR|\fB\-\-value\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fImessage\fR] .SH DESCRIPTION A customizable pretty printer for FIX messages .SH OPTIONS @@ -22,12 +22,15 @@ Set delimiter string to print after each FIX field \fB\-o\fR, \fB\-\-only\-fix\fR Only print FIX messages .TP -\fB\-f\fR, \fB\-\-strict\fR -Only consider full FIX messages containing both BeginString and Checksum +\fB\-\-porcelain\fR +print FIX messages closer to standard format, same as \-\-delimiter \\x01 \-\-strip .TP \fB\-r\fR, \fB\-\-repeating\fR Combine any repeating groups into a single field with a comma delimited value .TP +\fB\-f\fR, \fB\-\-strict\fR +Only consider full FIX messages containing both BeginString and Checksum +.TP \fB\-s\fR, \fB\-\-strip\fR Strip the whitespace around the = in each field .TP @@ -49,4 +52,4 @@ Print version [\fImessage\fR] FIX message to be parsed, if not provided will look for a message piped through stdin .SH VERSION -v1.1.5 +v1.2.0 diff --git a/src/main.rs b/src/main.rs index ff94777..3b78231 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,15 +16,28 @@ fn main() { let n_msgs = msgs.len(); for (i, msg) in msgs.enumerate() { let is_last_line = i + 1 == n_msgs; - prefix::run(msg, is_last_line, &msg_regex, &tag_regex, &summary_regexes, &flags); + prefix::run( + msg, + is_last_line, + &msg_regex, + &tag_regex, + &summary_regexes, + &flags, + ); } } else { let mut lines = io::stdin().lines().peekable(); while let Some(line) = lines.next() { let line = line.unwrap(); let is_last_line = lines.peek().is_none(); - prefix::run(&line, is_last_line, &msg_regex, &tag_regex, &summary_regexes, &flags); + prefix::run( + &line, + is_last_line, + &msg_regex, + &tag_regex, + &summary_regexes, + &flags, + ); } } - } diff --git a/src/prefix/mod.rs b/src/prefix/mod.rs index 7f1ed6c..141bff4 100644 --- a/src/prefix/mod.rs +++ b/src/prefix/mod.rs @@ -3,7 +3,9 @@ mod tags; use clap::ArgMatches; use regex::Regex; use std::{ - collections::HashMap, io::{self, IsTerminal, Write}, process + collections::HashMap, + io::{self, IsTerminal, Write}, + process, }; #[derive(Debug, PartialEq, Clone)] @@ -66,21 +68,30 @@ pub fn get_tag_regex() -> Regex { Regex::new(r"[0-9]+").unwrap() } -pub fn get_summary_regexes(flags: &Options) -> HashMap:: { +pub fn get_summary_regexes(flags: &Options) -> HashMap { let mut summary_regexes = HashMap::::new(); if flags.summary.is_some() { let template = flags.summary.as_ref().unwrap(); let re = Regex::new(r"\d+").unwrap(); for number in re.find_iter(template) { let number = number.as_str(); - summary_regexes.insert(number.to_string(), Regex::new(&format!(r"\b{}\b", number)).unwrap()); + summary_regexes.insert( + number.to_string(), + Regex::new(&format!(r"\b{}\b", number)).unwrap(), + ); } } summary_regexes } - -pub fn run(input: &str, last_line: bool, msg_regex: &Regex, tag_regex: &Regex, summary_regexes: &HashMap::, flags: &Options) { +pub fn run( + input: &str, + last_line: bool, + msg_regex: &Regex, + tag_regex: &Regex, + summary_regexes: &HashMap, + flags: &Options, +) { let mut stdout = io::stdout(); match parse_fix_msg(input, &msg_regex) { @@ -132,7 +143,11 @@ fn print_fix_msg( flags: &Options, ) { let result = if flags.summary.is_some() { - writeln!(stdout, "{}", format_to_summary(fix_msg, regex_by_tag, flags)) + writeln!( + stdout, + "{}", + format_to_summary(fix_msg, regex_by_tag, flags) + ) } else { // Avoid adding an empty new line at the bottom of the output. if last_line && flags.delimiter == "\n" { @@ -222,7 +237,11 @@ fn format_to_string(input: &[Field], flags: &Options) -> String { }) } -fn format_to_summary(input: &[Field], regex_by_tag: &HashMap::, flags: &Options) -> String { +fn format_to_summary( + input: &[Field], + regex_by_tag: &HashMap, + flags: &Options, +) -> String { let template = flags.summary.as_ref().unwrap(); let mut result = String::from(template); for field in input { @@ -240,7 +259,9 @@ fn format_to_summary(input: &[Field], regex_by_tag: &HashMap::, f if template.contains(&tag) { // Use a regex with line boundaries to ensure we don't overwrite partial numbers. // Replace tag numbers in template to tag name. - result = regex_by_tag[tag.as_str()].replace_all(&result, value).to_string(); + result = regex_by_tag[tag.as_str()] + .replace_all(&result, value) + .to_string(); } } if field.tag == 35 { @@ -402,7 +423,8 @@ mod tests { value: false, }; - let regex_by_tag = HashMap::::from([(String::from("55"), Regex::new(r"\b55\b").unwrap())]); + let regex_by_tag = + HashMap::::from([(String::from("55"), Regex::new(r"\b55\b").unwrap())]); let result = format_to_summary(&input, ®ex_by_tag, &flags); let expected = String::from("NewOrderSingle for EUR/USD"); assert_eq!(result, expected); From dd68e2bb4d833234a53329684e94ccccadffa4bb Mon Sep 17 00:00:00 2001 From: Mark Oborne Date: Sat, 19 Jul 2025 20:22:37 +0900 Subject: [PATCH 5/5] chore: fix clippy warnings --- build.rs | 7 ++----- src/prefix/mod.rs | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/build.rs b/build.rs index 24c5dcb..2012283 100644 --- a/build.rs +++ b/build.rs @@ -12,15 +12,12 @@ fn main() -> Result<(), Error> { for shell in [Bash, Fish, PowerShell, Zsh] { let completion_path = clap_complete::generate_to(shell, &mut cmd, "prefix", &out_dir)?; - println!( - "cargo:warning=completion file is generated: {:?}", - completion_path - ); + println!("cargo:warning=completion file is generated: {completion_path:?}"); } let man = clap_mangen::Man::new(cmd); let man_path = Man::generate_to(&man, out_dir)?; - println!("cargo:warning=man file is generated: {:?}", man_path); + println!("cargo:warning=man file is generated: {man_path:?}"); Ok(()) } diff --git a/src/prefix/mod.rs b/src/prefix/mod.rs index 141bff4..1bc8de8 100644 --- a/src/prefix/mod.rs +++ b/src/prefix/mod.rs @@ -77,7 +77,7 @@ pub fn get_summary_regexes(flags: &Options) -> HashMap { let number = number.as_str(); summary_regexes.insert( number.to_string(), - Regex::new(&format!(r"\b{}\b", number)).unwrap(), + Regex::new(&format!(r"\b{number}\b")).unwrap(), ); } } @@ -94,35 +94,35 @@ pub fn run( ) { let mut stdout = io::stdout(); - match parse_fix_msg(input, &msg_regex) { + match parse_fix_msg(input, msg_regex) { FixMsg::Full(parsed) => { - print_fix_msg(&mut stdout, last_line, &parsed, &summary_regexes, flags); + print_fix_msg(&mut stdout, last_line, &parsed, summary_regexes, flags); } FixMsg::Partial(parsed) => { if !flags.strict { - print_fix_msg(&mut stdout, last_line, &parsed, &summary_regexes, flags); + print_fix_msg(&mut stdout, last_line, &parsed, summary_regexes, flags); } else if !flags.only_fix { - print_non_fix_msg(&mut stdout, input, &tag_regex, flags); + print_non_fix_msg(&mut stdout, input, tag_regex, flags); } } FixMsg::None => { if !flags.only_fix { - print_non_fix_msg(&mut stdout, input, &tag_regex, flags); + print_non_fix_msg(&mut stdout, input, tag_regex, flags); } } } } fn handle_broken_pipe(result: io::Result<()>) { - if let Err(e) = result { + if let Err(error) = result { // When piping into certain programs like head, printing to stdout can fail. This is // expected and we do not want to panic, instead we terminate cleanly. Prefix is not // designed to be used for anything besides printing. And this keeps the behaviour closer // to other unix tools that will terminate upon receiving the SIGPIPE (which rust programs ignore by default) - if e.kind() == io::ErrorKind::BrokenPipe { + if error.kind() == io::ErrorKind::BrokenPipe { process::exit(0); } - panic!("Error writing to stdout: {}", e); + panic!("Error writing to stdout: {error}"); } } @@ -130,7 +130,7 @@ fn print_non_fix_msg(stdout: &mut io::Stdout, line: &str, tag_regex: &Regex, fla let result = if flags.tag { writeln!(stdout, "{}", parse_tags(line, tag_regex)) } else { - writeln!(stdout, "{}", line) + writeln!(stdout, "{line}") }; handle_broken_pipe(result); } @@ -203,7 +203,7 @@ fn parse_tags(input: &str, regex: &Regex) -> String { fn add_colour(input: &str, use_colour: bool) -> String { if use_colour { // TODO: Allow configuring colour using ENV variable - format!("\x1b[33m{}\x1b[0m", input) + format!("\x1b[33m{input}\x1b[0m") } else { input.to_string() } @@ -270,7 +270,7 @@ fn format_to_summary( .find(|(msg_type, _)| *msg_type == field.value) .expect("Invalid msg type") .1; - result = format!("{} {}", msg_type, result); + result = format!("{msg_type} {result}"); } } result