All Projects → sitaramc → active-aliases

sitaramc / active-aliases

Licence: GPL-2.0 license
repo is OBSOLETE; see "argmod.mkd" and related files in "notes" repo for updates. Sorry about that!

Programming Languages

perl
6916 projects
shell
77523 projects

You have reached the README for version 2 of active aliases. If you need the old one, checkout the tag "v1". If you need help migrating some of your rules, please email me or open an issue and I will respond as soon as I can.


Problem 1: I have way too many small shell functions and aliases and shell scripts.

Problem 2: in many of them, option/argument validation and processing is a pain, taking up far more code than the real action.



teaser examples

  1. I want an alias wg that force-converts http to https then calls wget:

    wg http://(.*)
        wget https://%1
    

    The url may not be the first argument, so cover that case also

    wg %% http://(.*)
        wget %1 https://%2
    
  2. I often need to pipe something to grep foo | grep bar | grep -v baz (find strings that contain foo AND bar but NOT baz). I want an alias mg, for multi-grep, that does this:

    mg % %%
        sh %aa mg %1 | %aa mg %2
    mg -(.*)
        mg -v %1
    mg
        grep -i
    

    Now ... | mg foo bar -baz will do what I described above.

    If anyone can find a shell script that does this as intuitively/cleanly, I'd like to hear about it.

If you're wondering where I got this terrific (terrible?) idea from... have you ever maintained a sendmail address rewriting ruleset?

active aliases

Anyway, "active aliases" is a somewhat pretentious name I cooked up for a tool that helps me with the problems I listed at the top.

For historical reasons the actual program is called __, but you can rename it to whatever you want!

IMPORTANT NOTE: If you've used the previous version of active aliases, there are several syntactic differences, but the biggest difference, by far, is that execution happens without going through the shell. This means files whose names contain shell meta-characters are safely handled. Read on to see how this can be overridden if you need shell features such as globbing, pipes, redirection, etc.

rule files

"aa" takes a set of rules and a command. The rules are picked up from:

  1. filename given in env var AA_RC, if it is set (see the section on embedding aa for more on this),
  2. $PWD/.aarc, if it exists, and $PWD is a subdirectory of $HOME,
  3. ~/.aarc, if it exists,
  4. and finally, ~/.config/aarc, if it exists. Note this is aarc, not .aarc.

running a command

"aa" can be invoked like __ alias args; for example:

find | __ mg foo bar -baz

But it is more convenient to do it by setting your shell's "command not found" handler to this:

command_not_found_handler () {
        __ "$@"
        exit $?
}
# NOTE: this is for zsh.  In bash this function is spelled without the 'r'
# at the end (i.e., command_not_found_handle) but otherwise it's the same.

With this function defined, you can run an active alias directly, assuming it does not conflict with an existing command, function, or alias. For example

find | mg foo bar -baz

rules: patterns, replacements, and matching

A rule consists of a pattern, followed by one or more replacements to be applied if the command matches the pattern.

The pattern starts at column 1, the replacements at column 5 on the next line. (As a sort of visual syntactic sugar, if you have only one replacement, you can have both the pattern and the replacement on the same line, separated by a hard tab. Look at examples/sf; to my mind it definitely looks better than the standard syntax, because most of the rules in sf have only one replacement).

Here's a quick one: show N most recent git commits, defaulting to 9

# running `glg 5` matches this pattern:
glg %
    # and runs this command, with %1 replaced by 5:
    git log --oneline -%1

# running `glg` (i.e., without any arguments) will not match the previous
# pattern, but will match this:
glg
    # and runs this command
    git log --oneline -9

More than one arguments:

vdd % %
    vim -c 'syntax off' -c 'DirDiff %1 %2'
    # %1 is replaced by the first argument, %2 by the second

Hint for people who know regexes, these are literally matched groups and references to them.

Each % matches one word in the command, but sometimes you need to match an unknown number of words. Here's an example that captures some frequently used find options and makes them easier to type, and also shows the important concept of "the tail":

# comments below refer to running: sf . -type f -s +50M -print

# %% means "match ONE OR MORE words"
# however, this pattern does not match our command; move on to the next one
sf %% -n %
    sf %1 -name %2

# nor does this; move on (remember it's a WORD match, so "-t" does not match "-type")
sf %% -t
    sf %1 -mtime

# this matches.  Specifically, the %% matches ". -type f".  The arguments
# left over after the match ("+50M -print") become what is called the
# "tail", and are implicitly added to the replacement
sf %% -s
    # the %1 is replaced by ". -type f", and the -s becomes -size
    sf %1 -size
    # so the current command is now:
    #   sf . -type f -size +50M -print

# final rule: change the actual command to find and run it.  Notice how in
# this case pretty much the whole command is the "tail"
sf
    find

(Note: see examples/sf for a much more comprehensive example; that's a command I use every day, but I've annotated it for easy understanding.)

You can use your own regexes if you need to; here's one which extracts part of a URL:

    # force wget to use https
    wg http://(.*)
        wget https://%1

Combining this and the previous one about %%, let's cover for the fact that the URL is not always the first argument to wget:

    wg %% http://(.*)
        wget %1 https://%2

execution

After all the rules have been exhausted, whatever remains is executed.

By default, execution is NOT via the shell, but directly.

    wg %% http://(.*)
        # when matched, runs wget, with the rest as arguments
        wget %1 https://%2

This is the default because it lets us stop worrying about filenames with spaces, parentheses, brackets, and assorted nasties.

But sometimes this is a problem:

vpl
    vim *.pl
    # WRONG!  Runs vim with one argument: "*.pl", because there's no
    # actual shell to do the globbing/filename expansion

To make it go via the shell, prefix an sh:

vpl
    sh vim *.pl

Other examples of needing sh (in this case due to pipes and backticks):

rpman %
    # show all man pages within an installed RPM package
    sh man `rpm -qd %1 | grep man.man`

IMPORTANT NOTE: For convenience, aa expands environment variables, ~ if it appears at the start of a word, and $$ (the current process's pid); you only need the sh prefix if you're using shell features other than these.

multiple replacement commands

Earlier, we said "one or more replacements". Here's an alias that prints the description, a blank line, and a columnated list of binaries, from a given RPM package, by running three different commands in sequence:

rpmq %
    rpm -q --queryformat="%{DESCRIPTION}\\n" %1
    echo
    sh rpm -ql %1 | grep -w bin | column
    # need `sh` prefix due to the pipes

conditionals

aa can do a limited form of conditionals. Here's a typical example:

tb
    ? pgrep -x thunderbird
    && echo thunderbird is already running
    || thunderbird

The sequence is important: a ? followed by a &&, then an optional ||. The command prefixed by ? is an external command; the other two are simply replacement commands.

Behaviour is undefined if you do not use this sequence. There is no checking!

Only one of the two replacement commands will be executed. Think of this loosely like a simple if/then/else.

The ? can be combined with sh also:

backup
    ? sh ip ro | grep default.via.172.25
    # we're on a fast connection to the backup server
    && ...full backup...
    # we're on a slower connection
    || ...backup only some directories...

If you need either the "then" or "else" parts to have more than one command, make the replacement command be another rule:

backup
    ? sh ip ro | grep default.via.172.25
    && _full_backup
    || _partial_backup

_full_backup
    ...
    ...
    ...

_partial_backup
    ...
    ...
    ...

special commands

die, exit, exec, cd

Sometimes it's easier to use the special die command:

tb
    ? pgrep -x thunderbird
    && die thunderbird is already running
    thunderbird

die is treated internally, and exits with failure after printing the message. As a result, you don't need || to shield the thunderbird command that follows.

exit is similar to die, but requires a numeric argument, which becomes the exit code for the program.

exec is an interesting command. You don't often need it, but it comes in handy with loops in aa. Here's an example:

avinfo % %%
    avinfo %1
    exec %aa avinfo %2

avinfo %
    ...command(s) for 'avinfo' of one file...

The crux of the looping is the exec %aa. It's pretty similar to "tail recursion" in concept. (The %aa is a special variable that gets replaced by the path to the aa program).

cd is another special command. It is handled internally because it's an important shell builtin and does not make sense to run as an external command.

export and environment variables

export is essentially the same as in shell, except that here it's a command, so you need to use it even when you're re-assigning a value to an existing environment variable. The syntax is export VAR=value (leave out the value to unset a variable).

For example, I have a bunch of things in ~/.local/bin which I don't want in the default path, but would like to invoke them conveniently when I need to. With this:

Local %%
    export PATH=$HOME/.local/bin:$PATH
    %1

I can simply type Local mkdocs, and ~/.local/bin/mkdocs will run.

To test for an env var, it's simplest to drop down to perl:

in_tmux %%
    ? pl $ENV{TMUX}
    && %1
    || die you are not in a tmux session

? sh test -n "$TMUX" also works, but is less efficient.

using captured output in a replacement command

Consider this shell function:

vw {
    vim `which $1`
}

You can that in aa also:

vw %
    sh vim `which %1`
    # note the 'sh' in the above line

But we'd like to avoid the shell as much as possible. So we do this:

vw %
    ! which %1
    vim %!

The ! tells aa to run the command after it immediately, and capture its output. In subsequent replacement commands, this is available as the special variable %!.

Now, that seems like a pretty useless thing, because you could just do it the old-fashioned way (modulo shell-unsafe filenames), but the real use of this comes in when the output can be used in further transformations.

backup
    ! hostname -s
    backup_%!

backup_sitaram-home-lt
    ...commands to backup home laptop...

backup_sitaram-work-lt
    ...commands to backup work laptop...

The ! can be combined with sh too. Here's one way to edit all files within the current directory which match a given pattern:

vd %
    ! sh find . -name .git -prune -o -type f -print | grep -i %1
    vim %!

IMPORTANT NOTE: when the ! command returns multiple lines, each line becomes a separate argument, and %! interpolates them properly (i.e., as separate arguments). In the example above, if there were files whose names contained spaces or other shell metas, it would still work fine.

other features

This section covers some less often used features, and maybe some tips and tricks, idioms, etc. I'll keep adding to this as I find suitable examples.

$ in a pattern

Look at this example again:

glg %
    git log --oneline -%1
glg
    git log --oneline -9

The git log command is repeated, which is not ideal; if its options change, you have to change them in two places).

This is much better:

glg $
    glg 9
glg %
    git log --oneline -%1

The $ says "no more arguments", so it matches exactly glg.

error handling

What if the user types in glg foo? You get fatal: unrecognized argument: -foo, which is not very informative, mainly because you made git report an error which your alias should have caught!

Now try this:

glg $
    glg 9
glg ([0-9]+)
    git log --oneline -%1
glg
    die argument must be numeric:

Now walk through that mentally, with glg foo as the command+argument.

SIDE NOTE: There's a lot of thought given to "where should error checking go", in many programming language designs. For example, the above function, in bash, would require you to deal with errors first, and then move on to the real work.

With aa, it is often more convenient and intuitive to deal with all the valid cases first, especially in short, simple, scripts that you'd like to get done quickly.

tail, %@, and %.

You've seen the tail already in the sf (find command) example earlier.

By default, the tail arguments are placed at the end, but that can be overridden:

save
    # explicitly place the tail arguments using %@
    cp %@ ~/.save
    du -sm ~/.save

The tail gets added to EVERY replacement command except cd, exit, export, and skip. This can cause... surprises!

save
    cp %@ ~/.save
    du -sm ~/.save
    # tail arguments implicitly added to the "du" command also

You can prevent that:

save
    cp %@ ~/.save
    du -sm ~/.save %.
    # ending with special variable %. explicitly disables adding tail

But honestly, if the tail arguments are NOT optional (as in this case), it's best to be explicit:

# explicit %%
save %%
    # %1 instead of %@
    cp %1 ~/.save
    # no need for %. at the end
    du -sm ~/.save

The tail is really only useful when trailing arguments are optional. That is, there may not be even one, so you can't use %% (remember, %% means "one or more words"), like in the sf example earlier.

more examples

This section is for real examples that I use.


I often want to know which (rpm) package a particular file or command belongs to. What I want is something like this:

$ qf /etc/pinforc
pinfo-0.6.10-17.fc28.x86_64

except that for commands, I don't even want to supply the full path. For example, I found a program called "ab", and was curious what it was:

$ qf ab
httpd-tools-2.4.34-3.fc28.x86_64

Here's the aa code:

qf (.*)/(.*)
    rpm -qf %1/%2
qf % $
    ! which %1
    rpm -qf %!

Until ripgrep becomes as common as grep, when I'm on strange machines where I can't install system utilities, I will have to rely on grep, egrep, etc. Unfortunately, grep/egrep do not have an "rc" file to store commonly used options, but even if it did, it could not do the two things I really want: smartcase, and automatic recursion into $PWD when STDIN is not a pipe.

So, here's a simple egrep wrapper that captures my simple needs.

ew (.*[A-Z].*)
    _ew %1
ew %
    _ew -i %1

_ew %%
    ? test -t 0
    && ew -r %1
    || ew %1

ew
    grep -E -D skip --exclude-dir=.git --color=auto -I

The first 4 lines show a common idiom in aa: jumping over some code based on some condition. Here, if the first arguments contains at least one uppercase letter, we replace the ew command with _ew. This means the second pattern and its replacement command don't match, and so are skipped.

On the other hand, if the first argument did NOT have any uppercase letters, then the first pattern and its replacement are skipped. The second one matches, so the replacement now includes a -i (for "ignore case" in grep).

At this point, the command is either _ew string or _ew -i string, depending on whether the string had an uppercase letter in it or not.

The next few lines enables recursion if STDIN is not a pipe, using a conditional, which you've already seen above.

IMPORTANT NOTE: although the || part is a no-op, you do need it, since you have to specify a replacement command for the else case also.

Notice also that the command is now back to ew from being (temporarily) _ew.

SIDE NOTE: when you feel tempted to use a shell conditional, like ! sh [ ... ], use the test command instead. There are subtle differences between them, but by and large they're the same, and running the test command is much more efficient than starting up a full shell.

appendix 1: embedding aa rules in a normal script

It is possible to get the benefit of aa argument parsing within a shell script. See examples/sf for a good example.

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].