794 lines
22 KiB
Bash
Executable File
794 lines
22 KiB
Bash
Executable File
#!/bin/sh
|
||
# SPDX-License-Identifier: GPL-3.0-only
|
||
#
|
||
# This file is part of the distrobox project:
|
||
# https://github.com/89luca89/distrobox
|
||
#
|
||
# Copyright (C) 2021 distrobox contributors
|
||
#
|
||
# distrobox is free software; you can redistribute it and/or modify it
|
||
# under the terms of the GNU General Public License version 3
|
||
# as published by the Free Software Foundation.
|
||
#
|
||
# distrobox is distributed in the hope that it will be useful, but
|
||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||
# General Public License for more details.
|
||
#
|
||
# You should have received a copy of the GNU General Public License
|
||
# along with distrobox; if not, see <http://www.gnu.org/licenses/>.
|
||
|
||
# Ensure we have our env variables correctly set
|
||
[ -z "${USER}" ] && USER="$(id -run)"
|
||
[ -z "${HOME}" ] && HOME="$(getent passwd "${USER}" | cut -d':' -f6)"
|
||
[ -z "${SHELL}" ] && SHELL="$(getent passwd "${USER}" | cut -d':' -f7)"
|
||
|
||
# POSIX
|
||
#
|
||
default_input_file="./distrobox.ini"
|
||
delete=-1
|
||
distrobox_path="$(dirname "${0}")"
|
||
dryrun=0
|
||
boxname=""
|
||
input_file=""
|
||
replace=0
|
||
root_flag=""
|
||
# tmpfile will be used as a little buffer to pass variables without messing up
|
||
# quoting and escaping
|
||
tmpfile="$(mktemp -u)"
|
||
tmp_download_file="$(mktemp -u)"
|
||
verbose=0
|
||
version="1.8.2.2"
|
||
# initializing block of variables used in the manifest
|
||
additional_flags=""
|
||
additional_packages=""
|
||
entry=""
|
||
home=""
|
||
hostname=""
|
||
image=""
|
||
clone=""
|
||
init=""
|
||
init_hooks=""
|
||
nvidia=""
|
||
pre_init_hooks=""
|
||
pull=""
|
||
root=""
|
||
start_now=""
|
||
unshare_groups=""
|
||
unshare_ipc=""
|
||
unshare_netns=""
|
||
unshare_process=""
|
||
unshare_devsys=""
|
||
unshare_all=""
|
||
volume=""
|
||
exported_apps=""
|
||
exported_bins=""
|
||
exported_bins_path="${HOME}/.local/bin"
|
||
|
||
# Cleanup tmpfiles on exit
|
||
trap 'rm -f ${tmpfile} ${tmp_download_file}' EXIT
|
||
|
||
# Despite of running this script via SUDO/DOAS being not supported (the
|
||
# script itself will call the appropriate tool when necessary), we still want
|
||
# to allow people to run it as root, logged in in a shell, and create rootful
|
||
# containers.
|
||
#
|
||
# SUDO_USER is a variable set by SUDO and can be used to check whether the script was called by it. Same thing for DOAS_USER, set by DOAS.
|
||
if [ -n "${SUDO_USER}" ] || [ -n "${DOAS_USER}" ]; then
|
||
printf >&2 "Running %s via SUDO/DOAS is not supported." "$(basename "${0}")"
|
||
printf >&2 "Instead, please try using root=true property in the distrobox.ini file.\n"
|
||
exit 1
|
||
fi
|
||
|
||
# Source configuration files, this is done in an hierarchy so local files have
|
||
# priority over system defaults
|
||
# leave priority to environment variables.
|
||
#
|
||
# On NixOS, for the distrobox derivation to pick up a static config file shipped
|
||
# by the package maintainer the path must be relative to the script itself.
|
||
self_dir="$(dirname "$(realpath "$0")")"
|
||
nix_config_file="${self_dir}/../share/distrobox/distrobox.conf"
|
||
|
||
config_files="
|
||
${nix_config_file}
|
||
/usr/share/distrobox/distrobox.conf
|
||
/usr/share/defaults/distrobox/distrobox.conf
|
||
/usr/etc/distrobox/distrobox.conf
|
||
/usr/local/share/distrobox/distrobox.conf
|
||
/etc/distrobox/distrobox.conf
|
||
${XDG_CONFIG_HOME:-"${HOME}/.config"}/distrobox/distrobox.conf
|
||
${HOME}/.distroboxrc
|
||
"
|
||
for config_file in ${config_files}; do
|
||
# Shellcheck will give error for sourcing a variable file as it cannot follow
|
||
# it. We don't care so let's disable this linting for now.
|
||
# shellcheck disable=SC1090
|
||
[ -e "${config_file}" ] && . "$(realpath "${config_file}")"
|
||
done
|
||
|
||
[ -n "${DBX_VERBOSE}" ] && verbose="${DBX_VERBOSE}"
|
||
|
||
# Fixup variable=[true|false], in case we find it in the config file(s)
|
||
[ "${verbose}" = "true" ] && verbose=1
|
||
[ "${verbose}" = "false" ] && verbose=0
|
||
|
||
# show_help will print usage to stdout.
|
||
# Arguments:
|
||
# None
|
||
# Expected global variables:
|
||
# version: string distrobox version
|
||
# Expected env variables:
|
||
# None
|
||
# Outputs:
|
||
# print usage with examples.
|
||
show_help()
|
||
{
|
||
cat << EOF
|
||
distrobox version: ${version}
|
||
|
||
Usage:
|
||
|
||
distrobox assemble create
|
||
distrobox assemble rm
|
||
distrobox assemble create --file /path/to/file.ini
|
||
distrobox assemble rm --file /path/to/file.ini
|
||
distrobox assemble create --replace --file /path/to/file.ini
|
||
|
||
Options:
|
||
|
||
--file: path or URL to the distrobox manifest/ini file
|
||
--name/-n: run against a single entry in the manifest/ini file
|
||
--replace/-R: replace already existing distroboxes with matching names
|
||
--dry-run/-d: only print the container manager command generated
|
||
--verbose/-v: show more verbosity
|
||
--version/-V: show version
|
||
EOF
|
||
}
|
||
|
||
# Parse arguments
|
||
while :; do
|
||
case $1 in
|
||
create)
|
||
delete=0
|
||
shift
|
||
;;
|
||
rm)
|
||
delete=1
|
||
shift
|
||
;;
|
||
--file)
|
||
# Call a "show_help" function to display a synopsis, then exit.
|
||
if [ -n "$2" ]; then
|
||
input_file="${2}"
|
||
shift
|
||
shift
|
||
fi
|
||
;;
|
||
-n | --name)
|
||
# Call a "show_help" function to display a synopsis, then exit.
|
||
if [ -n "$2" ]; then
|
||
boxname="${2}"
|
||
shift
|
||
shift
|
||
fi
|
||
;;
|
||
-h | --help)
|
||
# Call a "show_help" function to display a synopsis, then exit.
|
||
show_help
|
||
exit 0
|
||
;;
|
||
-d | --dry-run)
|
||
shift
|
||
dryrun=1
|
||
;;
|
||
-v | --verbose)
|
||
verbose=1
|
||
shift
|
||
;;
|
||
-R | --replace)
|
||
replace=1
|
||
shift
|
||
;;
|
||
-V | --version)
|
||
printf "distrobox: %s\n" "${version}"
|
||
exit 0
|
||
;;
|
||
--) # End of all options.
|
||
shift
|
||
break
|
||
;;
|
||
-*) # Invalid options.
|
||
printf >&2 "ERROR: Invalid flag '%s'\n\n" "$1"
|
||
show_help
|
||
exit 1
|
||
;;
|
||
*) # Default case: If no more options then break out of the loop.
|
||
# If we have a flagless option and container_name is not specified
|
||
# then let's accept argument as container_name
|
||
if [ -n "$1" ]; then
|
||
input_file="$1"
|
||
shift
|
||
else
|
||
break
|
||
fi
|
||
;;
|
||
esac
|
||
done
|
||
|
||
set -o errexit
|
||
set -o nounset
|
||
# set verbosity
|
||
if [ "${verbose}" -ne 0 ]; then
|
||
set -o xtrace
|
||
fi
|
||
|
||
# check if we're getting the right inputs
|
||
if [ "${delete}" -eq -1 ]; then
|
||
printf >&2 "Please specify create or rm.\n"
|
||
show_help
|
||
exit 1
|
||
fi
|
||
|
||
# Fallback to distrobox.ini if no file is passed
|
||
if [ -z "${input_file}" ]; then
|
||
input_file="${default_input_file}"
|
||
fi
|
||
|
||
# Check if file effectively exists
|
||
if [ ! -e "${input_file}" ]; then
|
||
|
||
if command -v curl > /dev/null 2>&1; then
|
||
download="curl --connect-timeout 3 --retry 1 -sLo"
|
||
elif command -v wget > /dev/null 2>&1; then
|
||
download="wget --timeout=3 --tries=1 -qO"
|
||
fi
|
||
|
||
if ! ${download} - "${input_file}" > "${tmp_download_file}"; then
|
||
printf >&2 "File %s does not exist.\n" "${input_file}"
|
||
exit 1
|
||
else
|
||
input_file="${tmp_download_file}"
|
||
fi
|
||
fi
|
||
|
||
# run_distrobox will create distrobox with parameters parsed from ini file.
|
||
# Arguments:
|
||
# name: name of the distrobox.
|
||
# Expected global variables:
|
||
# boxname: string name of the target container
|
||
# tmpfile: string name of the tmpfile to read
|
||
# delete: bool delete container
|
||
# replace: bool replace container
|
||
# dryrun: bool dryrun (only print, no execute)
|
||
# verbose: bool verbose
|
||
# Expected env variables:
|
||
# None
|
||
# Outputs:
|
||
# execution of the proper distrobox-create command.
|
||
run_distrobox()
|
||
{
|
||
name="${1}"
|
||
additional_flags=""
|
||
additional_packages=""
|
||
entry=""
|
||
home=""
|
||
hostname=""
|
||
image=""
|
||
clone=""
|
||
init=""
|
||
init_hooks=""
|
||
nvidia=""
|
||
pre_init_hooks=""
|
||
pull=""
|
||
root=""
|
||
start_now=""
|
||
unshare_groups=""
|
||
unshare_ipc=""
|
||
unshare_netns=""
|
||
unshare_process=""
|
||
unshare_devsys=""
|
||
unshare_all=""
|
||
volume=""
|
||
exported_apps=""
|
||
exported_bins=""
|
||
exported_bins_path="${HOME}/.local/bin"
|
||
|
||
# Skip item if --name used and no match is found
|
||
if [ "${boxname}" != "" ] && [ "${boxname}" != "${name}" ]; then
|
||
rm -f "${tmpfile}"
|
||
return
|
||
fi
|
||
|
||
# Source the current block variables
|
||
if [ -e "${tmpfile}" ]; then
|
||
# shellcheck disable=SC1090
|
||
. "${tmpfile}" && rm -f "${tmpfile}"
|
||
fi
|
||
|
||
if [ -n "${root}" ] && [ "${root}" -eq 1 ]; then
|
||
root_flag="--root"
|
||
fi
|
||
|
||
# We're going to delete, not create!
|
||
if [ "${delete}" -ne 0 ] || [ "${replace}" -ne 0 ]; then
|
||
printf " - Deleting %s...\n" "${name}"
|
||
|
||
if [ "${dryrun}" -eq 0 ]; then
|
||
# shellcheck disable=SC2086,2248
|
||
"${distrobox_path}"/distrobox rm ${root_flag} -f "${name}" > /dev/null || :
|
||
fi
|
||
|
||
if [ "${delete}" -ne 0 ]; then
|
||
return
|
||
fi
|
||
fi
|
||
|
||
# We're going to create!
|
||
printf " - Creating %s...\n" "${name}"
|
||
|
||
# If distrobox already exist, and we have replace enabled, destroy the container
|
||
# we have to recreate it.
|
||
# shellcheck disable=SC2086,2248
|
||
if "${distrobox_path}"/distrobox-list ${root_flag} | grep -qw " ${name} " && [ "${dryrun}" -eq 0 ]; then
|
||
printf >&2 "%s already exists\n" "${name}"
|
||
return 0
|
||
fi
|
||
|
||
# Now we dynamically generate the distrobox-create command based on the
|
||
# declared flags.
|
||
result_command="${distrobox_path}/distrobox-create --yes"
|
||
if [ "${verbose}" -ne 0 ]; then
|
||
result_command="${result_command} -v"
|
||
fi
|
||
if [ -n "${name}" ]; then
|
||
result_command="${result_command} --name $(sanitize_variable "${name}")"
|
||
fi
|
||
if [ -n "${image}" ]; then
|
||
result_command="${result_command} --image $(sanitize_variable "${image}")"
|
||
fi
|
||
if [ -n "${clone}" ]; then
|
||
result_command="${result_command} --clone $(sanitize_variable "${clone}")"
|
||
fi
|
||
if [ -n "${init}" ] && [ "${init}" -eq 1 ]; then
|
||
result_command="${result_command} --init"
|
||
fi
|
||
if [ -n "${root}" ] && [ "${root}" -eq 1 ]; then
|
||
result_command="${result_command} --root"
|
||
fi
|
||
if [ -n "${pull}" ] && [ "${pull}" -eq 1 ]; then
|
||
result_command="${result_command} --pull"
|
||
fi
|
||
if [ -n "${entry}" ] && [ "${entry}" -eq 0 ]; then
|
||
result_command="${result_command} --no-entry"
|
||
fi
|
||
if [ -n "${nvidia}" ] && [ "${nvidia}" -eq 1 ]; then
|
||
result_command="${result_command} --nvidia"
|
||
fi
|
||
if [ -n "${unshare_netns}" ] && [ "${unshare_netns}" -eq 1 ]; then
|
||
result_command="${result_command} --unshare-netns"
|
||
fi
|
||
if [ -n "${unshare_groups}" ] && [ "${unshare_groups}" -eq 1 ]; then
|
||
result_command="${result_command} --unshare-groups"
|
||
fi
|
||
if [ -n "${unshare_ipc}" ] && [ "${unshare_ipc}" -eq 1 ]; then
|
||
result_command="${result_command} --unshare-ipc"
|
||
fi
|
||
if [ -n "${unshare_process}" ] && [ "${unshare_process}" -eq 1 ]; then
|
||
result_command="${result_command} --unshare-process"
|
||
fi
|
||
if [ -n "${unshare_devsys}" ] && [ "${unshare_devsys}" -eq 1 ]; then
|
||
result_command="${result_command} --unshare-devsys"
|
||
fi
|
||
if [ -n "${unshare_all}" ] && [ "${unshare_all}" -eq 1 ]; then
|
||
result_command="${result_command} --unshare-all"
|
||
fi
|
||
if [ -n "${home}" ]; then
|
||
result_command="${result_command} --home $(sanitize_variable "${home}")"
|
||
fi
|
||
if [ -n "${hostname}" ]; then
|
||
result_command="${result_command} --hostname $(sanitize_variable "${hostname}")"
|
||
fi
|
||
if [ -n "${init_hooks}" ]; then
|
||
IFS="¤"
|
||
args=": ;"
|
||
separator=""
|
||
for arg in ${init_hooks}; do
|
||
if [ -z "${arg}" ]; then
|
||
continue
|
||
fi
|
||
|
||
# Convert back from base64
|
||
arg="$(echo "${arg}" | base64 -d)"
|
||
args="${args} ${separator} ${arg}"
|
||
|
||
# Prepare for the next line, if we already have a ';' or '&&', do nothing
|
||
# else prefer adding '&&'
|
||
if ! echo "${arg}" | grep -qE ';[[:space:]]{0,1}$' &&
|
||
! echo "${arg}" | grep -qE '&&[[:space:]]{0,1}$'; then
|
||
separator="&&"
|
||
else
|
||
separator=""
|
||
fi
|
||
done
|
||
result_command="${result_command} --init-hooks $(sanitize_variable "${args}")"
|
||
fi
|
||
# Replicable flags
|
||
if [ -n "${pre_init_hooks}" ]; then
|
||
IFS="¤"
|
||
args=": ;"
|
||
separator=""
|
||
for arg in ${pre_init_hooks}; do
|
||
if [ -z "${arg}" ]; then
|
||
continue
|
||
fi
|
||
|
||
# Convert back from base64
|
||
arg="$(echo "${arg}" | base64 -d)"
|
||
args="${args} ${separator} ${arg}"
|
||
|
||
# Prepare for the next line, if we already have a ';' or '&&', do nothing
|
||
# else prefer adding '&&'
|
||
if ! echo "${arg}" | grep -qE ';[[:space:]]{0,1}$' &&
|
||
! echo "${arg}" | grep -qE '&&[[:space:]]{0,1}$'; then
|
||
separator="&&"
|
||
else
|
||
separator=""
|
||
fi
|
||
done
|
||
result_command="${result_command} --pre-init-hooks $(sanitize_variable "${args}")"
|
||
fi
|
||
if [ -n "${additional_packages}" ]; then
|
||
IFS="¤"
|
||
args=""
|
||
for packages in ${additional_packages}; do
|
||
if [ -z "${packages}" ]; then
|
||
continue
|
||
fi
|
||
args="${args} ${packages}"
|
||
done
|
||
result_command="${result_command} --additional-packages $(sanitize_variable "${args}")"
|
||
fi
|
||
if [ -n "${volume}" ]; then
|
||
IFS="¤"
|
||
for vol in ${volume}; do
|
||
if [ -z "${vol}" ]; then
|
||
continue
|
||
fi
|
||
result_command="${result_command} --volume $(sanitize_variable "${vol}")"
|
||
done
|
||
fi
|
||
if [ -n "${additional_flags}" ]; then
|
||
IFS="¤"
|
||
for flag in ${additional_flags}; do
|
||
if [ -z "${flag}" ]; then
|
||
continue
|
||
fi
|
||
result_command="${result_command} --additional-flags $(sanitize_variable "${flag}")"
|
||
done
|
||
fi
|
||
|
||
# Execute the distrobox-create command
|
||
if [ "${dryrun}" -ne 0 ]; then
|
||
echo "${result_command}"
|
||
return
|
||
fi
|
||
eval "${result_command}"
|
||
|
||
# If we need to start immediately, do it, so that the container
|
||
# is ready to be entered.
|
||
if [ -n "${start_now}" ] && [ "${start_now}" -eq 1 ]; then
|
||
# Here we execute the `distrobox enter` command with a `/dev/null` stdin.
|
||
# This is due to the fact that this command is very likely to be executed inside
|
||
# the read loop in the prse_file function. This way we avoid stdin to be swallowed by
|
||
# this command execution. This is valid for all the `distrobox enter` calls from now on.
|
||
#
|
||
# shellcheck disable=SC2086,2248
|
||
"${distrobox_path}"/distrobox enter ${root_flag} "${name}" -- touch /dev/null < /dev/null
|
||
fi
|
||
|
||
# if there are exported bins and apps declared, let's export them
|
||
if [ -n "${exported_apps}" ] || [ -n "${exported_bins}" ]; then
|
||
# First we start the container
|
||
# shellcheck disable=SC2086,2248
|
||
"${distrobox_path}"/distrobox enter ${root_flag} "${name}" -- touch /dev/null < /dev/null
|
||
|
||
IFS="¤"
|
||
for apps in ${exported_apps}; do
|
||
# Split the string by spaces
|
||
IFS=" "
|
||
for app in ${apps}; do
|
||
# Export the app
|
||
# shellcheck disable=SC2086,2248
|
||
"${distrobox_path}"/distrobox enter ${root_flag} "${name}" -- distrobox-export --app "${app}" < /dev/null
|
||
done
|
||
done
|
||
|
||
IFS="¤"
|
||
for bins in ${exported_bins}; do
|
||
# Split the string by spaces
|
||
IFS=" "
|
||
for bin in ${bins}; do
|
||
# Export the bin
|
||
# shellcheck disable=SC2086,2248
|
||
"${distrobox_path}"/distrobox enter ${root_flag} "${name}" -- distrobox-export --bin "${bin}" \
|
||
--export-path "${exported_bins_path}" < /dev/null
|
||
done
|
||
done
|
||
fi
|
||
}
|
||
|
||
# encode_variable will encode an input in base64, removing surrounding single/double quotes.
|
||
# Arguments:
|
||
# variable: string
|
||
# Expected global variables:
|
||
# None
|
||
# Expected env variables:
|
||
# None
|
||
# Outputs:
|
||
# a value string encoded in base64
|
||
encode_variable()
|
||
{
|
||
variable="${1}"
|
||
# remove surrounding quotes possibly added by the user
|
||
if echo "${variable}" | grep -qE '^"'; then
|
||
variable="$(echo "${variable}" | sed -e 's/^"//' -e 's/"$//')"
|
||
elif echo "${variable}" | grep -qE "^'"; then
|
||
variable="$(echo "${variable}" | sed -e "s/^'//" -e "s/'$//")"
|
||
fi
|
||
|
||
echo "${variable}" | base64 -w 0
|
||
}
|
||
|
||
# sanitize_variable will sanitize an input, add single/double quotes and escapes
|
||
# Arguments:
|
||
# variable: string
|
||
# Expected global variables:
|
||
# None
|
||
# Expected env variables:
|
||
# None
|
||
# Outputs:
|
||
# a value string sanitized
|
||
sanitize_variable()
|
||
{
|
||
variable="${1}"
|
||
|
||
# If there are spaces but no quotes, let's add them
|
||
if echo "${variable}" | grep -q " " &&
|
||
! echo "${variable}" | grep -Eq "^'|^\""; then
|
||
|
||
# if we have double quotes we should wrap the whole line in single quotes
|
||
# in order to not "undo" them
|
||
if echo "${variable}" | grep -q '"'; then
|
||
variable="'${variable}'"
|
||
else
|
||
variable="\"${variable}\""
|
||
fi
|
||
fi
|
||
|
||
# Return
|
||
echo "${variable}"
|
||
}
|
||
|
||
# parse_file will read and parse input file and call distrobox-create accordingly
|
||
# Arguments:
|
||
# file: string path of the manifest file to parse
|
||
# Expected global variables:
|
||
# tmpfile: string name of the tmpfile to read
|
||
# Expected env variables:
|
||
# None
|
||
# Outputs:
|
||
# None
|
||
parse_file()
|
||
{
|
||
file="${1}"
|
||
name=""
|
||
|
||
IFS='
|
||
'
|
||
while read -r line; do
|
||
# Remove comments and trailing spaces
|
||
line="$(echo "${line}" |
|
||
sed 's/\t/ /g' |
|
||
sed 's/^#.*//g' |
|
||
sed 's/].*#.*//g' |
|
||
sed 's/ #.*//g' |
|
||
sed 's/\s*$//g')"
|
||
|
||
if [ -z "${line}" ]; then
|
||
# blank line, skip
|
||
continue
|
||
fi
|
||
|
||
# Detect start of new section
|
||
if [ "$(echo "${line}" | cut -c 1)" = '[' ]; then
|
||
# We're starting a new section
|
||
if [ -n "${name}" ]; then
|
||
# We've finished the previous section, so this is the time to
|
||
# perform the distrobox command, before going to the new section.
|
||
run_distrobox "${name}"
|
||
fi
|
||
|
||
# Remove brackets and spaces
|
||
name="$(echo "${line}" | tr -d '][ ')"
|
||
continue
|
||
fi
|
||
|
||
# Get key-values from the file
|
||
key="$(echo "${line}" | cut -d'=' -f1 | tr -d ' ')"
|
||
value="$(echo "${line}" | cut -d'=' -f2-)"
|
||
|
||
# Normalize true|false to 0|1
|
||
[ "${value}" = "true" ] && value=1
|
||
[ "${value}" = "false" ] && value=0
|
||
|
||
# Sanitize value, by whitespaces, quotes and escapes
|
||
if [ "${key}" = "init_hooks" ] || [ "${key}" = "pre_init_hooks" ]; then
|
||
# in case of shell commands (so the hooks) we prefer to pass the variable
|
||
# around encoded, so that we don't accidentally execute stuff
|
||
# and, we will execute sanitize_variable on the final string flag at the
|
||
# end, instead of key/value base.
|
||
value="$(encode_variable "${value}")"
|
||
else
|
||
value="$(sanitize_variable "${value}")"
|
||
fi
|
||
|
||
# Save options to tempfile, to source it later
|
||
touch "${tmpfile}"
|
||
if [ -n "${key}" ] && [ -n "${value}" ]; then
|
||
if grep -q "^${key}=" "${tmpfile}"; then
|
||
# make keys cumulative
|
||
value="\${${key}}¤${value}"
|
||
fi
|
||
echo "${key}=${value}" >> "${tmpfile}"
|
||
fi
|
||
done < "${file}"
|
||
# Execute now one last time for the last block
|
||
run_distrobox "${name}"
|
||
}
|
||
|
||
# read_section reads the content of a section from a TOML file.
|
||
# Arguments:
|
||
# section: Name of the section to find (string).
|
||
# file: Path to the file to search into (string).
|
||
# Expected global variables:
|
||
# None.
|
||
# Expected env variables:
|
||
# None.
|
||
# Outputs:
|
||
# Writes the found section body to stdout.
|
||
# Exit behavior / errors:
|
||
# Does not exit non‑zero on missing section; caller should treat empty output as needed.
|
||
read_section()
|
||
{
|
||
section="$1"
|
||
file="$2"
|
||
awk -v sec="[${section}]" '
|
||
$0 == sec {show=1; next}
|
||
show && /^\[/ {exit}
|
||
show && NF {print}
|
||
' "${file}"
|
||
}
|
||
|
||
# resolve_includes Resolve 'include' keys in a manifest by inlining referenced sections.
|
||
# Arguments:
|
||
# input_file: Path to the original manifest file that may contain 'include' keys.
|
||
# output_file: Path to the file where the resolved manifest will be written (if empty, a temp file is used).
|
||
# Expected global variables:
|
||
# read_section (function) - used to extract referenced section bodies from input_file.
|
||
# replace_line (function) - used to replace include lines with extracted content.
|
||
# Expected env variables:
|
||
# TMPDIR (optional) - may influence mktemp location.
|
||
# Outputs:
|
||
# Prints the path to the output file to stdout when completed.
|
||
# Exit behavior / errors:
|
||
# Exits with status 1 and writes to stderr on missing definitions, circular includes, or helper failures.
|
||
resolve_includes()
|
||
{
|
||
input_file="$1"
|
||
output_file="$2"
|
||
include_stack=""
|
||
|
||
# At the starting point, the output file is equal to input_file
|
||
# Later on, the output file will be edited as includes will be resolved
|
||
cat "${input_file}" > "${output_file}"
|
||
|
||
n=0
|
||
while true; do
|
||
total_lines=$(wc -l < "${output_file}")
|
||
[ "${n}" -gt "${total_lines}" ] && break
|
||
|
||
line=$(sed -n "$((n + 1))p" "${output_file}")
|
||
|
||
# Detected begin of a section: clear the include stack and go on
|
||
if [ "$(echo "${line}" | cut -c 1)" = '[' ]; then
|
||
include_stack=""
|
||
n=$((n + 1))
|
||
continue
|
||
fi
|
||
|
||
# Match key=value from the current line
|
||
key="$(echo "${line}" | cut -d'=' -f1 | tr -d ' ')"
|
||
value="$(echo "${line}" | cut -d'=' -f2-)"
|
||
|
||
if [ "${key}" = "include" ]; then
|
||
# Detect circular references while including other distrobox definitions
|
||
# The reference stack is handled as a string of shape [name].[name].[name]
|
||
if expr "${include_stack}" : ".*\[${value}\]" > /dev/null; then
|
||
printf >&2 "ERROR circular reference detected: including [%s] again after %s\n" "${value}" "${include_stack}"
|
||
exit 1
|
||
else
|
||
include_stack="[${value}]¤${include_stack}"
|
||
fi
|
||
|
||
# Read the definition for the distrobox to include from the original file
|
||
# and replace the current line with the found lines.
|
||
# The line counter is not incremented to allow recursive include resolution
|
||
inc=$(read_section "$(echo "${value}" | tr -d '"')" "${input_file}")
|
||
if [ -z "${inc}" ]; then
|
||
printf >&2 "ERROR cannot include '%s': definition not found\n" "${value}"
|
||
exit 1
|
||
fi
|
||
l=$((n + 1))
|
||
replace_line "${l}" "${inc}" "${output_file}" "${output_file}" > /dev/null
|
||
|
||
continue
|
||
fi
|
||
|
||
# Nothing to do, increment counter and go on
|
||
n=$((n + 1))
|
||
|
||
done
|
||
|
||
echo "${output_file}"
|
||
|
||
}
|
||
|
||
# replace_line Replace a 1-based numbered line in a file with provided (possibly multiline) text.
|
||
# Arguments:
|
||
# line_number: 1-based index of the line to replace.
|
||
# new_value: String to insert (may contain newlines).
|
||
# input_file: Path to the original file to read from.
|
||
# output_file: Path to write the resulting file (if empty, a temp file will be created).
|
||
# Expected global variables:
|
||
# None.
|
||
# Expected env variables:
|
||
# TMPDIR (optional) - may influence mktemp location.
|
||
# Outputs:
|
||
# Prints the path to the output file to stdout when complete.
|
||
# Exit behavior / errors:
|
||
# Exits with status 1 on fatal errors (e.g., mktemp failure).
|
||
replace_line()
|
||
{
|
||
line_number="$1"
|
||
new_value="$2"
|
||
input_file="$3"
|
||
output_file="$4"
|
||
|
||
# if no output file, use a temp file
|
||
if [ -z "${output_file}" ]; then
|
||
output_file=$(mktemp -u)
|
||
fi
|
||
|
||
tmpfile=$(mktemp) || exit 1
|
||
|
||
# Split the file into two parts around the line to replace and combine with new_value
|
||
# Print lines before line_number
|
||
sed "$((line_number - 1))q" "${input_file}" > "${tmpfile}"
|
||
|
||
# Append the new_value
|
||
printf "%s\n" "${new_value}" >> "${tmpfile}"
|
||
|
||
# Append lines after line_number
|
||
sed -n "$((line_number + 1)),\$p" "${input_file}" >> "${tmpfile}"
|
||
|
||
# Replace original file with tmpfile
|
||
mv "${tmpfile}" "${output_file}"
|
||
|
||
echo "${output_file}"
|
||
}
|
||
|
||
# Exec
|
||
expanded_file=$(mktemp -u)
|
||
resolve_includes "${input_file}" "${expanded_file}"
|
||
parse_file "${expanded_file}"
|