home *** CD-ROM | disk | FTP | other *** search
/ OS/2 Shareware BBS: 10 Tools / 10-Tools.zip / netdor3.zip / DISK_12 / IMAGE11.ZIP / ADMTOOLS / USER.CMD < prev    next >
OS/2 REXX Batch file  |  1993-09-27  |  21KB  |  601 lines

  1. /*****************************************************************************
  2.  *                 USER - Control Users on Multiple Domains                  *
  3.  *                  M. Stokes, T. Bridgman (CORE at WATSON)                  *
  4.  *****************************************************************************
  5.  *                    Licensed Materials-Property of IBM                     *
  6.  *               5604-472 (c) Copyright IBM Corporation, 1993                *
  7.  *                           All rights reserved.                            *
  8.  *                  US Government Users Restricted Rights -                  *
  9.  *                 Use, duplication or disclosure restricted                 *
  10.  *                by GSA ADP Schedule Contract with IBM Corp.                *
  11.  *****************************************************************************
  12.  * Change History                                                            *
  13.  * version 1.0 - 8 May 90 - mstokes                                          *
  14.  * version 1.1 - 8 Sep 91 - teb                                              *
  15.  * - Add command line argument capability.                                   *
  16.  * - Add support for either DC or domain name.                               *
  17.  * - No hidden password entry.                                               *
  18.  * - Add /X (/NOVERIFY) and /DELETE options                                  *
  19.  * - Add /GROUPS option to specify user's groups                             *
  20.  * - Add /ADMIN option to add user as an Administrator                       *
  21.  * - Rename to USER, incorporate DELUSER                                     *
  22.  * 23 Oct 91 - teb                                                           *
  23.  * - error checking for result of operation                                  *
  24.  * 24 Oct 91 - teb                                                           *
  25.  * - Added delay so RXDCNAME doesn't get confused.                           *
  26.  * 6 Nov 91 - teb                                                            *
  27.  * - User not added to groups when more than 1 domain specified.             *
  28.  * 23 Jan 91 - teb                                                           *
  29.  * - Add admin user to ADMIN group.                                          *
  30.  * 17 Aug 92 - teb                                                           *
  31.  * - Respect new RXUTILS error conventions.                                  *
  32.  * 20 Aug 92 - teb                                                           *
  33.  * - Add unique return codes for exit conditions.                            *
  34.  * - Add /D:ALL                                                              *
  35.  * 18 Jan 93 - teb                                                           *
  36.  * - Add /QUERY option to query userid existence.                            *
  37.  * - New fix for wrong info returned by RXDCNAME.                            *
  38.  * 24 May 93 - teb                                                           *
  39.  * - No default group if running under product.                              *
  40.  *****************************************************************************/
  41. trace 'O'
  42. '@ECHO OFF'
  43. call   on halt                         /* Enable error traps */
  44. signal on novalue
  45. signal on syntax
  46. parse arg Args
  47. Args=strip(Args)  /* PTR 266 */
  48. if abbrev(Args, '?') | Args = ''
  49.   then signal Tell
  50.  
  51. Globals = 'Parms. Opts. Required'
  52. call Initialize
  53. call ParseArgs
  54. call GetInfo
  55. call FindDomains
  56. call Execute
  57. exit 0
  58.  
  59. /*****************************************************************************
  60.  * INITIALIZE                                                                *
  61.  *****************************************************************************/
  62. Initialize: procedure expose (Globals)
  63. say
  64. say 'USER - version 1.2'
  65. call LoadRxUtils
  66. if rxOs2Ver() < 1.2
  67.   then call ErrExit 'OS/2 version 1.2 or later required.'
  68. call setlocal
  69. parse source . . Me
  70. MyPath = left(Me, max(lastpos('\', Me)-1, 3))
  71. call value 'PATH', MyPath';'value('PATH',,'OS2ENVIRONMENT'), 'OS2ENVIRONMENT'
  72. return 0
  73.  
  74. /*****************************************************************************
  75.  * PARSEARGS                                                                 *
  76.  *****************************************************************************/
  77. ParseArgs: procedure expose (Globals) Args
  78. parse value '' with Parms.!Desc Parms.!Uid Parms.!PWord Parms.!DomList
  79. parse value 0 with Opts.!NoConfirm 1 Opts.!Delete 1 Opts.!Admin
  80. if QProduct()
  81.   then Parms.!Groups = ''
  82.   else Parms.!Groups = 'CORE'
  83. ValidOpts = 'ADD DEL PW'
  84. parse var Args Parms.!Opt Args '/' SlashArgs
  85. Parms.!Opt = strip(translate(Parms.!Opt))  /* Strip added due to PTR 266 */
  86. select
  87.   when Parms.!Opt = 'ADD'
  88.     then do
  89.       parse upper var Args Parms.!Uid Parms.!PWord .
  90.       Parms.!Desc = subword(Args, 3)
  91.       Required = 'UID PWORD DESC DOMLIST'
  92.     end
  93.   when Parms.!Opt = 'DEL'
  94.     then do
  95.       parse upper var Args Parms.!Uid .
  96.       Required = 'UID DOMLIST'
  97.     end
  98.   when Parms.!Opt = 'PW'
  99.     then do
  100.       parse upper var Args Parms.!Uid Parms.!PWord .
  101.       Required = 'UID PWORD DOMLIST'
  102.     end
  103.   when Parms.!Opt = 'LOCK' | Parms.!Opt = 'UNLOCK'
  104.     then do
  105.       parse upper var Args Parms.!Uid .
  106.       Required = 'UID DOMLIST'
  107.     end
  108.   when Parms.!Opt = 'QUERY'
  109.     then do
  110.       parse upper var Args Parms.!Uid .
  111.       Required = 'UID DOMLIST'
  112.     end
  113.   otherwise call ErrExit 'Invalid option' Parms.!Opt '- USER ? for help.', 3
  114. end
  115. do while SlashArgs <> ''
  116.   parse var SlashArgs SArg '/' SlashArgs
  117.   parse upper var SArg SArg . ':' SOpt
  118.   select
  119.     when abbrev('DOMAINS', SArg)
  120.       then Parms.!DomList = SOpt
  121.     when SArg = 'X' | abbrev('NOVERIFY', SArg)
  122.       then Opts.!NoConfirm = 1
  123.     when abbrev('GROUPS', SArg)
  124.       then Parms.!Groups = SOpt
  125.     when abbrev('DELETE', SArg, 3)
  126.       then Opts.!Delete = 1
  127.     when SArg = 'ADMIN'
  128.       then Opts.!Admin = 1
  129.     otherwise call ErrExit 'Unrecognized option' SArg'.', 3
  130.   end
  131. end
  132.  
  133. if \QProduct() & Opts.!Admin & wordpos(Parms.!Groups, 'ADMIN') = 0 &,
  134.     Parms.!Groups <> ''
  135.   then Parms.!Groups = Parms.!Groups 'ADMIN'
  136. return 0
  137.  
  138. /*****************************************************************************
  139.  * GETINFO                                                                   *
  140.  *****************************************************************************/
  141. GetInfo: procedure expose (Globals)
  142.  
  143. if wordpos('UID', Required) > 0
  144.   then do while Parms.!Uid = ''
  145.     say
  146.     call rxSay 'Enter user id: '
  147.     parse upper linein Parms.!Uid .
  148.   end
  149.  
  150. if wordpos('DESC', Required) > 0
  151.   then do while Parms.!Desc = ''
  152.     say
  153.     call rxSay 'Enter user description (name and location): '
  154.     parse linein Parms.!Desc
  155.   end
  156. if Opts.!Admin
  157.   then Parms.!Desc = Parms.!Desc '- Admin'
  158.  
  159. if wordpos('PWORD', Required) > 0
  160.   then do
  161.     do while Parms.!PWord = ''
  162.       say
  163.       call rxSay 'Enter password: '
  164.       parse upper linein Parms.!PWord
  165.     end
  166.     if length(Parms.!PWord) > 14
  167.       then call ErrExit 'Password must be 14 characters or less.', 7
  168.   end
  169.  
  170. if wordpos('DOMLIST', Required) > 0
  171.   then do while Parms.!DomList = ''
  172.     say
  173.     call rxSay 'Enter domain list or domain group: '
  174.     parse upper linein Parms.!DomList
  175.   end
  176. return 0
  177.  
  178. /*****************************************************************************
  179.  * FINDDOMAINS                                                               *
  180.  *****************************************************************************/
  181. FindDomains: procedure expose (Globals)
  182. NewList = ''
  183. Sep = d2c(26)
  184. CDr = value('CORE.DIR',,'OS2ENVIRONMENT')
  185. ServFile = CDr'LOCAL\COMPLEX\SERVERS.COR'
  186. if Parms.!DomList = 'ALL'
  187.   then do
  188.     Parms.!DomList = ''
  189.     Temp = NameFind(ServFile ':TYPE DOMAIN :NICK /RETURN *')
  190.     if abbrev(Temp, '$RXERROR') | abbrev(Temp, 'ERROR:') 
  191.       then Temp = ''
  192.     do while Temp <> ''
  193.       parse var Temp ':NICK.' Dom (Sep) Temp
  194.       Parms.!DomList = Parms.!DomList Dom
  195.     end
  196.   end
  197.  
  198. Retries = 3
  199. do J = 1 until Parms.!DomList = ''
  200.   parse var Parms.!DomList Domain Parms.!DomList
  201.   DCName = rxDCName(Domain)
  202.   if abbrev(DCName, '$RXERROR') | abbrev(DCName, 'ERROR:')
  203.     then do
  204.       Servers = NameFind(ServFile ':GROUP' Domain ':NICK /RETURN *')
  205.       if abbrev(Servers, 'ERROR:') | Servers = ''
  206.         then DCName = ''
  207.         else do
  208.           do while Servers <> ''
  209.             parse var Servers ':NICK.' Server (Sep) Servers
  210.             Parms.!DomList = Server Parms.!DomList
  211.           end
  212.           iterate J
  213.         end
  214.     end
  215.   if DCName = ''
  216.     then do J = 1 to Retries until DCName <> ''
  217.       DCName = rxDCName(Domain)
  218.       if abbrev(DCName, '$RXERROR') | abbrev(DCName, 'ERROR:')
  219.         then DCName = ''
  220.      end
  221.   if DCName <> ''
  222.     then NewList = NewList DCName
  223.     else say d2c(7)'Warning: could not identify' Domain '- skipping.'
  224. end
  225. Parms.!DomList = strip(NewList)
  226. if Parms.!DomList = ''
  227.   then call ErrExit 'Could not identify any domain names.', 4
  228. return 0
  229.  
  230. /*****************************************************************************
  231.  * CONFIRM                                                                   *
  232.  *****************************************************************************/
  233. Confirm: procedure expose (Globals) Prompt.
  234. say
  235. do I = 1 to Prompt.0
  236.   say Prompt.I
  237. end
  238. if \Opts.!NoConfirm
  239.   then do
  240.     do until wordpos(Resp, 'Y N') > 0
  241.       say
  242.       call rxSay Prompt.!Question' '
  243.       Resp = translate(rxGetKey('ECHO'))
  244.       say
  245.     end
  246.     if Resp = 'N'
  247.       then call ErrExit 'Cancelled at user request.', 5
  248.   end
  249. return 0
  250.  
  251. /*****************************************************************************
  252.  * EXECUTE                                                                   *
  253.  *****************************************************************************/
  254. Execute: procedure expose (Globals)
  255. Prompt.0 = 0
  256. select
  257.   when Parms.!Opt = 'ADD'
  258.     then do
  259.       call rxStemInsert 'PROMPT.', Prompt.0+1, 'Id:       ' Parms.!Uid
  260.       call rxStemInsert 'PROMPT.', Prompt.0+1, 'User:     ' Parms.!Desc
  261.       call rxStemInsert 'PROMPT.', Prompt.0+1, 'Password: ' Parms.!PWord
  262.       call rxStemInsert 'PROMPT.', Prompt.0+1, 'Domains:  ' Parms.!DomList
  263.       call rxStemInsert 'PROMPT.', Prompt.0+1, 'Groups:   ' Parms.!Groups
  264.       call rxStemInsert 'PROMPT.', Prompt.0+1, 'Del First:' Opts.!Delete
  265.       Prompt.!Question = 'Add this user (Y/N)?'
  266.       call Confirm
  267.       call AddUser
  268.     end
  269.   when Parms.!Opt = 'DEL'
  270.     then do
  271.       call rxStemInsert 'PROMPT.', Prompt.0+1, 'Id:       ' Parms.!Uid
  272.       call rxStemInsert 'PROMPT.', Prompt.0+1, 'Domains:  ' Parms.!DomList
  273.       Prompt.!Question = 'Delete this user (Y/N)?'
  274.       call Confirm
  275.       call DelUser
  276.     end
  277.   when Parms.!Opt = 'PW'
  278.     then do
  279.       call rxStemInsert 'PROMPT.', Prompt.0+1, 'Id:       ' Parms.!Uid
  280.       call rxStemInsert 'PROMPT.', Prompt.0+1, 'Password: ' Parms.!PWord
  281.       call rxStemInsert 'PROMPT.', Prompt.0+1, 'Domains:  ' Parms.!DomList
  282.       Prompt.!Question = 'Change this user''s password (Y/N)?'
  283.       call Confirm
  284.       call ChangePW
  285.     end
  286.   when Parms.!Opt = 'LOCK' | Parms.!Opt = 'UNLOCK'
  287.     then do
  288.       call rxStemInsert 'PROMPT.', Prompt.0+1, 'Id:       ' Parms.!Uid
  289.       call rxStemInsert 'PROMPT.', Prompt.0+1, 'Domains:  ' Parms.!DomList
  290.       Prompt.!Question = Parms.!Opt 'this user (Y/N)?'
  291.       call Confirm
  292.       call UserAccess(Parms.!Opt)
  293.     end
  294.   when Parms.!Opt = 'QUERY'
  295.     then call Query
  296. end
  297. return
  298.  
  299. /*****************************************************************************
  300.  * ADDUSER                                                                   *
  301.  *****************************************************************************/
  302. AddUser: procedure expose (Globals)
  303. if Opts.!Delete
  304.   then call DelUser
  305. do until Parms.!DomList = ''
  306.   parse var Parms.!DomList Domain Parms.!DomList
  307.   say
  308.   say 'Adding' Parms.!Uid 'to' Domain'...'
  309.   if Opts.!Admin
  310.     then Priv = 'ADMIN'
  311.     else Priv = 'USER'
  312.   'NET ADMIN' Domain '/C NET USER' Parms.!Uid Parms.!PWord '/ADD',
  313.       '/ACTIVE:YES /PRIVILEGE:'Priv '/PASSWORDREQ:YES',
  314.       '/USERCOMMENT:\"'Parms.!Desc'\" /FULLNAME:\"'Parms.!Desc'\"'
  315.   if rc <> 0
  316.     then do
  317.       call ErrExit 'Error' rc 'adding user to' Domain'.', 6
  318.       iterate
  319.     end
  320.   do I = 1 to words(Parms.!Groups)
  321.     Group = word(Parms.!Groups, I)
  322.     say 'Adding' Parms.!Uid 'to' Group 'group...'
  323.     'NET ADMIN' Domain '/C NET GROUP' Group Parms.!Uid '/ADD'
  324.   end
  325. end
  326. return
  327.  
  328. /*****************************************************************************
  329.  * QUERY                                                                     *
  330.  *****************************************************************************/
  331. Query: procedure expose (Globals)
  332. say 'Querying presence of' Parms.!Uid'...'
  333. do until Parms.!DomList = ''
  334.   parse var Parms.!DomList Domain Parms.!DomList
  335.   PreQ = queued()
  336.   'NET ADMIN' Domain '/C NET USER' Parms.!Uid '2>&1 | RXQUEUE /LIFO'
  337.   Exist = 0
  338.   parse value '' with Name Cmt Priv PWSet LLogon
  339.   do while queued() > PreQ 
  340.     parse pull Line
  341.     ULine = translate(space(Line))
  342.     select
  343.       when ULine = 'USER ID' Parms.!Uid
  344.         then Exist = 1
  345.       when abbrev(ULine, 'USER''S COMMENT')
  346.         then Cmt = subword(Line, 3)
  347.       when abbrev(ULine, 'FULL NAME')
  348.         then Name = subword(Line, 3)
  349.       when abbrev(ULine, 'PRIVILEGE LEVEL')
  350.         then Priv = subword(Line, 3)
  351.       when abbrev(ULine, 'PASSWORD LAST SET')
  352.         then PWSet = subword(Line, 4)
  353.       when abbrev(ULine, 'LAST LOGON')
  354.         then LLogon = subword(Line, 3)
  355.       otherwise nop
  356.     end
  357.   end
  358.   call rxSay left(substr(Domain, 3), 12, '.')' '
  359.   if Exist
  360.     then do
  361.       if Name <> ''
  362.         then Info = Name
  363.         else Info = Cmt
  364.       if Cmt <> '' & Info <> Cmt
  365.         then Info = Info '('Cmt')'
  366.       if Info = ''
  367.         then Info = '(No user info)'
  368.       Info = strip(Info ' Priv:' Priv)
  369.       say Info
  370.       say copies(' ', 12) 'Last logon:' LLogon ' PW set:' PWSet
  371.     end
  372.     else say Parms.!Uid 'does not exist.'
  373. end
  374. return 0
  375.  
  376. /*****************************************************************************
  377.  * CHANGEPW                                                                  *
  378.  *****************************************************************************/
  379. ChangePW: procedure expose (Globals)
  380. say 'Setting password for' Parms.!Uid'...'
  381. call 'MULTCMD' 'NET USER' Parms.!Uid Parms.!Pword, Parms.!DomList
  382. return
  383.  
  384. /*****************************************************************************
  385.   * DELUSER                                                                  *
  386.  *****************************************************************************/
  387. DelUser: procedure expose (Globals)
  388. say 'Deleting' Parms.!Uid'...'
  389. call 'MULTCMD' 'NET USER' Parms.!Uid '/D', Parms.!DomList
  390. return
  391.  
  392. /*****************************************************************************
  393.  * USERACCESS                                                                *
  394.  *****************************************************************************/
  395. UserAccess: procedure expose (Globals)
  396. parse arg Op .
  397. if Op = 'LOCK'
  398.   then Arg = 'NO'
  399.   else Arg = 'YES'
  400. say Op'ing' Parms.!Uid'...'
  401. call 'MULTCMD' 'NET USER' Parms.!Uid '/ACTIVE:'Arg, Parms.!DomList
  402. return
  403.  
  404. /*****************************************************************************
  405.  * ERREXIT                                                                   *
  406.  *****************************************************************************/
  407. ErrExit:
  408. parse arg EMsg, XCode
  409. if XCode = '' then XCode = 2
  410. say d2c(7)EMsg
  411. exit XCode
  412.  
  413. /*****************************************************************************
  414.  * LOADRXUTILS                                                               *
  415.  *****************************************************************************/
  416. LoadRxUtils: procedure
  417. if \rxfuncadd('RXLOADFUNCS', 'RXUTILS', 'RXLOADFUNCS')
  418.   then do
  419.     signal on syntax name LoadRxUtils2
  420.     call rxLoadFuncs 'QUIET'
  421.   end
  422. return 0
  423.  
  424. LoadRxUtils2:
  425. signal off syntax    /* Turn off temp error trap */
  426. /* If you have an error trap in the program, use the following line instead
  427. signal on syntax name syntax
  428. */
  429. select
  430.   when rc = 40
  431.     then call rxLoadFuncs
  432.   when rc = 43
  433.     then do
  434.       say 'Error:  RXUTILS.DLL not found.'
  435.       exit 2
  436.     end
  437.   otherwise do
  438.     say 'Error: Error' rc 'registering RXUTILS functions.'
  439.     exit 2
  440.   end
  441. end
  442. return 0
  443.  
  444. QProduct: procedure
  445. call rxfuncadd 'RXCOUINFO', 'COUENV', 'RXCOUINFO'
  446. signal on syntax name QProduct2
  447. return (rxCouInfo('VER'))
  448.  
  449. QProduct2:
  450. return 1  /* Assume most restrictive (product) if we can't tell */
  451.  
  452. /*****************************************************************************
  453.  *                       DEBUGGING and ERROR RECOVERY                        *
  454.  *****************************************************************************/
  455. SignalOff:
  456. signal off error
  457. signal off failure
  458. signal off halt
  459. signal off novalue
  460. signal off notready
  461. signal off syntax
  462. return
  463.  
  464. BugInit:
  465. if symbol('GLOBALS') = 'LIT'
  466.   then do
  467.     Globals = 'TrVal'
  468.     TrVal = 'O'
  469.   end
  470. return
  471.  
  472. Halt:
  473. Where = SigL
  474. call off halt
  475. if abbrev(stream('STDIN:', 'C', 'CLOSE'), 'READY')
  476.   then do
  477.     call beep 100, 250
  478.     call rxSay 'Do you want to abort (Y/N)? '
  479.     do until pos(Ch, 'YND') <> 0
  480.       Ch = translate(rxGetKey('NOECHO'))
  481.     end
  482.     call rxSay Ch
  483.   end
  484.   else do
  485.     Ch = 'N'
  486.     say 'Could not close stdin.  Unconditional abort.'
  487.   end
  488. if Ch = 'N'
  489.   then call on halt
  490.   else if Ch = 'D'
  491.     then signal DebugHook
  492.     else exit 255
  493. return 0
  494.  
  495. Failure:
  496. Where = SigL
  497. call SignalOff
  498. call BugInit
  499. say '>> Failure raised in line' Where
  500. signal DebugExit
  501. Error:
  502. Where = SigL
  503. call SignalOff
  504. call BugInit
  505. say '>> Error raised in line' Where
  506. signal DebugExit
  507.  
  508. Syntax:
  509. Where = SigL
  510. call SignalOff
  511. call BugInit
  512. say '>> Syntax error' rc '('errortext(rc)') raised in line' Where
  513. signal DebugExit
  514.  
  515. Novalue:
  516. Where = SigL
  517. call SignalOff
  518. call BugInit
  519. say '>> Novalue error raised in line' Where
  520. say '>> Undefined variable was:' condition('D')
  521. signal DebugExit
  522.  
  523. DebugExit:
  524. parse upper arg SkipQues .
  525. parse source . . Me
  526. if SkipQues <> '<SKIP>'
  527.   then do
  528.     say 'Line reads: "'sourceline(Where)'"'
  529.     say 'Active file was:' Me
  530.     if symbol('ANSI.!Normal') = 'VAR'
  531.       then say ANSI.!Normal
  532.       else say
  533.     say 'Please notify the CORE Developers!  Press <Enter> to exit.'
  534.     if translate(linein('STDIN:')) <> '/D'
  535.       then exit
  536.   end
  537. DebugHook:
  538. trace ?i
  539. nop
  540. exit
  541.  
  542. /*****************************************************************************
  543.  * TELL                                                                      *
  544.  *****************************************************************************/
  545. Tell:
  546. if Args = '?' | Args = '' then do
  547. call rxCls
  548. say
  549. say 'USER - Network User Control over Multiple Domains'
  550. say
  551. say 'Adding a userid:'
  552. say 'USER ADD [userid [password [comment]]] [/Domains:domlist] [options]'
  553. say
  554. say '  userid   - network userid to add'
  555. say '  password - password for the userid'
  556. say '  comment  - user description (e.g., name and location)'
  557. say '  domlist  - list of domains (separated by spaces) or ALL'
  558. say '  "options" may be any combination of:'
  559. say '    /DELete         - Force deletion of userid before adding it.'
  560. say '    /Groups:grplist - Add user to specified groups.'
  561. say '    /ADMIN          - Add user with Admin privileges'
  562. say '    /X              - Quick execution (no confirmation prompt).'
  563. say
  564. say 'Deleting a userid:'
  565. say 'USER DEL [userid [/Domains:domlist]]'
  566. say
  567. say 'Changing a password for a userid:'
  568. say 'USER PW [userid [password]] [/Domains:domlist]'
  569. say
  570. call rxPause '[more]'
  571. call rxCls
  572. say
  573. say
  574. say 'USER - Network User Control over Multiple Domains'
  575. say
  576. say 'Locking or unlocking a user:'
  577. say 'USER LOCK|UNLOCK [userid] [/Domains:domlist]'
  578. say
  579. say 'Query existence of a user:'
  580. say 'USER QUERY [userid] [/Domains:domlist]'
  581. say
  582. say 'Enter USER ?? for return codes.'
  583. end
  584. if Args = '??' then do
  585. call rxCls
  586. say
  587. say
  588. say 'USER Return Codes'
  589. say
  590. say '   0:  Success'
  591. say '   2:  Miscellaneous error (none of the following)'
  592. say '   3:  Invalid option entered'
  593. say '   4:  No domains could be identified'
  594. say '   5:  User cancelled operation'
  595. say '   6:  Execution error performing operation'
  596. say '   7:  Password too long.'
  597. say ' 255:  Program error (syntax, novalue, etc.)'
  598. end
  599. say
  600. exit 0
  601.