Skip to content

Instantly share code, notes, and snippets.

@goisneto
Last active May 5, 2026 19:01
Show Gist options
  • Select an option

  • Save goisneto/a31e6b607369f3025694ba2e9556d9f3 to your computer and use it in GitHub Desktop.

Select an option

Save goisneto/a31e6b607369f3025694ba2e9556d9f3 to your computer and use it in GitHub Desktop.
This script is for Linux users who need to use Heimdall via SSH and, without many alternatives, do everything manually. You're tired of updating your phone and forgetting to add a partition to the Heimdall command line, leading to your old Samsung phone crashing and bugging for a long time without you knowing it was because you only updated part…
#!/bin/bash
#ANSI escape codes:
#No Color 0
#Black 0;30 Dark Gray 1;30
#Red 0;31 Light Red 1;31
#Green 0;32 Light Green 1;32
#Brown/Orange 0;33 Yellow 1;33
#Blue 0;34 Light Blue 1;34
#Purple 0;35 Light Purple 1;35
#Cyan 0;36 Light Cyan 1;36
#Light Gray 0;37 White 1;37
color_remove_surrounded_quotes(){
set +x
local \
TEXT="$*" \
colors_r=(
'\{r(ed)?\}'
'\{g(reen)?\}'
'\{y(ellow)?\}'
'\{b(lue)?\}'
'\{n(ocolor)?\}'
) \
_IFS="$IFS" \
IFS='|' \
sed_r;
sed_r=(
"s/^'([^']+)'$/\1/gI;"
"s/^(${colors_r[*]})'([^']+)'$/\1\2/gI;"
"s/^'([^']+)'(${colors_r[*]})$/\1\2/gI;"
"s/^(${colors_r[*]})'([^']+)'(${colors_r[*]})$/\1\2\3/gI;"
"s/^\"([^\"]+)\"$/\1/gI;"
"s/^(${colors_r[*]})\"([^\"]+)\"$/\1\2/gI;"
"s/^\"([^\"]+)\"(${colors_r[*]})$/\1\2/gI;"
"s/^(${colors_r[*]})\"([^\"]+)\"(${colors_r[*]})$/\1\2\3/gI;"
)
IFS="${_IFS}"
if [ $# -gt 0 ]; then
sed -r "${sed_r[*]}" <<<"${TEXT}"
else
sed -r "${sed_r[*]}"
fi
}
color_echo() {
set +x
local \
TEXT="$*" \
R=$'\e[31m' \
G=$'\e[32m' \
Y=$'\e[33m' \
B=$'\e[34m' \
N=$'\e[0m' # No Color
local sed_r=(
"s/\{r(ed)?\}/${R}/gI;"
"s/\{g(reen)?\}/${G}/gI;"
"s/\{y(ellow)?\}/${Y}/gI;"
"s/\{b(lue)?\}/${B}/gI;"
"s/\{n(ocolor)?\}/${N}/gI;"
)
if [ $# -gt 0 ]; then
color_remove_surrounded_quotes "${TEXT}{N}" | sed -r "${sed_r[*]}"
else
color_remove_surrounded_quotes | sed -r "${sed_r[*]}"
fi
}
color_read() {
set +x
local TEXT="$1"
shift
read -p "$(color_echo "${TEXT}")" "$@"
}
args_parser(){
set +x
local i
for (( i=1 ; i <= $# ; i++ )); do
case "${!i}" in
'--heimdall-binary=*')
cat <<-EOF | color_remove_surrounded_quotes
"HEIMDALL_BINARY_BY_ARG='='"
"HEIMDALL_BINARY='${!i//--heimdall-binary=/}'"
EOF
;;
'--heimdall-binary')
i++
cat <<-EOF | color_remove_surrounded_quotes
"HEIMDALL_BINARY_BY_ARG=' '"
"HEIMDALL_BINARY='${!i}'"
EOF
;;
'--workdir=*')
cat <<-EOF | color_remove_surrounded_quotes
"WORKDIR='${!i//--workdir=/}'"
EOF
;;
'--workdir')
i++
cat <<-EOF | color_remove_surrounded_quotes
"WORKDIR='${!i}'"
EOF
;;
'--pit=*')
cat <<-EOF | color_remove_surrounded_quotes
"PIT='${!i//--PIT=/}'"
EOF
;;
'--pit')
i++
cat <<-EOF | color_remove_surrounded_quotes
"PIT='${!i}'"
EOF
;;
'--help'|'--usage')
local -n heimdall='HEIMDALL_BINARY' workdir='WORKDIR' heimdall_by_arg='HEIMDALL_BINARY_BY_ARG'
local script_name="${BASH_SOURCE[0]}"
if [ -z "${script_name}" ]; then
script_name="$0"
fi
script_name="$(basename "${script_name}")"
color_echo <<-EOF >&2
"{G}Usage: {Y}${script_name} {G}<arguments>... {R}<extra arguments>..."
"\v"
"{B}Arguments:"
" {B}[{G}--heimdall-binary={Y}<path to Heimdall binary>{B}|{G}--heimdall-binary {Y}<path to Heimdall binary>{B}] (This is optional if the Heimdall binary is not in your {Y}PATH{B} environment variable)"
" {B}[{G}--workdir={Y}<path to work directory>{B}|{G}--workdir {Y}<path to work directory>{B}] (This is optional if you are already in the directory you want to work in. Note that this is where firmware is located, where data will be unpacked, and where files will be linked for flashing. {Y}It is highly recommended that this directory be used exclusively for this purpose. Failures can occur and data can be lost if it is a general-purpose directory. Furthermore, this directory is not cleaned at the end of the script or if errors occur, so you can understand any errors by reading the logs created here and use the script's guidance to flash manually to correct them.)"
" {B}[{G}--pit={Y}<path to device pit file>{B}|{G}--pit {Y}<path to device pit file>{B}] (This is optional and {G}highly recommended NOT to use. {Y}Normally, this file is provided by every firmware package. If not, it can be automatically downloaded from the device. {R}If you pass the wrong pit file, it can destroy the device partitions and brick the device.)"
" {B}[{G}--help{B}|{G}--usage{B}] (This message is shown, and if the Heimdall binary is found, its help menu is printed as well so you can look for extra arguments)"
"\v"
"{G}*{B}All extra arguments are passed directly to {Y}'heimdall (flash|check) [EXTRA ARGUMENTS]...' {B}when these commands run at the last step of this script. \v"
EOF
heimdall_binary_resolver
if [ -n "${heimdall}" ]; then
heimdall --help >&2
fi
echo 'exit 0'
;;
*)
cat <<-EOF | color_remove_surrounded_quotes
"APPEND_ARGS+=( '${!i}' )"
EOF
;;
esac
done
}
find_workdir(){
local -n workdir='WORKDIR'
if [ -z "${workdir}" ]; then
color_echo <<-EOF
"{R}You did not choose a work directory."
"{B}Here are three options to proceed:"
"{Y} You can enter the work directory path now."
"{G} You can leave it blank or type {Y}'.'{G} to use the current directory as the work directory."
"{B} Or you can press {Y}CTRL+C {B}or type one of these optional phrases exactly as mentioned to exit. You can then try again with a work directory in mind, set in the {Y}'WORKDIR'{B} environment variable, or by passing {G}--workdir{R}( |=){Y}[WORK DIRECTORY PATH]{B}. These are the exact phrases you can choose to type (without the quotes {Y}\"'\"{B}, obviously) to do this:"
" {G}1. {Y}'just stopping here i am out for now see you later'"
" {B}2. {Y}'i would prefer to decline providing a directory path at this moment so please terminate this session'"
" {R}3. {Y}'the user has elected to terminate the execution process rather than specifying a working directory path through the command line interface or environment variables at this time'"
EOF
color_read '{B}Enter your choice: ' workdir
case "${workdir}" in
'just stopping here i am out for now see you later'| \
'i would prefer to decline providing a directory path at this moment so please terminate this session'| \
'the user has elected to terminate the execution process rather than specifying a working directory path through the command line interface or environment variables at this time' \
)
return -1
;;
'')
workdir='.'
;;
esac
fi
if [ -z "${workdir}" ]; then
workdir='.'
fi
if [ -n "${workdir}" ] && [ -L "${workdir}" ]; then
color_echo "{Y}'${workdir}' {B}is a symbolic link. Using the realpath command to resolve the real path to the work directory." >&2
set -x
workdir="$(realpath "${workdir}")"
set +x
fi
if [ -n "${workdir}" ] && [ ! -d "${workdir}" ]; then
color_echo "{R}The work directory path {Y}'${workdir}' {R}is not a directory. It must exist as a directory and contain the firmware files (packed or unpacked) to perform a flash with this script." >&2
return -1
fi
}
heimdall_binary_resolver(){
local -n heimdall='HEIMDALL_BINARY' workdir='WORKDIR' heimdall_by_arg='HEIMDALL_BINARY_BY_ARG'
if [ -n "${heimdall}" ]; then
if [ -L "${heimdall}" ]; then
color_echo "{Y}'${heimdall}' {B}is a symbolic link. Using the realpath command to resolve the real path to the binary." >&2
set -x
heimdall="$(realpath "${heimdall}")"
set +x
fi
if [ -d "${heimdall}" ]; then
case "${heimdall_by_arg}" in
' '|'=')
color_echo "{R}You passed the argument {B}--heimdall-binary${heimdall_by_arg}{Y}'${heimdall}'{R}, but it is a directory. Please find the exact binary you want to use and pass the full path to it (not the directory containing the binary), and try executing this script again..." >&2
return -2
;;
*)
color_echo "{R}You passed the {B}HEIMDALL_BINARY {R}environment variable as {Y}'${heimdall}'{R}, but it is a directory. Please find the exact binary you want to use and pass the full path to it (not the directory containing the binary), and try executing this script again..." >&2
return -2
;;
esac
fi
if [ ! -f "${heimdall}" ] || [ ! -x "$HEIMDALL_BINARY" ]; then
case "${heimdall_by_arg}" in
' '|'=')
color_echo "{R}You passed the argument {B}--heimdall-binary${heimdall_by_arg}{Y}'${heimdall}'{R}, but it does not have the executable bit permission set. Try setting the executable bit permission using a command like 'chmod +x \"${HEIMDALL_BINARY}\"', and then try executing this script again..." >&2
return -2
;;
*)
color_echo "{R}You passed the {B}HEIMDALL_BINARY {R}environment variable as {Y}'${heimdall}'{R}, but it does not have the executable bit permission set. Try setting the executable bit permission using a command like 'chmod +x \"${HEIMDALL_BINARY}\"', and then try executing this script again..." >&2
return -2
;;
esac
fi
fi
if [ -z "${heimdall}" ]; then
set -x
heimdall="$(which heimdall 2> /dev/null || command -v heimdall 2>/dev/null)"
if [ -z "${heimdall}" ]; then
color_echo "{R}Heimdall binary not found. Install it first (for example, using the Debian apt command: {Y}'sudo apt install heimdall-flash'{R}) and try running this script again. If you know the binary path and do not want to add it to your {B}PATH {R}environment variable, you can manually set the {B}HEIMDALL_BINARY {R}environment variable before running this script and we will use it, or pass it as an argument using {Y}'--heimdall-binary=[binary path]'{R}." >&2
return -1
fi
fi
}
heimdall(){
set +x
local -n heimdall='HEIMDALL_BINARY' workdir='WORKDIR'
local text=''
if [ -n "${PRE_EXPLAIN}" ]; then
text="{G}${PRE_EXPLAIN}. {B}"
fi
text+="Heimdall is executing this command line"
if [ -n "${POS_EXPLAIN}" ]; then
text+=" {G}${POS_EXPLAIN}{B}"
fi
if [ -n "${heimdall}" ]; then
color_echo "{B}${text}: {Y}${heimdall} $@" >&2
if [ -n "${workdir}" ]; then
set -x
"${heimdall}" "$@" 2> >(tee -a "${workdir}/heimdall.error.log") > >(tee -a "${workdir}/heimdall.output.log")
set +x
else
"${heimdall}" "$@"
fi
else
color_echo "{R}An error occurred and the Heimdall path was lost from its internal script variable {Y}'\$heimdall'{R}. Someone, possibly the {B}CIA {R}or {B}China{R}, is messing with your computer's memory, or I'm crazy and this entire script has never worked... {G}ET Bilu says: {B}Seek knowledge..." >&2
return 51
fi
}
find_and_extract_zip(){
local -n workdir='WORKDIR'
color_echo "{B}Finding Firmware {G}ZIP {B}file and extracting it..." >&2
set -x
find "${workdir}" -maxdepth 1 -mindepth 1 -type f -name '*.zip' -print0 | xargs -r -0 -P$(nproc) -n1 sh -c 'unzip "$0" && rm "$0"' ||
color_echo "{R}Firmware {Y}ZIP {R}file not found. {G}Considering it already extracted and continuing..." >&2
set +x
}
find_and_extract_tar_dot_md5(){
local -n workdir='WORKDIR'
color_echo "{B}Finding Firmware {G}TAR.MD5 {B}files and extracting them..." >&2
set -x
find "${workdir}" -maxdepth 1 -mindepth 1 -type f -name '*.tar.md5' -print0 | xargs -r -0 -P$(nproc) -n1 sh -c 'tar_dir="$(dirname "$0")/$(basename "${0%.tar.md5}")"; mkdir -p "${tar_dir}"; tar -C "${tar_dir}" -xvf "$0" && rm "$0"' ||
color_echo "{R}Firmware {Y}TAR.MD5 {R}files not found. {G}Considering them already extracted and continuing..." >&2
set +x
}
find_and_extract_lz4(){
local -n workdir='WORKDIR'
color_echo "{B}Finding Firmware {G}LZ4 {B}files and extracting them..." >&2
set -x
find "${workdir}" -type f -name '*.lz4' -print0 | xargs -r -0 -P$(nproc) -n1 sh -c 'unlz4 --rm "${0}" "${0%.lz4}"' ||
color_echo "{R}Firmware {Y}LZ4 {R}files not found. {G}Considering them already extracted and continuing..." >&2
set +x
}
find_pit(){
local -n workdir='WORKDIR' pit='PIT'
local external_pit
if [[ "${pit}" != 'download-internal-pit' ]]; then
if [ -z "${pit}" ]; then
color_echo "{B}Finding Firmware {G}PIT (*.pit) {B}file and detecting relations between files and partitions..." >&2
set -x
pit="$(find "${workdir}" -type f -name '*.pit')"
set +x
else
color_echo "{B}Using the Firmware {G}PIT {Y}'${PIT}' {B}file passed from the argument {G}--pit{B} and detecting relations between files and partitions..." >&2
external_pit='true'
fi
else
pit=''
fi
if [ -z "${pit}" ]; then
color_echo "{Y}PIT (*.pit) {R}file not found among the firmware files. {G}Downloading it from the device. {B}You may need to restart back into download mode after this to perform the flash..." >&2
if ! heimdall download-pit --output "${workdir}/device.pit" --no-reboot --verbose --stdout-errors --wait --usb-log-level debug; then
color_echo "{R}Cannot download {Y}pit{R}. Check the output logs and try again..." >&2
return -1
else
pit="${workdir}/device.pit"
fi
fi
if ! ( heimdall print-pit --file "$PIT" | grep -E '^(Partition Name|Flash Filename):' | tee "${workdir}/part2file.txt" ); then
if [[ "${external_pit}" == 'true' ]]; then
color_echo "{R}Cannot read the {Y}'${PIT}' {R}file. It may be corrupt. Since this is a manually defined pit path, attempting to ignore this and search for another pit file or download the device pit." >&2
pit=''
PIT=''
find_pit
elif [[ "${pit}" != "${workdir}/device.pit" ]]; then
color_echo "{R}Cannot read the {Y}'${PIT}' {R}file. It may be corrupt. Attempting to ignore this and force-download the device pit." >&2
pit='download-internal-pit'
PIT='download-internal-pit'
find_pit
else
color_echo "{R}Cannot read the {Y}'${PIT}' {R}file. It may be corrupt. You can {Y}delete it and run the script again to download it from the device, {R}or {B}check the output logs {R}and try again..." >&2
return -1
fi
fi
}
find_relations_of_partitions_and_files(){
local -n workdir='WORKDIR'
color_echo "{B}Finding relations of files and partitions without a pair or with a non-existent pair file, and removing them from the list..." >&2
set -x
(
grep --no-group-separator -A1 -E '(Partition Name:) *$' "${workdir}/part2file.txt"
grep --no-group-separator -B1 -E '(Flash Filename:) *$' "${workdir}/part2file.txt"
) | sort -u | tee "${workdir}/part2file.remove.txt"
sed -r -i 's/$/$/g' "${workdir}/part2file.remove.txt"
grep -vf "${workdir}/part2file.remove.txt" "${workdir}/part2file.txt" > "${workdir}/part2file.txt.1"
rm "${workdir}/part2file.remove.txt"
mv "${workdir}/part2file.txt.1" "${workdir}/part2file.txt"
cut -d: -f2- "${workdir}/part2file.txt" | xargs -r -n2 > "${workdir}/part2file.txt.1"
mv "${workdir}/part2file.txt.1" "${workdir}/part2file.txt"
set +x
}
caution_user_to_userdata_format_partition(){
local -n workdir='WORKDIR'
local remove_userdata=''
while [[ "${remove_userdata,,}" != "y" ]] && [[ "${remove_userdata,,}" != "n" ]]; do
remove_userdata='y'
color_read '{B}Remove the USERDATA partition to maintain your user data? {N}[{G}Y{N}/{R}n{N}] ' remove_userdata
remove_userdata="${remove_userdata,,}"
if [ -z "${remove_userdata}" ]; then
remove_userdata='y'
fi
if [[ "${remove_userdata,,}" == "n" ]]; then
local not_remove_userdata='n'
color_read '{Y}You chose NOT to remove the USERDATA partition from the flashing process. This means formatting all user data and losing all media, apps, and data stored on the device. Do you really want this? {N}[{G}N{N}/{R}y{N}] ' not_remove_userdata
not_remove_userdata="${not_remove_userdata,,}"
if [[ "${not_remove_userdata,,}" == "n" ]]; then
remove_userdata='y'
fi
unset not_remove_userdata
fi
if [[ "${remove_userdata,,}" == "y" ]]; then
set -x
sed -i '/USERDATA/d;/userdata/d' "${workdir}/part2file.txt"
set +x
break
fi
done
unset remove_userdata
}
create_symboliclinks_of_files_neededs_to_flash_on_workdir(){
local -n workdir='WORKDIR'
color_echo "{B}Creating symbolic links for all needed files on the list in {Y}'${workdir}' {B}so they can be easily found when flashing..." >&2
set -x
cut -d' ' -f2- "${workdir}/part2file.txt" | xargs -r -n1 find "${workdir}" -type f -name | xargs -r -n1 sh -c 'ln -fs $0 '"${workdir}"
set +x
}
adding_argument_double_crossbar(){
local -n workdir='WORKDIR'
color_echo "{B}The Heimdall flash command line requires passing the partition name with a double hyphen, like {Y}'--[PARTITION NAME]'{B}. We are now adding this double hyphen to the start of every line in {Y}'${workdir}/part2file.txt' {B}to make it suitable for the Heimdall flash command." >&2
set -x
sed -r -i 's/^/--/g' "${workdir}/part2file.txt"
set +x
}
create_symboliclink_from_pit_file_on_workdir_as_indicated_on_relations_partitions_and_files(){
local -n workdir='WORKDIR'
color_echo "{B}Creating a symbolic link from {Y}'$PIT' {B}to the file name on the list in {Y}'${workdir}' {B}so it can be easily found when flashing..." >&2
set -x
ln -fs "$(find "${workdir}" -type f -name '*.pit')" "$(cut -d' ' -f2- < "${workdir}/part2file.txt" | grep -E '\.pit$')"
set +x
}
remove_all_relations_of_partitions_and_files_not_founded_or_inexistent(){
local -n workdir='WORKDIR'
color_echo "{B}Removing all files that were not found or cannot be linked to {Y}'${workdir}' {B}from the list..." >&2
set -x
grep -E "($(find "${workdir}" -maxdepth 1 -mindepth 1 -type l | xargs -n1 basename | xargs -r | tr ' ' '|'))" "${workdir}/part2file.txt" > "${workdir}/part2file.txt.1"
mv "${workdir}/part2file.txt.1" "${workdir}/part2file.txt"
set +x
}
do_flash(){
local -n heimdall='HEIMDALL_BINARY' workdir='WORKDIR' append_args='APPEND_ARGS'
color_echo "{G}Flashing. {B}You may need to manually reboot into download mode if an error occurs. Showing all logs so you can understand any errors and try to correct them before rebooting... Logs are saved to {Y}'${workdir}/heimdall.output.log' {B}for normal outputs and {Y}'${workdir}/heimdall.error.log' {B}for error outputs. Note that not all output in stderr is a real error, and the same applies to stdout. It is recommended to read both to correctly diagnose any errors if they occur..." >&2
if [ -n "$heimdall" ]; then
heimdall_flash=(
flash
$(xargs -r < "${workdir}/part2file.txt")
--wait
#--verbose
#--stdout-errors
#--usb-log-level debug
#--skip-size-check
#--resume
"${append_args[@]}"
)
heimdall_check=(
detect
#--verbose
#--stdout-errors
#--usb-log-level debug
--wait
"${append_args[@]}"
)
color_echo "{B}This command line is the flashing execution if you choose to stop here and run it manually in the console: {Y}${heimdall} ${heimdall_flash[@]}" >&2
is_ready='y'
color_read '{B}Ready to do it? {N}[{G}Y{N}/{R}n{N}] ' is_ready
is_ready="${is_ready,,}"
if [ -z "${is_ready}" ] || [[ "${is_ready,,}" == 'y' ]]; then
while ! PRE_EXPLAIN='Starting Heimdall flash command' POS_EXPLAIN='in a loop until successful' heimdall "${heimdall_flash[@]}"; do
PRE_EXPLAIN='Heimdall seems not to be done yet' POS_EXPLAIN='to check if it is ready' heimdall "${heimdall_check[@]}"
done
color_echo "{G}Flashing done... Yay, woohoo! {B}Removing the partition-to-file list relation in {Y}'${workdir}/part2file.txt' {B}to clean up for next time..." >&2
set -x
rm "${workdir}/part2file.txt" &&
color_echo "{B}File removed. Bye... {G}See you next time." >&2
set +x
else
color_echo "{R}You chose not to continue the flashing script, {B}but we are leaving {Y}'${workdir}/part2file.txt'{B}. It contains all the partition and file relation arguments if you decide to edit it, change any argument, and run Heimdall flash manually... {Y}Bye... {G}See you next time..." >&2
fi
else
color_echo "{R}An error occurred and the Heimdall path was lost from its internal script variable {Y}'\$heimdall'{R}. Someone, possibly the {B}CIA {R}or {B}China{R}, is messing with your computer's memory, or I'm crazy and this entire script has never worked... {G}ET Bilu says: {B}Seek knowledge..." >&2
return 51
fi
}
APPEND_ARGS=()
eval "$(args_parser "$@")"
find_workdir || exit
heimdall_binary_resolver || exit
find_and_extract_zip
find_and_extract_tar_dot_md5
find_and_extract_lz4
find_pit || exit
find_relations_of_partitions_and_files
caution_user_to_userdata_format_partition
create_symboliclinks_of_files_neededs_to_flash_on_workdir
adding_argument_double_crossbar
create_symboliclink_from_pit_file_on_workdir_as_indicated_on_relations_partitions_and_files
remove_all_relations_of_partitions_and_files_not_founded_or_inexistent
do_flash || exit
@goisneto
Copy link
Copy Markdown
Author

goisneto commented May 5, 2026

Some nonsense was added as a joke, just to make life difficult for those who do things without reading, such as the workspace options. Other workarounds were made solely for the purpose of making the code look nice in the IDE (nano/vim), such as the possibility of using each line starting with a "double quote" within the heredoc (EOF), which are later removed in the function that makes everything colorful and cute.

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