3. Tutorial: Peek and Pipe
In the previous example, we had a look at the contents of a folder with ls
and find
.
3.1. Select
Let’s pick out one specific file and have a closer look at the second photo we found:
> E = find("pics_tree")
...
> F = getfiles(E) # these first two steps can be combined to 'F = findfiles("pics_tree")'
...
> f = F[2]
FileEntry("/foo/exifuse/pics/pics_tree/bar/u.png", -rw-r--r--, 83761 bytes)
(Note that Julia, unlike Python, indexes its vectors from 1, not 0.)
We can also use functions to access a specific file, as in (we’ll use that seemingly ponderous approach below):
> f = first(F)
...
> f = second(F)
...
3.2. Exify Your First Photo
By default, the REPL shows a basic summary description of this file. We want to know more:
> ex = exify(f)
:SourceFile = /foo/exifuse/pics/pics_tree/bar/u.png
:ExifTool__ExifToolVersion = 11.88
:Other__FileName = u.png
:Other__Directory = /foo/exifuse/pics/pics_tree/bar
:Other__FileSize = 82 kB
:Other__FilePermissions = rw-r--r--
...
:Gamma = 2.2
:PixelsPerUnitX = 3779
:PixelsPerUnitY = 3779
:PixelUnits = meters
:ImageSize = 356x151
:Megapixels = 0.054
Whoa! That’s a massive info dump right here!
What happened here? The exify
function calls out to our trusted exiftool
friend and converts the resulting metadata into a nice Julia data structure; that data is stored in our variable ex; and the REPL than immediately displays it.
We can get specific fields of this large EXIF structure in several different ways:
Via the full tag name:
> s = ex.Time__FileModifyDate "2023:04:28 12:53:26+02:00"
Via the short tag name:
> s = ex.FileModifyDate "2023:04:28 12:53:26+02:00"
Quite convenient, but still quite keyboard-intensive a task—were it not for..
3.3. Tab Completion
Remembering, let alone typing EXIF names is tedious. Luckily the Julia REPL support tab-completion. Try this:
> ex.File[press <Tab> twice] FileAccessDate FileInodeChangeDate FileModifyDate FileName FilePermissions FileSize FileType FileTypeExtension > ex.FileM[press <Tab> twice]
Voilà—tag names at your fingertips!
As an exercise, try to select and exify a different file from our list (we used f[2]
above to select the second one), and to display some of its EXIF fields. Hint: maybe you found this already, but using <Cursor-up/-down> let’s us go back in the REPL history and avoid even more typing.
3.5. Get Into the Flow and Pipe It
..that will soon become out dear friend, after probably hurting our heads good:Above we looked for file entries in a whole hierarchy, weeded out the files, selected one of them, transformed it into an EXIF data structure, and had a look at its date. We did something like this (we will here just use the first file instead of the second):
> E = find("pics_tree") > F = getfiles(E) > f = first(F) > ex = exify(f) > s = exif2shortdate(ex)
In each step, we call a function and pass it on to a subsequent one. We can write this all in one line:
> s = exif2shortdate(exify(first(getfiles(find("pics_tree")))))
Having fun yet? Not really, I suppose. This is less typing, but at the expense of transparency and more brackets. In addition, the natural flow of things is upended—we read this line from left to right, but the actual order of what is happening is from right to left.
Julia provides a very neat way of writing this differently. Using the so-called pipe operator |>
, we can create a pipeline the neatly expresses the flow of data transformations:
> find("pics_tree") |> getfiles |> first |> exify |> exif2shortdate
This allows for a quite natural workflow. We first just type:
> find("pics_tree")
If we are happy with what we see, we use <Cursor-Up> to get back the last command, and just append the next step:
> find("pics_tree") |> getfiles
We look at the plausibility of the output, go <Cursor-Up>, and.. rinse and repeat.
What is actually happening here? the pipe operator is essentially just another way of writing a function call: x |> f
is simply equivalent to calling f
with x
which is just f(x)
. So you can always just rewrite some foo |> bar
as bar(foo)
and should get the same result. Try it:
> sin(3.14)
> 3.14 |> sin
...
> length([1,2,3])
> [1,2,3] |> length
...
> files |> length
> f |> filesize
...
We will go into pipes in detail later. For now just notice how here we don’t use round brackets within the pipeline, as we pass in function names and not their evaluation. (We will use brackets again in pipes, but that’s for another day..)