#!/usr/bin/bash

# Copyright (c) 2023-2026 Douglas Gilbert.
#
# SPDX-License-Identifier: BSD-2-Clause
#
# This script is designed to print out pseudo file system file names
# followed by their ASCII contents (i.e. their values) as appropriate.
# The file contents are not precise and modified to be printable on
# a single line. This utility targets sysfs (/sys ) in Linux but may
# be useful in other contexts and on other Unix like OSes.
#
# It is just too hard and too slow to process all the desired detection
# and representation of the contents of each regular file. For example
# the timeout(1) command takes 100 milliseconds even when the file can
# be read immediately. When scanning sysfs that is over 80% of the cases.
# The solution was to rely on a utility written in C whose source is in
#    ../src/ls_name_value_rd.c
# That is called for every filename that might have displayable contents
# (e.g. a directory name should not be passed to that C utility, but if
# it is <directory> is printed out).

version_str="1.26 20260913"

all=0
dir=0
empty=0
native_order=0  # 'ls -f' used for actual (native) directory order
nosym=0
num_val=80      # contents should fit on a line if the filename isn't too long
otherfs=0
show=0
is_root=0       # 0 for true; 1 for false (opposite of C)
first_st_dev=0
physical=0
utf8=0
verbose=0
writ_ind=0
content_reader="ls_name_value_rd"    # C program in lsscsi package
ls_bin="ls"                          # want to bypass any shell aliases
ls_args=() # this is an array holding absolute path to ls with its arguments
lnv_rd_opts=() # this is an array holding the command line options to
               # ls_name_value_rd

# The following are for the getopt(1) command from util-linux which
# follows the function definitions, about 230 lines down.
script_name=$(basename "$0")
short="adefhn:NopsuvVw"
long="all,dir,empty,help,native,num:,nosym,otherfs,physical,show,utf8,verbose,version,write"


usage()
{
  echo -n  "Usage: ls_name_value [-a] [-d] [-e] [-f] [-h] [-N] [-n NUM] "
  echo "[-o] [-p]"
  echo "                     [-s] [-u] [-v] [-V] [-w] [--] [<name>*]"
  echo -n "  where:  -a, --all         include hidden filenames (starting "
  echo "with '.')"
  echo "          -d, --dir         enter directories (descends 1 level)"
  echo "                            if given twice (-dd) descends 2 levels"
  echo -n "          -e, --empty       output <value> as '<empty>' rather "
  echo "than blank"
  echo "          -f, --native      use directory order (equiv: 'ls -f')"
  echo "          -h, --help        print usage message"
  echo "          -N, --nosym       do not descend symlinks to directories"
  echo "          -n, --num=NUM     max bytes in value (def: 80)"
  echo -n "          -o, --otherfs     not entering an other fs is default, "
  echo "override"
  echo -n "          -p, --physical    when entering a directory show "
  echo "physical"
  echo "                            address on next line (with 'pwd -P')"
  echo "          -s, --show        show directories, symlinks and specials"
  echo "          -u, --utf8        if <value> passes UTF-8 tests, output it"
  echo "          -v, --verbose     increase verbosity of output"
  echo "          -V, --version     print version string then exit"
  echo "          -w, --write       separator changed from : to + if writable"
  echo "          --                needed if any filenames start with '-'"
  echo ""
  echo "Prints lines of the form '<name> : <value>' where <name> is a regular"
  echo "filename. The <value> is the contents of <name>, up to 80 bytes of"
  echo "printable (7 bit) ASCII. If <value> cannot be read in 1 second, then"
  echo "<timeout> is printed instead. If one or more <name>s are given on the"
  echo "command line then they are used. If there are no names on the command"
  echo "line, then a listing of the regular files in the current directory is"
  echo "used. If one <name> is given and it is a directory then it is entered"
  echo "and then list the files in that directory."
  echo "Primarily designed for viewing sysfs in Linux (i.e. under /sys )."
}

