Convenience script for command-line 1Password client
Password manager
I am an active user of 1Password password manager. I even pay subscription fee for so called “family account”. It does support the major browsers (Chrome, Firefox, Edge, Brave, Safari) and mobile (iOS and Android) devices. Unfortunately, GUI app is available only for macOS, Windows and Linux, and I want FreeBSD.
I must mention that I tried to live in KeePassX ecosystem and it was, well, unfulfilling. Too many rough edges for my taste.
CLI
There is a command-line version of 1Password client here. It does support both FreeBSD and OpenBSD.
I am using FreeBSD/amd64 version and there is one thing I have noticed right
away: op signin -h
says
You can include 'eval $(op signin)' in scripts
and integrations without checking whether the user is already signed in.
'op signin' will only prompt for authentication if the user is not already
authenticated.
According to my experience it is not true and op sigin
will ask for the
password every time. This is why I do not call it from my script.
The problem with 1Password command-line client for normal usage is that it is
too verbose. Normally I want to provide a part of an item’s name (ideally
regex), choose one of the matching items, and print the values of the fields
the item has. To do that, I am going to tell op
to output information in JSON
format and to use jq to extract what I need.
We are going to write a script which takes one parameter (regex for the item’s name), allows to choose the item if there are several that match the given regex, and then prints all non-empty fields from the item.
Getting a list of all items and filtering it
The master plan is to use op item list
(treat errors as not authorized),
and then filter it with jq
finding the items with matching title
. In bash,
it looks like
pattern=$1 # The first argument to script holds the pattern to search for
# Now let us get the full list of items from 1Password in JSON format. An error
# is considered as "not authorized".
if ! items=$(op item list --format=json 2>/dev/null)
then
echo 'It seems that you are not logged in. Run eval $(op signin)'
# exit here, we are not autorized
fi
res=$(echo "$items" | jq -r ".[] | select(.title | test(\"$pattern\"; \"i\")) | .id, .title")
The command-line client will return an array of items. The jq
command means:
iterate through the array, take only those items where title
matches the
given pattern in a case-insensitive manner (i
), and for each of those items
print two lines: id
of the item and its title
. The -r
passed to jq
means “do not put double quotes around the strings in the output”. The result
is put into res
variable.
Choosing the right item
To choose the right item, in case several are matching the given pattern, we
will use dialog
utility. It is included in FreeBSD and can be installed in,
say, Ubuntu with sudo apt install dialog
. See man 1 dialog
for details.
We will put received in the previous step ids and titles in an array, and if the array length is more than 2 (meaning that more than item matched the given pattern) we will present a dialog box to the user, suggesting to choose one item. Otherwise the item id is just the first element of created array.
The array is created out of multi-line string using bash readarray
built-in
function. The bash code follows.
readarray -t choices <<<"$res"
if [[ ${#choices[@]} -eq 2 ]]
then
echo "Found only one match."
key=${choices[0]}
else
tmp=$(mktemp)
dialog --no-lines --menu "Choose one of $(( ${#choices[@]} / 2 )) matches" 0 0 0 "${choices[@]}" 2>"$tmp"
key=$(<"$tmp")
rm "$tmp"
fi
The -t
option says to readarray
that we are not interested in the newline
symbols, separating lines, and they should be dropped.
The dialog
utility puts its output on the standard error, and it is
redirected to a temporary file, which is deleted immediately after reading the
id of the chosen item in a key
variable. I did not manage to make it work
with streams redirection, without a temporary file. If you know how to do it,
let me know – but first check that your solution works with dialog
(I have
managed to produce many solutions working with other commands – but not with
dialog
).
Printing non-empty fields of the item
After we got the id of the item, we want to get it and print all non-empty
fields. But what is non-empty field? I define field as non-empty if both its
id and value are non-empty strings. Again, we can get item as JSON using op
and filter out what we need using jq
:
op item get "$key" --format=json | jq -r ".fields[] | select(.id | length > 0) | select(.value | length > 0) .label + \" => \" + .value"
Final script
For you convenience, here is the final version of the script I am using:
#!/usr/local/bin/bash
set -eu
# Standard exit codes: see 'man sysexits'
EX_USAGE=64
EX_DATAERR=65
EX_NOPERM=77
if [[ $# -lt 1 ]]
then
echo "Give (case-insensitive) pattern to search in a title as the first argument."
exit $EX_USAGE
fi
pattern=$1
if ! items=$(op item list --format=json 2>/dev/null)
then
# shellcheck disable=SC2016
echo 'It seems that you are not logged in. Run eval $(op signin)'
exit $EX_NOPERM
fi
res=$(echo "$items" | jq -r ".[] | select(.title | test(\"$pattern\"; \"i\")) | .id, .title")
if [[ -z $res ]]
then
echo "No matches found."
exit $EX_DATAERR
fi
readarray -t choices <<<"$res"
if [[ ${#choices[@]} -eq 2 ]]
then
echo "Found only one match."
key=${choices[0]}
else
tmp=$(mktemp)
dialog --no-lines --menu "Choose one of $(( ${#choices[@]} / 2 )) matches" 0 0 0 "${choices[@]}" 2>"$tmp"
key=$(<"$tmp")
rm "$tmp"
fi
op item get "$key" --format=json | jq -r ".fields[] | select(.id | length > 0) | select(.value | length > 0) .label + \" => \" + .value"