Módulo 2 · Lección 10 de 28
Opciones y banderas
Argumentos con guion que se pueden omitir: nombres cortos y largos, valores por defecto, interruptores, contadores y listas.
Los argumentos posicionales de la lección anterior son obligatorios y van en un orden fijo. Eso está bien para lo esencial de un programa, el fichero que hay que procesar o la cantidad que hay que convertir, pero no para todo lo demás: si quieres elegir el formato, cambiar la carpeta de destino o pedir que hable más, no puedes obligar a escribirlo todo siempre y en el mismo orden.
Para eso están las opciones, los argumentos que empiezan por guion. Se reconocen por su nombre y no por su sitio, se pueden poner en cualquier posición de la línea y, salvo que digas lo contrario, se pueden omitir. Es la diferencia entre cp origen destino, donde el orden lo es todo, y cp -r --verbose origen destino, donde -r y --verbose van donde quieras.
Posicional u opción: lo decide el guion#
La regla es literalmente esa. Si el nombre que le pasas a add_argument empieza por guion, es una opción; si no, es un posicional.
"""Exporta un fichero a otro formato."""
import argparse
analizador = argparse.ArgumentParser(description="Exporta un fichero a otro formato.")
analizador.add_argument("entrada", help="fichero que hay que exportar")
analizador.add_argument("-s", "--salida", help="fichero donde escribir el resultado")
argumentos = analizador.parse_args()
print(f"entrada = {argumentos.entrada!r}")
print(f"salida = {argumentos.salida!r}")La ayuda ya distingue los dos apartados:
python3 exportar.py -husage: exportar.py [-h] [-s SALIDA] entrada
Exporta un fichero a otro formato.
positional arguments:
entrada fichero que hay que exportar
options:
-h, --help show this help message and exit
-s, --salida SALIDA fichero donde escribir el resultadoFíjate en la línea usage:: [-s SALIDA] entre corchetes, entrada sin ellos. Los corchetes son la forma universal de decir "esto se puede omitir".
Y si se omite, el atributo existe igualmente y vale None:
python3 exportar.py datos.csventrada = 'datos.csv'
salida = NoneNombre corto y nombre largo#
A add_argument le puedes dar varias formas de la misma opción. La costumbre es dos:
- La corta, un solo guion y una sola letra:
-s. Para escribir rápido a diario. - La larga, dos guiones y una palabra:
--salida. Para que se entienda al leerla en un script seis meses después.
Las cuatro invocaciones siguientes son idénticas para argparse:
python3 exportar.py datos.csv --salida informe.json
python3 exportar.py datos.csv -s informe.json
python3 exportar.py --salida informe.json datos.csv
python3 exportar.py datos.csv --salida=informe.jsonentrada = 'datos.csv'
salida = 'informe.json'
entrada = 'datos.csv'
salida = 'informe.json'
entrada = 'datos.csv'
salida = 'informe.json'
entrada = 'datos.csv'
salida = 'informe.json'El valor puede ir separado por un espacio o pegado con un = en la forma larga. Con la forma corta también se puede pegar directamente, -sinforme.json, aunque eso se lee fatal y no conviene usarlo.
Lo que no puede faltar es el valor: una opción declarada así exige uno detrás.
python3 exportar.py -susage: exportar.py [-h] [-s SALIDA] entrada
exportar.py: error: argument -s/--salida: expected one argumentCómo se llama el atributo#
argparse deduce el nombre del atributo de la forma larga, o de la corta si no hay larga, quitando los guiones del principio. Y hay una conversión que sorprende la primera vez: los guiones interiores se cambian por guiones bajos, porque un guion no puede formar parte de un nombre de variable en Python.
import argparse
analizador = argparse.ArgumentParser(description="Cómo se nombran los atributos.")
analizador.add_argument("--nombre-fichero", help="con guion en medio")
analizador.add_argument("-n", "--numero-de-lineas", help="corta y larga")
analizador.add_argument("--dir", dest="directorio", help="nombre del atributo cambiado")
argumentos = analizador.parse_args()
print(vars(argumentos))
print(argumentos.nombre_fichero)
print(argumentos.numero_de_lineas)
print(argumentos.directorio)python3 nombres.py --nombre-fichero informe.md -n 20 --dir /tmp{'nombre_fichero': 'informe.md', 'numero_de_lineas': '20', 'directorio': '/tmp'}
informe.md
20
/tmpEn la línea de comandos se escribe --nombre-fichero, con guion, porque es lo que espera quien usa herramientas de terminal. En el código se lee argumentos.nombre_fichero, con guion bajo, porque es lo que exige Python. La traducción la hace argparse y no tienes que pensar en ella, solo recordarla.
El tercer argumento enseña el parámetro dest, que rompe esa deducción y fija el nombre del atributo a mano. Se usa en dos situaciones:
- Cuando la opción es corta o abreviada, como
--dir, y quieres un nombre legible dentro del código. - Cuando el nombre deducido chocaría con una palabra reservada de Python.
--classdaríaargumentos.class, que es sintaxis inválida; condest="clase"se arregla.
Valores por defecto#
Un None es incómodo: obliga a comprobarlo antes de usar el valor. Casi siempre es mejor dar un valor sensato con default:
analizador.add_argument("-p", "--patron", default="*",
help="patrón de nombre que deben cumplir los ficheros")Con eso, argumentos.patron vale "*" cuando nadie escribe la opción, y el resto del programa no tiene que preguntarse nada. La regla práctica: usa default siempre que exista un comportamiento razonable por omisión, y deja None solo cuando "no lo han dicho" sea una situación de verdad distinta de cualquier valor posible.
Banderas: opciones sin valor#
Una bandera es una opción que no lleva valor detrás. Está o no está, y eso es todo. Se declaran con action:
import argparse
analizador = argparse.ArgumentParser(description="Sincroniza una carpeta.")
analizador.add_argument("--forzar", action="store_true", help="sobrescribir sin preguntar")
analizador.add_argument("--sin-copia", action="store_false", dest="copia",
help="no crear copia de seguridad antes de escribir")
argumentos = analizador.parse_args()
print(f"forzar = {argumentos.forzar}")
print(f"copia = {argumentos.copia}")python3 respaldo.pyforzar = False
copia = Truepython3 respaldo.py --forzar --sin-copiaforzar = True
copia = FalseLas dos acciones son simétricas:
store_true: el valor por defecto esFalsey escribir la opción lo pone aTrue. Es el caso normal, para activar algo que de serie está apagado.store_false: el valor por defecto esTruey escribir la opción lo pone aFalse. Sirve para apagar algo que de serie está encendido.
Con store_false casi siempre querrás un dest. Sin él, la opción --sin-copia produciría el atributo argumentos.sin_copia, y tendrías que escribir if not argumentos.sin_copia:, una doble negación que se lee mal. Con dest="copia" el código queda if argumentos.copia:, que dice lo que hace.
Contar repeticiones con action="count"#
Casi todas las herramientas de terminal usan -v para hablar más y -vvv para hablar mucho más. Eso se consigue con count, que suma uno por cada vez que aparece la opción:
import argparse
NIVELES = ["solo errores", "normal", "detallado", "depuración"]
analizador = argparse.ArgumentParser(description="Niveles de detalle con -v.")
analizador.add_argument("-v", "--verboso", action="count", default=0,
help="sube el nivel de detalle, se puede repetir")
argumentos = analizador.parse_args()
nivel = min(argumentos.verboso, len(NIVELES) - 1)
print(f"verboso={argumentos.verboso}, salida {NIVELES[nivel]}")python3 detalle.py
python3 detalle.py -v
python3 detalle.py -vv
python3 detalle.py -vvv
python3 detalle.py -vvvvvverboso=0, salida solo errores
verboso=1, salida normal
verboso=2, salida detallado
verboso=3, salida depuración
verboso=5, salida depuraciónDos detalles que importan:
default=0no es opcional en la práctica. Sin él, el valor por defecto esNone, yNone >= 1lanza unTypeError. Concountpon siempredefault=0.- El
minque recorta el nivel. Nada impide escribir-vvvvvvv, así que el programa tiene que aguantarlo sin salirse del índice de la lista. Es un descuido muy fácil de cometer.
Fíjate también en que -vvv es un solo argumento en la terminal. Las opciones cortas de una letra se pueden apelotonar, y argparse las desmonta: -vvv equivale a -v -v -v, y -rv equivale a -r -v.
Acumular valores con action="append"#
Cuando una opción tiene sentido varias veces, append guarda todas las apariciones en una lista en vez de quedarse con la última:
import argparse
analizador = argparse.ArgumentParser(description="Acumula exclusiones.")
analizador.add_argument("-e", "--excluir", action="append", default=[],
help="texto que descarta un fichero, se puede repetir")
argumentos = analizador.parse_args()
print(argumentos.excluir)python3 filtrar.py -e "*.tmp" -e "*.log"['*.tmp', '*.log']Sin append, la segunda aparición pisaría a la primera y solo quedaría '*.log'. Y si no se escribe ninguna vez, el atributo es la lista vacía, que se puede recorrer sin comprobar nada:
python3 filtrar.py[]Las dos caras de una bandera con BooleanOptionalAction#
store_true sirve para encender algo apagado, y store_false para apagar algo encendido, pero ninguna de las dos permite las dos cosas a la vez. Cuando el valor por defecto depende de la situación y quien usa el programa necesita poder forzarlo en cualquier dirección, la solución es argparse.BooleanOptionalAction, que crea las dos opciones de golpe:
import argparse
analizador = argparse.ArgumentParser(description="Bandera de dos caras.")
analizador.add_argument("--color", action=argparse.BooleanOptionalAction, default=True,
help="colorear la salida")
argumentos = analizador.parse_args()
print(argumentos.color)python3 color.py -husage: color.py [-h] [--color | --no-color]
Bandera de dos caras.
options:
-h, --help show this help message and exit
--color, --no-color colorear la salidaLa ayuda las presenta como alternativas con la barra vertical, y funcionan las tres invocaciones:
python3 color.py
python3 color.py --color
python3 color.py --no-colorTrue
True
FalseEs exactamente el caso del color en la terminal que viste en la lección de colores y estilos: lo razonable es colorear cuando se está mirando la pantalla y no colorear cuando la salida va a un fichero, así que el programa lo decide solo. Pero quien lo use tiene que poder imponer su criterio en las dos direcciones, y para eso hacen falta las dos opciones.
Ten en cuenta que BooleanOptionalAction se pasa como objeto, sin comillas, al contrario que "store_true" y las demás, que son cadenas. Es un despiste habitual.
Opciones obligatorias con required=True#
Se puede obligar a escribir una opción:
import argparse
analizador = argparse.ArgumentParser(description="Una opción obligatoria.")
analizador.add_argument("--destino", required=True, help="carpeta donde dejar el resultado")
argumentos = analizador.parse_args()
print(argumentos.destino)python3 obligatoria.pyusage: obligatoria.py [-h] --destino DESTINO
obligatoria.py: error: the following arguments are required: --destinoFíjate en que en la línea usage: el --destino DESTINO sale sin corchetes, porque ya no es opcional.
Todas las acciones de un vistazo#
action | Qué hace | Valor por defecto recomendado |
|---|---|---|
"store" | Guarda el valor. Es lo que ocurre si no pones action | None o el que tenga sentido |
"store_true" | True si aparece, False si no | Lo pone solo |
"store_false" | False si aparece, True si no | Lo pone solo |
"count" | Cuenta las veces que aparece | 0, siempre |
"append" | Acumula cada aparición en una lista | [] |
"store_const" | Guarda el valor fijo de const si aparece | El que tenga sentido |
"append_const" | Añade el valor fijo de const a una lista | [] |
argparse.BooleanOptionalAction | Crea --x y --no-x | True o False, explícito |
"version" | Imprime la versión y termina | No aplica |
"help" | Imprime la ayuda y termina. Es lo que hace -h | No aplica |
Un programa con todo junto#
Este listador de ficheros usa un posicional y cinco opciones de tipos distintos:
"""Lista los ficheros de una carpeta con unas cuantas opciones."""
import argparse
from pathlib import Path
def construir_analizador() -> argparse.ArgumentParser:
analizador = argparse.ArgumentParser(description="Lista los ficheros de una carpeta.")
analizador.add_argument("carpeta", help="carpeta que hay que listar")
analizador.add_argument("-p", "--patron", default="*",
help="patrón de nombre que deben cumplir los ficheros")
analizador.add_argument("-e", "--excluir", action="append", default=[],
help="texto que descarta un fichero, se puede repetir")
analizador.add_argument("-r", "--recursivo", action="store_true",
help="entrar también en las subcarpetas")
analizador.add_argument("--ocultos", action=argparse.BooleanOptionalAction, default=False,
help="incluir los ficheros que empiezan por punto")
analizador.add_argument("-v", "--verboso", action="count", default=0,
help="explica lo que hace, repetible para más detalle")
return analizador
def main() -> None:
argumentos = construir_analizador().parse_args()
carpeta = Path(argumentos.carpeta)
if argumentos.verboso >= 1:
print(f"Buscando '{argumentos.patron}' en {carpeta}")
if argumentos.verboso >= 2:
print(f" recursivo={argumentos.recursivo} ocultos={argumentos.ocultos}")
print(f" exclusiones={argumentos.excluir}")
patron = f"**/{argumentos.patron}" if argumentos.recursivo else argumentos.patron
encontrados = 0
for ruta in sorted(carpeta.glob(patron)):
if not ruta.is_file():
continue
if not argumentos.ocultos and ruta.name.startswith("."):
continue
if any(texto in ruta.name for texto in argumentos.excluir):
if argumentos.verboso >= 2:
print(f" descartado {ruta.name}")
continue
print(ruta)
encontrados += 1
if argumentos.verboso >= 1:
print(f"Total: {encontrados} ficheros")
if __name__ == "__main__":
main()La ayuda que produce, sin escribir ni una línea de texto de uso:
python3 listar.py -husage: listar.py [-h] [-p PATRON] [-e EXCLUIR] [-r] [--ocultos | --no-ocultos]
[-v]
carpeta
Lista los ficheros de una carpeta.
positional arguments:
carpeta carpeta que hay que listar
options:
-h, --help show this help message and exit
-p, --patron PATRON patrón de nombre que deben cumplir los ficheros
-e, --excluir EXCLUIR
texto que descarta un fichero, se puede repetir
-r, --recursivo entrar también en las subcarpetas
--ocultos, --no-ocultos
incluir los ficheros que empiezan por punto
-v, --verboso explica lo que hace, repetible para más detalleSobre una carpeta proyecto/ que contiene notas.md, informe.md, datos.csv, borrador.tmp.md, un .oculto.md y una subcarpeta informes/ con anual.md:
python3 listar.py proyectoproyecto/borrador.tmp.md
proyecto/datos.csv
proyecto/informe.md
proyecto/notas.mdpython3 listar.py proyecto -p "*.md" --ocultos -e tmpproyecto/.oculto.md
proyecto/informe.md
proyecto/notas.mdpython3 listar.py proyecto -p "*.md" -r -vBuscando '*.md' en proyecto
proyecto/borrador.tmp.md
proyecto/informe.md
proyecto/informes/anual.md
proyecto/notas.md
Total: 4 ficherospython3 listar.py proyecto -p "*.md" -e tmp -vvBuscando '*.md' en proyecto
recursivo=False ocultos=False
exclusiones=['tmp']
descartado borrador.tmp.md
proyecto/informe.md
proyecto/notas.md
Total: 2 ficheros