12

Let me explain with an example:

I have a line where I declare an alias in my ~/.bashrc:

> grep lsdir ~/.bashrc

alias lsdir='ls -d */'

I have just added this line to my bashrc, and the alias is thus not yet added to my current session environment. I would also like not to re-source all my bashrc for some configuration reason, which is why I would like to simply run this grepped line on its own.

For the purpose of curiosity and education, I tried to do it without having to write it down manually, to no avail.

I tried this:

> $(grep lsdir ~/.bashrc)
bash: alias: -d: not found
bash: alias: */': not found

or this:

> "$(grep lsdir ~/.bashrc)"
bash: alias lsdir='ls -d */': No such file or directory

or even this:

> $("grep lsdir ~/.bashrc")
bash: grep lsdir ~/.bashrc: No such file or directory

But none worked. Is there any way to achieve this ?

adamency
  • 388
  • 4
    While the answers below are better answers to your question, it's dangerous to execute code you don't actually inspect. Can I suggest creating a separate file $HOME/.bash_aliases, and put your aliases in there, and source it inside your .bashrc; then when you need to update aliases, source only that file. You could even do alias updatealiases='source $HOME/.bash_aliases'. – frabjous Mar 28 '22 at 18:39
  • 3
    @frabjous: while I agree with what you said the code in your .bashrc is going to be executed with each new session whether you inspect it or not, and I'm sure most people aren't inspecting every line of their .bashrc that often. – jesse_b Mar 28 '22 at 18:46
  • 4
    @jesse_b You mean I'm the only one who changes it three times a day? LOL, but in all seriousness, we're talking here about sourcing parts of your .bashrc blindly, and those parts do not necessarily work the same as they do at start up. What if in the original .bashrc there's code inside a conditional which is no longer in a conditional when pulled out with grep, or uses a variable that is defined differently in the interactive shell? – frabjous Mar 28 '22 at 19:56
  • 1
    @frabjous: fair point – jesse_b Mar 28 '22 at 20:00
  • 3
    This is the same issue as in How can we run a command stored in a variable?. That is, the string you get from the expansion, alias lsdir='ls -d */', gets word-split to alias, lsdir='ls, -d, */' which isn't what you want, but more like running the command line alias "lsdir='ls" -d "*/'". (And the */' gets expanded to any matching filenames if there are any, and if there aren't failglob or nullglob will trigger.) – ilkkachu Mar 29 '22 at 13:15
  • I don't agree with @frabjous's point: 1. I feel that your argument about code execution without prior inspection is moot given your solution as it is implied in my question that I have ran and read the grep output before executing it, and if you want to say that the .bashrc may have changed between both execution, your .bash_aliases may perfectly have been changed aswell between your both source-ings. 2. As stated in the question, the main purpose of the question was to discuss if the operation was possible, and if so, how to do it, and it was not meant to be used in an automated process. – adamency Mar 30 '22 at 18:00

4 Answers4

23

You could use Process Substitution to source just the matching lines:

source <(grep lsdir ~/.bashrc)
jesse_b
  • 37,005
13

I found the answer:

