Shell scripting with Elixir
arathunku.com
arathunku.com
Of course, one can take a Shell script only so far. Eventually a Real Programming Language is the inescapable (pun intended) conclusion. [Insert "Always was" meme].
[1] I wrote about it here.
https://www.evalapply.org/posts/shell-aint-a-bad-place-to-fp...
That whole series of posts is basically a glorified bookmark for yours truly. Tactics and tricks accumulated over the years. I refer back to bits and bobs from time to time. Writing them was a lot of, ah, fn.
I think `map`, `filter`, and `reduce` (or `accumulate`) are automatic in pipelines; and have usually been enough for me. Still, it has a bunch of stuff one can pinch in a hurry. Starred!
For long running scripts perhaps. BEAM idles at ~50Mb, and startup is often more than 500ms.
Eg:
$ time ./hello
hello world
real 0m0,009s
user 0m0,003s
sys 0m0,007s
$ time elixir hello.exs
hello world
real 0m0,755s
user 0m0,992s
sys 0m0,304s
Where the first example is a BASH script.The default startup does a lot of things you simply don't need in most shell scripts. Starting a bunch of threads, setting up dist, I can't remember what else off the top of my head.
$ time erl -eval 'io:format("~s~n", [os:cmd("echo hello world")]), halt(0, [{flush, false}]).' -noshell -start_epmd false
hello world real 0m0,332s
user 0m0,461s
sys 0m0,092s
I spent some time last year trying to get those numbers down but this is as far as I made it. Do you know of any more flags that might help?A couple years ago that got me to 100 ms
https://news.ycombinator.com/item?id=25243246
Today's measurements with FreeBSD 13.2 on the same Pentium G3470
time /bin/sh -c "echo hi"
hi
real 0m0.004s
user 0m0.001s
sys 0m0.002s
time /usr/local/bin/bash -c "echo hi" hi hi
real 0m0.006s
user 0m0.007s
sys 0m0.000s
time /usr/local/lib/erlang26/bin/erl -eval 'io:format("hi~n"), halt(0, [{flush, false}])' Erlang/OTP 26 [erts-14.2.1] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [jit:ns] [dtrace] [sharing-preserving]
hi
real 0m0.197s
user 0m0.178s
sys 0m0.018s
time /usr/local/lib/erlang26/bin/erl -eval 'io:format("hi~n"), halt(0, [{flush, false}])' -noshell -start_epmd false -boot no_dot_erlang hi
real 0m0.183s
user 0m0.181s
sys 0m0.011s
This doesn't look very good, actually! FreeBSD has OTP 21-26 available through pkg, I ran the same thing on each of them several times to warm up, and get (real time): 21: 81ms
22: 81ms
23: 74ms
24: 129ms
25: 135ms
26: 177ms
So maybe there's some new stuff that needs to be avoided now.Unfortunately, that final flag doesn't change much on 26 for me. IIRC I dropped a bunch of flags after concluding that no shell and no epmd were the only flags that I noticed improvements with.
Send me an email (in profile), and I'll try to find some time to look closer.
Interestingly, what drove me to Ruby initially is exactly writing scripting tasks in a C#/C++ culture where Ruby would have been considered harmful.
Now Elixir can be used for the same type of tasks, but only better (eg: trivial parallelized downloads!)
Parallel downloads in C# boil down to
using var http = new HttpClient();
var urls = new[]
{
"http://example.com/file1.jpg",
"http://example.com/file2.jpg",
/* ... */
};
var files = await Task.WhenAll(
urls.Select(http.GetByteArrayAsync));
(you can replace '.GetByteArrayAsync' with .GetFromJsonAsync, .GetStreamAsync and more depending on your needs and e.g. forward the incoming data directly to files on the disk or another source)I'm surprised the testing worked like that, without trying myself. When I last fiddled with it, I stole a page from how they recommend you do it in Livebook, and had `ExUnit.start(autorun: false)` at the top and `ExUnit.run()` at the bottom. I didn't realize you could just `ExUnit.start()` by itself up front.
For anyone else who's tried an approach like this, have you managed to get doctests to work? I really like them, and think they're especially appropriate where you probably want lots of examples like here, but haven't been able to figure out how to stick them in ad-hoc scripts like this. I get some issue about the code not being compiled and available.
Mix.install([
{:req, "~> 0.2"},
{:floki, "~> 0.30.0"}
]) require 'bundler/inline'
gemfile do
source 'https://rubygems.org'
gem 'json', require: false
gem 'nap', require: 'rest'
gem 'cocoapods', '~> 0.34.1'
end double = fn a -> add.(a, a) end
double.(4)
It starts to look a bit like a weird untyped OCaml / F# if you use pattern matching: f = fn
x, y when x > 0 -> x + y
x, y -> x * y
endA lot of attempts to fill this space have large gaps in this aspects. Not sure how this scores on those dimensions.
For up to ~50 lines of script, bash is the most effective thing I found so far. Tried zsh, fish, python, ruby, but for such glue code bash rules.
That is true for sure if you write bash scripts quasi daily. However if you don't it becomes very easy to forget, at least for me. That's why I'm the type of person that tries to look out for alternatives in different languages.
I've had and read from cover to cover, Bash, sed, and AWK books, yet when I have to get back to them I still only remeber the most fundamental features that I use every couple of days or weeks.
I have 20+ years of .NET, VS IDE and debugger experience I can leverage - it doesn't make sense for me to use anything else.