4

I'm after THE proper way to see if a function is defined or not. A POSIX compliant way.

__function_defined() {
    FUNC_NAME=$1
    d=$(declare -f $FUNCNAME)

    if [ "${DISTRO_NAME_L}" = "centos" ]; then
        if typeset -f $FUNC_NAME &>/dev/null ; then
            echo " * INFO: Found function $FUNC_NAME"
            return 0
        fi

    # Try POSIXLY_CORRECT or not
    elif test -n "${POSIXLY_CORRECT+yes}"; then
        if typeset -f ${FUNC_NAME} >/dev/null 2>&1 ; then
            echo " * INFO: Found function $FUNC_NAME"
            return 0
        fi
    else
        # Arch linux seems to fall here
        if $( type ${FUNC_NAME}  >/dev/null 2>&1 ) ; then
            echo " * INFO: Found function $FUNC_NAME"
            return 0
        fi
    echo " * INFO: $FUNC_NAME not found...."
    return 1
}

All of the above are considered bash'isms according to debian's checkbashisms script.

Trying to grep the script is also not ok. for example:

    if [ "$(grep $FUNC_NAME $(dirname $0)/$(basename $0))x" != "x" ]; then
        # This is really ugly and counter producing but it was, so far, the
        # only way we could have a POSIX compliant method to find if a function
        # is defined within this script.
        echo " * INFO: Found function $FUNC_NAME"
        return 0
    fi

won't work because the script is also supposed to work like:

wget --no-check-certificate -O - http://URL_TO_SCRIPT | sudo sh

So, what's the proper, POSIX compliant way to do this?

Plain sh please, no bash, no ksh, no other shell, just plain sh. Oh, and without actually trying to run the function too :)

Is it possible?

I have found a POSIX compliant solution:

if [ "$(command -v $FUNC_NAME)x" != "x" ]; then
    echo " * INFO: Found function $FUNC_NAME"
    return 0
fi

The question now, Is there a better solution?

s0undt3ch
  • 755
  • 6
  • 12
  • 1
    defined in a script(file), but not loaded into a running process yet? OR defined in a running process script, the code possibly being loaded from an external file that was sourced or in-line per the program? Are you sure Bourne shell (.sh) is **the** "POSIX shell"? I don't think it is. Good luck. – shellter Jan 22 '13 at 03:42
  • @shellter, you are correct, the Bourne Shell is not the POSIX shell, my mistake.The POSIX shell is a superset of the initial Bourne Shell. I think my mistake came from the notion that the POSIX shell seems to now be **the** `sh` shell, ie, all superset shells like bash, etc, are supposed to follow that standard. – s0undt3ch Jan 22 '13 at 14:14
  • @shellter I ended up not answering your questions. The functions are defined within the script file itself. So I need to know what functions are defined on my script. **Yes**, that **is** what I'm after. I know what functions are there, but, most importantly, the script needs to know too. – s0undt3ch Jan 22 '13 at 14:20

4 Answers4

3

I have found a POSIX compliant way

if [ "$(command -v $FUNC_NAME)x" != "x" ]; then
    echo " * INFO: Found function $FUNC_NAME"
    return 0
fi

The question now, Is there a better solution?

s0undt3ch
  • 755
  • 6
  • 12
  • 2
    This does not distinguish between $FUNC_NAME being a function, an alias, a command, or a shell keyword. – William Pursell Jan 22 '13 at 23:38
  • @WilliamPursell although it did solve my problem, you are correct, no actual type check is done. It only checks for it's existence. I think using `-V` instead of `-v` also shows how what we're checking is defined, and then we can type check that. – s0undt3ch Jan 23 '13 at 14:40
  • While `type` is indeed an `XSI` extension (not a bashism by any means) note that `command` is POSIX but an optional (!) feature as well, so in practice not necessarily more portable. For example, `posh` implements neither. I might go as far as testing if `type` is available, and if it is not define a function `type` that uses `command -V` as its backend just to "try really hard", but in case you have neither I would argue that you are not really in a sane environment and then, from my current understanding, you are out of luck anyway. – Adrian Frühwirth May 11 '13 at 08:30
  • Thanks Adrian. So far, I have not found a distribution for which my solution was not applicable, but I'll stay alert for that `posh` implementation. – s0undt3ch May 11 '13 at 12:47
  • use `if test "$(command -v ${FUNC_NAME-})x" != "x"; then` if running in strict mode – balupton Nov 03 '21 at 06:26
