Tutorial de Argparse
********************

autor:
   Tshepang Lekhonkhobe

Este tutorial pretende ser uma introdução gentil ao "argparse" --- o
módulo recomendado na biblioteca padrão do Python para fazer a análise
de linha de comando.

Nota:

  Existem outros dois módulos que cumprem esta mesma tarefa, chamados
  "getopt" (equivalente ao "getopt()" da linguagem C) e outro que hoje
  está descontinuado "optparse". Note também que o "argparse" é
  baseado no módulo "optparse", e, portanto, possui bastante
  similaridade em termos de uso.


Conceitos
=========

Demonstraremos o tipo de funcionalidade que vamos explorar neste
tutorial introdutório fazendo uso do comando **ls**:

   $ ls
   cpython  devguide  prog.py  pypy  rm-unused-function.patch
   $ ls pypy
   ctypes_configure  demo  dotviewer  include  lib_pypy  lib-python ...
   $ ls -l
   total 20
   drwxr-xr-x 19 wena wena 4096 Feb 18 18:51 cpython
   drwxr-xr-x  4 wena wena 4096 Feb  8 12:04 devguide
   -rwxr-xr-x  1 wena wena  535 Feb 19 00:05 prog.py
   drwxr-xr-x 14 wena wena 4096 Feb  7 00:59 pypy
   -rw-r--r--  1 wena wena  741 Feb 18 01:01 rm-unused-function.patch
   $ ls --help
   Usage: ls [OPTION]... [FILE]...
   List information about the FILEs (the current directory by default).
   Sort entries alphabetically if none of -cftuvSUX nor --sort is specified.
   ...

Alguns conceitos que podemos aprender a partir destes quatro comandos:

* O comando **ls** é útil quando usado sem nenhuma opção. Por padrão,
  ele mostra o conteúdo do diretório atual.

* Se quisermos além do que ele fornece por padrão, contamos um pouco
  mais. Neste caso, queremos que ele exiba um diretório diferente,
  "pypy". O que fizemos foi especificar o que é conhecido como
  argumento posicional. Ele é chamado assim porque o programa deve
  saber o que fazer com o valor, apenas com base em onde ele aparece
  na linha de comando. Este conceito é mais relevante para um comando
  como **cp**, cujo uso mais básico é "cp SRC DEST". A primeira
  posição é *o que você deseja copiar* e a segunda posição é *para
  onde você deseja copiar*.

* Agora, digamos que queremos mudar o comportamento do programa. Em
  nosso exemplo, exibimos mais informações para cada arquivo em vez de
  apenas mostrar os nomes dos arquivos. O "-l" nesse caso é conhecido
  como um argumento opcional.

* Esse é um trecho do texto de ajuda. É muito útil que possas
  encontrar um programa que nunca usastes antes e poder descobrir como
  o mesmo funciona simplesmente lendo o seu texto de ajuda.


O básico
========

Comecemos com um exemplo muito simples que irá fazer (quase) nada:

   import argparse
   parser = argparse.ArgumentParser()
   parser.parse_args()

A seguir, temos o resultado da execução do código:

   $ python3 prog.py
   $ python3 prog.py --help
   usage: prog.py [-h]

   options:
     -h, --help  show this help message and exit
   $ python3 prog.py --verbose
   usage: prog.py [-h]
   prog.py: error: unrecognized arguments: --verbose
   $ python3 prog.py foo
   usage: prog.py [-h]
   prog.py: error: unrecognized arguments: foo

Eis aqui o que está acontecendo:

* Executar o script sem qualquer opção resultará que nada será exibido
  em stdout. Isso não é útil.

* O segundo começa a exibir as utilidades do módulo "argparse". Não
  fizemos quase nada, mas já recebemos uma boa mensagem de ajuda.

* A opção "--help", que também pode ser encurtada para "-h", é a única
  opção que obtemos livremente (ou seja, não é necessário
  determiná-la). Determinar qualquer outra coisa resulta num erro. Mas
  mesmo assim, recebemos uma mensagem de uso bastante útil, também de
  graça.


