Execute a passed alias inside a function? - bash

Background:
I'm trying make a function that runs commands on a set interval because I don't have access to a "watch" program. Simplified to it's most basic from, the function I'm trying to write is runit() { $1; }.
What works:
This works fine and dandy when I pass it things that aren't aliases. For example, runit "ls -l" works fine. I get the full output from the ls -l command.
What doesn't work:
The problem starts when I pass it an alias. For example, setting alias ll="ls -l" then calling runit "ll" will result in -bash: ll: command not found.
Things I have tried:
When I hard-code the alias runit() { ll; }, it works fine and gives me what I expect.
I feel like I might be overlooking something, but I can't quite place my finger on it.
Why would hard-coding the alias work fine, but passing it into the function fail?
Is there a way to accomplish what I'm attempting to do?

From the bash man page discussion of aliases (emphases mine):
Aliases are expanded when a command is read, not when it is executed.
Therefore, an
alias definition appearing on the same line as another command does not take effect until the next line of input is read. The
commands
following the alias definition on that line are not affected by the new alias. This behavior is also an issue when functions are
executed. Aliases are expanded when a function definition is read, not when the function is executed, because a function
definition is
itself a compound command. As a consequence, aliases defined in a function are not available until after that function is executed.
To
be safe, always put alias definitions on a separate line, and do not use alias in compound commands.
You can observe this effect in functions by using the type command:
$ run_it () { ll; }
$ type run_it
You should see that the body of the function contains a call to ls -l, not ll.
The last sentence of the section on aliases:
For almost every purpose, aliases are superseded by shell functions.
My interpretation of that line is: if you think you want to use an alias, try writing a function first. Don't use an alias unless the function demonstrably fails to do what you need.

You can use eval like this:
$ runit() { eval $1; }
$ alias ll="ls -l"
$ runit "ll"
eval will expand any alias in $1 before the execution.

One way to solve this problem is to define a shell function rather than an alias.
ll () {
ls -l "$#"
}
The alias is expanded as a macro on command input, whereas the shell function is matched when the command is executed. This is a perfect example of how the shell's macro processor language is good for interactive grace but rather complicates actual programming.

Related

Bash: alias in bash -c

To be short:
How could I execute a
bash -c "set -x; alias ll='ls -l -h'; ll"
?
The output is
+ alias 'll=ls -l -h'
+ ll
bash: ll: command not found
Seems bash will add a single quote before my 'll'...
Thanks in advance!
There are two things preventing the alias ll from being expanded:
Aliases are only expanded in an interactive shell or if you use shopt -s expand_aliases.
Aliases are expanded (if they are expanded) when the line containing the command is read, not when it is executed, so only aliases defined on previous lines can be expanded. (Also, bash reads an entire compound command before executing it, so you can't define an alias and use it in the same compound command. That doesn't apply here but it might be useful to know.)
So you need to both mark the bash as interactive and introduce a newline character into the command line. You could, for example, do this:
bash -ic "alias ll='ls -l -h'"$'\n'"ll"
But it would be a lot easier to use a function:
bash -c 'll() { ls -l -h "$#"; }; ll'
All of that is documented in the Bash manual, §6.6 Aliases:
Aliases are not expanded when the shell is not interactive, unless the expand_aliases shell option is set using shopt…
…Bash always reads at least one complete line of input, and all lines that make up a compound command, before executing any of the commands on that line or the compound command. Aliases are expanded when a command is read, not when it is executed. Therefore, an alias definition appearing on the same line as another command does not take effect until the next line of input is read. The commands following the alias definition on that line are not affected by the new alias.
That section ends with the advice to use functions instead:
For almost every purpose, shell functions are preferred over aliases.
To be short:You can't.
According to man bash:
The rules concerning the definition and use of aliases are somewhat
confusing. Bash always reads at least one complete line of input, and
all lines that make up a compound command, before executing any of the
commands on that line or the compound command. Aliases are expanded
when a command is read, not when it is executed. Therefore, an
alias definition appearing on the same line as another command does
not take effect until the next line of input is read. The commands
following the alias definition on that line are not affected by the
new alias. This behavior is also an issue when functions are
executed. Aliases are expanded when a function definition is read,
not when the function is executed, because a function definition is
itself a command. As a consequence, aliases defined in a function
are not available until after that function is executed. To be safe,
always put alias definitions on a separate line, and do not use alias
in compound commands.
For almost every purpose, aliases are superseded by shell functions.
Use functions instead:
bash -c "ll(){ ls -l -h; }; ll"

How to force interpretation as keyword instead of alias on command line?