# Returns owner permission of ${1} as single (octal) digit. Therefore it
# returns a value of 0 to 7. 0 for no permissions, 7 for read, write and
# execute (4+2+1)
get_root_perm()
{
  local s

  if [[ -e "$1" ]] ; then
    # 'stat -c ...' is a Linux extension. For FreeBSD use '-f'
    s=$(stat -c '%a' -- "$1" 2> /dev/null )
    if [[ -n "$s" ]] ; then
      echo "${s:0:1}"      # 1 octal digit (0 to 7)
    else
      echo "0"
    fi
  else
    echo "0"
  fi
}

# Takes filename to display name:value of as $1. Optionally takes
# leading pad as $2
one_name_value()
{
  local grp
  local name
  local value
  local pad
  local separ
  local fin_line
  local set_name
  local err

  if [[ $# -gt 1 ]] ; then
    pad=$2
  fi

  if [[ "${1:0:1}" = "/" ]] || [[ "${1:0:1}" = "." ]] ; then
    name="${pad}$( basename "$1" ) "
  else
    name="${pad}${1} "
  fi
  separ=":"

  if [[ -f "${1}" ]] ; then  # if exists + regular file (or symlink to regular)
    if [[ "${is_root}" -ne 0 ]] ; then    # false, so non-root
      if [[ "${writ_ind}" -gt 0 ]] && [[ -w "${1}" ]] ; then
        separ="+"
      fi
      echo -n "${name}"
      if [[ -h "${1}" ]] ; then
        echo -n "---> "
        readlink -n "${1}"
        echo -n " ${separ} "
      else
        echo -n "${separ} "
      fi

      # calls helper C function, waits up to 1 second for "hanging" files
      if [[ "${verbose}" -gt 4 ]] ; then
        echo -n "ls_name_value_rd invocation: "
        echo -n "${content_reader} "
        echo -n "${lnv_rd_opts[@]} "
        echo "${1} ${num_val}"
      fi
      value=$( "${content_reader}" "${lnv_rd_opts[@]}" "${1}" "${num_val}" )

      err=$?
      if [[ "${err}" -ne 0 ]] ; then
        echo " <error [[${err}]>"
      elif [[ -n "${value}" ]] ; then
        echo "${value}"
      else
        echo " "
      fi
    else        # run by root below, still regular file
      grp=$( get_root_perm "${1}" )
      if [[ "${grp}" -ge 4 ]] ; then      # at least read permissions
        if [[ "${writ_ind}" -gt 0 ]] && [[ "${grp}" -ge 6 ]] ; then
          separ="+"
        fi
        echo -n "${name}"
        if [[ -h "${1}" ]] ; then
          echo -n "---> "
          readlink -n "${1}"
          echo -n " ${separ} "
        else
          echo -n "${separ} "
        fi
        # calls helper C function, waits up to 1 second for "hanging" files
        if [[ "${verbose}" -gt 4 ]] ; then
          echo -n "ls_name_value_rd invocation: "
          echo -n "${content_reader} "
          echo -n "${lnv_rd_opts[@]}"
          echo "${1} ${num_val}"
        fi
        value=$( "${content_reader}" "${lnv_rd_opts[@]}" "${1}" "${num_val}" )

        err=$?
        if [[ "${err}" -ne 0 ]] ; then
          value=" <error [${err}]>"
        elif [[ -n "${value}" ]] ; then
          echo "${value}"
        else
          echo " "
        fi
      elif [[ "${grp}" -ge 2 ]] ; then   # at least write perms
        echo "${name} : <write only>"
      else
        echo "${name} : <cannot access>"
      fi
    fi
  else      # doesn't exist or irregular
    if [[ -e "${1}" ]] ; then       # exists so must be irregular
      if [[ "$show" -gt 0 ]] ; then   # irregular file (maybe symlink to ...)
        fin_line=0
        set_name=0
        if [[ -h "${1}" ]] ; then
          echo -n "${name}"
          set_name=1
          if [[ -d "${1}" ]] ; then
            echo -n "---> ["
            readlink -n "${1}"
            echo "]"
            fin_line=1
          else
            echo -n "---> "
            readlink -n "${1}"
            echo -n " :"
          fi
        else
          if [[ -d "${1}" ]] ; then
            echo "${pad}[${1}]"
            fin_line=1
          fi
        fi
        if [[ "${fin_line}" -eq 0 ]] ; then # not a directory nor symlink to one
          if [[ "${set_name}" -eq 0 ]] ; then
            echo -n "${name}:"
          fi
          if [[ -c "${1}" ]] ; then
            echo " <char device>"
          elif [[ -b "${1}" ]] ; then
            echo " <block device>"
          elif [[ -p "${1}" ]] ; then
            echo " <named pipe>"
          elif [[ -S "${1}" ]] ; then
            echo " <socket>"
          else
            echo " <unknown>"
          fi
        fi
      fi    # end of --show not given
    else    # doesn't exist: either dangling symlink or gone missing
      echo -n "${name}"
      if [[ -h "${1}" ]] ; then
        echo -n "---> "
        readlink -n "${1}"
        echo " : <target missing>"
      else
        echo " : <missing>"
      fi
    fi
  fi    # if irregular file and --show not given then bypass
}

if (( EUID != 0 )); then
  is_root=1     # false
fi

# Reference: /usr/share/doc/util-linux/examples/getopt-example.bash
if ! TEMP=$(getopt -o "${short}" --long "${long}" --name "$script_name" -- "$@") ; then
  echo "If any filenames might start with '-' then invoke like this:" >&2
  echo "    ls_name_value [<any_options>] -- [<name>*]" >&2
  echo 'Terminating...' >&2
  exit 1
fi

eval set -- "${TEMP}"

while :; do
  case "${1}" in
    -a | --all        ) (( all=all+1 )) ;          shift 1 ;;
    -d | --dir        ) (( dir=dir+1 )) ;          shift 1 ;;
    -e | --empty      ) (( empty=empty+1 )) ;      shift 1 ;;
    -f | --native     ) (( native_order=native_order+1 )) ;  shift 1 ;;
    -h | --help       ) usage;                     exit 0 ;;
    -n | --num        )
                num_val="$2"
                if ! [[ "$num_val" -eq "$num_val" ]] 2> /dev/null ; then
                  echo "--num expects an integer as its argument"
                  exit 1
                elif [[ "$num_val" -lt 1 ]] ; then
                  echo "--num expects an integer > 0"
                  exit 1
                fi
                shift 2 ;;
    -N | --nosym      ) (( nosym=nosym+1 )) ;       shift 1 ;;
    -o | --otherfs    ) (( otherfs=otherfs+1 )) ;   shift 1 ;;
    -p | --physical   ) (( physical=physical+1 )) ; shift 1 ;;
    -s | --show       ) (( show=show+1 )) ;         shift 1 ;;
    -u | --utf8       ) (( utf8=utf8+1 )) ;         shift 1 ;;
    -v | --verbose    ) (( verbose=verbose+1 )) ;   shift 1 ;;
    -V | --version    ) echo "${version_str}" ;     exit 0 ;;
    -w | --write      ) (( writ_ind=writ_ind+1 )) ; shift 1 ;;
    --                ) shift;                      break ;;
    *                 ) echo "Error parsing $1";    exit 1 ;;
  esac
