swiftbar / SwiftBar

Powerful macOS menu bar customization tool
https://swiftbar.app
MIT License
2.93k stars 92 forks source link

Shell parameters over-escaping #308

Closed kdeldycke closed 2 years ago

kdeldycke commented 2 years ago

In the meta_package_manger.7h.py plugin I'm maintaining for the last 5 years, I'm trying to execute a python3 -m pip command.

Which works for bash, but not for zsh as you can see:

❯ bash
The default interactive shell is now zsh.
To update your account to use zsh, please run `chsh -s /bin/zsh`.
For more details, please visit https://support.apple.com/kb/HT208050.

bash-3.2$ mpm --version
bash: mpm: command not found

bash-3.2$ python3 -m pip install --upgrade meta-package-manager>=4.2.0

bash-3.2$ mpm --version
mpm, version 4.12.1
❯ zsh --version
zsh 5.8.1 (arm-apple-darwin21.3.0)

❯ python3 -m pip install --upgrade meta-package-manager>=4.2.0
zsh: 4.2.0 not found

The trick is to quote the last parameter or escape the angle-bracket within:

❯ python3 -m pip install --upgrade "meta-package-manager>=4.2.0"
(...)
Successfully installed meta-package-manager-4.12.1

❯ python3 -m pip install --upgrade meta-package-manager\>=4.2.0
(...)
Requirement already satisfied: meta-package-manager>=4.2.0
bash-3.2$ python3 -m pip install --upgrade "meta-package-manager>=4.2.0"
(...)
Requirement already satisfied: meta-package-manager>=4.2.0

bash-3.2$ python3 -m pip install --upgrade meta-package-manager\>=4.2.0
(...)
Requirement already satisfied: meta-package-manager>=4.2.0

These examples have the advantage of working for both bash and zsh.

Now I'd like to transpose that workaround in SwiftBar. But no amount of tweaking let me have one of this CLI executed as-is.

Here is a Python-based SwiftBar plugin exploring the possibilities:

#!/usr/bin/env python3

print("Escape Test | dropdown=false")
print("---")

base_cmd = " | terminal=true shell=python3 param1=-m param2=pip param3=install param4=--upgrade "

# Bare argument
print("Test 1" + base_cmd + "param5=meta-package-manager>=4.2.0")

# Quoted argument
print("Test 2" + base_cmd + "param5='meta-package-manager>=4.2.0'")
print("Test 3" + base_cmd + 'param5="meta-package-manager>=4.2.0"')

# Quote hacks
print("Test 4" + base_cmd + 'param5=\\"meta-package-manager>=4.2.0\\"')
print("Test 5" + base_cmd + "param5=\\'meta-package-manager>=4.2.0\\'")
print("Test 6" + base_cmd + 'param5=\\\\"meta-package-manager>=4.2.0\\\\"')
print("Test 7" + base_cmd + "param5=\\\\'meta-package-manager>=4.2.0\\\\'")

# Angle bracket hacks
print("Test 8" + base_cmd + "param5=meta-package-manager\>=4.2.0")
print("Test 9" + base_cmd + "param5=meta-package-manager\\>=4.2.0")

On execution, it returns:

❯ ./escape_test.1d.py
Escape Test | dropdown=false
---
Test 1 | terminal=true shell=python3 param1=-m param2=pip param3=install param4=--upgrade param5=meta-package-manager>=4.2.0
Test 2 | terminal=true shell=python3 param1=-m param2=pip param3=install param4=--upgrade param5='meta-package-manager>=4.2.0'
Test 3 | terminal=true shell=python3 param1=-m param2=pip param3=install param4=--upgrade param5="meta-package-manager>=4.2.0"
Test 4 | terminal=true shell=python3 param1=-m param2=pip param3=install param4=--upgrade param5=\"meta-package-manager>=4.2.0\"
Test 5 | terminal=true shell=python3 param1=-m param2=pip param3=install param4=--upgrade param5=\'meta-package-manager>=4.2.0\'
Test 6 | terminal=true shell=python3 param1=-m param2=pip param3=install param4=--upgrade param5=\\"meta-package-manager>=4.2.0\\"
Test 7 | terminal=true shell=python3 param1=-m param2=pip param3=install param4=--upgrade param5=\\'meta-package-manager>=4.2.0\\'
Test 8 | terminal=true shell=python3 param1=-m param2=pip param3=install param4=--upgrade param5=meta-package-manager\>=4.2.0
Test 9 | terminal=true shell=python3 param1=-m param2=pip param3=install param4=--upgrade param5=meta-package-manager\>=4.2.0

Which renders into SwiftBar as:

Screen Shot 2022-04-13 at 15 10 08

Some of these 9 tests properly executes my command, others fails:

❌ Tests #1, #2 and #3 runs:

❯ export SWIFTBAR_PLUGIN_CACHE_PATH='/Users/kde/Library/Caches/com.ameba.SwiftBar/Plugins/escape_test.1d.py' SWIFTBAR='1' SWIFTBAR_PLUGINS_PATH='' SWIFTBAR_PLUGIN_DATA_PATH='/Users/kde/Library/Application Support/SwiftBar/Plugins/escape_test.1d.py' OS_VERSION_PATCH='1' OS_VERSION_MINOR='3' OS_VERSION_MAJOR='12' SWIFTBAR_VERSION='1.4.3' SWIFTBAR_LAUNCH_TIME='2022-04-06T14:20:14+04:00' OS_LAST_SLEEP_TIME='2022-04-10T10:29:57+04:00' OS_APPEARANCE='Light' SWIFTBAR_BUILD='422' SWIFTBAR_PLUGIN_PATH='/Users/kde/dotfiles/dotfiles/Library/Application Support/xbar/plugins/escape_test.1d.py' OS_LAST_WAKE_TIME='2022-04-10T14:48:52+04:00';python3 -m pip install --upgrade meta-package-manager>=4.2.0
zsh: 4.2.0 not found

