serhii.net

In the middle of the desert you can say anything you want

21 Jun 2024

Fish adventures in noglob, calculators and expressions

TL;DR: fish easy version below works, but needs quotes when expression is complex: cc 2+2 but cc 'floor(2.3)'.

I’m continuing to move my useful snippets from zsh to fish (240620-2109 Fish shell bits), and the most challenging one was the CLI python calculator I really love and depend on, since it contained arguments with parentheses (which are fish expressions as well).

Basically: cc WHATEVER runs WHATEVER inside python, can do both easy math a la 2+2 and more casual statistics-y mean([2,33,28]).

Before in zsh this was the magic function:

cc() python3 -c "from math import *; from statistics import *; print($*);"
alias cc='noglob cc'

Fish, easy version:

function cc
  command python3 -c "from math import *; from statistics import *; print($argv);"
end

Works for easy cc 2+2 bits, but as soon as functions and therefore parentheses get involved (cc floor(2.3)) it starts to error out.

[I] sh@nebra~/t $ cc mean([2,4])
fish: Unknown command: '[2,4]'
in command substitution
fish: Unknown command
cc mean([2,4])
       ^~~~~~^
[I] sh@nebra~/t $ cc mean\([2,4]\)

>>> mean([2,4])
3
[I] sh@nebra~/t $

(But I REALLY don’t want to do cc mean\([2, 3]\))

In the zsh snippet, noglob meant basically “take this literally w/o expanding anything”, and it passed everything as-is to python, and this is what fails in my fish solution.

Noglob in fish is fun:

THEN

  • command python3 -c "from math import *; from statistics import *; print($argv);"

    • cc ceil\(2\) +
    • cc ceil(2) -
  • `command python3 -c “from math import *; from statistics import *; print(’$argv’);”

    • literally prints the passed thing w/o python eval, w/ same rules
  • OK can I do a variable then?

  set pyc $argv
  echo $pyc
  command python3 -c "from math import *; from statistics import *; print($pyc);"

nope.

Bruteforcing the solution

(and learning to use fish loops mainly, of course there are better ways to do this.)


# list of simple, brackets, and parentheses + no, single, double quotes 
# no space between nums in brackets, python interpreter would add them. [2,3] — literal, [2, 3] — parsed by python
set cmds \
'2+2' \
'\'2+2\'' \
'"2+2"' \
'[2,3]' \
'\'[2,3]\'' \
'"[2,3]"' \
'floor(2.3)' \
'\'floor(2.3)\'' \
'"floor(2.3)"' 

function tcc
  set pyc $argv
  # command python3 -c "from math import *; from statistics import *; print" '(' "$pyc" ');'
  # command python3 -c "from math import *; from statistics import *; print($pyc);"
  command python3 -c "from math import *; from statistics import *; print($pyc);"
end


# loop through all test cases to see sth that works for all
for i in $cmds
  echo $i:
  echo "   $(tcc $i)"
end

At the end, no additional literal quotes + initial command didn’t error out, and we came full circle:

set cmds \
'2+2' \
'[2,3]' \
'floor(2.3)' 

# winner command!
function tcc
  command python3 -c "from math import *; from statistics import *; print($argv);"
end
[I] sh@nebra~/t $ ./test_cc.sh
2+2:
   4
[2,3]:
   [2, 3]
floor(2.3):
   2
  • Double quotes in the python command mean only $pyc gets expanded
  • $pyc in the working versions have no hard-coded quotes
  • in CLI tcc floor(2.3) still fails — because like that it’s a command, not a string. In the file it was inside single quotes, as a string. So I can do this in the CLI as well.

So simple and logical at the end.

Final solution

function cc
  echo ">>> $argv"
  command python3 -c "from math import *; from statistics import *; print($argv);"
end

When using, quotes are needed only for complex bits (parentheses, * etc.).

[I] sh@nebra~/t $ cc 2+2
>>> 2+2
4

[I] sh@nebra~/t $ cc [2,3,4]
>>> [2,3,4]
[2, 3, 4]

# no quotes
[I] sh@nebra~/t $ cc mean([2,3,4])
fish: Unknown command: '[2,3,4]'
in command substitution
fish: Unknown command
cc mean([2,3,4])
       ^~~~~~~~^

# with quotes
[I] sh@nebra~/t $ cc 'mean([2,3,4])'
>>> mean([2,3,4])
3

So I literally had to follow the advice from the first link I found and used single quotes in my initial command:

If you wish to use arguments that may be expanded somehow literally, quote them. echo ‘’ and echo “” both will print the literal.

Still, I learned a lot about fish in the process and honestly am loving it.

Nel mezzo del deserto posso dire tutto quello che voglio.