Looking for library similar to Unix "column" command

I am building a cli tool and want to implement the following functionality: I want to take in any multi-line string (this multi-line string will be the output of some other Unix command), and, if a delimiter is specified on the command line like ",", then I want to replace all ","s with tabs, which are then pretty aligned in stdout. This is exactly what the Unix cli tool column can do. Here an example:

echo "one,two\nthree,four" | column --table --separator ","

will print

one    two
three  four

I have found some crates that can do the pretty table-like printing for me (tabled - Rust or tabwriter - Rust), but I haven't found a library/crate that can also handle the splitting on a delimiter/seperator. Of course, I could (and would) implement that part myself (maybe package it as a "column" library crate myself), but I just wanted to ask here if anyone knows of crates that achieve this use-case already. Looking forward to your suggestions! Thanks!

xsv table does this, and it's implemented by parsing the data as CSV (which seems compatible with what you want) and then writing CSV but with a tab delimiter:


pub fn run(argv: &[&str]) -> CliResult<()> {
    let args: Args = util::get_args(USAGE, argv)?;
    let rconfig = Config::new(&args.arg_input)
        .delimiter(args.flag_delimiter)
        .no_headers(true);
    let wconfig = Config::new(&args.flag_output)
        .delimiter(Some(Delimiter(b'\t')));

    let tw = TabWriter::new(wconfig.io_writer()?)
        .minwidth(args.flag_width)
        .padding(args.flag_pad);
    let mut wtr = wconfig.from_writer(tw);
    let mut rdr = rconfig.reader()?;

    let mut record = csv::ByteRecord::new();
    while rdr.read_byte_record(&mut record)? {
        wtr.write_record(record.iter().map(|f| {
            util::condense(Cow::Borrowed(f), args.flag_condense)
        }))?;
    }
    wtr.flush()?;
    Ok(())
}

But if you don't need CSV and literally just need to replace , with \t, then... just do that and feed the rest to tabwriter.

2 Likes

Thanks for response, tabwriter does seem very suitable for my needs. Do you have a concrete example of how I can write a function that takes a String, replaces all \t characters in all lines with spaces (i.e. creates the table using tabwriter) and returns the result as a string again. Tabwriter uses the concept of a Writer, which I am not yet familiar with. Thanks!

Like this, maybe?

/*
[dependencies]
tabwriter = "^1.2.1"
*/
use std::io::Write;
use tabwriter::TabWriter;

fn as_table(s: &str) -> String {
    let mut tw = TabWriter::new(vec![]);
    
    write!(&mut tw, "{}", s.replace(",", "\t")).unwrap();
    
    tw.flush().unwrap();

    String::from_utf8(tw.into_inner().unwrap()).unwrap()
} 

fn main() {
    let raw = "col1,column2,col3\n\
               entry1,ent2,entry3";

    let table = as_table(raw);

    let expect = "col1    column2  col3\n\
                  entry1  ent2     entry3";
    
    assert_eq!(table, expect);
}

Rust explorer.

1 Like

Thanks for the solution, used it and it works well. But I have some questions on the syntax: Do you know why the vec![] is required in Tabwriter? Seems kind of unnecessary

TabWriter is a Write-trait wrapper and what you create it with is the Writer being wrapped. In the example that was a Vec<u8> but in your code it will probably be Stdout or such.

1 Like

Well yes, we don't really do anything with the possibilities offered by the interface here. The vector we provide is probably (I haven't checked, but it's the only thing that makes sense here) the buffer/writer/sink used internally by the TabWriter to write its output to. You could for example optimize here and provide a buffer that has already allocated the right size to hold the whole output and therefore avoid unnecessary and expensive allocation.

1 Like

I mean not really, like I mentioned, I want to convert a String to another String with the tabs replaced/formatted. I don't print this to stdout, but save each line of the String as a vector later. You can find the code at watchbind/lines.rs at main · fritzrehde/watchbind · GitHub

Oh, I see. In that case, I'll just rephrase to say that it's necessary because TabWriter wraps a Write implementor that you have to supply.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.