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) > ....+....1....+....2....+....3....+....4....+....5....+....6....+....7....+....8 ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ||||||||| 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) > >....+....1....+....2....+....3....+....4....+....5....+....6....+....7....+....
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.
a > APPLID(MYROSCOE) USER(CID,CURTIS1) > >....+....1....+....2....+....3....+....4....+....5....+....6....+....7....+....
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
ls > 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 ".*".
-- 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 99-bottles-of-beer.net, 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:
<<99bottles>> trap delete 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' endsel endloop return : 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:
<<bot99>> trap delete 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!' else if l1 gt 0 write aws * l1 | l2 | ' of beer on the wall.' else write aws * 'No more bottles of beer on the wall!' endif endif endloop return
...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 ><...+....1....+....2....+....3....+....4....+....5....+....6....+....7....+.... 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' 000008 //STEPLIB DD DSNAME=SYS1.COB2COMP,DISP=SHR WHERE TO FIND IT 000009 //SYSIN DD DSN=CURTIS1.S.SRCLIB(&PRGNAME.), SOURCE CODE 000010 // DISP=SHR 000011 //SYSLIN DD DSN=CURTIS1.S.OBJLIB(&PRGNAME.), OBJECT OUTPUT 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' 000300 //SYSLIB DD DSN=SYS1.COB2LIB,DISP=SHR SYSTEM LIBRARIES 000400 // DD DSN=SYS1.COB2COMP,DISP=SHR 000500 //SYSLIN DD DSN=CURTIS1.S.OBJLIB(&PRGNAME.), OBJECT INPUT 000600 // DISP=SHR 000700 //SYSLMOD DD DSN=CURTIS1.S.LOADLIB(&PRGNAME.), LOAD LIBRARY OUTPUT 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) > >....+....1....+....2....+....3....+....4....+....5....+....6....+....7....+....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) > ....+....1....+....2....+....3....+....4....+....5....+....6....+....7....+....8 JOBNO JOBNAME STATUS C PTY SID ORIGIN STEP/LINES HELD CPU TIME ------ -------- ------------------ - --- ---- --------- ---------- ---- -------- 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) > ....+....1....+....2....+....3....+....4....+....5....+....6....+....7....+....8 JOBNO JOBNAME STATUS C PTY SID ORIGIN STEP/LINES HELD CPU TIME ------ -------- ------------------ - --- ---- --------- ---------- ---- -------- 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 000002 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) > >....+....1....+....2....+....3....+....4....+....5....+....6....+....7....+....
This fetches the "COBRUN" library member without the fuss of doing the directory listing again.
sub > 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 000006 //STEPLIB DD DSN=CURTIS1.S.LOADLIB,DISP=SHR LOAD LIBRARY 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.
sta > 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 000002 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 >....+....1....+....2....+....3....+....4....+....5....+....6....+....7....+.... ROSCOE ALTER/STATUS PROCESSOR JOB NAME NO FILE LINE PAGE/ LINE FIND LIMIT I/O COUNT 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.