#!/usr/bin/env bash
# vim: filetype=bash :  -*- mode: sh; sh-shell: bash; -*-

set -euo pipefail

define() { IFS='\n' read -r -d '' ${1} || true ; }

myname="${0##*/}"

define pod <<"=cut"

=encoding utf-8

=for html <p align="center"><img src="https://raw.githubusercontent.com/tecolicom/App-dozo/main/images/dozo-logo.png" width="400"></p>

=head1 NAME

dozo - Dôzo, Docker with Zero Overhead

=head1 SYNOPSIS

dozo -I IMAGE [ options ] [ command ... ]

    -h, --help         show help
        --version      show version
    -d, --debug        debug mode (show full command)
    -x, --trace        trace mode (set -x)
    -q, --quiet        quiet mode
    -n, --dryrun       dry-run mode

    -I, --image=#      Docker image (required unless -D)
    -D, --default      use default image
    -E, --env=#        environment variable to inherit (repeatable)
    -W, --mount-cwd    mount current working directory
    -H, --mount-home   mount home directory
    -U, --unmount      do not mount any directory
        --mount-mode=# mount mode (rw or ro, default: rw)
    -R, --mount-ro     mount read-only (shortcut for --mount-mode=ro)
    -V, --volume=#     additional volume to mount (repeatable)
    -B, --batch        batch mode (non-interactive)
    -L, --live         use live (persistent) container
    -N, --name=#       live container name
    -K, --kill         kill and remove existing container
    -P, --port=#       port mapping (repeatable)
    -O, --other=#      additional docker options (repeatable)

=head1 VERSION

Version 1.00

=head1 USAGE

When executed without arguments, Dôzo starts an interactive shell
inside the container.  When arguments are given, they are executed as
a command.

    dozo -I alpine                  # start shell
    dozo -I alpine ls -la           # run command

By setting C<-D> or your favorite image with C<-I> in F<~/.dozorc>,
you can simply run Dôzo without specifying an image.  Since the git
top directory is automatically mounted, git commands work as expected
from anywhere in the tree.

    $ dozo                          # start shell
    $ dozo git log -p               # run git log -p

With C<-L> option, you can use a persistent container.  Tools
installed in the container will remain available for subsequent use.

    $ dozo -L                       # start shell and create container
    # apt update && apt install -y cowsay
    # exit
    $ dozo -L /usr/games/cowsay Dôzo
     ______
    < Dôzo >
     ------
            \   ^__^
             \  (oo)\_______
                (__)\       )\/\
                    ||----w |
                    ||     ||

=cut

[[ $pod =~ Version\ +([0-9.]+) ]] && my_version=${BASH_REMATCH[1]}

# -h callback: show help from POD
help() {
    sed -E \
        -e '/^$/N' \
        -e 's/^(\n*)=head[0-9]* */\1/' \
        -e '/^\n*[#=]/d' \
        -e '/Version/q' \
        <<< "$pod"
    exit 0
}

# -v callback: show version
version() {
    echo "$my_version"
    exit 0
}

# -x callback: trace mode
trace() { [[ $2 ]] && set -x || set +x; }

##############################################################################
# Utility functions
##############################################################################

_warn_buffer=()
warn() { _warn_buffer+=("$*"); }

debug() {
    [[ ${debug:-} ]] && echo "debug: $*" >&2 || true
}

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

# Check bash version (4.3+ required for getoptlong.sh)
if ((BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 3))); then
    die "bash 4.3+ required (found $BASH_VERSION)"
fi

git_topdir() {
    [[ $(git rev-parse --is-inside-work-tree 2>/dev/null) == true ]] || return
    local dir="$(git rev-parse --show-superproject-working-tree)"
    [[ $dir ]] || dir="$(git rev-parse --show-toplevel)"
    echo "$dir"
}

get_ip() {
    local ip=$(ifconfig 2>/dev/null | awk '/inet /{print $2}' | tail -1)
    echo "$ip"
}

