Saturday, October 03, 2009

Oldschool Mainframe/COBOL geek-out

Oldschool Mainframe/COBOL geek-out

So I logged onto my work's mainframe a few days ago (for an arcane project-related purpose I won't go into), and, as I am wont to do, I poked around in my directory to see what I was working on way back when. Last year, I did a lot of work in our mainframe developing a job-scheduling solution. This year, I've been hanging out mainly in Unix land, and haven't had any mainframe contact for several months.

I miss the mainframe, the misunderstood behemoth, with its OCOPY and BPXBATCH, its copybooks, its IDCAMS and LISTCAT, and, lastly, its COBOL. But on the other hand, it's insane, and has a lot of acronyms that are unfamiliar to most of the modern IT world, who are knee-deep in C#.NET, Eclipse, and Oracle, or maybe Linux, Python, and MySQL. In fact, in the late 90s when I was explaining to my NT Administrator buddy about working on a mainframe through a 3270 terminal, how the native character set was EBCDIC, not ASCII-based, and how synchronous comm like Bisync and SNA worked, he scratched his head in befuddlement at the queer, unfamiliar arcana.

So with that in mind, I suspect that no one I know who reads this will have any idea what any of this means, and so I will try to go slow, and attempt to explain all the fun I had taking my trip down memory lane. The punchline is this: Last year I played with COBOL in my downtime, to see what the fuss was all about. I used ROSCOE to write the code, to submit the jobs, to compile, to link, and to execute. And it was fun.

Step 1: 3270

This is my interface to the mainframe, a Windows-based 3270 emulator, with the stuff I'm not allowed to show you blocked out. Originally the interface to a mainframe was a punchcard reader and a printer to show the results, a nightmare that users now look back on fondly. Eventually IBM gave us dumb-terminals to use instead, little more than a monitor and keyboard, cabled directly into the mainframe. The "IBM 3270" was the model number of a more advanced dumb-terminal that became the standard. Today "3270" is used more to refer to the protocol the device used than the device itself, and sometimes you'll see "TN3270" (Telnet 3270) referring to sending 3270 traffic over an IP connection, as my emulator does.

3270 is a simplistic, but feature rich, text-only interface, most comparable to an old DOS program. Most screens have a home position you can return to by pressing the Home key, and in a typical program you can tab around to various input fields.

So for our purposes, 3270 = keyboard and screen. Rather than take lots of screenprints and upload them somewhere, I've simply copied the text of my 3270 sessions, losing the pretty colors, but little else. Also, instead of blocking company confidential items out, I've replaced them with innocent you-get-the-idea alternatives. I've done this as well with a couple other items that aren't confidential, per se, but things I determined to be too "Mitnicky".

Step 2: ROSCOE

ROSCOE is an application that runs on mainframes that bills itself as a "development environment" but I find it more closely resembles an operating system. Users log in and have their own directories where they keep their stuff. It provides access to submit jobs (run programs) to the mainframe, open and edit datasets (files), and has a scripting language called RPF that is roughly comparable to Windows batch files or Unix shell scripts.

Old-school mainframe guys typically hate ROSCOE, and prefer doing their work in TSO (Time Sharing Option [no idea]) which has a simple menu system, and a command line. Not me, though, I'm ROSCOE all the way. It just feels more natural. ROSCOE vs. TSO to mainframers is like emacs vs. vi to Linux geeks - and I prefer the least popular in both cases. (Emacs on Linux, by the way, is the road less traveled by, and as the poem goes, that has made all the difference. Emacs encouraged me to learn LISP and some sysadmin theory to get it running on a system whose admin really didn't want it there [export EMACSLOADPATH=~/lisp/progmodes, sucka!!!], which built up some more techie muscle mass. And street cred.)

Where was I? Ah yes! ROSCOE. The login screen looks like this:

> APPLID(MYROSCOE)                                                              
|||||||||              Roscoe (LPAR)     Columbus, Ohio               ||||||||| 
|||||||||                                                             ||||||||| 
|||||||||    KEY                                   DATE    09/29/09   ||||||||| 
|||||||||    PASSWORD                              TIME    10.22.03   ||||||||| 
|||||||||    GROUP CODE                            TERMID  MYTERMID   ||||||||| 
|||||||||    NEW PASSWORD >                                           ||||||||| 
|||||||||                 >              (Repeat for verification)    ||||||||| 
|||||||||                                                             ||||||||| 
|||||||||||        |||||      |||||       ||||        ||||      ||||       |||| 
||||||||||   |||   |||   ||   |||   |||||||||   ||||||||   ||   |||   ||||||||| 
|||||||||   |||   |||   ||   |||   |||||||||   ||||||||   ||   |||   |||||||||| 
||CA-|||         |||   ||   ||||       ||||   ||||||||   ||   |||      |||||||| 
|||||||   ||  |||||   ||   |||||||||   |||   ||||||||   ||   |||   |||Ver 6.0|| 
||||||   |||  ||||   ||   |||||||||   |||   ||||||||   ||   |||   |||Glvl 0612| 
|||||   |||   ||||      |||||       |||||       ||||      ||||       |||| SP09| 
||||||||| Copyright (c) 1994 Computer Associates International, Inc. |||||||||| 

Nothing fancy, just a prompt for your ID and password, and some text art. I'd say "ASCII art", but the character set isn't ASCII, it's EBCDIC. The reason we even *have* EBCDIC is because of this sumbich right here:

This is the IBM S/360, which was released in 1964, roughly the same time the ASCII standardization committee was trying to standardize for all computers which numbers between 0 and 255 should represent which printed characters. IBM was a proponent of ASCII, but they had shipping deadlines, and kept with their own character set rather than delay while they created ASCII peripherals. The 360 became popular and a lot of code was written for it which assumed the EBCDIC character set was being used, so future mainframes stayed EBCDIC to grandfather in all the code.

That one shipping deadline, years before I was born, has done more to keep me employed over the last 12 years than anything else. Except possibly Mountain Dew. I won't bore you with my exploits in the land of character set conversion problems; it will suffice to say that there are enough to keep you busy, and there are few people skilled at diagnosing them. Every techie who made it through the dot com bust without getting laid off knows the simple creed: Find a niche. I found one, Unix-to-Mainframe communications, and, knock on wood, I've been happily employed ever since.

f ls                                                                           

> APPLID(MYROSCOE)   USER(CID,CURTIS1)                                          

Once you are logged in, you are presented with a mostly blank screen, with a number line at the top, and a display showing you what application ID (what instance of ROSCOE) you're using, and what user ID you're logged in with. A few lines above that is the home position, where you enter commands. Output from most commands is shown below the number line.

Here I am entering into the home position the command "f ls". This will fetch from my library the member "LS". Library = directory, member = file. However, they aren't normal files like datasets are, they can only be accessed from within ROSCOE. Other than that, there's little difference.

My "LS" file is a little shortcut script I wrote to display the contents of ROSCOE libraries, mine or other people's, and optionally to filter based on a pattern. In Unix and Linux, "ls" is the same as "dir" in DOS and Windows, and I wanted something similar for ROSCOE libraries, to save some mental gymnastics and a few keystrokes here and there.


> APPLID(MYROSCOE)   USER(CID,CURTIS1)                                          

So now I've fetched the LS file, what happens next? Not much; the screen is still blank under the number line. This is because "fetching" loads things into your AWS (active workspace), which may or may not be displayed on the screen.

To display the AWS, I have to type "a" (attach) in the home position. You can attach other things, like lists of library members, the contents of datasets, or job output, but the default if you don't specify any parameters to "a" is to attach the active workspace. You can create other workspaces that are not active, which is roughly equivalent to opening Notepad and minimizing it. It's there, but not the thing you're looking at.

Step 3: RPF


> APPLID(MYROSCOE)   USER(CID,CURTIS1)                                          
> AWS(CID.LS)         SCRL CSR  COLS 00001 00072                   A<ROS1>      
>       <...+....1....+....2....+....3....+....4....+....5....+....6....+....7..
...... ================================ T O P =================================
000001   <<LS>>                                                                
000002 DECLARE MEM                                                             
000003 IF (A1 EQ '')                                                           
000004   LET MEM = S.PREFIX | '.*'                                             
000005 ELSE                                                                    
000006   LET MEM = A1                                                          
000007   IF LENGTH(MEM) EQ 3                                                   
000008     LET MEM = MEM | '.*'                                                
000009   ELSE                                                                  
000010     IF RIGHT(MEM 1) NE '*'                                              
000011       LET MEM = MEM | '*'                                               
000012     ENDIF                                                               
000013   ENDIF                                                                 
000014 ENDIF                                                                   
000015 +A +MEM+                                                                
...... ============================= B O T T O M ==============================

