#!/bin/sh
# shellcheck disable=SC2154
# sysd2v v0.3  --  systemd unit to sysvinit script converter
# Copyright (C) 2019  Trek http://www.trek.eu.org/devel/sysd2v
# distributed under the terms of the GNU General Public License 3


nl="
"

# read a systemd unit file and set variables named ini_{section}_{key}
# usage: read_unit filename instance
# filename	service unit configuration file, '-' to read from stdin
# instance	instance name for template units
read_unit()
{
  filename=$1
  instance=$2

  if [ "$filename" != - ]
  then
    inifile_unit_name=${filename##*/}
    inifile_unit_name=${inifile_unit_name%.*}
  fi

  rm_comm='/^[#;]/d'
  concat=':l; /\\$/ { N; s/[[:space:]]*\\\n/ /; tl }'
  subst_inst="s/%[Ii]/$instance/g"
  unit=$(
    cat "$filename" |
    sed "$rm_comm" |
    sed "$concat;$subst_inst"
  )
  section_list=$(
    printf %s "$unit" |
    sed -n 's/^\[\([[:alnum:]]\+\)\].*/\1/p'
  )
  oldifs=$IFS
  IFS=$nl

  for section in $section_list
  do
    get_sect='/^\['"$section"'\]/,/^\[.\+\]/'
    key_list=$(
      printf %s "$unit" |
      sed -n "$get_sect"'s/^\([[:alnum:]]\+\)[[:space:]]*=.*/\1/p' |
      sort -u
    )

    for key in $key_list
    do
      val=$(
        printf %s "$unit" |
        sed -n "$get_sect"'s/^'"$key"'[[:space:]]*=[[:space:]]*\(.*\)/\1/p'
      )
      var=$(
        echo "${section}_$key" |
        tr '[:upper:]' '[:lower:]'
      )
      eval "ini_$var=\$val"
      [ -n "$debug" ] && echo "ini_$var=$val" >&2
    done
  done

  IFS=$oldifs
}


# normalize a repeated command-list value from read_unit()
# usage: normalize_exec_list value section key
normalize_exec_list ()
{
  lst=
  section=$2
  key=$3

  while IFS= read -r cmd || [ -n "$cmd" ]
  do
    case $cmd in
      '')
        lst=
        ;;
      *)
        lst=${lst:+$lst$nl}$cmd
        ;;
    esac
  done <<EOF
$1
EOF

  # Command substitution strips trailing newlines, so a final empty
  # assignment has to be recovered from the parsed unit text.
  in_section=
  saw_key=
  last_cmd=
  while IFS= read -r cmd || [ -n "$cmd" ]
  do
    case $cmd in
      "[$section]")
        in_section=1
        continue
        ;;
      '['*)
        [ -n "$in_section" ] && break
        continue
        ;;
    esac

    [ -n "$in_section" ] || continue

    case $cmd in
      "$key"=*|"$key"[[:space:]]*=*)
        saw_key=1
        last_cmd=$(printf '%s\n' "$cmd" |
          sed -n 's/^'"$key"'[[:space:]]*=[[:space:]]*\(.*\)/\1/p')
        ;;
    esac
  done <<EOF
$unit
EOF

  [ -n "$saw_key" ] && [ -z "$last_cmd" ] && lst=

  printf %s "$lst"
}


# read a systemd configuration value and write its prefix to stdout
# usage: get_prefix val
# val		systemd configuration value
get_prefix () { printf %s "$1" | sed -n 's/^\([-@:+!|]*\).*/\1/p'; }


# read a boolean value and returns true or false
# usage: is_true val
# val		boolean value
is_true () { case "$1" in 1|[Oo][Nn]|[Tt]*|[Yy]*) true;; *) false; esac }


# read systemd services list and write LSB facilities to stdout
# usage: get_provides services
# services	list of service units
get_provides ()
{
  lst=
  for dep in $1
  do
    lst=${lst:+$lst }${dep%.service}
  done
  printf %s "$lst"
}


