home *** CD-ROM | disk | FTP | other *** search
/ OS/2 Shareware BBS: 18 REXX / 18-REXX.zip / cronrgf.zip / cronrgf.cmd next >
OS/2 REXX Batch file  |  1994-02-04  |  39KB  |  1,104 lines

  1. /*
  2. program: cronrgf.cmd
  3. type:    REXXSAA-OS/2, OS/2 2.x
  4. purpose: Unix-like cron; allow to repeatedly execute commands at given date/time
  5.          defined in a control file
  6. version: 2.0
  7. date:    1992-06-10
  8. changed: 1993-03-19, bug fixed (caused no scheduling between 23:00 and 23:59),
  9.                            logging added (so you can check for yourself)
  10.          1993-11-05, added option to reread (/R) the cronfile at specified times (default every 60 minutes),
  11.                            suggested by fmerrow@nyx10.cs.du.edu (Frank Merrow);
  12.                      added option to execute all programs once since midnight (/M) and the time cronrgf was invoked,
  13.                            suggested by sktoni@uta.fi (Tommi Nieminen);
  14.                      added colorized output and option (/B) to use black and white
  15.                            instead of ANSI-colors on output
  16.          1993-11-22, added ability to look for cronfile in the directory in which cronrgf.cmd is stored,
  17.                            if no path was explicitly given; hence one can start cronrgf.cmd from
  18.                            any drive/directory without the need to specify a full path for the cronfile
  19.  
  20. author:  Rony G. Flatscher
  21.          RONY@AWIWUW11.BITNET
  22.          Rony.Flatscher@wu-wien.ac.at
  23.  
  24. needs:   DATERGF.CMD, SCRCOLOR.CMD, some RxFuncts (loaded automatically)
  25.  
  26. usage:   CRONRGF [/T] [/M] [/R[nn]] [/B] cronfile
  27.  
  28.  
  29.     Reads file 'cronfile' and executes command[s] repeatedly according to it.
  30.     'cronfile' has to be formatted like in Unix; unlike the Unix-version a '%' is
  31.     treated like any other character; empty lines and ones starting with
  32.     a semi-colon (;) or a (#) are ignored.
  33.  
  34.     Switch '/T[estmode]': the cronfile is read and the user is presented with the date/times
  35.           and commands to be executed upon them. The planned and truly scheduled
  36.           times are written to the log-file.
  37.  
  38.     Switch '/M[idnight]': all commands which were scheduled the same day, starting from
  39.           midnight to the time of first invocation are executed once.
  40.  
  41.     Switch '/R[nn]': check cronfile at least every 'nn' (default 60) minutes whether it changed; if so, reread
  42.           it immediately and set the new schedule-times. If set, the cronfile will be checked for changes after
  43.           executing commands
  44.  
  45.     Switch '/B[lackAndWhite]': do not colorize output strings (e.g. for usage in more, less etc.).
  46.           This switch merely suppresses the ANSI-color-sequences attached to the
  47.           strings.
  48.  
  49.     example:  CRONRGF /TEST testcron
  50.               execute statements in file 'testcron' in testmode
  51.  
  52.     example for a control-file:
  53.  
  54.         ; Sample file for CRONRGF.CMD
  55.         ;
  56.         ; This is a comment (starts with a semicolumn)
  57.         # This is a comment too (starts with the Unix-comment symbol)
  58.         ; empty lines are ignored too...
  59.  
  60.  
  61.         ; LAYOUT OF THE CRON-FILE:
  62.         ;    * * * * * command
  63.         ; or
  64.         ;    minute hour day month weekday command
  65.         ; where minute  ranges from 0-59,
  66.         ;       hour    ranges from 0-23,
  67.         ;       day     ranges from 1-31,
  68.         ;       month   ranges from 1-12,
  69.         ;       weekday ranges from 1-7 (1 = Monday, 2 = Tuesday, ..., 7 = Sunday)
  70.         ;
  71.         ; you can give a list of values, separated by a comma (,), e.g. "1,3,7"
  72.         ; you can give a range of values, separated by a dash (-), e.g. "1-5"
  73.         ; you can give a star (*) instead of a value, meaning entire range of all valid values
  74.         ;
  75.         ; the given command is only executed when all criteriae are fullfilled !
  76.         ;
  77.         ; restriction: unlike to Unix, the percent-sign (%) is treated like any other character and
  78.         ;              not as a new-line
  79.         ;
  80.  
  81.         # the following command "@ECHO HI, I am Nr. 1 to be echoed every minute" would be
  82.         # executed every minute
  83.         *  *  *  *  *  @ECHO Hi, I am Nr. 1 to be echoed every minute & pause
  84.  
  85.         59 23 31 12 5 command, one minute before year's end, and only if the last day is a Friday
  86.  
  87.         ; comment: every year at 17:45 on June 7th:
  88.         45 17  7  6  *  dir c:\*.exe
  89.  
  90.         ; comment: on every full quarter of an hour
  91.         ;          at midnight, 6 in the morning, noon, 6 in the evening
  92.         ;          on the 1st, 15th and 31st of
  93.         ;          every month on
  94.         0,15,30,45   0,6,12,18   1,15,31   *   *   backup c:\*.* d:\ /s
  95.  
  96.         ; at noon on every day, if it is a weekday (Mo-Fri):
  97.         0 12 * * 1-5 XCOPY Q:\* D:\ /s
  98.  
  99.         ; every minute in January, March, May, July, September and November:
  100.         *  *  *  1,3,5,7,9,11  *  dir c:\*.cmd
  101.  
  102.         # at the last day of the year at 23:01, 23:02, 23:03, 23:05, 23:20, 23:21,
  103.         # 23:22, 23:23, 23:24, 23:25, 23:30, 23:31, 23:32, 23:33, 23:34, 23:35,
  104.         # 23:59
  105.         1,2,3,5,20-25,30-35,59   23   31   12   *   COPY D:\*.log E:\backup
  106.  
  107.         ; make backups of OS2.INI and OS2SYS.INI on every first monday of a month,
  108.         ; at 9 o'clock in the morning
  109.         0 9 1-7 * 1 showini /bt d:\os2\os2.ini
  110.         0 9 1-7 * 1 showini /bt d:\os2\os2sys.ini
  111.  
  112.         ; at midnight on every month:
  113.         0 0 1 * * tapebackup /all
  114.  
  115.         ; execute every minute, no restrictions:
  116.         *  *  *  *  *  @ECHO Hi, I am Nr. 2 to be echoed every minute & pause
  117.  
  118.         # execute every minute in January, February, March only !
  119.         * *  *  1,2,3  *  any-command any-arguments
  120.  
  121.         # execute every day at midnight
  122.         0 0 * * * any-command any-arguments
  123.  
  124.         # execute every wednesday at midnigth !
  125.         0 0 * * 3 any-command any-arguments
  126.  
  127.         # this is a comment which concludes the sample file ===========================
  128.  
  129. All rights reserved, copyrighted 1992, no guarantee that it works without
  130. errors, etc. etc.
  131.  
  132. donated to the public domain granted that you are not charging anything (money
  133. etc.) for it and derivates based upon it, as you did not write it,
  134. etc. if that holds you may bundle it with commercial programs too
  135.  
  136. you may freely distribute this program, granted that no changes are made
  137. to it
  138.  
  139. Please, if you find an error, post me a message describing it, I will
  140. try to fix and rerelease it to the net.
  141.  
  142. */
  143. SIGNAL ON HALT
  144. SIGNAL ON ERROR
  145. SIGNAL ON FAILURE    NAME ERROR
  146. SIGNAL ON NOTREADY   NAME ERROR
  147. SIGNAL ON NOVALUE    NAME ERROR
  148. SIGNAL ON SYNTAX     NAME ERROR
  149.  
  150. global. = ""                    /* default for global */
  151. global.eTestmode = "0"          /* default: no testmode */
  152. global.eFirstInvocation = 0     /* don't execute commands since midnight up to invocation time */
  153. global.eCheckCronFile = 0       /* don't check whether cronfile was changed */
  154.  
  155. stemSchedule. = ""      /* default for empty array elements */
  156.  
  157. pos = POS("/B", TRANSLATE(ARG(1)))
  158.  
  159. IF pos > 0 THEN         /* no screen-colors */
  160. DO
  161.    arg_1 = SUBSTR(ARG(1), 1, pos-1)
  162.  
  163.    /* ignore characters up to next blank or slash */
  164.    DO i = pos + 1 TO LENGTH(ARG(1))
  165.       IF VERIFY(SUBSTR(ARG(1), i, 1), "/ ") = 0 THEN LEAVE
  166.    END
  167.  
  168.    arg_1 = arg_1 || SUBSTR(ARG(1), i)
  169. END
  170. ELSE
  171. DO
  172.    /* get screen-colors */
  173.    PARSE VALUE ScrColor() WITH global.eScrNorm    global.eScrInv,
  174.                                global.eTxtNorm    global.eTxtInf,
  175.                                global.eTxtHi      global.eTxtAla,
  176.                                global.eTxtNormInv global.eTxtInfInv,
  177.                                global.eTxtHiInv   global.eTxtAlaInv .
  178.    arg_1 = ARG(1)
  179. END
  180.  
  181. IF arg_1 = "" THEN SIGNAL usage
  182.  
  183. /* three arguments ? */
  184. PARSE VAR arg_1 "/"switch1 "/"switch2 "/"switch3 filein
  185.  
  186. IF filein = "" THEN
  187.    PARSE VAR arg_1 filein "/"switch1 "/"switch2 "/"switch3
  188.  
  189.  
  190. /* two arguments ? */
  191. IF filein = "" THEN
  192.    PARSE VAR arg_1 "/"switch1 "/"switch2 filein
  193.  
  194. IF filein = "" THEN
  195.    PARSE VAR arg_1 filein "/"switch1 "/"switch2
  196.  
  197.  
  198. /* one argument ? */
  199. IF filein = "" THEN
  200.    PARSE VAR arg_1 "/"switch1 filein
  201.  
  202. IF filein = "" THEN
  203.    PARSE VAR arg_1 filein "/"switch1
  204.  
  205.  
  206. /* no argument */
  207. IF filein = "" THEN
  208.    PARSE VAR arg_1 filein                 /* get filename */
  209.  
  210. IF filein = "" | filein = "?" THEN
  211.    SIGNAL usage
  212.  
  213. switches = TRANSLATE(LEFT(switch1, 1) || LEFT(switch2, 1) || LEFT(switch3, 1))
  214.  
  215. /* check whether switches are valid */
  216. IF VERIFY(switches, "TMR ") <> 0 THEN
  217. DO
  218.    CALL say_c global.eTxtAla || "CRONRGF: unknown switch in [" || global.eTxtHi || arg_1 || global.eTxtAla || "]."
  219.    CALL say_c
  220.    CALL BEEP 2500, 100
  221.    SIGNAL usage              /* wrong switch */
  222. END
  223.  
  224. switch_text = ""
  225.  
  226. IF POS("T", switches) > 0 THEN
  227. DO
  228.    global.eTestmode = "1"
  229.    switch_text = switch_text "/T"
  230. END
  231.  
  232. IF POS("M", switches) > 0 THEN
  233. DO
  234.    global.eFirstInvocation = 1
  235.    switch_text = switch_text "/M"
  236. END
  237.  
  238. IF POS("R", switches) > 0 THEN
  239. DO
  240.    pos = POS("R", switches)
  241.    SELECT
  242.       WHEN pos = 1 THEN tmp = switch1
  243.       WHEN pos = 2 THEN tmp = switch2
  244.       OTHERWISE tmp = switch3
  245.    END
  246.  
  247.    PARSE UPPER VAR tmp "R" minutes
  248.  
  249.    IF minutes = "" THEN minutes = 60    /* default to 60 minutes */
  250.    ELSE IF \DATATYPE(minutes, "N") | minutes < 5 | minutes > 43200 THEN
  251.    DO
  252.       CALL say_c  global.eTxtAla || "CRONRGF: wrong minute-value in switch [/" || global.eTxtHi || tmp || global.eTxtAla || "]."
  253.       CALL say_c  global.eTxtAla || "         (valid range 5-43200 minutes = 5 minutes to 30 days)."
  254.       CALL say_c
  255.       CALL BEEP 2500, 100
  256.       SIGNAL usage              /* wrong switch */
  257.    END
  258.    switch_text = switch_text "/R" || minutes
  259.    global.eMinutesToSleep = minutes
  260.    global.eSecondsToSleep = minutes * 60
  261.    global.eCheckCronFile = 1
  262. END
  263.  
  264. /* construct name of LOG-file */
  265. PARSE SOURCE . . full_path_of_this_procedure
  266. global.eLogFile = SUBSTR(full_path_of_this_procedure, 1,,
  267.                          LASTPOS(".", full_path_of_this_procedure)) || "LOG"
  268.  
  269. filein = STRIP(filein)          /* get rid of leading and trailing spaces */
  270. global.eFilein = STREAM(filein, "C", "QUERY EXISTS")
  271.  
  272. IF global.eFilein = "" THEN
  273. DO
  274.    /* supply drive & path of cronrgf.cmd, if cronfile does not have any */
  275.    IF FILESPEC("Drive", filein) = "" & FILESPEC("Path", filein) = "" THEN
  276.    DO
  277.       PARSE SOURCE . . this
  278.       filein = FILESPEC("Drive", this) || FILESPEC("Path", this) || filein
  279.       global.eFilein = STREAM(filein, "C", "QUERY EXISTS")
  280.    END
  281.  
  282.    IF global.eFilein = "" THEN          /* still no cronfile ? */
  283.       CALL stop_it "cronfile [" || filein || "] does not exist."
  284. END
  285.  
  286. /* check whether RxFuncs are loaded, if not, load them */
  287. IF RxFuncQuery('SysLoadFuncs') THEN
  288. DO
  289.     /* load the load-function */
  290.     CALL RxFuncAdd 'SysLoadFuncs', 'RexxUtil', 'SysLoadFuncs'
  291.  
  292.     /* load the Sys* utilities */
  293.     CALL SysLoadFuncs
  294. END
  295.  
  296. CALL LINEOUT global.eLogFile, LEFT("", 80, "-")
  297. CALL LINEOUT global.eLogFile, "(Logging-session started:" date("S") time() || ")"
  298. CALL LINEOUT global.eLogFile, "  (Switches in effect) [" || STRIP(switch_text) || "]"
  299.  
  300. CALL read_cronfile     /* read given CRON-file */
  301.  
  302. IF global.0 > 0 THEN
  303.    CALL dispatch                                /* start dispatching */
  304.  
  305. CALL stop_it "Nothing to schedule. Program ended."
  306.  
  307.  
  308.  
  309.  
  310. /*
  311.         FOREVER-LOOP
  312. */
  313. DISPATCH: PROCEDURE EXPOSE global. stemSchedule.
  314.  
  315.    IF global.eCheckCronFile THEN
  316.       sleeping_time = global.eSecondsToSleep
  317.    ELSE                        /* take care of DosSleep()-unsigned long; to be safe just sleep a maximum */
  318.       sleeping_time = 2678400  /* of 31 days (= 31 * 24 * 60 * 60  == 2.678.400 seconds) at a time       */
  319.  
  320.  
  321.  
  322.    DO forever_loop = 1 TO 5
  323.       forever_loop = 1          /* don't let the forever-loop expire */
  324.       CALL say_c RIGHT("", 79, "=")
  325.       CALL say_c
  326.       CALL say_c "Scheduling commands given in cronfile:"      /* show user which file is being used */
  327.       CALL say_c
  328.  
  329.       CALL say_c "  [" || global.eTxtHi || global.eFilein || global.eTxtInf || "]"
  330.  
  331.       IF global.eCheckCronFile THEN
  332.          CALL say_c "  (being checked after executing commands, at least every"  global.eTxtHi || global.eMinutesToSleep  global.eTxtInf || "minutes)"
  333.  
  334.       IF global.eFirstInvocation THEN
  335.          CALL say_c "  (executing once all commands between midnight and now, switch:" global.eTxtHi ||  "/M" || global.eTxtInf || ")"
  336.  
  337.       CALL say_c
  338.  
  339.       CALL schedule_next
  340.       /* show user which command(s) will be executed when */
  341.       PARSE VAR stemSchedule.1 1 next_date_time 18      /* get next date/time */
  342.  
  343.       /* get actual DATE/TIME */
  344.       act_date_time = DATE("S") TIME()
  345.  
  346.       IF global.eTestmode THEN
  347.       DO
  348.          IF \global.eFirstInvocation THEN
  349.              act_date_time = next_date_time
  350.  
  351.          CALL say_c "command[s] being scheduled on:"  global.eTxtHi || act_date_time
  352.       END
  353.       ELSE
  354.          CALL say_c "command[s] being scheduled on:"  global.eTxtHi || next_date_time
  355.  
  356.       CALL say_c
  357.  
  358.       DO i = 1 TO stemSchedule.0
  359.          PARSE VAR stemSchedule.i 1 tmp_date_time 18 status index
  360.  
  361.          IF tmp_date_time > next_date_time THEN LEAVE
  362.  
  363.          IF status = "OK" THEN
  364.          DO
  365.             CALL say_c "  [" || global.eTxtHi || VALUE("global." || index || ".eCommand.eValues") || global.eTxtInf || "]"
  366.             IF global.eTestmode THEN           /* write info to LOG-File */
  367.             DO
  368.                CALL LINEOUT global.eLogFile, "  (Testmode)" act_date_time "(scheduled for" next_date_time ||,
  369.                                              ") [" || VALUE("global." || index || ".eCommand.eValues") || "]"
  370.             END
  371.          END
  372.       END
  373.       CALL say_c
  374.  
  375.       /* get actual DATE/TIME */
  376.       difference = DATERGF(next_date_time, "-S", act_date_time)
  377.  
  378.       IF difference > 0 THEN seconds_to_sleep = DATERGF(difference, "SEC") % 1
  379.                         ELSE seconds_to_sleep = 0
  380.  
  381.       IF global.eTestmode THEN
  382.       DO
  383.          IF \global.eFirstInvocation THEN
  384.             act_date_time = next_date_time
  385.  
  386.          CALL say_c RIGHT("", 79, "=")
  387.          CALL say_c "Testmode (dispatch):    next_invocation ="  global.eTxtHi || next_date_time
  388.          CALL say_c "Testmode (dispatch):   actual date/time ="  global.eTxtHi || act_date_time
  389.          IF difference < 0 THEN
  390.             CALL say_c "Testmode (dispatch):                     " global.eTxtHi || "Immediate !"
  391.          ELSE
  392.             CALL say_c "Testmode (dispatch): difference in days =" global.eTxtHi || difference,
  393.                        global.eTxtInf || "=" global.eTxtHi || seconds_to_sleep  global.eTxtInf || "seconds"
  394.  
  395.          CALL say_c RIGHT("", 79, "=")
  396.  
  397.          CALL say_c "Testmode (dispatch):"
  398.          CALL say_c "   input:"
  399.          DO i = 1 TO global.0
  400.             CALL say_c "     [" || global.eTxtHi || RIGHT(i, LENGTH(global.0)) || global.eTxtInf || "]" global.original.i
  401.          END
  402.          CALL say_c RIGHT("", 79, "=")
  403.  
  404.          CALL say_c "   schedule list:"
  405.          DO i = 1 TO stemSchedule.0
  406.             PARSE VAR stemSchedule.i . . . gIndex
  407.             CALL say_c "    " stemSchedule.i "[" || global.eTxtHi || VALUE("global." || gIndex || ".eCommand.eValues") || global.eTxtInf || "]"
  408.          END
  409.          CALL say_c RIGHT("", 79, "=")
  410.  
  411.          CALL say_c 'Press any key to continue, "q" to quit.'
  412.          CALL BEEP 500, 100
  413.          IF TRANSLATE(SysGetKey("NOECHO")) = "Q" THEN SIGNAL halt
  414.       END
  415.       ELSE
  416.       DO
  417.          DO WHILE seconds_to_sleep > 0
  418.             sleeping         = MIN(seconds_to_sleep, sleeping_time)
  419.             seconds_to_sleep = MAX(0, seconds_to_sleep - sleeping_time)
  420.  
  421.             CALL SysSleep sleeping                      /* sleep */
  422.  
  423.             /* update sleeping-time, get actual DATE/TIME */
  424.             difference = DATERGF(next_date_time, "-S", DATE("S") TIME())
  425.  
  426.             IF difference > 0 THEN seconds_to_sleep = DATERGF(difference, "SEC") % 1
  427.                               ELSE seconds_to_sleep = 0
  428.  
  429.             /* if cronfile should be checked and another sleeping-turn is coming up, then reread the cronfile */
  430.             IF global.eCheckCronFile & seconds_to_sleep > 0 THEN
  431.             DO
  432.                IF cronfile_changed() THEN
  433.                DO
  434.                   CALL do_the_cronfile_reread
  435.                   ITERATE forever_loop        /* reschedule */
  436.                END
  437.             END
  438.          END
  439.  
  440.          DO i = 1 TO stemSchedule.0
  441.            PARSE VAR stemSchedule.i 1 date_time 18 status index
  442.            IF date_time > act_date_time THEN LEAVE
  443.  
  444.            IF status = "OK" THEN
  445.            DO
  446.               /*
  447.                  start an own minimized session which closes automatically after the
  448.                  command was executed:
  449.               */
  450.               commandString = VALUE('global.' || index || '.eCommand.eValues')
  451.               title = '"CRONRGF:' date_time STRIP(commandString) '"'
  452.               ADDRESS CMD "@START" title '/C /WIN /MIN /B "' || commandString || '"'
  453.  
  454.               CALL LINEOUT global.eLogFile, "(dispatched)" date("S") time() "(scheduled for" date_time || ") [" || commandString || "]"
  455.            END
  456.          END
  457.       END
  458.  
  459.       global.eFirstInvocation = 0 /* programs from midnight up to invocation time were executed, stop that behavior */
  460.  
  461.       /* reread cronfile, if set and if cronfile changed or in testmode */
  462.       IF global.eCheckCronFile & (cronfile_changed() | global.eTestmode) THEN
  463.       DO
  464.          CALL do_the_cronfile_reread
  465.          ITERATE forever_loop        /* reschedule */
  466.       END
  467.  
  468.  
  469.       /* change the status of the executed programs to "NOK" */
  470.       DO i = 1 TO stemSchedule.0
  471.          PARSE VAR stemSchedule.i 1 date_time 18 status index
  472.  
  473.          IF date_time > act_date_time THEN LEAVE
  474.  
  475.          stemSchedule.i = act_date_time "NOK" index
  476.       END
  477.  
  478.    END
  479.  
  480.    RETURN
  481.  
  482.  
  483.  
  484. /*
  485.     calculate the schedule times, sort them in ascending order
  486. */
  487. SCHEDULE_NEXT: PROCEDURE EXPOSE global. stemSchedule.
  488.    /*
  489.       as long as no viable date/time to schedule was found, iterate
  490.    */
  491.    main_run = 0
  492.    DO WHILE WORD(stemSchedule.1, 3) <> "OK"
  493.       main_run = main_run + 1                           /* count loops until a valid day was found for any of the commands */
  494.  
  495.       IF global.eTestmode THEN
  496.          CALL say_c "Testmode (scheduling): main loop ="  global.eTxtHi || main_run
  497.  
  498.       IF main_run > 50 THEN
  499.       DO
  500.          CALL stop_it "SCHEDULE_NEXT(): aborting after 50 attempts to produce a valid date!"
  501.       END
  502.  
  503.       DO i = 1 TO stemSchedule.0
  504.          PARSE VAR stemSchedule.i 1 year 5 month 7 day 9 10 hour 12 13 minute 15 18 disp_status glob_index
  505.  
  506.          IF disp_status = "OK" THEN ITERATE             /* not yed scheduled */
  507.          old_year  = year
  508.          old_month = month
  509.          old_day   = day
  510.          old_hour  = hour
  511.  
  512.          /* defaults */
  513.          first_minute = WORD(global.glob_index.eMinute.eValues, 1)
  514.          first_hour   = WORD(global.glob_index.eHOur.eValues, 1)
  515.          first_day    = WORD(global.glob_index.eDay.eValues, 1)
  516.          first_month  = WORD(global.glob_index.eMonth.eValues, 1)
  517.  
  518.  
  519.          /* minute */
  520.          DO j = 1 TO global.glob_index.eMinute.0
  521.             tmp = WORD(global.glob_index.eMinute.eValues, j)
  522.             IF tmp > minute THEN LEAVE
  523.          END
  524.  
  525.          IF j > global.glob_index.eMinute.0 THEN        /* minutes to wrap around */
  526.             hour   = hour + 1
  527.          ELSE                                           /* minutes within same hour */
  528.             minute = tmp
  529.  
  530.          /* hour */
  531.          DO j = 1 TO global.glob_index.eHour.0
  532.             tmp = WORD(global.glob_index.eHour.eValues, j)
  533.             IF tmp >= hour THEN LEAVE
  534.          END
  535.          IF j > global.glob_index.eHour.0 THEN          /* hours to wrap around */
  536.             day    = day + 1
  537.          ELSE                                           /* hours within same day */
  538.             hour   = tmp
  539.  
  540.          ok = "NOK"                                     /* default: no date found yet */
  541.          run = 0
  542.          DO 50                                          /* try 50 times to produce a valid date */
  543.             run = run + 1
  544.             /* day */
  545.             DO j = 1 TO global.glob_index.eDay.0
  546.                tmp = WORD(global.glob_index.eDay.eValues, j)
  547.                IF tmp >= day THEN LEAVE
  548.             END
  549.  
  550.             IF j > global.glob_index.eDay.0 THEN        /* days to wrap around */
  551.             DO
  552.                day    = first_day
  553.                month  = month + 1
  554.             END
  555.             ELSE                                        /* days within same month */
  556.                day    = tmp
  557.  
  558.  
  559.             /* month */
  560.             DO j = 1 TO global.glob_index.eMonth.0
  561.                tmp = WORD(global.glob_index.eMonth.eValues, j)
  562.                IF tmp >= month THEN LEAVE
  563.             END
  564.  
  565.             IF j > global.glob_index.eMonth.0 THEN      /* months to wrap around */
  566.             DO
  567.                day    = first_day
  568.                month  = first_month
  569.                year   = year + 1
  570.             END
  571.             ELSE                                        /* months within same year */
  572.             DO
  573.                IF month <> tmp THEN                     /* did the month change ? */
  574.                   day = first_day
  575.  
  576.                month  = tmp
  577.             END
  578.  
  579.  
  580.             SELECT
  581.                WHEN old_year < year | old_month < month | old_day < day THEN
  582.                     next_invocation = year || month || day first_hour || ":" || first_minute || ":00"
  583.                WHEN old_hour < hour THEN
  584.                     next_invocation = year || month || day       hour || ":" || first_minute || ":00"
  585.                OTHERWISE
  586.                     next_invocation = year || month || day       hour || ":" ||       minute || ":00"
  587.             END
  588.  
  589.             IF global.eTestmode THEN
  590.                CALL say_c "Testmode (scheduling): next_invocation ="  global.eTxtHi || next_invocation DATERGF(next_invocation, "DN")
  591.  
  592.             /* check whether day-of-week is o.k. */
  593.             IF DATERGF(next_invocation, "M") = "" THEN  /* illegal date produced, e.g. 19950231 ? */
  594.             DO
  595.                day   = first_day
  596.                month = month + 1
  597.                IF month > 12 THEN
  598.                DO
  599.                  month = first_month
  600.                  year  = year + 1
  601.                END
  602.                ITERATE
  603.             END
  604.  
  605.  
  606.             next_Weekday = DATERGF(next_invocation, "DI")  /* get weekday */
  607.  
  608.             /* using POS because weekdays are in the form of 01, 02, ..., 07 */
  609.             IF POS(next_Weekday, global.glob_index.eWeekday.eValues) = 0 THEN      /* invalid weekday ? */
  610.             DO
  611.                next_invocation = DATERGF(next_invocation, "+", "1")     /* add one day to present date */
  612.                PARSE VAR next_invocation 1 year 5 month 7 day 9
  613.  
  614.                ITERATE
  615.             END
  616.  
  617.             ok = " OK"                                  /* o.k. to invoke, because valid date */
  618.             LEAVE
  619.          END
  620.  
  621.          IF global.eTestmode THEN
  622.          DO
  623.             CALL say_c "Testmode (scheduling): date/time-loop ="  global.eTxtHi || run  global.eTxtInf || "time[s]"
  624.             CALL say_c
  625.          END
  626.  
  627.  
  628.          /*
  629.             format in schedule list:
  630.             DATE TIME STATUS INDEX-INTO-GLOBAL-ARRAY
  631.          */
  632.          stemSchedule.i = next_invocation ok glob_index
  633.       END
  634.  
  635.       CALL sort_schedule_list
  636.    END
  637.  
  638.    RETURN
  639.  
  640. /*
  641.     sort the schedule list in ascending order
  642. */
  643. SORT_SCHEDULE_LIST: PROCEDURE EXPOSE global. stemSchedule.
  644.    length = 21          /* length of SUBSTR to compare, includes status */
  645.    /* define M for passes */
  646.    M = 1
  647.    DO WHILE (9 * M + 4) < stemSchedule.0
  648.       M = M * 3 + 1
  649.    END
  650.  
  651.    /* sort stem */
  652.    DO WHILE M > 0
  653.       K = stemSchedule.0 - M
  654.       DO J = 1 TO K
  655.          Q = J
  656.          DO WHILE Q > 0
  657.             L = Q + M
  658.             IF SUBSTR(stemSchedule.Q, 1, length) <= SUBSTR(stemSchedule.L, 1, length) THEN LEAVE
  659.             /* switch elements */
  660.             tmp            = stemSchedule.Q
  661.             stemSchedule.Q = stemSchedule.L
  662.             stemSchedule.L = tmp
  663.             Q = Q - M
  664.          END
  665.       END
  666.       M = M % 3
  667.    END
  668.  
  669.    RETURN
  670.  
  671.  
  672.  
  673.  
  674.  
  675. /*
  676.    analyze
  677.  
  678. */
  679.  
  680. CHECK_IT_OUT: PROCEDURE EXPOSE global. stemSchedule.
  681.  
  682. to_parse = ARG(1)
  683. line_no  = ARG(2)
  684.  
  685. PARSE VAR to_parse sMinute sHour sDay sMonth sWeekday sCommand
  686.  
  687. line_no = setup_minutes(sMinute, line_no)       /* setup minute-values */
  688.  
  689. IF line_no <> 0 THEN                            /* setup hour-values */
  690.    line_no = setup_hours(sHour, line_no)
  691.  
  692. IF line_no <> 0 THEN                            /* setup day-values */
  693.    line_no = setup_days(sDay, line_no)
  694.  
  695. IF line_no <> 0 THEN                            /* setup month-values */
  696.    line_no = setup_months(sMonth, line_no)
  697.  
  698. IF line_no <> 0 THEN                            /* setup weekday-values */
  699.    line_no = setup_weekdays(sWeekday, line_no)
  700.  
  701. IF line_no <> 0 THEN                            /* setup command-values */
  702. DO
  703.    global.line_no.eCommand.0       = 1
  704.    global.line_no.eCommand.eValues = sCommand
  705. END
  706.  
  707. RETURN line_no
  708.  
  709. /*
  710.         parse and setup minutes
  711.         ARG(1) - minute string
  712.         ARG(2) - index into global array
  713. */
  714. SETUP_MINUTES: PROCEDURE EXPOSE global. stemSchedule.
  715.    sMinute = ARG(1)
  716.    iIndex = ARG(2)
  717.    default.0 = 60
  718.    default.values = "00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19",
  719.                     "20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39",
  720.                     "40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59"
  721.  
  722.    string = parse_it("eMinute", sMinute, iIndex, 0, 59, default.0, default.values)
  723.  
  724.    IF string <> "" THEN
  725.    DO
  726.       CALL say_c  global.eTxtAla || "CRONRGF: error in minute-format" global.eTxtHi || "[" || string || "]"
  727.       CALL say_c
  728.       RETURN 0
  729.    END
  730.  
  731.    RETURN iIndex
  732.  
  733.  
  734.  
  735. /*
  736.         parse and setup hours
  737.         ARG(1) - hour string
  738.         ARG(2) - index into global array
  739. */
  740. SETUP_HOURS: PROCEDURE EXPOSE global. stemSchedule.
  741.    sHour = ARG(1)
  742.    iIndex = ARG(2)
  743.    default.0 = 24
  744.    default.values = "00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19",
  745.                     "20 21 22 23"
  746.  
  747.    string = parse_it("eHour", sHour, iIndex, 0, 23, default.0, default.values)
  748.  
  749.    IF string <> "" THEN
  750.    DO
  751.       CALL say_c  global.eTxtAla || "CRONRGF: error in hour-format" global.eTxtHi || "[" || string || "]"
  752.       CALL say_c
  753.       RETURN 0
  754.    END
  755.  
  756.    RETURN iIndex
  757.  
  758.  
  759.  
  760.  
  761. /*
  762.         parse and setup days
  763.         ARG(1) - day string
  764.         ARG(2) - index into global array
  765. */
  766. SETUP_DAYS: PROCEDURE EXPOSE global. stemSchedule.
  767.    sDay = ARG(1)
  768.    iIndex = ARG(2)
  769.    default.0 = 31
  770.    default.values = "   01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19",
  771.                     "20 21 22 23 24 25 26 27 28 29 30 31"
  772.  
  773.    string = parse_it("eDay", sDay, iIndex, 1, 31, default.0, default.values)
  774.  
  775.    IF string <> "" THEN
  776.    DO
  777.       CALL say_c  global.eTxtAla || "CRONRGF: error in day-format" global.eTxtHi || "[" || string || "]"
  778.       CALL say_c
  779.       RETURN 0
  780.    END
  781.  
  782.    RETURN iIndex
  783.  
  784.  
  785.  
  786.  
  787. /*
  788.         parse and setup months
  789.         ARG(1) - month string
  790.         ARG(2) - index into global array
  791. */
  792. SETUP_MONTHS: PROCEDURE EXPOSE global. stemSchedule.
  793.    sMonth = ARG(1)
  794.    iIndex = ARG(2)
  795.    default.0 = 12
  796.    default.values = "01 02 03 04 05 06 07 08 09 10 11 12"
  797.  
  798.    string = parse_it("eMonth", sMonth, iIndex, 1, 12, default.0, default.values)
  799.  
  800.    IF string <> "" THEN
  801.    DO
  802.       CALL say_c  global.eTxtAla || "CRONRGF: error in month-format" global.eTxtHi || "[" || string || "]"
  803.       CALL say_c
  804.       RETURN 0
  805.    END
  806.  
  807.    RETURN iIndex
  808.  
  809.  
  810.  
  811.  
  812. /*
  813.         parse and setup weekdays
  814.         ARG(1) - weekday string
  815.         ARG(2) - index into global array
  816. */
  817. SETUP_WEEKDAYS: PROCEDURE EXPOSE global. stemSchedule.
  818.    sWeekday = ARG(1)
  819.    iIndex = ARG(2)
  820.    default.0 = 7
  821.    default.values = "01 02 03 04 05 06 07"
  822.  
  823.    string = parse_it("eWeekday", sWeekday, iIndex, 1, 7, default.0, default.values)
  824.  
  825.    IF string <> "" THEN
  826.    DO
  827.       CALL say_c  global.eTxtAla || "CRONRGF: error in weekday-format" global.eTxtHi || "[" || string || "]"
  828.       CALL say_c
  829.       RETURN 0
  830.    END
  831.  
  832.    RETURN iIndex
  833.  
  834.  
  835.  
  836.  
  837.  
  838.  
  839.  
  840. /*
  841.         parse values, list, setup array
  842.         ARG(1) = eName ("element name" in array)
  843.         ARG(2) = string containing numbers
  844.         ARG(3) = index into global array
  845.         ARG(4) = lower bound (inclusive)
  846.         ARG(5) = upper bound (inclusive)
  847.         ARG(6) = default number of elements
  848.         ARG(7) = default values
  849. */
  850.  
  851. PARSE_IT: PROCEDURE EXPOSE global. stemSchedule.
  852.    eName       = ARG(1)
  853.    sValues        = ARG(2)
  854.    iIndex         = ARG(3)
  855.    lower          = ARG(4)
  856.    upper          = ARG(5)
  857.    default.0      = ARG(6)
  858.    default.values = ARG(7)
  859.  
  860.  
  861.    tmp = "global.iIndex." || eName || "."            /* build string of array-e-Name */
  862.    lastValue = 0
  863.  
  864.    IF sValues = "*" THEN                /* build all legal values */
  865.    DO
  866.       INTERPRET(tmp || "0 =" default.0)
  867.       INTERPRET(tmp || "eValues =" default.values)
  868.       RETURN ""
  869.    END
  870.  
  871.    INTERPRET(tmp || "0 = 0")                            /* set number of elements to 0 */
  872.    INTERPRET(tmp || 'eValues = ""')                     /* delete values */
  873.  
  874.    DO WHILE sValues <> ""
  875.       IF POS(",", sValues) > 0 THEN                     /* list of values ? */
  876.         PARSE VAR sValues tmpValue "," sValues
  877.       ELSE
  878.       DO
  879.         tmpValue = sValues
  880.         sValues = ""
  881.       END
  882.  
  883.       IF POS("-", tmpValue) > 0 THEN                    /* range of values ? */
  884.       DO
  885.          PARSE VAR tmpValue start "-" end
  886.       END
  887.       ELSE                                              /* single value */
  888.       DO
  889.          start = tmpValue
  890.          end   = tmpValue
  891.       END
  892.  
  893.  
  894.  
  895.       /* error in values ? */
  896.       IF start < lastValue | start < lower | start > end | ,
  897.          end > upper | ,
  898.          \DATATYPE(start, "N") | \DATATYPE(end, "N") THEN
  899.       DO
  900.          INTERPRET(tmp || '0  = ""')                    /* delete number of array elements */
  901.          INTERPRET(tmp || 'eValues = ""')               /* delete values */
  902.          SELECT
  903.             WHEN \DATATYPE(start, "N") THEN err_msg = '"' || start || '"' "is not numeric" '(part in error: "' || tmpValue || '")'
  904.             WHEN \DATATYPE(end,   "N") THEN err_msg = '"' || end || '"' "is not numeric" '(part in error: "' || tmpValue || '")'
  905.             WHEN start < lastValue     THEN err_msg = start "<" lastValue "= lower bound"  '(part in error: "' || tmpValue || '")'
  906.             WHEN start < lower         THEN err_msg = start "<" lower "= lower bound"      '(part in error: "' || tmpValue || '")'
  907.             WHEN start > end           THEN err_msg = start ">" end                        '(part in error: "' || tmpValue || '")'
  908.             WHEN end   > upper         THEN err_msg = end   ">" upper "= upper bound"      '(part in error: "' || tmpValue || '")'
  909.             OTHERWISE NOP
  910.          END
  911.  
  912.          RETURN err_msg
  913.       END
  914.  
  915.       /* build values */
  916.       DO i = start TO end
  917.          INTERPRET(tmp || "0 = " || tmp || "0 + 1")     /* increase counter for number of elements */
  918.          INTERPRET(tmp || "eValues = " || tmp || "eValues" RIGHT(i, 2, "0"))   /* add the next value */
  919.       END
  920.  
  921.       lastValue = i
  922.    END
  923.  
  924.    RETURN ""
  925.  
  926.  
  927.  
  928. /*
  929.    control-routine for rereading cronfile
  930. */
  931. DO_THE_CRONFILE_REREAD: PROCEDURE EXPOSE global. stemSchedule.
  932.    CALL say_c
  933.    CALL say_c "CRONRGF: cronfile [" || global.eTxtHi || global.eFilein || global.eTxtInf || "] changed, rereading ..."
  934.  
  935.    IF global.eTestmode THEN
  936.       tmp_leadin = "  (Testmode)"
  937.    ELSE
  938.       tmp_leadin = "            "
  939.  
  940.    CALL LINEOUT global.eLogFile, tmp_leadin date("S") time() "cronfile [" || global.eFilein || "] changed, rereading ..."
  941.  
  942.    CALL read_cronfile          /* read new cronfile */
  943.  
  944.    IF global.0 = 0 THEN        /* no valid entries found */
  945.       CALL stop_it "cronfile [" || global.eFilein || "] not a cronfile anymore ! Aborting ..."
  946.  
  947.    RETURN
  948.  
  949.  
  950.  
  951. /*
  952.    Read contents of CRON-file
  953. */
  954.  
  955. READ_CRONFILE: PROCEDURE EXPOSE global. stemSchedule.
  956.    DROP stemSchedule.
  957.    stemSchedule. = ""   /* default for empty array-elements */
  958.    iSchedule = 0        /* schedule counter */
  959.    line_no = 1          /* line-number */
  960.  
  961.    IF global.eFirstInvocation THEN      /* execute once from midnight to now */
  962.       date_time = DATERGF(DATE("S"), "-", DATERGF("1", "SECR")) /* Subtract 1 second from 00:00am */
  963.    ELSE
  964.       date_time = DATE("S") TIME()
  965.  
  966.    CALL SysFileTree global.eFilein, "aTmp", "F"
  967.    IF aTmp.0 <> 1 THEN
  968.    DO
  969.       CALL stop_it "control-file [" || global.eFilein || "] does not exist anymore !"
  970.    END
  971.    global.eFilein.eState = aTmp.1       /* assign present value of control-file */
  972.  
  973.    global.0 = 0
  974.    DROP stemSchedule.
  975.    stemSchedule. = ""
  976.  
  977.    DO WHILE LINES(global.eFilein) > 0
  978.       line = LINEIN(global.eFilein)
  979.  
  980.       /* no empty lines and no comments */
  981.       tmp = LEFT(STRIP(line), 1)
  982.       IF line = "" | tmp = ";" | tmp = "#" THEN ITERATE
  983.  
  984.       CALL say_c "parsing ["  global.eTxtInf || line || global.eTxtInf || "]"
  985.  
  986.       IF check_it_out(line, line_no) > 0 THEN
  987.       DO
  988.          iSchedule = iSchedule + 1
  989.          global.original.line_no = line
  990.          stemSchedule.iSchedule =  date_time "NOK" line_no  /* NOK = not o.k., get next date/time */
  991.          line_no = line_no + 1
  992.       END
  993.    END
  994.    CALL say_c
  995.  
  996.    CALL LINEOUT global.eLogFile,"  (cronfile)" date("S") time() "[" || STRIP(global.eFilein.eState) || "]"
  997.  
  998.    global.eOriginal.0 = line_no - 1     /* original number of lines */
  999.    global.0 = iSchedule                 /* set number of array elements */
  1000.    stemSchedule.0 = iSchedule           /* set number of array elements */
  1001.    CALL STREAM global.eFilein, "C", "CLOSE"     /* make sure, file is closed */
  1002.  
  1003.    RETURN
  1004.  
  1005.  
  1006.  
  1007. /*
  1008.    check, whether control-file changed
  1009. */
  1010. CRONFILE_CHANGED: PROCEDURE EXPOSE global. stemSchedule.
  1011.    CALL SysFileTree global.eFilein, "aTmp", "F"
  1012.    IF aTmp.0 <> 1 THEN
  1013.    DO
  1014.       CALL stop_it "cronfile [" || global.eFilein || "] does not exist anymore ! Aborting ..."
  1015.    END
  1016.  
  1017.    RETURN global.eFilein.eState <> aTmp.1
  1018.  
  1019.  
  1020.  
  1021. /*
  1022.    User pressed ctl-c or closed session
  1023. */
  1024. HALT:
  1025.    CALL STOP_IT "User interrupted program."
  1026.  
  1027. /*
  1028.    Clean up and close open files
  1029. */
  1030. STOP_IT:
  1031.    IF global.eLogFile <> "" THEN
  1032.    DO
  1033.       IF ARG(1) <> "" THEN
  1034.          CALL LINEOUT global.eLogFile, "  (***ERROR:" date("S") time() "-"  ARG(1) || ")"
  1035.  
  1036.       CALL LINEOUT global.eLogFile, "(Logging-session ended:" date("S") time() || ")"
  1037.       IF LEFT(STREAM(global.eLogFile), 1) = "R" THEN
  1038.          CALL STREAM global.eLogFile, "C", "CLOSE"    /* close log-file */
  1039.    END
  1040.  
  1041.    IF ARG(1) <> "" THEN CALL say_c  global.eTxtAla || "CRONRGF:"  global.eTxtAlaInv || ARG(1)
  1042.  
  1043.    IF global.eFilein <> "" THEN
  1044.       IF LEFT(STREAM(global.eFilein), 1) = "R" THEN
  1045.          tmp = STREAM(global.eFilein, "C", "CLOSE")    /* make sure, file is closed */
  1046.  
  1047.    EXIT -1
  1048.  
  1049.  
  1050.  
  1051. USAGE:
  1052.    CALL say_c  global.eTxtHi || "CRONRGF.CMD"  global.eTxtInf || "- Unix-like cron; executes commands in file repeatedly."
  1053.    CALL say_c
  1054.    CALL say_c "usage:"
  1055.    CALL say_c
  1056.    CALL say_c  global.eTxtHi || "      CRONRGF [/T] [/M] [/R[nn]] [/B] cronfile"
  1057.    CALL say_c
  1058.    CALL say_c "Reads file '" || global.eTxtHi || "cronfile" || global.eTxtInf || "' and executes command[s] repeatedly according to it."
  1059.    CALL say_c "'cronfile' has to be formatted like in Unix; unlike the Unix-version a '%' is"
  1060.    CALL say_c "treated like any other character; empty lines and ones starting with"
  1061.    CALL say_c "a semi-colon (;) or with '#' are ignored."
  1062.    CALL say_c
  1063.    CALL say_c "Switch '" || global.eTxtHi || "/T" || global.eTxtInf || "': the cronfile is read and the user is presented with the date/times"
  1064.    CALL say_c "       and commands to be executed upon them."
  1065.    CALL say_c "Switch '" || global.eTxtHi || "/M" || global.eTxtInf || "': execute once the programs which would have been scheduled"
  1066.    CALL say_c "       between midnight and time of invocation."
  1067.    CALL say_c "Switch '" || global.eTxtHi || "/R[nn]" || global.eTxtInf || "': check cronfile at least every 'nn' (default 60) minutes"
  1068.    CALL say_c "       whether it changed; if so, reread it immediately and set the new"
  1069.    CALL say_c "       schedule-times. If given, the cronfile will be checked after dispatching"
  1070.    CALL say_c "       commands. If not given, cronfile will be read only once."
  1071.    CALL say_c "Switch '" || global.eTxtHi || "/B" || global.eTxtInf || "': do not colorize output strings (e.g. for usage in more, less"
  1072.    CALL say_c "       etc.). This switch merely suppresses the ANSI-color-sequences attached"
  1073.    CALL say_c "       to the strings (hence BlackAndWhite)."
  1074.    CALL say_c
  1075.    CALL say_c "examples:" global.eTxtHi || "CRONRGF /T crontest"
  1076.    CALL say_c "          .. execute statements in file 'crontest' in testmode"
  1077.    CALL say_c  global.eTxtHi || "          CRONRGF /T /M /R30 cronfile"
  1078.    CALL say_c "          .. execute statements in file 'cronfile' in testmode, schedule"
  1079.    CALL say_c "             programs which would have been started since midnight and time"
  1080.    CALL say_c "             of invocation, reread 'cronfile' at least every 30 minutes."
  1081.    CALL say_c  global.eTxtHi || "          CRONRGF /M /R some_file"
  1082.    CALL say_c "          .. execute statements in file 'some_file', schedule programs which"
  1083.    CALL say_c "             would have been started since midnight and time of invocation,"
  1084.    CALL say_c "             reread 'some_file' at least every 60 minutes."
  1085.    CALL say_c  global.eTxtHi || "          CRONRGF /B"
  1086.    CALL say_c "          .. show usage of CRONRGF without colors"
  1087.  
  1088.    EXIT 0
  1089.  
  1090.  
  1091. SAY_C: PROCEDURE EXPOSE global. stemSchedule.
  1092.    SAY global.eTxtInf || ARG(1) || global.eScrNorm
  1093.    RETURN
  1094.  
  1095.  
  1096. ERROR:
  1097.    myrc        = RC
  1098.    errorlineno = SIGL
  1099.    errortext   = ERRORTEXT(myrc)
  1100.    errortype   = CONDITION("C")
  1101.    problem     = "Cause: Probably caused by a user-interrupt while in INTERPRET-statement."
  1102.    CALL stop_it myrc":" errortext "in line # ["errorlineno"] REXX-SIGNAL: ["errortype"] ===>" problem
  1103.  
  1104.