Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 38 additions & 5 deletions .github/workflows/publish-cli.yml
Original file line number Diff line number Diff line change
Expand Up @@ -234,15 +234,44 @@ jobs:
ls -la dist
env:
REPO: ${{ github.repository }}
# usage is the tool that reads these specs, so it prints its own. A
# consumer generates completions and documentation from the file with
# its own copy of usage, and nothing of this release's has to run.
- name: Publish the CLI specification
# The spec supports documentation generation without running the binary.
# Native completion scripts call the installed binary on each completion.
- name: Install completion syntax checkers
timeout-minutes: 5
run: |
if [[ -f /etc/apt/apt-mirrors.txt ]]; then
sudo sed -i 's|http://azure.archive.ubuntu.com/ubuntu|https://archive.ubuntu.com/ubuntu|g' /etc/apt/apt-mirrors.txt
fi
sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=15 -o Acquire::https::Timeout=15 update
sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=15 -o Acquire::https::Timeout=15 install -y zsh fish
# Hosted runners include PowerShell; Namespace images have its signed apt repository.
if ! command -v pwsh >/dev/null 2>&1; then
sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=15 -o Acquire::https::Timeout=15 install -y powershell
fi
- name: Publish the CLI specification and completions
run: |
mkdir -p bin
tar -xzf dist/usage-x86_64-unknown-linux-gnu.tar.gz -C bin
bin/usage --usage-spec > usage.usage.kdl
gh release upload "$GITHUB_REF_NAME" usage.usage.kdl --repo "$REPO" --clobber
for shell in bash zsh fish powershell; do
bin/usage --completions "$shell" > usage.$shell
test -s usage.$shell
grep -Fq '__complete_word__' usage.$shell
bin/usage __complete_word__ --shell "$shell" --line 'usage ge' | grep -Fq -- 'generate'
done
bash -n usage.bash
zsh -n usage.zsh
fish --no-execute usage.fish
cat > completion-syntax-check.ps1 <<'POWERSHELL'
$ErrorActionPreference = 'Stop'
foreach ($script in Get-ChildItem -LiteralPath "." -Filter '*.powershell') {
$parseErrors = $null
[void][System.Management.Automation.Language.Parser]::ParseFile($script.FullName, [ref]$null, [ref]$parseErrors)
if ($parseErrors.Count) { $parseErrors | Write-Error; exit 1 }
}
POWERSHELL
pwsh -NoProfile -NonInteractive -File completion-syntax-check.ps1
gh release upload "$GITHUB_REF_NAME" usage.usage.kdl usage.bash usage.zsh usage.fish usage.powershell --repo "$REPO" --clobber
env:
REPO: ${{ github.repository }}
- uses: jdx/packslip@a0d434ad57571c62dbd0ab5095b4eff607a71fd3 # v1.1.1
Expand All @@ -252,6 +281,10 @@ jobs:
resources: |
man=archive:usage.1
cli-spec/usage=asset:usage.usage.kdl
completion/bash=asset:usage.bash
completion/zsh=asset:usage.zsh
completion/fish=asset:usage.fish
completion/powershell=asset:usage.powershell

release:
runs-on: ubuntu-latest
Expand Down
4 changes: 2 additions & 2 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ tokio = { version = "1", features = ["rt", "macros", "io-std"] }
#
# `completions` is on for `usage_argv::install`, which `usage g completion --install`
# shares with a compiled binary installing its own script — so where a completion goes
# is decided once rather than twice. Not for completions of this CLI's own command line:
# it declares no `#[usage(completion)]`, and its own scripts are the checked-in assets.
# is decided once rather than twice. It also powers this CLI's own native callbacks
# and scripts, including the completion resources published with Packslip.
usage-rs = { workspace = true, features = ["completions"] }
# `validation` rather than `clap`: the CLI has no clap dependency to convert *from*, and it
# parses arbitrary specs — without `validation`, a spec carrying `validate=` gets an error
Expand Down
143 changes: 87 additions & 56 deletions cli/assets/completions/_usage
Original file line number Diff line number Diff line change
@@ -1,66 +1,97 @@
#compdef usage
# @generated by usage-cli from usage spec
local curcontext="$curcontext"

