aka: also known as

I was chatting with Anthony Scopatz last week, and one of the things we covered was how it'd be cool to have a subcommand launcher, kind of like git, where the subcommands were swappable. If you're not familiar, git automatically calls out to git-something (note the dash) whenever you run

$ git something

and something is not one of the builtin git commands. For me, ~/bin is in my PATH, so

$ git lost
git: 'lost' is not a git command. See 'git --help'.
$ echo "echo how rude!" > ~/bin/git-lost; chmod +x ~/bin/git-lost
$ git lost
how rude!

And so what Anthony was talking about was having two commands that are supposed to do the same thing, and being able to switch between them. For example: maybe we have git-away and git-gone and both of them perform a similar function, and we wish call our preferred one when we run git lost.

One way to do this would be to copy or symlink our chosen version as git-lost, and replace that file whenever we wanted to switch between git-away and git-gone. Another would be to rename both as git-lost and store them in different folders, adjusting our PATH variable so the preferred version of git-lost would get called.

Now with git itself, you can do this by making a git alias. Here are some of my frequently used aliases from my ~/.gitconfig:

[alias]
    diffw = diff --word-diff=color --ignore-cr-at-eol
    root = rev-parse --show-toplevel
    lgo=log
    lazy=commit -a -m.

git diffw will highlight only parts of the line that were added and removed, instead of whole lines
git root prints the top-most directory of the current git repository
git lgo is a typo for git log (I also have several versions of checkout typos)
git lazy is for those frequent times when having to think of a commit message is too onerous.

So we would add lost=away to our alias block to have git lost call git-away and change that alias to lost=gone when we wanted to have git lost call git-gone, instead.

I wondered what a simple, generic version of such capability might look like and wrote a short shell script and then a README file that's ten times longer than the script itself to explain it. :)

  .---------------------.
 / aka - also known as /
/---------------------/

Simple, persistent, shell-based command aliasing

Usage: aka alias               show stored aliases
       aka alias NAME CMD...   store an alias for CMD
       aka NAME [...]          run CMD, optionally with more arguments

By default, aliases are stored in the .aka file of the current working directory. Optionally, the location of that file can be modified by setting the AKA_FILE environment variable. Tested on Debian, OpenBSD, and OpenWRT, aka uses portable shell syntax and should work everywhere that has a typical /bin/sh

Examples:

$ aka alias demo more

$ aka demo --version
more from util-linux 2.36

$ aka alias demo git

$ aka demo --version
git version 2.28.0

The command you alias can have parameters

$ aka alias l ls -f

$ aka l
.aka .. aka aka_tiny . README .git

And you can pass additional parameters

$ aka l -tR
.. aka_tiny aka .git .aka README .

You can see all of the currently stored aliases

$ aka alias
l='ls -f'
demo='git'

And edit them using your favorite text editor

$ cat .aka
alias demo='git'
alias l='ls -f'

Everything that is not aliased will get executed as a regular command.

$ aka python3 --version
Python 3.8.6

Why would anyone want this?

A practical use case might be to have

.
├── c_proj
   └── .aka       | alias doit='make install'
├── go_proj
   └── .aka       | alias doit='go run'
└── python_proj
    └── .aka       | alias doit='pip install .'

Now you can aka doit to your heart's content inside each of those folders, and have that execute the appropriate build commands for the type of project it is.

$ for x in *; do (cd $x && aka doit); done
make: *** No rule to make target 'install'.  Stop.
go run: no go files listed
ERROR: Directory '.' is not installable. Neither 'setup.py' nor 'pyproject.toml' found.

Installation

Grab aka, make it executable, and place it somewhere in your path.

The standard version of aka (~40 lines) that prints usage and has comments explaining the code is here:

https://git.sr.ht/~pi/aka/blob/main/aka

If you just want the business logic, functional equivalent (~10 lines) is here:

https://git.sr.ht/~pi/aka/blob/main/aka_tiny

How it works

