Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ We love CSV tools and use them all the time! Here are a few that we rely on:

- improve zebra, blend with terminal bg #64 (@RVC2020)
- store typed cells, honor locale #66 (@RVC2020)
- support col args by index #66 (@RVC2020)

#### 0.7.1 (Jul '26)

Expand Down
54 changes: 54 additions & 0 deletions bin/riff
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env ruby

#
# Shows a git diff with inline Rust test modules removed.
#
# Usage: riff [git-diff arguments]
# riff
# riff --cached
# riff HEAD -- src/grid.rs
#

require "tempfile"
require_relative "run_kit"

include RunKit

CFG = /\A#\[cfg.*\btest\b/
OPEN = /\Amod tests \{/
CLOSE = /\A}\s*\z/

def textconv(filename)
idx = 0
lines = file_read(filename).lines
while idx < lines.length
if lines[idx].match?(CFG) && lines[idx + 1]&.match?(OPEN)
mod_end = ((idx + 2)...lines.length).find { lines[_1].match?(CLOSE) }
if mod_end
idx = mod_end + 1
next
end
end

print lines[idx]
idx += 1
end
end

if ARGV.first == "--textconv"
textconv(ARGV.fetch(1))
exit
end

script = Pathname($PROGRAM_NAME).expand_path.to_s.shellescape
Tempfile.create("riff-attributes") do |attrs|
attrs.write("*.rs diff=riff\n")
attrs.flush
system(
"git",
"-c", "core.attributesFile=#{attrs.path}",
"-c", "diff.riff.textconv=#{script} --textconv",
"diff", "--textconv", *ARGV
)
exit($?.exitstatus || 1)
end
20 changes: 19 additions & 1 deletion cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,10 @@ impl Main {
Self { args }
}