Apresentando os argumentos posicionais
======================================

Um exemplo:

   import argparse
   parser = argparse.ArgumentParser()
   parser.add_argument("echo")
   args = parser.parse_args()
   print(args.echo)

E executando o código:

   $ python3 prog.py
   usage: prog.py [-h] echo
   prog.py: error: the following arguments are required: echo
   $ python3 prog.py --help
   usage: prog.py [-h] echo

   positional arguments:
     echo

   options:
     -h, --help  show this help message and exit
   $ python3 prog.py foo
   foo

Aqui está o que acontecerá:

* Nós adicionamos o método "add_argument()", cujo o mesmo usamos para
  especificar quais opções de linha de comando o programa está
  disposto a aceitar. Neste caso, eu o nomeei "echo" para que ele
  esteja de acordo com sua função.

* Chamar o nosso programa neste momento, requer a especificação de uma
  opção.

* O método "parse_args()" realmente retorna alguns dados das opções
  especificadas, neste caso, "echo".

* A variável é uma forma de "mágica" que "argparse" executa de brinde
  (ou seja, não é necessário especificar em qual variável esse valor é
  armazenado). Você também notará que seu nome corresponde ao
  argumento string dado ao método, "echo".

Observe, no entanto, que, embora a tela de ajuda pareça boa e tudo,
atualmente não é tão útil quanto poderia ser. Por exemplo, vemos que
temos "echo" como um argumento posicional, mas não sabemos o que ele
faz, além de adivinhar ou ler o código-fonte. Então, vamos torná-lo um
pouco mais útil:

   import argparse
   parser = argparse.ArgumentParser()
   parser.add_argument("echo", help="echo the string you use here")
   args = parser.parse_args()
   print(args.echo)

E, iremos obter:

   $ python3 prog.py -h
   usage: prog.py [-h] echo

   positional arguments:
     echo        echo the string you use here

   options:
     -h, --help  show this help message and exit

Agora, que tal fazer algo ainda mais útil:

   import argparse
   parser = argparse.ArgumentParser()
   parser.add_argument("square", help="display a square of a given number")
   args = parser.parse_args()
   print(args.square**2)

A seguir, temos o resultado da execução do código:

   $ python3 prog.py 4
   Traceback (most recent call last):
     File "prog.py", line 5, in <module>
       print(args.square**2)
   TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'

Isso não correu tão bem. Isso porque "argparse" trata as opções que
damos a ele como strings, a menos que digamos o contrário. Então,
vamos dizer ao "argparse" para tratar essa entrada como um inteiro:

   import argparse
   parser = argparse.ArgumentParser()
   parser.add_argument("square", help="display a square of a given number",
                       type=int)
   args = parser.parse_args()
   print(args.square**2)

A seguir, temos o resultado da execução do código:

   $ python3 prog.py 4
   16
   $ python3 prog.py four
   usage: prog.py [-h] square
   prog.py: error: argument square: invalid int value: 'four'

Correu bem. O programa agora até fecha com ajuda de entrada ilegal
ruim antes de prosseguir.


Apresentando os argumentos opcionais
====================================

Até agora, jogamos com argumentos posicionais. Vamos dar uma olhada em
como adicionar opcionais:

   import argparse
   parser = argparse.ArgumentParser()
   parser.add_argument("--verbosity", help="increase output verbosity")
   args = parser.parse_args()
   if args.verbosity:
       print("verbosity turned on")

E a saída:

   $ python3 prog.py --verbosity 1
   verbosity turned on
   $ python3 prog.py
   $ python3 prog.py --help
   usage: prog.py [-h] [--verbosity VERBOSITY]

   options:
     -h, --help            show this help message and exit
     --verbosity VERBOSITY
                           increase output verbosity
   $ python3 prog.py --verbosity
   usage: prog.py [-h] [--verbosity VERBOSITY]
   prog.py: error: argument --verbosity: expected one argument

