Cómo construí una herramienta de línea de comandos de Python para mejorar la usabilidad de mi navegador

Cómo construí una herramienta de línea de comandos de Python para mejorar la usabilidad de mi navegador - Codelivly

 

Los programas de línea de comandos son poderosos porque pueden acceder y controlar la mayoría de las cosas en su máquina. Por lo tanto, permiten a los usuarios realizar tareas rápidamente y automatizar procesos con solo unos pocos comandos breves.

En este artículo, explicaré cómo construí mi herramienta de línea de comandos de Python y cómo mejoró mi experiencia de navegación. También te explicaré cómo puedes crear tu propia herramienta para mejorar tu experiencia de navegación.

La idea se inspiró en el artículo. Usando ChatGPT para hacer que Bash sea aceptable donde bash se usó para cerrar pestañas y abrir URL guardadas desde archivos. En este artículo se agregarán más características y se usará Python (aunque combinado con subcomandos de bash).

Uno de los beneficios de desarrollar una CLI para un programa como este en lugar de crear una extensión de Chrome, por ejemplo, es que se puede integrar perfectamente con otros comandos.

Por ejemplo, imagine que está ejecutando un proceso tedioso en la terminal y desea saber cuándo finaliza. Entonces puedes ejecutar lo siguiente:

my_long_running_process ; browsertool open_message "It's finished"

EL ; entre comandos significa que el segundo comando se ejecutará cuando se complete el primer comando:

https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FM5TrxOrZ25g%3Ffeature%3Doembed&display_name=YouTube&url=https%3A%2F%2Fwww.youtube. com%2Fwatch%3Fv%3DM5TrxOrZ25g&key=a19fcc184b9711e1b4764040d3dc5c07&type=text%2Fhtml&schema=youtube

En el video de ejemplo, utilicé el comando de suspensión para simular la ejecución de un proceso antes de abrir el mensaje, pero el comando de suspensión también puede ser útil en un escenario del mundo real.

Por ejemplo, supongamos que me gustaría relajarme durante una hora y ver un poco de YouTube, pero una vez que termine esa hora, me gustaría cerrar todas las pestañas y abrir mis pestañas de trabajo. Esto se puede hacer fácilmente con los siguientes comandos encadenados:

sleep 3600 ; browsertool clear ; browsertool open_tabs work

Al combinar diferentes programas de línea de comandos, se pueden crear herramientas poderosas.

En el resto del artículo, explicaré cómo construí la herramienta. Lo dividí en dos partes. Comienzo describiendo la funcionalidad y luego describiré cómo está estructurado el programa para facilitar una CLI y cómo extenderla.

¡Empecemos!

Comienzo importando los paquetes y configurando algunas variables constantes:

#!/usr/bin/env python3
from argparse import ArgumentParser
import os
import re
import subprocess
from typing import Callable, List
from dataclasses import dataclass
from pathlib import Path
import os

# specify where groups should be stored
SCRIPT_PATH = Path(__file__).parent.resolve()
GROUP_PATH = f"{SCRIPT_PATH}/groups"


La primera línea es un shebang para que el sistema operativo sepa que se trata de un script de Python cuando se ejecuta como CLI. EL GROUP_PATH La variable especifica dónde se almacenarán los archivos que contienen grupos de URL que se pueden guardar y abrir juntos.

Ahora definiré un método para ejecutar AppleScripts desde Python:

# will run the apple script and return the output
def run_apple_script(script: str) -> str:
return subprocess.check_output(f"osascript -e '{script}'", shell=True).decode("utf-8")

El método ejecutará el script en un subcomando y luego devolverá el resultado a Python como una cadena. Estoy usando anotaciones de tipo aquí, es decir:

  • script: str significa el script el argumento debe ser del tipo str
  • -> str significa que el método devolverá una cadena

Los usaré a lo largo del programa. No son necesarios y no cambian el comportamiento del tiempo de ejecución de Python, pero pueden ser útiles para documentar funciones y ayudar a su IDE u otras herramientas a detectar errores.

No sabía casi nada sobre AppleScripts antes de comenzar a crear este programa, pero descubrí que sería una herramienta adecuada para interactuar con las pestañas del navegador en Mac. Usé principalmente ChatGPT para armar estos scripts, ya que no se encontró mucho en Google. Por esta razón, no voy a entrar en detalles sobre ellos.

