Auto complete commands with "windows like" options (starting with slash) - zsh

I'm trying to write a custom auto-complete script for zsh (or bash) for a command that has options starting with a slash.
For instance: MyCommand /foo=bar.txt /yolo=test /final=4
I've been trying to use the zsh helper _arguments but it did not work:
#compdef MyCommand
_MyCommand()
{
local curcontext="$curcontext" state line
typeset -A opt_args
_arguments \
'/foo=:foo:_files'
}
_MyCommand "$#"
But when I replace the / with -- it works well.
How can I achieve this?

You can do that using _regex_arguments like this:
matchany=/$'[^\0]##\0'/
_regex_arguments _mycommand "$matchany" \( /$'/foo='/ ':option:option:(/foo=)' "$matchany" ':file:filename:_files' \| /$'/yolo='/ ':option:option:(/yolo=)' "$matchany" ':optarg:optarg:(test this)' \| /$'/final='/ ':option:option:(/final=)' /$'[0-9]##\0'/ ':number:number:' \) \#
_mycommand "$#"
You can read more about _regex_arguments here
http://zsh.sourceforge.net/Doc/Release/Completion-System.html#Completion-Functions
or here: https://github.com/vapniks/zsh-completions/blob/master/zsh-completions-howto.org
The important thing to note here is that there is no null char (\0) at the end of the pattern matches for the option names.

Related

zsh function case condition parse error near `)'

I'm following this blog to setup a zsh function to switch aws cli profiles
: https://mads-hartmann.com/2017/04/27/multiple-aws-profiles.html
This is the zsh function in the blog:
function aws-switch() {
case ${1} in
"")
clear)
export AWS_PROFILE=""
;;
*)
export AWS_PROFILE="${1}"
;;
esac
}
#compdef aws-switch
#description Switch the AWS profile
_aws-switch() {
local -a aws_profiles
aws_profiles=$( \
grep '\[profile' ~/.aws/config \
| awk '{sub(/]/, "", $2); print $2}' \
| while read -r profile; do echo -n "$profile "; done \
)
_arguments \
':Aws profile:($(echo ${aws_profiles}) clear)'
}
_aws-switch "$#"
I added these lines to my ~/.zshrc, when I run source ~/.zshrc
It gives /.zshrc:4: parse error near `)'
I read the zsh function doc but still not very good at understanding the syntax and how could I fix this.
Have a look at the zsh man page (man zshmisc):
case word in [ [(] pattern [ | pattern ] ... ) list (;;|;&|;|) ] ... esac
As you see, you have to separate multiple pattern by |:
case $1 in
|clear)
....

Zsh completion case insensitive _multi_parts function

I have a script that takes file like arguments (multi part arguments), I am fetching the possible values and putting them in an array called raw and then using
_multi_parts / "(${raw[#]})"
to autocomplete. The problem is that this is case sensitive, how can I make it so that it I type mycommand get fo and press Tab it will autocomplete to mycommand get Foo/ if Foo is one of the things in raw.
The full completion is here for reference:
_mycommand() {
local curcontext="$curcontext" state line
_arguments "1: :->command" "*: :->label"
case $state in
command)
_arguments "1: :(add get ls rm)"
;;
*)
case $words[2] in
add|get|ls|rm)
if [ -f ~/.pts ]; then
IFS=$'\n' read -d '' -r raw <<< "$(mycommand ls)"
_multi_parts / "(${raw[#]})"
fi
;;
*)
_files
;;
esac
esac
}
_mycommand "$#"
mycommand ls outputs path like names like the following:
Foo/Bar/x
Foo/Bar/y
Derp/z
Placeholder/z
Okay I figured it out:
Change lines
IFS=$'\n' read -d '' -r raw <<< "$(mycommand ls)"
_multi_parts / "(${raw[#]})"
To
IFS=$'\n' raw=($(mycommand ls))
_multi_parts -M "m:{[:lower:][:upper:]}={[:upper:][:lower:]}" / raw

How to quote strings in file names in zsh (passing back to other scripts)

