diff options
| author | Dennis Fink | 2026-02-22 08:41:04 +0100 |
|---|---|---|
| committer | Dennis Fink | 2026-02-22 08:41:24 +0100 |
| commit | d6b6cb1bfd71e95aeda2e784c214ddffcc777fd1 (patch) | |
| tree | 4ca07090f28242764425f6416c10b0203334da29 | |
| download | transcode.sh-d6b6cb1bfd71e95aeda2e784c214ddffcc777fd1.tar.gz transcode.sh-d6b6cb1bfd71e95aeda2e784c214ddffcc777fd1.zip | |
feat: initial release of transcode.sh v1.0.0
Batch transcode helper for media files using ffmpeg. Encoding parameters
are supplied by user-defined preset files (shell snippets that set an
`ffargs` array) stored under
${XDG_CONFIG_HOME:-$HOME/.config}/transcode.sh/presets/.
Key capabilities:
- Probes input codec and pixel format via ffprobe; derives an output
pixel format that preserves chroma subsampling and bit depth.
- Per-user and per-invocation codec skip lists to leave already-encoded
files untouched.
- Atomic in-place replacement: encodes to a temp file in the same
directory, then renames on success.
- --only-if-smaller: discard the result if it is larger than the
original.
- --saving: append filename, original/new sizes and percentage saved to
a log file after each successful encode.
- --dry-run, --verbose, --quiet, --nice and --continue flags.
- Respects the NO_COLOR standard.
- Script hardening: strict PATH, safe IFS, errtrace, nounset, pipefail.
Also included:
- Man page (transcode.sh.1)
- Bash completion script (transcode.sh.bash-completion)
- README.md
- .gitmessage commit message template
License: BSD-3-Clause
Diffstat (limited to '')
| -rw-r--r-- | .gitignore | 204 | ||||
| -rw-r--r-- | .gitmessage | 58 | ||||
| -rw-r--r-- | README.md | 176 | ||||
| -rwxr-xr-x | transcode.sh | 746 | ||||
| -rw-r--r-- | transcode.sh.1 | 274 | ||||
| -rw-r--r-- | transcode.sh.bash-completion | 149 |
6 files changed, 1607 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..28c0e4b --- /dev/null +++ b/.gitignore @@ -0,0 +1,204 @@ +# Created by https://www.toptal.com/developers/gitignore/api/windows,linux,macos,zsh,fish,vim,emacs,git +# Edit at https://www.toptal.com/developers/gitignore?templates=windows,linux,macos,zsh,fish,vim,emacs,git + +### Emacs ### +# -*- mode: gitignore; -*- +*~ +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +*.elc +auto-save-list +tramp +.\#* + +# Org-mode +.org-id-locations +*_archive + +# flymake-mode +*_flymake.* + +# eshell files +/eshell/history +/eshell/lastdir + +# elpa packages +/elpa/ + +# reftex files +*.rel + +# AUCTeX auto folder +/auto/ + +# cask packages +.cask/ +dist/ + +# Flycheck +flycheck_*.el + +# server auth directory +/server/ + +# projectiles files +.projectile + +# directory configuration +.dir-locals.el + +# network security +/network-security.data + + +### Fish ### +fishd.* +fish_history +fish_variables +config.local.fish + +### Git ### +# Created by git for backups. To disable backups in Git: +# $ git config --global mergetool.keepBackup false +*.orig + +# Created by git when using merge tools for conflicts +*.BACKUP.* +*.BASE.* +*.LOCAL.* +*.REMOTE.* +*_BACKUP_*.txt +*_BASE_*.txt +*_LOCAL_*.txt +*_REMOTE_*.txt + +### Linux ### + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon
+ +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Vim ### +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### Zsh ### +# Zsh compiled script + zrecompile backup +*.zwc +*.zwc.old + +# Zsh completion-optimization dumpfile +*zcompdump* + +# Zsh history +.zsh_history + +# Zsh sessions +.zsh_sessions + +# Zsh zcalc history +.zcalc_history + +# A popular plugin manager's files +._zinit +.zinit_lstupd + +# zdharma/zshelldoc tool's files +zsdoc/data + +# robbyrussell/oh-my-zsh/plugins/per-directory-history plugin's files +# (when set-up to store the history in the local directory) +.directory_history + +# MichaelAquilina/zsh-autoswitch-virtualenv plugin's files +# (for Zsh plugins using Python) +.venv + +# Zunit tests' output +/tests/_output/* +!/tests/_output/.gitkeep + +# End of https://www.toptal.com/developers/gitignore/api/windows,linux,macos,zsh,fish,vim,emacs,git diff --git a/.gitmessage b/.gitmessage new file mode 100644 index 0000000..571fb50 --- /dev/null +++ b/.gitmessage @@ -0,0 +1,58 @@ +# <type>[optional scope]: <short description> (max 72 chars total) ------>| +# +# Types (choose ONE): +# feat New feature (MINOR bump in SemVer) +# fix Bug fix (PATCH bump) +# docs Documentation only (README, man page, comments) +# style Formatting, whitespace — no logic change +# refactor Code restructure — no feature/fix +# perf Performance improvement +# test Tests only +# build Build system / dependency changes +# ci CI/CD configuration +# chore Maintenance (release commits, tooling) +# revert Revert a previous commit +# +# Scopes (optional, in parentheses after type): +# core Main script logic / encode_one() +# preset Preset loading / preset files +# color Terminal color / NO_COLOR handling +# cli Option parsing / help / version output +# skip Codec skip-list handling +# saving File-size savings logging +# docs Documentation (man page, README) +# completion Bash completion script +# +# Breaking changes: append ! before the colon → feat(cli)!: ... +# -------------------------------------------------------------------------- + +# [blank line — required before body] + +# Body (optional): explain WHY, not WHAT. Wrap at 72 chars. ---->| +# +# Motivation for the change, background, trade-offs considered, etc. +# Use multiple paragraphs separated by blank lines if needed. + +# [blank line before footers] + +# Footers (optional): +# BREAKING CHANGE: <description of what breaks and how to migrate> +# Fixes: #<issue> +# Refs: #<issue>, <commit-sha> +# Reviewed-by: Name <email> +# Co-authored-by: Name <email> +# +# -------------------------------------------------------------------------- +# Examples: +# +# feat(preset): add support for passing raw ffmpeg flags via env var +# +# fix(core): prevent tmp file leak when ffprobe exits non-zero +# +# docs: add man page and bash completion script +# +# feat(cli)!: rename --encodefile to --file-list for clarity +# +# BREAKING CHANGE: --encodefile/-f is now --file-list/-F. Update any +# scripts or aliases that use the old flag. +# -------------------------------------------------------------------------- diff --git a/README.md b/README.md new file mode 100644 index 0000000..65db49a --- /dev/null +++ b/README.md @@ -0,0 +1,176 @@ +# transcode.sh + +> Batch transcode helper for media files using [ffmpeg](https://ffmpeg.org/). + +`transcode.sh` re-encodes media files in place using user-defined *presets* — +small shell snippets that supply the ffmpeg arguments. It probes each input +file to detect the video codec and pixel format, skips files whose codec is on +the skip list, selects an appropriate output pixel format automatically, and +replaces the original atomically on success. + +## Features + +- **Preset-driven** — encoding parameters live in plain shell files, not + hard-coded flags. +- **Batch processing** — pass files on the command line or via a list file + (`-f`). +- **Codec skip list** — per-user and per-invocation lists of codecs to leave + untouched (e.g. skip files already encoded as AV1). +- **Only-if-smaller mode** (`-l`) — discard the re-encoded file if it is + larger than the original. +- **Savings log** (`-s`) — append filename, original size, new size and + percentage saved to a log file after each successful encode. +- **Dry-run** (`-n`) — preview what would happen without touching any files. +- **Nice** — runs ffmpeg at niceness 19 by default to avoid starving other + processes; configurable with `-N`. +- **Respects [NO\_COLOR](https://no-color.org/)**. + +## Requirements + +| Tool | Required | Notes | +|------|----------|-------| +| `ffmpeg` | always | encoding | +| `ffprobe` | always | codec/format detection | +| `nice` | always | process priority control | +| `bc` | only with `--saving` | percentage calculation | + +## Installation + +```sh +# Clone the repo +git clone https://codeberg.org/yourname/transcode.sh.git +cd transcode.sh + +# Make the script executable +chmod +x transcode.sh + +# Optional: install to a directory in your PATH +install -Dm755 transcode.sh ~/.local/bin/transcode.sh + +# Optional: install the man page +install -Dm644 transcode.1 ~/.local/share/man/man1/transcode.1 + +# Optional: install bash completion +install -Dm644 transcode.sh.bash-completion \ + /etc/bash_completion.d/transcode.sh +# or for your user only (requires bash-completion ≥ 2.2): +install -Dm644 transcode.sh.bash-completion \ + ~/.local/share/bash-completion/completions/transcode.sh +``` + +> **PATH note:** the script resets `PATH` to `/bin:/usr/bin:/usr/local/bin` +> for security. If your `ffmpeg` lives elsewhere (e.g. Homebrew on macOS, +> Nix), prepend its directory: +> ```sh +> PATH="/opt/homebrew/bin:$PATH" transcode.sh input.mp4 +> ``` + +## Configuration + +### Presets + +Presets live in: + +``` +${XDG_CONFIG_HOME:-$HOME/.config}/transcode.sh/presets/<name>.sh +``` + +Each preset is sourced as Bash and must define an array named `ffargs`. +The variable `output_pixel_format` is available and contains the +automatically selected pixel format (derived from the input file). + +**Example — `~/.config/transcode.sh/presets/default.sh`:** + +```sh +ffargs=( + -c:v:0 libsvtav1 + -crf 30 + -preset 6 + -pix_fmt "$output_pixel_format" +) +``` + +> **Security:** presets are executed as shell code. Only use presets from +> trusted sources. + +### Codec skip list + +To permanently skip files that are already in a particular codec, add codec +names (one per line) to: + +``` +${XDG_CONFIG_HOME:-$HOME/.config}/transcode.sh/skip.conf +``` + +**Example — skip files already encoded as AV1 or HEVC:** + +``` +av1 +hevc +``` + +## Usage + +``` +transcode.sh [OPTION] [--] FILE... +``` + +| Option | Description | +|--------|-------------| +| `-c`, `--continue` | Continue to next file if ffmpeg fails | +| `-f PATH`, `--encodefile PATH`, `--encodefile=PATH` | Read file list from PATH (one per line) | +| `-n`, `--dry-run` | Show what would be done; don't run ffmpeg | +| `-N VALUE`, `--nice VALUE`, `--nice=VALUE` | Niceness value for ffmpeg (default: 19) | +| `-s`, `--saving` | Log filesize savings after each encode | +| `--saving-file PATH`, `--saving-file=PATH` | Savings log path (default: `transcode_savings`) | +| `-S LIST`, `--skip-codec LIST`, `--skip-codec=LIST` | Codecs to skip (comma/space/colon separated) | +| `-l`, `--only-if-smaller` | Only replace original if new file is smaller | +| `-p NAME`, `--preset NAME`, `--preset=NAME` | Preset to use (default: `default`) | +| `-q`, `--quiet` | Suppress all output (overrides `-v`) | +| `-v`, `--verbose` | More detailed output | +| `--color` / `--no-color` | Force or disable colored output | +| `--version` | Print version information | +| `-h`, `-?`, `--help` | Show help and exit | + +### Examples + +```sh +# Transcode a single file with the default preset +transcode.sh video.mp4 + +# Transcode a list of files and record size savings +transcode.sh --saving --encodefile list.txt + +# Dry-run with verbose output to inspect detected formats +transcode.sh --dry-run --verbose my_movie.mkv + +# Use a custom preset, only keep result if smaller, skip AV1 inputs +transcode.sh --preset av1_fast --only-if-smaller --skip-codec av1 /media/films/*.mkv + +# Run at a moderate priority and continue past failures +transcode.sh --nice 10 --continue *.mp4 +``` + +## Debugging + +```sh +# High-level state messages +DEBUG=1 transcode.sh input.mkv + +# Full bash execution trace +TRACE=1 transcode.sh input.mkv +``` + +## Exit codes + +| Code | Meaning | +|------|---------| +| 0 | Success | +| 1 | Runtime failure (ffmpeg error on one or more files) | +| 2 | Usage error (unknown option, missing argument) | +| 3 | Configuration error (missing or invalid preset) | +| 127 | Missing dependency | + +## License + +[BSD-3-Clause](LICENSES/BSD-3-Clause.txt) © 2025 Dennis Fink \<dennis.fink@c3l.lu\> diff --git a/transcode.sh b/transcode.sh new file mode 100755 index 0000000..0d2e841 --- /dev/null +++ b/transcode.sh @@ -0,0 +1,746 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: 2025 Dennis Fink <dennis.fink@c3l.lu> +# +# SPDX-License-Identifier: BSD-3-Clause + +############################################################################### +# DESCRIPTION +# Batch transcode helper for media files using ffmpeg. +# +# The actual ffmpeg encoding parameters are provided by *presets* stored in: +# ${XDG_CONFIG_HOME:-$HOME/.config}/transcode.sh/presets/<name>.sh +# +# Each preset is a small bash snippet that must set an array named `ffargs`, +# for example: +# ffargs=( -c:v:0 libsvtav1 -crf 30 -preset 6 -pix_fmt "$output_pixel_format" ) +# +# Note: Presets are sourced as shell code. Only use trusted presets. +# +# A per-user list of video-codecs to skip can be provided in: +# ${XDG_CONFIG_HOME:-$HOME/.config}/transcode.sh/skip.conf +# +# NOTES +# This script respects the NO_COLOR standard (https://no-color.org/). +# +# AUTHOR +# Dennis Fink <dennis.fink@c3l.lu> +############################################################################### + +# TRACE enables bash execution tracing and is intended for debugging control +# flow / expansions. DEBUG enable structured debug messages (see debug()) and +# is intended for high-level state reporting. +# +# Recommended usage: +# TRACE=1 ./transcode.sh ... # show every command bash executes +# DEBUG=1 ./transcode.sh .... # show script-defined debug messages + +TRACE=${TRACE:-0} +if [[ $TRACE -ne 0 ]]; then + set -o xtrace +fi + +############################################################################### +# SHELL SCRIPT HARDENING +# +# This script enables strict and predictable behavior for improved safety, +# security, and debuggability. These measures help avoid common pitfalls +# such as accidental globbing, unexpected alias expansion, silent failures, +# or unintended word splitting. +############################################################################### + +# Unalias everything to avoid unexpected alias expansion +command unalias -a + +# Set a predictable and secure PATH (ignores user-controlled paths). +# Trade-off: tools installed outside these directories (e.g. ffmpeg via +# Homebrew on macOS at /opt/homebrew/bin, or via Nix at /nix/store/...) +# will not be found. In that case, invoke this script via a wrapper that +# prepends the correct path, e.g.: PATH="/opt/homebrew/bin:$PATH" ./transcode.sh +PATH='/bin:/usr/bin:/usr/local/bin' +export PATH + +# Clear the shell command hash table to avoid stale command lookups +hash -r + +# Set a safe IFS to prevent word-splitting vulnerabilities +IFS=$'\n\t' + +# Set a secure default file creation mask (controls default permissions) +UMASK=002 +umask "$UMASK" + +# Allow aliases to expand inside command substitutions (optional convenience) +shopt -s expand_aliases + +# Ensure ERR traps are inherited by functions and subshells +set -o errtrace +# Treat use of undefined variables as an error and exit +set -o nounset +# Make pipelines fail if any command in the pipeline fails +set -o pipefail + +############################################################################### +# SCRIPT METADATA +# +# These variables describe the script and are used for the --version output +# and for informational messages. They are marked readonly to prevent +# accidental modification at runtime. +############################################################################### +readonly SCRIPTNAME=${0##*/} +readonly DESCRIPTION="Batch transcode helper for media files using ffmpeg." +readonly DATE_OF_CREATION=2025-08-06 +readonly DATE_OF_REVISION=2026-02-22 +readonly VERSION=1.0.0 +readonly AUTHOR="Dennis Fink <dennis.fink@c3l.lu>" +readonly LICENSE="BSD-3-Clause" + +############################################################################### +# GLOBAL VARIABLES +############################################################################### + +ALL_OFF="" +BOLD="" +RED="" +GREEN="" +YELLOW="" +BLUE="" +MAGENTA="" +CYAN="" + +DEBUG=${DEBUG:-0} +QUIET=0 +VERBOSE=0 + +CONTINUE_ON_FAIL=0 +DRY_RUN=0 +ENCODE_FILE="" +NICE_VALUE=19 +ONLY_IF_SMALLER=0 +PRESET_NAME="default" +SAVING=0 +SAVING_FILE="transcode_savings" +declare -A SKIP_CODECS + +REMAINING_ARGS=() + +TMP_FILES=() + +readonly PRESET_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/transcode.sh/presets" +readonly SKIP_CODECS_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/transcode.sh/skip.conf" + +############################################################################### +# EXIT CODES +############################################################################### + +EXIT_OK=0 +EXIT_RUNTIME_FAILURE=1 +EXIT_USAGE_ERROR=2 +EXIT_CONFIG_ERROR=3 +EXIT_MISSING_DEPENDENCY=127 +readonly EXIT_OK EXIT_RUNTIME_FAILURE EXIT_USAGE_ERROR EXIT_CONFIG_ERROR EXIT_MISSING_DEPENDENCY + +############################################################################## +# FUNCTIONS +# +# This section defines the helper functions used throughout the script. They +# handle formatted output, error reporting, version information, utilities +# functions and runtime behaviour such as displaying usage instructions. +############################################################################### + +error() { + local mesg="$1" + shift + printf "${BOLD}${RED}==> ERROR:${ALL_OFF} ${BOLD}%s${ALL_OFF} %s\n" "$mesg" "$@" >&2 +} + +msg() { + if [[ $QUIET -eq 0 ]]; then + local mesg="$1" + shift + printf "${BOLD}${GREEN}==>${ALL_OFF} ${BOLD}%s${ALL_OFF} %s\n" "$mesg" "$@" + fi +} + +verbose() { + if [[ $VERBOSE -eq 1 && $QUIET -eq 0 ]]; then + local mesg="$1" + shift + printf "${BOLD}${BLUE}==>${ALL_OFF} ${BOLD}%s${ALL_OFF} %s\n" "$mesg" "$@" + fi +} + +debug() { + if [[ $DEBUG -eq 1 ]]; then + local mesg="$1" + shift + printf "${BOLD}${MAGENTA}==> DEBUG:${ALL_OFF} ${BOLD}%s${ALL_OFF} %s\n" "$mesg" "$@" + fi +} + +print_help() { + printf "%s - %s - %s + +Usage: %s [OPTION] [--] FILE... + +Options: + -c, --continue Continue with next file even if ffmpeg fails + -f, --encodefile PATH File containing list of files to encode + --encodefile=PATH + -n, --dry-run Show what would be done, don't run ffmpeg + -N, --nice VALUE Nice value for ffmpeg (default: 19) + --nice=VALUE + -s, --saving Log filesize savings (filename, orig, new, %%) + --saving-file PATH Specify where to save filesize savings (default: transcode_savings) + --saving-file=PATH + -S, --skip-codec LIST Add codecs to skip (comma/space/colon separated) + --skip-codec=LIST + -l, --only-if-smaller Only overwrite files if the transcoded file is smaller + -p, --preset NAME Load ffmpeg arguments from a preset (default: default) + --preset=NAME + -h, --help, -? Show this help text and exit + -q, --quiet Do not output anything, takes precedence over -v and --verbose + -v, --verbose Be more verbose + --version Print script information and version + --color Force colored output + --no-color Disable colored output + +Examples: + %s video.mp4 + %s -s -f list.txt + %s -n -N 10 my_movie.mkv\n" "$SCRIPTNAME" "$VERSION" "$DESCRIPTION" "$SCRIPTNAME" "$SCRIPTNAME" "$SCRIPTNAME" "$SCRIPTNAME" +} + +print_version() { + printf "${RED}${BOLD}Scriptname:${ALL_OFF} %s +${GREEN}${BOLD}Version:${ALL_OFF} %s +${YELLOW}${BOLD}Description:${ALL_OFF} %s +${BLUE}${BOLD}Author:${ALL_OFF} %s +${MAGENTA}${BOLD}Date of creation:${ALL_OFF} %s +${CYAN}${BOLD}Date of revision:${ALL_OFF} %s +${RED}${BOLD}License:${ALL_OFF} %s\n" "$SCRIPTNAME" "$VERSION" "$DESCRIPTION" "$AUTHOR" "$DATE_OF_CREATION" "$DATE_OF_REVISION" "$LICENSE" + + if [[ -f "LICENSES/${LICENSE}.txt" ]]; then + printf "\n" + cat "LICENSES/${LICENSE}.txt" + fi +} + +filesize() { + local f="$1" + local out + if out=$(stat -c "%s" -- "$f" 2>/dev/null); then + printf "%s\n" "$out" + else + stat -f "%z" -- "$f" + fi +} + +############################################################################### +# TERMINAL COLOR SETUP +# +# Colors and style escape sequences are configured dynamically through the +# `setup_colors` function, which enables or disables color output based on +# the value of ENABLE_COLOR. When enabled, terminal capabilities are detected +# via `tput`, falling back to ANSI escapes if unavailable. +# +# This script respects the NO_COLOR standard (https://no-color.org/). +# If the environment variable NO_COLOR is set (to any value), all color output +# is disabled. If FORCE_COLOR is set, colors are always enabled. +############################################################################### + +# NOTE: This block calls debug() and reads $DEBUG. Both must be defined before +# this point. Do not move this block above the GLOBAL VARIABLES or FUNCTIONS +# sections without adjusting those dependencies. +if [[ -n "${NO_COLOR:-}" ]]; then + debug "Colors are disabled via NO_COLOR" + ENABLE_COLOR=0 +elif [[ -n "${FORCE_COLOR:-}" ]]; then + debug "Colors are forced via FORCE_COLOR" + ENABLE_COLOR=1 +elif [[ -t 1 ]]; then + debug "Colors are enabled via interactive terminal" + ENABLE_COLOR=1 +else + debug "Colors are disabled" + ENABLE_COLOR=0 +fi + +setup_colors() { + if [[ "$ENABLE_COLOR" -eq 1 ]]; then + if tput setaf 0 >/dev/null 2>&1; then + debug "Setting colors via tput" + ALL_OFF="$(tput sgr0)" + BOLD="$(tput bold)" + RED="$(tput setaf 1)" + GREEN="$(tput setaf 2)" + YELLOW="$(tput setaf 3)" + BLUE="$(tput setaf 4)" + MAGENTA="$(tput setaf 5)" + CYAN="$(tput setaf 6)" + else + # Hardcoded ANSI fallback: colors only (no bold embedded), matching the + # tput path where BOLD and color variables are kept separate. + debug "Using hardcoded color ANSI escape sequences" + ALL_OFF="\e[0m" + BOLD="\e[1m" + RED="\e[31m" + GREEN="\e[32m" + YELLOW="\e[33m" + BLUE="\e[34m" + MAGENTA="\e[35m" + CYAN="\e[36m" + fi + else + ALL_OFF="" + BOLD="" + RED="" + GREEN="" + YELLOW="" + BLUE="" + MAGENTA="" + CYAN="" + fi +} +setup_colors + +############################################################################### +# MAIN EXECUTION +############################################################################### + +# Add a codec name to the SKIP_CODECS associative array. +# - Normalizes to lowercase +# - Ignores empty tokens +# - Used both for CLI --skip-codec and for skip.conf lines +add_skip_codec_token() { + local token="$1" + [[ -z "$token" ]] && return 0 + token=${token,,} # lowercase + if [[ ! -v SKIP_CODECS["$token"] ]]; then + debug "Add to skip codec:" "$token" + SKIP_CODECS["$token"]= + else + debug "Codec already skipped:" "$token" + fi + +} + +# Parse a user-provided codec list (comma/space/colon separated) +# and add each token to SKIP_CODECS +parse_skip_codec_parameter() { + local arg="$1" + local IFS=',: ' token + for token in $arg; do + [[ -z "$token" ]] && continue + add_skip_codec_token "$token" + done +} + +if [[ -f "$SKIP_CODECS_FILE" ]]; then + while IFS= read -r line || [[ -n "$line" ]]; do + # strip leading/trailing whitespace + line="${line#"${line%%[![:space:]]*}"}" + line="${line%"${line##*[![:space:]]}"}" + # skip blanks and comments + [[ -z "$line" || "${line:0:1}" == "#" ]] && continue + parse_skip_codec_parameter "$line" + done <"$SKIP_CODECS_FILE" +fi + +# Load ffmpeg argument preset by name. +# +# Security: +# Presets are sourced as shell code. Names are validated against a strict +# allowlist (alphanumerics, hyphens, underscores only) to prevent path +# traversal and shell metacharacter injection. Note: a valid preset name +# could still be a symlink to an arbitrary file; ensure the preset directory +# itself is not writable by untrusted users. +# +# Contract: +# The sourced preset MUST set the bash array `ffargs`. +# Presets may reference variables from the caller (encode_one) such as: +# output_pixel_format, input_pixel_format, input_codec +load_preset() { + local preset="$1" + # Strict allowlist: only alphanumerics, hyphens, and underscores are permitted. + # This prevents path traversal, shell metacharacter injection, and symlink-based + # attacks more reliably than a blocklist approach. + if [[ -z "$preset" || ! "$preset" =~ ^[a-zA-Z0-9_-]+$ ]]; then + error "Invalid preset name: $preset (only alphanumerics, hyphens, underscores allowed)" + exit $EXIT_USAGE_ERROR + fi + + local file="$PRESET_DIR/$preset.sh" + [[ -f "$file" ]] || { + error "Preset not found: $file" + exit $EXIT_CONFIG_ERROR + } + + debug "Sourcing preset file:" "$file" + + unset -v ffargs + ffargs=() + + # shellcheck source=/dev/null + source "$file" + + ((${#ffargs[@]})) || { + error "Preset '$preset' did not set ffargs" + exit $EXIT_RUNTIME_FAILURE + } + + debug "Preset ffargs:" "$(printf "%q " "${ffargs[@]}")" +} + +if [[ $# -eq 0 ]]; then + debug "No parameters were specified" + print_help + exit $EXIT_USAGE_ERROR +fi + +while [[ $# -gt 0 ]]; do + case "$1" in + -h | --help | -\?) + print_help + exit $EXIT_OK + ;; + --version) + print_version + exit $EXIT_OK + ;; + -q | --quiet) + QUIET=1 + shift + ;; + -v | --verbose) + VERBOSE=1 + shift + ;; + --color) + ENABLE_COLOR=1 + setup_colors + shift + ;; + --no-color) + ENABLE_COLOR=0 + setup_colors + shift + ;; + -c | --continue) + CONTINUE_ON_FAIL=1 + shift + ;; + -f | --encodefile) + [[ $# -ge 2 ]] || { + error "Missing value for $1" + exit $EXIT_USAGE_ERROR + } + ENCODE_FILE="$2" + shift 2 + ;; + --encodefile=*) + ENCODE_FILE="${1#*=}" + shift + ;; + -n | --dry-run) + DRY_RUN=1 + shift + ;; + -N | --nice) + [[ $# -ge 2 ]] || { + error "Missing value for $1" + exit $EXIT_USAGE_ERROR + } + NICE_VALUE="$2" + shift 2 + ;; + --nice=*) + NICE_VALUE="${1#*=}" + shift + ;; + -s | --saving) + SAVING=1 + shift + ;; + --saving-file) + [[ $# -ge 2 ]] || { + error "Missing value for $1" + exit $EXIT_USAGE_ERROR + } + SAVING_FILE="$2" + shift 2 + ;; + --saving-file=*) + SAVING_FILE="${1#*=}" + shift + ;; + -S | --skip-codec) + [[ $# -ge 2 ]] || { + error "Missing value for $1" + exit $EXIT_USAGE_ERROR + } + parse_skip_codec_parameter "$2" + shift 2 + ;; + --skip-codec=*) + parse_skip_codec_parameter "${1#*=}" + shift + ;; + -l | --only-if-smaller) + ONLY_IF_SMALLER=1 + shift + ;; + -p | --preset) + [[ $# -ge 2 ]] || { + error "Missing value for $1" + exit $EXIT_USAGE_ERROR + } + PRESET_NAME="$2" + shift 2 + ;; + --preset=*) + PRESET_NAME="${1#*=}" + shift + ;; + --) + shift + break + ;; + -*) + error "Unknown option: $1" + exit $EXIT_USAGE_ERROR + ;; + *) + REMAINING_ARGS+=("$1") + shift + ;; + esac +done + +command -v ffmpeg >/dev/null 2>&1 || { + error "ffmpeg not found." + exit $EXIT_MISSING_DEPENDENCY +} +command -v ffprobe >/dev/null 2>&1 || { + error "ffprobe not found." + exit $EXIT_MISSING_DEPENDENCY +} +command -v nice >/dev/null 2>&1 || { + error "nice not found." + exit $EXIT_MISSING_DEPENDENCY +} + +if [[ $SAVING -eq 1 ]]; then + # bc is only required when --saving is active; intentionally checked here + # rather than in the main dependency block to avoid requiring it universally. + command -v bc >/dev/null 2>&1 || { + error "bc not found." + exit $EXIT_MISSING_DEPENDENCY + } +fi + +# Remove temporary output files created during encoding. +# Registered via trap EXIT so it runs on normal exit and on failures +# shellcheck disable=SC2329 +cleanup() { + # "${TMP_FILES[@]+"${TMP_FILES[@]}"}" expands to nothing when the array is + # empty (safe under nounset), unlike [@]:-} which yields one empty iteration. + for t in "${TMP_FILES[@]+"${TMP_FILES[@]}"}"; do + debug "Cleanup temp file:" "$t" + [[ -f "$t" ]] && rm -f -- "$t" || true + done +} +trap cleanup EXIT + +# Encode a single media file according to PRESET_NAME. +# +# Steps: +# 1) Probe input codec and pixel format (v:0). +# 2) Optionally skip based on codec. +# 3) Normalize pixel formats (handle yuvj* forms). +# 4) Select an output pixel format that preserverse chroma subsampling/bit depth. +# 5) Load preset ffargs and run ffmpeg into a temp file in the same directory. +# 6) Optionally keep only-if-smaller; optionally log savings; then replace. +# +# Return: +# EXIT_OK on success or if skipped; EXIT_RUNTIME_FAILURE on failure. +encode_one() { + local file="$1" + msg "Processing:" "$file" + + [[ -z "$file" ]] && return $EXIT_OK + [[ ! -f "$file" ]] && { + error "Not found" + return $EXIT_RUNTIME_FAILURE + } + + local input_codec + input_codec=$(ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=nk=1:nw=1 "file:$file" 2>/dev/null) + if [[ -z "$input_codec" ]]; then + error "Could not determine input video codec" + return $EXIT_RUNTIME_FAILURE + fi + + if [[ -v SKIP_CODECS["$input_codec"] ]]; then + msg "Skip because of input codec:" "$input_codec" + return $EXIT_OK + fi + + local input_pixel_format + input_pixel_format=$(ffprobe -v error -select_streams v:0 -show_entries stream=pix_fmt -of default=nk=1:nw=1 "file:$file" 2>/dev/null) + if [[ -z "$input_pixel_format" ]]; then + error "Could not determine input pixel format" + return $EXIT_RUNTIME_FAILURE + fi + + # Some files report "yuvj*" formats (full-range JPEG-style). For encoding + # purposes we normalize to the equivalent "yuv*" formats. + # Covers all five variants ffmpeg can produce: 411p, 420p, 422p, 440p, 444p. + case "$input_pixel_format" in + yuvj411p) input_pixel_format="yuv411p" ;; + yuvj420p) input_pixel_format="yuv420p" ;; + yuvj422p) input_pixel_format="yuv422p" ;; + yuvj440p) input_pixel_format="yuv440p" ;; + yuvj444p) input_pixel_format="yuv444p" ;; + esac + debug "Normalized input pixel format:" "$input_pixel_format" + + # Preserve chroma subsampling (420/422/444) and bit depth (8/10/12/16-bit) + # to avoid unintended quality loss or incompatible output. + # Known limitation: exotic formats not matching these patterns (e.g. gbrp, + # yuva*, gray*) fall through to yuv420p. Extend the logic below if needed. + local output_pixel_format + if [[ "$input_pixel_format" == *"444"* ]]; then + if [[ "$input_pixel_format" == *"16"* ]]; then + output_pixel_format="yuv444p16le" + elif [[ "$input_pixel_format" == *"12"* ]]; then + output_pixel_format="yuv444p12le" + elif [[ "$input_pixel_format" == *"10"* ]]; then + output_pixel_format="yuv444p10le" + else + output_pixel_format="yuv444p" + fi + elif [[ "$input_pixel_format" == *"422"* ]]; then + if [[ "$input_pixel_format" == *"16"* ]]; then + output_pixel_format="yuv422p16le" + elif [[ "$input_pixel_format" == *"12"* ]]; then + output_pixel_format="yuv422p12le" + elif [[ "$input_pixel_format" == *"10"* ]]; then + output_pixel_format="yuv422p10le" + else + output_pixel_format="yuv422p" + fi + else + if [[ "$input_pixel_format" == *"16"* ]]; then + output_pixel_format="yuv420p16le" + elif [[ "$input_pixel_format" == *"12"* ]]; then + output_pixel_format="yuv420p12le" + elif [[ "$input_pixel_format" == *"10"* ]]; then + output_pixel_format="yuv420p10le" + else + output_pixel_format="yuv420p" + fi + fi + debug "Selected output pixel format:" "$output_pixel_format" + + load_preset "$PRESET_NAME" + + local extension filename directory tmp base stem + + base="${file##*/}" + if [[ "$base" == *.* ]]; then + extension="${base##*.}" + stem="${base%.*}" + filename="${file%.*}" + else + extension="" + stem="$base" + filename="$file" + fi + + directory=$(dirname -- "$file") + + # Temp file is created in the same directory as the input so that `mv` is + # atomic on the same filesystem (avoid cross-device rename issues). + tmp="$directory/.${stem}_$PRESET_NAME.$$.$RANDOM${extension:+.$extension}" + TMP_FILES+=("$tmp") + debug "Temporary output path:" "$tmp" + + # Build ffmpeg command: + # - Map all streams, metadata, and chapters + # - Default to stream copy for everything + # - Let the preset override specific streams (usually video) via ffargs + local cmd=(ffmpeg + -nostdin + -y + -hide_banner + -v error + -stats + -i "file:$file" + -map 0 + -map_metadata 0 + -map_chapters 0 + -c copy + "${ffargs[@]}" + "file:$tmp") + + if [[ $DRY_RUN -eq 1 ]]; then + msg "DRY RUN:" "Would encode with preset $PRESET_NAME" + verbose "Input codec:" "$input_codec" + verbose "Input pixel format:" "$input_pixel_format" + verbose "Output pixel format:" "$output_pixel_format" + return $EXIT_OK + fi + + msg "Encoding: $file" + debug "Nice value:" "$NICE_VALUE" + + if nice -n "$NICE_VALUE" "${cmd[@]}"; then + + local original_filesize new_filesize + original_filesize=$(filesize "$file") + new_filesize=$(filesize "$tmp") + + # If requested, keep the original if the new file is larger. + if [[ $ONLY_IF_SMALLER -eq 1 ]]; then + if [[ $new_filesize -gt $original_filesize ]]; then + msg "Did not replace $file as it was larger than the original." + rm -f -- "$tmp" + return $EXIT_OK + fi + fi + + if [[ $SAVING -eq 1 ]]; then + local pct + if [[ $original_filesize -gt 0 ]]; then + pct=$(bc <<<"($original_filesize - $new_filesize) * 100 / $original_filesize") + else + pct=0 + fi + printf "%s %s %s %s%%\n" "$filename" "$original_filesize" "$new_filesize" "$pct" >>"$SAVING_FILE" + fi + + mv -f -- "$tmp" "$file" + msg "Replaced: $file" + else + error "Encode FAILED: $file" + if [[ $CONTINUE_ON_FAIL -eq 1 ]]; then + return $EXIT_RUNTIME_FAILURE # fail this but keep going + else + exit $EXIT_RUNTIME_FAILURE + fi + fi +} + +STATUS=$EXIT_OK +if [[ -n $ENCODE_FILE ]]; then + while IFS= read -r file || [[ -n "$file" ]]; do + encode_one "$file" || STATUS=$EXIT_RUNTIME_FAILURE + done <"$ENCODE_FILE" +else + for file in "${REMAINING_ARGS[@]}"; do + encode_one "$file" || STATUS=$EXIT_RUNTIME_FAILURE + done +fi + +exit $STATUS diff --git a/transcode.sh.1 b/transcode.sh.1 new file mode 100644 index 0000000..9001e8a --- /dev/null +++ b/transcode.sh.1 @@ -0,0 +1,274 @@ +.TH TRANSCODE.SH 1 "2026-02-02" "1.0.0" "User Commands" +.SH NAME +transcode.sh \- batch transcode helper for media files using ffmpeg +.SH SYNOPSIS +.B transcode.sh +[\fIOPTION\fR]... [\fB\-\-\fR] \fIFILE\fR... +.SH DESCRIPTION +.B transcode.sh +is a batch transcoding wrapper around +.BR ffmpeg (1) +that processes one or more media files in place, replacing each with a +re-encoded version. +.PP +Encoding parameters are not hard-coded; instead they are supplied by +.IR presets , +which are small shell snippets stored under the preset directory (see +.B FILES +below). Each preset must define a Bash array named +.B ffargs +containing the extra arguments passed to +.BR ffmpeg (1). +.PP +The script probes every input file with +.BR ffprobe (1) +to detect the input video codec and pixel format. Files whose codec +appears in the skip list are silently skipped. The output pixel format +is derived automatically so that chroma subsampling (4:2:0, 4:2:2, +4:4:4) and bit depth (8, 10, 12, 16\-bit) are preserved. +.PP +All streams, metadata and chapter markers from the input are retained by +default (\fB\-map 0 \-map_metadata 0 \-map_chapters 0 \-c copy\fR); the +preset only needs to override the streams it cares about (typically +video). +.PP +Encoding is performed into a temporary file in the same directory as the +input, which ensures an atomic rename on completion. +.SH OPTIONS +.TP +.BR \-c ", " \-\-continue +Continue processing the next file even if +.BR ffmpeg (1) +fails on the current one. Without this flag the script exits immediately +on the first failure. +.TP +.BR \-f " \fIPATH\fR, " \-\-encodefile " \fIPATH\fR, " \-\-encodefile= \fIPATH\fR +Read the list of files to encode from \fIPATH\fR (one file per line) +instead of taking files from the command line. +.TP +.BR \-n ", " \-\-dry\-run +Print what would be done without actually invoking +.BR ffmpeg (1). +Combine with +.B \-v +to see the detected codec and pixel format information. +.TP +.BR \-N " \fIVALUE\fR, " \-\-nice " \fIVALUE\fR, " \-\-nice= \fIVALUE\fR +Run +.BR ffmpeg (1) +under +.BR nice (1) +with the given niceness value. Defaults to +.BR 19 +(lowest priority). +.TP +.BR \-s ", " \-\-saving +After each successful encode, append a line to the savings log file +recording the filename, original size in bytes, new size in bytes and the +percentage saved. Requires +.BR bc (1). +.TP +.BR \-\-saving\-file " \fIPATH\fR, " \-\-saving\-file= \fIPATH\fR +Path to the savings log file written by +.BR \-\-saving . +Defaults to +.B transcode_savings +in the current working directory. +.TP +.BR \-S " \fILIST\fR, " \-\-skip\-codec " \fILIST\fR, " \-\-skip\-codec= \fILIST\fR +Comma-, space- or colon-separated list of video codec names to skip. +Files whose detected input codec appears in this list are left untouched. +The per-user skip list from +.B skip.conf +(see +.BR FILES ) +is always loaded in addition to codecs supplied here. +.TP +.BR \-l ", " \-\-only\-if\-smaller +After encoding, replace the original only when the new file is strictly +smaller. If the transcoded file is larger the temporary file is removed +and the original is kept unchanged. +.TP +.BR \-p " \fINAME\fR, " \-\-preset " \fINAME\fR, " \-\-preset= \fINAME\fR +Load the preset named \fINAME\fR from the preset directory. Defaults to +.BR default . +.TP +.BR \-h ", " \-\-help ", " \-? +Print a short help message and exit. +.TP +.BR \-q ", " \-\-quiet +Suppress all normal output. Takes precedence over +.BR \-v / \-\-verbose . +.TP +.BR \-v ", " \-\-verbose +Emit additional informational messages (detected codec, pixel format, +etc.). +.TP +.B \-\-version +Print version, author and license information and exit. +.TP +.B \-\-color +Force colored output even when stdout is not a terminal or +.B NO_COLOR +is set. +.TP +.B \-\-no\-color +Disable colored output unconditionally. +.TP +.B \-\- +End of options. All subsequent arguments are treated as file names even +if they begin with +.BR \- . +.SH ENVIRONMENT +.TP +.B NO_COLOR +When set (to any value), colored output is disabled. See +.IR https://no-color.org/ . +.TP +.B FORCE_COLOR +When set, colored output is enabled regardless of +.B NO_COLOR +or terminal detection. +.TP +.B DEBUG +Set to +.B 1 +to enable structured debug messages (high-level state reporting). +.TP +.B TRACE +Set to +.B 1 +to enable Bash execution tracing +.RB ( set\ \-x ), +which prints every command as it is executed. +.TP +.B XDG_CONFIG_HOME +Base directory for user configuration. Defaults to +.BR $HOME/.config . +.SH FILES +.TP +.IR "${XDG_CONFIG_HOME:-$HOME/.config}/transcode.sh/presets/<name>.sh" +Preset files. Each preset is sourced as Bash code and must define an +array variable named +.B ffargs +containing additional arguments for +.BR ffmpeg (1). +The variable +.B output_pixel_format +is available to presets and contains the automatically selected output +pixel format string. +.IP +Example preset: +.RS +.nf +ffargs=( \-c:v:0 libsvtav1 \-crf 30 \-preset 6 \-pix_fmt "$output_pixel_format" ) +.fi +.RE +.IP +.B Warning: +presets are executed as shell code. Only use presets from trusted +sources. +.TP +.IR "${XDG_CONFIG_HOME:-$HOME/.config}/transcode.sh/skip.conf" +Optional per-user codec skip list. Each line should contain one codec +name (as reported by +.BR ffprobe (1)). +Files whose input video codec matches an entry are skipped without +encoding. +.SH EXIT STATUS +.TP +.B 0 +Success (or all skipped files were intentionally skipped). +.TP +.B 1 +Runtime failure (e.g. +.BR ffmpeg (1) +error on one or more files). +.TP +.B 2 +Usage error (invalid option or missing argument). +.TP +.B 3 +Configuration error (missing or invalid preset). +.TP +.B 127 +A required dependency +.RB ( ffmpeg , +.BR ffprobe , +.BR nice , +or +.B bc +when +.B \-\-saving +is active) was not found. +.SH EXAMPLES +Transcode a single file using the default preset: +.PP +.RS +.nf +transcode.sh video.mp4 +.fi +.RE +.PP +Transcode a batch of files listed in a file and log size savings: +.PP +.RS +.nf +transcode.sh \-s \-f list.txt +.fi +.RE +.PP +Dry-run with verbosity to inspect detected formats: +.PP +.RS +.nf +transcode.sh \-n \-v my_movie.mkv +.fi +.RE +.PP +Use a custom preset and only keep the result if it is smaller: +.PP +.RS +.nf +transcode.sh \-p av1_fast \-l /media/films/*.mkv +.fi +.RE +.PP +Run at a less aggressive niceness and skip already-AV1 files: +.PP +.RS +.nf +transcode.sh \-N 10 \-S av1 input.mkv +.fi +.RE +.PP +Use +.BR ffmpeg (1) +installed via Homebrew (macOS): +.PP +.RS +.nf +PATH="/opt/homebrew/bin:$PATH" transcode.sh input.mp4 +.fi +.RE +.SH NOTES +The script resets +.B PATH +to +.I /bin:/usr/bin:/usr/local/bin +for security. If +.BR ffmpeg (1) +is installed outside these directories (e.g. via Homebrew or Nix), +prepend the correct directory to +.B PATH +before invoking +.BR transcode.sh . +.SH SEE ALSO +.BR ffmpeg (1), +.BR ffprobe (1), +.BR nice (1), +.BR bc (1) +.SH AUTHOR +Dennis Fink <dennis.fink@c3l.lu> +.SH LICENSE +BSD\-3\-Clause diff --git a/transcode.sh.bash-completion b/transcode.sh.bash-completion new file mode 100644 index 0000000..ce153dd --- /dev/null +++ b/transcode.sh.bash-completion @@ -0,0 +1,149 @@ +# bash completion for transcode.sh +# Install to: +# /etc/bash_completion.d/transcode.sh +# or source manually: +# source transcode.sh.bash-completion +# +# Each option that takes an argument is supported in all three forms: +# short -f list.txt +# long space --encodefile list.txt +# long equals --encodefile=list.txt + +_transcode_sh() { + local cur prev words cword + _init_completion || return + + local -r preset_dir="${XDG_CONFIG_HOME:-$HOME/.config}/transcode.sh/presets" + + # --------------------------------------------------------------------------- + # Helper: collect preset names from the preset directory + # --------------------------------------------------------------------------- + _transcode_presets() { + local presets=() + if [[ -d "$preset_dir" ]]; then + local f + while IFS= read -r f; do + f="${f##*/}" # basename + f="${f%.sh}" # strip .sh extension + presets+=("$f") + done < <(find "$preset_dir" -maxdepth 1 -name '*.sh' -type f 2>/dev/null) + fi + printf '%s\n' "${presets[@]}" + } + + local codecs='h264 hevc av1 vp8 vp9 mpeg2video mpeg4 mjpeg theora' + local nice_vals='0 5 10 15 19' + + # --------------------------------------------------------------------------- + # Handle --option=VALUE: cur contains the entire "--opt=val" token. + # Strip the "opt=" prefix and delegate to the appropriate completer. + # This must be checked before the $prev dispatch below. + # --------------------------------------------------------------------------- + case "$cur" in + --encodefile=*) + cur="${cur#*=}" + _filedir + return + ;; + --saving-file=*) + cur="${cur#*=}" + _filedir + return + ;; + --nice=*) + cur="${cur#*=}" + COMPREPLY=( $(compgen -W "$nice_vals" -- "$cur") ) + return + ;; + --skip-codec=*) + cur="${cur#*=}" + COMPREPLY=( $(compgen -W "$codecs" -- "$cur") ) + return + ;; + --preset=*) + cur="${cur#*=}" + COMPREPLY=( $(compgen -W "$(_transcode_presets)" -- "$cur") ) + return + ;; + esac + + # --------------------------------------------------------------------------- + # Handle VALUE after short form (-f, -N, -S, -p) and + # long space form (--encodefile, --nice, --saving-file, --skip-codec, --preset). + # $prev is the option word, $cur is the value being typed. + # --------------------------------------------------------------------------- + case "$prev" in + -f|--encodefile) + _filedir + return + ;; + -N|--nice) + COMPREPLY=( $(compgen -W "$nice_vals" -- "$cur") ) + return + ;; + --saving-file) + _filedir + return + ;; + -S|--skip-codec) + COMPREPLY=( $(compgen -W "$codecs" -- "$cur") ) + return + ;; + -p|--preset) + COMPREPLY=( $(compgen -W "$(_transcode_presets)" -- "$cur") ) + return + ;; + esac + + # --------------------------------------------------------------------------- + # Complete option names themselves. + # For options that accept an argument we list both the bare long form + # (--nice) and the equals form (--nice=) so the user can choose their + # preferred style. compopt -o nospace is applied when the sole match + # ends with '=' to avoid an unwanted trailing space in that case. + # --------------------------------------------------------------------------- + case "$cur" in + --*) + local longopts=' + --continue + --encodefile + --encodefile= + --dry-run + --nice + --nice= + --saving + --saving-file + --saving-file= + --skip-codec + --skip-codec= + --only-if-smaller + --preset + --preset= + --help + --quiet + --verbose + --version + --color + --no-color + ' + COMPREPLY=( $(compgen -W "$longopts" -- "$cur") ) + # Suppress the trailing space when the only completion ends with '=' + [[ ${#COMPREPLY[@]} -eq 1 && "${COMPREPLY[0]}" == *= ]] \ + && compopt -o nospace + return + ;; + -*) + local shortopts='-c -f -n -N -s -S -l -p -h -? -q -v --' + COMPREPLY=( $(compgen -W "$shortopts" -- "$cur") ) + return + ;; + esac + + # --------------------------------------------------------------------------- + # Default: complete positional FILE arguments with common media extensions + # --------------------------------------------------------------------------- + _filedir '@(mkv|mp4|mov|avi|webm|flv|wmv|m4v|ts|mts|m2ts|mpg|mpeg|ogv|3gp|rm|rmvb)' +} + +complete -F _transcode_sh transcode.sh +complete -F _transcode_sh transcode |
