Dictlets

The most generic definition of dictlet is: an item that can be queried for metadata. For all practical intends and purposes this definition is silly though, as currently luci only supports dictlets that are text files. I’ll leave that generic definition here to indicate that, in theory, every thing could theoretically be a dictlet: a folder, a url, a jpeg…

But, for now, let’s just assume a dictlet is a text file that is enriched with some metadata that is not necessary for it’s primary purpose.

Development

Adding metadata

‘Developing’ a dictlet means adding structured metadata to it, in a way that doesn’t affect it’s original function. Practically that means adding comments that luci can read and interpret as a dictionary. luci uses so-called dictlet readers to extract relevant metadata. The only implemented reader at the moment is the YamlDictletReader, which inherits most of it’s logic from the base class TextFileDictletReader.

This reader checks a text file for certain marker string, and then extracts the metadata according to a set of rules:

  • check if the file contains the lucify keyword. if it does not:
    • if the file doesn’t contain the lucify keyword, check if it contains the default ‘root’ key for the lucifier that is used at the moment (most often that keyword is __luci__, but for example the DownloadLucifier uses url – check the source code of the lucifier in question for details)
  • if neither the lucify keyword is found, nor the lucifier specific keyword, luci will read the whole file
  • now, the file is read in this way:
    • store the characters that precede the keyword (on the line that contains it) as prefix (if any)
    • read all lines below that keyword into a buffer, removing the prefix for each line (usually those are comment indicators)
    • ignore all lines that do not start with the prefix
    • stop once the end of the file is reached, or a line contains the two keywords lucify and stop
    • hand the content of the buffer to the right reader sub-class, which parses the text into a dictionary

So, let’s say we have a csv file (people.csv) with the following content:

# lucify
# author: Markus Binsteiner
# date: "2018-03-03"
# tags:
#   - luci
#   - example

date,subject,result
2018-01-01,alice,okish
2018-01-02,tom,gone crazy

Using the luci metadata lucifier, we would extract the following metadata:

❯ luci metadata text_file_metadata_example.csv
author: Markus Binsteiner
date: '2018-03-03'
tags:
- luci
- example

The reader stops reading, once it doesn’t find a line starting with # anymore. We can make it stop beforehand though, if we want:

# lucify
# author: Markus Binsteiner
# date: "2018-03-03"
# lucify: stop
# tags:
#   - luci
#   - example

This would stop after the date key. The ‘:’ character after lucify is optional.

We can also split up our metadata into two blocks if we want to, getting the same result as above:

# lucify
# author: Markus Binsteiner
# date: "2018-03-03"

date,subject,result
2018-01-01,alice,okish
2018-01-02,tom,gone crazy

# tags:
#   - luci
#   - example

To control luci behavior like the adding of a command-line interface, we put the metadata relevant to this under the special __luci__ key. Like so:

#! /usr/bin/env luci
#
# __luci__:
#   __default_lucifier__: template
#   vars:
#     - he
#     - she
#
What he said: {{ he }}
Then, she said: {{ she }}

The __default_lucifier__ key is optional, but handy if you use the luci she-bang (#! /usr/bin/env luci) as it tells luci which lucifier to use in this case (unfortunately, it’s not possible to specify more than one argument in a she-bang line). Everything under var determines how the command-line interface for the dictlet is rendered. As this behavior can be controlled quite detailed, check out the section below and/or the relevant help page.

Debugging

Until you get familiar with dictlets, it’s quite likely you get the formatting wrong, miss a whitespace, or use the wrong key-word. In order to help with those issues, you can use both the metadata and debug lucifiers.

Let’s assume we have a (very weird) dictlet (called debug_example) like this:

# lucify: start
#
# defaults:
#    default1: value1
#    default2: value2
#    help_string: "{{:: defaults.default2 ::}} help string"
#    user_default: 42
#
# __luci__:
#     defaults_luci:
#         luci1: "{{:: defaults.default1 ::}}"
#     doc:
#       help: "{{:: defaults.help_string ::}} for dictlet"
#       short_help: "dictlet short help example"
#       epilog: "dictlet epilog example"
#     vars:
#       __user_input__.arg1:
#         meta:
#           alias: number
#           type: list
#           required: false
#         doc:
#           help: {{:: defaults.help_string ::}}
#           short_help: {{:: __luci__.defaults_luci ::}}
#         cli:
#           enabled: true
#           option:
#             type: int
#             multiple: false
#             default: {{:: defaults.user_default ::}}
# lucify: stop
And the user said: "{{ __user_input__.arg1 }}"

We can get the resulting metadata, with and without user input using the metadata lucifier:

❯ luci metadata debug_example
__user_input__:
  arg1: 42
defaults:
  default1: value1
  default2: value2
  help_string: value2 help string
  user_default: 42

❯ luci metadata debug_example --number 12
__user_input__:
  arg1: 12
defaults:
  default1: value1
  default2: value2
  help_string: value2 help string
  user_default: 42

This gives us the ‘useable’ metadata we could use for example when doing template-ing:

❯ luci template debug_example
And the user said: "42"

❯ luci template debug_example --number 12
And the user said: "12"

If we need more information, about what goes on internally, we can use the debug lucifier. This lucifier comes with a lot of options to control the values to display (``luci debug –help``_). Here’s how to display the variables resulting from user input:

❯ luci debug -mu debug_example --number 12

metadata: user vars:

__user_input__:
  arg1: 12

Command-line interface basics

Getting a dictlet (called minimal_arg) to create a command-line interface can be as easy as adding:

# __luci__:
#   vars:
#     - arg1
#     - arg2

This adds two command-line options, named arg1 and arg2, which both are optional. This is what it looks like when called with the metadata lucifier:

❯ luci metadata minimal_arg --help
Usage: luci metadata minimal_arg [OPTIONS]

Options:
  --arg1 TEXT  n/a
  --arg2 TEXT  n/a
  --help       Show this message and exit.

User input will go into the keys with the same name:

❯ luci metadata minimal_arg --arg1 value1
arg1: value1

Now we can make one of the arguments mandatory, like so:

# __luci__:
#    vars:
#      - arg1
#      - arg2:
#          meta:
#            required: true

We use the meta key to provide generic information about the option. There is another key, cli which is used for more command-line specific properties of a cli parameter. Let’s call the above dictlet without the required option:

❯ luci metadata minimal_arg --arg1 value1
Usage: luci metadata minimal_arg [OPTIONS]

Error: Missing option "--arg2".

And we can give the other argument a default value:

# __luci__:
#    vars:
#      arg1:
#         meta:
#           default: VALUE_1
#      arg2:
#         meta:
#           required: true

Notice how the value of the var key is a dict now, instead of a list. luci doesn’t really care which you use. Let’s try to call the command again, providing arg2 for a change:

❯ luci metadata minimal_arg --arg2 value2
arg1: VALUE_1
arg2: value2

If you need more fine-grained control, check out the cli specific help page.