A continuación, definamos algunos métodos que interactúan con el navegador.

Índice
  1. Guardar pestañas
  2. Pestañas abiertas
  3. Lista de pestañas guardadas
  4. Eliminar pestañas guardadas
  5. Cerrar pestañas
  6. abrir un mensaje

Guardar pestañas

Esta función guardará todas las pestañas abiertas actualmente en un archivo con un nombre específico:

def get_group_filepath(name: str) -> str:
return f"{GROUP_PATH}/{name}.txt"

# remove duplicates while preserving order
def remove_duplicates(arr: List[str]) -> List[str]:
added = set()
new_arr = []
for e in arr:
if e in added:
continue
new_arr.append(e)
added.add(e)
return new_arr

# save the urls of the currently open tabs to a group
def save_tabs(name: str, replace: bool=False) -> None:
# get all open tabs
tabs = get_tabs()
urls = [tab["url"].strip() for tab in tabs]
urls = [u for u in urls if u != ""]
# get filename to store in
filename = get_group_filepath(name)
# create if not exists
Path(filename).touch(exist_ok=True)
with open(filename, "r+") as f:
# if replace=False, concatenate the new urls with the old ones
# but make sure no duplicates are added
if not replace:
old_urls = f.read().strip().split("\n")
urls = old_urls + urls
urls = remove_duplicates(urls)
# replace content
f.seek(0)
f.write("\n".join(urls).strip())
f.truncate()


La función extrae las URL de todas las pestañas abiertas con get_tabs (que se muestra más adelante) luego los agrega a un archivo con una URL por línea. Si el archivo no existe o la anulación se establece en False, el archivo contendrá solo las URL abiertas actualmente; de ​​lo contrario, se concatenará con las URL antiguas en el archivo.

EL get_tabs() se usará varias veces en el programa y usa un AppleScript:

# will returns all open tabs
def get_tabs() -> List[dict]:
# a suffix is added to simplify
# splitting the output of the apple script
suffix = "<$suffix$>"

# escape { and } in f-string using {{ and }}
tabs = run_apple_script(f"""
set tabList to {{}}
tell application "Google Chrome"
repeat with w in windows
repeat with t in tabs of w
set end of tabList to {{id: id of t, URL: (URL of t) & "{suffix}"}}
end repeat
end repeat
end tell
return tabList
""").strip()

# remove the suffix at the last URL
tabs = tabs[:-len(suffix)]

def tab_to_dict(x: str) -> dict:
# x = "id: ..., URL: ..."
tab = {}
id, url = x.replace("id:", "").split(", URL:")
tab["id"] = id.strip()
tab["url"] = url.strip()
return tab

