#!/usr/bin/env python3 """Csvview — CSV/TSV viewer with column alignment, filtering, and search.""" import argparse, csv, io, os, sys, math def color(text, code): return f"\033[{code}m{text}\033[0m" if sys.stdout.isatty() else text def read_csv(path, delimiter=","): if path == "-": content = sys.stdin.read() reader = csv.reader(io.StringIO(content), delimiter=delimiter) else: f = open(path, newline="") reader = csv.reader(f, delimiter=delimiter) rows = list(reader) if path != "-": f.close() return rows def calc_widths(rows, max_rows=None): widths = [] display = rows[:max_rows] if max_rows else rows for row in display: for i, cell in enumerate(row): while len(widths) <= i: widths.append(0) widths[i] = max(widths[i], len(cell)) return widths def format_row(row, widths): parts = [] for i, cell in enumerate(row): w = widths[i] if i < len(widths) else len(cell) parts.append(cell.ljust(w)) return " │ ".join(parts) def color_row(row, widths, header=False): if header: cells = [] for i, cell in enumerate(row): w = widths[i] if i < len(widths) else len(cell) cells.append(color(cell.ljust(w), "36;1")) return " │ ".join(cells) return format_row(row, widths) def cmd_view(args): try: rows = read_csv(args.file, delimiter=args.delimiter) except FileNotFoundError: print(f"Error: File '{args.file}' not found", file=sys.stderr) sys.exit(1) except Exception as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) if not rows: print(f"{color('(empty)', '90')}") return has_header = not args.no_header and rows # Filter if args.filter: col_idx = None if "=" in args.filter: col_name, val = args.filter.split("=", 1) if has_header: try: col_idx = rows[0].index(col_name) except ValueError: print(f"Error: Column '{col_name}' not found", file=sys.stderr) sys.exit(1) else: try: col_idx = int(col_name) - 1 except ValueError: print(f"Error: Invalid column index", file=sys.stderr) sys.exit(1) if col_idx is not None: rows = [rows[0]] + [r for r in rows[1:] if col_idx < len(r) and val in r[col_idx]] # Search if args.search: rows = [rows[0]] + [r for r in rows[1:] if any(args.search.lower() in c.lower() for c in r)] # Stats if args.stats: print(f"\n {color('CSV Stats', '1')}") print(f" {color('─' * 40, '90')}") print(f" Rows: {len(rows) - (1 if has_header else 0)}") print(f" Columns: {len(rows[0]) if rows else 0}") if has_header: print(f" Headers: {', '.join(color(h, '36') for h in rows[0])}") if len(rows) > 1: print(f" File size: {os.path.getsize(args.file) if args.file != '-' else '?'} bytes") print() return widths = calc_widths(rows, max_rows=50) # Truncation message max_rows = 500 if len(rows) > max_rows: print(f"{color(f'Showing {max_rows} of {len(rows)} rows. Use --search to narrow.', '33')}") rows = rows[:max_rows] print() if has_header: print(color_row(rows[0], widths, header=True)) print(color("─" * (sum(widths) + 3 * len(widths)), "90")) data = rows[1:] else: data = rows for row in data: print(color_row(row, widths)) print(f"\n{color(f'{len(data)} rows', '90')}") return def main(): parser = argparse.ArgumentParser( description="Csvview — CSV/TSV viewer", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=( "Examples:\n" " csvview data.csv # View CSV\n" " csvview data.tsv --delimiter $'\\t' # View TSV\n" " csvview data.csv --stats # Show column stats\n" " csvview data.csv --search error # Search for 'error'\n" " csvview data.csv --filter 'status=active' # Filter by column\n" " cat data.csv | csvview - # Pipe input\n" ), ) parser.add_argument("file", help="CSV file (use - for stdin)") parser.add_argument("--delimiter", default=",", help="Field delimiter (default: ,)") parser.add_argument("--no-header", action="store_true", help="File has no header row") parser.add_argument("--filter", help="Filter: column=value or col_idx=value") parser.add_argument("--search", help="Search term across all columns") parser.add_argument("--stats", action="store_true", help="Show column statistics") parser.add_argument("--version", action="version", version="csvview 1.0.0") args = parser.parse_args() cmd_view(args) if __name__ == "__main__": main()