Skip to content

Instantly share code, notes, and snippets.

@joehillen
Last active December 23, 2022 22:02
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joehillen/30f08738c1c3c0ca3e4c754ad33ad2ff to your computer and use it in GitHub Desktop.
Save joehillen/30f08738c1c3c0ca3e4c754ad33ad2ff to your computer and use it in GitHub Desktop.
Build bash scripts with `source` files into a single script.
#!/usr/bin/env bash
#
# https://gist.github.com/joehillen/30f08738c1c3c0ca3e4c754ad33ad2ff
#
# This script inlines 'source' files.
#
# For long scripts, it is nice to be able to break them into multiple files
# to make them easier to work with but still release as a single script.
#
# Inspired by https://stackoverflow.com/a/37533160/334632 with the following enhancements:
# - supports sourcing files with quotes, spaces, and ~.
# - supports sources in sources
# - inline scripts from $PATH
# - errors on (mutual) recursion
# - preserves indentation
# - omits shebangs in sourced files
#
# This will NOT work with variables in source paths.
#
# WARNING MacOS users: Requires a modern version of Bash
set -e
set -o pipefail
declare -A sourced
function _inline_sources {
while IFS='' read -r line; do
if [[ $line =~ ^([[:space:]]*)(source|\.)[[:space:]]+(.+) ]]; then
indent=${BASH_REMATCH[1]}
source_command=${BASH_REMATCH[2]}
file=${BASH_REMATCH[3]}
echo "${indent}# $source_command $file"
if [[ $file != */* ]]; then
fp=$(type -p "$file" || :) # look in $PATH
fi
if [[ -z $fp ]]; then
fp=$(eval realpath "$file") # resolve links, relative paths, ~, quotes, and escapes.
fi
# fail if we've already sourced this file
if [[ ${sourced[$fp]} = 1 ]]; then
echo "ERROR: Recursion detected: $fp"
exit 1
fi
sourced["$fp"]=1
_inline_sources "$fp" | sed -e "/^#!.*/d;s/^/${indent}/" # remove shebang and add indentation
sourced["$fp"]=0
continue
fi
echo "$line"
done < "$1"
}
_inline_sources "$1"
@joehillen
Copy link
Author

joehillen commented Oct 27, 2022

PROTIP: Turn an external script in $PATH into a function to make it portable:

function myscript {
  echo "This is myscript in a function: $1"
  source myscript
}

myscript "$1"

@carlocorradini
Copy link

For anyone who is struggling with this, I've developed a POSIX script that is compatible with ShellCheck and completely customizable: https://github.com/carlocorradini/inline
Hope this is useful πŸ₯³

@c33s
Copy link

c33s commented Dec 23, 2022

the script from https://stackoverflow.com/a/37533160 (just wrapped it into a function like yours)

#!/usr/bin/env bash

set -e
set -o pipefail

declare -A sourced

function _inline_sources {

    while read line; do
        if [[ "$line" =~ (\.|source)\s+.+ ]]; then
            file="$(echo $line | cut -d' ' -f2)"
            echo "$(cat $file)"
        else
          echo "$line"
        fi
    done < "$1"
}

_inline_sources "$1"

sadly your script includes the first sourced file for all further sources.

.
β”œβ”€β”€ bin
β”‚Β Β  └── build.sh
β”œβ”€β”€ lib
β”‚Β Β  β”œβ”€β”€ example.sh
β”‚Β Β  β”œβ”€β”€ header.sh
β”‚Β Β  └── lib-wget.sh
└── test.sh

test.sh

#!/bin/ash
source lib/header.sh

echo "my script start"

source lib/lib-wget.sh
source lib/example.sh
echo do something

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment