@echo off
SetLocal EnableExtensions EnableDelayedExpansion

:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Copyright (c) 2025 Alexey Kuryakin daqgroup@mail.ru
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Script to roll files in the way that logrotate rolls   :
:: log files. Keeps up to the specified amount of files.  :
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Based on roll.sh from site: :::::::::::::::::::::::::::::::::::::
:: https://gist.github.com/mzpqnxow/88761eaaecc393ea3e75ab8040465eca
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

set /a fatal_notify_uses=1       & :: uses notify-send for fatal?
set /a fatal_notify_colorized=1  & :: fatal message is colorized?

set /a opt_verb=0                & :: option --verb
set /a opt_touch=0               & :: option --touch
set /a defnmaxkeep=9             & :: default value for nmaxkeep

set "scriptbase=%~n0"            & :: basename of script
set "scriptname=%~nx0"           & :: filename of script

goto :main

:note
if "%opt_verb%" == "1" call :echo %*
goto :EOF

:print
<nul set /p "_=%~1"
goto :EOF

:echo
:println
<nul set /p "_=%~1"
echo.
goto :EOF

:print_stderr
<nul 1>&2 set /p "_=%~1"
goto :EOF

:echo_to_stderr
:println_stderr
1>&2 <nul set /p "_=%~1"
1>&2 echo.
goto :EOF

:fatal
call :echo_to_stderr "%~2"
call :fatal_tooltip  "%~2"
exit %~1
goto :EOF

:fatal_tooltip
if "%fatal_notify_uses%" == "0" goto :EOF
for /f "tokens=* delims=" %%i in ('unix getdatetime "+%%Y.%%m.%%d-%%H:%%M:%%S"') do set datetime=%%i
unix tooltip-notifier guid logroll preset stdError delay 3600000 text "!datetime!: !scriptbase! %~1"
goto :EOF

:print_version
call :echo "%scriptname% version 1.0"
goto :EOF

:print_copyright
call :echo "Copyright (c) 2025 Alexey Kuryakin daqgroup@mail.ru"
goto :EOF

:print_help
 call :print_version
 call :print_copyright
 call :echo "Utility to roll (rotate) logfile(s) (like logrotate do it)."
 call :echo "Usage:"
 call :echo "------"
 call :echo " %scriptname% [-options] [basefile [nmaxkeep [compress]]]"
 call :echo ""
 call :echo "Description:"
 call :echo "------------"
 call :echo " Roll a file (basefile) up to a maximum keep count (nmaxkeep)"
 call :echo " in the way that logrotate rolls and keeps log files."
 call :echo "Options:"
 call :echo "--------"
 call :echo " --version   - print version"
 call :echo " -h,--help   - print help screen"
 call :echo " -v,--verb   - verbose execution with delails and notes"
 call :echo " -m,--mute   - mute (silent) execution, no tootips on error"
 call :echo " -t,--touch  - touch basefile, i.e. create zero file after roll"
 call :echo "Arguments:"
 call :echo "----------"
 call :echo " $1 basefile: The filename that should be rolled."
 call :echo " $2 nmaxkeep: Optional number of rolled files to keep. Default: 9."
 call :echo " $3 compress: Optional compressor name:gz,xz,bz,lz,no. Default: gz."
 call :echo "Notes:"
 call :echo "------"
 call :echo "This will not clean up files that are above the keep count. For example, if"
 call :echo "your keep count is 5 and you basefile.8 exists, it will not be cleaned up. It"
 call :echo "will properly clean up basefile.5."
goto :EOF

:log_roll
 call :note "Fetch args:"
 set "basefile=%~1"
 set "nmaxkeep=%~2"
 set "ext=%~3"
 call :note "Prepare local vars:"
 set "rolltemp=!basefile!.0"
 set "rolltarg=!basefile!.1"
 set /a nblogroll=0 & :: num succeed log roll  done
 set /a ncompress=0 & :: num succeed compress  done
 set /a nbremoved=0 & :: num files that was removed
 set /a nbtouched=0 & :: num files that was touched
 set /a nbsucceed=0 & :: num succeed operation done
 if "!nmaxkeep!" == "" set /a nmaxkeep=!defnmaxkeep!
 if "!ext!" == "" set "ext=gz"
 call :note "Prepare compressor:"
 set "compressor=" & set "copt="
 if /i "!ext!" == "gz" ( set "ext=gz"  & set "compressor=gzip"  & set "copt=-n" & goto :compresor_ok )
 if /i "!ext!" == "xz" ( set "ext=xz"  & set "compressor=xz"    & set "copt=  " & goto :compresor_ok )
 if /i "!ext!" == "bz" ( set "ext=bz2" & set "compressor=bzip2" & set "copt=  " & goto :compresor_ok )
 if /i "!ext!" == "lz" ( set "ext=lz"  & set "compressor=lzip"  & set "copt=  " & goto :compresor_ok )
 if /i "!ext!" == "no" ( set "ext="    & set "compressor="      & set "copt=  " & goto :compresor_ok )
 call :fatal 1 "Error: bad compressor name: !ext!"
