Zsh Mailing List Archive
Messages sorted by:
Reverse Date,
Date,
Thread,
Author
new function passphrase generator
- X-seq: zsh-workers 53908
- From: Clinton Bunch <cdb_zsh@xxxxxxxxxxx>
- To: "zsh-workers@xxxxxxx" <zsh-workers@xxxxxxx>
- Subject: new function passphrase generator
- Date: Mon, 1 Sep 2025 20:50:24 -0500
- Archived-at: <https://zsh.org/workers/53908>
- List-id: <zsh-workers.zsh.org>
Uses zsh/random and diceware to generate a passphrase. I seem to have
problems with my completion setup so I wasn't able to do serious testing
on the completion function. Hopefully someone will test it out.
diff --git a/Completion/Zsh/Function/_diceware
b/Completion/Zsh/Function/_diceware
new file mode 100644
index 000000000..a706ccd70
--- /dev/null
+++ b/Completion/Zsh/Function/_diceware
@@ -0,0 +1,18 @@
+#compdef diceware
+
+# Completion function for the diceware Zsh function.
+# Provides suggestions for options like --capitalize, --separator,
--numbers,
+# --num-words, and --file, as well as file path suggestions for the
wordlist.
+
+local curcontext="$curcontext" state line
+typeset -A opt_args # To store arguments of options if needed (e.g.,
--separator=X)
+local -a args # Array to build our arguments specification
+
+_arguments -C -s -S \
+ '(-c --capitalize)'{-c,--capitalize}'[Capitalize the first letter of
each word]' \
+ '(-s --separator)'{-s+,--separator}'[Use SEP as the word
separator]:separator string:_request_prompt "separator string"
separator-string' \
+ '(-N --numbers)'{-N+::,--numbers::}'[Include COUNT random numbers
pasted to random words (default: 1)]::count of numbers:_request_prompt
"number count" num-count' \
+ '(-n --num-words)'{-n+,--num-words}'[Specify the number of words in
the passphrase (default: 6, min 2)]::word count:_request_prompt "word
count" word-count' \
+ '(-f --file)'{-f+,--file}'[Specify the path to the diceware
wordlist]:wordlist file:_files -g "*.txt"' \
+ '*::arguments:_files' # Catch any other non-option arguments as files
(though our function expects none after options)
+
diff --git a/Functions/Misc/diceware b/Functions/Misc/diceware
new file mode 100644
index 000000000..5b838cccc
--- /dev/null
+++ b/Functions/Misc/diceware
@@ -0,0 +1,207 @@
+#!/usr/bin/zsh
+# diceware - Generates a diceware style passphrase
+#
+emulate -L zsh # Ensure Zsh option defaults and FUNCTION_ARGZERO are set
+
+local default_wordlist_file="eff_large_wordlist.txt"
+local -a wordlist_search_paths=(
+ '.'
+ '${ZDOTDIR:-$HOME/.zsh}' # ZDOTDIR or ~/.zsh
+ '${XDG_CONFIG_HOME:-$HOME/.config}/zsh'
+ '/usr/share/diceware'
+ '/usr/local/share/diceware'
+)
+
+local usage
+read -rd '' usage << EOU
+ Usage: $0 [options]
+
+ Options:
+ -c, --capitalize
+ Capitalize the first letter of each word.
+ -s SEP, --separator SEP
+ Use SEP as the word separator (default: " ").
+ -N [COUNT], --numbers [COUNT]
+ Include COUNT random numbers (0-9) pasted to random words.
+ If COUNT is omitted, defaults to 1.
+ -n N, --num-words N
+ Specify the number of words in the passphrase (defaults to 6,
minimum 2).
+ -f FILE, --file FILE
+ Specify the diceware wordlist.
+ If omitted, '${default_wordlist_file}' is searched for in:
+ ${(pj:\n :)wordlist_search_paths}
+ -h, --help : This message
+
+ Examples:
+# 6 words, searches for wordlist, no options
+ $0
+# 8 words, searches for wordlist
+ $0 -n 8
+# 6 words, custom wordlist path
+ $0 --file /my/list.txt
+# Capitalize, hyphen-separated, 7 words
+ $0 -c --separator '-' -n 7
+# 6 words, 1 number pasted to a random word
+ $0 -N
+# 6 words, 3 numbers pasted to random words
+ $0 --numbers 3
+# Capitalize, dot-separated, 2 numbers, 5 words
+ $0 -c -s'.' -N 2 -n 5
+EOU
+#
+# Requires: zsh/random module, sed
+
+# Load the zsh/random module early. Critical failure if not available.
+if ! zmodload zsh/random; then
+ print -u2 "Error: zsh/random module could not be loaded. Is it
installed and available?"
+ return 1
+fi
+
+
+# Declare option variables with default values and `integer` type
+integer capitalize=0
+local separator=" "
+integer num_to_add_to_words=0
+integer num_words=6 # Default number of words
+local wordlist_file=""
+
+# zparseopts setup for short and long options
+local -a parsed_opts # Array to hold parsed options
+zparseopts -E -D -M -A parsed_opts \
+ c='-capitalize' -capitalize \
+ s:='-separator' -separator: \
+ N::='-numbers' -numbers:: \
+ n:='-num-words' -num-words: \
+ f:='-file' -file \
+ h='-help' "'?'"='-help' -help \
+ || { print -u2 "$usage" ; return 1; }
+
+# Process parsed options
+local opt_name
+for opt_name opt_val in "${(@kv)parsed_opts}"; do
+ if [[ $opt_val =~ '^=.*' && $#opt_val -gt 1 ]]; then
+ opt_val="${opt_val#=}"
+ fi
+ case "$opt_name" in
+ --capitalize)
+ capitalize=1
+ ;;
+ --separator)
+ separator="${opt_val}" # zparseopts stores arg in array at option
index
+ ;;
+ --numbers)
+ # Check if an argument was provided for -N/--numbers
+ if [[ -z "${opt_val}" ]]; then
+ num_to_add_to_words=1 # -N given without arg defaults to 1
+ else
+ num_to_add_to_words="${opt_val}"
+ fi
+ ;;
+ --num-words)
+ num_words="${opt_val}"
+ ;;
+ --file)
+ wordlist_file="${opt_val}"
+ ;;
+ --help)
+ print -u2 $usage
+ return 0
+ ;;
+ *)
+ print -u2 $usage
+ return 1
+ ;;
+ esac
+done
+
+# Enforce minimum number of words
+if (( num_words < 2 )); then
+ print -u2 "Error: Minimum number of words is 2."
+ print -u2 "$usage"
+ return 1
+fi
+
+# Determine wordlist file if not explicitly specified
+if [[ -z "$wordlist_file" ]]; then
+ for p in "${(@e)wordlist_search_paths}"; do
+ if [[ -f "$p/$default_wordlist_file" ]]; then
+ wordlist_file="$p/$default_wordlist_file"
+ break
+ fi
+ done
+
+ if [[ -z "$wordlist_file" ]]; then
+ print -u2 "
+Error: Diceware wordlist 'eff_large_wordlist.txt' not found in any
search path.
+
+Please download the official wordlist from the Electronic Frontier
Foundation
+and place it in one of the searched directories (e.g., ~/.config/zsh/).
+
+Canonical URL: https://www.eff.org/files/2016/07/18/eff_large_wordlist.txt
+
+Alternatively, specify a custom path using the -f or --file option."
+ return 1
+ fi
+fi
+
+if [[ "$separator" == "\n" || "$separator" == "\t" ]]; then
+ print -u2 "Warning: Using a newline or tab as a separator can produce
unexpected output."
+fi
+
+integer num_lines=$(wc -l < "$wordlist_file" | tr -d ' ')
+if (( num_lines == 0 )); then
+ print -u2 "Error: Wordlist is empty."
+ print -u2 "$usage"
+ return 1
+fi
+
+local -a line_numbers=()
+integer i
+for i in {1..$num_words}
+do
+ line_numbers+=( $(( zrand_int($num_lines,1,1) )) ) # zrand_int used
in arithmetic context
+done
+
+local sed_command=""
+if (( num_words > 0 )); then
+ # Correct Zsh specific syntax for sorting numerically, appending
'p', and joining
+ local -a tmp_sed_parts=(${^${(n)line_numbers}}p)
+ sed_command="${(j:;:)tmp_sed_parts}"
+fi
+
+local -a raw_words
+if [[ -n "$sed_command" ]]; then
+ raw_words=( $(sed -n "$sed_command" "$wordlist_file") )
+else
+ raw_words=() # No words to fetch if num_words was 0
+fi
+
+local -a passphrase_words=()
+local x word
+for x word in "${raw_words[@]}"; do
+ if (( capitalize )); then
+ word="${(C)word}" # Capitalize the first letter using Zsh (C) flag
+ fi
+ passphrase_words+=( "$word" )
+done
+
+# Paste random numbers to words if requested
+if (( num_to_add_to_words > 0 && ${#passphrase_words[@]} > 0 )); then
+ for i in {1..$num_to_add_to_words}
+ do
+ integer random_digit=$(( zrand_int(10) )) # 0-9 inclusive
+ # Use zrand_int with (upper, lower, inclusive) for 1-based array
indexing directly
+ integer target_word_index=$(( zrand_int(${#passphrase_words},1,1)
)) # 1 to num_words inclusive
+ integer paste_position=$(( zrand_int(2) )) # 0 for prefix, 1 for suffix
+
+ if (( paste_position == 0 )); then
+
passphrase_words[target_word_index]="${random_digit}${passphrase_words[target_word_index]}"
# Prefix
+ else
+
passphrase_words[target_word_index]="${passphrase_words[target_word_index]}${random_digit}"
# Suffix
+ fi
+ done
+fi
+
+local final_passphrase="${(pj:$separator:)passphrase_words}"
+
+print "$final_passphrase"
Messages sorted by:
Reverse Date,
Date,
Thread,
Author