Last active
September 25, 2025 12:05
-
-
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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