done

if command -v "${ls_bin}" > /dev/null 2>&1 ; then
  ls_bin=$( command -v "${ls_bin}" )
  ls_args+=( "${ls_bin}" )
  if [[ "${all}" -gt 0 ]] ; then
    if [[ "${native_order}" -gt 0 ]] ; then
      ls_args+=("-f")
      ls_args+=("-A")
    else
      ls_args+=("-A")
    fi
  elif [[ "${native_order}" -gt 0 ]] ; then
    ls_args+=("-f")
  fi
  ls_args+=("--zero")
  ls_args+=("--")            # need this for filenames that start with '-'
  if [[ "${verbose}" -gt 2 ]] ; then
    echo -n "ls invocation 3: "
    echo "${ls_args[@]}"
  fi
else
  echo "Unable to find ls binary, is this Unix? ; exiting"
  exit 1
fi

# In production ${content_reader} is expected to be on $PATH.
# In development it should be found in a sibling directory.
if ! command -v "${content_reader}" > /dev/null 2>&1 ; then
  dev_content_reader="../src/${content_reader}"
  if [[ -x "${dev_content_reader}" ]] ; then
    content_reader=$(pwd)/"${dev_content_reader}"
  else
    echo "Unable to find helper: ${content_reader}, exiting"
    exit 1
  fi
