Parsing arguments in fish shell without argparse - fish

I'm using fish shell and wrote my own little parser function because I found argparse confusing. Basically, if a flag is matched, it uses the information from the following argument. However, I'm assuming my method must introduce bugs as I haven't seen this method used online. Are there advantages to using argparse that I'm missing?
function check_args
for current_arg in (seq 1 (count $argv))
#grab next argument
set next_arg $argv[(math $current_arg + 1)]
switch $argv[$current_arg]
case -h --help
usage
break
case -t --theme
echo "theme: " $next_arg
set -g theme themes/$next_arg.css
case -f --format
echo "format: " $next_arg
set -g format $next_arg
case -o --output
echo "output: " $next_arg
set -g output $next_arg
end
end
end
check_args $argv #calls the function with the passed arguments

With argparse:
# the -- is required!
argparse h/help t/theme= f/format= o/output= -- $argv
or exit 1
# just to inspect the variables
set -S _flag_h _flag_help _flag_t _flag_theme _flag_f _flag_format _flag_o _flag_output
if set -q _flag_help
usage
exit
end
set theme themes/$_flag_theme.css
set format $_flag_format
set output $_flag_output

Related

How to implement dynamic tab completion of options in Fish shell?

I want to implement dynamic tab completion of options to a Fish command. This is easy for the -a switch, but I can't figure out how to do it for the -l switch.
Consider the following lines:
$ complete -c foo -a '(echo bar\nbaz\nbiz)' -f
$ complete -c foo -l '(echo bar\nbaz\nbiz)' -f
The behavior of my shell is then as follows:
$ foo b<tab>
bar baz biz
$ foo --<tab>
foo --\(echo\ bar\\nbaz\\nbiz\)
Instead I'd like it to suggest three options --bar, --baz and --biz. Is this possible?
edit: Now I understand better. You can do this by just making your "arguments" start with dashes. Here's an example that uses a function for clarity:
function get_foo_completions
echo --bar
echo --baz
echo --biz
set prev_arg (commandline -pco)[-1]
test "$prev_arg" = print
and echo --conditional
end
complete -c foo -a '(get_foo_completions)' -f
The --conditional argument will only be printed if the previous argument is print which illustrates that these can be dynamic.
To my knowledge this isn't yet possible. Options are declarative, and only arguments to those options may be dynamic.
If you give more details about your use case I might be able to suggest other approaches.

using eval and background a process in the same line in Fish

How can I use eval to execute the command in a variable and also background it in the same line? I'm trying the following but it's not working. For example s xeyes I would expect the shell to return.
function s --description "Start a command in the background and remove from jobs list"
echo (count $argv)
if test (count $argv) -ne 1
echo "illegal number of parameters"
return 1
end
eval $argv[1] 2>&1 > /dev/null &
disown
end
Fish does not support backgrounding functions. eval is a function, so backgrounding it is not supported.
You need to put the & into the eval'd code, so
eval $argv[1] 2>&1 > /dev/null &
might work.
Alternatively, since fish 3.0 eval isn't needed here anymore, so you can just do
$argv[1] 2>&1 >/dev/null &

Can zsh `else` reserved keyword command be aliased and the lexem itself be repurposed as `fi` keyword command?

Following ZSH: Call in-built function from zsh function that uses the same name and Run a command that is shadowed by an alias, it might be expected that a command keyword equivalent of what builtin and command are doing for their respective eponymous token category; so that
if [ -z 'love' ]; then echo 'sad world'; keyword else echo 'wonderful world'; fi
would be equivalent to
if [ -z 'love' ]; then echo 'sad world'; else echo 'wonderful world'; fi
This problem was found in the following tricky scenario: being able to replace else with alie and fi with else. See Can zsh buildtins be aliased? for more details.
So an hypothetical attempt to implement that, if the keyword command existed, would be:
alias alie="keyword else"
alias else='fi'
So, to sum it up, the question is how do you make the following peace of zsh code works as expected by the previous command:
if [ -z 'love' ]; then echo 'sad world'; alie echo 'wonderful world'; else
This is not yet a working solution, but here is an idea: using the -r flag of enable and disable builtin commands to change visibility of the else keyword. So:
alias se='enable -r else; if'
alias alie='else'
disable -r else
alias else="fi; disable -r else"
This unfortunately doesn't work
se [ -z 'amo' ]; then echo 'trista mondo'; alie echo 'mirinda mondo'; else
# zsh: parse error near `fi'
This is however really on the "else" substitution that something break, as a non-inline version will indeed enter the else-branch and print "mirinda mondo".

