ZSH autocomplete function using existing autocompletions - autocomplete

I have a function mycmd to launch a program that I wrote. The program needs the first argument to be foo, ssh or ls. The second argument depends on the first argument as follows,
foo -> No second argument
ssh -> Something to ssh to
ls -> A file
I want to write zsh autocomplete function for mycmd which suggest the second argument depending on the first argument. In the simplest form, I know that I can do the following for the first argument
_mycmd() {
compadd foo ssh ls
}
compdef _mycmd mycmd
I have a hard time understanding what to do for the second argument from here. How do I use _ssh autocomplete for ssh argument and _ls autocomplete for ls argument? (And nothing for foo as well)

To inspect the current command line, it could be used $words and $CURRENT that are the completion special parameters.
CURRENT
This is the number of the current word, i.e. the word the cursor is currently on in the words array.
...
words
This array contains the words present on the command line curently being edited.
--- zshcompwid(1), completion special parameters, zshcompwid - zsh completion widgets
The completion function could modify $words and $CURRENT (and/or other variables) and then start the entire completion system based with its modified command line. For example:
$ mycmd ls -al<TAB> ;# This is the input, and
;# $words == ("mycmd" "ls" "-al") ;# original value for $words.
;# $words=("ls" "-al") ;# We could update $words for making zsh
;# $ ls -al<TAB> ;# to start the completion system with
;# its modified command line.
;# Finally, _ls would be called.
The utility function _normal could be used.
_normal
...
A second use is to reexamine the command line specified by the $words array and the $CURRENT parameter after those have been modified.
-- zshcompsys(1), utility function, _normal
_mycmd could be listed below:
_mycmd () {
if ((CURRENT == 2)); then
compadd foo ssh ls
elif ((CURRENT > 2)); then
case "$words[2]" in
(ssh|ls)
shift words
((CURRENT--))
_normal -p mycmd
;;
(foo)
_nothing
;;
(*)
_message "mycmd: invalid subcommand or arguments"
;;
esac
fi
return $?
}
or even, more use of the completion builtin/utility functions like below:
_mycmd () {
local curcontext="${curcontext}" state context line
local -A opt_args
_arguments '*:: :->subcmd'
if [[ "$state" == "subcmd" ]]; then
if ((CURRENT == 1)); then
_describe -t mycmd-subcmd "mycmd command" '(foo ssh ls)'
else
curcontext="${curcontext%:*:*}:mycmd-$words[1]:"
case "$words[1]" in
(ssh|ls)
compset -n 1
_normal -p $service
;;
(foo)
_nothing
;;
(*)
_message "mycmd: invalid subcommand or arguments"
;;
esac
fi
fi
return $?
}

Related

Separate command-line arguments into two lists and pass to programs (shell)