aka is a tiny portable shell script that gets executed via /bin/sh, sources AKA_FILE and evals the positional parameters, thus applying aliases and whatever other shell script shenanigans stored in AKA_FILE.

For example, this means that you can put something like

alias my='AKA_FILE=~/.my_aliases aka'

in your shell's startup files, and thereafter use the my command as an aka invocation that always sources ~/.my_aliases file in your home directory, regardless of where you run it.

Notes

Initial prototype uses aliases, which may not work for programs that depend on establishing their behavior based on the argv[0] name they were called by (busybox, for example).

If you want to create aliases that include quotes, multiple commands, output redirection, or job control, you either have to escape the special characters like > and & with backslashes when creating the alias, or edit .aka file to add them directly in there:

$ aka alias redir_example echo \"hi\" \> log.txt
$ aka alias double_date date \;  date

In the example above, if we don't put a backslash in front of the semicolon, we will be terminating our aka alias for double_date with just one call do date, and calling the second date command immediately.

And finally, aka stores the aliases using single quotes, so if you have commands where you need to preserve single quotes, you should edit the .aka file by hand to change the alias definition to use double quotes.

https://direnv.net: "direnv is an extension for your shell. It augments existing shells with a new feature that can load and unload environment variables depending on the current directory." The direnv website has links to a half dozen similar projects.

Debian's update-alternatives Debian's alternatives system is similar: it lets you switch out raw XX command for all users, which is why you have to run update-alternatives as root. With aka, you're defining the aka XX commands and they only apply on a per-folder basis (or per AKA_FILE environment variable, if set).

License

aka is distributed under a 3-clause BSD license.

Author

aka was written by Paul Ivanov

https://sr.ht/~pi/aka

Changelog

2020-10-02: initial version

2020-10-03: added AKA_FILE environment variable

2020-10-08: publicly announced

2020-10-28: added notes about multiple commands, redirection, and quotes

Source code

Standard ~40 line aka with comments and usage printing:

#!/bin/sh
# aka, written by Paul Ivanov: https://git.sr.ht/~pi/aka
if [ $# -eq 0 ] ; then
    set -- -h # print usage on 0 arguments
fi

if [ $# -ge 1 ] ; then
    case $1 in
        -h|--help|-help|-H)
echo "Usage: aka alias               show stored aliases"
echo "       aka alias NAME CMD...   store an alias for CMD"
echo "       aka NAME [...]          run CMD, optionally with more arguments"
            exit 0
            ;;
    esac
fi

aliasFile=${AKA_FILE:-./.aka}
# Is this an alias creation? If so, we should have at least 3 arguments,
# such as:
#
# $ aka alias pager more
#    \     \     \    \
#     $0    $1    $2   $3
#
if [ $# -ge 3 ]  && [ $1 = alias ] ; then
    # cmd='pager'
    cmd=$2
    # make 'more' the new $1
    shift 2
    # remove previous aliases for 'pager'
    [ -e "$aliasFile" ] && grep -v "alias $cmd=" "$aliasFile"  > "$aliasFile"~
    echo "alias $cmd='$*'" >> "$aliasFile"~ && mv "$aliasFile"~ "$aliasFile"
    exit 0
fi

# Load up aliases...
[ -e "$aliasFile" ] && . "$aliasFile"

# ...and execute command
eval $*

Tiny ~10 line aka without comments:

#!/bin/sh
# aka, written by Paul Ivanov: https://git.sr.ht/~pi/aka
aliasFile=${AKA_FILE:-./.aka}
if [ $# -ge 3 ]  && [ $1 = alias ] ; then
    cmd=$2
    shift 2
    [ -e "$aliasFile" ] && grep -v "alias $cmd=" "$aliasFile"  > "$aliasFile"~
    echo "alias $cmd='$*'" >> "$aliasFile"~ && mv "$aliasFile"~ "$aliasFile"
    exit 0
fi
[ -e "$aliasFile" ] && . "$aliasFile"
eval $*