# caching config
_usage_usage_cache_policy() {
if [[ -z "${lifetime}" ]]; then
lifetime=$((60*60*4)) # 4 hours
fi
local -a oldp
oldp=( "$1"(Nms+${lifetime}) )
(( $#oldp ))
}
# @generated by usage-argv for `usage __complete_word__ --shell zsh`

_usage() {
emulate -L zsh
typeset -A opt_args
local curcontext="$curcontext" cache_policy
local -a values=() descriptions=() inserts=() extensions=()
local -a __usage_words=()
local __usage_files= __usage_line __usage_menu=0 __usage_input __usage_prefix
local __usage_unprefixed __usage_command __usage_word
local __usage_assignment='^[A-Za-z_][A-Za-z0-9_]*\+?='
local -A __usage_aliases_seen=()
# `$BUFFER[1,CURSOR]` is the text before the cursor, cut with zsh's own offset — see the
# bash script on why the cutting happens here rather than through a `--cursor` argument.
__usage_input="${BUFFER[1,CURSOR]}"
while (( 1 )); do
__usage_prefix="${__usage_input%%[^[:blank:]]*}"
__usage_unprefixed="${__usage_input#"$__usage_prefix"}"
__usage_words=("${(z)__usage_unprefixed}")
__usage_command=
for __usage_word in "${__usage_words[@]}"; do
if [[ "$__usage_word" =~ $__usage_assignment ]]; then
__usage_unprefixed="${__usage_unprefixed#"$__usage_word"}"
__usage_prefix+="$__usage_word${__usage_unprefixed%%[^[:blank:]]*}"
__usage_unprefixed="${__usage_unprefixed#"${__usage_unprefixed%%[^[:blank:]]*}"}"
else
__usage_command="$__usage_word"
break
fi
done
[[ "${+aliases[$__usage_command]}" == 1 &&
-z "${__usage_aliases_seen[$__usage_command]-}" ]] || break
__usage_aliases_seen[$__usage_command]=1
__usage_input="${__usage_prefix}${aliases[$__usage_command]}${__usage_unprefixed#"$__usage_command"}"
done
while IFS= read -r __usage_line; do
case "$__usage_line" in
$'\001files') __usage_files=any; continue ;;
$'\001dirs') __usage_files=dirs; continue ;;
$'\001executables') __usage_files=executables; continue ;;
$'\001commands') __usage_files=commands; continue ;;
$'\001extensions\t'*)
__usage_files=extensions
extensions=("${(@ps:\t:)${__usage_line#*$'\t'}}")
continue
;;
'') continue ;;
esac
local -a parts=("${(@ps:\t:)__usage_line}")
values+=("${parts[1]}")
descriptions+=("${parts[2]}")
inserts+=("${parts[3]}")
# A quoted insert means the value needed quoting, and zsh should offer a menu rather
# than silently inserting one of several possibilities.
[[ "${parts[3]}" == "'"* ]] && __usage_menu=1
done < <(command 'usage' __complete_word__ --shell zsh \
--line "$__usage_input" 2>/dev/null)