Here is what the LS script looks like, finally, after attaching the AWS containing the library I just fetched (like how I'm speaking mainframe now and you're understanding it? Or does it piss you off the same way the invented argots of Watership Down and A Clockwork Orange did?)

The RPF language superficially resembles BASIC, and most of it will be clear on a first glance to programmers of any language. LET assigns values to variables, IF/ELSE/ENDIF blocks decision trees, the weird thing on line 1 is probably a program identifier, A1 isn't defined, so it must be a standard input variable, a switch you can pass to the program. The thing that might throw a non-mainframer is the use if the pipe symbol ( | ) on lines 4, 8, and 11. Pipe on Unix commandlines takes output from one program and makes it the input to the next program, in most PC languages it implies a logical OR (usually it's two pipes together for OR). What it does here is concatenate strings. Line 15 will look strange, too; the + at the beginning of the line denotes "this line is a ROSCOE command which contains variable substitution". The variable MEM surrounded by plus signs denotes "substitute this variable name with its contents".

So a quick breakdown of what happens is as follows:
- If I don't pass any parameters to LS, create MEM as my short user name (stored in S.PREFIX, the value I'm showing here as "CID", and the same name prepends all my library members) followed by ".*".
- Otherwise,
-- if the parameter I pass is three letters long, say, long enough to be someone else's S.PREFIX, then use that and append ".*" to it.
-- otherwise, just append "*"
- Finally, Attach the library pattern specified by MEM

RPF is sort of fun, and useful for doing search and replaces in a lot of datasets or library members, and it can even fetch JCL ("real" mainframe jobs), manipulate it however you want, and submit it (ask the mainframe to run the job).

A few years ago, I went searching for sample RPF code on the Internet, and I found lots of people asking RPF questions, or mentioning it in passing, but I found exactly one hit that had actual source code. My theory for why this is so is simple: nobody ever "plays" on a mainframe, a problem I try to correct whenever I'm feeling dinosaurish. The one hit I did find was on, which is a collection of source code in different languages whose goal is to print the entire lyrics to "99 Bottles of Beer on the Wall". Funny, and I hadn't run across the website prior.

Here is the source code I found:

let l1 = 99                                                          
let l2 = ' bottles'                                                  
loop 99 times                                                        
write aws * l1 | l2 | ' of beer on the wall,' | l2 | ' of beer' 
let l1 = l1 - 1                                                 
select any                                                      
when l2 eq 9                                                    
let l2 = 'bottle'                                          
when l2 gt 0                                                    
write aws * 'Take one down and pass it around,'            
when none                                                       
let l2 = 'No more ' | l2                                   
write aws * l2 | ' of beer on the wall,' | l2 | 's of beer'
: RPF version of 99 bottles of beer     
: programmer: Günter Laudenklos         

A few things may jump out at you. First, it's in lowercase. On the mainframe most things are uppercase, a throwback to the BCDIC character set (before it had been extended - the E in EBCDIC), which supported only 64 characters, and had no room for lowercase. RPF can be lowercase just fine, I just go mentally into uppercase mode as a rule when working on mainframes. RPF casts functions and variable names to uppercase silently when it needs to. Another thing that may jump out is that he's assigning values to variables that haven't been declared. L1 through L6 are local variables, and don't need to be declared, however a script may only use local or declared variables, not both.

I have a feeling Günter (who seems, judging from his Facebook profile, to be a nice enough fellow) never tested this script, as it couldn't possibly work. On some lines he thinks L2 is the number of bottles, on some he thinks it's the placeholder for "bottles" and "bottle" for when you get down to 1 more left. There are other problems not worth examining in detail; instead, here's what a working RPF to print the whole song would look like:

let l1 = 99                                                             
let l2 = ' bottles'                                                     
loop 99 times                                                           
write aws * l1 | l2 | ' of beer on the wall, ' | l1 | l2| ' of beer,'  
write aws * 'you take one down, pass it around,'                   
let l1 = l1 - 1                                                    
if l1 eq 1                                                         
let l2 = ' bottle'                                               
write aws * '1 more bottle of beer on the wall!'                 
if l1 gt 0                                                       
write aws * l1 | l2 | ' of beer on the wall.'               
write aws * 'No more bottles of beer on the wall!'          

