Skip to content

Instantly share code, notes, and snippets.

@i30817
Last active September 25, 2025 12:05
Show Gist options
  • Select an option

  • Save i30817/26acab68550944b1d29aba3ef1ea4f39 to your computer and use it in GitHub Desktop.

Select an option

Save i30817/26acab68550944b1d29aba3ef1ea4f39 to your computer and use it in GitHub Desktop.
dosbox FAT16/FAT32 image disk creator and expander\shrinker. Doesn't clone the windows system partition correctly (doesn't boot).
#!/usr/bin/env bash
ex=0
hash parted 2>/dev/null || { echo >&2 "Program requires parted but it's not installed."; ex=1; }
hash numfmt 2>/dev/null || { echo >&2 "Program requires numfmt but it's not installed."; ex=1; }
hash truncate 2>/dev/null || { echo >&2 "Program requires truncate but it's not installed."; ex=1; }
hash mcopy 2>/dev/null || { echo >&2 "Program requires mtools but it's not installed."; ex=1; }
hash sed 2>/dev/null || { echo >&2 "Program requires sed but it's not installed."; ex=1; }
[ "$ex" -eq "1" ] && exit 1
ME=$(basename "$0")
function usage(){
cat >&2 <<ENDOFHELP
Usage: $ME [-h|--help] [-f|--fat32] [-c|--clone IMG] [-a|--append DIR] SIZE NAME
$ME creates a single partition FAT (CHS)
filesystem, already formated and outputs the
the dosbox imgmount and the linux mount commands.
FAT 16 is the default (dosbox readable).
The size will be restricted between ≃35MB and ≃1GB
for FAT16 and between ≃35MB and ≃7.84GB for FAT32.
Do not be surprised if a large drive gives you less
than expected space in the OS, because the cluster
size limits that enable that extra space, make the
OS waste space on larger drives because all files
must occupy at least 1 cluster, so if you have many
small files (like in the OS) it's better to use a
small drive just for it.
You can use the strings 'MIN' and 'MAX' as size.
You can use a human readable suffix for the SIZE
in SI or IEC-I standards. SI is the modern hd size
standard with powers of 10 and IEC-I is the older
hd size standard with powers of two. Bytes will be
assumed if no suffix is given.
SI goes KB, MB, GB, TB...
IEC-I goes KiB, MiB, GiB, TiB...
-h show this help
-f force FAT32 type
-c IMG clone IMG file primary partition to
the new image, preserving DOS attributes,
errors if there is not enough space, does
not work with bootable IMG
-a PATH copy path to the new image root,
if path is a folder and ends with / copy
the files inside instead.
ENDOFHELP
}
#silent mode, h or f or c or a or - followed by a 'argument' to handle long options
#(notice that all of these require - already, including '-')
while getopts ":hfca-:" opt; do
case $opt in
-)
case "${OPTARG}" in
fat32) FAT=32; ;;
clone) CLONE="${!OPTIND}"; OPTIND=$(( OPTIND + 1 )); ;;
append) COPY="${!OPTIND}"; OPTIND=$(( OPTIND + 1 )); ;;
help) usage; exit 0; ;;
*) usage; exit 1; ;;
esac;;
f) FAT=32; ;;
c) CLONE="${!OPTIND}"; OPTIND=$(( OPTIND + 1 )); ;;
a) COPY="${!OPTIND}"; OPTIND=$(( OPTIND + 1 )); ;;
h) usage; exit 0; ;;
*) usage; exit 1; ;;
esac
done
shift $((OPTIND-1))
(( "$#" != 2 )) && { usage; exit 1; }
[[ ! -d "$PWD" ]] && { echo >&2 "$ME: could not retrieve current directory. Exit"; exit 1; }
#variable checks, leave CLONE unset if unset
NAME=$(realpath -- "$2") #deleted on error, needs to created after options parsing
BASE=$(basename "$NAME")
FAT=${FAT-16} #if unset set to 16
SIZE="$1" #currently a string which may have suffixes (MB etc), needs validation
MIN_FAT=35000000
#FAT16 2gb should work, but mformat img a bit above this fail in dosbox win95, this value is from blind search
MAX_FAT16=1074000000
MAX_FAT32=8414461440 #derived from http://web.allensmith.net/Storage/HDDlimit/Int13h.htm but with 1023 heads (for a DOS hang bug)
[[ -d "$NAME" ]] && { echo >&2 "$ME: output file is a directory. Exit"; exit 1; }
[[ -f "$NAME" ]] && { echo >&2 "$ME: output file already exists. Exit"; exit 1; }
if [[ "$SIZE" == MIN ]]; then
SIZE=$MIN_FAT
elif [[ "$SIZE" == MAX ]]; then
if (( FAT == 32 )); then
SIZE=$MAX_FAT32
else
SIZE=$MAX_FAT16
fi
else
SIZE=$(LC_ALL=C numfmt --from=auto --suffix="B" "$SIZE" 2>/dev/null) \
|| { echo >&2 "$ME: could not convert size input to bytes. Exit."; exit 1; }
SIZE=${SIZE%B}
fi
if (( FAT == 16 )); then
(( SIZE >= MIN_FAT )) && (( SIZE <= MAX_FAT16 )) \
|| { echo >&2 "$ME: requested size for FAT 16 image not in [35MB-2GB] range. Exit"; exit 1; }
else
(( SIZE >= MIN_FAT )) && (( SIZE <= MAX_FAT32 )) \
|| { echo >&2 "$ME: requested size for FAT 32 image not in [35MB-8GB] range. Exit"; exit 1; }
fi
#cleanup code attached here (previous exits should NOT delete name)
function finish(){
EXIT=$? #type of exit non zero is error
FILE="$1"
(( EXIT != 0 )) && { rm -f "$FILE" 2>/dev/null; }
exit $EXIT
}
trap 'finish "$NAME"' EXIT
OFFSET=$((2048*512)) #1024KiB or 2048 sectors, offsets partition
# align to next smallest multiple of offset (that is multiple of mb)
# https://stackoverflow.com/a/29925587
SIZE=$(( (SIZE + OFFSET - 1)/OFFSET * OFFSET ))
# Number of Sectors in the filesystem that parted will give
SECTORS=$(( (SIZE-OFFSET) / 512 ))
#create fs name based on name limited to 11 chars
DNAME="${NAME##*/}"
DNAME="${DNAME%.*}"
DNAME="${DNAME:0:11}"
DNAME="${DNAME^^}"
#sparse file
truncate -s "$SIZE" "$NAME"
##create the bootable CHS partition with optimal alignment
parted "$NAME" -s -a optimal mklabel msdos mkpart primary fat"$FAT" "${OFFSET}B" "100%" set 1 lba off set 1 boot off 2>/dev/null \
|| { echo >&2 "$ME: could not partition filesystem. Exit"; exit 1; }
if (( FAT == 32 )); then #doesn't have a else on purpose
FLAG=(-F) #array expansion of nothing gives nothing unlike string
fi
#tries to create a optimal csh by just specifying sectors and limiting max size
MTOOLSRC=<( echo "drive c: file=\"$NAME\" partition=1" ) \
mformat -i "${NAME}"@@"${OFFSET}" -T $SECTORS -v "$DNAME" "${FLAG[@]}" \
|| { echo "$MTOOLSRC"; echo >&2 "$ME: error formating the filesystem. Exit"; exit 1; }
#get CHS info
INFO=$(MTOOLSRC=<( echo "drive c: file=\"$NAME\" partition=1" ) \
minfo -i "${NAME}"@@"${OFFSET}")
#parse CHS https://stackoverflow.com/a/30872943
HEA=$( sed -En '0,/^heads: ([0-9]+)$/{s/^heads: ([0-9]+)$/\1/p}' <<< "$INFO" )
SEC=$( sed -En '0,/^sectors per track: ([0-9]+)$/{s/^sectors per track: ([0-9]+)$/\1/p}' <<< "$INFO" )
CYL=$( sed -En '0,/^cylinders: ([0-9]+)$/{s/^cylinders: ([0-9]+)$/\1/p}' <<< "$INFO" )
#clone and copy dont check for size or overwrite. The img is deleted if they fail
if [[ -r "$CLONE" && -f "$CLONE" ]]; then #exists and is readable and is regular file
#mtools supplies a option to provide a 'third' config file which we're using here to redefine c and d
#without nuking or creating config files (-i is not enough for two images). This keeps attributes
MTOOLSRC=<( echo "drive c: file=\"$NAME\" partition=1"; echo "drive d: file=\"$CLONE\" partition=1" ) \
mcopy -bospmQ "d:" "c:" >/dev/null \
|| { echo >&2 "$ME: error transfering the filesystem from $CLONE. Exit"; exit 1; }
elif [[ -v CLONE ]]; then #is not file or is not readable and was assigned
echo >&2 "$ME: given clone image is not readable. Exit"; exit 1;
fi
if [[ -r "$COPY" && -e "$COPY" ]]; then #exists and is readable
if [[ -d "$COPY" && "$COPY" == */ ]]; then #is a directory and the passed string has the terminal slash
echo "Copying contents of '$COPY' into the image..."
MTOOLSRC=<( echo "drive c: file=\"$NAME\" partition=1") mcopy -bospmQ "${COPY}"* "c:" >/dev/null \
|| { echo >&2 "$ME: Failed to copy directory contents to image. Exit"; exit 1; }
else
echo "Copying '$COPY' into the image..."
MTOOLSRC=<( echo "drive c: file=\"$NAME\" partition=1") mcopy -bospmQ "${COPY}" "c:" >/dev/null \
|| { echo >&2 "$ME: Failed to copy path to image. Exit"; exit 1; }
fi
elif [[ -v COPY ]]; then #does not exist or is not readable and was assigned
echo >&2 "$ME: given copy path is not readable. Exit"; exit 1;
fi
if (( FAT == 32 )); then
cat >&2 << ENDOFHELP
Success - FAT32 can be used for dosbox windows\extra windows drive boot
mount as c (3 is d) for boot in dosbox:
imgmount 2 "$BASE" -size 512,$SEC,$HEA,$CYL -fs none
mount in linux, doesn't set DOS-unique attributes if written to:
udisksctl loop-setup -f "$BASE"
unmount in linux with DEV the device of the previous command:
udisksctl unmount -b "DEVp1"
clone/shrink/expand image: use the -c flag, it preserves attributes
ENDOFHELP
else
cat >&2 << ENDOFHELP
Success
mount as c in dosbox:
imgmount c "$BASE" -size 512,$SEC,$HEA,$CYL
mount in linux, doesn't set DOS-unique attributes if written to:
udisksctl loop-setup -f "$BASE"
unmount in linux with DEV the device of the previous command:
udisksctl unmount -b "DEVp1"
clone/shrink/expand image: use the -c flag, it preserves attributes
ENDOFHELP
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment