Modernizing AWK, a 45-year old language, by adding CSV support
benhoyt.com
benhoyt.com
csvquote test.csv | awk '{print $1, $2}' | csvquote -u
[1] https://github.com/dbro/csvquoteI'd argue that needing a different tool for handling records in text so that you can pass it to a tool for handling records in text is a bit too far.
> ah yes, awk's famously narrow focus, [...]
"GNU's Not Unix"
https://github.com/adamgordonbell/csvquote
The History section of the readme explains that they are historically related.
https://miller.readthedocs.io/en/latest/why/ has a nice section on "why miller":
> First: there are tools like xsv which handles CSV marvelously and jq which handles JSON marvelously, and so on -- but I over the years of my career in the software industry I've found myself, and others, doing a lot of ad-hoc things which really were fundamentally the same except for format. So the number one thing about Miller is doing common things while supporting multiple formats: (a) ingest a list of records where a record is a list of key-value pairs (however represented in the input files); (b) transform that stream of records; (c) emit the transformed stream -- either in the same format as input, or in a different format.
OTOH, zsv count only takes 0.114 sec for me (but of course, as I'm sure you know, that also only counts rows not columns which some might complain about). { EDIT: and I've never tried to time a "parse only" mode for c2tsv. }
From a performance perspective, strictly delimiter-separated values { again, ironically redundant ;-) } can be parsed with memchr. On Linux, memchr should be SIMD vectorized at least on x86_64 glibc via ELF 'i' symbols. So, while you give up SIMD on the "messy part" with a byte-at-a-time DFA, you regain it on the other side. (I have no idea if Apple gives you SIMD-vectorized memchr.)
Send to a file and segmentation (for parallel handling of segments) is also a simple application of memchr rather than needing an index of where rows start. You just split by bytes and find the next newline char. (Roughly). This can get you 16..128X speed-ups (today, anyway, on just one host) depending upon what you do.
Conversion to something properly byte-delimited basically restores whatever charm you might have thought ?SV had. I can only imagine a few corner cases where running directly off a complex format like quoted CSV makes sense ("tiny" data, "cannot/will not spend 2X space+must save input", "cannot/will not spend time to recompress", "running on a network fileysystem shared with those who refuse simplicity".) These cases are not common (for me). When they do happen, perf is usually limited by other things like network IO, start-up overheads, etc. Usually that little extra bit to write buffers out to a pipeline will either not matter or be outright immediately repaid in parallelism, parsing simplicity, or both.
Converting from any ASCII to even faster binary formats has a similar story, but usually with even more perf improvement (depending..) and more "choices" like how to represent strings [1]. Fully pre-parsed, the performance of conversion matters much less. (Whatever the ratio of processings per initial parse is.) Between both parallelism and ASCII->binary, however fast you make your serial zsv parser/ETL stuff, actual data analysis may still run 10,000 times slower than it could be on just 1 CPU (depending upon what throttles your workloads..you may only get 10000x for CPU local L1 resident nested loop stuff). { But we veer now toward trying to cram a databases course into an HN comment. :) And I'm probably repeating myself/others. Direct email from here may work better. }
Sigh... If only everyone had used ASCII (and Unicode!) characters 30 and 31 for delimiters, since they are actual delimiter characters: https://en.wikipedia.org/wiki/Delimiter#ASCII_delimited_text
I don't think I've ever seen them in the wild. :-(
No improvement of CSV handling will ever improved on that.
"ASCII Delimited Text – Not CSV or TAB delimited text": https://news.ycombinator.com/item?id=7474600
I suppose the best way would be a suite of tools to edit them that are compatible with existing editors? TBH a great value-add for adoption would be versioning s.t. you can see (and revert) when other tools mess up your files.
Their "but why" section should really go into more detail about why filters are not 100% if your data has the possibility of containing your preferred line/record separator.
But real world, I have never had a situation where a csv-to-awk filter (written in awk of course) did not work.
https://git.sr.ht/~sforman/sv/tree/master/item/sv.py
(Python code for ASCII-separated values. TL;DR: it's so stupid simple that I feel any objection to using this format has just got to be wrong. It's like "Hey, we don't have to keep stabbing ourselves in the face." ... "But how will we use our knives?" See? It's like that.)
How do I edit the data file in a text editor?
More generally, how do I edit the entire data file, including field/record/etc seperators, in a editor that can display only characters that are valid content of data fields?
(Not that CSV (barring de-facto-nonstandard extensions like rfc4180's `"b CRLF bb"` and `"b""bb"`) is any good for this either, of course.)
Choose your tools wisely, I'm not aware the situation.
> How do I edit the data file in a text editor?
Use a non-sucky text editor.
For example, AFAICR NotePad++ displays those characters. Windows doesn't have any ultra-easy way to input them, but within NP++ you can copy-paste them.
Great! Now go back to the other half of the requirement: how do I store all characters that NotePad++ displays in the content of a data field?
@include "csv"
BEGIN { CSVMODE = 1 }
{ print $2 }
https://earthly.dev/blog/awk-csv/#gawkextlibUSV is like CSV and simpler because of no escaping and no quoting. I can donate $50 to you or your charity of choice as a token of thanks and encouragement.
That makes no sense. Sure, they've chosen significantly less common separator characters than something like ','. but they are still characters that may appear in the data. How do you represent a value containing ␟ Unit Separator in USV?
In-band signalling is ever going to remain in-band signalling. And in-band signalling will need escaping.
USV is for the 99.999% of cases that don't embed USV Control Picture characters in the content.
If you need escaping, then I can suggest you consider USVX which is USV + extensions for escaping and other conveniences, or you can use your content's own escaping such as ampersand escaping for HTML text, or you can use any other format such as JSON, XML, SQL, etc.
I agree that wanting to put one of these characters in a csv is super rare. The vast majority of use cases would not notice that certain inout characters are prohibited. But certain inout characters _are_ prohibited, which means that encoders must check this case, and tools using those encoders neex to handle its failure.
In a way, the fact that failure will be super rare will make it more dangerous, because people will omit these checks because "it won't happen for them" -- and then at some point it will wreak havoc.
And with USVX all we've gained is less backslashes but more bytes per separator, so for usual data that doesn't contain a comma in every field, the USVX encoding will even increase file size, without requiring less code anywhere.
I admit there is no ideal solution, though; while out-of-band signalling (i.e. length-prefixing) avoids all of these issues (which is why binary formats are universally length-prefixing formats), escaped formats are work much better for humans. And if a human wouldn't need to read it, one wouldn't use csv anyway, usually.
Not doing the obviously correct superior easy strategy because there's some infrequent weird corner case prevents any kind of progress. Perfection is the enemy of good enough, engineering is balancing constraints, ad nauseum...
There are many cases where the "solution" to the CSV inband signalling problem was to just reject values with commas, because they should never come in and if they ever do they should be investigated because they weren't valid data for whatever the CSV was storing. The whole problem is that programmers don't think to do that. The siren call of the string append function is just too strong, especially when programmers don't even realize they should be resisting.
This is the root of all spreadsheet evil, right here.
TSV. Replace/strip/ban tab + newline from fields when writing. Done. If you need those characters, encode them using backslashes if you absolutely must (i.e. you're an Excel weenie doing Excel weenie things that really aren't "spreadsheet" things but since you only know of one hammer you then use that hammer to do everything)
$ cat t.usv && echo
id␟name␟age␞1␟Bob "Billy" Smith␟42␞2␟Jane
Brown␟37
$ goawk -F␟ -vRS=␞ -vOFS=, '{ print $1, $2, $3 }' t.usv
id,name,age
1,Bob "Billy" Smith,42
2,Jane
Brown,37
I've also added explicit tests for ASCII and Unicode unit and record separators, just to ensure I don't regress: https://github.com/benhoyt/goawk/commit/215652de58f33630edb0...And this perennial favorite here at HN:
https://ronaldduncan.wordpress.com/2009/10/31/text-file-form...
I've been toying with the idea of starting a change.org campaign, asking Microsoft to add support to Excel for importing and exporting such files. I don't see any way to make any progress if Microsoft won't push the industry forward.
With CSV-like data, bulk conversion from quoted-escaped RFC4180 CSV to a simpler-to-parse format is the best plan for several reasons. First, it may "catch on", help Microsoft/R/whoever embrace the format and in doing so squash many bugs written by "data analyst/scientist coders". Second, in a shell "a|b" runs programs a & b in parallel on multi-core and allow things like csv2x|head -n10000|b or popen("csv2x foo.csv"). Third, bulk conversion to a random access file where literal delimiters cannot occur as non-delimiters allows trivial file segmentation to be nCores times faster (under often satisfied assumptions). There are some D tools for this bulk convert in https://github.com/eBay/tsv-utils and a much smaller stand-alone Nim tool https://github.com/c-blake/nio/blob/main/utils/c2tsv.nim . Optional quoting was always going to be a PITA due to its non-locality. What if there is no quote anywhere? Fourth, by using a program as the unit of modularity in this case, you make things programming language agnostic. Someone could go to town and write a pure SIMD/AVX512 converter in assembly even and solve the problem "once and for all" on a given CPU. The problem is actually just simple enough that this smells possible.
I am unaware of any "document" that "standardizes" this escaped/lossless TSV format. { Maybe call it "DSV" for delimiter separated values where "delimiters actually separate"? Ironically redundant. ;-) } Someone want to write an RFC or point to one? It can be just as "general/lossless" (see https://news.ycombinator.com/item?id=31352170).
Of course, if you are going to do a lot of data processing against some data, it is even better to parse all the way to down to binary so that you never have to parse again (well, unless you call CPUs loading registers "parsing") which is what database systems have been doing since the 1960s.
That should be parallelism friendly, even with UTF-8, where an ascii tab or newline byte always mean tab and newline.
A fast streaming converter into my suggested "DSV" can also be faster end-to-end. These kinds of things can vary a lot based upon how many columns rows have. I could not find "huge.csv". So, to be specific/possibly reproducible, using the 151492068 bytes of data from here:
http://burntsushi.net/stuff/worldcitiespop.csv
put into /dev/shm and making a symlink to huge.csv and then using the csvbench.sh in the goawk distro, I got (best of 3 elapsed times): Python 2.88user 0.01system 0:02.91elapsed 99%CPU (9460maxresident)k
Goawk 1.15user 0.07system 0:01.11elapsed 110%CPU (8460maxresident)k
Go 0.86user 0.04system 0:00.84elapsed 107%CPU (7300maxresident)k
Go vs. Goawk time ratios were similar to Ben's article but inverted. Probably a number of columns effect. frawk failed to compile for me because my Rust was not new enough, according to the error messages. On the same data: c2tsv+gawk 0.22user 0.04system 0:00.57elapsed 46%CPU (2680maxresident)k
(as *c2tsv<huge.csv|gawk -F'\t' '{nfs+=NF} END{print NR, nfs}'*)
c2tsv+mawk 0.62user 0.09system 0:00.46elapsed 155%CPU (2780maxresident)k
(as *c2tsv<huge.csv|mawk -F'\t' '{nfs+=NF} END{print NR, nfs}'*)
c2tsv V2 0.19user 0.07system 0:00.23elapsed 115%CPU (2680maxresident)k
(as *c2tsv<huge.csv|wc -l*, very similar to c2tsv<huge.csv>/dev/null)
c2tsv V3 0.19user 0.00system 0:00.20elapsed 99%CPU (2632maxresident)k
(as *c2tsv<huge.csv>/dev/null*)
A little 100 line Nim program combined with a standard utility seems to be about 4x faster (0.84/0.23) than Go results even though said program writes out all the data again. How can this be? Well, my pipe IO is usually around 4.4 GB/s (as assessed by a dd piped to a read-only sink) while 151e6/.2=only 755 MB/s. So it need only use ~17% of available pipe BW.I also did a quick test using csvcut to pull out a single field, and compared it to GoAWK. Looks like GoAWK is about 4x as fast here:
$ time csvcut -c agency_id huge.csv >/dev/null
real 0m25.977s
user 0m25.240s
sys 0m0.424s
$ time goawk -i csv -H -o csv '{ print @"agency_id" }' huge.csv >/dev/null
real 0m6.584s
user 0m7.434s
sys 0m0.480sI imagine there's some use case where AWK will be able to come up with output that csvkit can't.
But for simple cases, csvkit's invocation is easier to remember.
awk -F '^"|","|"$|,' '{print $2,$3}' whatever.csv
The above works perfectly well, it handles quoted fields, or even just unquoted fields.... This snippet is taken from a presentation I give on AWK and BASH scripting.That's the thing about AWK, it's already does everything. No need to extended it much at all.
However, the decimal separator that I was taught at primary school and have used in handwriting all my life is neither comma, nor period, but '·'.
828.497.171.614,2?
They use spaces or dots.
But if I add my 2 cents - I prefer 1,000.00 because I think it is similar to commas and full-stops in writing, where a comma is mainly a reading/speaking guide to help break up a sentence (or in this case a number) and a full stop is a much harder termination of a sentence (in the case of a number the official origin where int/non-fractional part ends).
I’m afraid we can’t really change that on a whim. Why would we, anyway?
To be compatible with the world, so Germans can use international software, and could sell software to international people.
It is the only software I know of where you regularly hear of "implementation" -- which in this context means installing the software, not writing it -- projects being planned to take years, actually taking twice as long, and sometimes being abandoned altogether when the prospective client finds out it just can't be done.
I mean, it is SAP you're talking about, right?
[0] Yes, that shows my limited perception.
This is basically the classical problem of democratic systems, when they need to balance the interests of different sized groups. Numbers alone don't make a fair solution. And unless you have the power to force them, you will not convince everyone to follow you just by arguing with numbers anyway.
In my opinion, when we are talking about a data interchange format like CSV, having a simple, common format would be far more practical and efficient than allowing each country to decide for itself its own standard. Having dealt with exactly this problem in a global SaaS product where a minority of clients submitted CSV files with commas for decimal separators, I can say it would have made the parsing code a lot simpler and more robust if our system (and countless others like it) did not need to build in exceptions for this minority use case.
You guys are already slowly encroaching on milliards and billiards in other languages with your illogical short scale numbers.
The way I see it, CSV's purpose is information storage and transfer, not presentation.
Presentation is where you decide the font face, the font size, cell background color (if you are viewing the file through a spreadsheet editor) etc. Number formatting belongs here.
In information transfer and storage it's much more important that the parsing/generation functions are as simple as possible. So let's go with one numeric format only. I think we should use the dot as a decimal separator since it's the most common separator in all programming languages. Maybe extend it to include exponential notation as well, because that is what other languages like json support. But that's it.
I hold the same opinion about dates tbh.
(The same goes for dates, btw - yyyy-mm-dd or death)
https://en.m.wikipedia.org/wiki/ISO_8601
The only real problem with that format is the distasteful "T" in the middle (but, hey, at least it is whitespace-free!)
On the question wether new parsers shouldn't be able to read it... if I have to build them or maintain them, then *those* parsers shouldn't be able to read it. I don't want to have to deal with the whole messiness of the thing, and I know how to change Excel's locale to generate CSV in a reasonable format. If it's something that someone else maintains and I can mostly ignore the crazy number formatting as well as other quirks, I could live with it. But I would still prefer if they didn't implement any of it, because the extra code could make the parts that I need worse (for example, by provoking a segfault, or making a line ambiguous).
Even if it's parsers that other people use and I never use, I still would prefer if they didn't do it, because that would increase the overall possibility of me having to deal with another (sigh) semicolon-separated CSV file.
CSV is only an exchange format if there is no user generated strings in the data... If there are, then you'll almost certainly screw up the encoding when someones name has a newline or comma or speech mark in it, or some obscure unicode etc. Even moreso if awk is part of your toolkit.
Laughs in SSIS…
There are some significant tools (or common add-ins for them) that don't entirely respect RFC4180. Though I see few files that breach it these days, thre are tools that break with conforming files (looking at you, Excel, trying to be clever about anything isn't conclusively provable not to be a date).
Our clients use it all the time, to the point where we'd lose sales if we didn't support it, but CSV is far from a safe way to transport data IMO. Each time a new requirement to deal with CSV comes in I treat it as a custom format that may or may not be something like RFC4180.
That's great to hear.
Are you planning to add support for xml, json, etc next? Something like Python's `json` module that gives you a dictionary object.
Chapter 2 is the complete description of the language in 40 pages.
I think this link works...
https://ia803404.us.archive.org/0/items/pdfy-MgN0H1joIoDVoIC...
eBay's TSV Utilities: Command line tools for large, tabular data files. Filtering, statistics, sampling, joins and more.
includes csv to tsv: https://github.com/eBay/tsv-utils
I think F#-interactive (FSI) with its Hindley-Milner type-inference, would have been a much better base for a shell.
let x = 1;
x is now an integer. Now you can't do printfn "%s" x;
Only printfn "%i" x;
So it has strong static typing, but most of the time you don't need to be explicit about them. It can even infer function types.If you use FSI (F# interactive( it will always print the signatures in between, so that it's really easy to explore interfaces.
LOL, this is amazing...
What makes it "so hard". $object.Proprety or $object.Method() is the same. new versus new-object? [type] vs type ?
I phrase that carefully. "Better"? "Worse"? Very subjective. But in the current environment, "likely to beat out CSV"? Oh, most definitely yes.
A solid upside is a single encoding story for JSON. CSV is a mess and can't be un-messed now. Size bloat from endless repetition of the object keys is a significant disadvantage, though.
The emergence and constantly increasing complexity of these small, bespoke DSLs like this or jq does not inspire confidence in me.
That. Pipes and unstructured binary data isn't compositional enough, making the divide between the kinds of things you can express in the language you use to write a stage in a pipeline and the kinds of things you can express by building a pipeline too large.
>In general, using FPAT to do your own CSV parsing is like having a bed with a blanket that’s not quite big enough. There’s always a corner that isn’t covered. We recommend, instead, that you use Manuel Collado’s CSVMODE library for gawk.
fastest to slowest: zsv (0.07), xsv (0.16), goawk (0.42), python (~1.6)
Obviously, does not tell the whole story as this test was limited to "count" and an interpreted language is expected to always be slower compared to a precompiled command, but, it might be relevant to a user deciding what tool to use. Also, might be instructive as to room for improvement in the go code (or possibly the go code could use the c lib)-- I note that even if the goawk command is '{}' the runtime is still about the same.
full results:
---
goawk:
1000001 7000007 real 0m0.435s user 0m0.435s sys 0m0.031s
1000001 7000007 real 0m0.413s user 0m0.419s sys 0m0.024s
1000001 7000007 real 0m0.425s user 0m0.430s sys 0m0.024s
xsv:
1000000 real 0m0.157s user 0m0.141s sys 0m0.013s
1000000 real 0m0.156s user 0m0.141s sys 0m0.012s
1000000 real 0m0.158s user 0m0.142s sys 0m0.013s
zsv:
1000000 real 0m0.066s user 0m0.053s sys 0m0.010s
1000000 real 0m0.077s user 0m0.060s sys 0m0.012s
1000000 real 0m0.069s user 0m0.056s sys 0m0.010s
python:
1000001 7000007 real 0m1.589s user 0m1.553s sys 0m0.026s
1000001 7000007 real 0m1.583s user 0m1.550s sys 0m0.025s
1000001 7000007 real 0m2.122s user 0m1.675s sys 0m0.037s
---
The script for this was:
---
echo 'goawk:'
(time goawk -i csv '{ w+=NF } END { print NR, w }' < worldcitiespop_mil.csv) 2>&1 | xargs
(time goawk -i csv '{ w+=NF } END { print NR, w }' < worldcitiespop_mil.csv) 2>&1 | xargs
(time goawk -i csv '{ w+=NF } END { print NR, w }' < worldcitiespop_mil.csv) 2>&1 | xargs
echo 'xsv:'
(time xsv count < worldcitiespop_mil.csv) 2>&1 | xargs
(time xsv count < worldcitiespop_mil.csv) 2>&1 | xargs
(time xsv count < worldcitiespop_mil.csv) 2>&1 | xargs
echo 'zsv:'
(time zsv count < worldcitiespop_mil.csv) 2>&1 | xargs
(time zsv count < worldcitiespop_mil.csv) 2>&1 | xargs
(time zsv count < worldcitiespop_mil.csv) 2>&1 | xargs
echo 'python:'
(time python3 count.py < worldcitiespop_mil.csv) 2>&1 | xargs
(time python3 count.py < worldcitiespop_mil.csv) 2>&1 | xargs
(time python3 count.py < worldcitiespop_mil.csv) 2>&1 | xargs
Yeah, sorry about huge.csv -- I found it online somewhere originally by searching for something like "large csv example", but can't for the life of me find it now. It's a monster 1.5GB file with 286 columns including quoted fields (whereas worldcitiespop only has a few columns and it doesn't look like it has quoted fields). I can upload to a file transfer service and send a link to you if you want ... though I should really update my benchmarks to use an easily-downloadable file instead.
But for most everyday usage, even for large inputs, GoAWK's performance is quite sufficient. The CSV support, and its use as a Go library -- they're more important to me than raw speed at this point.
The problem is commas inside quoted strings, as in the example "Smith, Bob".
I believe the field-separator option on awk will break the quoted string at the interior comma.
awk -F'"' -v OFS='' '{ for (i=2; i<=NF; i+=2) gsub(",", "", $i) } 1' infile
1. https://unix.stackexchange.com/questions/48672/remove-comma-...