To get the output of a command, you need to read it somehow. type
writes it on its stdout. And we need somehow to get that and pass it to the [
command.
$(...)
uses a pipe for that. But for a pipe, you need a writer and a reader process, so you'll have to fork a process even for running a command that is builtin. You can try reading and writing to a pipe in the same process but that's usually prone to deadlocks as the writing can block if nobody is reading (though you need the pipe buffer to get full for that to happen which is unlikely for type
).
You could do it with the yash
shell that has a raw interface to pipe()
:
{
type echo >&3
echo 3>&- # close the writing end so the reader can see an eof
IFS= read -r answer <&4
} 3>>|4
Above, you'd get a deadlock if type
's output was larger than the pipe buffer size (64KiB by default on modern versions of Linux).
With bash
, you could always do:
type -t echo > file
IFS= read -rd '' type < file
if [ "$type" = builtin ]...
But while that avoids the subshell, that means littering the file system with that file
.
Here type
is a builtin. Its output is generated by the shell and it's true it feels a bit silly to have to fork a process to be able to use that output in the shell.
Some shells (ksh93
and fish
) do implement some optimisations there. In their $(type echo)
((type echo)
in fish
), they actually fake the writing of the output and the reading of it (FreeBSD sh
also does it for single builtin invocations (like type
here)). When a builtin's stdout is a command substitution, instead of writing the output, the shell just appends the would-be-output text to the command substitution result and there's no need for a fork.
Actually, fish
's (type echo)
is more like ksh93
's ${ type echo;}
in that it doesn't even create a subshell environment. With $(...)
, ksh93
emulates a subshell environment so that it appears as though a child process was forked to interpret the code in it and doesn't do that for its ${ ...;}
variant.
ksh93$ a=1
ksh93$ echo "$(a=2; type echo) $a"
echo is a shell builtin 1
ksh93$ echo "${ a=3; type echo;} $a"
echo is a shell builtin 3
fish> set a 1
fish> echo (set a 2; type echo) $a
echo is a builtin 2
You'll find that in some shells that don't do that optimisation, a lot of the builtins can be called in such a way where instead of writing their result, they store it in a variable.
The most obvious ones are the standard read
and getopts
that do that by default (you do IFS= read -r var
instead of var=$(line)
). bash
and zsh
also have printf -v variable format args
. zsh
can do the same for its stat
, strftime
... builtins.
Some shells also make some information already available automatically in some special variables, like ksh
's $SECONDS
and $RANDOM
found in a few other shells (and standard ones like $-
, $#
(equivalent to fish
's (count $argv)
for instance)).
In zsh
, that's generalised to most of the shell's internal information so you almost never need to use command substitution on the output of a builtin. For instance, it's got associative arrays for the list of builtins, keywords, commands...
if (($+builtins[echo])); then
echo echo is a builtin
fi