How are zsh autocompletions for commands with subcommands defined? - zsh

I am trying to write a tab-completion script for borg.
So far, I have managed to define completions for borg itself, as well as borg key with its subcommands and borg benchmark with its singular subcommand. However, I am now trying to define completion for borg init and I am having trouble.
The issue presents itself only when I define two arguments under the borg init command to use the same description text; i.e. both -e and --encryption should use the same description, as they are practically the same argument. This has worked fine for borg's arguments, but now it breaks.
This is my code, slightly redacted to spare you the redundancy:
compdef _borg borg
function _borg {
local line ret=1
local -a argus
local logs="--critical --error --warning --debug --info -v --verbose"
argus+=(
"(*)"{-h,--help}"[Show help and exit]"
"(*)-V[Show Borg version and exit]"
"($logs)--critical[Work on log level CRITICAL]"
"($logs)--error[Work on log level ERROR]"
"($logs)--warning[Work on log level WARNING (default)]"
"($logs)"{--info,-v,--verbose}"[Work on log level INFO]"
"($logs)--debug[Enable debug output; log level DEBUG]"
{-p,--progress}"[Show progress]"
"--log-json[Output one JSON object per log line instead of formatted text]"
"--show-version[Show/log borg version]"
"--show-rc[Show/log returncode]"
"--consider-part-files[treat part files like normal files (e.g. to list/extract them)]"
"--lock-wait[Wait at most SECONDS for acquiring a repository/cache lock (default 1)]:SECONDS:()"
"--umask[Set umask to M (local and remote; default 0077)]:M (umask value, e.g. 0077):()"
"--remote-path[Use PATH as borg executable on the remote (default: \"borg\")]:PATH:()"
"--remote-ratelimit[Set remote network upload rate limit in kiByte/s (default: 0=unlimited)]:RATE:()"
"--debug-profile[Write execution profile in Borg format into FILE.]:FILE:_files"
"--rsh[Use this command to connect to the \"borg serve\" process (default: \"ssh\")]:RSH:()"
"1: :((init\:\"Initialize a new repository\" \
create\:\"Create a new archive\" \
extract\:\"Extract the contents of an archive\" \
check\:\"Verifies consistency of a repository and its archives\" \
rename\:\"Renames an archive in a repository\" \
list\:\"Lists contents of a repository or archive\" \
diff\:\"Finds differences between archives\" \
delete\:\"Deletes an archive or an entire repository (and its cache)\" \
prune\:\"Prunes a repository\" \
info\:\"Shows info about a repository or archive\" \
mount\:\"Mounts an archive as a FUSE filesystem\" \
unmount\:\"Unmounts a FUSE filesystem mounted with \\\"borg mount\\\"\" \
key\:\"Keyword for key-related functions\" \
upgrade\:\"Upgrade a local Borg repository\" \
recreate\:\"EXPERIMENTAL: Recreates contents of existing archives\" \
export-tar\:\"Creates a tarball from an archive\" \
serve\:\"Starts repository server process. Not usually used manually.\" \
config\:\"Gets and sets options in local repository and cache config files\" \
with-lock\:\"Executes another command with the repository lock held\" \
break-lock\:\"Breaks the repository and cache locks\" \
benchmark\:\"Keyword for the benchmark function\"))" \
"*::arg:->args"
)
_arguments -w -s -S -C $argus[#] && ret=0
case $line[1] in
benchmark)
_borg_benchmark
;;
init)
_borg_init
;;
key)
_borg_key
;;
esac
return ret
}
function _borg_benchmark {
# stuff
}
function _borg_benchmark_crud {
# stuff again
}
function _borg_init {
local line ret=1
local -a argus
argus+=(
"-t[This is a test]"
"--test[This is a test]"
"(--append-only)--append-only[Create an append-only mode repository]"
"*::arg:->args"
)
_arguments -w -s -S -C $argus[#] && ret=0
return ret
}
function _borg_key {
# key stuff
}
function _borg_key_changepassphrase {
# stuff
}
function _borg_key_export {
# more stuff
}
function _borg_key_import {
# other stuff
}
If I try to tab-complete borg init - using this setup, I get the following output:
$ borg init -
Completing option
--append-only
--test
-t
-- Create an append-only mode repository
-- This is a test
--append-only
--test
-t
-- Create an append-only mode repository
-- This is a test
--append-only
--test
-t
-- Create an append-only mode repository
-- This is a test
--append-only
--test
-t
-- Create an append-only mode repository
-- This is a test
The completion appears to forget what tabs are and repeats itself four times. If I change --test[This is a test] to --test[This is another test] in _borg_init, I instead get the following completion:
$ borg init -
Completing option
--append-only -- Create an append-only mode repository
--test -- This is another test
-t -- This is a test
The above is "correct", in the sense that it's not broken, but I cannot seem to define arguments that share a description in a subcommand. How should I do that? And, more generally, how are you supposed to define completions for commands with subcommands (which may, in turn, have more arguments)?

Related

QEMU serial std output diverges on archlinux guest

I'm trying to bootstrap some installation automation of a freshly downloaded ISO in QEMU. I create a clean img to install to and kick off QEMU like this:
$ qemu-img create -f qcow2 out/main.img 15G
$ qemu-system-x86_64
-m 8G \
-serial stdio \
-cdrom out/linux.iso \
-drive file=out/main.img,if=virtio \
-netdev user,id=net0 \
-device e1000,netdev=net0
and I can see Arch boot up. At first both the display and the terminal are in sync, but they soon diverge after this the GRUB boot up screen.
I'm not sure what piece I'm missing to get this to work. I've seen some people suggest adding -append "root=/dev/sda console=ttyS0" to your QEMU arguments, but (from what I can tell) while it requires you to extract the kernel and the initram from the ISO (which should be easy enough as mounting and copy/pasting the correct files) but it also expects you to already have an installed system on /dev/sda (which is what I'm trying to bootstrap).
At this point I don't know what to search for next, how do I get the full terminal session in my current terminal and not just in my display?
In this case, it was as #Peter Maydell commented; this is not a QEMU question. QEMU was doing exactly what it was supposed to do, but Arch had to be told to utilize the serial console as its primary means of communication.
Two samples of how this can be done
bash via console
pipe_dir="$(mktemp -d)"
mkfifo "${pipe_dir}/pipe.in" "${pipe_dir}/pipe.out"
function cleanup {
rm -rfv "${pipe_dir}"
}
trap cleanup EXIT
qemu-system-x86_64 \
-m 8G \
-display none \
-serial stdio \
-drive file=./out/linux.iso,index=0,media=cdrom \
-drive file=./out/main.img,if=virtio &
sleep 2s
printf "\t" > "${pipe_dir}/pipe.in"
sleep 2s
printf " console=ttyS0,115200" > "${pipe_dir}/pipe.in"
sleep 2s
echo > "${pipe_dir}/pipe.in"
# Whatever other interactions you want go here...
wait
expect via console
set timeout -1
spawn qemu-system-x86_64 \
-m 8G \
-display none \
-serial stdio \
-drive file=./out/linux.iso,index=0,media=cdrom \
-drive file=./out/main.img,if=virtio
sleep 1
send \t
sleep 1
send " console=ttyS0,115200"
sleep 1
send \n
In theory this should be fine, but in practice I still had difficulty interacting with the console and sending characters over to login correctly. I'm sure there is probably more user-error on my part than not.
A better solution (again contextual to Arch and not QEMU specifically) was to use a cloud-init script that included my SSH public key. Interactions with the VM were stable, reliable, and easily reproducible.
bash with cloud-init/ssh
$ touch ./out/meta-data
$ cat > ./out/user-data <<EOF
#cloud-config
users:
- name: root
ssh_authorized_keys:
- $(cat ${HOME}/.ssh/id_ed25519.pub)
EOF
$ xorriso -as genisoimage -output ./out/cloud-init.iso \
-volid CIDATA -joliet -rock ./out/meta-data ./out/user-data
$ qemu-system-x86_64 \
-m 8G \
-drive file=./out/linux.iso,index=0,media=cdrom \
-drive file=./out/cloud-init.iso,index=1,media=cdrom \
-drive file=./out/main.img,if=virtio \
-net user,hostfwd=tcp::10022-:22 \
-net nic &
$ function qemu-ssh {
ssh -q -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o "UserKnownHostsFile /dev/null" -p 10022 root#localhost ${#}
}
$ printf 'Waiting for SSH to go live (this will take a while)...'
$ until qemu-ssh exit; do
printf '.'
done
# This convenience function starts an interactive
# session when supplied with no additional arguments
# but your automation can go here
$ qemu-ssh

Is possible to write a multi-line alias in .gitconfig?

I know that is possible to use && (and) statement to go running multiple commands for a same alias. However for long combinations it loses in readability. For example:
save = !git status && git add -A && git commit -m \"$1\" && git push --force && git log && :
Is there a multi-line way to write it?
Maybe wrapping it with {} for example?
You can use a line escape (\) to break lines like this:
[alias]
save = !git status \
&& git add -A \
&& git commit -m \"$1\" \
&& git push -f \
&& git log -1 \
&& : # Used to distinguish last command from arguments
You can also put multiple statements inside a function like this:
[alias]
save = "!f() { \
git status; \
git add -A; \
git commit -m "$1"; \
git push -f; \
git log -1; \
}; \
f; \
unset f"
See Also: Git Alias - Multiple Commands and Parameters
I'd refrain from writing such extensive aliases in the config file. You can also add new commands by adding an executable file named git-newcommand to your PATH. This could be a Bash script, Python script or even a binary as long as it's executable and named with the prefix "git-".
In case of scripts you've to add the proper Hashbang:
#!/usr/bin/env python
Export the PATH, for example in your home:
export PATH="${PATH}:${HOME}/bin"
This is more modular, portable and easier debuggable.

--allow-root doesn't work running wp-cli in docker container

When using WP CLI in docker, I need to execute it as root.
I need to add the flag --allow-root directly in .bashrc and I am trying to figure out why it doesn't work.
FROM webdevops/php-dev:7.3
# configure postfix to use mailhog
RUN postconf -e "relayhost = mail:1025"
# install wp cli
RUN curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && \
chmod +x wp-cli.phar && \
mv wp-cli.phar /usr/local/bin/wp && \
echo 'wp() {' >> ~/.bashrc && \
echo '/usr/local/bin/wp "$#" --allow-root' >> ~/.bashrc && \
echo '}' >> ~/.bashrc
WORKDIR /var/www/html/
my .bashrc
# ~/.bashrc: executed by bash(1) for non-login shells.
# Note: PS1 and umask are already set in /etc/profile. You should not
# need this unless you want different defaults for root.
# PS1='${debian_chroot:+($debian_chroot)}\h:\w\$ '
# umask 022
# You may uncomment the following lines if you want `ls' to be colorized:
# export LS_OPTIONS='--color=auto'
# eval "`dircolors`"
# alias ls='ls $LS_OPTIONS'
# alias ll='ls $LS_OPTIONS -l'
# alias l='ls $LS_OPTIONS -lA'
#
# Some more alias to avoid making mistakes:
# alias rm='rm -i'
# alias cp='cp -i'
# alias mv='mv -i'
wp() {
/usr/local/bin/wp "$#" --allow-root
}
when I try to execute any wp command I get this error:
Error: YIKES! It looks like you're running this as root. You probably meant to run this as the user that your WordPress installation exists under.
If you REALLY mean to run this as root, we won't stop you, but just bear in mind that any code on this site will then have full control of your server, making it quite DANGEROUS.
If you'd like to continue as root, please run this again, adding this flag: --allow-root
If you'd like to run it as the user that this site is under, you can run the following to become the respective user:
sudo -u USER -i -- wp <command>
It looks like that command line doesn't consider what I input into .bashrc
Guys, do you have any suggestion how to fix this problem?
You are struggling with the classic conundrum: What goes in bashrc and what in bash_profile and which one is loaded when?
The extreme short version is:
$HOME/.bash_profile: read at login shells. Should always source $HOME/.bashrc. Should only contain environmental variables that can be passed on to other functions.
$HOME/.bashrc: read only for interactive shells that are not login
(eg. opening a terminal in X). Should only contain aliases and functions
How does this help the OP?
The OP executes the following line:
$ sudo -u USER -i -- wp <command>
The flag -i of the sudo-command initiates a login-shell
-i, --login: Run the shell specified by the target user's password database entry as a login shell. This means that login-specific resource files such as .profile, .bash_profile or .login will be read by the shell. If a command is specified, it is passed to the shell for execution via the shell's -c option. If no command is specified, an interactive shell is executed.
So the OP initiates a login-shell which only reads the .bash_profile. The way to solve the problem is now to source the .bashrc file in there as is strongly recommended.
# .bash_profile
if [ -n "$BASH" ] && [ -r ~/.bashrc ]; then
. ~/.bashrc
fi
more info on dot-files:
http://mywiki.wooledge.org/DotFiles
man bash
What's the difference between .bashrc, .bash_profile, and .environment?
About .bash_profile, .bashrc, and where should alias be written in?
related posts:
Run nvm (bash function) via sudo
Can I run a command loaded from .bashrc with sudo?
I recently had the same problem. In my Dockerfile, I was running:
RUN wp core download && wp plugin install woocommerce --activate --allow-root
I looked at the error message, and thought that from the way it was worded, the --allow-root gets ignored the first time you use it. So I added it to the first wp command, and It worked.
RUN wp core download --allow-root && wp plugin install woocommerce --activate --allow-root
The problem is that ~/.bashrc is not being sourced. It will only be sourced in an interactive Bash shell.
You might get better results doing it via executables. Something like this:
# install wp cli
RUN curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && \
chmod +x wp-cli.phar && \
mv wp-cli.phar /usr/local/bin/wp-cli.phar && \
echo '#!/bin/sh' >> /usr/local/bin/wp && \
echo 'wp-cli.phar "$#" --allow-root' >> /usr/local/bin/wp && \
chmod +x /usr/local/bin/wp