container_name() {
    local name
    if [[ ${given_name:-} ]]
    then
        name="$given_name"
    else
        if [[ ${image:-} =~ (.*/)?([-_.a-zA-Z0-9]+) ]]
        then name=${BASH_REMATCH[2]}
        else name=$myname
        fi
        local dir="${mount_dir:-$cwd}"
        [[ $dir ]] && name+=.${dir##*/}
    fi
    echo "$name"
}

docker_find() {
    local OPT OPTARG OPTIND status=() id
    while getopts "s:" OPT
    do
        case $OPT in
            s) status+=("$OPTARG") ;;
        esac
    done
    shift $((OPTIND - 1))
    id=$(command docker ps -a -q ${status[@]/#/-f status=} -f name="^/$1\$")
    [[ $id ]] && echo "$id" || return 1
}

docker_status() {
    command docker inspect --format='{{.State.Status}}' $1
}

docker() {
    debug "docker $*"
    if [[ ${dryrun:-} ]]; then
        echo docker "$@"
        return 0
    fi
    command docker "$@"
}

##############################################################################
# Main
##############################################################################

# Default environment variables to inherit
DEFAULT_ENV=(
    LANG TZ
    HTTP_PROXY HTTPS_PROXY
    http_proxy https_proxy
    TERM_PROGRAM TERM_BGCOLOR COLORTERM
    DEEPL_AUTH_KEY
    OPENAI_API_KEY
    ANTHROPIC_API_KEY
    LLM_PERPLEXITY_KEY
)

# Initialize variables
top_dir=$(git_topdir) || top_dir=
cwd=$(pwd)
default_image=tecolicom/xlate
hostname=
workdir=/work
localtime=/etc/localtime
detach=
mount_dir=
container=
relative=
display=

# Set default volume based on git directory
if [[ $top_dir && $top_dir != $cwd ]]; then
    mount_dir="$top_dir"
fi

##############################################################################
# Read .dozorc
##############################################################################

# Collect .dozorc paths in reverse priority order (lowest first)
declare -a rcpath=()
[[ $HOME != $cwd ]] && rcpath+=("$HOME")
[[ $top_dir && $top_dir != $cwd ]] && rcpath+=("$top_dir")
rcpath+=(.)

# Collect options from .dozorc files
declare -a rc_opts=()
for dir in "${rcpath[@]}"; do
    rc="$dir/.dozorc"
    [[ -r $rc ]] || continue
    warn "reading $rc"
    while IFS= read -r line; do
        [[ $line =~ ^# ]] && continue
        [[ $line ]] || continue
        while IFS= read -r -d '' arg; do
            rc_opts+=("$arg")
        done < <(xargs printf '%s\0' <<< "$line")
    done < "$rc"
done

##############################################################################
# Option definitions
##############################################################################

define USAGE <<END
Dôzo - Generic Docker Runner

Usage: $myname [ options ] [ command ... ]
END

default() { image="${DOZO_DEFAULT_IMAGE:-$default_image}"; }

declare -A OPTS=(
    [&USAGE]="$USAGE" [&PERMUTE]= [&REQUIRE]=0.6.0
    [       help | h ! # show help                       ]=
    [    version |   ! # show version                    ]=
    [      debug | d   # debug mode                      ]=
    [      trace | x ! # trace mode (set -x)             ]=
    [      quiet | q   # quiet mode                      ]=
    [     dryrun | n   # dry-run mode                    ]=
    [      image | I : # Docker image                    ]=
    [    default | D ! # use default image               ]=
    [        env | E @ # environment variable to inherit ]=
    [  mount-cwd | W   # mount current working directory ]=
    [ mount-home | H   # mount home directory            ]=
    [    unmount | U   # do not mount any directory      ]=
    [ mount-mode |   : # mount mode (rw or ro)           ]=rw
    [   mount-ro | R   # mount read-only                 ]=
    [     volume | V @ # additional volume to mount      ]=
    [      batch | B   # batch mode (non-interactive)    ]=
    [       live | L   # use live container              ]=
    [       name | N : # live container name             ]=
    [       kill | K   # kill existing container         ]=
    [       port | P @ # port mapping                    ]=
    [      other | O @ # additional docker option        ]=
)

##############################################################################
# Parse options
##############################################################################

. getoptlong.sh OPTS "${rc_opts[@]}" "$@"

# Flush buffered warnings
warn() { [[ ${quiet:-} ]] || echo "$*" >&2; }
for _m in "${_warn_buffer[@]}"; do warn "$_m"; done; _warn_buffer=()

# Apply options
[[ ${mount_cwd:-} ]] && mount_dir="$cwd"
[[ ${mount_home:-} ]] && { mount_dir="$HOME"; DEFAULT_ENV+=(HOME=$workdir); }
[[ ${mount_ro:-} ]] && mount_mode=ro  # -R overrides --mount-mode
given_name="${name:-}"

# Merge environment variables
ENV=("${DEFAULT_ENV[@]}" "${env[@]}")

##############################################################################
# Kill container
##############################################################################

if [[ ${kill:-} ]]; then
    container=$(container_name)
    if id=$(docker_find "$container"); then
        docker rm -f "$container" | sed -e "s/\$/ ($id) is removed/"
    fi
    [[ ${live:-} ]] || exit 0
fi

##############################################################################
# Live container handling
##############################################################################

if [[ ${live:-} ]]; then
    container=$(container_name)
    if id=$(docker_find "$container"); then
        status=$(docker_status $id)
        case $status in
            exited)
                warn "restarting exited container $container ($id)"
                docker start $id > /dev/null || exit 1
                [[ ${dryrun:-} ]] && status=running && debug "fake status=running"
                ;;
            paused)
                warn "unpausing container $container ($id)"
                docker unpause $id || exit 1
                [[ ${dryrun:-} ]] && status=running && debug "fake status=running"
                ;;
            running)
                ;;
            *)
                warn "unknown status $status of $container ($id)"
                ;;
        esac
    else
        debug "container not found"
    fi
    if [[ ${status:-} == running ]] || id=$(docker_find -s running "$container"); then
        if (( $# > 0 )); then
            declare -a exec_opts
            [[ ! ${batch:-} ]] && exec_opts+=(--interactive)
            [[ ! ${batch:-} ]] && [[ -t 0 ]] && exec_opts+=(--tty)
            export DOCKER_CLI_HINTS=false
            docker exec "${exec_opts[@]}" $id "$@"
        else
            warn "attaching to container $container ($id)"
            docker attach $id
        fi
        exit
    fi
    warn "create live container \"$container\""
fi

##############################################################################
# Run container
##############################################################################

[[ $image ]] || die "Docker image must be specified with -I option"

if [[ ! ${unmount:-} && $mount_dir && $mount_dir != $cwd ]]; then
    warn "mount $mount_dir to $workdir"
    [[ ${cwd#$mount_dir/} != $cwd ]] && relative="${cwd#$mount_dir/}"
fi

if [[ ${DISPLAY:-} ]]; then
    ip=$(get_ip)
    [[ $ip ]] && display="$ip:0"
fi
: ${hostname:=$(<<< "${image}" sed -e 's:.*/::' -e 's/:.*//' | tr -d '[:space:]')}

declare -a dockopt=(
    -e DOZO_RUNNING_ON_DOCKER=1
    -e XLATE_RUNNING_ON_DOCKER=1
    --init
)
[[ ! ${batch:-}   ]] && dockopt+=(--interactive)
[[ ! ${batch:-}   ]] && [[ -t 0 ]] && dockopt+=(--tty)
[[ ! ${live:-}    ]] && dockopt+=(--rm)
[[   ${live:-}    ]] && dockopt+=(${container:+--name "$container"})
[[ -e $localtime  ]] && dockopt+=(-v $localtime:$localtime:ro)
[[ ! ${unmount:-} ]] && dockopt+=(
        -v "${mount_dir:-$cwd}:${workdir}:${mount_mode}"
        -w "${workdir}${relative:+/${relative}}"
    )
for v in "${volume[@]}"; do
    [[ $v == *:* ]] || v="$v:$v"
    dockopt+=(-v "$v")
done
for p in "${port[@]}"; do
    [[ $p == *:* ]] || p="$p:$p"
    dockopt+=(-p "$p")
done
for e in  "${ENV[@]}"; do dockopt+=(-e "$e"); done

dockopt+=(
    ${detach:+--detach}
    ${hostname:+--hostname "${hostname}"}
    ${display:+-e DISPLAY="$display"}
    "${other[@]}"
)

# Display docker run info
if [[ ! ${quiet:-} ]]; then
    info="image=${image} env=${#ENV[@]}"
    [[ ${live:-} && ${container:-} ]] && info+=" container=${container}"
    (( $# > 0 )) && info+=" command=$*"
    warn "docker run ${info}"
fi

docker run "${dockopt[@]}" "${image}" "$@"
exit

: <<'=cut'

=head1 INSTALLATION

Using L<cpanminus|https://metacpan.org/pod/App::cpanminus>:

    cpanm -n App::dozo

To install the latest version from GitHub:

    cpanm -n https://github.com/tecolicom/App-dozo.git

Alternatively, you can simply place C<dozo> and C<getoptlong.sh> in
your PATH.

B<Dôzo> requires Bash 4.3 or later.

=head1 DESCRIPTION

B<Dôzo> is a generic Docker runner that simplifies running commands in
Docker containers.  The name comes from the Japanese word "dôzo"
(どうぞ) meaning "please" or "go ahead", and also stands for "B<D>ocker
with B<Z>ero B<O>verhead".  The command name is C<dozo> for ease of
typing.

It automatically configures the tedious Docker options such as volume
mounts, environment variables, working directories, and interactive
terminal settings, so you can focus on the command you want to run.

B<Dôzo> is distributed as a standalone module and can be used as a
general-purpose Docker runner. It was originally developed as part of
L<App::Greple::xlate> and is used by L<xlate> for Docker operations.

B<Dôzo> uses L<getoptlong.sh|https://github.com/tecolicom/getoptlong>
for option parsing.

=head2 Key Features

=over 4

=item B<Git Friendly>

If you are working in a git environment, the git top directory is
automatically mounted. Otherwise the current directory is mounted.

=item B<Live Container>

Use C<-L> to create or attach to a persistent container that survives
between invocations. Container names are automatically generated from
the image name and mount directory.

=item B<Environment Inheritance>

Common environment variables are automatically inherited: C<LANG>,
C<TZ>, proxy settings, terminal settings, and API keys for AI/LLM
services (DeepL, OpenAI, Anthropic, Perplexity).

=item B<Flexible Mounting>

Various mount options: current directory (C<-W>), home directory
(C<-H>), additional volumes (C<-V>), read-only mode (C<-R>), or no
mount (C<-U>).

=item B<X11 Support>

When C<DISPLAY> is set, the host IP is automatically detected and
passed to the container, enabling GUI applications.

=item B<Configuration File>

Use C<.dozorc> to set default options. Searched in current directory,
git top directory, and home directory.

=item B<Standalone Operation>

B<Dôzo> can operate independently of L<xlate>. The C<getoptlong.sh>
script is provided by the L<Getopt::Long::Bash> CPAN distribution,
which is installed as a dependency. Alternatively, you can place
C<getoptlong.sh> in your C<PATH> manually.

=back

=head1 OPTIONS

=over 7

=item B<-h>, B<--help>

Show help message.

=item B<-d>, B<--debug>

Enable debug mode. Shows the full docker command line that will be executed.

=item B<-x>, B<--trace>

Enable trace mode (set -x).

=item B<-q>, B<--quiet>

Quiet mode.

=item B<-n>, B<--dryrun>

Dry-run mode. Show docker commands without executing them.
Useful for testing and debugging.

=item B<-I> I<image>, B<--image>=I<image>

Specify Docker image. Required unless C<-D> is given, but you can put
it in F<.dozorc> so you don't have to type it every time.

=item B<-D>, B<--default>

Use the default Docker image. If C<DOZO_DEFAULT_IMAGE> environment
variable is set, use that image. Otherwise, use
C<tecolicom/xlate:VERSION> where VERSION is the current Dôzo version.
See L</DEFAULT IMAGE> section for details about the default image.

=item B<-E> I<name>[=I<value>], B<--env>=I<name>[=I<value>]

Specify environment variable to inherit. Repeatable.

=item B<-W>, B<--mount-cwd>

Mount current working directory.

=item B<-H>, B<--mount-home>

Mount home directory.

=item B<-V> I<path>, B<-V> I<from>:I<to>, B<--volume>=I<from>:I<to>

Specify additional directory to mount. If only I<path> is given
(without C<:>), it is mounted to the same path in the container.
Repeatable.

=item B<-U>, B<--unmount>

Do not mount any directory.

=item B<--mount-mode>=I<mode>

Set mount mode. I<mode> is either C<rw> (read-write, default) or C<ro>
(read-only).

=item B<-R>, B<--mount-ro>

Mount directory as read-only. Shortcut for C<--mount-mode=ro>.

=item B<-B>, B<--batch>

Run in batch mode (non-interactive).

=item B<-N> I<name>, B<--name>=I<name>

Specify container name explicitly.

=item B<-K>, B<--kill>

Kill and remove existing container.

=item B<-L>, B<--live>

Use live (persistent) container.

=item B<-P> I<port>, B<-P> I<host>:I<container>, B<--port>=I<host>:I<container>

Specify port mapping. If only I<port> is given (without C<:>), it is
mapped to the same port in the container (e.g., C<-P 8000> becomes
C<8000:8000>). Repeatable.

=item B<-O> I<option>, B<--other>=I<option>

Specify additional docker options. Repeatable.

Note: Spaces and commas in option values are treated as delimiters and
will split the value into multiple elements.

=back

=head1 LIVE CONTAINER

The C<-L> option enables live (persistent) container mode. Unlike
normal mode where containers are removed after execution (C<--rm>),
live containers persist between invocations, allowing you to maintain
state and reduce startup overhead.

=head2 Container Lifecycle

When C<-L> is specified, B<Dôzo> behaves as follows:

=over 4

=item 1. B<Container does not exist>

Create a new persistent container (without C<--rm> flag).

=item 2. B<Container exists and is running>

If a command is given, execute it using C<docker exec>. Otherwise,
attach to the container using C<docker attach>.

=item 3. B<Container exists but is paused>

Unpause the container with C<docker unpause>, then proceed as above.

=item 4. B<Container exists but is exited>

Start the container with C<docker start>, then proceed as above.

=back

=head2 Container Naming

Container names are automatically generated in the format:

    <image_name>.<mount_directory>

For example, if you run:

    dozo -I tecolicom/xlate -L

from C</home/user/project>, the container name would be
C<xlate.project>.

You can override the auto-generated name using the C<-N> option:

    dozo -I tecolicom/xlate -L -N mycontainer

=head2 Managing Live Containers

=over 4

=item B<Attach to existing container>

    dozo -I myimage -L

If no command is given, attaches to the container's main process.

=item B<Execute command in existing container>

    dozo -I myimage -L ls -la

Runs the command in the existing container using C<docker exec>.

=item B<Kill and recreate container>

    dozo -I myimage -KL

The C<-K> option removes the existing container before C<-L> creates
a new one. Useful when you need a fresh container state.

=item B<Kill container only>

    dozo -I myimage -K

Without C<-L>, the container is removed and the command exits.

=back

=head2 Interactive Mode

In live container mode, interactive mode (C<-i> and C<-t> flags for
Docker) is automatically enabled when:

=over 4

=item * Standard input is a terminal (TTY)

=item * The C<-B> (batch) option is not specified

=back

This allows seamless interactive use when attaching to containers or
running interactive commands.

=head1 CONFIGURATION FILE

C<.dozorc> files are loaded from the following locations in order:

=over 4

=item 1. Home directory C<.dozorc>

=item 2. Git top directory C<.dozorc> (if different)

=item 3. Current directory C<.dozorc>

=item 4. Command line arguments

=back

For single-value options (like C<-I>, C<-N>), later values override
earlier ones. For repeatable options (like C<-E>, C<-V>, C<-P>, C<-O>),
all values are accumulated in order.

You can use any command line option in the configuration file:

    # Example .dozorc
    -I tecolicom/xlate:latest
    -E CUSTOM_VAR=value
    -V /data:/data

Lines starting with C<#> are treated as comments.

=head1 DOCKER-IN-DOCKER

To use Docker commands inside the container, mount the host's Docker
socket:

    # .dozorc for Docker-in-Docker
    -I docker
    -V /var/run/docker.sock

This allows you to run Docker commands from within the container using
the host's Docker daemon:

    $ dozo docker run --rm alpine uname -a

Or run it as a one-liner without C<.dozorc>:

    $ dozo -I docker -V /var/run/docker.sock docker run --rm alpine uname -a

=head1 DEFAULT IMAGE

The C<tecolicom/xlate> image is specifically designed for document
translation and text processing tasks, providing a comprehensive
environment with the following features:

=head2 Translation and AI Tools

=over 4

=item * B<DeepL CLI> - Command-line interface for DeepL translation API

=item * B<gpty> - GPT command-line tool for AI-powered text processing

=item * B<llm> - Unified LLM interface with plugins for multiple providers:
Gemini, Claude 3, Perplexity, and OpenRouter

=back

=head2 Text Processing Tools

=over 4

=item * B<greple> with xlate module - Pattern-based text extraction and
translation

=item * B<sdif> - Side-by-side diff viewer with word-level highlighting

=item * B<ansicolumn>, B<ansifold>, B<ansiexpand> - ANSI-aware text
formatting tools

=item * B<optex textconv> - Document format converter (PDF, Office, etc.)

=back

=head2 Greple Extensions

Multiple L<App::Greple> extension modules are pre-installed:

=over 4

=item * B<msdoc> - Microsoft Office document support

=item * B<xp> - Extended pattern syntax

=item * B<subst> - Text substitution with dictionary

=item * B<frame> - Frame-style output formatting

=back

=head2 Git Integration

The image includes a pre-configured git environment optimized for
document comparison and review. Since B<Dôzo> automatically mounts
the git top directory by default, git commands work seamlessly with
full repository context:

=over 4

=item * B<Side-by-side diff> - C<git diff>, C<git log>, and C<git show>
use B<sdif> for word-level side-by-side comparison

=item * B<Colorful blame> - C<git blame> uses B<greple> for enhanced
label coloring

=item * B<Office document diff> - Compare Word (.docx), Excel (.xlsx),
and PowerPoint (.pptx) files directly with git

=item * B<PDF diff> - View PDF metadata changes

=item * B<JSON diff> - Normalized JSON comparison using B<jq>

=back

=head2 Additional Utilities

=over 4

=item * B<MeCab> - Japanese morphological analyzer with IPA dictionary

=item * B<poppler-utils> - PDF processing tools (pdftotext, etc.)

=item * B<jq>, B<yq> - JSON and YAML processors

=back

=head2 Environment

=over 4

=item * Based on Ubuntu with Japanese locale (ja_JP.UTF-8)

=item * Perl and Python3 runtime environments

=item * Common API keys are automatically inherited from host
(DEEPL_AUTH_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.)

=back

=head1 ENVIRONMENT

=head2 Configuration Variables

=over 4

=item C<DOZO_DEFAULT_IMAGE>

Specifies the default Docker image used when C<-D> (C<--default>) option
is given. If not set, C<tecolicom/xlate:VERSION> is used where VERSION
is the current Dôzo version.

=back

=head2 Inherited Variables

The following environment variables are inherited by default:

    LANG TZ
    HTTP_PROXY HTTPS_PROXY http_proxy https_proxy
    TERM_PROGRAM TERM_BGCOLOR COLORTERM
    DEEPL_AUTH_KEY OPENAI_API_KEY ANTHROPIC_API_KEY LLM_PERPLEXITY_KEY

=head2 Container Variables

The following environment variables are set inside the container:

=over 4

=item C<DOZO_RUNNING_ON_DOCKER=1>

Indicates the command is running inside a container started by Dôzo.

=item C<XLATE_RUNNING_ON_DOCKER=1>

For compatibility with xlate. Used to prevent recursive Docker
invocation when xlate is run inside the container.

=back

=head1 SEE ALSO

L<xlate>, L<App::Greple::xlate>

L<getoptlong.sh|https://github.com/tecolicom/getoptlong>

=head1 AUTHOR

Kazumasa Utashiro

=head1 LICENSE

Copyright © 2025-2026 Kazumasa Utashiro.

This software is released under the MIT License.
L<https://opensource.org/licenses/MIT>

=cut