Elegant solution to show bash alias value - bash

Like everyone who uses *nix shells, I have a significant number of aliases and I frequently forget the details of an alias I'm using.
Possibly this is laziness, but reducing keystrokes and command executions increases my productivity. So I'm looking for a clean solution that does the below, for every alias I have defined in my bash4 environment.
Alias defined as:
alias l='ls -laH'
Desired additional alias to be created before entering in the shell:
alias l?='alias l'
Desired results:
$ l?
alias l='ls -laH'
The format is always "{alias_name}?"
The goal is not to have to create an individual "help" alias for every alias. I could iterate, but that seems clunky to me.

Since you are using bash 4, you can take advantage of the command_not_found_handle hook:
command_not_found_handle () {
if [[ $1 =~ .*\? ]]; then
alias ${1%?}
else
printf "Command not found: $1\n"
return 127
fi
}
However, ? is a poor choice for the suffix to use, since it is the shell pattern metacharacter that matches any single character. For example, if there is a file in the current directory that matches l? (say, la), then the shell will attempt to run la rather than l?, and your custom alias hook will not run. You could disable globbing with set -f, but invoking your alias hook with l\? would probably be better.
Another option would be to use #—for "#lias" :)—instead of ? as the suffix to trigger the hook, as # is unlikely to occur in any command name and is not a shell metacharacter.

EDIT: If you're using Bash 4, you can use the command_not_found_handle to do this, as suggested by chepner below. I would accept that answer in lieu of mine.
If you're stuck with Bash <=3, though, I fear the best you can do is iterate, with something like this:
for a in $(alias | sed 's/alias \([^=]*\)=.*/\1/'); do alias "${a}?"="alias '$a'"; done
(Pick your favorite way of extracting the alias names out of the alias output..)
To avoid that, the closest thing I can think of would be to make ? itself an alias (or some other character; as chepner also notes, ? is a single-character wildcard, which can make the contents of your current working directory randomly blow up your alias). You could make it an alias for alias itself, so you could do e.g. ? l instead of l?, with simply alias \?=alias. As you said, autocomplete might make that nicer, which you could do with complete -a \?.

You can just type alias in the command prompt
$ alias l
alias l='ls -laH'
Note: invoking alias without argument will display every alias that is currently defined

I think MarkReed's ? as alias/function solution is a fairly elegant one.
Something like:
alias ?=alias
complete -a ?
or:
?() {
for arg; do
if [ "$(type -t "$arg")" = alias ]; then
alias "$arg"
else
echo "No such alias: $arg"
fi
done
}
complete -a ?
With the function (and bash 4) you could even then keep an aliasinfo associative array with more usage/help/etc. information about each alias and then echo that out too.
?() {
for arg; do
printf %s "${aliasinfo[$arg]:+"$aliasinfo[$arg]"$'\n'}"
if [ "$(type -t "$arg")" = alias ]; then
alias "$arg"
else
echo "No such alias: $arg"
fi
done
}
And then when you define an alias you do something like this:
aliasinfo[l]="Full long directory listing"
alias l='ls -laH'

Related

What is the best way to override an existing shell command?

If one were to have a need to do modify an existing shell command (built-in or otherwise), what is the best way to go about this?
Let me state that I understand there are a number of potential risks involved in attempting a procedure such as this, but that is the very reason I ask this question.
I would like to create a shell function (called ping) to use in place of the common ping command. For the sake of this topic, let's say that it should at the very least be compatible in both Bash and Zsh shell environments.
Specifically, I would like to allow for ping to accept full URLs in the hostname (with protocol, trailing slash/pathname, query parameters, etc). The string manipulation is not what is stumping me, but rather how to properly execute the original ping executable without calling the function something other than ping.
For example, the following two commands should yield the same result:
# EXAMPLE 2 (What `ping` will accept)
ping who.is
# EXAMPLE 1 (What I would like to be able to do with `ping`)
ping https://who.is/my/ping/?example=this
A shim might look like the following (if your real ping is in /usr/bin):
#!/usr/bin/env bash
uri_re='^[[:alnum:]]+://([^/]+)/'
if [[ $1 =~ $uri_re ]]; then
exec /usr/bin/ping "${BASH_REMATCH[1]}" "${#:2}"
else
exec /usr/bin/ping "$#"
fi
Put it somewhere like /opt/overrides/bin, and put that earlier in the PATH than /usr/bin (so PATH=/opt/overrides/bin:/bin:/usr/bin:/usr/local/bin or such).
Or, for a portable function that will work on all POSIX-compliant shells (and zsh as well, even though it doesn't try to be one):
ping() {
local _ping_addr >/dev/null 2>&1 ||: "ignore failure on shells that don't support local"
case $1 in
*://*/)
_ping_addr=${1#*://}
_ping_addr=${_ping_addr%%/*}
shift
command ping "$_ping_addr" "$#"
;;
*)
command ping "$#" ;;
esac
}
I'd like to address #blizzrdof77's original question (paraphrased below):
"I would like to create a shell function (called 'ping') to use in place of the common ping🛈 command." [ℹ]
This drop-in portable shell function [ℹ] works well on Mac & Linux in zsh,bash,sh, and all POSIX-compliant shells.
# ---------------------------------------------------------
# Better `ping` support for various URL formats/protocols
#
# #param $1 - hostname
# EXAMPLE USAGE: `ping http://google.com/`
# ---------------------------------------------------------
ping() {
local pingdomain="$1"
shopt -s nocasematch # allow lowercase
pingdomain=${pingdomain/#*:\/\/} # strip protocol
pingdomain=${pingdomain/#*:*#/} # strip leading 'user:pass#'
pingdomain=$(echo "${pingdomain//"?"//}") # remove '?'
pingdomain="$(echo "$pingdomain" | cut -d/ -f 1)" # clear last '/'
command ping $pingdomain
}

Wildcard that executes command once for each match

Alternate title: How to loop without a loop or xargs.
Recently, I switched to zsh because of its many features. I'm curious: Is there a feature which expands wildcards such that the command is executed once for each match instead of only one time for all matches at once.
Example
The command ebook-convert input_file output_file [options] accepts just one input file. When I want to convert multiple files, I have to execute the command multiple times manually or use a loop, for instance:
for i in *.epub; do
ebook-convert "$i" .mobi
done
What I'd like is a wildcard that functions like the loop so that I can save a few keystrokes. Let said wildcard be ⁂. The command
ebook-convert ⁂.epub .mobi
should expand to
ebook-convert 1stMatch.epub .mobi
ebook-convert 2ndMatch.epub .mobi
ebook-convert 3rdMatch.epub .mobi
...
Still interested in other answers
I accepted an answer that works for me (thanks to Grisha Levit). But if you know other shells with such a feature, alternative commands which are shorter than writing a loop, or even a way to extend zsh with the wanted wildcard your answers are appreciated.
so that I can save a few keystrokes
OK, so let's say you typed out
ebook-convert *.epub .mobi
…and now you realized that this isn't going to work — you need to write a loop. What would you normally do? Probably something like:
add ; done to the end of the line
hit CtrlA to go the beginning of the line
type for i in…
etc…
This looks like a good fit for readline keyboard macro:
Let's write this out the steps in terms of readline commands and regular keypresses:
end-of-line # (start from the end for consistency)
; done # type in the loop closing statement
character-search-backward * # go back to the where the glob is
shell-backward-word # (in case the glob is in the mid-word)
shell-kill-word # "cut" the word with the glob
"$i" # type the loop variable
beginning-of-line # go back to the start of the line
for i in # type the beginning of the loop opening
yank # "paste" the word with the glob
; do # type the end of the loop opening
Creating the binding:
For any readline command used above that does not have a key-binding, we need to create one. We also need to create a binding for the new macro that we are creating.
Unless you've already done a lot of readline customization, running the commands below will set the bindings up for the current shell. This uses default bindings like \C-e ➙ end-of-line.
bind '"\eB": shell-backward-word'
bind '"\eD": shell-kill-word'
bind '"\C-i": "\C-e; done\e\C-]*\eB\eD \"$i\"\C-afor i in\C-y; do "'
The bindings can also go into the inputrc file for persistence.
Using the shortcut:
After setting things up:
Type in something like
ebook-convert *.epub .mobi
Press CtrlI
The line will transform into
for i in *.epub; do ebook-convert "$i" .mobi; done
If you want to run the command right away, you can modify the macro to append a \C-j as the last keypress, which will trigger accept-line (same as hitting Return).
You could checkout zargs in zsh.
This function has a similar purpose to GNU xargs. Instead of reading lines of arguments from the standard input, it takes them from the command line
zshcontrib(1): OTHER FUNCTIONS, zargs
So, we could write:
autoload -Uz zargs
zargs -I⁂ -- *.epub -- ebook-convert ⁂ .mobi
PS: you could find zmv is handy if you need to capture some portions of patterns for building commands.
The for loop has a shortened form that you might like:
for f (*.epub) ebook-convert $f .mobi
You could make yourself a script that does this :
#!/bin/bash
command="$1"
shift
if
[[ $# -lt 3 ]]
then
echo "Usage: command file/blog arg1, arg2..."
exit 1
fi
declare -a files=()
while [ "$1" != "--" ]
do
[ "$1" ] || continue
files+=("$1")
shift
done
if
[ "$1" != "--" ]
then
echo "Separator not found : end file list with --"
exit 1
fi
shift
for file in "${files[#]}"
do
"$command" "$file" "$#"
done
You cal call this like this (assumes the script is called apply_to).
apply_to command /dir/* arg1, arg2...
EDIT
I modified the code to insert filenames at the beginning of the command.

bash to sh (ash) spoofing

I have a 3rd party generator that's part of my build process (sbt native packager). It generates a bash script to be used to run my built program.
Problem is I need to use sh (ash), not bash. So the generator cranks out a line like this:
declare -a app_mainclass=("com.mypackage.Go")
sh chokes on this as there is no 'declare' command.
Clever me--I just added these lines:
alias declare=''
alias '-a'=''
This worked on all such declarations except this one--because of the parens. sh apparently has no arrays.
Given that I cannot practically change the generator, what can I do to spoof the sh code to behaving properly? In this case I logically want to eliminate the parens. (If I do this manually in the generated output it works great.)
I was thinking of trying to define a function app_mainclass= () { app_mainclass=$1; } but sh didn't like that--complained about the (. Not sure if there's a way to include the '=' as part of the function name or not.
Any ideas of a way to trick sh into accepting this generated command (the parens)?
I hesitate to suggest it, but you might try a function declaration that uses eval to execute any assignments produced by a declare statement. I might verify that the generated declare statements are "safe" before using this. (For example, that the assigned value doesn't contain any thing that might be executed as arbitrary code by eval.)
declare () {
array_decl=
for arg; do
# Check if -a is used to declare an array
[ "$arg" = -a ] && array_decl=1
# Ignore non-assignment arguments
expr "$arg" : '.*=.*' || continue
# Split the assignment into separate name and value
IFS='=' read -r name value <<EOF
$arg
EOF
# If it's an array assignment, strip the leading and trailing parentheses
if [ -n "array_decl" ]; then
value=${value#(}
value=${value%)}
fi
# Cross your fingers... I'm assuming `$value` was already quoted, as in your example.
eval "$name=$value"
done
}

Programmatically dereference/resolve aliases in bash

I need to determine which command a shell alias resolves to in bash, programmatically; i.e., I need to write a bash function that will take a name potentially referring to an alias and return the "real" command it ultimately refers to, recursing through chains of aliases where applicable.
For example, given the following aliases:
alias dir='list -l'
alias list='ls'
where my function is dereference_alias,
dereference_alias list # returns "ls"
dereference_alias dir # also returns "ls"
Is there some builtin I don't know about that does this neatly, or shall I resign myself to scraping the output of alias?
Here's a version I wrote that does not rely on any external commands and also handles recursive aliases without creating an infinite loop:
# Arguments:
#
# $1 Command to compact using aliases
#
function command-to-alias()
{
local alias_key
local expansion
local guess
local command="$1"
local search_again="x"
local shortest_guess="$command"
while [[ "${search_again:-}" ]]; do
unset search_again
for alias_key in "${!BASH_ALIASES[#]}"; do
expansion="${BASH_ALIASES[$alias_key]}"
guess="${command/#"$expansion"/$alias_key}"
test "${#guess}" -lt "${#shortest_guess}" || continue
shortest_guess="$guess"
search_again="x"
done
command="$shortest_guess"
done
echo "$command"
}
Here's how I'm doing it, though I'm not sure it's the best way:
dereference_alias () {
# recursively expand alias, dropping arguments
# output == input if no alias matches
local p
local a="$1"
if [[ "alias" -eq $(type -t $a) ]] && p=$(alias "$a" 2>&-); then
dereference_alias $(sed -re "s/alias "$a"='(\S+).*'$/\1/" <<< "$p")
else
echo $a
fi
}
The major downsides here are that I rely on sed, and my means of dropping any arguments in the alias stops at the first space, expecting that no alias shall ever point to a program which, for some reason, has spaces in its name (i.e. alias badprogram='A\ Very\ Bad\ Program --some-argument'), which is a reasonable enough assumption, but still. I think that at least the whole sed part could be replaced by maybe something leveraging bash's own parsing/splitting/tokenization of command lines, but I wouldn't know where to begin.

Shell script : changing working dir and spaces in folder name

I want to make a script that takes a file path for argument, and cds into its folder.
Here is what I made :
#!/bin/bash
#remove the file name, and change every space into \space
shorter=`echo "$1" | sed 's/\/[^\/]*$//' | sed 's/\ /\\\ /g'`
echo $shorter
cd $shorter
I actually have 2 questions (I am a relative newbie to shell scripts) :
How could I make the cd "persistent" ? I want to put this script into /usr/bin, and then call it from wherever in the filesystem. Upon return of the script, I want to stay in the $shorter folder. Basically, if pwd was /usr/bin, I could make it by typing . script /my/path instead of ./script /my/path, but what if I am in an other folder ?
The second question is trickier. My script fails whenever there is a space in the given argument. Although $shorter is exactly what I want (for instance /home/jack/my\ folder/subfolder), cd fails whith the error /usr/bin/script : line 4 : cd: /home/jack/my\: no file or folder of this type. I think I have tried everything, using things like cd '$shorter' or cd "'"$shorter"'" doesn't help. What am I missing ??
Thanks a lot for your answers
in your .bashrc add the following line:
function shorter() { cd "${1%/*}"; }
% means remove the smaller pattern from the end
/* is the patern
Then in your terminal:
$ . ~/.bashrc # to refresh your bash configuration
$ type shorter # to check if your new function is available
shorter is a function
shorter ()
{
cd "${1%/*}"
}
$ shorter ./your/directory/filename # this will move to ./your/directory
The first part:
The change of directory won't be “persistent” beyond the lifetime of your script, because your script runs in a new shell process. You could, however, use a shell alias or a shell function. For example, you could embed the code in a shell function and define it in your .bash_profile or other source location.
mycdfunction () {
cd /blah/foo/"$1"
}
As for the “spaces in names” bit:
The general syntax for referring to a variable in Bourne shells is: "$var" — the "double quotes" tell the shell to expand any variables inside of them, but to group the outcome as a single parameter.
Omitting the double quotes around $var tells the shell to expand the variable, but then split the results into parameters (“words”) on whitespace. This is how the shell splits up parameters, normally.
Using 'single quotes' causes the shell to not expand any contents, but group the parameters togethers.
You can use \ (backslash-blank) to escape a space when you're typing (or in a script), but that's usually harder to read than using 'single quotes' or "double quotes"…
Note that the expansion phase includes: $variables wild?cards* {grouping,names}with-braces $(echo command substitution) and other effects.
| expansion | no expansion
-------------------------------------------------------
grouping | " " | ' '
splitting | (no punc.) | (not easily done)
For the first part, there is no need for the shorter variable at all. You can just do:
#!/bin/bash
cd "${1%/*}"
Explanation
Most shells, including bash, have what is called Parameter Expansion and they are very powerful and efficient as they allow you to manipulate variables nativly within the shell that would normally require a call to an external binary.
Two common examples of where you can use Parameter Expansion over an external call would be:
${var%/*} # replaces dirname
${var##*/} # replaces basename
See this FAQ on Parameter Expansion to learn more. In fact, while you're there might as well go over the whole FAQ
When you put your script inside /usr/bin you can call it anywhere. And to deal with whitespace in the shell just put the target between "" (but this doesn't matter !!).
Well here is a demo:
#!/bin/bash
#you can use dirname but that's not apropriate
#shorter=$(dirname $1)
#Use parameter expansion (too much better)
shorter=${1%/*}
echo $shorter
An alternate way to do it, since you have dirname on your Mac:
#!/bin/sh
cd "$(dirname "$1")"
Since you mentioned in the comments that you wanted to be able to drag files into a window and cd to them, you might want to make your script allow file or directory paths as arguments:
#!/bin/sh
[ -f "$1" ] && set "$(dirname "$1")" # convert a file to a directory
cd "$1"

Resources