The key is using the builtin eval. Doing this will achieve the wanted behavior (more precisely running the command in the actual shell we're logged in, instead of a subshell):

> eval "$(grep lsdir ~/.bashrc)"
> type lsdir
lsdir is aliased to `ls -d */'
ilkkachu
  • 138,973
adamency
  • 388
  • 6
    You should double-quote the command substitution (i.e. eval "$(grep lsdir ~/.bashrc)") -- without that, the command goes through word splitting and wildcard expansion before it's evaled, which can have really weird effects. – Gordon Davisson Mar 28 '22 at 19:12
  • 2
    though word splitting is a bit weird here, since eval does join any arguments it gets with spaces before running the command, so it seems it would work. But it'll fall down with something like eval $(printf 'echo foo\necho bar\n') where the newline gets turned into a space and the whole thing runs as one command, instead of two as it would if the middle newline was intact. – ilkkachu Mar 29 '22 at 13:08
  • Thanks @ilkkachu for mentioning an actual case where double quotes are needed for it work. (I was not sure about Gordon's point as my command worked without it) – adamency Mar 30 '22 at 17:46
  • @adamency, or anything with glob characters or multiple consecutive spaces, e.g. eval "$(printf 'echo "1 * 2"')" – ilkkachu Mar 30 '22 at 18:31
10

While existing answers provide viable solutions, none of them explains why the original command $(grep lsdir ~/.bashrc) didn't work.

In fact, the shell does a limited amount of work on commands during substitution: you get the word splitting, but no quote handling:

pi@raspberrypi:~ $ echo 'a'
a
pi@raspberrypi:~ $ echo "echo 'a'"
echo 'a'
pi@raspberrypi:~ $ $(echo "echo 'a'")
'a'

As you can see in the last line, echo 'a' was correctly split in the command (echo) and the argument ('a'), but the argument itself didn't undergo quote processing, so quotes were kept intact. BTW, pretty much the same thing happens when you try to store a shell command in a variable.

In your case, alias lsdir='ls -d */' got split into the command, alias, its argument, lsdir='ls and two additional arguments (-d and */') that alias didn't expect.

One natural solution allowing to re-run all command processing (including quote handling) is to use eval, and one of the answers already mentions.

-1

you can just use a variable as the output for that

torun=$(some commands | adapting/parsing results #be aware of multiline returns too)
${torun} # call the generated command

As mentionned here due to discussion, & good idea in most cases you have to add a subcommand using a pipe for parsing/parsing the result of the command to avoid escape characters or quotting issues when appling the ${torun} variable as a command

in brief : do not forget to make the string become a usable command inside the variable definition line.

typical :

torun=$( command | sed '....;.....;....' ) #or awk or any string management tools
francois P
  • 1,219
  • 3
    This doesn't parse quotes, escapes, etc (the same problem that $(grep lsdir ~/.bashrc) had). – Gordon Davisson Mar 28 '22 at 19:14
  • this only depends how you adjust the result inside the variable=$(...) command to avoid this problem adding for example a | sed patternmanagement – francois P Mar 29 '22 at 08:16
  • 1
    No, adjusting the result won't help because the shell parses quotes and escapes (and most other shell syntax) before expanding variables, so putting quotes, escapes, etc in variables doesn't do anything useful. See "Why does shell ignore quoting characters in arguments passed to it through variables?" – Gordon Davisson Mar 29 '22 at 08:43
  • of course it does if you do it has it needs to, the method is used in many many contexts/client companies, in prod since years, I m really experimented in that method and the eval method so far, the rare context this cannot be applied, method is replaced by a script generator & calling it. but trust me I really use since years thoses methods in prod in many many different clients architectures. – francois P Mar 29 '22 at 09:31
  • 2
    Try it with something that prints alias lsdir='ls -d */' (the same command as in the original question). You will get the same errors (bash: alias: -d: not found and bash: alias: */': not found); then run alias lsdir and you'll see a garbled mess: alias lsdir=''\''ls' – Gordon Davisson Mar 29 '22 at 10:21
  • because you do not treat the character string before assigning it in to the variable to avoid that just like I said before ; and YES using ALIAS it will fail ! I never said no to that point and this is exactly why you absolutly need to use a variable & not an alias & even with a variable have to treat the string to build a working one. do not compare 2 differents things having 2 different results :) – francois P Mar 29 '22 at 10:31
  • 1
    This won't work. It's the same issue as in the question, you only get a single string from the command substitution, and passing it through a variable doesn't change that. See How can we run a command stored in a variable? for how it fails. Or just try something like echo "foo bar" vs. x=$(printf 'echo "foo bar"'); $x. Or just the case in the question. – ilkkachu Mar 29 '22 at 13:13
  • Any talk about "adjusting the string" would involve parsing any quotes in the output of the command substitution in the same way the shell would parse them, which is just a pain to do (and unnecessary reimplementation). And even if you did that, glob characters would be an issue, as would shell operators like && etc. You can't get them to work this way. Which you would have seen if you'd actually attempted to do the work, instead of just skimming over it as if it was trivial... – ilkkachu Mar 29 '22 at 13:59