# can now split using the suffix + ","
tabs = [tab_to_dict

Esta función es un poco confusa. Primero, se ejecuta un AppleScript para abrir todas las pestañas en Chrome. La parte complicada fue que inicialmente la cadena devuelta tenía el siguiente formato:

 

id: ..., URL: ..., id: ..., URL: ..., etc.

Esto significa que si la URL contiene una coma, será problemático si .split(",") es utilizado.

Por esta razón, agrego un sufijo al final de la URL, lo que me permite dividir con ese sufijo para obtener tanto la identificación como la URL en cada división. Luego simplemente extraiga los valores y devuélvalos como una lista de diccionarios.

Pestañas abiertas

Como guardamos las URL en un archivo, podemos leerlas fácilmente y luego abrirlas en el navegador usando AppleScript:

# open the urls in the tab group    
def open_tabs(name: str) -> None:
filename = get_group_filepath(name)
if Path(filename).exists():
with open(filename, "r") as f:
urls = f.read().split("\n")
to_open = "\n".join([f'open location "{url}"' for url in urls])
run_apple_script(f"""
tell application "Google Chrome"
activate
make new window
{to_open}
end tell
""")
else:
raise ValueError("Group does not exist.")

Lista de pestañas guardadas

Una vez que las pestañas se guardan en archivos, es fácil enumerarlas desde la carpeta a la que se agregaron:

# return a list with all tab groups
def get_tab_groups() -> List[str]:
return [f.replace(".txt", "") for f in os.listdir(GROUP_PATH) if ".txt" in f]

def list_tab_groups() -> None:
print("\n- ".join(["Saved tab groups", *get_tab_groups()]))


Eliminar pestañas guardadas

Simplemente elimine el archivo:

def group_delete(name: str) -> None:
os.remove(get_group_filepath(name))

Cerrar pestañas

Este método cerrará las pestañas si sus URL coinciden con una expresión regular determinada. Entonces puede escribir algo como "stackoverflow | google" para cerrar todas las pestañas con stackoverflow o google en sus URL.

# will close the tabs with the given ids
def close_ids(ids: List[str]) -> None:
ids = ",".join(ids)
run_apple_script(f"""
set ids to {{{ids}}}
tell application "Google Chrome"
repeat with w in windows
repeat with t in (get tabs of w)
if (get id of t) is in the ids then
close t
end if
end repeat
end repeat
end tell
""")

# will close all tabs that match the given regex
def close_tabs(regex: str) -> None:
tabs = get_tabs()
remove = []
for t in tabs:
if re.search(re.compile(regex), t["url"]):
remove.append(t["id"])
close_ids(remove)


EL close_tabs El método devuelve todas las pestañas abiertas, verifica si la expresión regular coincide con las URL y, de ser así, agrega sus ID a una lista. Entonces esta lista se le da a la close_ids método que cierra estas pestañas.

abrir un mensaje

Este método mostrará un mensaje en una nueva pestaña:

# open a message in a new tab
def open_message(message: str) -> None:
# format the message to be displayed
html="<h1 style="font-size: 50px; position: absolute; top: 30%; left: 50%; transform: translate(-50%, -50%); text-align: center;">"
html += message
html += "</h1>"
# escape " and '
html = html.replace('"', '\\"').replace("'", "\'\\'\'")

# show it with AppleScript
run_apple_script(f"""
set theHTML to "{html}"
set theBase64 to do shell script "echo " & quoted form of theHTML & " | base64"
set theURL to "data:text/html;base64," & theBase64
tell application "Google Chrome"
activate
if (count of windows) = 0 then
make new window
open location theURL
else
tell front window
make new tab
set URL of active tab to theURL
end tell
end if
end tell
""")


El mensaje está encerrado en una etiqueta h1 con algo de estilo, escapado y luego se muestra configurando una nueva pestaña en la versión base64 del mismo.

Ahora describiré cómo está estructurado el programa para que se puedan agregar e integrar fácilmente nuevas funciones en la CLI. Primero, defino una clase de datos llamado existencias. Cada instanciación de una acción definirá la funcionalidad en el programa, por lo que si desea ampliar el programa, lo que deberá agregar es un objeto de acción.

# class to represent an action to be taken
@dataclass(frozen=True)
class Action:
name: str
arguments: List[Arg]
# ... = taking any number of arguments
method: Callable[..., None]
description: str

En este programa, solo se usa para definir sucintamente una clase, no mucho más. La clase tiene:

  • un nombre, que se usará para hacer referencia a él en la línea de comando
  • una serie de argumentos de tipo Arg (que se muestra a continuación), que define los argumentos que necesita y cómo se establecerán en la línea de comando
  • el metodo para llamar
  • una descripción para mostrar al usar--help

EL Arg se define a continuación:

# class to represent an argument for an action
@dataclass(frozen=True)
class Arg:
name: str
flag: bool = False

Tiene dos atributos:

  • nombre: corresponde a un parámetro en el método de la acción a la que está conectado
  • bandera: si es una bandera opcional O argumento posicional

Si no está familiarizado con los términos "marca" u "opción", le recomiendo que consulte este artículo introductorio sobre la creación de CLI en Python. Estos dos atributos no definen todas las formas posibles de establecer argumentos de línea de comandos de ninguna manera, pero he limitado el programa de esta manera por simplicidad. Más adelante le mostraré cómo se usarán para construir la CLI.

Luego, las acciones se definen en una lista y se utilizarán para formalizar la funcionalidad que describí en la parte anterior del artículo:

actions = [
Action(
name="save_tabs",
arguments=[Arg("name"), Arg("replace", flag=True)], 
method=save_tabs, 
description="Save tabs to supplied name. If replace is set to true, it will replace the existing group, otherwise append."
),
Action(
name="list_saved_tabs",
arguments=[],
method=list_tab_groups,
description="List all saved tab groups"
),
Action(
name="open_tabs",
arguments=[Arg("name")], 
method=open_tabs, 
description="Select a tab group to open"
),
Action(
name="clear",
arguments=[], 
method=lambda: close_tabs(""), 
description="Clear/deletes all tabs"
),
Action(
name="open_message",
arguments=[Arg("message")],
method=open_message,
description="Open message"
),
Action(
name="close_tabs",
arguments=[Arg("regex")], 
method=close_tabs, 
description="Close tabs that match the supplied regex"
),  
Action(
name="delete_saved_tabs",
arguments=[Arg("name")], 
method=group_delete, 
description="Delete one of the tab groups"
)
]

Ahora se pueden usar acciones para construir la CLI. Cada acción tendrá un sub-analizador en el argparse módulo que habilita diferentes argumentos para diferentes acciones:

if __name__ == "__main__":
parser = ArgumentParser(
prog="Browser controller",
description="Perform actions on the browser"
)

# depending on what action is taken, different arguments will
# be available
subparsers = parser.add_subparsers(title="The action to take", required=True)

# add all actions and their respective arguments to argparse
for action in actions:
# add subcommand of name `action.name`
subparser = subparsers.add_parser(action.name, description=action.description)
for argument in action.arguments:
# if flag, add -- to the argument name
prefix = "--" if argument.flag else ""
subparser.add_argument(
prefix + argument.name,
# store_true means true if specified, false otherwise
# store means return argument value as string
action="store_true" if argument.flag else "store"
)
# set the method value to be the action method
subparser.set_defaults(method=action.method)

# turn args into dict
args = vars(parser.parse_args())

# separate method from arguments
# and then call method with arguments
method = args.pop("method")
method(**args)


Averigüemos qué sucedió aquí, paso a paso. Primero un ArgumentParser desde argparse se instancia con un título y una descripción. Posteriormente, para cada acción, se agrega un subanalizador usando:

subparsers.add_parser(action.name, ...)

Cada una de estas llamadas creará un subcomando que se puede activar con:

python3 main.py <action.name> ...

Los subcomandos son útiles porque permiten diferentes argumentos para diferentes situaciones o, en este caso, diferentes acciones. Los argumentos para cada subcomando y acción se definen usando action.arguments en bucle :

for argument in action.arguments:
# if flag, add -- to the argument name
prefix = "--" if argument.flag else ""
subparser.add_argument(
prefix + argument.name, 
# store_true means true if specified, false otherwise
# store means return argument value as string
action="store_true" if argument.flag else "store"
)

Para argumentos posicionales, tenemos que action="store", lo que significa que solo devolverá el valor proporcionado como una cadena. Para la bandera, un guión doble -- se agrega prefijo para hacerlo opcional, y action="store_true"lo que significa presencia = Verdadero, ausencia = Falso, es decir:

# myflag is specified => myflag=True
> somecommand --myflag
# no myflag is specified => myflag=False
> somecommand

Después del ciclo, el método se agrega de forma predeterminada para que sea accesible con los argumentos analizados:

# set the method value to be the action method
subparser.set_defaults(method=action.method)

Finalmente, se llama al método con los argumentos proporcionados:

# turn args into dict
args = vars(parser.parse_args())

# separate method from arguments
# and then call method with arguments
method = args.pop("method")
method(**args)


si llamas parser.parse_args() obtienes un espacio de nombres con los argumentos, y usando vars(...) lo convierte en un diccionario. Luego, los argumentos del método y el método en sí se separan usando .pop("method") que devuelve y elimina el método del dict. Entonces el resto de los valores en args (es decir, los argumentos) se pueden proporcionar al método como kwargs.

¡Y esa es la estructura del programa!

Si te ha gustado este artículo:

  • 🙏 Continuará Gorjeosi quieres leer mis próximos artículos, ¡nuevos cada semana!
  • 📚 Si buscas más contenido, consulta mis listas de lectura sobre inteligencia artificial, Python o ciencia de datos

Gracias por leer y que tengas un buen día.

Escrito originalmente por Jacob Ferus

 

Si quieres conocer otros artículos parecidos a Cómo construí una herramienta de línea de comandos de Python para mejorar la usabilidad de mi navegador puedes visitar la categoría Tutoriales.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Subir

Esta página web utiliza cookies para analizar de forma anónima y estadística el uso que haces de la web, mejorar los contenidos y tu experiencia de navegación. Para más información accede a la Política de Cookies . Ver mas