I have a script that has a string in a file name like so:
filename_with_spaces="a file with spaces"
echo test > "$filename_with_spaces"
test_expect_success "test1: filename with spaces" "
run cat \"$filename_with_spaces\"
run grep test \"$filename_with_spaces\"
"
test_expect_success is defined as:
test_expect_success () {
echo "expecting success: $1"
eval "$2"
}
and run is defined as:
#!/bin/zsh
# make nice filename removing special characters, replace space with _
filename=`echo $# | tr ' ' _ | tr -cd 'a-zA-Z0-9_.'`.run
echo "#!/bin/zsh" > $filename
print "$#" >> $filename
chmod +x $filename
./$filename
But when I run the toplevel script test_expect_success... I get cat_a_file_with_spaces.run with:
#!/bin/zsh
cat a file with spaces
The problem is the quotes around a file with spaces in cat_a_file_with_spaces.run is missing. How do you get Z shell to keep the correct quoting?
Thanks
Try
run cat ${(q)filename_with_spaces}
. It is what (q) modifier was written for. Same for run script:
echo -E ${(q)#} >> $filename
. And it is not bash, you don't need to put quotes around variables: unless you specify some option (don't remember which exactly)
command $var
always passes exactly one argument to command no matter what is in $var. To ensure that some zsh option will not alter the behavior, put
emulate -L zsh
at the top of every script.
Note that initial variant (run cat \"$filename_with_spaces\") is not a correct quoting: filename may contain any character except NULL and / used for separating directories. ${(q)} takes care about it.
Update: I would have written test_expect_success function in the following fashion:
function test_expect_success()
{
emulate -L zsh
echo "Expecting success: $1" ; shift
$#
}
Usage:
test_expect_success "Message" run cat $filename_with_spaces

How to custom display prompt in KornShell to show hostname and current directory?

I am using KornShell (ksh) on Solaris and currently my PS1 env var is:
PS1="${HOSTNAME}:\${PWD} \$ "
And the prompt displays: hostname:/full/path/to/current/directory $
However, I would like it to display: hostname:directory $
In other words, how can I display just the hostname and the name of the current directory, i.e. tmp or ~ or public_html etc etc?
From reading the ksh man page you want
PS1="${HOSTNAME}:\${PWD##*/} \$ "
Tested on default ksh on SunOS 5.8
Okay, a little old and a little late, but this is what I use in Kornshell:
PS1='$(print -n "`logname`#`hostname`:";if [[ "${PWD#$HOME}" != "$PWD" ]] then; print -n "~${PWD#$HOME}"; else; print -n "$PWD";fi;print "\n$ ")'
This makes a prompt that's equivalent to PS1="\u#\h:\w\n$ " in BASH.
For example:
qazwart#mybook:~
$ cd bin
qazwart#mybook:~/bin
$ cd /usr/local/bin
qazwart#mybook:/usr/local/bin
$
I like a two line prompt because I sometimes have very long directory names, and they can take up a lot of the command line. If you want a one line prompt, just leave off the "\n" on the last print statement:
PS1='$(print -n "`logname`#`hostname`:";if [[ "${PWD#$HOME}" != "$PWD" ]] then; print -n "~${PWD#$HOME}"; else; print -n "$PWD";fi;print "$ ")'
That's equivalent to PS1="\u#\h:\w$ " in BASH:
qazwart#mybook:~$ cd bin
qazwart#mybook:~/bin$ cd /usr/local/bin
qazwart#mybook:/usr/local/bin$
It's not quite as easy as setting up a BASH prompt, but you get the idea. Simply write a script for PS1 and Kornshell will execute it.
For Solaris and other Older Versions of Kornshell
I found that the above does not work on Solaris. Instead, you'll have to do it the real hackish way...
In your .profile, make sure that ENV="$HOME/.kshrc"; export ENV
is set. This is probably setup correctly for you.
In your .kshrc file, you'll be doing two things
You'll be defining a function called _cd. This function will change to the directory specified, and then set your PS1 variable based upon your pwd.
You'll be setting up an alias cd to run the _cd function.
This is the relevant part of the .kshrc file:
function _cd {
logname=$(logname) #Or however you can set the login name
machine=$(hostname) #Or however you set your host name
$directory = $1
$pattern = $2 #For "cd foo bar"
#
# First cd to the directory
# We can use "\cd" to evoke the non-alias original version of the cd command
#
if [ "$pattern" ]
then
\cd "$directory" "$pattern"
elif [ "$directory" ]
then
\cd "$directory"
else
\cd
fi
#
# Now that we're in the directory, let's set our prompt
#
$directory=$PWD
shortname=${directory#$HOME} #Possible Subdir of $HOME
if [ "$shortName" = "" ] #This is the HOME directory
then
prompt="~$logname" # Or maybe just "~". Your choice
elif [ "$shortName" = "$directory" ] #Not a subdir of $HOME
then
prompt="$directory"
else
prompt="~$shortName"
fi
PS1="$logname#$hostname:$prompt$ " #You put it together the way you like
}
alias cd="_cd"
This will set your prompt as the equivelent BASH PS1="\u#\h:\w$ ". It isn't pretty, but it works.
ENV=~/.kshrc, and then in your .kshrc:
function _cd {
\cd "$#"
PS1=$(
print -n "$LOGNAME#$HOSTNAME:"
if [[ "${PWD#$HOME}" != "$PWD" ]]; then
print -n "~${PWD#$HOME}"
else
print -n "$PWD"
fi
print "$ "
)
}
alias cd=_cd
cd "$PWD"
Brad
HOST=`hostname`
PS1='$(print -n "[${USER}#${HOST%%.*} ";[[ "$HOME" == "$PWD" ]] && print -n "~" ||([[ "${PWD##*/}" == "" ]] && print -n "/" || print -n "${PWD##*/}");print "]$")'
PS1=`id -un`#`hostname -s`:'$PWD'$
and...
if you work between two shells for most of your effort [ksh and bourne sh]
and desire a directory tracking display on your command line
then PWD can be substituted easily in ksh
and if you use /usr/xpg4/bin/sh for your sh SHELL, it will work there as well
Try this:
PS1="\H:\W"
More information on: How to: Change / Setup bash custom prompt, I know you said ksh, but I am pretty sure it will work.

Quoting command-line arguments in shell scripts

The following shell script takes a list of arguments, turns Unix paths into WINE/Windows paths and invokes the given executable under WINE.
#! /bin/sh
if [ "${1+set}" != "set" ]
then
echo "Usage; winewrap EXEC [ARGS...]"
exit 1
fi
EXEC="$1"
shift
ARGS=""
for p in "$#";
do
if [ -e "$p" ]
then
p=$(winepath -w $p)
fi
ARGS="$ARGS '$p'"
done
CMD="wine '$EXEC' $ARGS"
echo $CMD
$CMD
However, there's something wrong with the quotation of command-line arguments.
$ winewrap '/home/chris/.wine/drive_c/Program Files/Microsoft Research/Z3-1.3.6/bin/z3.exe' -smt /tmp/smtlib3cee8b.smt
Executing: wine '/home/chris/.wine/drive_c/Program Files/Microsoft Research/Z3-1.3.6/bin/z3.exe' '-smt' 'Z: mp\smtlib3cee8b.smt'
wine: cannot find ''/home/chris/.wine/drive_c/Program'
Note that:
The path to the executable is being chopped off at the first space, even though it is single-quoted.
The literal "\t" in the last path is being transformed into a tab character.
Obviously, the quotations aren't being parsed the way I intended by the shell. How can I avoid these errors?
EDIT: The "\t" is being expanded through two levels of indirection: first, "$p" (and/or "$ARGS") is being expanded into Z:\tmp\smtlib3cee8b.smt; then, \t is being expanded into the tab character. This is (seemingly) equivalent to
Y='y\ty'
Z="z${Y}z"
echo $Z
which yields
zy\tyz
and not
zy yz
UPDATE: eval "$CMD" does the trick. The "\t" problem seems to be echo's fault: "If the first operand is -n, or if any of the operands contain a backslash ( '\' ) character, the results are implementation-defined." (POSIX specification of echo)
bash’s arrays are unportable but the only sane way to handle argument lists in shell
The number of arguments is in ${#}
Bad stuff will happen with your script if there are filenames starting with a dash in the current directory
If the last line of your script just runs a program, and there are no traps on exit, you should exec it
With that in mind
#! /bin/bash
# push ARRAY arg1 arg2 ...
# adds arg1, arg2, ... to the end of ARRAY
function push() {
local ARRAY_NAME="${1}"
shift
for ARG in "${#}"; do
eval "${ARRAY_NAME}[\${#${ARRAY_NAME}[#]}]=\${ARG}"
done
}
PROG="$(basename -- "${0}")"
if (( ${#} < 1 )); then
# Error messages should state the program name and go to stderr
echo "${PROG}: Usage: winewrap EXEC [ARGS...]" 1>&2
exit 1
fi
EXEC=("${1}")
shift
for p in "${#}"; do
if [ -e "${p}" ]; then
p="$(winepath -w -- "${p}")"
fi
push EXEC "${p}"
done
exec "${EXEC[#]}"
I you do want to have the assignment to CMD you should use
eval $CMD
instead of just $CMD in the last line of your script. This should solve your problem with spaces in the paths, I don't know what to do about the "\t" problem.
replace the last line from $CMD to just
wine '$EXEC' $ARGS
You'll note that the error is ''/home/chris/.wine/drive_c/Program' and not '/home/chris/.wine/drive_c/Program'
The single quotes are not being interpolated properly, and the string is being split by spaces.
You can try preceeding the spaces with \ like so:
/home/chris/.wine/drive_c/Program Files/Microsoft\ Research/Z3-1.3.6/bin/z3.exe
You can also do the same with your \t problem - replace it with \\t.

Resources