✅ Tests #4 succeeded:

❯ export OS_VERSION_PATCH='1' SWIFTBAR_PLUGINS_PATH='' OS_LAST_WAKE_TIME='2022-04-10T14:48:52+04:00' OS_VERSION_MAJOR='12' SWIFTBAR_VERSION='1.4.3' SWIFTBAR_BUILD='422' OS_VERSION_MINOR='3' OS_APPEARANCE='Light' SWIFTBAR_PLUGIN_CACHE_PATH='/Users/kde/Library/Caches/com.ameba.SwiftBar/Plugins/escape_test.1d.py' OS_LAST_SLEEP_TIME='2022-04-10T10:29:57+04:00' SWIFTBAR='1' SWIFTBAR_PLUGIN_PATH='/Users/kde/dotfiles/dotfiles/Library/Application Support/xbar/plugins/escape_test.1d.py' SWIFTBAR_LAUNCH_TIME='2022-04-06T14:20:14+04:00' SWIFTBAR_PLUGIN_DATA_PATH='/Users/kde/Library/Application Support/SwiftBar/Plugins/escape_test.1d.py';python3 -m pip install --upgrade "meta-package-manager>=4.2.0"
(...)
Successfully installed meta-package-manager-4.12.1

❌ Tests #7 runs:

❯ export OS_VERSION_MINOR='3' SWIFTBAR_PLUGIN_DATA_PATH='/Users/kde/Library/Application Support/SwiftBar/Plugins/escape_test.1d.py' SWIFTBAR_LAUNCH_TIME='2022-04-06T14:20:14+04:00' SWIFTBAR='1' SWIFTBAR_BUILD='422' OS_VERSION_PATCH='1' OS_APPEARANCE='Light' SWIFTBAR_PLUGIN_CACHE_PATH='/Users/kde/Library/Caches/com.ameba.SwiftBar/Plugins/escape_test.1d.py' OS_LAST_WAKE_TIME='2022-04-10T14:48:52+04:00' OS_LAST_SLEEP_TIME='2022-04-10T10:29:57+04:00' SWIFTBAR_VERSION='1.4.3' SWIFTBAR_PLUGIN_PATH='/Users/kde/dotfiles/dotfiles/Library/Application Support/xbar/plugins/escape_test.1d.py' SWIFTBAR_PLUGINS_PATH='' OS_VERSION_MAJOR='12';python3 -m pip install --upgrade \'meta-package-manager>=4.2.0\'
zsh: 4.2.0' not found

❌ Tests #5, #6, #8 and #9 don't work at all and fails to launch a terminal.

Looking at these examples, it seems there is something inconsistent in the way xbar is parsing parameters, handling outer quotes and/or over-escaping special characters.

Here is my environment:

For the record, I tested the same test suite on xbar but the results are completely different.

melonamin commented 2 years ago

Hey @kdeldycke, thanks for such a thorough issues description.

Let me try to explain what is hapening under the hood.

First step, parse this thing:

Test 1 | terminal=true shell=python3 param1=-m param2=pip param3=install param4=--upgrade param5=meta-package-manager>=4.2.0

  1. Everething after | is a parameters string.
  2. Everything before = is parameter name, tabs and spaces are trimmed
  3. Everything after = and before a space is parameter value
  4. If you want a parameter value to contain a space, then you can quote it. In this case 3. works differently: everything within quotes after the = is a value. The quotes are not considered to be a part of the value and being stripped

That's why the internal shell command, before it is run will look like this:

Second step, execute the the shell command we've formed on the previous step. ~Unfortunately~ We have two options:

Run in background is more or less straightforward(not really), and doesn't mess with the escaping any further, run in terminal on the other hand is going through Apple Script. To make things a bit more complicated, there two, slightly different Apple Script srcipts, one for Terminal and one for iTerm.

        switch PreferencesStore.shared.terminal {
        case .Terminal:
            appleScript = """
            tell application "Terminal"
                activate
                tell application "System Events" to keystroke "t" using {command down}
                delay 0.2
                do script "\(runInTerminalScript)" in front window
                activate
            end tell
            """
        case .iTerm:
            appleScript = """
            tell application "iTerm"
                activate
                try
                    select first window
                    set onlywindow to false
                on error
                    create window with default profile
                    select first window
                    set onlywindow to true
                end try
                tell the first window
                    if onlywindow is false then
                        create tab with default profile
                    end if
                    tell current session to write text "\(runInTerminalScript)"
                end tell
            end tell
            """
        }

Because of that, as far as I can tell here is what we have:

All in all Test 4 is OK for terminal run and 8-9 for background.

I haven't time to thorougly test this, but I almost sure everything above is correct. Didn't look into bash instead zsh either, but I hope it helps.

melonamin commented 2 years ago

@kdeldycke hey, do you have any suggestions on how to make it easier for you to use?

kdeldycke commented 10 months ago

Might be related to: https://github.com/swiftbar/SwiftBar/issues/366