# Copyright 2026 Nadeŭka # Distributed under the terms of the GNU General Public License v2 # @ECLASS: egetent.eclass # @MAINTAINER: # Nadeŭka # @SUPPORTED_EAPIS: 8 9 # @BLURB: Root-aware drop-in replacement for getent(1) # @DESCRIPTION: # A drop-in replacement for GNU glibc getent(1) with alternate ROOT support. # # For 'ahosts', 'ahostsv4', and 'ahostsv6' database queries with keys, this eclass # delegates directly to the host's getent(1) to perform getaddrinfo(3) network socket # resolution. # # For all other databases (and enumeration of ahosts*), it parses the target # database files directly in Bash, respecting -R/--root options and ${ROOT} path, # mirroring glibc getent output formatting, padding, and exit codes. # # Supported databases: # ahosts, ahostsv4, ahostsv6, aliases, ethers, group, gshadow, hosts, # initgroups, netgroup, networks, passwd, protocols, rpc, services, shadow, # subuid, subgid. # # Exit status codes: # 0 - Command completed successfully. # 1 - Missing arguments, or database unknown. # 2 - One or more supplied keys could not be found, or target database file is missing. # 3 - Enumeration not supported on this database (ethers, netgroup, initgroups). # 64 - Command line option parsing error (missing option argument). # # The eclass does not rely on extglob, so it leaves the global shell options untouched. if [[ ! -v _EGETENT_ECLASS ]]; then _EGETENT_ECLASS=1 case ${EAPI} in 8|9) ;; *) die "${ECLASS}: EAPI ${EAPI:-0} not supported" ;; esac # @FUNCTION: egetent_try # @INTERNAL # @DESCRIPTION: # Prints the standard getent usage hint to stdout (callers redirect if needed). egetent_try() { printf "Try \`getent --help' or \`getent --usage' for more information.\n" } # @FUNCTION: egetent_print_entry # @USAGE: # @INTERNAL # @DESCRIPTION: # Formats and prints a single database entry to stdout according to glibc getent specifications. egetent_print_entry() { local -a w=( $2 ) local pad sep=' ' v case $1 in aliases) v=${2#*:} printf '%-15s %s\n' "${2%%:*}:" "${v#"${v%%[![:space:]]*}"}" return ;; ethers) printf '%s %s\n' "${w[0]}" "${w[1]}" return ;; protocols|networks|services) pad=21 ;; rpc) pad=15 sep=' ' ;; hosts|ahosts*) pad=15 ;; *) printf '%s\n' "$2"; return ;; esac printf '%-*s %s%s\n' "${pad}" "${w[0]}" "${w[1]}" "${w[2]:+${sep}${w[*]:2}}" } # @FUNCTION: egetent_match_key # @USAGE: # @INTERNAL # @DESCRIPTION: # Evaluates whether a database entry matches the specified query key. # Returns 0 on exact match, 2 on secondary fallback match (e.g. IPv4 line for # a hostname in hosts), 1 otherwise. egetent_match_key() { local db=$1 k=$2 n= local -a w # colon-separated databases: name is field 0, numeric id is field n case ${db} in passwd|group) n=2 ;; subuid|subgid) n=1 ;; shadow|gshadow|aliases) n=0 ;; esac if [[ -n ${n} ]]; then IFS=: read -ra w <<< "$3" [[ ${w[0]} == "${k}" || -n ${k} && -z ${k//[0-9]} && ${w[n]} == "${k}" ]] return fi w=( $3 ) local all=" ${w[*]} " ipv4='^[0-9]+(\.[0-9]+){3}$' case ${db} in services) local names=" ${w[0]} ${w[*]:2} " if [[ ${k} == */* ]]; then [[ ${w[1]} == *"/${k#*/}" && ( ${w[1]} == "${k}" || ${names} == *" ${k%/*} "* ) ]] return fi [[ -n ${k} && -z ${k//[0-9]} && ${w[1]} == "${k}/"* || ${names} == *" ${k} "* ]] ;; hosts) # address key: compare with the first field only if [[ ${k} == *:* || ${k} =~ ${ipv4} ]]; then [[ ${w[0],,} == "${k,,}" ]] return fi # name key: IPv6 line is an exact match, IPv4 line a fallback [[ ${all,,} == *" ${k,,} "* ]] || return 1 [[ ${w[0]} == *:* ]] && return 0 return 2 ;; ethers) [[ ${all,,} == *" ${k,,} "* ]] ;; *) [[ ${all} == *" ${k} "* ]] ;; esac } # @FUNCTION: egetent # @USAGE: [-R|--root ] [-s|--service ] [-A|--no-addrconfig] [-i|--no-idn] [key ...] # @DESCRIPTION: # Query administrative databases with alternate ROOT support. egetent() { local root=${ROOT:-/} db local -a opts=() while (( $# )); do case $1 in -R|--root|-s|--service) if (( $# < 2 )); then if [[ $1 == --* ]]; then printf "getent: option '%s' requires an argument\n" "$1" >&2 else printf "getent: option requires an argument -- '%s'\n" "${1#-}" >&2 fi egetent_try >&2 return 64 fi if [[ $1 == -R || $1 == --root ]]; then root=$2 else opts+=( "$1" "$2" ) fi shift 2 ;; --root=*) root=${1#*=}; shift ;; --) shift; break ;; -*) opts+=( "$1" ); shift ;; *) break ;; esac done if (( $# < 1 )); then printf 'getent: wrong number of arguments\n' >&2 egetent_try return 1 fi db=$1; shift case ${db} in ahosts|ahostsv4|ahostsv6|aliases|ethers|group|gshadow|hosts|initgroups|netgroup|networks|passwd|protocols|rpc|services|shadow|subuid|subgid) ;; *) printf 'Unknown database: %s\n' "${db}" >&2 egetent_try return 1 ;; esac if (( $# == 0 )); then case ${db} in ethers|netgroup|initgroups) printf 'Enumeration not supported on %s\n' "${db}" >&2 return 3 ;; esac elif [[ ${db} == ahosts* ]]; then # network resolution cannot be emulated from files: delegate to the host getent "${opts[@]}" "${db}" "$@" return fi local etc=${root%/}/etc file=${db} case ${db} in ahosts*) file=hosts ;; initgroups) file=group ;; aliases) [[ -r ${etc}/aliases ]] || file=mail/aliases ;; esac [[ -r ${etc}/${file} ]] || return 2 # load non-empty, comment-stripped lines local -a lines=() local line key found addr ret=0 while IFS= read -r line || [[ -n ${line} ]]; do line=${line%%#*} [[ ${line} == *[![:space:]]* ]] && lines+=( "${line}" ) done < "${etc}/${file}" if [[ ${db} == initgroups ]]; then local gids gid mem for key; do gids= for line in "${lines[@]}"; do IFS=: read -r _ _ gid mem <<< "${line}" [[ ,${mem}, == *",${key},"* && " ${gids} " != *" ${gid} "* ]] && gids+=" ${gid}" done printf '%-21s%s\n' "${key}" "${gids}" done return 0 fi # enumeration (hosts/ahosts* skip IPv6 lines) if (( $# == 0 )); then ret=2 for line in "${lines[@]}"; do if [[ ${db} == hosts || ${db} == ahosts* ]]; then read -r addr _ <<< "${line}" [[ ${addr} == *:* ]] && continue fi egetent_print_entry "${db}" "${line}" ret=0 done return ${ret} fi # key lookup: first exact match wins, otherwise first fallback match for key; do found= for line in "${lines[@]}"; do egetent_match_key "${db}" "${key}" "${line}" case $? in 0) found=${line}; break ;; 2) found=${found:-${line}} ;; esac done if [[ -n ${found} ]]; then egetent_print_entry "${db}" "${found}" else ret=2 fi done return ${ret} } fi