:compresor_ok
 call :note "Start rolling:"
 if exist "!basefile!" (
  call :echo "Rolling file: !basefile!"
  call :note "Check compresor"
  if defined compressor (
   set /a exec_result=0
   call unix !compressor! --help 1>nul 2>nul || set /a exec_result=1
   if "!exec_result!" == "0" (
    call :echo "Compressor found: !compressor!"
   ) else (
    call :fatal 1 "Error: Compressor not found !compressor!"
   )
  )
  call :note "Remove temporary file:"
  if exist "!rolltemp!" (
   del /f /q "!rolltemp!" >nul && set /a nbremoved+=1
  )
  call :note "Try roll basefile to temporary:"
  move /y "!basefile!" "!rolltemp!" >nul
  if exist "!rolltemp!" (
   call :note "Touch to create zero file:"
   if "!opt_touch!" == "1" ( unix touch "!basefile!" && set /a nbtouched+=1 )
   call :note "Compress 1-st backup:"
   if exist "!rolltarg!" (
    if defined compressor (
     set /a exec_result=0
     call unix !compressor! --help 1>nul 2>nul || set /a exec_result=1
     if "!exec_result!" == "0" (
      if exist "!rolltarg!.!ext!" ( del /f /q "!rolltarg!.!ext!" >nul && set /a nbremoved+=1 )
      unix !compressor! !copt! "!rolltarg!" && set /a ncompress+=1
     ) else (
      call :fatal 1 "Error: Compressor not found !compressor!"
     )
    )
   )
   move /y "!rolltemp!" "!rolltarg!" >nul && (
    set /a nblogroll+=1
    set /a nbsucceed+=1
   )
   call :note "Roll compressed file:"
   for /l %%n in (1,1,!nmaxkeep!) do (
    set /a i=!nmaxkeep!-%%n & set /a i1=!i!+1
    set "nextfile=!basefile!.!i!.!ext!"
    set "nextkeep=!basefile!.!i1!.!ext!"
    if exist "!nextfile!" (
     @echo on
     if !i! GEQ !nmaxkeep! ( del /f /q "!nextfile!" >nul && set /a nbremoved+=1 )
     if !i! LSS !nmaxkeep! ( move /y "!nextfile!" "!nextkeep!" >nul && set /a nblogroll+=1 )
     @echo off
    )
   )
  ) else (
   call :fatal 1 "Error: file locked !basefile!"
  )
 ) else (
  call :fatal 1 "Error: file missed !basefile!"
 )
 call :note "Report result:"
 set "msg=Done !scriptbase!:"
 set "msg=!msg! !nblogroll! file(s) rolled,"
 set "msg=!msg! !ncompress! compressed,"
 set "msg=!msg! !nbtouched! touched,"
 set "msg=!msg! !nbremoved! removed."
 if !nbsucceed! gtr 0 (
  call :echo "!msg!"
  exit /b 0
 ) else (
  call :echo_to_stderr "!msg!"
  exit /b 1
 )
 echo ok
goto :EOF

:::::::
:: Main
:::::::
:main
 if "%~1" == "" call :fatal 1 "Error: empty argiments. Call %scriptname% -h for help."
:parse_options
 if /i "%~1" == "--version" ( call :print_version & exit /b 0 )
 if /i "%~1" == "-h"        ( call :print_help    & exit /b 0 )
 if /i "%~1" == "-help"     ( call :print_help    & exit /b 0 )
 if /i "%~1" == "--help"    ( call :print_help    & exit /b 0 )
 if /i "%~1" == "-v"        ( set /a opt_verb=1 & shift /1 & goto :parse_options )
 if /i "%~1" == "-verb"     ( set /a opt_verb=1 & shift /1 & goto :parse_options )
 if /i "%~1" == "--verb"    ( set /a opt_verb=1 & shift /1 & goto :parse_options )
 if /i "%~1" == "-verbose"  ( set /a opt_verb=1 & shift /1 & goto :parse_options )
 if /i "%~1" == "--verbose" ( set /a opt_verb=1 & shift /1 & goto :parse_options )
 if /i "%~1" == "-m"        ( set /a fatal_notify_uses=0 & shift /1 & goto :parse_options )
 if /i "%~1" == "-mute"     ( set /a fatal_notify_uses=0 & shift /1 & goto :parse_options )
 if /i "%~1" == "--mute"    ( set /a fatal_notify_uses=0 & shift /1 & goto :parse_options )
 if /i "%~1" == "-t"        ( set /a opt_touch=1         & shift /1 & goto :parse_options )
 if /i "%~1" == "-touch"    ( set /a opt_touch=1         & shift /1 & goto :parse_options )
 if /i "%~1" == "--touch"   ( set /a opt_touch=1         & shift /1 & goto :parse_options )
:start_roll
 call :log_roll %1 %2 %3
 exit /b %errorlevel%
goto :EOF

::::::::::::::
:: END OF FILE
::::::::::::::
