Package: hashbang
HashbangHashbang is a Python 3 library for quickly creating command-line ready scripts. In the most basic form, a simple hashbang command can be just a simple annotation. For more complex types, it relies on Python3'skeyword-only argumentsto provide a seamless syntax for command line usage.#!/usr/bin/env python3fromhashbangimportcommand@commanddefecho(message):print(message)if__name__=='__main__':echo.execute()InstallationHashbang can be installed from pippython3-mpipinstallhashbang[completion]This will also includeargcompletewhich powers the autocomplete for hashbang. The completion feature is optional; if you would like to exclude it, install usingpip install hashbang.Quick Start ExamplesLet's start with some examples.Simple, no argument script#!/usr/bin/env python3importosfromhashbangimportcommand@commanddefpwd():returnos.getcwd()if__name__=='__main__':pwd.execute()$ pwd.py$ pwd.py
/home/mauricelam/code/hashbangThe return value from the function is printed to stdout.The additional value you get from using hashbang in this simple case is the help message, and the usage message when unexpected arguments are supplied.$ pwd.py --help$ pwd.py --help
usage: pwd.py [-h]

optional arguments:
  -h, --help  show this help message and exitPositional argument (nargs='?')@commanddefls(dir=None):returnos.listdir(path=dir)$ ls.py$ ls.py
bin
etc
home
usr
var$ ls.py bin$ ls.py bin
cp
df
echo
mkdir
mv
pwd
rmMultiple positional argument (nargs=None)@commanddefcp(src,dest):shutil.copy2(src,dest)$ cp.py textfile.txt copy_of_textfile.txt$ cp.py textfile.txt copy_of_textfile.txt
$ ls
textfile.txt    copy_of_textfile.txtVariadic positional argument (nargs='*')@commanddefecho(*message):print(' '.join(message))$ echo.py Hello world$ echo.py Hello world
Hello worldBoolean flag (default False) (action='store_true')@commanddefpwd(*,resolve_symlink=False):cwd=os.cwd()ifresolve_symlink:cwd=os.path.realpath(cwd)returncwd$ pwd.py$ pwd.py
/var$ pwd.py --resolve_symlink$ pwd.py --resolve_symlink
/private/varBoolean flag (default True) (action='store_false')@commanddefecho(*message,trailing_newline=True):print(' '.join(message),end=('\n'iftrailing_newlineelse''))$ echo.py Hello world && echo '.'$ echo.py Hello world && echo '.'
Hello world
.$ echo.py --notrailing_newline Hello world && echo '.'$ echo.py --notrailing_newline Hello world && echo '.'
Hello world.Keyword argument (action='store')@commanddefcut(*,fields=None,chars=None,delimeter='\t'):result=[]forlineinsys.stdin:seq=line.strip('\n').split(delimeter)iffieldselseline.strip('\n')pos=fieldsorcharsresult.append(''.join(seq[int(p)]forpinpos.split(',')))return'\n'.join(result)$ echo -e 'a,b,c,d\ne,f,g,h' | cut.py --fields '1,2,3' --delimeter=','$ echo -e 'a,b,c,d\ne,f,g,h' | cut.py --fields '1,2,3' --delimeter=','
bc
fgCheatsheetParameter typePython syntaxCommand line exampleargparse equivalentPositional (no default value)def func(foo)command.py foonargs=NonePositional (with default value)def func(foo=None)command.py foonargs='?'Var positionaldef func(*foo)command.py foo bar baznargs='*'Var positional (named\_REMAINDER\_)def func(*_REMAINDER_)nargs=argparse.REMAINDERKeyword-only (default false)def func(*, foo=False)command.py --fooaction='store_true'Keyword-only (default true)def func(*, foo=True)command.py --nofooaction='store_false'Keyword-only (other default types)def func(*, foo='bar')command.py --foo valueaction='store'Var keyworddef func(**kwargs)Not allowed in hashbangSee theAPI referencewiki page for the full APIs.Command delegationThehashbang.subcommandsfunction can be used to create a chain of commands, likegit branch.@commanddefbranch(newbranch=None):ifnewbranchisNone:return'\n'.join(Repository('.').heads.keys())returnRepository('.').create_head(newbranch)@commanddeflog(*,max_count=None,graph=False):logs=Repository('.').log()ifgraph:returnformat_as_graph(logs)else:return'\n'.join(logs)if__name__=='__main__':subcommands(branch=branch,log=log).execute()$ git.py branch$ git.py branch
master$ git.py branch hello$ git.py branch hello
$ git.py branch
master
hello$ git.py log$ git.py log
commit 602cbd7c68b0980ab1dbe0d3b9e83b69c04d9698 (HEAD -> master, origin/master)
Merge: 333d617 34c0a0f
Author: Maurice Lam <me@mauricelam.com>
Date:   Mon May 13 23:32:56 2019 -0700

    Merge branch 'master' of ssh://github.com/mauricelam/hashbang

commit 333d6172a8afa9e81baea0d753d6cfdc7751d38d
Author: Maurice Lam <me@mauricelam.com>
Date:   Mon May 13 23:31:17 2019 -0700

    Move directory structure to match pypi importCustom command delegatorIfsubocommandsis not sufficient for your purposes, you can use the@command.delegatordecorator. Its usage is the same as the@commanddecorator, but the implementing function must then either call.execute(_REMAINDER_)on another command, or raiseNoMatchingDelegateexception.@commanddefnormal_who(*,print_dead_process=False,print_runlevel=False):return...@commanddefwhoami():'''Prints who I am.'''returngetpass.getuser()@command.delegatordefwho(am=None,i=None,*_REMAINDER_):if(am,i)==('am','i'):returnwhoami.execute([])elif(am,i)==(None,None):returnnormal_who.execute(_REMAINDER_)else:raiseNoMatchingDelegate$ who.py$ who.py