...and here is what it would output:

> APPLID(MYROSCOE)   USER(CID,CURTIS1)                                          
> AWS()               SCRL CSR  COLS 00001 00072                   A<ROS1>      
>       <...+....1....+....2....+....3....+....4....+....5....+....6....+....7..
028300 5 bottles of beer on the wall, 5 bottles of beer,                       
028400 you take one down, pass it around,                                      
028500 4 bottles of beer on the wall.                                          
028600 4 bottles of beer on the wall, 4 bottles of beer,                       
028700 you take one down, pass it around,                                      
028800 3 bottles of beer on the wall.                                          
028900 3 bottles of beer on the wall, 3 bottles of beer,                       
029000 you take one down, pass it around,                                      
029100 2 bottles of beer on the wall.                                          
029200 2 bottles of beer on the wall, 2 bottles of beer,                       
029300 you take one down, pass it around,                                      
029400 1 more bottle of beer on the wall!                                      
029500 1 bottle of beer on the wall, 1 bottle of beer,                         
029600 you take one down, pass it around,                                      
029700 No more bottles of beer on the wall!                                    
...... ============================= B O T T O M ==============================

The proof is in the pudding, as they say. This version doesn't format the output perfectly, and omits the optional extra lyric (go to the store, buy some more...), but you get the point, which is basically: Test your shit, Günter! Granted the number of people who were likely to verify your code were pretty small, but still. To be fair, I found some of his other code, a Mahjong web app, and it seemed to work well enough.

And now back to the results of running my "LS" script, sans variables, which would be the same in my case as typing "A CID.*", which runs a process to list all of my library members, prompting me to choose which one to attach.

Step 4: Compiling and Linking

> APPLID(MYROSCOE)   USER(CID,CURTIS1)                                          
> LIB()               SCRL CSR  COLS 00001 00079                   LINE 000133  
SIGNON KEY                                    MEM  EXCL BLOCKS  M-LINES   LINES
CURTIS1                                       240     0    243    10000    5090
MEMBER          STATUS              DESCRIPTION           CREATED  UPDATED  
CID.ALIAS                                                 04/30/08 04/30/08 
CID.CALLOUT                                               03/05/08 03/05/08 
CID.CD                    Copy dataset to AWS             01/11/08 01/30/08 
CID.CJ                    Copy job output to AWS          12/20/07 01/18/08 
CID.CL                    Copy library to AWS             09/19/07 01/31/08 
a  CID.COBCOMP               Compile a COBOL program         05/14/08 05/25/08 
CID.COBLINK               Link COBOL object files         05/14/08 05/22/08 
CID.COBRUN                Run a COBOL program             05/14/08 05/27/08 
CID.COBTEST                                               05/14/08 05/19/08 
CID.DOSKIP                                                01/18/08 06/17/08 
CID.EIALIAS                                               04/30/08 05/01/08 
CID.EICALL                                                01/30/08          
CID.EICALLA                                               05/01/08 05/01/08 
CID.EICALLW                                               11/12/08          
CID.EICALW2                                               03/03/09         

When I finished doing the real work on the mainframe I had set out to do, I started poking around in my directory, running my "LS" RPF script, which produced the above output. The COBOL JCLs jumped out at me in an "Oh yeah, that was fun!" moment, and I decided to take a peek at them. In this case I just had to cursor down to the one I was interested in, COBCOMP, and put an "a" beside it.

