How to get the output of a shell function without forking a sub shell?
Asked Answered
A

8

16

I have the following functions.

hello () {
        echo "Hello"
}
func () {
        hello
        echo "world"
}

If I don't want the output of the hello function to be printed but want to do something with it, I want to capture the output in some variable, Is the only possible way is to fork a subshell like below? Is it not an unnecessary creation of a new child process? Can this be optimized?

func () {
        local Var=$(hello)
        echo "${Var/e/E} world"
}
Aciculate answered 21/9, 2011 at 16:3 Comment(2)
It may be a creation of a subshell, but why is it a problem? Are you sure you're not optimizing prematurely?Newsmonger
@evilotto It may be a problem if function has side effects besides outputting to stdout: for example, changing variables.Alt
M
5

An ugly solution is to temporarily replace echo so that it sets a global variable, which you can access from your function:

func () {
  echo () {
    result="$@"
  }
  result=
  hello
  unset -f echo
  echo "Result is $result"
}

I agree it's nasty, but avoids the subshell.

Mail answered 21/9, 2011 at 16:39 Comment(1)
I was also thinking on similar lines. I can replace all echo with Echo which does something like this. Will this make the shell functions faster?Aciculate
C
3

How about using a file descriptor and a Bash here string?

hello () {
    exec 3<<<"Hello"
}

func () {
    local Var
    exec 3>&-
    hello && read Var <&3
    echo "${Var/e/E} world"
    exec 3>&-
}

func
Canteen answered 22/9, 2011 at 18:22 Comment(2)
Bash cryptics are practically endless,so4the~99% of us who even as long-time Bash coders NOT know-else-memomorize 100% of Bash docs, IN ENGLISH,"recode called fn so all its output is to a file-descriptor OTHER than stdout then manipulate that"(advanced!),per relevant OFFICIAL URLs FOR ADVANCED Bash ops gnu.org/software/bash/manual/html_node/… & gnu.org/software/bash/manual/html_node/… tho no find defined there quote(>&-) tho I do at tldp.org/LDP/abs/html/io-redirection.html#CFDEagleeyed
--Advanced! But is there a way (put in another answer here) to do this as the asker apparently asked(I really want,too): WITHOUT having to recode the function being called(so say in this case function remains hello(){ echo "Hello" })? -but still NOT creating a sub-shell(so then allowing the fn to do visible environment side-effects&more benefits),as called fn's stdout still redirected to this other file-descriptor. I made attempts such as my (internal)ID N6ZPBM where quote(exec hello 1>&3) (see my last cmt for official defs of these ops),but sadly these just give quote(3: Bad file descriptor).Eagleeyed
G
3

You can make the caller pass in a variable name to hold the output value and then create a global variable with that name inside the function, like this:

myfunc() { declare -g $1="hello"; }

Then call it as:

myfunc mystring
echo "$mystring world" # gives "hello world"

So, your functions can be re-written as:

hello() {
    declare -g $1="Hello"
}

func() {
    hello Var
    echo "${Var/e/E} world"
}

The only limitation is that variables used for holding the output values can't be local.


Related post which talks about using namerefs:

Gamy answered 4/3, 2018 at 22:31 Comment(0)
L
2

Not a bash answer: At least one shell, ksh optimises command substitution $( ... ) to not spawn a subshell for builtin commands. This can be useful when your script tends to perform a lot of these.

Leotaleotard answered 4/3, 2018 at 23:26 Comment(5)
bash appears to do the sameFloatfeed
@LaurenceRenshaw In bash 4.4.12(1) on linux, echo $(echo foo) calls clone(), according to strace. That makes me think it does spawn a subshell. Do you have evidence that it doesn't?Kanarese
It is a child process, but not a full sub-shell. The child does not call exec(), so does not incur the full overhead of a sub-shell, but it is still a separate process.Marriage
@AndrewVickers The ksh93 shell does not always fork for $( ... ), so no new process is created.Leotaleotard
@Hank. Good to know for ksh, but apparently not the case for bash.Marriage
M
1

Do you have the option of modifying the hello() function? If so, then give it an option to store the result in a variable:

#!/bin/bash

hello() {
  local text="hello"

  if [ ${#1} -ne 0 ]; then
    eval "${1}='${text}'"
  else
    echo "${text}"
  fi
}

func () {
  local var     # Scope extends to called functions.
  hello var
  echo "${var} world"
}

And a more compact version of hello():

hello() {
  local text="hello"
  [ ${#1} -ne 0 ]  && eval "${1}='${text}'" || echo "${text}"
}
Marriage answered 18/10, 2019 at 19:55 Comment(2)
Actually, why even bother with -v. If there is a parameter, we can assume it is the variable name. Edited accordingly.Marriage
Fantastic! This works in a primitive BusyBox interpreter too.Trilingual
T
1

This doesn't literally answer the question, but it is a viable alternate approach for some use cases...

This is sort of a spin off from @Andrew Vickers, in that you can lean on eval.

Rather than define a function, define what I'll call a "macro" (the C equivalent):

MACRO="local \$var=\"\$val world\""

func()
{ 
    local var="result"; local val="hello"; eval $MACRO; 
    echo $result; 
}
Trilingual answered 21/10, 2019 at 15:1 Comment(0)
V
1
  1. Redirect the stdout of the function to the FD of the write end of an "automatic" pipe. Then, after the (non-forking) call, ...
  2. Read the FD of the read end of the same pipe.
#!/usr/bin/env bash
# This code prints 'var=2, out=hello' meaning var was set and the stdout got captured
# See: https://mcmap.net/q/734027/-how-to-get-the-output-of-a-shell-function-without-forking-a-sub-shell

main(){
  local -i var=1             # Set value
  local -i pipe_write=0 pipe_read=0  # Just defensive programming
  create_pipe                # Get 2 pipe automatic fd, see function below

  # HERE IS THE CALL
  callee >&"$pipe_write"     # Run function, see below

  exec {pipe_write}>&-       # Close fd of the pipe writer end (to make cat returns)
  local out=$(cat <&"$pipe_read")  # Grab stdout of callee
  exec {pipe_read}>&-        # Just defensive programming
  echo "var=$var, out=$out"  # Show result
}

callee(){
  var=2       # Set an outer scope value
  echo hello  # Print some output
}

create_pipe(){
  : 'From: https://superuser.com/questions/184307/bash-create-anonymous-fifo
    Return: pipe_write and pipe_read fd => to outer scope
  '
  exec 2> /dev/null  # Avoid job control print like [1] 1030612
  tail -f /dev/null | tail -f /dev/null &
  exec 2>&1

  # Save the process ids
  local -i pid2=$!
  local -i pid1=$(jobs -p %+)

  # Hijack the pipe's file descriptors using procfs
  exec {pipe_write}>/proc/"$pid1"/fd/1
  # -- Read
  exec {pipe_read}</proc/"$pid2"/fd/0
  
  disown "$pid2"; kill "$pid1" "$pid2"
}

main

Note that it would be much shorter code using an automatic normal fd as follows:

exec {fd}<> <(:)

instead of using the create_pipe function as this code does (copying this answer). But then the reading FD line used here like:

local out=$(cat <&"$fd")

would block. And it would be necessary to try reading with a timeout like the following:

local out=''
while read -r -t 0.001 -u "${fd}" line; do
  out+="$line"$'\n'
done

But I try to avoid arbitrary sleeps or timeouts if possible.\ Here the closing of the FD of write end of the pipe makes the read cat line returns at the end of content (magically from my poor knowledge).

Velar answered 10/1, 2023 at 3:21 Comment(0)
J
1

You can use the buildin mapfile:

hello > foo     # put the output of hello into file 'foo'
mapfile Var < foo # put the content of file foo in variable Var

No fork subshell happens here (we checked with strace).

Of course, instead, you have an access (write then read) to the file-system, which may be RAM only if /tmp, and file-cache only in most usecases.

Jeri answered 28/2 at 21:37 Comment(1)
This is the most compact and minimal change solution. I had to use brew to update my Mac's version of bash.Caliber

© 2022 - 2024 — McMap. All rights reserved.