#!/usr/bin/env bash

# Purpose: Convert POSIX file/directory names to/from NCZarr store-names

# Copyright (C) 2022--present Charlie Zender

# This file is part of NCO, the netCDF Operators. NCO is free software.
# You may redistribute and/or modify NCO under the terms of the 
# 3-Clause BSD License.

# You are permitted to link NCO with the HDF, netCDF, OPeNDAP, and UDUnits
# libraries and to distribute the resulting executables under the terms 
# of the BSD, but in addition obeying the extra stipulations of the 
# HDF, netCDF, OPeNDAP, and UDUnits licenses.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
# See the 3-Clause BSD License for more details.

# The original author of this software, Charlie Zender, seeks to improve
# it with your suggestions, contributions, bug-reports, and patches.
# Please contact the NCO project at http://nco.sf.net or write to
# Charlie Zender
# Department of Earth System Science
# University of California, Irvine
# Irvine, CA 92697-3100

# Prerequisites: Bash, NCO
# Script could use other shells, e.g., dash (Debian default) after rewriting function definitions and loops
# Debug with 'bash -x ncz2psx --dbg=dbg_lvl' where 0 <= dbg_lvl <= 5

# Insta-install:
# scp ~/nco/data/ncz2psx zender1@acme1.llnl.gov:bin
# scp ~/nco/data/ncz2psx andes.olcf.ornl.gov:bin_andes
# scp ~/nco/data/ncz2psx ac.zender@blues.lcrc.anl.gov:bin_blues
# scp ~/nco/data/ncz2psx cheyenne.ucar.edu:bin
# scp ~/nco/data/ncz2psx ac.zender@chrysalis.lcrc.anl.gov:bin_chrysalis
# scp ~/nco/data/ncz2psx compy.pnl.gov:bin
# scp ~/nco/data/ncz2psx cooley.alcf.anl.gov:bin
# scp ~/nco/data/ncz2psx cori.nersc.gov:bin_cori
# scp ~/nco/data/ncz2psx dust.ess.uci.edu:bin
# scp ~/nco/data/ncz2psx e3sm.ess.uci.edu:bin
# scp ~/nco/data/ncz2psx frazil.ess.uci.edu:bin
# scp ~/nco/data/ncz2psx perlmutter-p1.nersc.gov:bin_perlmutter
# scp ~/nco/data/ncz2psx skyglow.ess.uci.edu:bin
# scp ~/nco/data/ncz2psx theta.alcf.anl.gov:bin_theta
# scp dust.ess.uci.edu:bin/ncz2psx ~/bin
# scp dust.ess.uci.edu:bin/ncz2psx ${MY_BIN_DIR}
# scp zender@dust.ess.uci.edu:bin/ncz2psx ${MY_BIN_DIR}

