Skip to content

Commit

Permalink
bash completions (cea-hpc#563)
Browse files Browse the repository at this point in the history
* CLI/Nodeset: add --completion command (cea-hpc#563)

Special command for bash completion that lists group sources, groups in
current source and nodes from groups passed as argument:

cluset --completion [-s source] [groups]

Example:

cluset --completion @*

Part of cea-hpc#563.

* tests: add basic tests for cluset --completion (cea-hpc#563)

Part of cea-hpc#563.

* bash completions for clush and cluset (cea-hpc#563)

Provide bash completion scripts for clush and cluset/nodeset to
autocomplete group sources, groups and "all" nodes from the
default or selected source.

Part of cea-hpc#563.

* packaging: add bash_completion.d/* (cea-hpc#563)

Part of cea-hpc#563.

---------

Co-authored-by: Stephane Thiell <sthiell@stanford.edu>
  • Loading branch information
martinetd and thiell authored Jan 23, 2025
1 parent a11656e commit 45a2357
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 3 deletions.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
include ChangeLog
include README.md
include COPYING.LGPLv2.1
include bash_completion.d/*
include conf/*.conf
include conf/*.example
include conf/clush.conf.d/README
Expand Down
79 changes: 79 additions & 0 deletions bash_completion.d/cluset
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# cluset bash completion
#
# to install in /usr/share/bash-completion/completions/ or ~/.local/share/bash-completion/completions/
_cluset()
{
# shellcheck disable=SC2034 # set/used by _init_completion
local cur prev words cword split
local word options="" skip=argv0 groupsource="" cleangroup=""

_init_completion -s -n : || return

# stop parsing if there had been any non-option before (or --)
for word in "${words[@]}"; do
case "$skip" in
"") ;;
groupsource)
groupsource="$word"
;& # fallthrough
*)
skip=""
continue
;;
esac
case "$word" in
"") ;;
--) return;;
# no-arg options
--version|-h|--help|-n|--nostdin|-a|--all|-q|--quiet|\
-v|--verbose|-d|--debug) ;;
# get source separately...
--groupsource=*) groupsource="${word#*=}";;
-s|--groupsource) skip=groupsource;;
# assume all the rest as options...
# options with = included in word
--*=*) ;;
-*) skip=any;;
*) return;; # was non-option
esac
done

case "$prev" in
-c|--count|-e|--expand|-f|--fold|\
-x|--exclude|-i|--intersection|-X|--xor)
case "$cur" in
*:*)
groupsource="${cur%%:*}"
groupsource="${groupsource#@}"
;;
*)
if [ -n "$groupsource" ]; then
cleangroup=1
fi
;;
esac
options="$(cluset ${groupsource:+-s "$groupsource"} --completion @*)"
if [ -n "$cleangroup" ]; then
options=${options//@"$groupsource":/@}
fi
;;
-s|--groupsource)
options=$(cluset --groupsources --quiet)
;;
# no-arg options
--version|-h|--help|-l|--list|-L|--list-all|-r|--regroup|\
--list-sources|--groupsources|-d|--debug|-q|--quiet|\
-R|--rangeset|-G|--groupbase|--contiguous) ;;
# any other option: just ignore.
-*)
return;;
esac
# get all options from help text... not 100% accurate but good enough.
[ -n "$options" ] || options="$(cluset --help | grep -oP -- '(?<=[ \t])(-[a-z]|--[^= \t]*)')"

# append space for everything that doesn't end in `:` (likely a groupsource)
mapfile -t COMPREPLY < <(compgen -W "$options" -- "$cur" | sed -e 's/[^:]$/& /')
# remove the prefix from COMPREPLY if $cur contains colons and
# COMP_WORDBREAKS splits on colons...
__ltrim_colon_completions "$cur"
} && complete -o nospace -F _cluset ${BASH_SOURCE##*/}
91 changes: 91 additions & 0 deletions bash_completion.d/clush
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# clush bash completion
#
# to install in /usr/share/bash-completion/completions/ or ~/.local/share/bash-completion/completions/
_clush()
{
# shellcheck disable=SC2034 # set/used by _init_completion
local cur prev words cword split
local word options="" compopts="" skip=argv0 groupsource="" cleangroup=""

_init_completion -s -n : || return

# stop parsing if there had been any non-option before (or --)
for word in "${words[@]}"; do
case "$skip" in
"") ;;
groupsource)
groupsource="$word"
;& # fallthrough
*)
skip=""
continue
;;
esac
case "$word" in
"") ;;
--) return;;
# no-arg options
--version|-h|--help|-n|--nostdin|-a|--all|-q|--quiet|\
-v|--verbose|-d|--debug) ;;
# get source separately...
--groupsource=*) groupsource="${word#*=}";;
-s|--groupsource) skip=groupsource;;
# assume all the rest as options...
# options with = included in word
--*=*) ;;
-*) skip=any;;
*) return;; # was non-option
esac
done

