5

I had thought this would work:

source <(sed '1,/# HELPER FUNCTIONS #/d' "$0")
fun abc

HELPER FUNCTIONS

fun() { echo"$@" }

But now that I think about it, that would bring in the helper function into the current shell after the script is done.

Is there a way to put my helper functions at the end of the script?

Adrian
  • 283
  • 1
    Excellent answer from @Kusalananda below, but I'd like to ask: why you want to do that? To make the script easier to read? In order not to have to scroll to the bottom to do your actual scripting? – markgraf Nov 09 '22 at 08:47
  • @markgraf, to keep the more relevant code near the beginning and the helper functions just details. – Adrian Nov 09 '22 at 18:14
  • suggestion: wrap all the non-helper code in function main { } and then call it at the very end of the script. Then it comes at the top of the script, but gets invoked after all the helpers are created, so it has access to them. – Quelklef Nov 09 '22 at 18:55
  • 1
    Yeah @Quelklef, I already got that. Thx. :D – Adrian Nov 10 '22 at 01:17

1 Answers1

13

Note that your approach actually works, but that the statements declaring the helper functions would be executed twice, once by source and once after running fun abc. I would therefore insert a plain exit statement before the divider.

#!/bin/bash

source <(sed '1,/^# HELPER FUNCTIONS #$/d' "$0") fun ABC

exit

HELPER FUNCTIONS

fun () { echo "$@" }

Also, note that you need to anchor the regular expression, at least to the start of the line, or it will match the line containing the source command, which in turn would cause the top half of the script (sans sed) to be executed by source, which we want to avoid.

Personally, I think this makes the code a bit difficult to read. It also changes some things after the divider. For example, the BASH_SOURCE array would have an extra filename as its first element, BASH_LINENO would be offset by the size of the initial part of the script, and any line numbers (and the script name) reported in error messages from that lower part of the script would be difficult to follow up.


Another way to do this, which does not require $0 to have a sensible value, nor calls sed to modify the script on the fly, is to get into the habit of putting the main body of the script into its own function, possibly called main, and then call that function at the very end of the script:

#!/bin/sh

main () { fun ABC }

fun () { echo "$@" }

Call "main" with the arguments given to the script

(even if the function may not use them).

main "$@"

This means that the script would be read to the end by the shell, all functions would be instantiated, and main could call any function that it may need to do its work, regardless of where in the code those functions are located.

See also the Google style guide for shell scripts, which specifically suggests this, although it says to put the code for the main function last too, which is not strictly necessary. In fact, it may be worth putting it first to make it easy to find.

Related:

Kusalananda
  • 333,661