# Set script name, directory, PID, run directory
drc_pwd=${PWD}
# Security: Explicitly unset IFS before wordsplitting, so Bash uses default IFS=<space><tab><newline>
unset IFS
# Set these before 'module' command which can overwrite ${BASH_SOURCE[0]}
# NB: dash supports $0 syntax, not ${BASH_SOURCE[0]} syntax
# http://stackoverflow.com/questions/59895/can-a-bash-script-tell-what-directory-its-stored-in
spt_src="${BASH_SOURCE[0]}"
[[ -z "${spt_src}" ]] && spt_src="${0}" # Use ${0} when BASH_SOURCE is unavailable (e.g., dash)
while [ -h "${spt_src}" ]; do # Recursively resolve ${spt_src} until file is no longer a symlink
  drc_spt="$( cd -P "$( dirname "${spt_src}" )" && pwd )"
  spt_src="$(readlink "${spt_src}")"
  [[ ${spt_src} != /* ]] && spt_src="${drc_spt}/${spt_src}" # If ${spt_src} was relative symlink, resolve it relative to path where symlink file was located
done
cmd_ln="${spt_src} ${@}"
drc_spt="$( cd -P "$( dirname "${spt_src}" )" && pwd )"
spt_nm=$(basename ${spt_src}) # [sng] Script name (unlike $0, ${BASH_SOURCE[0]} works well with 'source <script>')
spt_pid=$$ # [nbr] Script PID (process ID)

# Debugging and Benchmarking:
# /bin/ls -d ${HOME}/nco/data/h000? | ncz2psx
# /bin/ls -d ${HOME}/nco/data/h000? | ncz2psx | ncea -O ~/out.nc

# Production usage:
# /bin/ls -d ${HOME}/nco/data/h000? | ncz2psx | ncea -O ~/out.nc

# 20221108: Passing environment variable NCO_PATH_OVERRIDE (NPO) to ncremap in batch queues fails on Cori
# Approaches that fail include: 1. export NPO='Yes';ncremap ... 2. export NPO='Yes' ncremap ...
# Direct approach that works is to pass NPO flag to ncremap via command-line switch
# Require that path-override switch to be first command-line option (i.e., $1) found with shell syntax
# ncremap/ncclimo implement NPO (though not getopt) logic prior to invoking NCO
# This switch is a no-op in main getopt() block below (since it has already been parsed here)
hrd_pth='No' # [sng] Hard-code machine-dependent paths/modules if HOSTNAME in database
if [ -n "${1}" ]; then
    if [ "${1}" = '--hrd_pth' ] || [ "${1}" = '--npo' ] || [ "${1}" = '--nco_path_override' ] || [ "${1}" = '--NCO_PATH_OVERRIDE' ]; then
	hrd_pth='Yes'
	NCO_PATH_OVERRIDE='Yes'
    fi # !hrd_pth
fi # !$1

# Configure paths at High-Performance Computer Centers (HPCCs) based on ${HOSTNAME}
if [ -z "${HOSTNAME}" ]; then
    if [ -f /bin/hostname ] && [ -x /bin/hostname ]; then
	export HOSTNAME=`/bin/hostname`
    elif [ -f /usr/bin/hostname ] && [ -x /usr/bin/hostname ]; then
	export HOSTNAME=`/usr/bin/hostname`
    fi # !hostname
fi # HOSTNAME

# Ensure batch jobs access correct 'mpirun' (or, with SLURM, 'srun') command, netCDF library, and NCO executables and library
# 20170914 Entire block is identical between ncclimo and ncremap---keep it that way!
# 20190421 Change override default from opt-out to opt-in
# 20221108 Implement hrd_pth in block above prior to getopt()
# Leave NCO_PATH_OVERRIDE unset or set to 'No' to prevent NCO from executing next block that overrides PATH
# Set NCO_PATH_OVERRIDE to 'Yes' in environment to cause NCO to execute next block and to override PATH:
# export NCO_PATH_OVERRIDE='Yes'
if [ "${hrd_pth}" = 'Yes' ] && [ "${NCO_PATH_OVERRIDE}" = 'Yes' ]; then
    # If HOSTNAME is not in database, change hrd_pth_fnd to 'No' in case-statement default fall-through
    hrd_pth_fnd='Yes' # [sng] Machine-dependent paths/modules for HOSTNAME found in database
    case "${HOSTNAME}" in 
	acme1* )
	    export PATH='/home/zender1/bin:/p/user_pub/e3sm_unified/envs/base/envs/e3sm_unified_latest/bin'\:${PATH}
            export LD_LIBRARY_PATH='/home/zender1/lib:/p/user_pub/e3sm_unified/envs/base/envs/e3sm_unified_latest/lib'\:${LD_LIBRARY_PATH} ; ;;
	andes* )
	    # 20190827: Must guarantee finding mpirun
	    source ${MODULESHOME}/init/sh # 20150607: PMC Ensures find module commands will be found
	    if [ ${spt_nm} = 'ncremap' ]; then
		module load esmf
	    fi # !ncremap
            export PATH='/ccs/home/zender/bin_andes'\:${PATH}
	    export LD_LIBRARY_PATH='/ccs/home/zender/lib_andes:/ccs/proj/cli900/sw/andes/e3sm-unified/base/envs/e3sm_unified_latest/lib'\:${LD_LIBRARY_PATH} ; ;;
	blues* | blogin* | b[0123456789][0123456789][0123456789] )
	    export PATH='/home/zender/bin_blues'\:${PATH}
	    export LD_LIBRARY_PATH='/home/zender/lib_blues'\:${LD_LIBRARY_PATH} ; ;;
	chrysalis* | chrlogin* | chr-[0123456789][0123456789][0123456789][0123456789] )
	    # 20221006 Add build environment modules
	    module load gcc/9.2.0-ugetvbp
	    module load openmpi/4.0.4-hpcx-hghvhj5
	    if [ ${spt_nm} = 'ncremap' ]; then
		E3SMU_ROOT='/lcrc/soft/climate/e3sm-unified/base/envs/e3sm_unified_latest'
	    fi # !ncremap
	    export PATH='/home/ac.zender/bin_chrysalis:/home/ac.zender/anaconda/bin'\:${PATH}
	    export LD_LIBRARY_PATH='/home/ac.zender/lib_chrysalis:/home/ac.zender/anaconda/lib'\:${LD_LIBRARY_PATH} ; ;;
	*cheyenne* )
	    # 20180112: Cheyenne support not yet tested in batch mode
	    if [ ${spt_nm} = 'ncremap' ]; then
		# On cheyenne, module load ncl installs ERWG in /glade/u/apps/ch/opt/ncl/6.4.0/intel/17.0.1/bin (i.e., ${NCARG_ROOT}/bin)
		module load ncl
	    fi # !ncremap
	    if [ -n "${NCARG_ROOT}" ]; then
		export PATH="${PATH}:/glade/u/apps/ch/opt/ncl/6.6.2/gnu/8.3.0/bin"
	    fi # !NCARG_ROOT
            export PATH='/glade/u/home/zender/bin'\:${PATH}
            export LD_LIBRARY_PATH='/glade/u/apps/ch/opt/netcdf/4.6.3/gnu/9.1.0/lib:/glade/u/apps/ch/opt/udunits/2.2.26/gnu/9.1.0/lib:/glade/u/apps/ch/opt/gsl/2.4/gnu/6.3.0/lib:/glade/u/home/zender/lib'\:${LD_LIBRARY_PATH} ; ;;
	compy* | n[0123456789][0123456789][0123456789][0123456789] )
	    module purge
	    module load gcc/10.2.0
	    if [ ${spt_nm} = 'ncremap' ]; then
		# 20210519: This script takes significant time (5-10 seconds) to load
		source /compyfs/software/mbtempest.envs.sh
		E3SMU_ROOT='/share/apps/E3SM/conda_envs/base/envs/e3sm_unified_latest'
	    fi # !ncremap
	    export PATH='/qfs/people/zender/bin:/qfs/people/zender/anaconda/bin'\:${PATH}
	    export LD_LIBRARY_PATH='/qfs/people/zender/lib:/qfs/people/zender/anaconda/lib'\:${LD_LIBRARY_PATH} ; ;;
	cooley* | cc[0123456789][0123456789][0123456789] )
	    # 20160421: Split cooley from mira binary locations to allow for different system libraries
	    # http://www.mcs.anl.gov/hs/software/systems/softenv/softenv-intro.html
	    soft add +mvapich2 
            export PBS_NUM_PPN=12 # Spoof PBS on Soft (which knows nothing about node capabilities)
	    export PATH='/home/zender/bin_cooley'\:${PATH}
	    export LD_LIBRARY_PATH='/home/zender/lib_cooley'\:${LD_LIBRARY_PATH} ; ;;
	cori* | cmem* | nid[0123456789][0123456789][0123456789][0123456789][0123456789] )
	    # 20220811 Add build environment modules
	    module purge
	    module load PrgEnv-intel
	    module load cray-netcdf
	    module load gsl/2.7
	    module load antlr/2.7.7-intel
	    module load udunits
	    if [ ${spt_nm} = 'ncremap' ]; then
		MOAB_ROOT=/project/projectdirs/e3sm/software/moab
		TEMPESTREMAP_ROOT=/project/projectdirs/e3sm/software/tempestremap
		E3SMU_ROOT='/global/common/software/e3sm/anaconda_envs/base/envs/e3sm_unified_latest'
	    fi # !ncremap
	    if [ -n "${NCARG_ROOT}" ]; then
		export PATH="${PATH}:${NCARG_ROOT}/bin"
	    fi # !NCARG_ROOT
	    export PATH='/global/homes/z/zender/bin_cori'\:${PATH}
            export LD_LIBRARY_PATH='/global/homes/z/zender/lib_cori'\:${LD_LIBRARY_PATH} ; ;;
	mira* )
	    export PATH='/home/zender/bin_mira'\:${PATH}
	    export LD_LIBRARY_PATH='/soft/libraries/netcdf/current/library:/home/zender/lib_mira'\:${LD_LIBRARY_PATH} ; ;;
	perlmutter* | login[0123456789][0123456789] | nid[0123456789][0123456789][0123456789][0123456789][0123456789][0123456789] )
	    # 20221103 Add build environment modules
	    module load PrgEnv-gnu
	    module load gcc/11.2.0
	    module load cray-hdf5/1.12.2.1
	    module load cray-netcdf/4.9.0.1
	    if [ ${spt_nm} = 'ncremap' ]; then
		MOAB_ROOT=/project/projectdirs/e3sm/software/moab
		TEMPESTREMAP_ROOT=/project/projectdirs/e3sm/software/tempestremap
		E3SMU_ROOT='/global/common/software/e3sm/anaconda_envs/base/envs/e3sm_unified_latest'
	    fi # !ncremap
	    if [ -n "${NCARG_ROOT}" ]; then
		export PATH="${PATH}:${NCARG_ROOT}/bin"
		export LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${NCARG_ROOT}/lib"
	    fi # !NCARG_ROOT
	    export PATH='/global/homes/z/zender/bin_perlmutter'\:${PATH}
            export LD_LIBRARY_PATH='/global/homes/z/zender/lib_perlmutter'\:${LD_LIBRARY_PATH} ; ;;
	theta* )
	    export PATH='/opt/cray/pe/netcdf/4.6.1.2/gnu/7.1/bin'\:${PATH}
	    export LD_LIBRARY_PATH='/opt/cray/pe/netcdf/4.6.1.2/gnu/7.1/lib'\:${LD_LIBRARY_PATH} ; ;;
	titan* )
	    source ${MODULESHOME}/init/sh # 20150607: PMC Ensures find module commands will be found
	    module load gcc
	    if [ ${spt_nm} = 'ncremap' ]; then
		# 20170831: Use module load ncl (6.3.0 lacks ERWG)
		module load ncl # 20170916 OK
	    fi # !ncremap
	    if [ -n "${NCARG_ROOT}" ]; then
		export PATH="${PATH}:${NCARG_ROOT}/bin"
	    fi # !NCARG_ROOT
            export PATH='/ccs/home/zender/bin_titan'\:${PATH}
	    export LD_LIBRARY_PATH='/opt/cray/netcdf/4.4.1.1/GNU/49/lib:/sw/xk6/udunits/2.1.24/sl_gcc4.5.3/lib:/ccs/home/zender/lib_titan'\:${LD_LIBRARY_PATH} ; ;;
	* ) # Default fall-through
	    hrd_pth_fnd='No' ; ;;
    esac # !HOSTNAME
    # 20210519: E3SM-U supplies many uncommon executables, e.g., ESMF_RegridWeightGen
    # Append E3SM-U to end of PATH so NCO binaries not redirected, e.g., from CSZ's development directory to E3SM-U
    if [ -n "${E3SMU_ROOT}" ]; then
	export PATH="${PATH}:${E3SMU_ROOT}/bin"
    fi # !MOAB_ROOT
fi # !hrd_pth && !NCO_PATH_OVERRIDE

# 20220131 ncclimo/ncremap commands within scripts that open E3SMU environment
# (e.g., zppy-generated scripts) must be told where to find NCO binaries.
# Only necessary on login nodes since Spack handles this fine on compute nodes
if [ "${E3SMU_MPI}" = 'NOMPI' ] && [ -n "${E3SMU_SCRIPT}" ] && [ -n "${CONDA_PREFIX}" ]; then
   export PATH="${CONDA_PREFIX}/bin"\:${PATH}
fi # !E3SMU_MPI
# Set NCO version and directory
nco_exe=`which ncks`
if [ -z "${nco_exe}" ]; then
    echo "${spt_nm}: ERROR Unable to find NCO, \${nco_exe} = ${nco_exe}"
    echo "${spt_nm}: HINT Carefully examine your environment setup (e.g., .bashrc) to avoid inadvertently overriding (with, e.g., conda-initialization) paths intended to be provided by an analysis-package environment (e.g., E3SM-Unified)"
    exit 1
fi # !nco_exe
# StackOverflow method finds NCO directory
while [ -h "${nco_exe}" ]; do
  drc_nco="$( cd -P "$( dirname "${nco_exe}" )" && pwd )"
  nco_exe="$(readlink "${nco_exe}")"
  [[ ${nco_exe} != /* ]] && nco_exe="${drc_nco}/${nco_exe}"
done
drc_nco="$( cd -P "$( dirname "${nco_exe}" )" && pwd )"
nco_vrs=$(ncks --version 2>&1 > /dev/null | grep NCO | awk '{print $5}')
nco_sng=$(ncks --version 2>&1 > /dev/null | grep NCO | awk -F '"' '{print $2}')

# 20190218: Die quickly when NCO is found yet cannot run, e.g., due to linker errors
if [ -z "${nco_vrs}" ]; then
    echo "${spt_nm}: ERROR ${nco_exe} dies with error message on next line:"
    $(ncks --version)
    exit 1
fi # !nco_vrs
lbr_vrs=$(ncks --library 2>&1 > /dev/null | awk '{print $6}')

# Detect and warn about mixed modules (for Qi Tang 20170531)
if [ "${drc_spt}" != "${drc_nco}" ]; then
    echo "INFO: Mixture of NCO scripts and binaries from different locations. Script ${spt_nm} is from directory ${drc_spt} while NCO binaries are from directory ${drc_nco}. Normally this script and the binaries are from the same executables directory. This INFO may be safely ignored for customized scripts and/or binaries that the user has intentionally split into different directories."
    echo "HINT (All-users): Conflicting script and binary directories may result from 1) Hardcoding an NCO script and/or binary pathnames, 2) Incomplete NCO installations in one or more directories in the \$PATH environment variable, 3) (Re-)Installing or (re-)building NCO without issuing a \"hash -r\" command afterward to update the executable pathnames that the shell remembers from before."
    echo "HINT (E3SM-only): In a Conda-based NCO environment, such as E3SM-Unified (which uses NCO in MPAS Analysis and E3SM-Diags), it is possible that some features may be unavailable because the upstream packages (e.g., UDUnits) were not properly linked by Conda. The ncclimo and ncremap scripts contain a mechanism to access the (presumably correctly linked) NCO binary executable and library paths in C. Zender's home directories on the major E3SM machines. Users may turn-on the machine-dependent, hard-coded path by invoking ncclimo and/or ncremap after altering their environment with: \"export NCO_PATH_OVERRIDE=Yes\"."
fi # drc_spt

# When running in a terminal window (not in an non-interactive batch queue)...
if [ -n "${TERM}" ]; then
    # Set fonts for legibility
    if [ -x /usr/bin/tput ] && tput setaf 1 &> /dev/null; then
	fnt_bld=`tput bold` # Bold
	fnt_nrm=`tput sgr0` # Normal
	fnt_rvr=`tput smso` # Reverse
	fnt_tlc=`tput sitm` # Italic
    else
	fnt_bld="\e[1m" # Bold
	fnt_nrm="\e[0m" # Normal
	fnt_rvr="\e[07m" # Reverse
	fnt_tlc="\e[3m" # Italic
    fi # !tput
fi # !TERM
    
# Pre-define enumerated types used in defaults
fl_nbr=0 # [nbr] Number of input filenames
frg_dfl='dfl' # [sng] Default fragment
frg_ncz='ncz' # [sng] NCZarr fragment
frg_s3='s3' # [sng] S3 fragment
frg_xry='xry' # [sng] Xarray fragment
frg_zip='zip' # [sng] Zip fragment
frg_zrr='zrr' # [sng] Pure Zarr fragment
scm_dfl='dfl' # [sng] Default scheme
scm_file='file' # [sng] File scheme
scm_s3='s3' # [sng] S3 scheme
scm_http='http' # [sng] HTTP scheme

# Defaults for command-line options and some derived variables
# Modify these defaults to save typing later
bch_pbs='No' # [sng] PBS batch (non-interactive) job
bch_slr='No' # [sng] SLURM batch (non-interactive) job
dbg_lvl=0 # [nbr] Debugging level
frg_sng='' # [sng] Fragment string to prepend/remove to/from filenames
frg_typ="${frg_dfl}" # [sng] Fragment type
inp_aut='No' # [sng] Input file list automatically generated
inp_glb='No' # [sng] Input file list from globbing directory 
inp_psn='No' # [sng] Input file list from positional arguments
inp_std='No' # [sng] Input file list from stdin
scm_sng='' # [sng] Scheme string to prepend/remove to/from filenames
scm_typ="${scm_dfl}" # [sng] Scheme type
std_chk='Yes' # [sng] Check stdin for input file list
vrs_prn='No' # [sng] Print version information

function fnc_usg_prn { # NB: dash supports fnc_nm (){} syntax, not function fnc_nm{} syntax
    # Print usage
    printf "${fnt_rvr}Basic usage:\n${fnt_nrm}${fnt_bld}ls *.nc | ${spt_nm} | ncea ...${fnt_nrm} # Default settings\n"
    printf "${fnt_bld}ls *.nc | ${spt_nm} -s scm_typ -f frg_typ | ncecat ...${fnt_nrm} # Short options with pre-defined types\n"
    printf "${fnt_bld}ls *.nc | ${spt_nm} --scm=file --frg=ncz | ncecat ...${fnt_nrm} # Long options with pre-defined types\n"
    printf "${fnt_bld}ls *.nc | ${spt_nm} --scm_sng=file:// --frg_sng='#mode=nczarr,file' | ncea ...${fnt_nrm} # User-defined scheme and fragment strings\n\n"
    echo "Command-line options [long-option synonyms in ${fnt_tlc}italics${fnt_nrm}]:"
    echo "${fnt_rvr}-d${fnt_nrm} ${fnt_bld}dbg_lvl${fnt_nrm}  Debug level (default ${fnt_bld}${dbg_lvl}${fnt_nrm}) [${fnt_tlc}dbg_lvl, dbg, debug, debug_level${fnt_nrm}]"
    echo "${fnt_rvr}-f${fnt_nrm} ${fnt_bld}frg_typ${fnt_nrm}  Scheme type (default ${fnt_bld}${frg_typ}${fnt_nrm}) [${fnt_tlc}frg_typ, frg, fragment, fragment_type${fnt_nrm}] [${fnt_tlc}default | nczarr | s3 | xarray | zip | zarr${fnt_nrm}]"
    echo " ${fnt_bld}--frg_sng${fnt_nrm}  User-defined fragment string (empty means none) (default \"${fnt_bld}${frg_sng}${fnt_nrm}\") [${fnt_tlc}frg_sng, fragment_string${fnt_nrm}]"
#    echo " ${fnt_bld}--hrd_pth${fnt_nrm}  Use hard-coded paths on known machines (e.g., chrysalis, compy, cori) NB: Must be first option! [${fnt_tlc}hrd_pth, hard_path, npo, nco_path_override${fnt_nrm}]"
    echo "${fnt_rvr}-s${fnt_nrm} ${fnt_bld}scm_typ${fnt_nrm}  Scheme type (default ${fnt_bld}${scm_typ}${fnt_nrm}) [${fnt_tlc}scm_typ, scm, scheme, scheme_type${fnt_nrm}] [${fnt_tlc}default | file | http | s3${fnt_nrm}]"
    echo " ${fnt_bld}--scm_sng${fnt_nrm}  User-defined scheme string (empty means none) (default \"${fnt_bld}${scm_sng}${fnt_nrm}\") [${fnt_tlc}scm_sng, scheme_string${fnt_nrm}]"
    echo " ${fnt_bld}--version${fnt_nrm}  Version and configuration information [${fnt_tlc}version, vrs, config, configuration, cnf${fnt_nrm}]"
    printf "\n"
    printf "${fnt_rvr}Examples:${fnt_nrm}\n"
    printf "${fnt_bld}ls *.nc | ${spt_nm} | ncea out.nc${fnt_nrm} # Default settings\n"
    printf "${fnt_bld}ls *.nc | ${spt_nm} -s file -f nczarr | ncea out.nc${fnt_nrm} # Explicit type defaults\n"
    printf "${fnt_bld}/bin/ls -d ${HOME}/nco/data/h000? | ${spt_nm} --dbg=2${fnt_nrm} # Debug ${spt_nm}\n"
    printf "${fnt_bld}/bin/ls -d ${HOME}/nco/data/h000? | ${spt_nm} | ncea -O ~/out.nc${fnt_nrm} # Simple test\n"
    printf "${fnt_bld}/bin/ls -d ${HOME}/nco/data/h000? | ${spt_nm} --scm_sng=file:// --frg_sng='#mode=nczarr,file' | ncea -O ~/out.nc${fnt_nrm} # User-defined scheme, mode\n"
    printf "\nComplete documentation at http://nco.sf.net/nco.html#${spt_nm}\n\n"
    exit 1
} # !fnc_usg_prn()

# Parse command-line options:
# http://stackoverflow.com/questions/402377/using-getopts-in-bash-shell-script-to-get-long-and-short-command-line-options (see method by Adam Katz)
# http://tuxtweaks.com/2014/05/bash-getopts
while getopts :d:f:s:-: OPT; do
    case ${OPT} in
	d) dbg_lvl="${OPTARG}" ;; # Debugging level
	f) frg_nm_usr="${OPTARG}" ;; # Fragment name
	s) scm_nm_usr="${OPTARG}" ;; # Scheme name
	-) LONG_OPTARG="${OPTARG#*=}"
	   case ${OPTARG} in
	       # Hereafter ${OPTARG} is long argument key, and ${LONG_OPTARG}, if any, is long argument value
	       # Long options with no argument, no short option counterpart
	       # Long options with argument, no short option counterpart
	       # Long options with short counterparts, ordered by short option key
	       dbg_lvl=?* | dbg=?* | debug=?* | debug_level=?* ) dbg_lvl="${LONG_OPTARG}" ;; # -d # Debugging level
	       frg_typ=?* | frg=?* | fragment=?* | fragment_type=?* ) frg_typ="${LONG_OPTARG}" ;; # -f # Fragment type
	       frg_sng=?* | fragment_string=?* ) frg_sng_usr="${LONG_OPTARG}" ;; # -f # Fragment string
	       hrd_pth | hard_path | npo | nco_path_override ) hrd_pth='Yes' ;; # # Use hard-coded paths on known machines (intentional no-op because already handled prior to getopt())
	       hrd_pth=?* | hard_path=?* | npo=?* | nco_path_override=?* ) echo "No argument allowed for --${OPTARG switch}" >&2; exit 1 ;; # # Use hard-coded paths on known machines
	       scm_typ=?* | scm=?* | scheme=?* | scheme_type=?* ) scm_typ="${LONG_OPTARG}" ;; # # Scheme type
	       scm_sng=?* | scheme_string=?* ) scm_sng_usr="${LONG_OPTARG}" ;; # # Scheme string
	       version | vrs | config | configuration | cnf ) vrs_prn='Yes' ;; # # Print version information
	       version=?* | vrs=?* | config=?* | configuration=?* | cnf=?* ) echo "No argument allowed for --${OPTARG switch}" >&2; exit 1 ;; # # Print version information
               '' ) break ;; # "--" terminates argument processing
               * ) printf "\nERROR: Unrecognized option ${fnt_bld}--${OPTARG}${fnt_nrm}\n" >&2; fnc_usg_prn ;;
	   esac ;; # !OPTARG
	\?) # Unrecognized option
	    printf "\nERROR: Option ${fnt_bld}-${OPTARG}${fnt_nrm} not recognized\n" >&2
	    fnc_usg_prn ;;
    esac # !OPT
done # !getopts
shift $((OPTIND-1)) # Advance one argument
psn_nbr=$#
if [ ${psn_nbr} -ge 1 ]; then
    inp_psn='Yes'
    # 20200430 Input files on command-line mean we need not check standard-input
    std_chk='No'
fi # !psn_nbr

if [ ${vrs_prn} = 'Yes' ]; then
    printf "${spt_nm}, the NCO POSIX<->NCZarr filename filter, version ${nco_vrs} \"${nco_sng}\"\n"
    printf "Copyright (C) 2022--present Charlie Zender\n"
    printf "This program is part of NCO, the netCDF Operators\n"
    printf "NCO is free software and comes with a BIG FAT KISS and ABSOLUTELY NO WARRANTY\n"
    printf "You may redistribute and/or modify NCO under the terms of the\n"
    printf "3-Clause BSD License with exceptions described in the LICENSE file\n"
    printf "BSD: https://opensource.org/licenses/BSD-3-Clause\n"
    printf "LICENSE: https://github.com/nco/nco/tree/master/LICENSE\n"
    printf "Config: ${spt_nm} script located in directory ${drc_spt}\n"
    printf "Config: NCO binaries located in directory ${drc_nco}, linked to netCDF library version ${lbr_vrs}\n"
    if [ "${hrd_pth_fnd}" = 'Yes' ]; then
	printf "Config: Employ NCO machine-dependent hardcoded paths/modules for ${HOSTNAME}. (If desired, turn-off NCO hardcoded paths with \"export NCO_PATH_OVERRIDE=No\").\n"
    else
	printf "Config: No hardcoded machine-dependent path/module overrides. (If desired, turn-on NCO hardcoded paths at supported national labs with \"export NCO_PATH_OVERRIDE=Yes\").\n"
    fi # !hrd_pth_fnd
    exit 0
fi # !vrs_prn

# Detect input on pipe to stdin:
# http://stackoverflow.com/questions/2456750/detect-presence-of-stdin-contents-in-shell-script
# http://unix.stackexchange.com/questions/33049/check-if-pipe-is-empty-and-run-a-command-on-the-data-if-it-isnt
# 20170119 "if [ ! -t 0 ]" tests whether unit 0 (stdin) is connected to terminal, not whether pipe has data
# Non-interactive batch mode (e.g., qsub, sbatch) disconnects stdin from terminal and triggers false-positives with ! -t 0
# 20170123 "if [ -p foo ]" tests whether foo exists and is a pipe or named pipe
# Non-interactive batch mode (i.e., sbatch) behaves as desired for -p /dev/stdin on SLURM
# Non-interactive batch mode (e.g., qsub) always returns true for -p /dev/stdin on PBS, leads to FALSE POSITIVES!
# This is because PBS uses stdin to set the job name
# Hence -p /dev/stdin test works everywhere tested except PBS non-interactive batch environment
# Check stdin if user has not explicitly disallowed it with --no_stdin
if [ "${std_chk}" = 'Yes' ]; then
    if [ -n "${PBS_ENVIRONMENT}" ]; then
	if [ "${PBS_ENVIRONMENT}" = 'PBS_BATCH' ]; then
	    # PBS batch detection suggested by OLCF ticket CCS #338970 on 20170127
	    bch_pbs='Yes'
	fi # !PBS_ENVIRONMENT
    fi # !PBS
    if [ -n "${SLURM_JOBID}" ] && [ -z "${SLURM_PTY_PORT}" ]; then
	# SLURM batch detection suggested by NERSC ticket INC0096873 on 20170127
	bch_slr='Yes'
    fi # !SLURM
    if [ ${bch_pbs} = 'Yes' ] || [ ${bch_slr} = 'Yes' ]; then
	# Batch environment
	if [ ${bch_pbs} = 'Yes' ]; then
	    if [ ! -p /dev/stdin ]; then
		# PBS batch jobs cause -p to return true except for stdin redirection 
		# When -p returns true we do not know whether stdin pipe contains any input
		# User must explicitly indicate use of stdin pipes with --stdin option
		# Redirection in PBS batch jobs unambiguously causes -p to return false
		inp_std='Yes'
	    fi # !stdin
	fi # !bch_slr
	if [ ${bch_slr} = 'Yes' ]; then
	    if [ -p /dev/stdin ]; then
		# SLURM batch jobs cause -p to return true for stdin pipes
		# When -p returns false we do not know whether output was redirectd
		# User must explicitly indicate use of redirection with --stdin option
		# Stdin pipes in SLURM batch jobs unambiguously cause -p to return true
		inp_std='Yes'
	    fi # !stdin
	fi # !bch_slr
    else # !bch
	# Interactive environment
	if [ -p /dev/stdin ] || [ ! -t 0 ]; then
	    # Interactive environments unambiguously cause -p to return true for stdin pipes
	    # Interactive environments unambiguously cause -t 0 to return false for stdin redirection
	    inp_std='Yes'
	fi # !stdin
    fi # !bch
    if [ ${inp_std} = 'Yes' ] && [ ${inp_psn} = 'Yes' ]; then
	echo "${spt_nm}: ERROR expecting input from both stdin and positional command-line arguments"
	exit 1
    fi # !inp_std
fi # !std_chk

# Read files from stdin pipe, positional arguments, or directory glob
#printf "dbg: inp_aut  = ${inp_aut}\n"
#printf "dbg: inp_glb  = ${inp_glb}\n"
#printf "dbg: inp_psn  = ${inp_psn}\n"
#printf "dbg: inp_std  = ${inp_std}\n"

# Check argument number and complain accordingly
arg_nbr=$#
#printf "\ndbg: Number of arguments: ${arg_nbr}"
if [ ${inp_std} != 'Yes' ]; then
    if [ ${arg_nbr} -eq 0 ]; then
	fnc_usg_prn
    fi # !arg_nbr
fi # !inp_std

if [ -n "${frg_sng_usr}" ]; then
    frg_typ='user-defined'
    frg_sng="${frg_sng_usr}"
fi # !frg_sng_usr    
if [ -n "${scm_sng_usr}" ]; then
    sng_typ='user-defined'
    scm_sng="${scm_sng_usr}"
fi # !scm_sng_usr    

if [ -z "${frg_sng_usr}" ]; then
    if [ "${frg_typ}" = ${frg_dfl} ] || [[ "${frg_typ}" =~ dfl ]] || [[ "${frg_typ}" =~ default ]]; then 
	frg_typ=${frg_ncz}
    fi # !frg_dfl
    if [ "${frg_typ}" = ${frg_ncz} ] || [[ "${frg_typ}" =~ (ncz|[nN][cC][zZ]arr) ]]; then 
	frg_typ=${frg_ncz}
	frg_sng='#mode=nczarr,file'
    elif [ "${frg_typ}" = ${frg_s3} ] || [ "${frg_typ}" = 's3' ] || [[ "${frg_typ}" =~ [sS]3 ]] || [[ "${frg_typ}" =~ (aws|AWS) ]] || [[ "${frg_typ}" =~ [aA]mazon ]]; then 
	frg_typ=${frg_s3}
	frg_sng='#mode=nczarr,s3'
    elif [ "${frg_typ}" = ${frg_xry} ] || [[ "${frg_typ}" =~ (xry|xarray) ]]; then 
	frg_typ=${frg_xry}
	frg_sng='#mode=xarray,file'
    elif [ "${frg_typ}" = ${frg_zip} ] || [[ "${frg_typ}" =~ [zZ]ip ]]; then 
	frg_typ=${frg_zip}
	frg_sng='#mode=nczarr,zip'
    elif [ "${frg_typ}" = ${frg_zrr} ] || [[ "${frg_typ}" =~ (zrr|ZRR|Zarr|zarr) ]]; then 
	frg_typ=${frg_zrr}
	frg_sng='#mode=zarr,file'
    else 
	echo "ERROR: Invalid -f frg_typ option = ${frg_typ}"
	echo "HINT: Valid frg_typ arguments include '${frg_dfl}' (or 'default'), '${frg_ncz}' (or 'nczarr' or 'NCZarr'), '${frg_s3}' (or 'S3'), '${frg_xry}' (or 'xarray'), '${frg_zip}' (or 'Zip'), and '${frg_zrr}' (or 'zarr' or 'Zarr'). For testing, select '${frg_dfl}' which causes ${spt_nm} to do something useful."
	exit 1
    fi # !frg_typ
fi # !frg_sng_usr

if [ -z "${scm_sng_usr}" ]; then
    if [ "${scm_typ}" = ${scm_dfl} ] || [[ "${scm_typ}" =~ dfl ]] || [[ "${scm_typ}" =~ default ]]; then 
	scm_typ=${scm_file}
    fi # !scm_dfl
    if [ "${scm_typ}" = ${scm_file} ] || [[ "${scm_typ}" =~ [fF]ile ]] || [[ "${scm_typ}" =~ [fF]l ]]; then 
	scm_typ=${scm_file}
	scm_sng='file://'
    elif [ "${scm_typ}" = ${scm_http} ] || [[ "${scm_typ}" =~ (http|HTTP) ]]; then 
	scm_typ=${scm_http}
	scm_sng='https://'
    elif [ "${scm_typ}" = ${scm_s3} ] || [ "${scm_typ}" = 's3' ] || [[ "${scm_typ}" =~ [sS]3 ]] || [[ "${scm_typ}" =~ (aws|AWS) ]] || [[ "${scm_typ}" =~ [aA]mazon ]]; then 
	scm_typ=${scm_s3}
	scm_sng='s3://'
    else 
	echo "ERROR: Invalid -s scm_typ option = ${scm_typ}"
	echo "HINT: Valid scm_typ arguments include '${scm_file}' (or 'fl'), '${scm_http}' (or 'HTTP'), and '${scm_s3}' (or 'S3' or 'AWS' or 'amazon'). For testing, select '${scm_file}' which causes ${spt_nm} to do something useful."
	exit 1
    fi # !scm_typ
fi # !scm_sng_usr

if [ ${inp_glb} = 'Yes' ]; then 
    for fl in "${drc_in}"/*.nc "${drc_in}"/*.nc3 "${drc_in}"/*.nc4 "${drc_in}"/*.nc5 "${drc_in}"/*.nc6 "${drc_in}"/*.nc7 "${drc_in}"/*.cdf "${drc_in}"/*.hdf "${drc_in}"/*.he5 "${drc_in}"/*.h5 ; do
	if [ -f "${fl}" ]; then
	    fl_in[${fl_nbr}]=${fl}
	    let fl_nbr=${fl_nbr}+1
	fi # !file
    done
fi # !inp_glb
if [ ${inp_psn} = 'Yes' ]; then
    # Read any positional arguments
    for ((psn_idx=1;psn_idx<=psn_nbr;psn_idx++)); do
	fl_in[(${psn_idx}-1)]=${!psn_idx}
	fl_nbr=${psn_nbr}
    done # !psn_idx
fi # !inp_psn
if [ ${inp_std} = 'Yes' ]; then
    # Input awaits on unit 0, i.e., on stdin
    while read -r line; do # NeR05 p. 179
	fl_in[${fl_nbr}]=${line}
	let fl_nbr=${fl_nbr}+1
    done < /dev/stdin
fi # !inp_std

# Prepend drc_in to fl_in in MFOs (ncea, ncecat, ncra, ncrcat)
ppn_opt="-p ${drc_in}"
# 20220111 If input files include absolute path, then use fl_in as-is later on
# 20220130 fl_in is defined here only for inp_[std,cmd,glb], do not test iff inp_aut
if [ "${fl_in[0]}" ] && [ "$(basename ${fl_in[0]})" != "${fl_in[0]}" ]; then
    ppn_opt=''
fi # !basename

# Print initial state
if [ ${dbg_lvl} -ge 2 ]; then
    printf "dbg: dbg_lvl = ${dbg_lvl}\n"
    printf "dbg: frg_sng = ${frg_sng}\n"
    printf "dbg: frg_typ = ${frg_typ}\n"
    printf "dbg: hrd_pth = ${hrd_pth}\n"
    printf "dbg: inp_aut = ${inp_aut}\n"
    printf "dbg: inp_glb = ${inp_glb}\n"
    printf "dbg: inp_psn = ${inp_psn}\n"
    printf "dbg: inp_std = ${inp_std}\n"
    printf "dbg: scm_sng = ${scm_sng}\n"
    printf "dbg: scm_typ = ${scm_typ}\n"
fi # !dbg
if [ ${dbg_lvl} -ge 2 ]; then
    psn_nbr=$#
    if [ ${psn_nbr} -ge 1 ]; then
	printf "dbg: Found ${psn_nbr} positional parameters (besides \$0):\n"
	for ((psn_idx=1;psn_idx<=psn_nbr;psn_idx++)); do
	    printf "dbg: psn_arg[${psn_idx}] = ${!psn_idx}\n"
	done # !psn_idx
    fi # !psn_nbr
    if [ "${fl_nbr}" -ge 1 ]; then
	printf "dbg: Filtering ${fl_nbr} filenames from/to stdin:\n"
	for ((fl_idx=0;fl_idx<fl_nbr;fl_idx++)); do
	    printf "dbg: fl_in[${fl_idx}] = ${fl_in[${fl_idx}]}\n"
	done # !fl_idx
    fi # !fl_nbr
fi # !dbg

# Human-readable summary
date_srt=$(date +"%s")
if [ ${dbg_lvl} -ge 2 ]; then
    printf "ncz2psx, NCO's NCZarr<->POSIX filter, invoked with command:\n"
    echo "${cmd_ln}"
fi # !dbg

# Main Loop
if [ "${fl_nbr}" -ge 1 ]; then
    # Prepend scheme string to filename
    for ((fl_idx=0;fl_idx<fl_nbr;fl_idx++)); do
	fl_out[${fl_idx}]="${scm_sng}${fl_in[${fl_idx}]}${frg_sng}"
	#printf "fl_out[${fl_idx}] = ${fl_out[${fl_idx}]}\n"
	echo ${fl_out[${fl_idx}]}
    done # !fl_idx
fi # !fl_nbr

if [ ${dbg_lvl} -ge 2 ]; then
    date_end=$(date +"%s")
    date_dff=$((date_end-date_srt))
    echo "Elapsed time $((date_dff/60))m$((date_dff % 60))s"
fi # !dbg

exit 0
