I am migrating from ksh
to fish
. I am finding that I miss the ability to define an associative array, hash table, dictionary, or whatever you wish to call it. Some cases can be simulated as in
set dictionary$key $value
eval echo '$'dictionary$key
But this approach is heavily limited; for example, $key
may contain only letters, numbers, and underscores.
I understand that the fish
approach is to find an external command when one is available, but I am a little reluctant to store key-value information in the filesystem, even in /run/user/<uid>
, because that limits me to "universal" scope.
How do fish
programmers work around the lack of a key-value store? Is there some simple approach that I am just missing?
Here's an example of the sort of problem I would like to solve: I would like to modify the fish_prompt
function so that certain directories print not using prompt_pwd
but using special abbreviations. I could certainly do this with a switch
command, but I would much rather have a universal dictionary so I can just look up a directory and see if it has an abbreviation. Then I could change the abbreviations using set
instead of having to edit a function.
You can store the keys in one variable and values in the other, and then use something like
if set -l index (contains -i -- foo $keys) # `set` won't modify $status, so this succeeds if `contains` succeeds
echo $values[$index]
end
to retrieve the corresponding value.
Other possibilities include alternating between key and value in one variable, though iterating through this is a pain, especially when you try to do it only with builtins. Or you could use a separator character and store a key-value pair as one element, though this won't work for directories because variables cannot contain \0 (which is the only possible separator for paths).
Here is how I implemented the alternative solution mentioned by @faho
I'm using '__' as seperator.
function set_field --argument-names dict key value
set -g $dict'__'$key $value
end
function get_field --argument-names dict key
eval echo \$$dict'__'$key
end
@faho's answer got me thinking about this and there are a few this I wanted to add.
At first I wrote a small set of fish functions (A sort of library, if you will) that dealt with serialization, you would call a dict
function with a key name, an operation (get
, set
, add
or del
) and it would use global variables to keep track of keys and their values. Works fine for flat hash
es/dict
s/objects, but felt somewhat unsatisfactory.
Then I realized I could use something like jq
to (de-)serialize JSON. That would also make it a lot easier to deal with nesting, plus that allows having different dict
s which use the same name for a key
without any issues. It also separates "dealing-with-environment-variables" and "dealing-with-dicts(/hashes/etc)", which seems like a good idea. I'll focus on jq
here, but the same applies to yq
or pretty much anything, the core point is: Serialize data before storing, de-serialize when reading, and use some tool to work with such data.
I then proceeded to rewrite my functions using jq
. however I soon realized it was easier to just use jq
without any functions. To summarize the workfolow, let's consider OP's scenario and imagine we want to use abbreviations for User folders, or even better, we wanna use icons for such folders. To do that, let's assume we use Nerdfonts and have their icons availabe. A quick search for folders on Nerdfont's cheat sheet show we only have folder icons for the home folder (f74b
), downloads(f74c
) and images(f74e
), so I'll use Material Design Icon's "File document box" (f719
) for documents, and Material Design Icon's "Video" (fa66
) for Videos.
So our Codepoints are:
\uf74b
\uf74c
\uf74e
\uf719
\ufa66
So our JSON is:
{"~":"\uf74b","downloads":"\uf74c","images":"\uf74e","documents":"\uf719","videos":"\ufa66"}
I kept it in a single line for a reason which will become obvious now. Let's visualize this using jq
:
echo '{"~":"\uf74b","downloads":"\uf74c","images":"\uf74e","documents":"\uf719","videos":"\ufa66"}' | jq
For completeness sake, here's how it looks with Nerdfonts installed:
Now let's store this as a variable:
set -g FOLDER_ICONS (echo '{"~":"\uf74b","downloads":"\uf74c","images":"\uf74e","documents":"\uf719","videos":"\ufa66"}' | jq -c)
jq -c
interprets JSON and outputs JSON in a compact
structure, i.e., a single line. Ideal for storing variables.
If you need to edit something you can use jq
, lat's say you want to change the abbreviation for documents to "doc" instead of an icon. Just do:
set -g FOLDER_ICONS (echo $FOLDER_ICONS | jq -c '.["documents"]="doc"')
The echo
part is for reading a variable, and the set -g
is for updating the variable. So those can be ignored if you're not working with variables.
As for retrieving values, jq
also does that, obviously. Let's say you want to get the abbreviation for the documents
folder, you can simply do:
echo $FOLDER_ICONS | jq -r '.["documents"]'
It will return doc
. If you leave out the -r
it will return "doc"
, with quotes, since strings are quoted in JSON.
You can also remove keys pretty easily, i.e.:
set -g FOLDER_ICONS (echo $FOLDER_ICONS | jq -c 'del(."documents")')
will set
the variable FOLDER_ICONS
to the result of reading it and passing its contents to jq -c 'del(."documents")'
, which tels jq
to delete the key "documents"
and output a compact representation of the JSON, i.e. a single line.
Everything I tried worked perfectly fine with nested JSON objects, so it seems like a pretty good solution. It's just a matter of keeping the operations in mind:
reading .["key"]
writing .["key"]="value"
deleting del(."key")
jq
also has many other nice features, I wanted to showcase a bit of them so I tried looking for stuff that might be nice to include here. One of the things I use jq
for is dealing with wayland stuff, especially swaymsg -t get_tree
, which I've just ran and, with a mere 4 workspaces with a single window in each, outputs a 706-line JSON from hell (Was 929 when I wrote this, 6 windows across 5 workspaces, later I closed 2 windows I was done with so I came back here and re-ran the command to share the lowest possible value).
To give a more complex example of how jq
might be used, here's parsing the swaymsg -t get_tree
:
swaymsg -t get_tree | jq -C '{"id": .id, "type": .type, "name": .name, "nodes": (.nodes | map(.nodes) | flatten | map({"id": .id, "type": .type, "name": .name, "nodes": (.nodes | map(.nodes) | flatten | map({"id": .id, "type": .type, "name": .name}))}))}'
This will give you a tree with only id
, type
, name
and nodes
, where nodes
is an array of objects, each consisting of the id
, type
, name
and nodes
of the children, with the children nodes
also being an array of objects, now consisting of only id
, type
and name
. In my case, it returned:
{
"id": 1,
"type": "root",
"name": "root",
"nodes": [
{
"id": 2147483646,
"type": "workspace",
"name": "__i3_scratch",
"nodes": []
},
{
"id": 184,
"type": "workspace",
"name": "1",
"nodes": []
},
{
"id": 145,
"type": "workspace",
"name": "2",
"nodes": []
},
{
"id": 172,
"type": "workspace",
"name": "3",
"nodes": [
{
"id": 173,
"type": "con",
"name": "Untitled-4 - Code - OSS"
}
]
},
{
"id": 5,
"type": "workspace",
"name": "4",
"nodes": []
}
]
}
You can also easily make a flattened version of that with jq
by slightly changing the command:
swaymsg -t get_tree | jq -C '[{"id": .id, "type": .type, "name": .name}, (.nodes | map(.nodes) | flatten | map([{"id": .id, "type": .type, "name": .name}, (.nodes | map(.nodes) | flatten | map({"id": .id, "type": .type, "name": .name}))]))] | flatten'
Now instead of having a key nodes
, the child nodes are also in the parent's array, flattened, in my case:
[
{
"id": 1,
"type": "root",
"name": "root"
},
{
"id": 2147483646,
"type": "workspace",
"name": "__i3_scratch"
},
{
"id": 184,
"type": "workspace",
"name": "1"
},
{
"id": 145,
"type": "workspace",
"name": "2"
},
{
"id": 172,
"type": "workspace",
"name": "3"
},
{
"id": 173,
"type": "con",
"name": "Untitled-4 - Code - OSS"
},
{
"id": 5,
"type": "workspace",
"name": "4"
}
]
It's pretty nifty, not limited to environment variables, and solves pretty much every problem I can think of. The only con is verbosity, so it may be a good idea to write a few fish functions for dealing with that, but that's beyond the scope here, as I'm focusing on a general approach to (de-)serialization of key-value mappings (i.e., dict
s, hash
es, object
s etc), which can be (also) used with environment variables. For reference, a good starting point if dealing with variables might be:
function dict
switch $argv[2]
case write
read data
set -xg $argv[1] "$data"
case read, '*'
echo $$argv[1]
end
end
This simply deals with reading and writing to a variable, the only reason it's worth sharing is, first, that it allows piping something to a variable, and second, that it sets a starting point to make something more complex, i.e. automatically piping the echo
ed value to jq
, or adding an add
operation or whatever.
There's also the option of writing a script to deal with that, instead of using jq
. Ruby's Marshal
and to_yaml
seems like interesting options, since I like ruby, but each person has their own preferences. For Python, pickle
, pyyaml
and json
seem worth mentioning.
It's worth mentioning I'm not affiliated to jq
in any way, never contributed nor even posted anything on issues or whatever, I just use it, and as someone who used to write scripts whenever I had to deal with JSON or YAML, it was quite surprising when I realized how powerful it was.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With