Eis aqui o que está acontecendo:

* O programa é escrito de forma a exibir algo quando "--verbosity" é
  especificado e não exibir nada quando não for.

* Para mostrar que a opção é realmente opcional, não há erro ao
  executar o programa sem ela. Observe que, por padrão, se um
  argumento opcional não for usado, a variável relevante, neste caso
  "args.verbosity", recebe "None" como valor, razão pela qual falha no
  teste de verdade da instrução "if".

* A mensagem de ajuda é um pouco diferente.

* Ao usar a opção "--verbosity", deve-se também especificar algum
  valor, qualquer valor.

O exemplo acima aceita valores inteiros arbitrários para "--
verbosity", mas para nosso programa simples, apenas dois valores são
realmente úteis, "True" ou "False". Vamos modificar o código de
acordo:

   import argparse
   parser = argparse.ArgumentParser()
   parser.add_argument("--verbose", help="increase output verbosity",
                       action="store_true")
   args = parser.parse_args()
   if args.verbose:
       print("verbosity turned on")

E a saída:

   $ python3 prog.py --verbose
   verbosity turned on
   $ python3 prog.py --verbose 1
   usage: prog.py [-h] [--verbose]
   prog.py: error: unrecognized arguments: 1
   $ python3 prog.py --help
   usage: prog.py [-h] [--verbose]

   options:
     -h, --help  show this help message and exit
     --verbose   increase output verbosity

Eis aqui o que está acontecendo:

* A opção agora é mais um sinalizador do que algo que requer um valor.
  Até mudamos o nome da opção para corresponder a essa ideia. Observe
  que agora especificamos uma nova palavra reservada, "action", e
  damos a ela o valor ""store_true"". Isso significa que, se a opção
  for especificada, atribui o valor "True" para "args.verbose". Não
  especificá-la implica em "False".

* Ele reclama quando você especifica um valor, no verdadeiro espírito
  do que os sinalizadores realmente são.

* Observe o texto de ajuda diferente.


Opções curtas
-------------

Se você estiver familiarizado com o uso da linha de comando, notará
que ainda não toquei no tópico das versões curtas das opções. É bem
simples:

   import argparse
   parser = argparse.ArgumentParser()
   parser.add_argument("-v", "--verbose", help="increase output verbosity",
                       action="store_true")
   args = parser.parse_args()
   if args.verbose:
       print("verbosity turned on")

E aqui vai:

   $ python3 prog.py -v
   verbosity turned on
   $ python3 prog.py --help
   usage: prog.py [-h] [-v]

   options:
     -h, --help     show this help message and exit
     -v, --verbose  increase output verbosity

Observe que a nova habilidade também é refletida no texto de ajuda.


Combinando argumentos posicionais e opcionais
=============================================

Nosso programa continua crescendo em complexidade:

   import argparse
   parser = argparse.ArgumentParser()
   parser.add_argument("square", type=int,
                       help="display a square of a given number")
   parser.add_argument("-v", "--verbose", action="store_true",
                       help="increase output verbosity")
   args = parser.parse_args()
   answer = args.square**2
   if args.verbose:
       print(f"the square of {args.square} equals {answer}")
   else:
       print(answer)

E agora a saída:

   $ python3 prog.py
   usage: prog.py [-h] [-v] square
   prog.py: error: the following arguments are required: square
   $ python3 prog.py 4
   16
   $ python3 prog.py 4 --verbose
   the square of 4 equals 16
   $ python3 prog.py --verbose 4
   the square of 4 equals 16

* Trouxemos de volta um argumento posicional, daí a reclamação.

* Observe que a ordem não importa.

Que tal devolvermos a este nosso programa a capacidade de ter vários
valores de verbosidade e realmente usá-los:

   import argparse
   parser = argparse.ArgumentParser()
   parser.add_argument("square", type=int,
                       help="display a square of a given number")
   parser.add_argument("-v", "--verbosity", type=int,
                       help="increase output verbosity")
   args = parser.parse_args()
   answer = args.square**2
   if args.verbosity == 2:
       print(f"the square of {args.square} equals {answer}")
   elif args.verbosity == 1:
       print(f"{args.square}^2 == {answer}")
   else:
       print(answer)