# read systemd units list and write LSB facilities to stdout
# usage: get_depends dependencies [ignores]
# dependencies	list of required units
# ignores	facilities to ignore
get_depends ()
{
  lst=
  for dep in $1
  do
    d=
    case $dep in
      local-fs-pre.target) d=mountkernfs;;
      time-sync.target) d=\$time;;
      systemd-modules-load.service) d=kmod;;
      local-fs.target|network-pre.target) d=\$local_fs;;
      systemd-sysctl.service) d=procps;;
      network.target|network-online.target|systemd-networkd.service)
        d=\$network;;
      nss-lookup.target) d=\$named;;
      rpcbind.target|remote-fs-pre.target) d=\$portmap;;
      remote-fs.target|sysinit.target|basic.target) d=\$remote_fs;;
      syslog.service) d=\$syslog;;
      boot-complete.target|multi-user.target|default.target) d=\$all;;
      *.service) d=${dep%.service};;
      *) echo "WARNING: unsupported target '$dep'" >&2
    esac

    ign=${2:+$2 }$lst
    { [ -z "$ign" ] || [ -n "${ign%%*"$d"*}" ]; } &&
      lst=${lst:+$lst }$d
  done

  printf %s "$lst"
}


# read LSB facilities list and write runlevel to stdout
# usage: get_runlevel facilities
# facilities	list of required facilities
get_runlevel ()
{
  case $1 in
    *\$remote_fs*) echo 2 3 4 5;;
    *) echo S
  esac
}


# write a list of environment files to be executed
# usage: write_env list
# list		files separated by newlines, with prefix (-)
write_env ()
{
  oldifs=$IFS
  IFS=$nl

  for env in $1
  do
    pre=$(get_prefix "$env")
    noerr=

    [ -n "$pre" ] && [ -z "${pre%%*-*}" ] &&
      noerr="[ -r ${env#"$pre"} ] && "

    printf '%s\n' "$noerr. ${env#"$pre"}"
  done

  IFS=$oldifs
}


# write an environment variable containing paths
# usage: write_path name prefix list
# name		name of the environment variable
# prefix	path prefix to append directories
# list		paths separated by spaces or newlines
write_path ()
{
  lst=
  for dir in $3
  do
    lst=${lst:+$lst:}$2/$dir
  done

  [ -z "$3" ] || printf '%s=%s\n' "$1" "$lst"
}


# write a list of directories to be created
# usage: write_install prefix list [user [group [mode]]]
# prefix	path prefix to append directories
# list		paths separated by spaces or newlines
# user		user ownership
# group		group ownership
# mode		permission mode
write_install ()
{
  for dir in $2
  do
    printf '  install -d %s%s/%s || return 2\n' \
      "${3:+-o $3 }${4:+-g $4 }${5:+-m $5 }" "$1" "$dir"
  done
}


# write a list of commands applying systemd executable prefixes
# usage: write_commands list [run [runpriv]]
# list		commands separated by newlines, with prefixes (-@:+!)
# run		command line to run each command (nice, chrt, ...)
# runpriv	command line to set privileges (runuser, ...)
write_commands ()
{
  oldifs=$IFS
  IFS=$nl

  for cmd in $1
  do
    pre=$(get_prefix "$cmd")
    beg=$3
    end=' || return 2'

    if [ -n "$pre" ]
    then
      [ -z "${pre%%*-*}" ] && end=
      [ -z "${pre%%*[+!]*}" ] && beg=
      [ -z "${pre%%*[@:]*}" ] &&
        echo "WARNING: unsupported exec prefix '$pre'" >&2
    fi

    printf '  %s\n' "$2$beg${cmd#"$pre"}$end"
  done

  IFS=$oldifs
}


