122

I'm well aware of the source (aka .) utility, which will take the contents from a file and execute them within the current shell.

Now, I'm transforming some text into shell commands, and then running them, as follows:

$ ls | sed ... | sh

ls is just a random example, the original text can be anything. sed too, just an example for transforming text. The interesting bit is sh. I pipe whatever I got to sh and it runs it.

My problem is, that means starting a new sub shell. I'd rather have the commands run within my current shell. Like I would be able to do with source some-file, if I had the commands in a text file.

I don't want to create a temp file because feels dirty.

Alternatively, I'd like to start my sub shell with the exact same characteristics as my current shell.

update

Ok, the solutions using backtick certainly work, but I often need to do this while I'm checking and changing the output, so I'd much prefer if there was a way to pipe the result into something in the end.

sad update

Ah, the /dev/stdin thing looked so pretty, but, in a more complex case, it didn't work.

So, I have this:

find . -type f -iname '*.doc' | ack -v '\.doc$' | perl -pe 's/^((.*)\.doc)$/git mv -f $1 $2.doc/i' | source /dev/stdin

Which ensures all .doc files have their extension lowercased.

And which incidentally, can be handled with xargs, but that's besides the point.

find . -type f -iname '*.doc' | ack -v '\.doc$' | perl -pe 's/^((.*)\.doc)$/$1 $2.doc/i' | xargs -L1 git mv

So, when I run the former, it'll exit right away, nothing happens.

4
  • Does your complex command work when you pipe to a temp file first and then source it? If not, what's the problem with the generated output? The output of your command won't work if your filenames have spaces in them or if certain sequences aren't escaped properly. I'd want to add quotes around $1 and $2.doc at a minimum. Commented Aug 14, 2009 at 23:22
  • Is there any good reason for having to run this in the original shell ? - these examples doesn't manipulate the current shell so you gain nothing by doing so. The quick solution is you redirect output to a file and source that file though Commented Aug 14, 2009 at 23:43
  • @kaleb the output runs fine. in this particular case, even if i pipe to sh. the file names are space-safe, but thanks for noting. @nos git environment variables on the original shell. and again, these are just examples. the question is for life. Commented Aug 15, 2009 at 9:30
  • source /dev/stdin didn't work for me when needing assigned variables to stick around. geirha on freenode bash pointed me to mywiki.wooledge.org/BashFAQ/024 and suggested I try a process substitution source <(command) which worked for me Commented Jan 30, 2015 at 23:44

9 Answers 9

164

The eval command exists for this very purpose.

eval "$( ls | sed... )"

More from the bash manual:

eval

          eval [arguments]

The arguments are concatenated together into a single command, which is then read and executed, and its exit status returned as the exit status of eval. If there are no arguments or only empty arguments, the return status is zero.

Sign up to request clarification or add additional context in comments.

5 Comments

The only issue here is that you may need to insert ;'s to separate your commands. I use this method myself to work on AIX, Sun, HP, and Linux.
Tanktalus, thanks for that comment, it just made my script work. On my machine eval does not separate commands on newlines and using source does not work even with semi-colons. Eval with semi-colons is the solution. I wish I could give you some points.
@MilanBabuškov: I ranked him up for you ;-)
This is excellent. However, for my use case, I ended up having to put quotes around my $(), like so: eval "$( ssh remote-host 'cat ~/.bash_profile' )" Note that I am indeed using bash 3.2.
This works for me where the accepted answer did not.
104
$ ls | sed ... | source /dev/stdin

UPDATE: This works in bash 4.0, as well as tcsh, and dash (if you change source to .). Apparently this was buggy in bash 3.2. From the bash 4.0 release notes:

Fixed a bug that caused `.' to fail to read and execute commands from non-regular files such as devices or named pipes.

8 Comments

Oh, and since you're listing the shells, it works in zsh too.
I thought this was ingenious until I tried it in msys/mingw (where there is no /dev folder (or even devices)! I tried many combinations of eval, $(), and backticks ``. I couldn't make anything work, so I finally just redirected output into a temp file, sourced the temp file, and the removed it. Basically "sed ... > /tmp/$$.tmp && . /tmp/$$.tmp && rm /tmp/$$.tmp". Anybody got a msys/mingw solution without temp files???
Just a remark: This one does not seem to handle export statements as expected. If the piped string has an export statement, the exportet variable is not available in your terminal environment afterwards, whereas with eval export also works perfectly.
In bash without the lastpipe option set, this creates a subshell just like | bash would
Given that MacOS X usually has bash 3.2 (maybe Yosemite has 4.0?), and the need for lastpipe trickery in my use case (exporting all the variables in a file by pre-processing each line with sed to add 'export ') I chose to go with the eval "$(sed ...)" approach - but thanks for the 3.2 bug heads-up!
|
56

Try using process substitution, which replaces output of a command with a temporary file which can then be sourced:

source <(echo id)

8 Comments

Which kind of sorcery is this?
This was my first thought as well. Though I like the eval solution better. @PauloTorrens <(xyz) simply executes xyz and replaces <(xyz) with the name of a file that will write the output of xyz. It's really easy to understand how this works by doing for example: echo <(echo id), giving the output /dev/fd/12 (12 is an example), cat <(echo id), giving the outputid, and then source <(echo id) giving the same output as simply writing id
@PauloTorrens, this is called process substitution. See linked docs for official explanation, but the short answer is that "<()" is a special syntax that was designed for cases like this in mind.
@PauloTorrens, It is a special syntax just for process substitution.
I'm from the future and this is still the best solution. Clear, short, simple and free of side effects. Specially now that backticks are considered obsolete and anti-pattern.
|
44

Wow, I know this is an old question, but I've found myself with the same exact problem recently (that's how I got here).

Anyway - I don't like the source /dev/stdin answer, but I think I found a better one. It's deceptively simple actually:

echo ls -la | xargs xargs

Nice, right? Actually, this still doesn't do what you want, because if you have multiple lines it will concat them into a single command instead of running each command separately. So the solution I found is:

ls | ... | xargs -L 1 xargs

the -L 1 option means you use (at most) 1 line per command execution. Note: if your line ends with a trailing space, it will be concatenated with the next line! So make sure each line ends with a non-space.

Finally, you can do

ls | ... | xargs -L 1 xargs -t

to see what commands are executed (-t is verbose).

Hope someone reads this!

Comments

9

I believe this is "the right answer" to the question:

ls | sed ... | while read line; do $line; done

That is, one can pipe into a while loop; the read command command takes one line from its stdin and assigns it to the variable $line. $line then becomes the command executed within the loop; and it continues until there are no further lines in its input.

This still won't work with some control structures (like another loop), but it fits the bill in this case.

Comments

8
`ls | sed ...`

I sort of feel like ls | sed ... | source - would be prettier, but unfortunately source doesn't understand - to mean stdin.

2 Comments

after seeing mark4o's answer, doesn't it feel like it was right in our faces all this time?
Heh, yeah. I never remember that that stuff exists.
2

To use the mark4o's solution on bash 3.2 (macos) a here string can be used instead of pipelines like in this example:

. /dev/stdin <<< "$(grep '^alias' ~/.profile)"

1 Comment

or use backticks: . /dev/stdin <<< `grep '^alias' ~/.profile`
1

I think your solution is command substitution with backticks: http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_03_04.html

See section 3.4.5

2 Comments

If you're using bash the $( ) syntax might be preferred. 'Course backticks work in more shells...
$(command) and $((1+1)) work in all posix shells. I think many are put off by vim marking them as syntax errors, but that's just because vim is highlighting for the original Bourne shell which very few use. To get vim to highlight correctly put this in your .vimrc: let g:is_posix = 1
0

Why not use source then?

$ ls | sed ... > out.sh ; source out.sh

1 Comment

He mentioned that temp files are icky.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.