> APPLID(MYROSCOE)   USER(CID,CURTIS1)                                          
> LIB(CID.COBCOMP)    SCRL CSR  COLS 00001 00073                   LINE 000001  
>      <...+....1....+....2....+....3....+....4....+....5....+....6....+....7...
=============================== T O P ================================= 
000001 //CURTIS1C  JOB (59000),'COBOLCOMPILE',CLASS=Q,                          
000002 // MSGCLASS=H,REGION=4M                                                  
000003 /*ROUTE XEQ COLUMBUS                                                     
000004 /*ROUTE PRINT COLUMBUS                                                   
000005 // SET PRGNAME=HW                                                  
000006 //COMPILE  EXEC PGM=IGYCRCTL,                         COMPILER           
000007 //    PARM='DYNAM,RESIDENT,DATA(24),NUMBER,LIST,MAP,TEST'                
000009 //SYSIN    DD   DSN=CURTIS1.S.SRCLIB(&PRGNAME.),      SOURCE CODE        
000010 //         DISP=SHR                                                      
000012 //    DISP=(OLD,KEEP,KEEP),SPACE=(CYL,(1,1))                             
000013 //SYSPRINT DD   SYSOUT=H                              COMPILER MESSAGES  
000014 //SYSUT1   DD   UNIT=SYSDA,SPACE=(CYL,(1,1))          WORK SPACE         
000015 //SYSUT2   DD   UNIT=SYSDA,SPACE=(CYL,(1,1))          (NEED ALL 7)       
000016 //SYSUT3   DD   UNIT=SYSDA,SPACE=(CYL,(1,1))                             
000017 //SYSUT4   DD   UNIT=SYSDA,SPACE=(CYL,(1,1))                             
000018 //SYSUT5   DD   UNIT=SYSDA,SPACE=(CYL,(1,1))                             
000019 //SYSUT6   DD   UNIT=SYSDA,SPACE=(CYL,(1,1))                             
000020 //SYSUT7   DD   UNIT=SYSDA,SPACE=(CYL,(1,1))                             
000021 //*                                                                      
000100 //LINK     EXEC PGM=IEWL,REGION=1024K,             LINK PROGRAM          
000200 //          PARM='XREF,LET,LIST,AMODE=24,RMODE=24'                       
000400 //         DD  DSN=SYS1.COB2COMP,DISP=SHR                                
000600 //         DISP=SHR                                                      
000800 //         DISP=(OLD,KEEP,KEEP),SPACE=(CYL,(1,1))                        
000900 //SYSUT1   DD  UNIT=SYSDA,DCB=BLKSIZE=1024,        WORKSPACE             
001000 //         SPACE=(CYL,(1,1))                                             
001100 //SYSPRINT DD   SYSOUT=H                           MESSAGES FROM LINKER  
============================ B O T T O M ============================== 

Yes, here is where it gets strange. This is JCL, and is a bear to visually process until you get used to. Most lines start with //, which serves no other purpose than to show the user which way his punch card is supposed to go into the reader - another throwback to archaic systems that kept getting grandfathered in up to the present day. Other than the first line, and anything starting with /*, and variable declarations (SET PRGNAME=HW), everything has three columns: a step name or file identifier, a statement or file definition, and an optional comment. Long statements that wrap to more than one line get indented on lines after the first; in other words if a line is missing a step name, it is most likely a continuation of the line above it.

This JCL is fairly well commented, and I just want to focus on a couple of lines, namely 6 and 100. These are the EXEC steps, saying what program to run. Lines after EXEC steps up to the next EXEC step define files the step might use, and any parameters the program being called may require. Line 6 runs IGYCRCTL, the COBOL compiler, and shows it where my source code is (in this case, it is being shown where a simple "Hello World" file I wrote is). Line 100 runs IEWL, a linker, which takes the object code from the compiler, and creates an executable file out of it.

Do what? Compiler? Linker? Executable file? Ultimately what programming boils down to is getting a computer processor to run commands from its instruction set. It can do math, it can store values, jump around in memory, read and write from peripherals including storage and input, write to the screen, etc. But it can't speak COBOL, nor can it speak C, BASIC, or Java. To further complicate things, each processor has different features and different command sets. A compiler is the first step in bridging the gap between a programming language and a processor command sets. It creates from source code an object file written a particular binary format - ELF, a.out, Mach-O, to name a few. Some binary formats are specific to a particular processor, some need some sort of interpreter and can run on multiple processors.

That's only half the battle, though. A final executable program may (and usually will) have more than one object file. Visual languages (VB, Visual C++, the .NET suite) will have one object file for each dialog box in the program, and possibly one or more object files with code shared between different dialogs. The linker strings all that together into a single program file, does some tricks with how to assign memory, and does some other hoodoo with symbol tables that I don't have a complete understanding of.

So, two steps to make a program: compile the source code, link the objects into an executable. Modern development suites do both without making you fuss over the details; not so in the mainframe world. Fortunately, I can do both in a single job, making my JCL file above much like a modern MAKEFILE in Unix.

a d curtis1.s.srclib(hw)                                                       

> APPLID(MYROSCOE)   USER(CID,CURTIS1)                                          
The above command attaches the dataset ("a d") that contains my COBOL program...
> APPLID(MYROSCOE)   USER(CID,CURTIS1)                                          
> DSN()               SCRL CSR  COLS 00001 00073                   LINE 000001  
> CURTIS1.S.SRCLIB(HW)                                                          
>      <...+....1....+....2....+....3....+....4....+....5....+....6....+....7...
=============================== T O P ================================= 
000001 000100 IDENTIFICATION DIVISION.                                          
000002 000200 PROGRAM-ID. HW.                                                   
000003 000300 PROCEDURE DIVISION.                                               
000004 000400 MAIN.                                                             
000005 000500     DISPLAY 'HELLO, WORLD.'.                                      
000006 000600     GOBACK.                                                       
============================ B O T T O M ============================== 

...and here is what it looks like. Odd. Madness. Everything is a sentence that ends with a period, and long descriptive words like "IDENTIFICATION" and "DISPLAY" are used, different sections of the code are in DIVISIONs. What's up with all that? Well, what does COBOL stand for? COmmon Business Oriented Language, the stress being on business-oriented. It was an early attempt to make code more accessible to management, a fruitless task that has been attempted a myriad of times of the last half century with no success. However, long COBOL programs really benefit from the structure and mandated indenting; all that makes it easier to visually scan code.

COBOL does math in an English-sounding way, too. "SUBTRACT 5 FROM NUMBER-OF-APPLES." that sort of thing. That syntax leads itself to a joke few people find funny, mainly because the setup takes too long for non-mainframers before they get it, and mainframers have no sense of humor to begin with. Another prerequisite is to understand that in C-based languages you can increment a number with a statement like "var_x++", which would add one to var_x. Finally, object-oriented languages started to sprout up like weeds a couple decades ago: C++, J++, and the like. So with all that setup, the joke:

Joe: "Hey Bob, have you heard of that new object-oriented COBOL?"
Bob: "No, what's that?"
Joe: "ADD 1 TO COBOL."

Thank you, thank you. Try the veal. Tip your waitress. I'm here all week. The joke became even less funny, if possible, in 2002 when a new COBOL standard came out that actually *does* support object-oriented structure.

> APPLID(MYROSCOE)   USER(CID,CURTIS1)                                          
------ -------- ------------------ - --- ---- --------- ---------- ---- --------
23370 CURTIS1C EXECUTING          Q   9 LPAR LOCAL     COMPILE             0.02

OK, so I submitted the JCL from way back to compile and link my code. The above is a display of my running job, including what step it is on, what priority on the mainframe it has (9 is pretty low, I have no authority on the mainframe, which is probably why no one has ever told me to quit screwing around on it) and what job number it has been assigned.

a j 23370

> APPLID(MYROSCOE)   USER(CID,CURTIS1)                                          
------ -------- ------------------ - --- ---- --------- ---------- ---- --------
23370 CURTIS1C AWAITING PRINT (H)     1  ANY LOCAL                             

A completed job looks like this, and I can attach the job output ("a j") to see if it ran successfully.

> APPLID(MYROSCOE)   USER(CID,CURTIS1)                                          
> JOB(CURTIS1C,23370) SCRL CSR  COLS 00001 00073                   F 01 P 0001  
>      <...+....1....+....2....+....3....+....4....+....5....+....6....+....7...
=============================== T O P ================================= 
000001                    J E S 2  J O B  L O G  --  S Y S T E M  C O C B  --  N
000003 10.32.57 JOB23370 ---- TUESDAY,   29 SEP 2009 ----                       
000004 10.32.57 JOB23370  IRR010I  USERID CURTIS1  IS ASSIGNED TO THIS JOB.     
000005 10.32.57 JOB23370  DTM4750I JOB CPU LIMITED TO 60:00                     
000006 10.32.57 JOB23370  ICH70001I CURTIS1  LAST ACCESS AT 10:22:52 ON TUESDAY,
000007 10.32.57 JOB23370  $HASP373 CURTIS1C STARTED - INIT 36   - CLASS Q - SYS 
000008 10.32.57 JOB23370  IEF403I CURTIS1C - STARTED - TIME=10.32.57            
000009 10.35.35 JOB23370  -                                         --TIMINGS (M
000010 10.35.35 JOB23370  -JOBNAME  STEPNAME PROCSTEP    RC   EXCP    CPU    SRB
000011 10.35.35 JOB23370  -CURTIS1C          COMPILE     00    473    .00    .00
000012 10.35.35 JOB23370  -CURTIS1C          LINK        00     72    .00    .00
000013 10.35.35 JOB23370  IEF404I CURTIS1C - ENDED - TIME=10.35.35              
000014 10.35.35 JOB23370  -CURTIS1C ENDED.  NAME-COBOLCOMPILE         TOTAL CPU 
000015 10.35.35 JOB23370  $HASP395 CURTIS1C ENDED                               
000016 ------ JES2 JOB STATISTICS ------                                        
000017   29 SEP 2009 JOB EXECUTION DATE                                         
000018            34 CARDS READ                                                 
000019           545 SYSOUT PRINT RECORDS                                       
000020             0 SYSOUT PUNCH RECORDS                                       
000021            29 SYSOUT SPOOL KBYTES                                        
000022          2.63 MINUTES EXECUTION TIME                                     
============================ B O T T O M ============================== 

This is a lot of mess, too, until you are familiar with what you're looking at. Lines 11 and 12 show the results of the COMPILE and LINK steps, and the return code (RC column) of 00 indicates that everything was fine. So now I should be able to run the program it compiled with some more JCL, this time a little simpler looking:

f cobrun                                                                       

> APPLID(MYROSCOE)   USER(CID,CURTIS1)                                          

This fetches the "COBRUN" library member without the fuss of doing the directory listing again.


> APPLID(MYROSCOE)   USER(CID,CURTIS1)                                          
> AWS(CID.COBRUN)     SCRL CSR  COLS 00001 00072                   A<ROS1>      
>       <...+....1....+....2....+....3....+....4....+....5....+....6....+....7..
...... ================================ T O P =================================
000001 //CURTIS1R  JOB (59000),'COBOLRUN',CLASS=Q,                             
000002 // MSGCLASS=H,REGION=4M                                                 
000003 /*ROUTE XEQ COLUMBUS                                                    
000004 /*ROUTE PRINT COLUMBUS                                                  
000005 //HELLO    EXEC PGM=HW                         PGM = LOADLIB MEMBER     
000007 //SYSOUT   DD   SYSOUT=H                           STDOUT               
...... ============================= B O T T O M ==============================

Much simpler. I could have also squeezed 3 lines out of it. Lines 1 and 2 could have been combined, and lines 3 and 4 just explicitly say where on the mainframe to run the program, and I don't need any constraints like that with this simple of a program. The rest just says what program to run, where it is, and what to do with the output.

The "sub" at the top is the function to actually submit the JCL.


> APPLID(MYROSCOE)   USER(CID,CURTIS1)                                          
> JOB(CURTIS1R,23457) SCRL CSR  COLS 00001 00073                   F 01 P 0001  
>      <...+....1....+....2....+....3....+....4....+....5....+....6....+....7...
=============================== T O P ================================= 
000001                    J E S 2  J O B  L O G  --  S Y S T E M  C O C B  --  N
000003 10.41.40 JOB23457 ---- TUESDAY,   29 SEP 2009 ----                       
000004 10.41.40 JOB23457  IRR010I  USERID CURTIS1  IS ASSIGNED TO THIS JOB.     
000005 10.41.40 JOB23457  DTM4750I JOB CPU LIMITED TO 60:00                     
000006 10.41.40 JOB23457  ICH70001I CURTIS1  LAST ACCESS AT 10:39:57 ON TUESDAY,
000007 10.41.40 JOB23457  $HASP373 CURTIS1R STARTED - INIT 36   - CLASS Q - SYS 
000008 10.41.40 JOB23457  IEF403I CURTIS1R - STARTED - TIME=10.41.40            
000009 10.41.41 JOB23457  -                                         --TIMINGS (M
000010 10.41.41 JOB23457  -JOBNAME  STEPNAME PROCSTEP    RC   EXCP    CPU    SRB
000011 10.41.41 JOB23457  -CURTIS1R          HELLO       00     34    .00    .00
000012 10.41.41 JOB23457  IEF404I CURTIS1R - ENDED - TIME=10.41.41              
000013 10.41.41 JOB23457  -CURTIS1R ENDED.  NAME-COBOLRUN             TOTAL CPU 
000014 10.41.41 JOB23457  $HASP395 CURTIS1R ENDED                               
000015 ------ JES2 JOB STATISTICS ------                                        
000016   29 SEP 2009 JOB EXECUTION DATE                                         
000017             9 CARDS READ                                                 

The job status shows that the HELLO step finished with no errors. The "sta" command (status) brings up all the outputs for the job.
> APPLID(MYROSCOE)   USER(CID,CURTIS1)                    J PENDING             
> STA(CURTIS1R,23457) SCRL CSR  COLS 00001 00079                                
ROSCOE ALTER/STATUS PROCESSOR                         

CURTIS1R  23457       1         1      1        1     262144           9       

(1)         (2) (3)              (4)      (5)                                  
A  FILE STA C  DEST       LINES FORM     CPY NOTES                            
_     1 NOP H LOCAL           21 STD      1   JES2.JESMSGLG                   
_     2 NOP H LOCAL            9 STD      1   JES2.JESJCL                     
_     3 NOP H LOCAL           17 STD      1   JES2.JESYSMSG                   
a     4 NOP H LOCAL            1 STD      1   HELLO.SYSOUT                    
=========================== END OF OUTPUT FILES ===========================    

Anything that starts with "JES" is system messages. "I'm running this job, it has this priority. Here is what step I'm on and what I have to load". I'm not interested in any of that, just the output of HELLO, file 4, which I'm attaching here.
> APPLID(MYROSCOE)   USER(CID,CURTIS1)                                          
> JOB(CURTIS1R,23457) SCRL CSR  COLS 00001 00073                   F 04 P 0001  
>      <...+....1....+....2....+....3....+....4....+....5....+....6....+....7...
=============================== T O P ================================= 
000001 HELLO, WORLD.                                                            
============================ B O T T O M ============================== 

Yep, that's it, all that work for a single fixed line of output. Why bother? Well, it's a ritual. You have to.

Getting that single line of output is the standard basic task that programmers do when they learn a new system. To achieve that simple line of output, you have to learn some basic syntax of the programming language (COBOL), the interface (3270) to the system the code will run on (z/OS, in this case), how to get the code to compile, and to execute. Lastly, you need to learn how to find and interpret the results (attaching jobs, finding the SYSOUT file).

In the process of getting your "Hello, world" to the screen, you begin to become a programmer and a systems analyst. Or if you already are one of those things and this is just a new language, or new compiler system, you become just a little bit better, seeing the view from the other side of the fence. And with the basics out of the way, now you can jump into doing "real" work with your new language.

So that's the mad world of mainframes. Arcane, frustrating, and a money-maker. Whenever I cash a paycheck, when I go home to my house, when I take my wife out to dinner, when I buy pretty clothes for my daughters, I'm pleased as hell that I invested the time into learning how to navigate them.


  1. Nice trip down memory lane. I actually liked ROSCOE better than TSO too. We left it behind when my first shop left their service bureau and bought their own system. I was a kid, but I remember thinking this about ISPF: "Numbered Menus? Eeeew, I have to remember those NUMBERS? Where's the SAVAWS? Give me ROSCOE."

  2. Actually, cobol is a very efficent language for handling lots of data. I always preferred IBM assembler, though, as pretty much nothing is transparent to you in the program. ISPF thru TSO is much more user friendly than ROSCOE, and even has dialogue management services where you can create panels to do just about anything you want to do.

  3. tears to my eyes!
    IBM 360/50 OS/MFT - Jumping from punch cards to programming Cobol using Roscoe on Raytheon PTS100 3270-terminal made the days great.

  4. Ahhh, yes! Realtime Operating System Conversation Operative Environment. Shockingly, Madison Area Technical College made mainframe assembler language mandatory in their computer science course a mere 20 years ago. ROSCOE was locked down so that I couldn't touch the JCL, but I could see it if I looked at pages I wasn't supposed to mess with. The assembler code we produced consisted of endlessly fiddling around with manually defined string buffers and twaddling the CPU registers to do any sort of arithmetic. Then we inserted a subroutine name that did all the real work. When our code got stuck in loops, and we got EBCDIC (Extended Binary Coded Decimal Interchange Code) HEX dumps, it was evident that the subroutine name inserted an order of magnitude more code than we were writing. Because of this "hands off" approach to teaching mainframe programming, I never actually learned how to read and write from standard IO devices.