# write a list of ExecCondition commands
# usage: write_conditions list ok [run [runpriv]]
# list		commands separated by newlines, with prefixes (-@:+!)
# ok		case pattern of exit statuses that should continue startup
# run		command line to run each command (nice, chrt, ...)
# runpriv	command line to set privileges (runuser, ...)
write_conditions ()
{
  oldifs=$IFS
  IFS=$nl
  ok=${2:-0}

  for cmd in $1
  do
    pre=$(get_prefix "$cmd")
    beg=$4

    if [ -n "$pre" ]
    then
      [ -z "${pre%%*[+!]*}" ] && beg=
      [ -z "${pre%%*[@:]*}" ] &&
        echo "WARNING: unsupported exec prefix '$pre'" >&2
    fi

    printf '  %s\n' "$3$beg${cmd#"$pre"}"
    { [ -z "$pre" ] || [ -n "${pre%%*-*}" ]; } || continue

    cat <<EOF
  __exec_condition_rc=\$?
  case \$__exec_condition_rc in
    $ok)
      ;;
    126|127|255)
      return 2
      ;;
    *)
      command -V log_progress_msg >/dev/null 2>&1 &&
        log_progress_msg "condition not met, not starting"
      return 0
      ;;
  esac
EOF
  done

  IFS=$oldifs
}


# read a list of commands separated by newlines and write an override function
# usage: write_function name [commands]
# name		function name (start_cmd, stop_cmd, ...)
# commands	list of commands, read from stdin if omitted
write_function ()
{
  lst=${2-$(cat)}

  [ -n "$lst" ] || return
  [ "$lst" = : ] && printf 'do_%s_override () :\n\n' "$1" && return

  end='  true\n'
  { [ -z "${lst%%*|| return [0-9]}" ] ||
    [ -z "${lst%%*|| return \$?}" ]; } && end=
  printf "do_${1}_override ()\n{\n%s\n$end}\n\n" "$lst"
}