How to create a variable in shell script whose name is present in variable?

Let's say I declare two variables key and value like this in shell script
$ key=key1
$ value=value1
now I want to create a variable of name key1 and assign it value value1. What I tried is
$ export ${key}
$ export ${value}
$ $($key=$value)
output: key1=value1: command not found
I don't know how to do this.
Use eval $key=$value instead of $($key=$value): The shell substitutes variable values first, and then the $(...) substitution. As there is not command with that name the shell shows command not found in STDERR. Tell the shell to evaluate the substitution result as a regular shell command again by using eval. Later you can export the result with export $key. Using the -x flag for the shell provides a good insight of what happens:
$ key=key1
$ value=value1
$ set -x
$ $key=$value
+ key1=value1
sh: key1=value1: not found [No such file or directory]
$ $($key=$value)
+ key1=value1
sh: key1=value1: not found [No such file or directory]
$ eval $key=$value
+ eval key1=value1
+ key1=value1
$ echo $key1
+ echo value1
value1
$ export $key
+ export key1
Also, be careful when dealing with variables this way: Whitespaces in shell variables can have unexpected results in this kind of constructions.

Escaping whitespace within nested shell/perl scripts

I'm trying to run a perl script from within a bash script (I'll change this design later on, but for now, bear with me). The bash script receives the argument that it will run. The argument to the script is as follows:
test.sh "myscript.pl -g \"Some Example\" -n 1 -p 45"
within the bash script, I simple run the argument that was passed:
#!/bin/sh
$1
However, in my perl script the -g argument only gets "Some (that's with the quotes), instead of the Some Example. Even if I quote it, it cuts off because of the whitespace.
I tried escaping the whitespace, but it doesn't work... any ideas?
To run it as posted test.sh "myscript.pl -g \"Some Example\" -n 1 -p 45" do this:
#!/bin/bash
eval "$1"
This causes the $1 argument to be parsed by the shell so the individual words will be broken up and the quotes removed.
Or if you want you could remove the quotes and run test.sh myscript.pl -g "Some Example" -n 1 -p 45 if you changed your script to:
#!/bin/bash
"$#"
The "$#" gets replaced by all the arguments $1, $2, etc., as many as were passed in on the command line.
Quoting is normally handled by the parser, which isn't seeing them when you substitute the value of $1 in your script.
You may have more luck with:
#!/bin/sh
eval "$1"
which gives:
$ sh test.sh 'perl -le "for (#ARGV) { print; }" "hello world" bye'
hello world
bye
Note that simply forcing the shell to interpret the quoting with "$1" won't work because then it tries to treat the first argument (i.e., the entire command) as the name of the command to be executed. You need the pass through eval to get proper quoting and then re-parsing of the command.
This approach is (obviously?) dangerous and fraught with security risks.
I would suggest you name the perl script in a separate word, then you can quote the parameters when referring to them, and still easily extract the script name without needing the shell to split the words, which is the fundamental problem you have.
test.sh myscript.pl "-g \"Some Example\" -n 1 -p 45"
and then
#!/bin/sh
$1 "$2"
if you really have to do this (for whatever reason) why not just do:
sh test.sh "'Some Example' -n 1 -p 45"
in:
test.sh
RUN=myscript.pl
echo `$RUN $1
(there should be backticks ` before $RUN and after $1)