What I want to do is take a list of command-like arguments like abc "def ghi" "foo bar" baz (note that some arguments are quoted because they contain spaces), and separate them out into two lists of arguments which then get passed to other programs that are invoked by the script. For example, odd-numbered arguments to one program and even-numbered arguments to another program. It is important to preserve proper quoting.
Please note, I need a solution in pure Bourne Shell script (i.e., sh not bash or such). The way I'd do this in Bash would be to use arrays, but of course the Bourne Shell doesn't have support for arrays.
At the cost of iterating over the original arguments twice, you can define a function that can run a simple command using only the even or odd arguments. This allows us to use the function's arguments as an additional array.
# Usage:
# run_it <cmd> [even|odd] ...
#
# Runs <cmd> using only the even or odd arguments, as specified.
run_it () {
cmd=${1:?Missing command name}
parity=${2:?Missing parity}
shift 2
n=$#
# Collect the odd arguments by discarding the first
# one, turning the odd arguments into the even arguments.
if [ $# -ge 1 ] && [ $parity = odd ]; then
shift
n=$((n - 1))
fi
# Repeatedly move the first argument to the
# to the end of the list and discard the second argument.
# Keep going until you have moved or discarded each argument.
while [ "$n" -gt 0 ]; do
x=$1
if [ $n -ge 2 ]; then
shift 2
else
shift
fi
set -- "$#" "$x"
n=$((n-2))
done
# Run the given command with the arguments that are left.
"$cmd" "$#"
}
# Example command
cmd () {
printf '%s\n' "$#"
}
# Example of using run_it
run_it cmd even "$#"
run_it cmd odd "$#"
This might be what you need. Alas, it uses eval. YMMV.
#!/bin/sh
# Samples
foo() { showme foo "$#"; }
bar() { showme bar "$#"; }
showme() {
echo "$1 args:"
shift
local c=0
while [ $# -gt 0 ]; do
printf '\t%-3d %s\n' $((c=c+1)) "$1"
shift
done
}
while [ $# -gt 0 ]; do
foo="$foo \"$1\""
bar="$bar \"$2\""
shift 2
done
eval foo $foo
eval bar $bar
There's no magic here -- we simply encode alternating arguments with quote armour into variables so they'll be processed correctly when you eval the line.
I tested this with FreeBSD's /bin/sh, which is based on ash. The shell is close to POSIX.1 but is not necessarily "Bourne". If your shell doesn't accept arguments to shift, you can simply shift twice in the while loop. Similarly, the showme() function increments a counter, an action which can be achieved in whatever way is your favourite if mine doesn't work for you. I believe everything else is pretty standard.

How to define a function that either takes arguments or doesnt?

I am using Fish shell....
Basically, to do something like this:
if (first argument == --r) {
do something
} else {
Do something
if (first argument == --n) {
do more
}
}
To achieve the first if statement I tried:
if test (count $argv) -eq 1 -a $argv[1] = '--r'
But that gives a message:
test: Missing argument at index 6
Functions in Fish don't require their parameters to be specified when you define the function. Any arguments sent to the function by the user are automatically stored in an array called argv. In order to determine whether arguments were sent, you can either count the number of elements in the array, or determine the length of the array as a string. I do the latter:
function my_func
if [ -z "$argv" ]; # No arguments
echo "No arguments supplied"
return
else # At least one argument
if [ "$argv[1]" = "--r" ];
echo "Excellent!"
return
end
end
end
If you prefer to use count, then it will look more like this:
function my_func
if [ (count $argv) -eq 1 -a "$argv[1]" = "--r" ];
# Exactly one argument with specified value "--r"
echo "Excellent!"
return
else # May have arguments, but none equal to "--r"
echo "Give me the right arguments"
return
end
end
Your use of set -q argv[1] is also a good option. But when you're checking for string equality, don't forget to surround your variable in quotes, like this: test "$argv[1]" = "--r".
Here's another method, using the switch...case conditional test:
function my_func
# No arguments
if [ -z "$argv" ]; and return
# At least one argument
switch $argv[1];
case --r;
# do some stuff
return
case "*";
# Any other arguments passed
return
end
end
end
This worked for me:
if set -q argv[1] ;and test $argv[1] = "--r"
Let's start with the error you get when executing this:
if test (count $argv) -eq 1 -a $argv[1] = '--r'
That happens because fish first expands $argv[1] then executes test. If argv has no values then that statement turns into
if test 0 -eq 1 -a = '--r'
Which isn't valid syntax for the test command. It doesn't matter that the first sub-expression evaluates to false since test parses the entire expression before evaluating it.
Rather than doing test (count $argv) -eq 1 just do set -q argv[1] to test if argv has at least one argument. Note the lack of a dollar-sign.
If you're using fish 2.7.0 or newer I recommend the new argparse builtin for handling arguments. Several of the standard functions that ship with fish use it so you can look at them, as well as man argparse, for examples of how to use it. Using argparse is almost always safer, less likely to result in bugs due to sloppy argument parsing using hand written fish script, and will provide argument parsing semantics identical to most commands including all the fish builtins. Including correctly handling short and long flags.
well if the argument is optional then you can do this by:
//check if variable exists
if (typeof variable === 'undefined'){
}
else{
if(typeof variable){}
}

How do I use argv with a wildcard?

I'm trying to write a function which will change the extension for all matching files in the current directory:
function chext
if count $args -eq 2 > /dev/null
for f in (ls *$argv[1])
mv (basename $f $argv[2]) (basename $f $argv[1])
end
else
echo "Error: use the following syntax"
echo "chext oldext newext"
end
end
I keep getting this output though:
(*master) λ chext markdown txt
No matches for wildcard '*$argv[1]'. (Tip: empty matches are allowed in 'set', 'count', 'for'.)
~/.config/fish/config.fish (line 1): ls *$argv[1]
^
in command substitution
called on line 60 of file ~/.config/fish/config.fish
in function 'chext'
called on standard input
with parameter list 'markdown txt'
I know there must be a way to interpolate the variable and keep * as a wildcard, but I can't figure it out. I have tried using (string join '*' $argv[1]), but it turned the wildcard into a string.
I did get this to almost work:
function chext
if count $args -eq 2 > /dev/null
for f in (ls (string join * $argv[1]))
mv (basename $f $argv[2]) (basename $f $argv[1])
end
else
echo "Error: use the following syntax"
echo "chext oldext newext"
end
end
It removed the extensions from all of my files but didn't add the new one, then gave me an error which was all of the file names combined and said file name too long.
UPDATE:
I'm sure there is a way to get the correct ls working, but I did get this working successfully:
if count $args -eq 2 > /dev/null
for f in (ls)
mv $f (string replace -r \.$argv[1]\$ .$argv[2] (basename $f))
end
else
echo "Error: use the following syntax"
echo "chext oldext newext"
end
end
The answer is in the message:
No matches for wildcard '*$argv[1]'. (Tip: empty matches are allowed in 'set', 'count', 'for'.)
That means
for i in *argv[1]
works, as does
set -l expanded *argv[1]
If you try to launch any other command with a glob that doesn't match anything, fish will skip the execution and print an error.
Your function has a number of other issues, allow me to go through them one by one.
if count $args -eq 2 > /dev/null
count does not take the "-eq 2" argument. It will return 0 (i.e. true) whenever it is given an argument, so this will always be true.
What you tried to do was if test (count $args) -eq 2.
What also works (and what I happen to like more) is if set -q argv[2]
(also, it's "argv", not "args").
for f in (ls *$argv[1])
Please do not use the output of ls. It doesn't have as much problems in fish as it does in bash (where it'll break once a file has a space, tab or newline in the name), but it's unnecessary (it launches an additional process) and still has an issue - it'll break when a file has a newline in its name.
Just do for f in *$argv[1], which will also nicely solve your question.
mv (basename $f $argv[2]) (basename $f $argv[1])
I'm not sure what basename you are using here, but mine just removes a suffix. So if you did chext .opus .ogg, this would execute
mv (basename song.opus .ogg) (basename song.opus .opus)
which would resolve to
mv song.opus song
Personally, I'd use string replace for this. Something like
mv $f (string replace -- $argv[1] $argv[2] $f)
(Note: This would do the wrong thing if the extension string appears before, e.g. if you had a file called "f.ogg-banana.ogg" only the first ".ogg" would be changed. You might want to use regular expressions for this: string replace -r -- "$argv[1]\$" $argv[2] $f)

tcl script to compare 2 texts and return the line numbers of the lines that differ only. Also, How to avoid "child process exit abnormally"?

can anybody guide me how to write tcl script to compare 2 texts and return the line numbers of the lines that differ only ?
I know how to do it in bash, but to include the bash in tcl doesnt seem to be very neat, here's the bash command :
diff --old-line-format '%L' --new-line-format '' --unchanged-line-format '' <(nl File1) <(nl File2) | awk '{print $1 }' > difflines
To include this in tcl, i did the following :
exec cat nl File1 > File11
exec cat nl File2 > File22
exec diff --old-line-format {%L} --new-line-format {} --unchanged-line-format {} <
File11 < File22 | awk {{print $1 }} > difflines
Is there a cleaner way ?
Also if there's difference i get the "child exit abnormally", how can i avoid this ?
Thanks
The struct::list package in Tcllib has tools for computing longest-common-subsequences, which is the key part of a diff tool. To use, you load your data into Tcl and split it into a list of lines:
proc getLines {filename} {
set f [open $filename]
set result [split [read $f] "\n"]
close $f
return $result
}
Then you can get the information about the common elements (== common lines):
set sharedLineInfo [struct::list longestCommonSubsequence $file1_lines $file2_lines]
This returns a pair of lists, where each list is the indices (counting from zero) of the common lines; the first list will be for the first file, and the second list for the second file. Any line number not listed will be one that has changed.
There's also a function to invert the information provided to get instructions on how to change one sequence into the other:
set changes [struct::list lcsInvert $sharedLineInfo \
[llength $file1_lines] [llength $file2_lines]]
This returns a list of triples, where the first is the operation performed (added, changed or deleted) and the second and third are the ranges of indices in each of the relevant lists (i.e., zero-based line numbers).
I'm not quite sure how to take this information and produce what you are looking for, but I guess we could put it together like this:
package require struct::list
proc getLines {filename} {
set f [open $filename]
set result [split [read $f] "\n"]
close $f
return $result
}
proc variedLines {filename1 filename2} {
set l1 [getLines $filename1]
set l2 [getLines $filename2]
lassign [struct::list longestCommonSubsequence $l1 $l2] common1
set result {}
for {set i 0} {$i < [llength $l1]} {incr i} {
if {$i ni $common1} {
lappend result [expr {$i + 1}]
}
}
return $result
}
If you want the results written to a file, puts $f [join $someList "\n"] is likely to feature, but I'll leave that as an exercise…
Regarding "child process exited abnormally", from the exec man page (emphasis mine):
If any of the commands in the pipeline exit abnormally or are killed or suspended, then exec will return an error and the error message will include the pipeline's output followed by error messages describing the abnormal terminations; the -errorcode return option will contain additional information about the last abnormal termination encountered. If any of the commands writes to its standard error file and that standard error is not redirected and -ignorestderr is not specified, then exec will return an error; the error message will include the pipeline's standard output, followed by messages about abnormal terminations (if any), followed by the standard error output.
"commands exit abnormally" means that the command exits with a non-zero status. Some common commands like grep and diff return a non-zero exit status to indicate something normal, so you have to wrap that exec call in a catch
set rc [catch {exec bash -c {
diff --old-line-format '%L' --new-line-format '' --unchanged-line-format '' <(nl File1) <(nl File2) | awk '{print $1}' > difflines
}} output]
if {$rc == 0} {
puts "no differences found"
} elseif {$rc == 1} {
puts "differences found:"
puts $output
} else {
puts "diff returned an error: $output"
}

How do I output the current history event number (%! or %h) -1 in my ZSH prompt?

In my ZSH prompt I want to be able to output the current history event number (%! or %h) -1
If current history event number is !256, I want to subtract it by one and output the result in my prompt (i.e. !255).
Here's the way it looks now and how I'd like it to be:
Below is my current ZSH theme (and the code pertaining to this question lies in the previous_history_event_number () function, which is triggered from the return_code_enabled= declaration:
# ------------------------------------------------------------------------------
# FILE: hced.zsh-theme
# DESCRIPTION: oh-my-zsh theme file.
# (Credits to authors of blinks.zsh-theme and dieter.zsh-theme
# from which themes I've taken useful bits.)
# AUTHOR: hced
# VERSION: 0.0.1
# SCREENSHOT:
# ------------------------------------------------------------------------------
function _prompt_char() {
if $(git rev-parse --is-inside-work-tree >/dev/null 2>&1); then
echo "%{%F{blue}%}±%{%f%k%b%}"
else
echo ' '
fi
}
ZSH_THEME_GIT_PROMPT_PREFIX=" [%{%B%F{blue}%}"
ZSH_THEME_GIT_PROMPT_SUFFIX="%{%f%k%b%K{black}%B%F{green}%}]"
ZSH_THEME_GIT_PROMPT_DIRTY=" %{%F{red}%}*%{%f%k%b%}"
ZSH_THEME_GIT_PROMPT_CLEAN=""
PROMPT='%{%f%k%b%}
%{%K{black}%B%F{green}%}%n%{%B%F{blue}%}#%{%B%F{cyan}%}%m%{%B%F{green}%} %{%b%F{yellow}%K{black}%}%~%{%B%F{green}%}$(git_prompt_info)%E%{%f%k%b%}
%{%K{black}%}$(_prompt_char)%{%K{black}%} %#%{%f%k%b%} '
#RPROMPT='!%{%B%F{cyan}%}%!%{%f%k%b%}'
function previous_history_event_number () {
prev_hist_num=("%!"-1)
declare -i prev_hist_num
echo "$prev_hist_num gave exit code: "
}
# elaborate exitcode on the right when >0
return_code_enabled="%(?..%{$fg[red]%}$(previous_history_event_number)%?%{$reset_color%})"
return_code_disabled=
return_code=$return_code_enabled
RPS1='${return_code} !%{%B%F{cyan}%}%!%{%f%k%b%}'
function accept-line-or-clear-warning () {
if [[ -z $BUFFER ]]; then
time=$time_disabled
return_code=$return_code_disabled
else
time=$time_enabled
return_code=$return_code_enabled
fi
zle accept-line
}
zle -N accept-line-or-clear-warning
bindkey '^M' accept-line-or-clear-warning
Disclaimer: I'm not a programmer so the code within the previous_history_event_number () function is pretty (vastly) clueless right now.
If current history event number is shown when you add %! atom in a prompt then you can get it into a variable by using (%) modifier:
V='%!'
integer HISTORY_EVENT_NUMBER=${(%)V}-1