pub fn run(&self) -> Result<()> {
pub fn run(mut self) -> Result<()> {
// read input
let input = self.load()?;
self.resolve_columns(&input);

// --peek
if self.args.peek {
Expand Down Expand Up @@ -124,6 +125,23 @@ impl Main {
// data transformation
//

fn resolve_columns(&mut self, grid: &Grid) {
for columns in [
&mut self.args.big1,
&mut self.args.big2,
&mut self.args.big3,
&mut self.args.deselect,
&mut self.args.rscale,
&mut self.args.scale,
&mut self.args.select,
&mut self.args.sort,
] {
for column in columns {
*column = grid.resolve_header(column).to_owned();
}
}
}

fn transform(&self, mut grid: Grid) -> Result<Grid> {
// --filter
if let Some(ref needle) = self.args.filter {
Expand Down
27 changes: 25 additions & 2 deletions src/builder/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,23 @@ impl Builder {
/// Builds the table. This will return an error if something is off in the
/// builder.
pub fn build(self) -> Result<Table> {
let Builder { grid, options, record_options, select, deselect } = self;
let Builder { grid, options, record_options, mut select, mut deselect } = self;
let grid = match grid {
Some(grid) => grid?,
None => grid::from_cells(Vec::new())?,
};

// Finalize terminal-sensitive options before constructing Table. Renderers
// assume color/theme are concrete and never start terminal probes.
let options = record_options.unwrap_or_default().merge(options);
let mut options = record_options.unwrap_or_default().merge(options);
resolve_columns(&grid, &mut select);
resolve_columns(&grid, &mut deselect);
for (column, _) in &mut options.bigs {
*column = grid.resolve_header(column).to_owned();
}
for (column, _) in &mut options.color_scales {
*column = grid.resolve_header(column).to_owned();
}
let resolved = crate::resolved::Resolved::new(options);

// (de)select, then make sure options work with the list of headers
Expand All @@ -117,6 +125,12 @@ impl Builder {
// standalone helpers
//

fn resolve_columns(grid: &Grid, columns: &mut [String]) {
for column in columns {
*column = grid.resolve_header(column).to_owned();
}
}

fn pick_columns(mut grid: Grid, select: &[String], deselect: &[String]) -> Result<Grid> {
if !select.is_empty() {
grid = grid.select(select)?;
Expand Down Expand Up @@ -368,6 +382,15 @@ mod tests {
);
}

#[test]
fn test_column_indexes() {
let grid = named_grid(&["name", "score"], &[vec!["alice", "1234"]]);
let table = build(Table::builder().load_grid(grid).select(["2", "1"]).big("2").color_scale("2", ColorScale::Green));
assert_eq!(["score", "name"], table.headers());
assert_eq!(ColumnBig::Big, table.options.column_big("score"));
assert_eq!(Some(ColorScale::Green), table.options.color_scale("score"));
}

#[test]
fn test_select_can_be_set_after_loading() {
let rows = [BTreeMap::from([("name".to_owned(), "alice".to_owned()), ("score".to_owned(), "1234".to_owned())])];
Expand Down
49 changes: 41 additions & 8 deletions src/grid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ impl Grid {
self.types[index]
}

/// Convert a 1-based column index to its header, leaving names unchanged.
pub fn resolve_header<'a>(&'a self, reference: &'a str) -> &'a str {
reference
.parse::<usize>()
.ok()
.and_then(|index| index.checked_sub(1))
.and_then(|index| self.headers.get(index))
.map_or(reference, String::as_str)
}

pub fn position(&self, name: &str) -> Result<usize> {
self.headers.iter().position(|str| str.eq_ignore_ascii_case(name)).ok_or_else(|| Error::MissingColumn {
column: name.to_owned(),
Expand All @@ -104,22 +114,29 @@ impl Grid {
/// Keep only the columns with the given names, in the given order.
pub fn select(self, names: &[String]) -> Result<Self> {
let positions = self.positions(names)?;
let project = |source: &[String]| positions.iter().map(|&i| source[i].clone()).collect();
let project_cells = |source: &[Cell]| positions.iter().map(|&i| source[i].clone()).collect();
let types = positions.iter().map(|&index| self.types[index]).collect();
Ok(Self::from_parts(project(&self.headers), self.rows.iter().map(|row| project_cells(row)).collect(), types))
Ok(self.project(&positions))
}

/// Remove the columns with the given names.
pub fn deselect(self, names: &[String]) -> Result<Self> {
let positions = self.positions(names)?;
let keep = self
let names = names
.iter()
.map(|name| self.position(name).map(|index| self.headers[index].as_str()))
.collect::<Result<Vec<_>>>()?;
let positions = self
.headers
.iter()
.enumerate()
.filter_map(|(ii, header)| (!positions.contains(&ii)).then_some(header.clone()))
.filter_map(|(index, header)| (!names.iter().any(|name| header.eq_ignore_ascii_case(name))).then_some(index))
.collect::<Vec<_>>();
self.select(&keep)
Ok(self.project(&positions))
}

fn project(self, positions: &[usize]) -> Self {
let headers = positions.iter().map(|&index| self.headers[index].clone()).collect();
let types = positions.iter().map(|&index| self.types[index]).collect();
let rows = self.rows.iter().map(|row| positions.iter().map(|&index| row[index].clone()).collect()).collect();
Self::from_parts(headers, rows, types)
}

//
Expand Down Expand Up @@ -285,6 +302,19 @@ mod tests {
);
}

#[test]
fn test_resolve_header() {
let grid = abc();
assert_eq!("name", grid.resolve_header("1"));
assert_eq!("score", grid.resolve_header("02"));
assert_eq!("SCORE", grid.resolve_header("SCORE"));
assert_eq!("0", grid.resolve_header("0"));
assert_eq!("3", grid.resolve_header("3"));

let numeric = Grid::new(vec!["2".to_owned(), "name".to_owned()], Vec::new()).unwrap();
assert_eq!("name", numeric.resolve_header("2"));
}

#[test]
fn test_select() {
let grid = abc().select(&["score".to_owned(), "name".to_owned()]).unwrap();
Expand All @@ -298,6 +328,9 @@ mod tests {
let grid = abc().deselect(&["score".to_owned()]).unwrap();
assert_eq!(["name"], grid.headers());
assert_eq!(["bob"], grid.rows()[0].as_slice());

let grid = abc().select(&["score".to_owned(), "name".to_owned(), "score".to_owned()]).unwrap();
assert_eq!(["name"], grid.deselect(&["score".to_owned()]).unwrap().headers());
}

#[test]
Expand Down
12 changes: 10 additions & 2 deletions tests/smoke.bats
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ run_tty() {
[[ "$output" != *"Ideal"* ]]

# -b
run_ok --color=off --width 80 -b cut "$ROOT/tests/test.csv"
run_ok --color=off --width 80 -b 2 "$ROOT/tests/test.csv"
[[ "$output" == *"Ideal"* ]]

# -bb
Expand All @@ -284,7 +284,7 @@ run_tty() {

@test "--scale and --rscale" {
# scale
run_ok --color=on --theme dark --width 80 --scale carat "$ROOT/tests/test.csv"
run_ok --color=on --theme dark --width 80 --scale 1 "$ROOT/tests/test.csv"
[[ "$output" == *$'\e[48;2;'* ]]

# reverse scale
Expand Down Expand Up @@ -461,9 +461,17 @@ run_tty() {
[[ "$output" == *"│ name │"* ]]
[[ "$output" == *"│ alice │"* ]]
[[ "$output" != *"score"* ]]

# indexes always refer to original columns
run_ok --color=off --width 80 --select 2,1,2 --deselect 2 "$ROOT/tests/test.json"
[[ "$output" == *"name"* ]]
[[ "$output" != *"score"* ]]
}

@test "--sort" {
run_ok --color=off --width 80 --sort 1 --head 2 "$ROOT/tests/test.json"
[[ "$output" == *"alice"* ]]

# before head
run_ok --color=off --width 80 --sort name --head 2 "$ROOT/tests/test.json"
[[ "$output" == *"alice"* ]]
Expand Down