Use webbrowser to open firefox with options

I want to execute the following command using the webbrowser module:

werner@x13dai-t:~$ ~/Public/browser/mozilla/firefox/firefox --profile ~/Public/browser/mozilla/default --new-window

But I don’t know how to pass options to it. Any tips will be helpful.

Regards,
Zhao

Why do you want to use the webbrowser module for this? Ite primarily purpose is for the situations where you don’t care exactly what browser is being started, i.e. if you want a quick-cross platform solution to open e.g. help pages.

Clearly you are willing to tie yourself to a specific browser, os and probably even system, so the subprocess module is a better choice.

2 Likes

But the webbrowser module itself also calls subprocess. That’s why I want to solve this problem based on the webbrowser module.

I was going to say “but the webbrowser module doesn’t provide that
degree of control”. But I was wrong.

Have a look at the webbrowser.register function. I think you should be
able to write a constructor function which does what you want.

I inspected the following code snippet, but still failed to do the trick:

I think it should be possible by creating an instance of webbrowser.Mozilla using the specific path as a name, giving that instance a custom value for remote_args to include the profile data, and then using its .open method. The --new-window functionality is already provided by webbrowser; you just pass new=1 to the open call.

But really, webbrowser isn’t designed for this kind of flexibility. It’s designed to make a simple task easy. For the amount of work it would take to make this work (and test it), it would probably be easier to just use subprocess directly.

1 Like

Wonderful tip. Below is my implementation based on the above idea and it works smoothly:

In [16]: import os
    ...: import webbrowser
    ...: 
    ...: class CustomMozilla(webbrowser.Mozilla):
    ...:     def __init__(self, name="firefox"):
    ...:         # 展开 ~
    ...:         name = os.path.expanduser(name)
    ...:         super().__init__(name)
    ...:         # 自定义 remote_args 并展开 ~
    ...:         profile_path = os.path.expanduser("~/.mozilla/firefox")
    ...:         # 可以进一步添加 args。你可以在 self.remote_args 中添加更多参数,只需按照现有的格式添加新的参数即可。例如,如果你想添加一个新的参数 --new-arg,可以这
    ...: 样做:
    ...:         # self.remote_args = ["--profile", profile_path, "--no-remote", "--new-arg", "%action", "%s"]
    ...:         self.remote_args = ["--profile", profile_path, "--no-remote", "--new-window", "%action", "%s"]
    ...: 
    ...:     def open(self, url, new=0, autoraise=True):
    ...:         if new == 0:
    ...:             action = self.remote_action
    ...:         elif new == 1:
    ...:             action = self.remote_action_newwin
    ...:         elif new == 2:
    ...:             action = self.remote_action_newtab
    ...:         else:
    ...:             raise webbrowser.Error(f"Bad 'new' parameter to open(); expected 0, 1, or 2, got {new}")
    ...: 
    ...:         args = [arg.replace("%s", url).replace("%action", action) for arg in self.remote_args]
    ...:         args = [arg for arg in args if arg]  # 移除空字符串
    ...:         success = self._invoke(args, True, autoraise, url)
    ...: 
    ...:         if not success:
    ...:             # 如果远程调用失败,尝试直接调用
    ...:             args = [arg.replace("%s", url) for arg in self.args]
    ...:             return self._invoke(args, False, False)
    ...:         else:
    ...:             return True
    ...: 
    ...: # 注册自定义的浏览器,使用指定的可执行文件路径
    ...: webbrowser.register("custom_mozilla", None, CustomMozilla("~/Public/browser/mozilla/firefox/firefox"))
    ...: 
    ...: # 打开 URL 使用自定义的浏览器
    ...: webbrowser.get("custom_mozilla").open("https://www.baidu.com", new=1)
Out[16]: True

1: Have a look at the code below that, the get() function. It looks
like you can pass a shell comand with a %s placeholder for the URL. So
maybe just:

 my_browser = webbrowser.get('firefox --profile BLAH %s')

would get you off the ground. It using shelx.split on the string, so
you can quote arguments.

2: If you use webbrowser.register you can either supply a callable
(there named klass, but it could as easily be a factory function) to
produce a brwoser (a thing with a .open(url) method), or
additionally supply an existing instances of such a thing as the
instance parameter.

Wonderful tips and comments. Below are the corresponding implementations:

In [23]: import webbrowser
    ...: import os
    ...: 
    ...: # 展开路径
    ...: firefox_path = os.path.expanduser("~/Public/browser/mozilla/firefox/firefox")
    ...: profile_path = os.path.expanduser("~/.mozilla/firefox")
    ...: 
    ...: # 构建命令字符串,包含展开的路径
    ...: command = f'{firefox_path} --profile {profile_path} --no-remote --new-window %s'
    ...: 
    ...: # 获取浏览器实例
    ...: my_browser = webbrowser.get(command)
    ...: 
    ...: # 打开 URL
    ...: my_browser.open("https://www.baidu.com", new=1)