# write an init-d-script file starting from the ini_* vars (see read_unit)
# usage: write_init servicename instance
# servicename	name of the service provided
# instance	instance name for template units
write_init ()
{
  name=$1
  instance=$2

  norm_execstart=$(normalize_exec_list "$ini_service_execstart" Service ExecStart)
  norm_execcondition=$(normalize_exec_list "$ini_service_execcondition" Service ExecCondition)
  norm_execstartpre=$(normalize_exec_list "$ini_service_execstartpre" Service ExecStartPre)
  norm_execstartpost=$(normalize_exec_list "$ini_service_execstartpost" Service ExecStartPost)
  norm_execstop=$(normalize_exec_list "$ini_service_execstop" Service ExecStop)
  norm_execstoppost=$(normalize_exec_list "$ini_service_execstoppost" Service ExecStopPost)
  norm_execreload=$(normalize_exec_list "$ini_service_execreload" Service ExecReload)

  if [ "${name%@}" != "$name" ]
  then
    name=$name$instance
  fi

  daemon_pre=$(get_prefix "$norm_execstart")
  daemon=${norm_execstart#"$daemon_pre"}

  if [ "${daemon%%[[:space:]]*}" != "$daemon" ]
  then
    daemon_args=${daemon#*[[:space:]]}
    daemon=${daemon%%[[:space:]]*}
  fi

  pidfile=$ini_service_pidfile

  if [ -n "$ini_service_user" ]
  then
    start_args="--user $ini_service_user"
    [ -n "$daemon_pre" ] && [ -z "${daemon_pre%%*[+!]*}" ] ||
      start_args="$start_args --chuid $ini_service_user"
    stop_args="--user $ini_service_user"
    runprivstart="runuser -u $ini_service_user -- "
    is_true "$ini_service_permissionsstartonly" || runpriv=$runprivstart
  fi

  cls=$ini_service_ioschedulingclass
  pri=$ini_service_ioschedulingpriority
  [ -n "$cls$pri" ] &&
    start_args="$start_args --iosched ${cls:-best-effort}${pri:+:$pri}" &&
    run="ionice ${cls:+-c $cls }${pri:+-n $pri }"

  pol=$ini_service_cpuschedulingpolicy
  pri=$ini_service_cpuschedulingpriority
  [ -n "$pol$pri" ] &&
    start_args="$start_args --procsched ${pol:-other}${pri:+:$pri}" &&
    run="${run}chrt ${pol:+--$pol }${pri:-0} "

  [ -n "$ini_service_nice" ] &&
    start_args="$start_args --nicelevel $ini_service_nice" &&
    run="${run}nice -n $ini_service_nice "

  pre=$(get_prefix "$ini_service_workingdirectory")
  workdir=${ini_service_workingdirectory#"$pre"}
  [ "$workdir" = '~' ] && workdir=\~$ini_service_user
  [ -n "$workdir" ] &&
    start_args="$start_args --chdir $workdir" &&
    chdir="${pre}cd $workdir"

  if [ -z "${service_type:=$ini_service_type}" ]
  then
    if [ -n "$ini_service_busname" ]
    then
      service_type=dbus
    elif [ -n "$norm_execstart" ]
    then
      service_type=simple
    else
      service_type=oneshot
    fi
  fi

  if [ "$service_type" != forking ]
  then
    start_args="$start_args --background"
    [ -z "$pidfile" ] && [ "$ini_service_killmode" != none ] &&
      start_args="$start_args --make-pidfile" &&
      pidfile="/var/run/$name-sysd2v.pid"
  fi

  dbus_wait=
  if [ "$service_type" = notify ]
  then
    start_args="$start_args --notify-await"
    timeout=${ini_service_timeoutstartsec:-$ini_service_timeoutsec}
    timeout=${timeout%s}
    [ -n "${timeout#60}" ] &&
      start_args="$start_args --notify-timeout $timeout"
    [ -n "$timeout" ] && [ -z "${timeout%%*[^0-9]*}" ] &&
      echo "WARNING: unsupported timeout '$timeout'" >&2
  elif [ "$service_type" = dbus ]
  then
    timeout=${ini_service_timeoutstartsec:-$ini_service_timeoutsec}
    timeout=${timeout%s}

    case $timeout in
      '') timeout=60;;
      *[!0-9]*)
        echo "WARNING: unsupported timeout '$timeout'" >&2
        timeout=60
        ;;
    esac

    if [ -z "$ini_service_busname" ]
    then
      echo "WARNING: Type=dbus requires BusName" >&2
    else
      dbus_wait=$(printf '%s%s' "$nl" "if command -v dbus-send >/dev/null 2>&1; then \
__dbus_wait_i=0; \
while [ \$__dbus_wait_i -lt $timeout ]; do \
dbus-send --system --dest=org.freedesktop.DBus --type=method_call \
--print-reply /org/freedesktop/DBus org.freedesktop.DBus.NameHasOwner \
string:\"$ini_service_busname\" 2>/dev/null | grep -q 'boolean true' && break; \
__dbus_wait_i=\$((\$__dbus_wait_i + 1)); sleep 1; done; \
[ \$__dbus_wait_i -lt $timeout ]; \
else \
echo \"WARNING: dbus-send not found, skipping BusName check for $ini_service_busname\" >&2; \
fi")
    fi
  fi

  signal=${ini_service_killsignal#SIG}
  timeout=${ini_service_timeoutstopsec:-$ini_service_timeoutsec}
  timeout=${timeout%s}
  { [ -n "${signal#TERM}" ] || [ -n "${timeout#90}" ]; } &&
    stop_args="$stop_args --retry=${signal:-TERM}/${timeout:-90}/KILL/5"

  limitnofile=$ini_service_limitnofile
  [ "$limitnofile" = infinity ] && limitnofile=unlimited

  need_install=$ini_service_runtimedirectory
  need_install=$need_install$ini_service_statedirectory
  need_install=$need_install$ini_service_cachedirectory
  need_install=$need_install$ini_service_logsdirectory
  need_install=$need_install$ini_service_configurationdirectory

  need_do_start=$norm_execcondition
  need_do_start=$need_do_start$norm_execstartpre$norm_execstartpost
  need_do_start=$need_do_start$need_install

  condition_ok=0
  if [ -n "$norm_execcondition" ]
  then
    # POSIX sh cannot distinguish a signal-terminated child from an
    # explicit 128+ exit, so only the documented shell error codes are
    # treated as hard failures here.
    while IFS= read -r success_list || [ -n "$success_list" ]
    do
      case $success_list in
        *[![:space:]]*) ;;
        *)
          condition_ok=0
          continue
          ;;
      esac

      for status in $success_list
      do
        case $status in
          ''|0|255)
            ;;
          *[!0-9]*)
            echo "WARNING: unsupported SuccessExitStatus '$status'" >&2
            ;;
          *)
            condition_ok=$condition_ok\|$status
            ;;
        esac
      done
    done <<EOF
