D as a scripting language
September 12, 2014
Many of us encounter small tasks involving some file operations, text processing and
running other processes. These are usually solved using either shell or glue
languages (often called scripting languages because of this) like Python, Perl
or Ruby. Compiled statically typed languages are rarely used in such cases
because popular ones usually require too much effort to write and run a simple
script. However some static languages can actually be used here easily instead
of scripting ones, providing their usual benefits such as checking for types and
typos. Here's an example of using D in such scenario.
Earlier this year I discovered a nice tool called
TimeSnapper that sits in background
and saves everything that happens on your screen by doing screenshots on regular
time intervals. It can also collect some stats about apps you run and sites you
browse. During first month I used this feature in the demo version and proved
my suspicion that I waste way too much time on reddit and LiveJournal. ;) Later I
switched to free version of TimeSnapper that doesn't collect and report stats
but just makes the screenshots and allows playing them like a video with timeline.
This serves me as a precise visual memory so that later I can always see what happened
at certain moments. Found a weird unwanted shortcut on my desktop, what app placed it
here? Quick look at its creation time, replay what I was doing then, ah, here's
the thing I was installing at that moment. Here's a library I built on day X, but in
which project was I using it? Replay that hour, see the opened project, ah, now I
remember.
The problem is however that TimeSnapper saves screenshots as PNG files. I configured
it to save one every minute, so the shots fill up 5 GBs in a few days, after
which TS deletes the old ones. This is wasteful. Since most consecutive shots
are similar (most of the screen doesn't change much if at all) a video format that
stores just the difference will take much less space, even if the compression is still
lossless. Moreover, I even know a video codec that is just perfect for such
screen captures: ScreenPressor! So I want to
convert sets of PNG images to video files compressed with that codec. What tools
do I have for this task? VirtualDub can do, when used manually via its UI, not sure
about its command-line interface though. And it won't open a sequence of those PNGs
so easily because their file names (given by TimeSnapper) look like timestamps,
not consequent numbers.
Then I remember about AviSynth I have installed.
It can handle a sequence of PNGs (again, if files are named properly) and present it
as uncompressed video. And then I need a tool that will compress this video to
an AVI file. It's found quickly: AVS2AVI. Having ScreenPressor, AviSynth and AVS2AVI
on board all I need is a simple script that will orchestrate them, preparing
the files and AVS script and running the tool.
Below is full source code of my script, in D. It should be run from the directory
where TimeSnapper stores its images. For each day TS will create a folder named
after this date. Inside will be PNG files with timestamps in their filenames.
My script scans current directory for subfolders and for each
subfolder (except for today's) it creates a video file. To make a video file
it renames the PNG files to 0001.png, 0002.png, etc. so that AviSynth could open
the sequence, remembering for each renamed file its original name.
Then a text file is created: AVS script that tells AviSynth what to do.
It opens the image sequence, and applies some resizing.
Then avs2avi.exe is executed with the AVS script file, output file and codec id
as arguments. After its execution ends, the images are renamed back
so that TimeSnapper could find them. When my program is run with
argument "delete" it will also delete the images in those subfolders for which
corresponding video file already exists.
import std.stdio, std.file, std.array, std.algorithm, std.string, std.range,
std.process, std.datetime, std.typecons;
void compressImages(string dirname, bool deleting)
{
if (dirname.startsWith(".\\")) dirname = dirname[2..$];
auto now = Clock.currTime;
string today = format("%04d-%02d-%02d", now.year, now.month, now.day);
if (dirname == today) return writeln("skipping ", dirname, " - today.");
auto files = dirEntries(dirname, "*.png", SpanMode.shallow, false).array;
if (files.empty) return;
auto outfile = dirname ~ ".avi";
if (outfile.exists) {
if (deleting) {
foreach(fname; files)
remove(fname);
return writeln(dirname, ": ", files.length, " files deleted.");
} else return writeln(outfile," already exists.");
}
sort(files);
Tuple!(string,string)[] renamings;
foreach(i, fname; files) {
string newname = format("%s\\%04d.png", dirname, i);
if (fname != newname) {
renamings ~= tuple(cast(string)fname, newname);
rename(fname, newname);
}
}
auto f = File("conv.avs","wt");
f.writefln("clip = ImageSource(\"%s\\%%04d.png\", 0, %d, 4)", dirname, files.length - 1);
f.writeln("clip.LanczosResize(clip.width / 2, clip.height / 2)");
f.close;
auto args = cast(char[][])["avs2avi.exe", "conv.avs", outfile, "-c", "scpr"];
writeln(args);
execute(args);
foreach(t; renamings)
rename(t[1], t[0]);
}
void main(string[] argv)
{
bool deleting = argv.length > 1 && argv[1]=="delete";
foreach(d; dirEntries(".", "*", SpanMode.shallow, false).filter!(nm => nm.isDir))
compressImages(d, deleting);
}
As you can see the source code is quite short, clear and straightforward. You don't
see many type annotations and declarations, there is no boilerplate usually
associated with statically typed languages. The same script if written in Python or Ruby
would be about the same size. How do we run it? Just like in Ruby you would
just type "ruby script.rb" here you just type "rdmd script.d" .
rdmd (which comes with main D compiler - DMD) will compile and run the program,
caching the binary so that subsequent
calls will skip compilation and run the ready binary straight away. That means
the first run is fast, the following runs are super fast.
If you want to use this script too but don't have DMD installed, I compiled it
to an exe file and packed together with ScreenPressor and AVS2AVI in one archive
here. All you
need to have installed (besides TimeSnapper) is AviSynth. This is, by the way, another
advantage of using D instead of usual scripting languages: it's easy to make a binary
that can be run without installing any interpreters, frameworks or runtimes, and
it's just a few hundred KBs, not several MBs you would get using Python-to-exe or
Ruby-to-exe packers.
Here's an example result of using TimeSnapper and stuff from this post: video of
my participation in this year's ICFP contest:
Here a compiler was written in D and the newly created language was used to program
a bot to play Pacman. Unfortunately I got a working solution only one hour after
the contest ended, so
the version submitted was a very dirty work-in-progress that crashed often due
to wrong arguments order in one function. That brain-dead version happened to
gain a positive number of points but of course landed near the end of scoreboard.
A working version with source code can be found
here.
tags: programming
|