Boas xente,
Neste post vouvos amosar unha serie de truquiños que, pola miña experiencia, son cremita para aumentar a usabilidade das utilidades de consola. Vounos deixar aquí plasmados para non esquecelos e coa esperanza de que tamén lle poidan servir a máis xente.
Ao final a clave e facer os programas simples, o que non é tan fácil como parece, pero agardo que estes truquiños che axuden.
Eu polo xeral programo pequenas ferramentas/scripts en Linux, polo que estes consellos enfocanse nesa plataforma, pero deberían funcionar noutras.
Ademais, decir que moito ven inspirado polo libro The Art of Unix Programming, que che recomendo se che cunde a programación en Unix/Linux.
Vou a asumir un coñecemento básico de Linux. Se non sabes usar Linux, recoméndoche encarecidamente que aprendas a utilizalo.
Veña, ao choio.
Facer programas fáciles de instalar
Isto pode ser tedioso a primeira vez que se fai, xa que non necesariamente implica programar, senon preparar o programa para que se poida desplegar fácilmente. Sen embargo, cunde moito cando se precisa usar o programa.
O primeiro é facer que o programa esté dispoñible nun repositorio dunha forxa como Github ou Gitlab.
Unha vez a ferramenta está dispoñible para a descarga, debería ser fácil de instalar/compilar. É común que os programas se compilen e instalen facendo uso dun makefile. Por exemplo:
Esta práctica é común en programas en C, onde o comando make
se usa para
compilar e make install
para instalar. Inda que o teu programa non estea en C,
eu recomendoche crear un makefile para facer a instalación dun xeito estándar e
facilitarlle a vida a outras usuarias.
Por exemplo, no caso de ter un programa en Python, que non precisa ser
compilado, podes usar o comando make
para instalar as dependencias executando
pip install -r requirements.txt
, mentres que make install
pode instalalo no
sistema con pip install .
.
Pode ser que no teu caso o programa sexa soamente un script, entón abondaría con
que make
executase algo como cp ./myscript.py /usr/bin/myscript
(recorda
facer o teu scrip executable (chmod +x
) e engadirlle un shebang).
Nota: No caso de que queiras instalar o programa a nivel de usuaria (porque non
tes privilexios de root ou o que sexa), podes copialo en ~/.local/bin
(teste
que lembrar de engadir este directorio ao the PATH, e crealo de ser necesario).
Para programas en Python, eu recoméndoche crear un ficheiro setup.py
para
instalalo dun xeito sinxelo, incluso sen un makefile. Ou polo menos crear un
requirements.txt
para indicar as dependencias. Non indiques as dependencias no
readme, é moi molesto ter que instalalas manualmente.
De feito, a instalación debería ser tan sinxela como sexa posible. Se pensas que a túa ferramenta é útil dabondo, podes metela nun rexistro de paquetes para que sexa fácil de instalar por calquera.
Por exemplo, as ferramentas de Python pódense rexistrar en pypi para poder
instalalas con pip install mytool
. No caso de Rust, podes metela no rexistro
de crates, para instalala con cargo install mytool
. Ademais, podes crear un
paquete para unha distribución de Linux de xeito que se poida instalar con apt
ou yum
.
En resumo, incluso os pequenos programas son creados para usarse, e o primeiro paso é instalalos. Facer que sexa fácil, incluso se é tedioso, afórralle moitas dores de cabeza ás usuarias.
Parámetros
As opcións da liña de comandos ou parámetros son unha parte crucial de cada programa de consola. Para eles, gústame usar as seguintes regras:
Nota: Os argumentos son os valores que o usuario lle pasa aos parámetros.
Parámetros longos/curtos
Os parámetros deberían ter sempre un nome longo fácil de recordar, que empeza
por dous guións --
. Por exemplo, --help
, --out-file
, etc.
Ás veces e complicado elexir un nome. Por exemplo, debería escoller pass
,
password
or passwd
? Eu prefiro escoller o máis descriptivo, neste caso
password
e poñer o resto como alias.
Ademais, os parámetros máis usados deberían ter un nome curto, que debería ser
unha letra ou un número, precedido por un guión -
. O normal é usar letras en
minúscula. As letras maiúsculas utilizanse no caso de que a letra en minúscula
máis intuitiva esté ocupada. Por exemplo, curl usa -H
para indicar unha
cabeceira (header) HTTP xa que -h
é usado para a axuda (help).
As flags (opcións booleanas sen valor) poden usarse xuntas soamente cun un único
guión -
. Por exemplo, ls -lah
é o mesmo que ls -l -a -h
. Hai programas que
usan nomes curtos de 2 o máis letras, como -out
, pero isto impide o unir flags.
En entornos Windows soiase usar unha barra /
en vez dun guión. Pero
actualmente hai moitas ferramentas que usan guións, así que prefiro manter os
guións. Ademais, o uso de guións evita confusións con arguments que
sexan rutas de arquivos en estilo Unix, que poden empezar cunha barra.
Segue as convencións para os parámetros
Existen certos parámetros que teñen un uso común en tódolos programas e no se deberían usar para outra cousa (sen unha razón). Os máis importantes son os seguintes:
-
-h/--help
-> Amosa a axuda do programa. Debería ser implementado por tódolos programas. -
-v/--verbose
-> Usado para que o programa mostre unha saída máis detallada. Soe ser un parámetro que fai que o programa aumente o nivel de detalle canto máis se indique. Por exemplo,-v
para un detalle pequeno e-vvv
para un gran nivel de detalle. -
-V/--version
-> Amosa a versión do programa.
Ademais, as ferramentas con funcionamentos parecidos ou relacionados deberían usar os mesmo nome para os parámetros. É boa idea usar como nome de parámetro os usados por ferramentas coñecidas.
Por exemplo, se creas un programa que serve principalmente para facer peticións
HTTP, deberías usar parámetros similares aos usados por curl
ou wget
. Nese
caso poderías usar -H
para que o usuario poida especificar unha cabeceira
personalizada ou -A
para indicar o user agent, como curl
. Deste xeito eche
máis fácil escoller nome para os parámetros e reducir a curva de aprendizaxe das
usuarias.
Ten parámetros flexibles
Unha característica que atopo moi cómoda nos proogramas é que para un mesmo parámetro se acepten diferentes tipos de argumentos ou se deduza o tipo de argumento sen necesidade que ser especificado. Deixame amosarche un exemplo:
Por exemplo, imaxina un programa para facer forza bruta que acepta unha lista de
usuarios e contrasinais. Sería moi cómodo ter un parametro -u/--user
que
acepte tanto un nome de usuario como un ficheiro con nomes de usuario (e o mesmo
para os contrasinais). Deste xeito no tes que andar recordando 2 parámetros coma
-u/--user
e -U/--user-list
para diferentes argumentos, e podes facer algo
como o seguinte:
Quédaste co tema? Podes lanzar diferentes tipos de ataque sen ter que andando a recordar un montón de parámetros porque o programa comproba se lle estás a indicar un ficheiro ou non.
Isto a min paréceme tan útil, que incluso teño unha plantilla de python para coller a entrada do programa dun ficheiro, do argumento ou de stdin.
Outro exemplo é o programa tar
que descomprime un ficheiro sen
necesidade de que lle indiques o formato. Por exemplo podes executar tar -xf
file.tgz
ou tar -xf file.tar.xz
e xa comproba que formato é e descomprime o
ficheiro. Moi cómodo.
A clave é que o programa debería deducir o máximo de información posible dunha entrada mínima do usuario, aforrandolle o ter que introducir datos que moitas veces poden ser redundantes.
Pero ten en conta que a flexibilidade debe ser intuitiva, xa que procesar parámetros de maneiras extrañas pode confundir ao usuario e levar a comportamentos inesperados.
Usa unha libraría para manexar os parámetros
As linguaxes de programación soen ter alomenos unha libraría para o manexo de parámetros. Usaá. É máis fácil, máis rápido e máis limpo que parsear os parámetros por ti mesmo. Deixote exemplos de librarías que manexan parámetros:
Ás veces é necesario procesar os argumentos a man, pero sempre que poidas trata de manexalos dende a libraría. Estas soen traer opcións para moitas situacións:
-
Auto xeración de axuda co parámetro
-h/--help
-
Uso de flags (parámetros booleanos que non aceptan valor)
-
Parámetros que so aceptan un grupo de opcións
-
Parámetros que poden ser usados moitas veces (como verbose)
-
Parámetros que non poden ser usados xuntos (exclusivos)
-
Definir o tipo de valor dun argumento, se é un número, unha cadea de caracteres ou un ficheiro, etc
-
Hooks para engadir rutinas de procesado dos parámetros personalizadas (moi útiles)
Ficheiros de configuración
Os ficheiros de configuración son moi usados por programas, especialmente servidores (nginx, ssh) e daemons (cron), pero tamén clientes (git, proxychains) ou programas interactivos (i3, emacs).
No caso de ter que usar ficheiros de configuración, o seguir unhas pautas pode facilitarllela vida ás usuarias.
Usa ficheiros de configuración fácilmente editables
Unha das cousas a ter en conta é o formato de ficheiro de configuración. Existen varios formatos que foron deseñados para este propósito, como TOML, INI ou YAML, así que non fai falta que inventes un. Estes formatos son fáciles de entender e editar con calquera editor de texto e ademais moitos linguaxes teñen librarías para procesalos (ten coidado con YAML xa que pode levar á execución de código arbitrario).
Inda que existen ferramentas que usan formatos XML ou JSON para os ficheiros de configuración, por experiencia non os recomendo, xa que son máis liosos de editar e leer para un humano. Penso que o seu uso máis apropiado é o intercambio de datos entre programas, como no caso das APIs REST/SOAP ou bases de datos.
Pon os ficheiros de configuración no seu sitio
En Linux existen determinados lugares onde é común almacenar os ficheiros de configuración.
Os ficheiros de configuración que conteñen configuracións para todo o sistema
soense gardar no directorio /etc
. E dentro deste directorio almacénanse nos
seguintes sitios:
-
Ficheiro co nome do programa seguido de .conf
<program>.conf
, como/etc/krb5.conf)
. -
Directorio co nome do programa, como
/etc/nginx/
. -
Directorio co nome do programa seguido de .d
<program>.d
, como/etc/cron.d
.
Por outro parte, os ficheiros con configuracións específicas para un usuario almacénanse no directorio deste. Normalmente son ficheiros ocultos (que empezan por .) que se atopan nos seguintes lugares:
-
Ficheiro con nome do programa
.<program>
, como~/.ssh
. -
Directorio co nome do programa seguido de .d
.<program>.d
, como~/.emacs.d/
. -
No directorio .config, nun directorio ou ficheiro co nome do programa
.config/<program>
, como~/.config/i3/
.
Adicionalmente, os programas teñen un parámetro que permite indicar na liña de comandos a ruta do ficheiro de configuración, por se o temos nun sitio non estándar.
Saída
A saída do programa é un dos principais medios de interacción co usuario e outros programas, polo que ter unha saída limpa que proporcione información útil é crucial á hora de crear un programa.
Aquí van uns consellos a ter en conta para mellorar a saída.
Usa stdout e stderr
Tódolos programas están conectados a 2 streams de saída: stdout e stderr.
Stdout debe usarse para escribir os datos útiles da saída do programa, mentres que stderr debe usarse para escribir información adicional como erros, advertencias, mensaxes de depuración, etc.
Nos entornos Unix, o uso de pipes |
para conectar a saída dun programa
(stdout) á entrada do seguinte (stdin) é unha práctica común. Polo tanto, se
toda a saída se envía a stdout, incluíndo o logo do programa ou as advertencias,
é máis difícil, e en ocasións ata inviable, procesar o resultado con ferramnentas
como grep
ou cut
.
Por exemplo, mira este programa para comprobar credenciais de usuario:
$ credbrute -u /tmp/users.txt -p superman -v >>> Credbrute: The fastest credential checker <<< [INFO] Testing admin:superman -> Fail [INFO] Testing bar:superman -> Success bar:superman [INFO] Testing foo:superman -> Success foo:superman
Se todo se escribe a stdout e nos queremos obter o nome de usuario das probas exitosas, o procesado pode acabar mal:
$ credbrute -u /tmp/users.txt -p superman -v | cut -d ':' -f 1 > superman_users.txt $ cat superman_users.txt >>> Credbrute [INFO] Testing admin [INFO] Testing bar bar [INFO] Testing foo foo
Sen embargo, se soamente escribimos o resultado a stdout e o resto a stderr, a cousa vai mellor:
$ credbrute -u /tmp/users.txt -p superman -v | cut -d ':' -f 1 > superman_users.txt >>> Credbrute: The fastest credential checker <<< [INFO] Testing admin:superman -> Fail [INFO] Testing bar:superman -> Success [INFO] Testing foo:superman -> Success $ cat superman_users.txt bar foo
Como podes observar, unha saída ben redirixida a stdout e stderr pode mellorar a usabilidade dun programa, especialmente cando é usado con outros programas.
Saída grepable
Como vimos, normalmente a saída dun programa úsase como entrada doutros, e iso
é algo cos hackers de Unix saben. Esa é a razón pola que existen tantas
ferramentas para o procesado de liñas de texto coma grep
, cut
, sed
,
etc. Se a saída dun programa se formatea de xeito que poida ser usado polo
resto de ferramentas a través de pipes |
, a usabilidade mellora.
Por exemplo, imaxina que queres obter os usuarios que teñen "superman" como contrasinal:
$ credbrute -u /tmp/users.txt -p superman >>> Credbrute: The fastest credential checker <<< Fail: Incorrect password for user admin -> superman Success!! The password of user foo is superman Success!! The password of user bar is superman $ credbrute -u /tmp/users.txt -p superman | grep "The password of" | cut -d ' ' -f 6 foo bar
Pódese mellorar a usabilidade cunha saída máis concisa:
$ credbrute -u /tmp/users.txt -p superman foo:superman bar:superman $ credbrute -u /tmp/users.txt -p superman | cut -d ':' -f 1 foo bar
E no caso de precisar máis detalles, podes escribilos en stderr:
$ credbrute -u /tmp/users.txt -p superman | cut -d ':' -f 1 [INFO] Testing admin:superman -> Fail [INFO] Testing foo:superman -> Success foo [INFO] Testing bar:superman -> Success bar
Por outra parte, tamén poderías contar ou número de usuarios co contrasinal "superman":
$ credbrute -u /tmp/users.txt -p superman | wc -l 2
Deste xeito, podes usar outras ferramentas para procesar fácilmente a saída, permitindo ao usuario manipular fácilmente os resultados se necesidad que teñas que incorporar novas funcionalidades ao teu programa.
Por outro lado está a consola de Powershell, que a diferencia coas consolas sh, é unha experta en manexar obxetos en vez de texto. Polo que, no caso dos scripts/cmdlets de Powershell, é preferible devolver obxetos en vez de texto.
Saída estructurada
Algúns programas devolven datos complexos (ou non tan complexos) que pode ser útil estructurar nun formato coma JSON ou XML, para que poder procesalos con outros programas.
Por exemplo, nmap permite gardar os resultados dos escaneos nun ficheiro XML, o que pode facilitar o procesado de moitos escaneos distintos cun so programa.
Personalmente prefiro JSON, xa que é máis simple que XML. Ademais, pode procesarse con ferramentas como jq ou gron.
De calquera xeito, se o teu programa produce unha saída estructurada, non olvides indicar o seu formato nun esquema. En XML pode facerse con DTDs, e en JSON con un JSON Schema.
Uso de cores
Cando a saída está deseñada para ser leída por persoas, o uso de cores pode mellorar a lectura e á identificación, especialmente nos programas con saídas de centos de liñas.
Cando se usan, inda que o significado das cores dependen do programa, é importante ser consistente e dar so un significado a cada cor, de xeito que o cerebro do usuario poda relacionar a cor cun evento na execución do programa.
Por exemplo, vermello para erros, verde para boas novas, amarelo para mensaxes de información e azul para mensaxes de depuración.
Detalle da saída
Para permitir que o usuario se centre no importante, a saída debería ser mínima. Sen embargo, o programa tamén debería permitir incrementar o nivel de detalle das mensaxes por se o usuario desexa saber que está acontecendo para entender ou depurar o programa.
Por exemplo, a utilidade cp
non mostra saída se non se require, soamente copia
os ficheiros que usuario indica, mais o nivel de detalle pode aumentarse se se
precisa:
$ cp /tmp/a /tmp/b $ cp /tmp/a /tmp/b -v '/tmp/a' -> '/tmp/b'
Depende do programa, pero unha regla que me gusta seguir é, por defecto,
soamente amosar o resultado do programa e posibles erros (se hai algún), e usar
o parámetro -v
para aumentar o nivel de detalle e amosar ao usuario mensaxes
informativos ou de depuración.
Respecto deste tema, unha práctica que atopo molesta é mostrar un logo na execución do programa. Un logo é unha peza de información irrelevante que desplaza a saída dos comandos anteriores, aumentando a necesidade de facer scroll para revisar os resultados dos comandos anteriores e as veces facendo imposible facer capturas da terminal con toda a información relevante.
Sei que queda súper cool deseñár e mostrar un logo, fíxeno algunha vez, pero é unha práctica que ocupa demasiado espazo. Por favor, non mostredes o logo na execución. Se iso pódese crear unha funcionalidade específica para amosalo.
Documentación: Pon exemplos
Documentar é duro e aburrido. Todo o mundo o sabe. Sen embargo, un truquiño para facer útil a documentación é incluir exemplos do uso do programa. A xente é preguiceira, polo que se temos un exemplo, podemos copialo, pegalo e modificalo para os nosos propósitos.
Polo menos deberías ter exemplos do uso común da ferramenta, incluindo a entrada e a saída, de xeito que os usuarios poidan facerse unha idea do que esperar no resultado. Por exemplo, o readme de Rubeus contén un exemplo de cada comando para saber que se pode facer.
Ademais, se engades casos de uso da túa ferramenta no que se combina con outras, isto pode dar aos usuarios unha idea de que modo pode ser útil.
Do mesmo xeito, amosa exemplos dos ficheiros empregados polo programa. Por un lado podes dar plantillas dos ficheiros de configuración, como a configuración do Responder. E por outra parte amosar exemplos e esquemas dos ficheiros de resultados (JSON, XML, etc) producidos polo programa, para que outra xente sexa consciente do formato destes e poida construir outros programas para procesalos.
Outro conselliño é non poñer no readme a saída da axuda do programa
(-h/--help
), xa que se pode ver co propio programa e é díficil de manter
actualizada. Pero isto non significa que non se poidan explicar os parámetros (
e dar exemplos de uso).
Conclusión
Bueno xente, espero que estas prácticas che axuden a facer programas máis útiles para ti e para todos. No caso de coñecer outros trucos, por favor compárteos!!
Veña, a pasalo ben!!