fi
if [[ "${verbose}" -gt 2 ]] ; then
  echo -n "content_reader: "
  echo "${content_reader}"
fi

# If one argument given and it's a directory, cd to it and swallow
if [[ $# -eq 1 ]] && [[ -e "$1" ]] && [[ -d "$1" ]] && [[ -x "$1" ]] ; then
  # 'stat -c ...' is a Linux extension. For FreeBSD use '-f'
  first_st_dev=$( stat -c %d -- "$1" 2> /dev/null )
  if ! cd -- "$1" ; then
    echo "cd to $1 failed from $( pwd -P) "
    exit 1
  fi
  shift
fi

if [[ "${verbose}" -gt 0 ]] ; then
  echo -n "> current working directory: "
  pwd
  if [[ "${verbose}" -gt 1 ]] ; then
    echo -n "> physical working directory: "
    pwd -P
  fi
  echo ""
fi

SAVEIFS=$IFS
IFS=$'\n'

if [[ "${empty}" -eq 1 ]] ; then
  lnv_rd_opts+=("-e")
elif [[ "${empty}" -gt 1 ]] ; then
  lnv_rd_opts+=("-ee")
fi

if [[ "${utf8}" -gt 0 ]] ; then
  lnv_rd_opts+=("-u")
fi

if [[ "${verbose}" -eq 3 ]] ; then
  lnv_rd_opts+=("-v")
elif [[ "${verbose}" -gt 3 ]] ; then
  vv_opt="-v"
  n=${verbose}
  for (( k=3 ; k<n ; k=k+1 )) ; do
    vv_opt="${vv_opt}v"   # append a 'v'
  done
  lnv_rd_opts+=("${vv_opt}")
fi
lnv_rd_opts+=("--")

n=$#
if [[ $# -gt 0 ]] ; then
  # place command line arguments into a bash array
  for (( k=0 ; k<n ; k=k+1 )) ; do
    fileArray[k]="$1"
    shift
  done
else
  # Place filenames of current directory in fileArray (a bash array)
  mapfile -d '' -t fileArray < <( "${ls_args[@]}" 2> /dev/null )
fi

tLen=${#fileArray[@]}
for (( k=0; k<tLen; k++ )) ; do
  name1="${fileArray[$k]}"
  if [[ "${verbose}" -gt 3 ]] ; then
    echo ">> fileArray[$k]: ${name1}"
  fi
  if [[ "${first_st_dev}" -eq 0 ]] ; then
    if [[ -e "${name1}" ]] ; then
      first_st_dev=$( stat -c %d -- "${name1}" 2> /dev/null )
    fi
  fi
  if [[ -h "${name1}" ]] ; then
    symlnk1=0
    [[ "${nosym}" -gt 0 ]]
    nosym_active1=$?
  else
    symlnk1=1
    nosym_active1=1
  fi
  if [[ "${dir}" -gt 0 ]] && [[ -d "${name1}" ]] && [[ -x "${name1}" ]] && [[ "${nosym_active1}" -ne 0 ]] ; then
    if [[ "${otherfs}" -eq 0 ]] ; then
      if [[ -e "${name1}" ]] ; then
        a_st_dev=$( stat -c %d -- "${name1}" 2> /dev/null )
      fi
    else
      a_st_dev=${first_st_dev}
    fi
    if [[ "${first_st_dev}" -eq "${a_st_dev}" ]] ; then
      echo -n ">> enter directory: "
      if [[ -h "${name1}" ]] ; then
        echo -n "${name1}"
        echo -n " ---> [ "
        readlink -n "${name1}"
        echo " ]"
      else
        echo "[ ${name1} ]"
      fi
      cur=$( pwd )
      if ! cd -- "${name1}" ; then
        echo "cd to ${name1} failed from $( pwd -P) "
        IFS=$SAVEIFS
        exit 1
      fi
      if [[ "${physical}" -gt 0 ]] ; then
        value=$( pwd -P )
        echo "[ ${value} ]"
      elif [[ "${verbose}" -gt 2 ]] && [[ "${symlnk1}" -eq 0 ]] ; then
        echo -n "  > physical working directory: "
        pwd -P
      fi
      # Place filenames of current directory in fileArray2
      mapfile -d '' -t fileArray2 < <( "${ls_args[@]}" 2> /dev/null )
      t2Len=${#fileArray2[@]}
      for (( j=0; j<t2Len; j++ )) ; do
        name2="${fileArray2[$j]}"
        if [[ -h "${name2}" ]] ; then
          symlnk2=0
          [[ "${nosym}" -gt 0 ]]
          nosym_active2=$?
        else
          symlnk2=1
          nosym_active2=1
        fi
        [[ "${nosym}" -gt 0 ]] && [[ -h "${name2}" ]]
        nosym_active2=$?
        if [[ "${dir}" -gt 1 ]] && [[ -d "${name2}" ]] && [[ -x "${name2}" ]] && [[ "${nosym_active2}" -ne 0 ]] ; then
          if [[ "${otherfs}" -eq 0 ]] ; then
            b_st_dev=$( stat -c %d -- "${name2}" 2> /dev/null )
          else
            b_st_dev=${first_st_dev}
          fi
          if [[ "${first_st_dev}" -eq "${b_st_dev}" ]] ; then
            echo -n "    >> enter directory: "
            if [[ -h "${name2}" ]] ; then
              echo -n "${name2}"
              echo -n " ---> [ "
              readlink -n "${name2}"
              echo " ]"
            else
              echo "[ ${name2} ]"
            fi
            cur2=$( pwd )
            if ! cd -- "${name2}" ; then
              echo "cd to ${name2} failed from $( pwd -P) "
              IFS=$SAVEIFS
              exit 1
            fi
            if [[ "${physical}" -gt 0 ]] ; then
              value=$( pwd -P )
              echo "[ ${value} ]"
            elif [[ "${verbose}" -gt 2 ]] && [[ "${symlnk2}" -eq 0 ]] ; then
              echo -n "  > physical working directory: "
              pwd -P
            fi
            # Place filenames of current directory in fileArray3
            mapfile -d '' -t fileArray3 < <( "${ls_args[@]}" 2> /dev/null )
            t3Len=${#fileArray3[@]}
            for (( m=0; m<t3Len; m++ )) ; do
              one_name_value "${fileArray3[m]}" "        "
            done
            if ! cd -- "${cur2}" ; then
              echo "cd to ${cur2} failed from $( pwd -P) "
              IFS=$SAVEIFS
              exit 1
            fi
            # echo ""
          else
            echo -n "    >> not entering directory: ${name2} : "
            echo -n "<different filesystem"
            if [[ "${verbose}" -gt 0 ]] ; then
              echo " [st_dev=${b_st_dev}]>"
            else
              echo ">"
            fi
          fi
        else
          one_name_value "${name2}" "    "
        fi
      done
      if ! cd -- "${cur}" ; then
        echo "cd to ${cur} failed from $( pwd -P) "
        IFS=$SAVEIFS
        exit 1
      fi
      # echo ""
    else
      echo -n ">> not entering directory: ${name1} : <different filesystem"
      if [[ "${verbose}" -gt 0 ]] ; then
        echo " [st_dev=${a_st_dev}]>"
      else
        echo ">"
      fi
    fi
  else
    one_name_value "${name1}" ""
  fi
done   # end of loop over command line arguments or files in current directory
IFS=$SAVEIFS
