{GUIDE} How to Easily Create .CHD Files on Linux

I have been converting my game files to .chd to save storage space and clear up all the extra .bin files in my playlists on RetroArch. I had a few questions and ran into some issues while figuring this out for myself so I wanted to share my solution so people can easily find out what to do.

This was done on Linux Mint (Ubuntu based), I don’t know if it’s the same process for other non-Ubuntu based distros.

  1. Install “Mame-tools” from your package manager or elsewhere. This will allow you to use CHDman in terminal.

  2. Go to your folder that is storing the game files for your particular game/ROM in question. You’ll likely see one or more .bin files and a .cue file.

  3. Right click on the folder background and select “Open in Terminal”.

  4. Type: chdman createcd -i “name of rom (USA) (Rev 2).cue” -o "name of rom (USA) (Rev 2).chd"

4.1. -i is the input file name with a .cue suffix, not .bin. -o (the letter, not a zero) is the output name you wish to name the file with a suffix of .chd. Be sure to name it the same thing as the -i so the game will be read properly for your RetroArch playlist and boxart. You can rename the file afterwards by right clicking on it and renaming like normal if you messed it up in terminal.

4.2. I had issues in my troubleshooting of the terminal displaying this message: bash: syntax error near unexpected token `(’ This means something along the lines that you didn’t enlcose your parantheses correctly in bash. To avoid this just be sure to follow my template above and place the " in the spots I have shown.

  1. You’ll see the terminal run and see the .chd file appear in your game folder with the other files. Once the terminal is done processing you can delete all your .bin and .cue files and just keep the .chd file.

PS. Be sure your core can read .chd files and be sure the name your chose doesn’t have any typos.

1 Like

Manual conversion tutorials that explain are always welcome. So people actually learn and understand how it works. But after a while you want to have some automation.

https://github.com/thingsiplay/tochd: I wrote a Python program (Linux only) that automates a few steps for this, such as naming the output file automatically. You need Mame-tools and 7z program.

Supports .cue, .iso and even archive files. Archives such as .7z or .zip are unpacked to convert the content of it (if it contains convertable files). In example command tochd * will read all files in directory and convert all supported files to .chd with correct name, if it does not exist already.

1 Like

Very good!

I use this script that I took from a friend of the forum and added a few more things to automate.

It can be used for a single set or several sets in batch processing.

What this does is; unzip the game, delete the compressed files, convert CUE+BIN to CHDs, delete the CUE+BIN…

At the end you are left with only the CHDs with the correct names.

7z x '*.7z'; 7z x '*.zip'; unrar x '*.rar'; rm *.zip *.7z *rar; for i in *.cue; do chdman createcd -i "$i" -o "${i%cue}chd"; done; rm *.bin *.cue

PS: Obviously, if you want to decompress all these formats you need to have the compressors installed.
sudo apt install p7zip rar unrar unace zip unzip p7zip-full p7zip-rar -y

You should be careful with those lines. Because you could end up deleting archive files that do not contain any game files to convert. Or those which have subfolders in example would fail to convert, but then you already delete the archive. The second for loop uses extractcd, which will extract any chd into its parts, then delete the .chd files too. So you end up deleting all archives, cue and the chd files! If you want automatically delete files, then I recommend at least to check if the final .chd creation exist already, to make sure it was a success.

To do this correctly, it would require a rewrite. A starting point is the following, but it does nothing else than just output the input file path. To do this right it requires careful handling, so you don’t end up losing files.

#!/usr/bin/env bash

# shopt nullglob is needed, so the bottom for loop does not use *.cue literally
# as a "file", if there is no file with that extension. This is complicated to
# explain. In short it makes the for loop work as a sane people would expect to.
shopt -s nullglob

# Loop over all files with the following extensions between the {}-brackets.
for file in *.{cue,iso,7z,zip}; do
    echo "${file}"
done

I’ve gone through all of this already, that’s why I recommend my script (just a single tochd.py script).

Thank goodness you noticed. I’ve already accommodated it. This way I use it to convert multiple BINs to single BIN (DOSBox does not support multiple BINs).

I have been using this for a long time to convert folders with redump games, I don’t think I have any problems, if there is any failure, it stops in ‘done’. Now, if you have in the folder zips of all kinds, well, it does not do the magic.

Hi!

I had to make an account just to jump into this thread, because it was helpful for me.

Nintendo Switch RetroArch currently has a bug that does not allow it to properly hash CHD files for RetroAchievements, so I had to convert a ton of my CHDs… Anyways, here’s a more robust script to convert between CHD/CUE/ISO/ZIP:

#!/bin/bash
# chdsh — Convert between CHD and other disc image formats
# Requirements: chdman (from mame-tools AUR package), unzip (for zip), 7z (for 7z), unrar (for rar).
# Usage: chdsh [options] <input> <output_format>
#
#   input         A format extension (chd, cue, gdi, iso, zip, 7z, rar)
#                 or a specific file path (e.g. game.iso)
#   output_format Target format: chd, cue, gdi, iso
#
# Options:
#   -c          Delete original file(s) after successful conversion
#   -o <dir>    Output directory (default: ./chdsh_output)
#   -r          Recurse into subdirectories when scanning for files
#   -h          Show this help and exit
#
# Supported conversions:
#   chd  → cue, gdi, iso        (chdman extractcd)
#   cue  → chd                  (chdman createcd)
#   gdi  → chd                  (chdman createcd)
#   iso  → chd                  (chdman createcd)
#   zip/7z/rar → chd            (extract CUE/BIN, then chdman createcd)
#
# Examples:
#   chdsh chd cue               Convert all .chd files in current dir to .cue
#   chdsh game.iso chd          Convert a single file to .chd
#   chdsh -c zip chd            Convert .zip archives to .chd, delete originals
#   chdsh -r chd cue            Recurse into subdirectories
#   chdsh -o /tmp/out iso chd   Custom output directory

SCRIPT_NAME=$(basename "$0")
OUTPUT_DIR="./chdsh_output"
CLEAN=false
RECURSIVE=false

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

print_help() {
cat >&2 <<'EOF'
chdsh — Convert between CHD and other disc image formats

Usage: chdsh [options] <input> <output_format>

  input         A format extension (chd, cue, gdi, iso, zip, 7z, rar)
            or a specific file path (e.g. game.iso)
  output_format Target format: chd, cue, gdi, iso

Options:
  -c          Delete original file(s) after successful conversion
  -o <dir>    Output directory (default: ./chdsh_output)
  -r          Recurse into subdirectories when scanning for files
  -h          Show this help and exit

Supported conversions:
  chd  → cue, gdi, iso        (chdman extractcd)
  cue  → chd                  (chdman createcd)
  gdi  → chd                  (chdman createcd)
  iso  → chd                  (chdman createcd)
  zip/7z/rar → chd            (extract CUE/BIN, then chdman createcd)

Examples:
  chdsh chd cue               Convert all .chd files in current dir to .cue
  chdsh game.iso chd          Convert a single file to .chd
  chdsh -c zip chd            Convert .zip archives to .chd, delete originals
  chdsh -r chd cue            Recurse into subdirectories
  chdsh -o /tmp/out iso chd   Custom output directory
EOF
}

die() {
echo "$SCRIPT_NAME: error: $*" >&2
exit 1
}

in_array() {
local val="$1"; shift
for item in "$@"; do [[ "$item" == "$val" ]] && return 0; done
return 1
}

# ---------------------------------------------------------------------------
# Option parsing
# ---------------------------------------------------------------------------

while getopts ":co:rh" opt; do
case $opt in
    c) CLEAN=true ;;
    o) OUTPUT_DIR="$OPTARG" ;;
    r) RECURSIVE=true ;;
    h) print_help; exit 0 ;;
    \?) die "Invalid option: -$OPTARG" ;;
    :)  die "Option -$OPTARG requires an argument." ;;
esac
done
shift $((OPTIND-1))

if [[ $# -lt 2 ]]; then
print_help
exit 1
fi

INPUT_ARG="$1"
OUTPUT_FORMAT="${2,,}"

# ---------------------------------------------------------------------------
# Determine input files and format
# ---------------------------------------------------------------------------

VALID_INPUTS=(chd cue gdi iso zip 7z rar)
VALID_OUTPUTS=(chd cue gdi iso)

if [[ -f "$INPUT_ARG" ]]; then
# Single-file mode
INPUT_FORMAT="${INPUT_ARG##*.}"
INPUT_FORMAT="${INPUT_FORMAT,,}"
mapfile -t FILES < <(printf '%s\n' "$INPUT_ARG")
else
# Format mode — scan current directory (optionally recursive)
INPUT_FORMAT="${INPUT_ARG,,}"
DEPTH_FLAG=()
$RECURSIVE || DEPTH_FLAG=(-maxdepth 1)
mapfile -t FILES < <(find "$(pwd)" "${DEPTH_FLAG[@]}" -type f -iname "*.${INPUT_FORMAT}" | sort)
fi

in_array "$INPUT_FORMAT" "${VALID_INPUTS[@]}" \
|| die "Unsupported input format '$INPUT_FORMAT'. Supported: ${VALID_INPUTS[*]}"

in_array "$OUTPUT_FORMAT" "${VALID_OUTPUTS[@]}" \
|| die "Unsupported output format '$OUTPUT_FORMAT'. Supported: ${VALID_OUTPUTS[*]}"

[[ "$INPUT_FORMAT" == "$OUTPUT_FORMAT" ]] && die "Input and output formats are the same ('$INPUT_FORMAT')."

# Archives can only target chd
if in_array "$INPUT_FORMAT" zip 7z rar && [[ "$OUTPUT_FORMAT" != "chd" ]]; then
die "Archive inputs (zip, 7z, rar) can only be converted to chd."
fi

# ---------------------------------------------------------------------------
# Resolve chdman subcommand
# ---------------------------------------------------------------------------

case "$INPUT_FORMAT:$OUTPUT_FORMAT" in
chd:cue|chd:gdi|chd:iso) CHDMAN_CMD="extractcd" ;;
cue:chd|gdi:chd|iso:chd) CHDMAN_CMD="createcd"  ;;
zip:chd|7z:chd|rar:chd)  CHDMAN_CMD="createcd"  ;; # applied after extraction
*) die "No conversion path from '$INPUT_FORMAT' to '$OUTPUT_FORMAT'." ;;
esac

# ---------------------------------------------------------------------------
# Dependency check (only what is actually needed)
# ---------------------------------------------------------------------------

DEPS=(chdman)
case "$INPUT_FORMAT" in
zip) DEPS+=(unzip) ;;
7z)  DEPS+=(7z)    ;;
rar) DEPS+=(unrar) ;;
esac

MISSING=()
for CMD in "${DEPS[@]}"; do
command -v "$CMD" >/dev/null 2>&1 || MISSING+=("$CMD")
done

if [[ ${#MISSING[@]} -gt 0 ]]; then
echo "$SCRIPT_NAME: missing dependencies: ${MISSING[*]}" >&2
if command -v apt >/dev/null 2>&1; then
    echo "  Install: sudo apt update && sudo apt install ${MISSING[*]}" >&2
elif command -v dnf >/dev/null 2>&1; then
    echo "  Install: sudo dnf install ${MISSING[*]}" >&2
fi
exit 1
fi

# ---------------------------------------------------------------------------
# Pre-flight summary
# ---------------------------------------------------------------------------

if [[ ${#FILES[@]} -eq 0 ]]; then
echo "$SCRIPT_NAME: no .$INPUT_FORMAT files found." >&2
exit 1
fi

echo "Conversion : $INPUT_FORMAT → $OUTPUT_FORMAT"
echo "Output dir : $OUTPUT_DIR"
$RECURSIVE && echo "Mode       : recursive"
echo ""
echo "Files:"

TOTAL_SIZE=0
for FILE in "${FILES[@]}"; do
SIZE_HR=$(du -h "$FILE" | cut -f1)
printf "  %-8s %s\n" "$SIZE_HR" "$FILE"
FILE_SIZE=$(stat --format="%s" "$FILE")
TOTAL_SIZE=$(( TOTAL_SIZE + FILE_SIZE ))
done

TOTAL_HR=$(numfmt --to=iec --suffix=B "$TOTAL_SIZE")
FREE=$(df -h "$(pwd)" | awk 'NR==2 {print $4}')

echo ""
printf "%-11s %s\n" "Files:"     "${#FILES[@]}"
printf "%-11s %s\n" "Total:"     "$TOTAL_HR"
printf "%-11s %s\n" "Free:"      "$FREE"
$CLEAN && echo "Originals  : will be DELETED after successful conversion"
echo ""

if $CLEAN; then
read -rp "Proceed? Originals will be deleted on success (y/n): " CONFIRM
else
read -rp "Proceed? (y/n): " CONFIRM
fi
[[ "$CONFIRM" != "y" ]] && echo "Aborted." && exit 0

# ---------------------------------------------------------------------------
# Conversion
# ---------------------------------------------------------------------------

mkdir -p "$OUTPUT_DIR"
SUCCESS=0
FAIL=0

convert_archive() {
local IN_FILE="$1" OUT_FILE="$2"
local TEMP_DIR
TEMP_DIR=$(mktemp -d)

# Extract based on extension
case "${IN_FILE,,}" in
    *.zip) unzip -q "$IN_FILE" -d "$TEMP_DIR" ;;
    *.7z)  7z x -y "$IN_FILE" -o"$TEMP_DIR" > /dev/null ;;
    *.rar) unrar x -o+ "$IN_FILE" "$TEMP_DIR" > /dev/null ;;
esac

# Prefer .cue (track layout) over bare .bin
local CUE_FILE
CUE_FILE=$(find "$TEMP_DIR" -type f -iname "*.cue" | head -n 1)
[[ -z "$CUE_FILE" ]] && CUE_FILE=$(find "$TEMP_DIR" -type f -iname "*.bin" | head -n 1)

local RET=0
if [[ -z "$CUE_FILE" ]]; then
    echo "  [SKIP] no CUE/BIN found in $(basename "$IN_FILE")"
    RET=1
elif chdman createcd -i "$CUE_FILE" -o "$OUT_FILE" --force 2>&1 | sed 's/^/         /'; then
    echo "  [OK]   $(basename "$IN_FILE") → $(basename "$OUT_FILE")"
else
    echo "  [FAIL] $(basename "$IN_FILE")"
    RET=1
fi

rm -rf "$TEMP_DIR"
return $RET
}

echo ""
for FILE in "${FILES[@]}"; do
BASENAME=$(basename "$FILE")
BASENAME="${BASENAME%.*}"
OUT_FILE="$OUTPUT_DIR/$BASENAME.$OUTPUT_FORMAT"

if in_array "$INPUT_FORMAT" zip 7z rar; then
    if convert_archive "$FILE" "$OUT_FILE"; then
        (( SUCCESS++ )) || true
        if $CLEAN; then
            rm -f -- "$FILE" && echo "  [DEL]  $FILE"
        fi
    else
        (( FAIL++ )) || true
    fi
else
    if chdman "$CHDMAN_CMD" -i "$FILE" -o "$OUT_FILE" --force 2>&1 | sed 's/^/         /'; then
        echo "  [OK]   $(basename "$FILE") → $(basename "$OUT_FILE")"
        (( SUCCESS++ )) || true
        if $CLEAN; then
            rm -f -- "$FILE" && echo "  [DEL]  $FILE"
        fi
    else
        echo "  [FAIL] $(basename "$FILE")"
        (( FAIL++ )) || true
    fi
fi
done

echo ""
echo "Done — success: $SUCCESS | failed: $FAIL"
[[ $FAIL -gt 0 ]] && exit 1 || exit 0

Copy to /usr/local/bin, make it executable and reload the shell (source ~/.bashrc)

Then run it from any folder. Just include the source format and destination format. For example:

chdsh chd cue This converts all CHD in current directory to CUE

By default, source file are left in place. Use the -c flag to cleanup after conversion.

1 Like

I also have a script for converting ISOs or BIN/CUEs to CHD: https://github.com/InquisitiveCoder/mkchd

The main difference in my script is that it changes the compression type to zstd and halves the hunk size to greatly speed up decompression in really underpowered systems like 3DS and MiSTer (-c cdzs,cdfl -hs 9792). Using PCSX ReARMed on a New Nintendo 3DS, the default settings caused stuttering during FMVs, and the MiSTer PSX core could only go up to x4 CD speed. With these settings the FMVs run fine and you can go up to x8 CD speed (which is the highest the core supports.)

I also tried to make it as bulletproof as possible so that it can be used to convert large collections. It uses a long random string as the filename while the CHD is being created, so even if you do something crazy like run multiple copies of the script for the same file, they won’t step on each other’s toes. If you kill it halfway through, it cleans up the temp file before exiting. And if the input is bin/cue, it only removes bin files listed in the cue file when it’s done.

2 Likes

Thanks a lot for the info!