1

This answer isn't POSIX so won't compete with the accepted answer, however can be a FWIW/TDIL for when you just need something for specific shells.

In zsh you can do test "$(whence -w name)" = 'name: function'.

In bash you can do test "$(type -t name)" = 'function'.

Extra details for bash:

function fn () {
  echo 'my function'
}

if test "$(type -t fn)" = 'function'; then
  echo 'fn is a function'
else
  echo 'fn is not a function'
fi

if test "$(type -t missing-fn)" = 'function'; then
  echo 'missing-fn is a function'
else
  echo 'missing-fn is not a function'
fi

You can learn more about bash bultins by typing help in your prompt, then help type and help test.

type -t name can also tell whether name is all sorts of things, including a builtin, a file, or as demonstrated a function.

If you only care that a variable or function exists, you can use:

if test -v name; then
  echo "name is present: $name"
else
  echo "name is missing"
fi
balupton
  • 47,113
  • 32
  • 131
  • 182
  • 1
    `-t` argument to `type` is not POSIX https://pubs.opengroup.org/onlinepubs/009696699/utilities/type.html . `test -v` is not POSIX - `test` is a separate program, it can't know about shell variables. And anyway, `-v` is not POSIX. https://pubs.opengroup.org/onlinepubs/9699919799/utilities/test.html . `help` is specific to bash, I don't know about other shells. Aaaanyway, using `type` is _the_ method, assuming `type` outputs something sane, I would go with `case "$(type fn)" *"function"*)`. – KamilCuk Sep 02 '21 at 20:13
  • @KamilCuk thanks, I've amended it, also thanks for the resources and advice. – balupton Sep 02 '21 at 20:21
  • I'm sorry ;) `in sh, and bash` There is no `sh`, I doubt you tested it on _the_ Bourne shell (I see on wiki last from 1989 SVR4). On nowadays systems `sh` is a link - to `bash` or to `dash`(ubuntu) or to `ash`(alpine). For example, on `ash`, `type -t` does not work as far as I can tell. – KamilCuk Sep 02 '21 at 20:23
  • @KamilCuk right again, on Manjaro: `sh --version` results in `GNU bash, version 5.1.8(1)-release (x86_64-pc-linux-gnu)` – balupton Sep 02 '21 at 20:25
1

The following is POSIX compliant should work possibly everywhere:

checkfunc() {
   case "$(type "$1" 2>/dev/null | head -n1)" in
   *" function"|*" function from zsh") return 0; ;;
   esac
   return 1
}
# Tested with:
# My shell is bash
script="$(declare -f checkfunc)"'
if checkfunc checkfunc; then :; else echo $0:error:not function; exit 1; fi
if checkfunc ls; then echo $0:error:executable; exit 1; fi
if checkfunc type; then echo $0:error:builtin; exit 1; fi
if checkfunc does-not-exists; then echo $0:error:not-exists; exit 1; fi
'
docker run --rm bash bash -c "$script"
docker run --rm alpine ash -c "$script"
docker run --rm ubuntu dash -c "$script"
docker run --rm alpine ash -c 'apk add mksh >/dev/null && mksh -c "$1"' - "$script"
docker run --rm alpine ash -c 'apk add zsh >/dev/null && zsh -c "$1"' - "$script"

If I could say, I would say that I also could have sources of SVR4 and patched ksh so it's compilable with current GCC and tested it there too and type could just output <xxx> is a function there, just like ash.

zsh happens to output <func> is a shell function from zsh, so I decided to handle the from zsh specially.

KamilCuk
  • 120,984
  • 8
  • 59
  • 111
-1

While I can understand why you might think so, dynamically pulling your script from another location is no great barrier to parsing (or "grepping") it as you like before you run it. Such a thing can be accomplished in several ways, but, as far as I can make out, they will all require a basic file descriptor for functionally complete operation.

Bash and its ilk provide us the convenience of transparent file descriptors via redirected sub-shelled commands which allow for all kinds of pipeline flexibility like this:

eval "$(wget --no-check-certificate -O - ${_URL} | tee \
    >(read func; echo '_FUNC='"$(grep -Eo '^[^ |^#]*()' <(echo "${func}"))") \
    | sudo sh)" 

But, as has been mentioned already, such syntax devices are not specified by POSIX, and so you must find another way. Also, shorter though it may be, that can be a little confusing. Probably there are much better ways to do that, but I imagine them requiring 2 or 3 subshell levels at least. (It sure would be cool to be shown otherwise, though.)

What POSIX does specify, however, is the very useful heredoc. The heredoc is rare among declarable POSIX variable syntax types not only because it can neatly span multiple lines without escape kludges, or because it allows for clean definition of shell substitution by simply quoting its terminator, but most importantly because it describes a file type handle. This quality of the heredoc is often overlooked, perhaps because Bash and co have largely obviated its importance for some time now, but we should be reminded of it every time we adhere to the convention of terminating a heredoc with the "end of file" acronym EOF.

In order to pull in a script file, modify it programmatically, and then execute it with a shell interpreter you will need a file descriptor. You can read and write to /tmp/ to allow for this, of course, but I generally try to avoid that because it just feels sloppy. You could even, perhaps, rewrite yourself if you must, but it is probably not the best habit to get into. If you find real cause to do this, however, the safest way is probably to define the rewrite code in a function at the beginning of your script and only call it at the very end so you can be sure the entire file has been fully loaded into memory. Something like this:

_rewrite_me() {printf %s "${1}" > ${0} && exec ${0}} 
#SHELL CODE
#MORE 
_NEW_ME="$(printf %s "${_MORE_SHELL_CODE}" | cat <${0})"
rewrite_me ${_NEW_ME}

Still, I like the heredoc best, and I think it is the missing puzzle piece in this case. The following is made short and simple with use of the heredoc, should be fully POSIX compliant, and should easily be adapted to your purposes, I hope:

#!/bin/sh
_URL='http://URL/TO/SCRIPT'

_SCRIPT="$(wget --no-check-certificate -O - ${_URL} |\
    grep -Ev '^#!')" 

_FUNC="$(sed -n 's:^\([^ |^#|^=]*\)().*{.*$:\1:p' <<_EOF_)"
${_SCRIPT}
_EOF_
echo "${_FUNC}"

sh <<_EOF_
${_SCRIPT}
_EOF_
mikeserv
  • 694
  • 7
  • 9
  • @s0undt3ch: Will you review this and at least comment please? This is an actually POSIX-compatible answer to your question. – mikeserv Nov 15 '13 at 11:42
  • This doesn't address the question at all. – reinierpost Jul 01 '19 at 08:38
  • @reinierpost This does partially address the question. It shows two (unreliable) ways to see if a shell script contains function definitions. Yes, OP is obviously interested whether the function is already defined in the already running script, but the question may also be read as "does script s define function f?" mikeserv, it does not actually answer even that question, it stops half-way. OP is looking for a conditional expression. In the question, multiple candidate conditions are shown. This answer could be improved if results like $_FUNC were actually used to test for a specific function. – Rainer Blome Feb 24 '21 at 14:32
  • @RainerBlome I take the question to mean: how can I test within my script whether some word, if I call it at this point, is defined as a function? It doesn't ask *where* the function is defined. E.g. this: `echo 'fun() { echo defined; }' | sh -c 'IsDefined() { ???; }; if IsDefined fun; then echo yes; else echo no; fi'` should print `yes`. – reinierpost Feb 24 '21 at 14:47
  • 1
    @reinierpost I agree wholeheartedly with your reading. I was just pointing out that a different reading is possible, and that this "answer" aims in that other direction, even though at first glance the answer seems like it was completely out of place, intended for a different question, which may or may not exist elsewhere. Also, answerer explicitly asked for review and comment. Since I saw good faith in the answer, and had some fun figuring out what it meant, I provided. – Rainer Blome Feb 25 '21 at 12:23