Out[23]: True
In [18]: import webbrowser
    ...: import os
    ...: 
    ...: # 自定义浏览器类
    ...: class CustomMozilla(webbrowser.BaseBrowser):
    ...:     def __init__(self, name="firefox"):
    ...:         self.name = os.path.expanduser(name)
    ...:         self.profile_path = os.path.expanduser("~/.mozilla/firefox")
    ...: 
    ...:     def open(self, url, new=0, autoraise=True):
    ...:         if new == 1:
    ...:             action = "--new-window"
    ...:         elif new == 2:
    ...:             action = "--new-tab"
    ...:         else:
    ...:             action = ""
    ...: 
    ...:         # 构建命令
    ...:         cmd = f'{self.name} --profile {self.profile_path} --no-remote --new-window {action} {url}'
    ...:         return os.system(cmd) == 0
    ...: 
    ...: # 注册自定义浏览器实例
    ...: webbrowser.register("custom_mozilla", None, CustomMozilla("~/Public/browser/mozilla/firefox/firefox"))
    ...: 
    ...: # 获取并使用自定义浏览器
    ...: my_browser = webbrowser.get("custom_mozilla")
    ...: my_browser.open("https://www.baidu.com", new=1)
Out[18]: True
1 Like

This bit is subject to injection attacks. Please use subprocess.run([args,...], check=True)
instead of os.system(), for example:

 subprocess.run([
     self.name,
     '--profile', self.profile_path,
     '--no-remote',
     '--new-window', str(action),
     url,
 ], check=True)

Otherwise a hostile URL or a careless name, profile_path or action might
lead to an incorrect (i.e. subverted) command invocation because the
shell will be parsing whatever punctuation may occur in these values.

So, use the following:

import os
import subprocess
import webbrowser

class CustomMozilla(webbrowser.Mozilla):
    def __init__(self, name="firefox"):
        # 展开路径
        self.name = os.path.expanduser(name)
        self.profile_path = os.path.expanduser("~/.mozilla/firefox")
        super().__init__(self.name)

    def open(self, url, new=0, autoraise=True):
        # 根据 new 参数确定 action
        if new == 0:
            action = ""
        elif new == 1:
            action = "--new-window"
        elif new == 2:
            action = "--new-tab"
        else:
            raise webbrowser.Error(f"Bad 'new' parameter to open(); expected 0, 1, or 2, got {new}")

        # 构建命令
        cmd = [
            self.name,
            '--profile', self.profile_path,
            '--no-remote',
            action,
            url,
        ]

        # 移除空字符串
        cmd = [arg for arg in cmd if arg]

        try:
            subprocess.run(cmd, check=True)
            return True
        except subprocess.CalledProcessError:
            return False

# 注册自定义的浏览器实例
webbrowser.register("custom_mozilla", None, CustomMozilla("~/Public/browser/mozilla/firefox/firefox"))

# 获取并使用自定义浏览器
my_browser = webbrowser.get("custom_mozilla")
my_browser.open("https://www.baidu.com", new=1)

In summary, considering various possibilities and security factors, using only subprocess is enough:

import os
import subprocess

# 展开路径
firefox_path = os.path.expanduser("~/Public/browser/mozilla/firefox/firefox")
profile_path = os.path.expanduser("~/.mozilla/firefox")

def open_browser(url, new=0):
    # 根据 new 参数确定 action
    if new == 1:
        action = "--new-window"
    elif new == 2:
        action = "--new-tab"
    else:
        action = ""

    # 构建命令
    cmd = [
        firefox_path,
        '--profile', profile_path,
        '--no-remote',
        action,
        url,
    ]

    # 移除空字符串
    cmd = [arg for arg in cmd if arg]

    try:
        subprocess.run(cmd, check=True)
        print(f"Opened {url} successfully.")
    except subprocess.CalledProcessError as e:
        print(f"Failed to open {url}: {e}")

# 打开 URL 使用自定义的命令
open_browser("https://www.baidu.com", new=1)

You comment above “remove the empty string”. But why? I normally would
not do this. For a general command, passing an empty string may be a
meaningful thing. For your target command, perhaps less so, but I’d
rather the command itself complained than to enforce some arbitrary
policy here.

It’s just a matter of personal habit and maybe your idea is better.

To me this is what I term “policy”: decisions in code which constrain
what’s possible for reasons external to the code. I try to put as little
as possible in “library” code, utility code which anyone might want to
call. I like library code to be purely mechanism - what’s needed to make
things work. It might have defaults which cause it work a particular
way, but if those are choices about how things work, they can usually be
overridden - the defaults exist to produce reasonable behaviour in the
absence of detailed stuff from the user.

But also, consider what your code would do if, say, the user specified
an empty profile_path. It might be insane, but if they did and your
discard-empty-strings code ran the command would look like:

 browser_name --profile --no-remote action url

That effectively tells the browser that the profile path is
--no-remote, because that option expects a following argument. And
that is nonsense.

If you’ve got a policy to reject an empty profile_path, that belongs
in the __init__ method, raising a ValueError, and you wouldn’t need
or want this discard-empty-strings code.

1 Like