mauricelam console  May  8 00:02 
mauricelam ttys000  May  8 00:03 
mauricelam ttys001  May  8 00:04$ who.py am i$ who.py am i 
mauricelam ttys001  May  8 00:04$ who.py --print_dead_process$ who.py --print_dead_process
mauricelam ttys002  May  8 00:40 	term=0 exit=0$ who.py are you$ who.py are you
Error: No matching delegateWhile using the regular@commanddecorator will still work in this situation, but tab-completion and help message will be wrong.✓ Using@command.delegator$ who.py --help
usage: who.py [--print_dead_process] [--print_runlevel]

optional arguments:
  --print_dead_process
  --print_runlevel$ who.py am i --help
usage: who.py am i

Prints who I am.✗ Using@command$ who.py am i --help
usage: who.py [-h] [am] [i]

positional arguments:
  am
  i

optional arguments:
  -h, --help  show this help message and exitArgument customizationAn argument can be further customized using theArgumentclass in the@commanddecorator.For example, an alias can be added to the argument.@command(Argument('trailing_newline',aliases=('n',))defecho(*message,trailing_newline=True):print(' '.join(message),end=('\n'iftrailing_newlineelse''))$ echo.py Hello world && echo '.'$ echo.py Hello world && echo '.'
Hello world
.$ echo.py -n Hello world && echo '.'$ echo.py -n Hello world && echo '.'
Hello world.Alternatively, you can also choose to specify theArgumentusing argument annotation syntax defined inPEP 3107.@commanddefecho(*message,trailing_newline:Argument(aliases=('n',))=True):print(' '.join(message),end=('\n'iftrailing_newlineelse''))Seehttps://github.com/mauricelam/hashbang/wiki/API-reference#argumentfor the fullArgumentAPI.Help messageThe help message for the command is take directly from the docstring of the function. Additionally, thehelpargument inArgumentcan be used to document each argument. A paragraph in the docstring prefixed withusage:(case insensitive) is used as the usage message.@commanddefgit(command:Argument(help='Possible commands are "branch", "log", etc',choices=('branch','log')),*_REMAINDER_):'''git - the stupid content trackerUsage:  git [--version] [--help] [-C <path>] [-c <name>=<value>][--exec-path[=<path>]] [--html-path] [--man-path] [--info-path][-p|--paginate|-P|--no-pager] [--no-replace-objects] [--bare][--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>][--super-prefix=<path>]<command> [<args>]'''return...$ git.py --help$ git.py --help
usage:  git [--version] [--help] [-C <path>] [-c <name>=<value>]
         [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
         [-p|--paginate|-P|--no-pager] [--no-replace-objects] [--bare]
         [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
         [--super-prefix=<path>]
         <command> [<args>]

git - the stupid content tracker

positional arguments:
  {branch,log}  Possible commands are "branch", "log", etc

optional arguments:
  -h, --help    show this help message and exit$ git.py --nonexistent$ git.py --nonexistent
unknown option: --nonexistent
usage: git [--version] [--help] [-C <path>] [-c <name>=<value>]
           [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
           [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]
           [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
           <command> [<args>]Tab completionSetupHashbang also comes with tab completion functionality, powered byargcomplete. Since argcomplete is an optional dependency of hashbang, you can install the completion feature usingpython3-mpipinstallhashbang[completion]After installing, to register a command for tab-completion, runeval"$(register-python-argcompletemy-awesome-script)"Alternatively, to activate global completion for all commands, follow the one-time setup directions in theGlobal completionsection of argcomplete's documentation, and then add the stringPYTHON_ARGCOMPLETE_OKto the top of the file as a comment (after the#!line).Specifying the choicesThe simplest way to use tab completion is via thechoicesargument in theArgumentconstructor.@commanddefapt_get(command:Argument(choices=('update','upgrade','install','remove')),*_REMAINDER_):returnsubprocess.check_output(['apt-get',command,*_REMAINDER])$ apt_get.py <TAB><TAB>$ apt_get.py <TAB><TAB>
update    upgrade    install    remove$ apt_get.py up<TAB><TAB>
update    upgrade$ apt_get.py upg<TAB>
$ apt_get.py upgradeUsing a completerIf the choices are not known ahead of time (before execution), or is too expensive to precompute, you can instead specify a completer for the argument.@commanddefcp(src:Argument(completer=lambdax:os.listdir()),dest):shutils.copy2(src,dest)$ cp.py <TAB><TAB>$ cp.py <TAB><TAB>
LICENSE           build             hashbang          requirements.txt  tests$ cp.py LIC<TAB>
$ cp.py LICENSEExit codesJust like normal Python programs, the preferred way to set an exit code is usingsys.exit(). By default, exit code0is returned for functions that run without raising an exception, or printed a help message withhelp. If a function raises an exception, the result code is1. If a function quits withsys.exit(), the exit code of is preserved.In addition, you can also callsys.exit()inside theexception_handlerif you want to return different exit codes based on the exception that was thrown. Seetests/extension/custom_exit_codes.pyfor an example.Further readingFor further reading, check out thewikipages.