$ini_service_successexitstatus
EOF
  fi

  execstop=$norm_execstop
  pretest=

  if [ "$service_type" != oneshot ]
  then
    [ "$pidfile" = "/var/run/${daemon##*/}.pid" ] && unset pidfile
    [ "$name" = "${daemon##*/}" ] && unset name

    [ -n "$daemon_args" ] && [ -z "${daemon_args%%*[\"\\]*}" ] &&
      echo "WARNING: DAEMON_ARGS needs to be escaped" >&2
    errcheck=' || return $?'

    if [ -n "$daemon_pre" ]
    then
      [ -z "${daemon_pre%%*-*}" ] && errcheck=
      [ -z "${daemon_pre%%*[@:]*}" ] &&
        echo "WARNING: unsupported exec prefix '$daemon_pre'" >&2
    fi

    [ -n "$need_do_start" ] &&
      pretest='-+do_status >/dev/null 2>&1 && return 1'
    { [ -n "$need_do_start" ] || [ -z "$errcheck" ]; } &&
      execstart="-+do_start_cmd$errcheck"
    [ -n "$dbus_wait" ] && [ -z "$execstart" ] &&
      execstart="-+do_start_cmd$errcheck"

    errcheck=' || return $?'
    [ -n "$execstop" ] && errcheck=
    [ -n "$execstop$norm_execstoppost" ] &&
      [ "$ini_service_killmode" != none ] &&
      killstop="-do_stop_cmd$errcheck"

    [ -n "$timeout" ] && [ -z "${timeout%%*[^0-9]*}" ] &&
      echo "WARNING: unsupported timeout '$timeout'" >&2
  else
    daemon=none
    pidfile=none
    : "${name:=SERVICE_NAME}"
    unset daemon_args start_args stop_args
    execstart=$norm_execstart
    runstart=$run
  fi

  need_do_start=$need_do_start$execstart$dbus_wait
  start_args=${start_args# }
  stop_args=${stop_args# }

  aliases=$(get_provides "$ini_install_alias")

  [ -z "$ini_unit_defaultdependencies" ] ||
    is_true "$ini_unit_defaultdependencies" &&
    defdep=sysinit.target

  req_start=$(get_depends "$ini_unit_requires $defdep")
  should_start=$(get_depends "$ini_unit_wants $ini_unit_after" "$req_start")

  default_start=$(get_runlevel "$req_start $should_start")
  [ "$default_start" = S ] && default_stop='0 6' || default_stop='0 1 6'
  [ -z "$execstop$norm_execstoppost" ] &&
    { [ "$service_type" = oneshot ] || [ "$ini_service_killmode" = none ]; } &&
    default_stop=

  [ "$default_start" = S ] && ignore=\$remote_fs
  start_before=$(get_depends "$ini_unit_requiredby $ini_install_wantedby
    $ini_unit_before" "$req_start $should_start \$all $ignore")

  cat <<EOF
#!/bin/sh
# Generated by sysd2v v0.3  --  http://www.trek.eu.org/devel/sysd2v
# kFreeBSD do not accept scripts as interpreters, using #!/bin/sh and sourcing.
if [ true != "\$INIT_D_SCRIPT_SOURCED" ] ; then
    set "\$0" "\$@"; INIT_D_SCRIPT_SOURCED=true . /lib/init/init-d-script
fi
### BEGIN INIT INFO
# Provides:       ${name:-${daemon##*/}}${aliases:+ $aliases}
# Required-Start: $req_start
# Required-Stop:  ${default_stop:+$req_start}
${should_start:+# Should-Start:   $should_start
${default_stop:+# Should-Stop:    $should_start
}}${start_before:+# X-Start-Before: $start_before
${default_stop:+# X-Stop-After:   $start_before
}}# Default-Start:  $default_start
# Default-Stop:   $default_stop
# Description:    ${ini_unit_description:-SERVICE_DESCRIPTION}
### END INIT INFO
EOF

  if [ -n "$ini_service_environment$ini_service_environmentfile$need_install" ]
  then
    echo set -a
    write_path RUNTIME_DIRECTORY /run "$ini_service_runtimedirectory"
    write_path STATE_DIRECTORY /var/lib "$ini_service_statedirectory"
    write_path CACHE_DIRECTORY /var/cache "$ini_service_cachedirectory"
    write_path LOGS_DIRECTORY /var/log "$ini_service_logsdirectory"
    write_path CONFIGURATION_DIRECTORY /etc \
      "$ini_service_configurationdirectory"
    printf '%s' "${ini_service_environment:+$ini_service_environment$nl}"
    write_env "$ini_service_environmentfile"
    printf 'set +a\n\n'
  fi

  cat <<EOF
${name:+DESC=\"$name\"
}DAEMON=$daemon
${daemon_args:+DAEMON_ARGS=\"$daemon_args\"
}${pidfile:+PIDFILE=$pidfile
}${start_args:+START_ARGS=\"$start_args\"
}${stop_args:+STOP_ARGS=\"$stop_args\"
}${limitnofile:+ulimit -n $limitnofile
}${ini_service_umask:+umask $ini_service_umask
}
EOF

  if [ -n "$need_do_start" ]
  then
    {
      write_commands "$pretest"
      write_install /run "$ini_service_runtimedirectory" \
        "$ini_service_user" "$ini_service_group" \
        "$ini_service_runtimedirectorymode"
      write_install /var/lib "$ini_service_statedirectory" \
        "$ini_service_user" "$ini_service_group" \
        "$ini_service_statedirectorymode"
      write_install /var/cache "$ini_service_cachedirectory" \
        "$ini_service_user" "$ini_service_group" \
        "$ini_service_cachedirectorymode"
      write_install /var/log "$ini_service_logsdirectory" \
        "$ini_service_user" "$ini_service_group" \
        "$ini_service_logsdirectorymode"
      write_install /etc "$ini_service_configurationdirectory" '' '' \
        "$ini_service_configurationdirectorymode"
      write_commands "$chdir"
      write_conditions "$norm_execcondition" "$condition_ok" \
        "$run" "$runpriv"
      write_commands "$norm_execstartpre" "$run" "$runpriv"
      write_commands "$execstart" "$runstart" "$runprivstart"
      write_commands "$dbus_wait"
      write_commands "$norm_execstartpost" "$run" "$runpriv"
    } | write_function start_cmd
  else
    [ "$service_type" = oneshot ] && write_function start :
  fi

  if [ -n "$execstop$norm_execstoppost" ]
  then
    {
      write_commands "$chdir"
      write_commands "$execstop" "$run" "$runpriv"
      write_commands "$killstop"
      write_commands "$norm_execstoppost" "$run" "$runpriv"
    } | write_function stop_cmd
  else
    { [ "$service_type" = oneshot ] || [ "$ini_service_killmode" = none ]; } &&
      write_function stop :
  fi

  reload_sigusr1="/bin/kill -HUP \$MAINPID"
  if [ "$norm_execreload" = "$reload_sigusr1" ] &&
     [ -z "$run$runpriv" ]
  then
    printf 'alias do_reload=do_reload_sigusr1\n\n'
  elif [ -n "$norm_execreload" ]
  then
    {
      write_commands "$chdir"
      write_commands "$norm_execreload" "$run" "$runpriv"
    } | write_function reload_cmd

    cat <<"EOF"
do_reload ()
{
  log_daemon_msg "Reloading $DESC configuration files" "$NAME"
  MAINPID=$(cat $PIDFILE)
  do_reload_cmd_override
  log_end_msg $?
}
EOF
  fi

  [ "$service_type" = oneshot ] && write_function status :
}


# parse command line
while getopts di:n: opt
do
    case $opt in
      d) debug=1;;
      i) instance=$OPTARG;;
      n) name=$OPTARG;;
      ?) printf "Usage: %s [-d] [-i instance] [-n servicename] [filename]\n" \
           "$0"
         exit 2;;
    esac
done

: "${instance:=INSTANCE_NAME}"
shift $((OPTIND - 1))


# convert unit file
read_unit "${1:--}" "$instance"
write_init "${name-$inifile_unit_name}" "$instance"