if ! type -p usage &> /dev/null; then
echo >&2
echo "Error: usage CLI not found. This is required for completions to work in usage." >&2
echo "See https://usage.jdx.dev for more information." >&2
return 1
fi
local __usage_ret=1
(( __usage_menu )) && compstate[insert]=menu
if (( ${#inserts[@]} )); then
local -a display=()
local i max=0 value pad
for value in "${values[@]}"; do
(( ${#value} > max )) && max=${#value}
done
for (( i = 1; i <= ${#values[@]}; i++ )); do
if [[ -n "${descriptions[i]}" ]]; then
pad=$(( max - ${#values[i]} ))
display+=("${values[i]}${(l:pad:: :)} -- ${descriptions[i]}")
else
display+=("${values[i]}")
fi
done
# `-U` because the binary already filtered by the typed prefix, and `-Q` because the
# inserts are quoted already.
compadd -l -d display -U -Q -S '' -a inserts && __usage_ret=0
fi

local spec_dir="${XDG_CACHE_HOME:-$HOME/.cache}/usage"
[[ -d "$spec_dir" ]] || mkdir -p -m 700 "$spec_dir"
local spec_file="$spec_dir/usage__usage_spec_usage.spec"
command usage --usage-spec >| "$spec_file"
local -a values=() descs=() inserts=()
local needs_menu=0 line
while IFS= read -r line; do
local -a parts=("${(@ps:\t:)line}")
values+=("${parts[1]}")
descs+=("${parts[2]}")
inserts+=("${parts[3]}")
[[ "${parts[3]}" == "'"* ]] && needs_menu=1
done < <(command usage complete-word --shell zsh -f "$spec_file" --cword=$((CURRENT - 1)) -- "${(Q)words[@]}")
(( needs_menu )) && compstate[insert]=menu
if (( ${#inserts[@]} )); then
local -a _usage_display=()
local _usage_i _usage_max=0 _usage_v _usage_pad
for _usage_v in "${values[@]}"; do
(( ${#_usage_v} > _usage_max )) && _usage_max=${#_usage_v}
done
for ((_usage_i=1; _usage_i<=${#values[@]}; _usage_i++)); do
if [[ -n "${descs[_usage_i]}" ]]; then
_usage_pad=$(( _usage_max - ${#values[_usage_i]} ))
_usage_display+=("${values[_usage_i]}${(l:_usage_pad:: :)} -- ${descs[_usage_i]}")
else
_usage_display+=("${values[_usage_i]}")
fi
done
compadd -l -d _usage_display -U -Q -S '' -a inserts
fi
return 0
case "$__usage_files" in
any) _files && __usage_ret=0 ;;
dirs) _files -/ && __usage_ret=0 ;;
executables) _files -g '*(-/,*)' && __usage_ret=0 ;;
commands) _command_names && __usage_ret=0 ;;
extensions)
local __usage_glob="*.(${(j:|:)extensions})"
_files -g "$__usage_glob" && __usage_ret=0
;;
esac
return $__usage_ret
}

# Installed either way. Dropped in `$fpath` as `_usage`, compinit autoloads the file and calls
# the function named after it — which is why the function is `_usage` and not something tidier.
# Sourced from a config instead, nothing has called it yet, so it registers itself.
if [ "$funcstack[1]" = "_usage" ]; then
_usage "$@"
else
compdef _usage usage
compdef _usage 'usage'
fi

# vim: noet ci pi sts=0 sw=4 ts=4
98 changes: 62 additions & 36 deletions cli/assets/completions/usage.bash
Original file line number Diff line number Diff line change
@@ -1,39 +1,65 @@
# @generated by usage-cli from usage spec
# Requires bash-completion (https://github.com/scop/bash-completion), which must be
# sourced before this script.
_usage() {
if ! type -P usage &> /dev/null; then
echo >&2
echo "Error: usage CLI not found. This is required for completions to work in usage." >&2
echo "See https://usage.jdx.dev for more information." >&2
return 1
fi
if ! declare -F _init_completion > /dev/null 2>&1; then
echo >&2
echo "Error: bash-completion is required for usage completions but was not loaded." >&2
echo "Install it from your package manager and source it before this script." >&2
return 1
fi
# @generated by usage-argv for `usage __complete_word__ --shell bash`

_usage_complete_usage() {
local __usage_out __usage_line __usage_files= __usage_extensions=
# Truncated here rather than passed with an offset: every shell counts a cursor in its own
# units — characters in a UTF-8 locale for bash and zsh, characters for fish and PowerShell —
# and a number that means one thing here and another there is a bug waiting for a non-ASCII
# command line. Cut with the shell's own offset, the units cancel out and what arrives is
# exactly the text before the cursor.
__usage_out="$(command 'usage' __complete_word__ --shell bash \
--line "${COMP_LINE:0:$COMP_POINT}" 2>/dev/null)" || return 1

local cur prev words cword comp_args
_init_completion -n : -- "$@" || return
local spec_dir="${XDG_CACHE_HOME:-$HOME/.cache}/usage"
[[ -d "$spec_dir" ]] || mkdir -p -m 700 "$spec_dir"
local spec_file="$spec_dir/usage__usage_spec_usage.spec"
command usage --usage-spec >| "$spec_file"
# shellcheck disable=SC2207
COMPREPLY=($(compgen -W "$(command usage complete-word --shell bash -f "$spec_file" --cword="$cword" -- "${words[@]}")" -- "$cur"))
__ltrim_colon_completions "$cur"
# shellcheck disable=SC2181
if [[ $? -ne 0 ]]; then
unset COMPREPLY
COMPREPLY=()
while IFS= read -r __usage_line; do
case "$__usage_line" in
$'\001files') __usage_files=any ;;
$'\001dirs') __usage_files=dirs ;;
$'\001executables') __usage_files=executables ;;
$'\001commands') __usage_files=commands ;;
$'\001extensions\t'*)
__usage_files=extensions
__usage_extensions="${__usage_line#*$'\t'}"
;;
'') ;;
*) COMPREPLY+=("$__usage_line") ;;
esac
done <<< "$__usage_out"

if [[ -n $__usage_files ]]; then
# Set here rather than on `complete`, because whether this position takes a path is not
# known until the answer comes back. It is what makes bash append a `/` to a directory
# and stop escaping what it should not.
[[ $__usage_files == commands ]] || compopt -o filenames 2>/dev/null
local __usage_cur="${COMP_WORDS[COMP_CWORD]}" __usage_path
local -a __usage_paths=()
if [[ $__usage_files == commands ]]; then
while IFS= read -r __usage_path; do __usage_paths+=("$__usage_path"); done \
< <(compgen -c -- "$__usage_cur")
elif [[ $__usage_files == executables ]]; then
while IFS= read -r __usage_path; do
[[ -d "$__usage_path" || -x "$__usage_path" ]] && __usage_paths+=("$__usage_path")
done < <(compgen -f -- "$__usage_cur")
elif [[ $__usage_files == dirs ]]; then
while IFS= read -r __usage_path; do __usage_paths+=("$__usage_path"); done \
< <(compgen -d -- "$__usage_cur")
else
while IFS= read -r __usage_path; do
if [[ $__usage_files != extensions || -d "$__usage_path" ]]; then
__usage_paths+=("$__usage_path")
continue
fi
local __usage_extension
while IFS= read -r __usage_extension; do
[[ "$__usage_path" == *."$__usage_extension" ]] && {
__usage_paths+=("$__usage_path")
break
}
done < <(tr '\t' '\n' <<< "$__usage_extensions")
done < <(compgen -f -- "$__usage_cur")
fi
# Guarded, because an empty array expands to one empty word in older bash.
(( ${#__usage_paths[@]} )) && COMPREPLY+=("${__usage_paths[@]}")
fi
return 0
}

if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then
shopt -u hostcomplete && complete -o nospace -o bashdefault -o nosort -F _usage usage
else
shopt -u hostcomplete && complete -o nospace -o bashdefault -F _usage usage
fi
# vim: noet ci pi sts=0 sw=4 ts=4 ft=sh
complete -F _usage_complete_usage 'usage'
Loading