You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Botspot-Pi-Apps/api

573 lines
22 KiB
Bash

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#!/bin/bash
error() {
echo -e "\e[91m$1\e[39m" 1>&2
exit 1
}
#if this script is being run standalone, not sourced
if [[ "$0" == */api ]];then
DIRECTORY="$(readlink -f "$(dirname "$0")")"
fi
if [ -z "$DIRECTORY" ] || [ "$DIRECTORY" == "$HOME" ];then
echo "api: DIRECTORY variable must be set to valid pi-apps folder. Default folder: $HOME/pi-apps"
return 1
fi
repo_url="$(cat "${DIRECTORY}/etc/git_url" || echo 'https://github.com/Botspot/pi-apps')"
#determine if host system is 64 bit arm64 or 32 bit armhf
if [ "$(od -An -t x1 -j 4 -N 1 "$(readlink -f /sbin/init)")" = ' 02' ];then
arch=64
elif [ "$(od -An -t x1 -j 4 -N 1 "$(readlink -f /sbin/init)")" = ' 01' ];then
arch=32
else
error "Failed to detect OS CPU architecture! Something is very wrong."
fi
list_intersect() { #Outputs only the apps that appear in both stdin and in $1
# change \n to \| | remove last "\|"
grep -x "$(echo "$1" | sed -z 's/\n/\\|/g' | sed -z 's/\\|$/\n/g')"
}
list_subtract() { #Outputs a list of apps from stdin, minus the ones that appear in $1
# change \n to \| | remove last "\|"
grep -vx "$(echo "$1" | sed -z 's/\n/\\|/g' | sed -z 's/\\|$/\n/g')"
}
list_apps() { # $1 can be: installed, uninstalled, corrupted, cpu_installable, hidden, visible, online, online_only, local, local_only
if [ -z "$1" ] || [ "$1" == local ];then
#list all apps
ls "${DIRECTORY}/apps"
elif [ "$1" == all ];then
#combined list of apps, both online and local. Removes duplicate apps from the list.
echo -e "$(list_apps local)\n$(list_apps online)" | sort | uniq
elif [ "$1" == installed ];then
#list installed apps
#list apps| only show ( list of installed apps | remove match string | basename )
list_apps local | list_intersect "$(grep -rx 'installed' "${DIRECTORY}/data/status" | awk -F: '{print $1}' | sed 's!.*/!!')"
elif [ "$1" == corrupted ];then
#list corrupted apps
#list apps|only show ( list of corrupted apps | remove match string | basename )
list_apps local | list_intersect "$(grep -rx 'corrupted' "${DIRECTORY}/data/status" | awk -F: '{print $1}' | sed 's!.*/!!')"
elif [ "$1" == disabled ];then
#list corrupted apps
#list apps|only show ( list of disabled apps | remove match string | basename )
list_apps local | list_intersect "$(grep -rx 'disabled' "${DIRECTORY}/data/status" | awk -F: '{print $1}' | sed 's!.*/!!')"
elif [ "$1" == uninstalled ];then
#list uninstalled apps
#list apps that have a status file containing "uninstalled"
list_apps local | list_intersect "$(grep -rx 'uninstalled' "${DIRECTORY}/data/status" | awk -F: '{print $1}' | sed 's!.*/!!')"
#also list apps that don't have a status file
list_apps local | list_subtract "$(ls "${DIRECTORY}/data/status")"
elif [ "$1" == cpu_installable ];then
#list apps that can be installed on the device's OS architecture (32-bit or 64-bit)
#find all apps that have install-XX script or an install script
find "${DIRECTORY}/apps" -type f \( -name "install-$arch" -o -name "install" \) | sed "s+/install-$arch++g" | sed "s+/install++g" | sed "s+${DIRECTORY}/apps/++g" | sort | uniq
elif [ "$1" == hidden ];then
#list apps that are hidden
cat "${DIRECTORY}/data/categories/structure" | grep '|hidden' | awk -F'|' '{print $1}'
elif [ "$1" == visible ];then
#list apps that are in any other category but 'hidden', and aren't disabled
cat "${DIRECTORY}/data/categories/structure" | grep -v '|hidden' | awk -F'|' '{print $1}' # | list_subtract "$(list_apps disabled)"
elif [ "$1" == online ];then
#list apps that exist on the online git repo
if [ -d "${DIRECTORY}/update/pi-apps/apps" ];then
#if update folder exists, just use that
ls "${DIRECTORY}/update/pi-apps/apps" | grep .
else
#if update folder doesn't exist, then parse github HTML to get a list of online apps. Horrible idea, but it works!
wget -qO- "${repo_url}/tree/master/apps" | grep 'title=".*" data-pjax=' -o | sed 's/title="//g' | sed 's/" data-pjax=//g'
fi
elif [ "$1" == online_only ];then
#list apps that exist only on the git repo, and not locally
list_apps online | list_subtract "$(list_apps local)"
elif [ "$1" == local_only ];then
#list apps that exist only locally, and not on the git repo
list_apps local | list_subtract "$(list_apps online)"
fi
}
app_categories() { #lists all apps in a virtual filesystem based on categories file
#cat "${DIRECTORY}/data/categories/structure" | awk -F'|' '{print $2"/"$1}'
#find apps not in categories file
{
missingapps="$(list_apps | list_subtract "$(cat "${DIRECTORY}/data/categories/structure" | awk -F'|' '{print $1}')")"
if [ ! -z "$missingapps" ];then
PREIFS="$IFS"
IFS=$'\n'
for app in $missingapps ;do
echo "WARNING: $app not found in categories file." 1>&2
if list_apps online | grep -qx "$app" ;then
#if app found online, then use online category line
if [ -z "$onlinestructurefile" ];then
onlinestructurefile="$(wget -qO- 'https://raw.githubusercontent.com/Botspot/pi-apps/master/data/categories/structure')"
fi
if echo "$onlinestructurefile" | grep -q '^'"$app|" ;then
#if line found in online structure file
echo "Putting $app in the $(echo "$onlinestructurefile" | grep '^'"$app|" | awk -F'|' '{print $2}') category." 1>&2
echo "$(echo "$onlinestructurefile" | grep '^'"$app|")" >> "${DIRECTORY}/data/categories/structure"
else
#app exists online, but no structure line found
echo -e "\e[33mHUGE WARNING: the $app exists on github, but no category was found for it on github!\nPlease report this to Botspot.\e[39m" 1>&2
echo "Putting $app in the / category." 1>&2
#put the app in root directory - no category
echo "$app|" >> "${DIRECTORY}/data/categories/structure"
fi
else
#app not found online
echo "Putting $app in the / category." 1>&2
#put the app in root directory - no category
echo "$app|" >> "${DIRECTORY}/data/categories/structure"
fi
done
IFS="$PREIFS"
fi
}
#find apps in categories file that don't exist
{
ghostapps="$(cat "${DIRECTORY}/data/categories/structure" | awk -F'|' '{print $1}' | list_subtract "$(list_apps)")"
if [ ! -z "$ghostapps" ];then
PREIFS="$IFS"
IFS=$'\n'
for app in $ghostapps ;do
echo "WARNING: $app does not exist but it was found in categories file." 1>&2
echo "Removing $app from the categories file..." 1>&2
#put the app in root directory - no category
sed -i "/$app/d" "${DIRECTORY}/data/categories/structure"
done
IFS="$PREIFS"
fi
}
#category file cleaned up past this point
#show normal categories
cat "${DIRECTORY}/data/categories/structure" | grep . | awk -F'|' '{print $2"/"$1}' | sed 's+^/++g'
#show special Installed category
list_apps installed | sed 's+^+Installed/+g'
#show special All Apps category
list_apps cpu_installable | list_intersect "$(list_apps visible)" | sed 's+^+All Apps/+g'
}
usercount() { #Return number of users for specified app. $1 is app name. if empty, all are shown.
clicklist="$(wget -qO- 'https://raw.githubusercontent.com/Botspot/pi-apps-analytics/main/clicklist')"
[ -z "$clicklist" ] && error "usercount(): clicklist empty. Likely no internet connection"
if [ -z "$1" ];then
echo "$clicklist"
else
# $1 is app
echo "$clicklist" | grep " $1"'$' | awk '{print $1}' | head -n1
fi
}
text_editor() { #Open user-preferred text editor. $1 is file to open
[ -z "$1" ] && error "text_editor(): no file specified"
#find the best text editor
preferrededitor="$(cat "${DIRECTORY}/data/settings/Preferred text editor")"
#change preferred editor if user-default doesn't exist
if ! command -v "$preferrededitor" >/dev/null;then
preferrededitor=geany
fi
if ! command -v "$preferrededitor" >/dev/null;then
preferrededitor=mousepad
fi
if ! command -v "$preferrededitor" >/dev/null;then
preferrededitor=leafpad
fi
if ! command -v "$preferrededitor" >/dev/null;then
preferrededitor=nano
fi
if [ "$preferrededitor" == nano ];then
#terminal-based text editor
"${DIRECTORY}/etc/terminal-run" "nano "\""$1"\""" "Editing $(basename "$1")"
else
#non-terminal text editor
"$preferrededitor" "$1"
fi
}
script_name() { #returns name of install script(s) for the $1 app. outputs: '', 'install-32', 'install-64', 'install', 'install-32 install-64'
[ -z "$1" ] && error 'script_name(): requires an argument'
#ensure $1 is valid app name
[ ! -d "${DIRECTORY}/apps/$1" ] && error "script_name: '$1' is an invalid app name.\n${DIRECTORY}/apps/$1 does not exist."
if [ -f "${DIRECTORY}/apps/$1/install-32" ] && [ ! -f "${DIRECTORY}/apps/$1/install-64" ];then
echo 'install-32'
elif [ -f "${DIRECTORY}/apps/$1/install-64" ] && [ ! -f "${DIRECTORY}/apps/$1/install-32" ];then
echo 'install-64'
elif [ -f "${DIRECTORY}/apps/$1/install-64" ] && [ -f "${DIRECTORY}/apps/$1/install-32" ];then
echo 'install-32 install-64'
elif [ -f "${DIRECTORY}/apps/$1/install" ];then
echo 'install'
else
true
#error "No install script found for the $app app! Please report this to Botspot."
fi
}
script_name_cpu() { #get script name to run based on detected CPU arch
[ -z "$1" ] && error 'script_name_cpu(): requires an argument.'
#ensure $1 is valid app name
if ! list_apps all | grep -q "$1" ;then
error "script_name_cpu: '$1' is an invalid app name."
fi
#this is used by the updater so we need to check the update folder too
if [ -f "${DIRECTORY}/apps/$1/install-32" ] && [ $arch == 32 ];then
echo 'install-32'
elif [ -f "${DIRECTORY}/apps/$1/install-64" ] && [ $arch == 64 ];then
echo 'install-64'
elif [ -f "${DIRECTORY}/apps/$1/install" ];then
echo 'install'
elif [ -f "${DIRECTORY}/update/pi-apps/apps/$1/install-32" ] && [ $arch == 32 ];then
echo 'install-32'
elif [ -f "${DIRECTORY}/update/pi-apps/apps/$1/install-64" ] && [ $arch == 64 ];then
echo 'install-64'
elif [ -f "${DIRECTORY}/update/pi-apps/apps/$1/install" ];then
echo 'install'
else
true #app not compatible with current arch
fi
}
app_status() { #Gets the $1 app's current status. installed, uninstalled, corrupted, disabled
[ -z "$1" ] && error 'app_status(): requires an argument.'
#don't check if app exists, it may be a new app in an update
if [ -f "${DIRECTORY}/data/status/${1}" ];then
cat "${DIRECTORY}/data/status/${1}"
else
echo 'uninstalled' #if app status file doesn't exist, assume uninstalled
fi
}
will_reinstall() { #return 0 if $1 app will be reinstalled during an update, otherwise return 1.
[ -z "$1" ] && error 'will_reinstall(): requires an argument'
#detect which installation script exists and get the hash for that one
scriptname="$(script_name_cpu "$1")"
oldinstallhash="$(sha1sum "${DIRECTORY}/apps/${1}/${scriptname}" 2>/dev/null | awk '{print $1}')"
newinstallhash="$(sha1sum "${DIRECTORY}/update/pi-apps/apps/${1}/${scriptname}" 2>/dev/null | awk '{print $1}')"
#if install script was changed #if installed already
if [ "$newinstallhash" != "$oldinstallhash" ] && [ "$(app_status "${1}")" == 'installed' ];then
return 0
else
return 1
fi
}
app_search() { #search all apps for $1
[ -z "$1" ] && error "app_search(): requires a search query."
#search description and website
results="$(find "${DIRECTORY}/apps" \( -name description -o -name website -o -name credits \) -exec grep -Fi "$1" {} + | awk -F: '{print $1}' | sed "s+${DIRECTORY}/apps/++g" | sed "s+/description++g" | sed "s+/website++g")"
#hide incompatible and hidden/disabled apps
results="$(echo "$results" | list_intersect "$(list_apps visible)" | list_intersect "$(list_apps cpu_installable)" | sort)"
#search app names
results="$(list_apps cpu_installable | list_intersect "$(list_apps visible)" | grep -i "$1" | sort)
$results"
#remove duplicate entries
echo "$results" | awk '!seen[$0]++'
}
app_search_gui() {
output="$(yad --title=Search --center --width=310 --window-icon="${DIRECTORY}/icons/logo.png" \
--text="Searches app names, descriptions, websites, and credits."$'\n'"Case-insensitive." \
--form --entry='')" || exit 0
results="$(app_search "$output")"
if [ ! -z "$results" ];then
IFS=$'\n'
LIST=''
for app in $results ;do
LIST="$LIST
${DIRECTORY}/apps/${app}/icon-24.png
$app
$(echo "$(cat "${DIRECTORY}/apps/${app}/description" || echo "Description unavailable")" | head -n1)"
done
LIST="${LIST:1}" #remove first empty newline
output="$(echo "$LIST" | yad --title=Results --center --width=310 --height=250 --window-icon="${DIRECTORY}/icons/logo.png" \
--list --no-headers --column=:IMG --column=name --column=tooltip:HD \
--print-column=2 --tooltip-column=3 --separator='\n')" || exit 0
echo "$output"
else
yad --title=Results --center --width=310 --window-icon="${DIRECTORY}/icons/logo.png" \
--text="No results found for "\""<b>$output</b>"\""." \
--button=OK:0
exit 0
fi
}
#non-app functions below
runonce() { #run command only if it's never been run before. Useful for one-time migration or setting changes.
#Runs a script in the form of stdin
script="$(cat /dev/stdin)"
runonce_hash="$(echo "$script" | sha256sum | awk '{print $1}')"
if grep -qx '^'"$runonce_hash"'$' "${DIRECTORY}/data/runonce_hashes" ;then
#hash found
#echo "runonce: '$script' already run before. Skipping."
true
else
#run the script.
bash <(echo "$script")
#if it succeeds, add the hash to the list to never run it again
if [ $? == 0 ];then
echo "$runonce_hash" >> "${DIRECTORY}/data/runonce_hashes"
echo "'$script' succeeded. Added to list."
else
echo "'$script' failed. Not adding hash to list."
fi
fi
}
apt_lock_wait() { #Wait until other apt processes are finished before proceeding
echo -n "Waiting until APT locks are released... "
#while sudo fuser /var/lib/dpkg/lock &>/dev/null ; do
# sleep 0.5
#done
#while sudo fuser /var/lib/dpkg/lock-frontend &>/dev/null ; do
# sleep 0.5
#done
#while sudo fuser /var/lib/apt/lists/lock &>/dev/null ; do
# sleep 0.5
#done
#if [ -f /var/log/unattended-upgrades/unattended-upgrades.log ]; then
# while sudo fuser /var/log/unattended-upgrades/unattended-upgrades.log &>/dev/null ; do
# sleep 0.5
# done
#fi
while [ ! -z "$(sudo fuser /var/lib/dpkg/lock /var/lib/apt/lists/lock /var/cache/apt/archives/lock /var/log/unattended-upgrades/unattended-upgrades.log /var/lib/dpkg/lock-frontend 2>/dev/null)" ];do
sleep 1
done
sleep 0.5
echo "Done"
}
is_supported_system() {
if uname -m | grep -q 'x86' || uname -m | grep -q 'i686' || uname -m | grep -q 'i386';then
echo "Pi-Apps is not supported on x86 processors. Expect almost all apps to fail. Consider switching to this x86 port of Pi-Apps: https://github.com/MCRaspRBX/pi-apps-x86"
return 1
elif cat /etc/os-release | grep PRETTY_NAME | tr -d '"' | awk -F= '{print $2}' | grep -qi 'stretch\|wheezy\|jessie';then
echo "Pi-Apps is not supported on your outdated operating system. Expect many apps to fail. Consider upgrading your operating system."
return 1
elif cat /etc/os-release | grep PRETTY_NAME | tr -d '"' | awk -F= '{print $2}' | grep -qi 'manjaro';then
echo "Pi-Apps is not supported on Manjaro."
return 1
elif [[ "$(uname -m)" == armv6* ]];then
echo "Pi-Apps is not supported on ARMv6 Raspberry Pi boards. Expect some apps to fail."
return 1
elif [ "$(id -u)" == 0 ]; then
echo "Pi-Apps is not designed to be run as root user."
return 1
elif [ "$(df -a / -B 1 --output=avail | tail -1 | tr -d ' ')" -lt $((1*1024*1024*1024)) ];then
echo "Your system drive has less than 1GB of free space. Watch out for "\""disk full"\"" errors."
return 1
else
return 0
fi
}
format_log_file() { #remove ANSI escape sequences from a given file, and add OS information to beginning of file
[ -z "$1" ] && error "format_log_file: no filename given!"
[ ! -f "$1" ] && error "format_log_file: given filename ($1) does not exist or is not a file!"
echo -e "$(get_device_info)\n\nBEGINNING OF LOG FILE:\n-----------------------\n\n$(cat "$1" | tr '\r' '\n' | sed 's/\x1b\[[0-9;]*m//g' | sed 's/\x1b\[[0-9;]*//g' | sed "s,\x1B\[[0-9;]*[a-zA-Z],,g" | grep -vF '.......... .......... .......... .......... ..........')" > "$1"
}
get_device_info() { #returns information about current install and hardware
echo "OS: $(cat /etc/os-release | grep PRETTY_NAME | tr -d '"' | awk -F= '{print $2}')"
echo "OS architecture: ${arch}-bit"
[ ! -z "$DIRECTORY" ] && echo "Pi-Apps local commit ID: $(cd "$DIRECTORY"; git rev-parse HEAD)"
echo "Kernel: $(uname -m) $(uname -r)"
echo "Device model: $(cat /proc/cpuinfo | grep Model | sed 's/Model.*: //g')"
if [ -f /etc/rpi-issue ];then
echo "Raspberry Pi OS image version: $(cat /etc/rpi-issue | grep 'Raspberry Pi reference' | sed 's/Raspberry Pi reference //g')"
fi
if [ ! -z "$LANG" ];then
echo "Language: $LANG"
elif [ ! -z "$LC_ALL" ];then
echo "Language: $LC_ALL"
fi
}
send_error_report() { #non-interactively send a Pi-Apps error log file to the Botspot discord server
[ -z "$1" ] && error "send_error_report(): requires an argument"
[ ! -f "$1" ] && error "send_error_report(): '$1' is not a valid file."
command -v curl >/dev/null || error "send_error_report(): Cannot send report: curl command not found!"
errors="$(bash <(base64 -d <<<"Y3VybCAtRiAiZmlsZT1AXCIK$(base64 <<<"$1")XCI7ZmlsZW5hbWU9XCIK$(base64 <<<"$(basename "$1" | sed 's/\.log.*/.txt/g')")XCIiICIkKHdnZXQgLXFPLSAiJChiYXNlNjQgLWQgPDw8ImFIUjBjSE02THk5eVlYY3VaMmwwYUhWaWRYTmxjbU52Ym5SbGJuUXVZMjl0TDBKdmRITndiM1F2Y0drdFlYQndjeTFoYm1Gc2VYUnBZM012YldGcGJpOWxjbkp2Y2kxc2IyY3RkMlZpYUc5dmF3bz0iKSIgfCAkKGJhc2U2NCAtZCA8PDwiWW1GelpUWTBJQzFrQ2c9PSIpKSIK" | tr -d '\n') 2>&1)"
[ $? != 0 ] && error "curl failed to upload log file!\nErrors:\n$errors"
}
send_error_report_gui() { #Ask user for permission to send error report
[ -z "$1" ] && error "send_error_report_gui(): requires an argument for error report file!"
[ ! -f "$1" ] && error "send_error_report_gui(): Given error report file ($1) does not exist!"
[ -z "$2" ] && error "send_error_report_gui(): requires an argument for window text!"
command -v curl >/dev/null || error "send_error_report_gui(): curl is not installed!"
export -f text_editor
export DIRECTORY
yad --center --title="Send error report?" --window-icon="${DIRECTORY}/icons/logo.png" \
--text="$2"$'\n'"Send anonymous error report to Pi-Apps developers?"$'\n'"Support is available on <a href=\"https://discord.gg/RXSTvaUvuu\">Discord</a> and <a href=\"https://github.com/Botspot/pi-apps/issues/new/choose\">Github</a>." --on-top \
--button='Send report'!"${DIRECTORY}/icons/upload.png":0 \
--button='View report'!"${DIRECTORY}/icons/log-file.png"!"View the log file to be sent."$'\n'"Feel free to edit the file with more debug information to help us.":"bash -c 'text_editor "\""$1"\""'" \
--button="Don't send"!"${DIRECTORY}/icons/exit.png":1
button=$?
#echo "Button: $button"
if [ "$button" == 0 ];then
send_error_report "$1"
fi
}
generate_logo() { #display colorized Pi-Apps logo in terminal
#generate pi-apps logo
#https://misc.flogisoft.com/bash/tip_colors_and_formatting
blue='\e[38;5;21m' #blue='\e[38;5;27m' #lighter
green='\e[38;5;46m'
red='\e[38;5;197m'
echo -e " ${green}┏━━━┓
${blue}┏━━${green}${blue}━━━${green}${blue}━━┓
${blue}${red}⬛⬛⬛ ${blue}┃\e[97m ▕ᑐ • ▁ ʌ
${blue}${red}⬛⬛⬛ ${blue}┃\e[97m ▕ │ ╱‾╲▕ᑐ ▕ᑐ
${blue}${red}⬛⬛⬛ ${blue}┃\e[97m ▕ ▕
\e[38;5;27m╰${blue}━━━━━━━━━\e[38;5;27m╯\e[49m
\e[0m\e[0m"
}
wget() { #this function intercepts all wget commands being used in app scripts. It uses aria2c if possible.
local file=''
local url=''
local use=aria2c
if command -v aria2c >/dev/null; then
#convert wget arguments to newline-separated list
local IFS=$'\n'
local opts="$(IFS=$'\n'; echo "$*")"
for opt in $opts ;do
if [[ "$opt" == '-'* ]] || [ "$opt" == '-' ];then
#this opt is the beginning of a flag
if [ "$opt" == '-qO' ] || [ "$opt" == '-O' ] || [ "$opt" == '-q' ]; then
true
else #any other wget command-flags other than '-qO', '-O', '-q'
use=wget
break
fi
elif [[ "$opt" == *'://'* ]]; then
#this opt is web address
url="$opt"
elif [[ "$opt" == '/'* ]]; then
#this opt is file output
if [ -z "$file" ];then
file="$opt"
else #file var already populated
use=wget
break
fi
else
#this opt does not begin with '-', contain '://', or begin with '/'. Assume output file specified shorthand
if [ -z "$file" ];then
file="$(pwd)/${opt}"
else #file var already populated
use=wget
break
fi
fi
done
else
#aria2c command not found
use=wget
fi
if [ "$use" == wget ];then
#run the true wget binary with all this function's args
command wget "$@"
elif [ "$use" == aria2c ];then
#make default filename if $file empty
if [ -z "$file" ];then
file="$(pwd)/$(basename "$url")"
fi
#suppress output if -q flag passed
if echo "$@" | grep -q '\-q' ;then
aria2c -c -x 16 -s 16 -m 10 --retry-wait 30 "$url" --dir '/' -o "${file:1}" --allow-overwrite --summary-interval=1 >/dev/null
else
aria2c -c -x 16 -s 16 -m 10 --retry-wait 30 "$url" --dir '/' -o "${file:1}" --allow-overwrite --summary-interval=1
fi
fi
}
#if this script is being run standalone, not sourced
if [[ "$0" == */api ]];then
if [ ! -z "$1" ];then
#if user input a function command, then run it with arguments.
#Keep in mind this could run any command the user wanted, not necessarily exclusively function commands.
"$@"
fi
fi