Sdkman Incorrect zsh completion script output

I am using oh-my-zsh and I have been trying to develop a custom completion script for sdkman.
However I have encountered a small problem when trying to mutualize some of the commands.
Below is the beginning of the completion script. There are three functions using the _describe method to output a completion help.
#compdef sdk
zstyle ':completion:*:descriptions' format '%B%d%b'
# Gets candidate lists and removes all unecessery things just to get candidate names
__get_candidate_list() {
echo `sdk list | grep --color=never "$ sdk install" | sed 's/\$ sdk install //g' | sed -e 's/[\t ]//g;/^$/d'`
}
__get_current_installed_list() {
echo `sdk current | sed "s/Using://g" | sed "s/\:.*//g" | sed -e "s/[\t ]//g;/^$/d"`
}
__describe_commands() {
local -a commands
commands=(
'install: install a program'
'uninstall: uninstal an existing program'
)
_describe -t commands "Commands" commands && ret=0
}
__describe_install() {
local -a candidate_list
candidate_list=( $( __get_candidate_list ) )
_describe -t candidate_list "Candidates available" candidate_list && ret=0
}
__describe_uninstall() { # FIXME THis is not working, it only displays candidate list
local -a candidates_to_uninstall
candidates_to_uninstall=( $( __get_current_installed_list ) )
_describe -t candidates_to_uninstall "Uninstallable candidates" candidates_to_uninstall && ret=0
}
The __get_candidate_list echoes the following values:
ant asciidoctorj bpipe ceylon crash cuba cxf gaiden glide gradle grails groovy groovyserv infrastructor java jbake kotlin kscript lazybones leiningen maven micronaut sbt scala spark springboot sshoogr vertx visualvm
The __get_current_installed_list echoes the following values:
gradle java kotlin maven sbt scala
The second part of the script below is where we call everything so that the completion script is used correctly by zsh:
function _sdk() {
local ret=1
local target=$words[2]
_arguments -C \
'1: :->first_arg' \
'2: :->second_arg' \
&& ret=0
case $state in
first_arg)
__describe_commands
;;
second_arg)
case $target in
install)
__describe_install
;;
uninstall)
__describe_uninstall
;;
*)
;;
esac
;;
esac
return $ret
}
_sdk "$#"
The problem is the following: when I type sdk install I get the right output, the one from the __get_candidate_list method BUT when I use sdk uninstall it still gives me the output from __get_candidate_list althought I am expecting __get_current_installed_list output.
EDIT: After a bit of debugging, it seems that zsh is not at fault here. I can't figure out why, but sdkman gives me the same output with both sdk list and sdk current (After the sed commands) from inside the completion script. IN my shell, both commands work properly with shell.
Is there something wrong with the way I use the _describe method ?
Is there anything else I am not seeing ?
Thanks for your help.
So I finally found a workaround to fix this but it is not ideal.
I chose to launch the commands in the background when launching the plugin, and fill text files with the results, so that completion scripts can use these after.
Below is the code I used in the zsh-sdkman.plugin.zsh file, in case my github repository disappears:
# --------------------------
# -------- Executed on shell launch for completion help
# --------------------------
# NOTE: Sdkman seems to always output the candidate list rather than the currently installted list when we do this through the completion script
# There are two goals in the code below:
# - Optimization: the _sdkman_get_candidate_list command can take time, so it is done once and in background
# - Bug correction: correct the problem with sdkman command output explained above
# WARNING: We are setting this as a local variable because we don't have it yet at the time of initialization
# A better approach would be welcome
SDKMAN_DIR_LOCAL=~/.sdkman
# Custom variables for later
export ZSH_SDKMAN_CANDIDATE_LIST_HOME=~/.zsh-sdkman.candidate-list
export ZSH_SDKMAN_INSTALLED_LIST_HOME=~/.zsh-sdkman.current-installed-list
_sdkman_get_candidate_list() {
(sdk list | grep --color=never "$ sdk install" | sed 's/\$ sdk install //g' | sed -e 's/[\t ]//g;/^$/d' > $ZSH_SDKMAN_CANDIDATE_LIST_HOME &)
}
_sdkman_get_current_installed_list() {
(sdk current | sed "s/Using://g" | sed "s/\:.*//g" | sed -e "s/[\t ]//g;/^$/d" > $ZSH_SDKMAN_INSTALLED_LIST_HOME &)
}
# "sdk" command is not found if we don't do this
source "$SDKMAN_DIR_LOCAL/bin/sdkman-init.sh"
# Initialize files with available candidate list and currently installted candidates
_sdkman_get_candidate_list "$#"
_sdkman_get_current_installed_list "$#"
For more information, you can see the complete repository of my plugin: https://github.com/matthieusb/zsh-sdkman
If you have another cleaner solution, I'll be willing to make the necessary modifications, or don't hesitate to make a pull request on the project.

Holding state in zsh completions

I want to write zsh completions for a program with the following calling convention:
program [generaloptions] operation [operationoptions]
where operation is one of --install, --upgrade...
What I have so far, are the general options and the operation options. My code looks something like this:
local generaloptions; generaloptions=(...)
local installoptions; installoptions=(...)
local upgradeoptions; upgradeoptions=(...)
case "$state" in
(install)
_arguments -s \
"$installoptions[#]" \
&& ret=0
(upgrade)
_arguments -s \
"$upgradeoption[#]" \
&& ret=0
*)
_arguments -s \
"$generaloptions[#]" \
'--install[...]: :->install' \
'--upgrade[...]: :->upgrade' \
&& ret=0
The problem is, after I type the operation and the first operation option, the state gets reset to the *) case.
Example
$ program --install --installoption --<tab>
list of general options
How can I set the next state to be the same as the old? Which command has similar calling conventions, so I can look at the code of the completion for this command?
The main problem is that the operations start with a --, so it is harder to find them in the arguments. In git for example all subcommands are only a word without dashes. So git solves this problem something like this:
Find the first argument without dashes because this must be the subcommand
Dispatch based on the subcommand to the commandline arguments for that subcommand.
So git dispatches in every call to the completion function (this was what I meant with "holding the state").
The way I solved this problem was by looking through many completion functions and finding a command that had a similar calling convention. The command that I found the most useful is pacman. Here is what I extracted from that:
# This somehow disassembles the commandline options
args=( ${${${(M)words:#-*}#-}:#-*}
case $args in
*i)
_arguments -s \
${installoptions} \
'(-i[...]' \
&& ret=0
;;
*u)
_arguments -s \
${upgradeoption} \
'-u[...]' \
&& ret=0
;;
*)
case ${(M)words:#--*} in
*--install*)
_arguments -s \
${installoptions} \
'--install[...]' \
&& ret=0
;;
*--upgrade*)
_arguments -s \
${upgradeoption} \
'--upgrade[...]' \
&& ret=0
;;
*)
_arguments -s \
{generaloptions} \
&& ret=0
;;
esac
esac
I know, there is a lot of dublication, but I think you get the point. Also notice, I moved the --install and --upgrade options from the general case to the operation case. If you don't do that, you loose the argument if you want complete after --install or --upgrade

Resources