From 8c079e816015cf9f82d42ccbda39d61f3e2d4a46 Mon Sep 17 00:00:00 2001 From: Jeremy Melanson <1080872+zish@users.noreply.github.com> Date: Sun, 7 Jun 2026 15:19:04 -0400 Subject: [PATCH] feat!: Add support for "iw" command - CLI completion for iw (Linux wireless configuration utility). - Directly parses "iw help" output to discover commands and caches the result. - $service is used as the iw binary name. This allows this completion to work for differently-named iw binaries: compdef _iw my-iw-binary --- plugins/iw/README.md | 40 +++++++ plugins/iw/_iw | 220 +++++++++++++++++++++++++++++++++++++++ plugins/iw/iw.plugin.zsh | 17 +++ 3 files changed, 277 insertions(+) create mode 100644 plugins/iw/README.md create mode 100644 plugins/iw/_iw create mode 100644 plugins/iw/iw.plugin.zsh diff --git a/plugins/iw/README.md b/plugins/iw/README.md new file mode 100644 index 000000000..0c8851921 --- /dev/null +++ b/plugins/iw/README.md @@ -0,0 +1,40 @@ +# iw plugin + +This plugin adds tab completion for [`iw`](https://wireless.wiki.kernel.org/en/users/documentation/iw), +the standard Linux command-line tool for configuring wireless network interfaces. + +To use it, add `iw` to the plugins array in your zshrc file: + +```zsh +plugins=(... iw) +``` + +## Completion + +Completion is generated dynamically by parsing the output of `iw help` and cached for +performance. The cache is stored in `$ZSH_CACHE_DIR/_iw_cache` and is automatically +regenerated when the installed version of `iw` changes. + +The following command structure is completed: + +| Level | Examples | +| ----- | -------- | +| Top-level commands | `dev`, `phy`, `wdev`, `reg`, `list`, `event`, … (discovered dynamically) | +| Interface / phy names | `wlan0`, `phy0` (discovered from `/sys/class/net` and `/sys/class/ieee80211`) | +| Subcommands | `dev scan`, `dev station`, `phy set`, … | +| Sub-subcommands | `dev scan dump`, `dev station get`, `phy set txpower`, … | + +## Functions + +| Function | Description | +| -------- | ----------- | +| `iw-clear-cache` | Delete the cached completion data (useful after upgrading `iw`) | + +## Requirements + +- `iw` must be installed and in `$PATH` +- Completion for a differently-named `iw` binary can be enabled with: + + ```zsh + compdef _iw my-iw-binary + ``` diff --git a/plugins/iw/_iw b/plugins/iw/_iw new file mode 100644 index 000000000..8860e9ff9 --- /dev/null +++ b/plugins/iw/_iw @@ -0,0 +1,220 @@ +#compdef iw +# + +# ZSH completion for iw (Linux wireless configuration utility). +# Parses 'iw help' output to discover commands and caches the result. +# +# $service is used as the iw binary name. This allows this completion to work +# for differently-named iw binaries: compdef _iw my-iw-binary + +# Parse 'iw help' output and write a sourceable cache file. +_iw_build_cache() { + local iw_cmd="$1" + local version="$2" + local cache_file="$3" + local help_output + + help_output=$($iw_cmd help 2>&1) || return 1 + + local -a top_cmds dev_subcmds phy_subcmds wdev_subcmds reg_subcmds + local -A dev_subs phy_subs wdev_subs + local line + local -a words lines + + # Single pass: collect top-level commands, subcmds, and sub-subcommands. + # words[4] entries starting with '<', '[', or '-' are placeholders, not completions. + lines=(${(f)help_output}) + for line in $lines; do + words=(${=line}) + [[ $line == $'\t'[a-z]* && $line != $'\t\t'* ]] && top_cmds+=($words[1]) + case $words[1] in + dev) + if [[ $words[2] == '' && $words[3] == [a-z]* ]]; then + dev_subcmds+=($words[3]) + if (( ${#words} > 3 )); then + case $words[4] in + '<'*|'['*|'-'*) ;; + *) dev_subs[$words[3]]+=" $words[4]" ;; + esac + fi + fi + ;; + phy) + if [[ $words[2] == '' && $words[3] == [a-z]* ]]; then + phy_subcmds+=($words[3]) + if (( ${#words} > 3 )); then + case $words[4] in + '<'*|'['*|'-'*) ;; + *) phy_subs[$words[3]]+=" $words[4]" ;; + esac + fi + fi + ;; + wdev) + if [[ $words[2] == '' && $words[3] == [a-z]* ]]; then + wdev_subcmds+=($words[3]) + if (( ${#words} > 3 )); then + case $words[4] in + '<'*|'['*|'-'*) ;; + *) wdev_subs[$words[3]]+=" $words[4]" ;; + esac + fi + fi + ;; + reg) + [[ $words[2] == [a-z]* ]] && reg_subcmds+=($words[2]) + ;; + esac + done + + # (ou): sorted + unique, replacing sort -u + top_cmds=(${(ou)top_cmds}) + dev_subcmds=(${(ou)dev_subcmds}) + phy_subcmds=(${(ou)phy_subcmds}) + wdev_subcmds=(${(ou)wdev_subcmds}) + reg_subcmds=(${(ou)reg_subcmds}) + + local subcmd subs_str + local -a subs_arr tmp_arr + + { + print "# iw completion cache" + print "# version: ${version}" + + print "_iw_top_cmds=(${top_cmds[*]})" + print "_iw_dev_subcmds=(${dev_subcmds[*]})" + for subcmd in $dev_subcmds; do + if [[ -n "${dev_subs[$subcmd]}" ]]; then + tmp_arr=(${=dev_subs[$subcmd]}) + subs_arr=(${(ou)tmp_arr}) + subs_str="${(j: :)subs_arr}" + print "_iw_dev_subsubcmds[${subcmd}]=${(qq)subs_str}" + fi + done + + print "_iw_phy_subcmds=(${phy_subcmds[*]})" + for subcmd in $phy_subcmds; do + if [[ -n "${phy_subs[$subcmd]}" ]]; then + tmp_arr=(${=phy_subs[$subcmd]}) + subs_arr=(${(ou)tmp_arr}) + subs_str="${(j: :)subs_arr}" + print "_iw_phy_subsubcmds[${subcmd}]=${(qq)subs_str}" + fi + done + + print "_iw_wdev_subcmds=(${wdev_subcmds[*]})" + for subcmd in $wdev_subcmds; do + if [[ -n "${wdev_subs[$subcmd]}" ]]; then + tmp_arr=(${=wdev_subs[$subcmd]}) + subs_arr=(${(ou)tmp_arr}) + subs_str="${(j: :)subs_arr}" + print "_iw_wdev_subsubcmds[${subcmd}]=${(qq)subs_str}" + fi + done + + print "_iw_reg_subcmds=(${reg_subcmds[*]})" + + } >| "$cache_file" +} + +_iw() { + local context state line curcontext="$curcontext" + integer ret=1 + + # Declare cache variables; populated by sourcing the cache file below. + local -a _iw_top_cmds _iw_dev_subcmds _iw_phy_subcmds _iw_wdev_subcmds _iw_reg_subcmds + local -A _iw_dev_subsubcmds _iw_phy_subsubcmds _iw_wdev_subsubcmds + + local cache_file="${ZSH_CACHE_DIR:-${HOME}/.cache}/_iw_cache" + local current_version ver_output + ver_output=$(${service} --version 2>&1) + [[ $ver_output =~ '([0-9]+\.[0-9]+)' ]] && current_version=$match[1] || current_version="" + + local cache_ver="" _l1 _l2 + if [[ -f "$cache_file" ]]; then + { IFS= read -r _l1; IFS= read -r _l2; } < "$cache_file" 2>/dev/null + cache_ver=${_l2#\# version: } + fi + + if [[ $cache_ver != $current_version ]]; then + _iw_build_cache "${service}" "$current_version" "$cache_file" + fi + + source "$cache_file" 2>/dev/null + + _arguments -C \ + '--debug[enable netlink debugging]' \ + '(- 1 2 3 4)--version[show version]' \ + '1:: :->cmd' \ + '2:: :->arg2' \ + '3:: :->arg3' \ + '4:: :->arg4' \ + && return 0 + + case "$state" in + cmd) + local -A _iw_cmd_descs + _iw_cmd_descs=( + dev 'network interface commands' + phy 'wireless hardware commands' + wdev 'wireless device commands' + reg 'regulatory domain commands' + list 'list all wireless devices and their capabilities' + event 'monitor kernel events' + features 'list supported features' + commands 'list all known commands' + help 'show command usage' + ) + local -a top_cmds + local _cmd + for _cmd in $_iw_top_cmds; do + if [[ -n "${_iw_cmd_descs[$_cmd]}" ]]; then + top_cmds+=("${_cmd}:${_iw_cmd_descs[$_cmd]}") + else + top_cmds+=("$_cmd") + fi + done + _describe -t commands 'iw command' top_cmds && ret=0 + ;; + + arg2) + case "$line[1]" in + dev) + local -a wlan_devs + wlan_devs=(/sys/class/net/*/phy80211(N:h:t)) + compadd -a wlan_devs && ret=0 + ;; + phy) + local -a phy_devs + phy_devs=(/sys/class/ieee80211/*(N:t)) + compadd -a phy_devs && ret=0 + ;; + reg) + compadd -a _iw_reg_subcmds && ret=0 + ;; + esac + ;; + + arg3) + case "$line[1]" in + dev) compadd -a _iw_dev_subcmds && ret=0 ;; + phy) compadd -a _iw_phy_subcmds && ret=0 ;; + wdev) compadd -a _iw_wdev_subcmds && ret=0 ;; + esac + ;; + + arg4) + local -a subs + case "$line[1]" in + dev) subs=(${=_iw_dev_subsubcmds[$line[3]]}) ;; + phy) subs=(${=_iw_phy_subsubcmds[$line[3]]}) ;; + wdev) subs=(${=_iw_wdev_subsubcmds[$line[3]]}) ;; + esac + (( ${#subs} > 0 )) && compadd -a subs && ret=0 + ;; + esac + + return ret +} + +_iw "$@" diff --git a/plugins/iw/iw.plugin.zsh b/plugins/iw/iw.plugin.zsh new file mode 100644 index 000000000..a36439f31 --- /dev/null +++ b/plugins/iw/iw.plugin.zsh @@ -0,0 +1,17 @@ +# iw Oh-My-ZSH plugin +# Provides tab completion for iw (Linux wireless configuration utility). + +if (( ! $+commands[iw] )); then + return +fi + +# Remove the cached iw completion data (useful after upgrading iw). +iw-clear-cache() { + local cache_file="${ZSH_CACHE_DIR:-${HOME}/.cache}/_iw_cache" + if [[ -f "$cache_file" ]]; then + rm -f "$cache_file" + print "iw completion cache cleared." + else + print "No iw completion cache found." + fi +}