E a saída:

   $ python3 prog.py 4
   16
   $ python3 prog.py 4 -v
   usage: prog.py [-h] [-v VERBOSITY] square
   prog.py: error: argument -v/--verbosity: expected one argument
   $ python3 prog.py 4 -v 1
   4^2 == 16
   $ python3 prog.py 4 -v 2
   the square of 4 equals 16
   $ python3 prog.py 4 -v 3
   16

Todos eles parecem bons, exceto o último, que expõe um bug em nosso
programa. Vamos corrigi-lo restringindo os valores que a opção "--
verbosity" pode aceitar:

   import argparse
   parser = argparse.ArgumentParser()
   parser.add_argument("square", type=int,
                       help="display a square of a given number")
   parser.add_argument("-v", "--verbosity", type=int, choices=[0, 1, 2],
                       help="increase output verbosity")
   args = parser.parse_args()
   answer = args.square**2
   if args.verbosity == 2:
       print(f"the square of {args.square} equals {answer}")
   elif args.verbosity == 1:
       print(f"{args.square}^2 == {answer}")
   else:
       print(answer)

E a saída:

   $ python3 prog.py 4 -v 3
   usage: prog.py [-h] [-v {0,1,2}] square
   prog.py: error: argument -v/--verbosity: invalid choice: 3 (choose from 0, 1, 2)
   $ python3 prog.py 4 -h
   usage: prog.py [-h] [-v {0,1,2}] square

   positional arguments:
     square                display a square of a given number

   options:
     -h, --help            show this help message and exit
     -v {0,1,2}, --verbosity {0,1,2}
                           increase output verbosity

Observe que a alteração também reflete tanto na mensagem de erro
quanto na string de ajuda.

Agora, vamos usar uma abordagem diferente de brincar com a
verbosidade, que é bastante comum. Ele também corresponde à maneira
como o executável do CPython trata seu próprio argumento de
verbosidade (verifique a saída de "python --help"):

   import argparse
   parser = argparse.ArgumentParser()
   parser.add_argument("square", type=int,
                       help="display the square of a given number")
   parser.add_argument("-v", "--verbosity", action="count",
                       help="increase output verbosity")
   args = parser.parse_args()
   answer = args.square**2
   if args.verbosity == 2:
       print(f"the square of {args.square} equals {answer}")
   elif args.verbosity == 1:
       print(f"{args.square}^2 == {answer}")
   else:
       print(answer)

Introduzimos outra ação, "contar", para contar o número de ocorrências
de opções específicas.

   $ python3 prog.py 4
   16
   $ python3 prog.py 4 -v
   4^2 == 16
   $ python3 prog.py 4 -vv
   the square of 4 equals 16
   $ python3 prog.py 4 --verbosity --verbosity
   the square of 4 equals 16
   $ python3 prog.py 4 -v 1
   usage: prog.py [-h] [-v] square
   prog.py: error: unrecognized arguments: 1
   $ python3 prog.py 4 -h
   usage: prog.py [-h] [-v] square

   positional arguments:
     square           display a square of a given number

   options:
     -h, --help       show this help message and exit
     -v, --verbosity  increase output verbosity
   $ python3 prog.py 4 -vvv
   16

* Sim, agora é mais um sinalizador (semelhante a
  "action="store_true"") na versão anterior do nosso script. Isso deve
  explicar a reclamação.

* Ele também se comporta de maneira semelhante à ação "store_true".

* Agora aqui está uma demonstração do que a ação "contar" oferece.
  Você provavelmente já viu esse tipo de uso antes.

* E se você não especificar o sinalizador "-v", esse sinalizador será
  considerado como tendo valor "None".

* Como deve ser esperado, especificando a forma longa do sinalizador,
  devemos obter a mesma saída.