I have
alias time='/usr/bin/time -v'
configured in my bash environment.
However, I sometimes need an access to keyword time for some one-liners such as
(expensive-command1 & expensive-command2 & time wait); something-that-requires-prev-commands-to-be-complete
Where the wait should obviously wait for both expensive-command1 and expensive-command2 to complete and the keyword time is able to measure the time taken.
However, when I have alias time pointing to actual binary I get following error instead:
/usr/bin/time: cannot run wait: No such file or directory
How to force interpretation keyword for the time in command above? I know that if I run unalias time before running above line it works but then my alias is gone for that shell.
I also know that if I wanted to go from keyword to actual binary, I could use syntax (cmd1 & cmd2 & command time wait). And if I needed built-in command I could use syntax (cmd1 & cmd2 & builtin time wait). However, the time needed here is not command nor builtin. It's a keyword but I cannot figure out how to get the above one-liner to be interpreted as such.
Define a function like below before the line where alias time is defined in your bashrc.
_time() { time "$#"; }
Then you can use _time whenever you need the keyword time.
$ _time() { time "$#"; }
$ alias time='echo foo'
$ time ls
foo ls
$ _time ls
.bash_history .bashrc
real 0m0.024s
user 0m0.010s
sys 0m0.000s
The manual explains why this works as follows.
Aliases are expanded when a function definition is read, not when the function is executed, because a function definition is itself a command.
In an environment where you don't have permission to change bashrc, you can do:
$ shopt -u expand_aliases
$ _time() { time "$#"; }
$ shopt -s expand_aliases
and get the same functionality.
As of Bash version 4.4, there's no nice way to force interpretation as keyword instead of alias. For workarounds, see answer by oguz ismail. In short, all workarounds create alternate name for the original keyword behavior and then you have to always use the alternate instead of the correct keyword.
For now, it seems that the best way to proceed is to consider all bash keywords as reserved words and never ever define any alias with identical name. This is the solution that I'm personally using.

Bash: declaration of an alias to be used in a single lined multiple commands

I would like to do something like
alias myls=ls && myls
This gives me an error
bash: myls: command not found
Strangely, a subsequent
myls
in the same session would be recognized.
Do you know how to declare alias, that is used in the same command sequence?
According to the bash manual:
The rules concerning the definition and use of aliases are somewhat confusing.
Bash always reads at least one complete line of input before executing any of
the commands on that line. Aliases are expanded when a command is read, not
when it is executed. Therefore, an alias definition appearing on the same line
as another command does not take effect until the next line of input is read.
The commands following the alias definition on that line are not affected by the
new alias. This behavior is also an issue when functions are executed. Aliases
are expanded when a function definition is read, not when the function is executed,
because a function definition is itself a compound command. As a consequence,
aliases defined in a function are not available until after that function is executed.
To be safe, always put alias definitions on a separate line,
and do not use alias in compound commands.
For almost every purpose, aliases are superseded by shell functions.
alias has to be used on a separate line so even this won't work:
alias myls=ls; myls
This will result in -bash: myls: command not found error.
However, you can use function and use it on same line:
myls() { ls "$#"; } && myls
anubhava has already provided a better answer, but if for some reason you don't want to use a function instead of an alias then you can do
alias myls=ls && eval myls
Note, however, that usage of eval introduces an extra level of expansion, which may result in unexpected behaviour for more complex commands:
$ echo "A B"
A B
$ eval echo "A B"
A B
$ echo '$SHELL'
$SHELL
$ eval echo '$SHELL'
/bin/bash

Bash variable space in directory path

How can I remove a white space when I use a variable in a directory path.
For example, I'm trying to do
alias test='echo /david/$1'
and when I try
test hhh
this produces
/david/ hhh
with a white space before the variable. It seems very simple but I can't find a solution.
alias doesn't do parameter expansion. At all. Use a function instead.
test (){
echo "/david/$1"
}
man bash:
There is no mechanism for using arguments in the replacement text. If arguments are needed, a shell function should be used (see FUNCTIONS below). [...] For almost every purpose, aliases are superseded by shell functions.
As a part of alias expansion a space is added at the end of the expanded string (otherwise no argument could be added, like alias ll='ls -l'. So ll -a would be expanded to ls -l-a which would be wrong). So I see no solution to this problem anything else then to use function as Ignacio proposed.
Anyway using test as function or alias is not the best thing to do as it is a bash built-in command (if no aliased or named a function). You can check how a mnemonic will be interpreted using the type bash built-in.
I defined an alias and a function called test:
$ type test
type test
test is aliased to `echo /xxx/'
$ type -a test
type -a test
test is aliased to `echo /xxx/'
test is a function
test ()
{
echo "/yyy/$1"
}
test is a shell builtin
test is /usr/bin/test

How can I automatically quote or group commandline arguments for alias in bash?

I have a script that takes a command and executes it on a remote host. It works fine, for example:
$ rexec "ant build_all"
will execute the "ant build_all" command on the remote system (passing it through SSH, etc).
Because I'm lazy, I want to set up an alias for this command (and ultimately, several others), such that, for example, I can just invoke
$ rant build_all
and bash will it will automatically invoke
$ rexec "ant build_all"
I tried doing this with alias, but if I define
alias rant=rexec ant
then any arguments passed to "rant" will just be appended to the end, like so:
$ rant build_all -Dtarget=Win32
(interpreted as:)
$ rexec "ant" build_all -Dtarget=Win32
This fails, because rexec really takes just one argument, and ignores the others.
I could probably do this with a bash wrapper script, but I was wondering if bash had any built-ins for doing this for me, perhaps a named-argument version of alias, or a perl-like quote string command (e.g. qw/ / ), or some such.
For all arguments, this will work.
function rant () {
rexec "ant $*"
}
You may need to adjust the quoting, depending on what arguments you're passing.
I ended up using Ken G's answer, but one better: defining rex as a function, like so:
function rex {
run_remote.sh -c "$*"
}
allowed me to then use rexec in aliases, like this:
alias rant="rex ant"
and still have it wrap the arguments up the way I need them.
I forgot I could use functions like that in bash. This does exactly what I needed, without having to create a wrapper script.
Great tip, thanks!
edit: changed "rexec" to "rex", because I found that my system already had a program called "rexec"
You can do it as a function, not an alias:
function rant { rexec "ant $1"; }
You can call it at the command line like an alias

Resources