case "$prev" in
-w|-x|-g|--group|-X)
case "$cur" in
*:*)
groupsource="${cur%%:*}"
groupsource="${groupsource#@}"
;;
*)
if [ -n "$groupsource" ]; then
cleangroup=1
fi
;;
esac
if [ "$prev" = "-w" ]; then
compopts="@*" # include all nodes
fi
options="$(cluset ${groupsource:+-s "$groupsource"} --completion $compopts)"
if [ -n "$cleangroup" ]; then
options=${options//@"$groupsource":/@}
fi
case "$prev" in
-g|--group|-X)
options=${options//@/}
;;
esac
;;
-s|--groupsource)
options=$(cluset --groupsources --quiet)
;;
--color)
options="never always auto"
;;
-R|--worker)
options="ssh exec rsh"
;;
# no-arg options
--version|-h|--help|-n|--nostdin|-a|--all|-q|--quiet|\
-v|--verbose|-d|--debug) ;;
# any other option: just ignore.
-*)
return;;
esac
# get all options from help text... not 100% accurate but good enough.
[ -n "$options" ] || options="$(clush --help | grep -oP -- '(?<=[ \t])(-[a-z]|--[^= \t]*)')"

# append space for everything that doesn't end in `:` (likely a groupsource)
mapfile -t COMPREPLY < <(compgen -W "$options" -- "$cur" | sed -e 's/[^:]$/& /')
# remove the prefix from COMPREPLY if $cur contains colons and
# COMP_WORDBREAKS splits on colons...
__ltrim_colon_completions "$cur"
} && complete -o nospace -F _clush ${BASH_SOURCE##*/}
12 changes: 12 additions & 0 deletions clustershell.spec.in
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
%define py2 1
%endif

%{!?bash_completions_dir: %global bash_completions_dir %{_datadir}/bash-completion/completions}

%global srcname ClusterShell

Name: clustershell
Expand Down Expand Up @@ -149,6 +151,13 @@ install -p -m 0644 doc/extras/vim/syntax/clushconf.vim %{buildroot}/%{vimdatadir
install -p -m 0644 doc/extras/vim/syntax/groupsconf.vim %{buildroot}/%{vimdatadir}/syntax/
%{?suse_version:%fdupes %{buildroot}}

install -d %{buildroot}%{bash_completions_dir}
install -p -m 0644 bash_completion.d/cluset -t %{buildroot}%{bash_completions_dir}
install -p -m 0644 bash_completion.d/clush -t %{buildroot}%{bash_completions_dir}
pushd %{buildroot}%{bash_completions_dir}
ln -s cluset nodeset
popd

%if 0%{?rhel}
%clean
rm -rf %{buildroot}
Expand Down Expand Up @@ -228,6 +237,9 @@ rm -rf %{buildroot}
%{vimdatadir}/ftdetect/clustershell.vim
%{vimdatadir}/syntax/clushconf.vim
%{vimdatadir}/syntax/groupsconf.vim
%{bash_completions_dir}/cluset
%{bash_completions_dir}/clush
%{bash_completions_dir}/nodeset

%changelog
* Fri Sep 29 2023 Stephane Thiell <sthiell@stanford.edu> 1.9.2-1
Expand Down
19 changes: 16 additions & 3 deletions lib/ClusterShell/CLI/Nodeset.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def print_source_groups(source, level, xset, opts):

def command_list(options, xset, group_resolver):
"""List command handler (-l/-ll/-lll/-L/-LL/-LLL)."""
list_level = options.list + options.listall
list_level = options.list + options.listall + options.completion
if options.listall:
# useful: sources[0] is always the default or selected source
sources = group_resolver.sources()
Expand Down Expand Up @@ -175,7 +175,7 @@ def nodeset():
cmdcount = int(options.count) + int(options.expand) + \
int(options.fold) + int(bool(options.list)) + \
int(bool(options.listall)) + int(options.regroup) + \
int(options.groupsources)
int(options.groupsources) + int(options.completion)
if not cmdcount:
parser.error("No command specified.")
elif cmdcount > 1:
Expand Down Expand Up @@ -231,7 +231,9 @@ def nodeset():
# Include all nodes from external node groups support.
xset.update(NodeSet.fromall()) # uses default_source when set

if not args and not options.all and not (options.list or options.listall):
if not args and not options.all and not (options.list or
options.listall or
options.completion):
# No need to specify '-' to read stdin in these cases
process_stdin(xset.update, xset.__class__, autostep)

Expand Down Expand Up @@ -264,6 +266,17 @@ def nodeset():
# The list command has a special handling
if options.list > 0 or options.listall > 0:
return command_list(options, xset, group_resolver)
elif options.completion: # --completion is used for bash completion
# list group source prefixes unless source is already specified
if not options.groupsource:
for src in group_resolver.sources():
print("@%s:" % src)
# list groups in group source (similar to list)
command_list(options, None, group_resolver)
# then list nodes from the groups passed as argument, if any
if not xset:
return
options.expand = True

# Interpret special characters (may raise SyntaxError)
separator = eval('\'\'\'%s\'\'\'' % options.separator,
Expand Down
4 changes: 4 additions & 0 deletions lib/ClusterShell/CLI/OptionParser.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,10 @@ def install_nodeset_commands(self):
default=False,
help="list all active group sources (see "
"groups.conf(5))")
# special hidden command for bash completion scripts
optgrp.add_option("--completion", action="store_true",
dest="completion",
default=False, help=optparse.SUPPRESS_HELP)
self.add_option_group(optgrp)

def install_nodeset_operations(self):
Expand Down
11 changes: 11 additions & 0 deletions tests/CLINodesetTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,17 @@ def test_040_wildcards(self):
self._nodeset_t(["-s", "other", "--autostep=3", "-f", "*!*[033-099/2]"],
None, b"nova[030-032,034-100/2,101-489]\n")

def test_041_completion(self):
"""test nodeset --completion"""
self._nodeset_t(["--completion"], None,
b"@test:\n@other:\n@bar\n@foo\n@moo\n")
self._nodeset_t(["--completion", "node1", "node2"], None,
b"@test:\n@other:\n@bar\n@foo\n@moo\nnode1 node2\n")
self._nodeset_t(["-s", "other", "--completion"], None,
b"@other:baz\n@other:norf\n@other:qux\n")
self._nodeset_t(["-s", "other", "--completion", "node1", "node2"], None,
b"@other:baz\n@other:norf\n@other:qux\nnode1 node2\n")


class CLINodesetGroupResolverTest3(CLINodesetTestBase):
"""Unit test class for testing CLI/Nodeset.py with custom Group Resolver
Expand Down

0 comments on commit 45a2357

Please sign in to comment.