Managing Software Licenses
In true nerd fashion. I don’t use a spreadsheet (anymore) to manage the various licenses that I buy for software. CSV files are fine for shorter lists, but become problematic when dealing with more, especially trying to properly format the document. So, I started looking for alternatives.
There are a couple of purpose built programs, generally for enterprises, that claim to manage your software licenses, but I’m not that big, and I don’t have enterprise cash to throw around. So I started looking at SQLite, which is a durable format that even the Library of Congress endorses as a archival format. However, it can be a little obtuse to work with. Plus you can’t just crack open the file in an editor and edit it. So, you need to know SQL and how to work around the command-line tools for it. Still, a tempting choice.
However, there are text-only databases that can be opened and handled by text
editors and command-line tooling. The one that comes to mind is
recutils
, a suite of utilities that can parse, format and error check a
database made up of a text-only schema and it’s associated data. It kind of
looks like this:
%rec: License
%doc: A collection of personal software licenses that I have purchased.
%typedef: OS_t enum Windows macOS Linux CrossPlatform
%key: Id
%type: Id int
%type: Name line
%type: PurchaseDate date
%type: OS OS_t
%mandatory: Name LicenseKey
%auto: Id
Id: 1
Name: Acorn
Version: 8
PurchaseDate: 1970-01-01
LicenseKey:
OS: macOS
The stuff behind the %
are field identifiers, allowing you to define what a
field should contain and conform to. You can set your own types, like I did with
%Id
. Additionally you can set mandatory and unique fields to better wrangle
compliance of data being entered. All in all, it’s a pretty damn simple format
that works really well. Bash, the shell, also has a built in recread
built-in
to read in data like this, so if you’re already handling it on the command-line,
it’s one less thing you have to run. For me, I can get by with the standard
utilities in this ZSH shell script:
#!/usr/bin/env zsh
# VERSION: 2.3
# Hello, future me. This is a mini-zsh program to make working with the software
# license database a bit easier from the command-line. macOS, specifically.
# However, there shouldn't be anything in here that is specific to macOS, except
# maybe the path to the database itself.
# Usage:
## You'll want to set the variables in the '_decrypt-license' and 'license-add'
## functions to the proper path for the encryption key and the license recutils
## file.
# CHANGELOG
## Version 1:
## Initial creation.
## Version 2:
## Add encryption and reverse the sort options on fzf
## Version 2.1:
## Add guard-rails for missing license and keyfiles
## Version 2.1.1:
## Fix comparison for missing files (&& vs ||). Thanks ChatGPT!
## Version 2.1.2:
## Add check for `age` tool
## Version 2.2:
## Not really a fix in this file, but fixes to the database allow this to work
## Version 2.3:
## Add purchase date field and pre-check for 'age' command
if (( $+commands[recsel] )) && (( $+commands[age] ));
then
# Decrypt the license file with the age key, this will dump the file to
# stdout, so all subsequent uses should be a pipeline dealing with the
# output
_decrypt-license()
{
local LICENSE_FILE="path-to-license.rec"
local KEY_FILE="path-to-encryption-key.txt"
if ! (( $+commands[age] ));
then
echo "Unable to find the `age` command-line tool in the PATH, please install before retrying."
return 1
fi
if ! [[ -e "$KEY_FILE" && -e "$LICENSE_FILE" ]];
then
echo "Unable to decrypt the license file, please ensure both the license file exists and the keyfile exists."
echo "Key File: $KEY_FILE"
echo "License File: $LICENSE_FILE"
return 1
fi
age --decrypt --identity "$KEY_FILE" "$LICENSE_FILE"
}
# Add to the license file
license-add()
{
local LICENSE_FILE="path-to-license.rec"
local KEY_FILE="path-to-encryption-key.txt"
# First, lets get some information from the user
read "?Software name: " name
read "?Platform (Windows, macOS, Linux or CrossPlatform): " os
read "?Purchase date (YYYY-MM-DD): " pdate
read "?Version number: " version
read "?License key: " key
if ! [[ -n $name || -n $os || -n $version || -n $key ]];
then
echo "All fields are required!"
return 1
fi
# Decrypt, edit the stream, encrypt.
cp "$LICENSE_FILE" "$LICENSE_FILE".bkup
_decrypt-license | \
recins --verbose \
--type=License \
-f Name -v "$name" \
-f PurchaseDate -v "$pdate" \
-f LicenseKey -v "$key" \
-f "OS" -v "$os" | \
age --encrypt --identity "$KEY_FILE" --output "$LICENSE_FILE"
}
# Search by name (case-insensitive)
license-find()
{
_decrypt-license | \
recsel --type=License --case-insensitive \
--expression="Name = '$1'" \
--print=Name,OS,Version,LicenseKey
}
# Search by OS
license-os() {
_decrypt-license | \
recsel --type=License --case-insensitive \
--expression "OS = '$1'" \
--print-row=Name
}
if (( $+commands[fzf] ));
then
license-fzf() {
local selected=$(_decrypt-license | \
recsel --collapse --type=License \
--print-values=Name | \
fzf --height 10 --reverse)
if [[ -n "$selected" ]];
then
license-find "$selected"
else
return
fi
}
fi
fi
Here, I use recutils
(recsel
and recins
) to manage the actual database,
and age to encrypt/decrypt the data. You could, of course, use whatever
encryption system you want to handle the data at rest. The nice thing here is
that age (and GNUPG) support reading data from stdin
and writing to stdout
,
which makes it perfect for manipulating the database in-memory, without having
to do any jiggery-pokery with temp files and risking data being left somewhere
unencrypted. Much like pass
, this lets each tool do what it does best,
and just strings each tool along, passing the relevant data to/from the user as
needed. There’s even a handy filtering tool in fzf
to help the user find an
entry that they might not know the name of off-hand.
Anyway, I hope this helps someone build their own license system, or other cool tool. Let me know if it helped!
Respond via email.