* Infelizmente, nossa saída de ajuda não é muito informativa sobre a
  nova habilidade que nosso script adquiriu, mas isso sempre pode ser
  corrigido melhorando a documentação de nosso script (por exemplo,
  através do argumento nomeado "help").

* Essa última saída expõe um bug em nosso programa.

Vamos corrigir:

   import argparse
   parser = argparse.ArgumentParser()
   parser.add_argument("square", type=int,
                       help="display a square of a given number")
   parser.add_argument("-v", "--verbosity", action="count",
                       help="increase output verbosity")
   args = parser.parse_args()
   answer = args.square**2

   # bugfix: replace == with >=
   if args.verbosity >= 2:
       print(f"the square of {args.square} equals {answer}")
   elif args.verbosity >= 1:
       print(f"{args.square}^2 == {answer}")
   else:
       print(answer)

E isso aqui é o mesmo retorna:

   $ python3 prog.py 4 -vvv
   the square of 4 equals 16
   $ python3 prog.py 4 -vvvv
   the square of 4 equals 16
   $ python3 prog.py 4
   Traceback (most recent call last):
     File "prog.py", line 11, in <module>
       if args.verbosity >= 2:
   TypeError: '>=' not supported between instances of 'NoneType' and 'int'

* A primeira saída correu bem e corrige o bug que tínhamos antes. Ou
  seja, queremos que qualquer valor >= 2 seja o mais detalhado
  possível.

* A terceira saída não está tão boa.

Vamos corrigir esse bug:

   import argparse
   parser = argparse.ArgumentParser()
   parser.add_argument("square", type=int,
                       help="display a square of a given number")
   parser.add_argument("-v", "--verbosity", action="count", default=0,
                       help="increase output verbosity")
   args = parser.parse_args()
   answer = args.square**2
   if args.verbosity >= 2:
       print(f"the square of {args.square} equals {answer}")
   elif args.verbosity >= 1:
       print(f"{args.square}^2 == {answer}")
   else:
       print(answer)

Acabamos de introduzir outra palavra reservada, "default". Nós o
configuramos como "0" para torná-lo comparável aos outros valores int.
Lembre-se que por padrão, se um argumento opcional não for
especificado, ele obtém o valor "None", e isso não pode ser comparado
a um valor int (daí a exceção "TypeError").

E:

   $ python3 prog.py 4
   16

Você pode ir muito longe apenas com o que aprendemos até agora, e nós
apenas arranhamos a superfície. O módulo "argparse" é muito poderoso,
e vamos explorar um pouco mais antes de terminar este tutorial.


Avançando um pouco mais
=======================

E se quiséssemos expandir nosso pequeno programa, ampliando seu
potencial:

   import argparse
   parser = argparse.ArgumentParser()
   parser.add_argument("x", type=int, help="the base")
   parser.add_argument("y", type=int, help="the exponent")
   parser.add_argument("-v", "--verbosity", action="count", default=0)
   args = parser.parse_args()
   answer = args.x**args.y
   if args.verbosity >= 2:
       print(f"{args.x} to the power {args.y} equals {answer}")
   elif args.verbosity >= 1:
       print(f"{args.x}^{args.y} == {answer}")
   else:
       print(answer)

Saída:

   $ python3 prog.py
   usage: prog.py [-h] [-v] x y
   prog.py: error: the following arguments are required: x, y
   $ python3 prog.py -h
   usage: prog.py [-h] [-v] x y

   positional arguments:
     x                the base
     y                the exponent

   options:
     -h, --help       show this help message and exit
     -v, --verbosity
   $ python3 prog.py 4 2 -v
   4^2 == 16

Observe que até agora estamos usando o nível de verbosidade para
*alterar* o texto que é exibido. O exemplo a seguir usa o nível de
verbosidade para exibir *mais* texto:

   import argparse
   parser = argparse.ArgumentParser()
   parser.add_argument("x", type=int, help="the base")
   parser.add_argument("y", type=int, help="the exponent")
   parser.add_argument("-v", "--verbosity", action="count", default=0)
   args = parser.parse_args()
   answer = args.x**args.y
   if args.verbosity >= 2:
       print(f"Running '{__file__}'")
   if args.verbosity >= 1:
       print(f"{args.x}^{args.y} == ", end="")
   print(answer)

Saída:

   $ python3 prog.py 4 2
   16
   $ python3 prog.py 4 2 -v
   4^2 == 16
   $ python3 prog.py 4 2 -vv
   Running 'prog.py'
   4^2 == 16


Opções conflitantes
-------------------

Até agora, trabalhamos com dois métodos de uma instância
"argparse.ArgumentParser". Vamos apresentar um terceiro,
"add_mutually_exclusive_group()". Ele nos permite especificar opções
que entram em conflito umas com as outras. Vamos também alterar o
resto do programa para que a nova funcionalidade faça mais sentido:
vamos introduzir a opção "--quiet", que será o oposto da opção "--
verbose":

   import argparse

   parser = argparse.ArgumentParser()
   group = parser.add_mutually_exclusive_group()
   group.add_argument("-v", "--verbose", action="store_true")
   group.add_argument("-q", "--quiet", action="store_true")
   parser.add_argument("x", type=int, help="the base")
   parser.add_argument("y", type=int, help="the exponent")
   args = parser.parse_args()
   answer = args.x**args.y

   if args.quiet:
       print(answer)
   elif args.verbose:
       print(f"{args.x} to the power {args.y} equals {answer}")
   else:
       print(f"{args.x}^{args.y} == {answer}")

Nosso programa agora está mais simples e perdemos algumas
funcionalidades para demonstração. De qualquer forma, aqui está a
saída:

   $ python3 prog.py 4 2
   4^2 == 16
   $ python3 prog.py 4 2 -q
   16
   $ python3 prog.py 4 2 -v
   4 to the power 2 equals 16
   $ python3 prog.py 4 2 -vq
   usage: prog.py [-h] [-v | -q] x y
   prog.py: error: argument -q/--quiet: not allowed with argument -v/--verbose
   $ python3 prog.py 4 2 -v --quiet
   usage: prog.py [-h] [-v | -q] x y
   prog.py: error: argument -q/--quiet: not allowed with argument -v/--verbose

Isso deve ser fácil de seguir. Eu adicionei essa última saída para que
você possa ver o tipo de flexibilidade que obtém, ou seja, misturar
opções de formato longo com formatos curtos.

Antes de concluirmos, você provavelmente quer dizer aos seus usuários
o propósito principal do seu programa, caso eles não saibam:

   import argparse

   parser = argparse.ArgumentParser(description="calculate X to the power of Y")
   group = parser.add_mutually_exclusive_group()
   group.add_argument("-v", "--verbose", action="store_true")
   group.add_argument("-q", "--quiet", action="store_true")
   parser.add_argument("x", type=int, help="the base")
   parser.add_argument("y", type=int, help="the exponent")
   args = parser.parse_args()
   answer = args.x**args.y

   if args.quiet:
       print(answer)
   elif args.verbose:
       print(f"{args.x} to the power {args.y} equals {answer}")
   else:
       print(f"{args.x}^{args.y} == {answer}")

Observe essa pequena diferença no texto de uso. Observe o "[-v | -q]",
que nos diz que podemos usar "-v" ou "-q", mas não ambos ao mesmo
tempo:

   $ python3 prog.py --help
   usage: prog.py [-h] [-v | -q] x y

   calculate X to the power of Y

   positional arguments:
     x              the base
     y              the exponent

   options:
     -h, --help     show this help message and exit
     -v, --verbose
     -q, --quiet


Conclusão
=========

O módulo "argparse" oferece muito mais do que o mostrado aqui. Sua
documentação é bastante detalhada e completo, e cheia de exemplos.
Tendo passado por este tutorial, você deve digeri-la facilmente sem se
sentir sobrecarregado.
