Computers and AI will never be able to replace human programmers until they learn to laugh at the boss’s bad jokes.
You Should Use Environment Variables
I’ve noticed that not too many traditional IBM i programmers are using environment variables. I’ve been wondering why because I find them to be very useful. I’ve been asking around, and some of the responses I’ve been getting surprised me. Here are the three most common answers I’ve been receiving:
- I didn’t know about them! (Not surprising.)
- I’ve seen them but didn’t know I could make my own. (Surprising!)
- Aren’t they like system values? My boss won’t let me touch that stuff. (SHOCKING!!)
What are Environment Variables?
They are variables that are scoped to each job on the system. (There are also system-level variables, but more about that later…) They allow you to define variables and assign values that persist between the various programs that are called within that job. They provide a great way to store data in the job that might be used by a subsequent routine.
All operating systems that I’ve worked on have them in one form or another. This includes Windows, MacOS and Unix/Linux, where they are a core tool that’s used very widely. On IBM i, we have traditionally used data areas in the QTEMP library instead. (Or, for those that are very old-school, perhaps job switches, or the LDA.)
For example, in a Windows Command Prompt, you could type the following:
dir > %OUTFILE%
This sets an environment variable named OUTFILE to a filename, then when %OUTFILE% appears at the command line, the contents of the variable is inserted, in this case to output a directory listing to that file.
It’s important to note that there’s nothing special about the name “OUTFILE” – it is purely a name I made up just now when trying to think of a very simple and easy to understand example. When I set the name with the “set” command, it will automatically create a variable if it doesn’t exist. Or, if it does exist, it’ll update the existing variable’s value.
It’s also important to understand that environment variables are case-sensitive. OUTFILE, outfile and Outfile are 3 different variables because their names are capitalized differently. Personally, I like to code environment variables in all uppercase – this way I won’t have problems with the upper/lowercase names not matching. Also, I like that the all-uppercase names stand out.
On Unix, Linux, MacOS, QShell or PASE the syntax is a little different, but it does the same thing. For example:
ls -l > $OUTFILE
Like the last example, this creates a variable named OUTFILE if it doesn’t exist and sets it to /tmp/output_file.txt. If the OUTFILE variable did exist, this would’ve updated its value to /tmp/output_file.txt.
Using Environment Variables in the CL Environment
IBM provides three commands in the traditional (CL) command environment for working with environment variables. They are:
- ADDENVVAR – add (or replace) and environment variable.
- CHGENVVAR – change an existing environment variable.
- RMVENVVAR – remove an existing environment variable.
I find it a little awkward to use a separate command for changing a variable vs. adding a new one. Thankfully, the ADDENVVAR command has a REPLACE(*YES) option, so I don’t need to use the CHGENVVAR command.
All of the environments (Windows, Unix-like, IBM i) have ways to remove environment variables, but most of the time there’s little reason to remove existing variables, so I will leave it as an exercise for the reader.
With that in mind, I could do the same process from CL like this:
ADDENVVAR ENVVAR(OUTFILE) VALUE(‘/tmp/output_file.txt’) REPLACE(*YES)
QSH CMD(‘ls -l > $OUTFILE’)
Sadly, there isn’t an easy way to insert the value of a variable into a CL command like there is with Unix-like environments, so I needed to use QShell to demonstrate listing files to the file referenced in the OUTFILE variable.
Using Environment Variables in RPG
I hope it’s clear that an environment variable can have any name, and contain any data you like (though, they are always character strings, so if you wanted to use it for a number, for example you’d have to convert it to/from character. That’s usually easy to do.)
That said, I will stick with the same basic example of changing where output is going, just because it’s very simple to code and understand. In this case, suppose we have an RPG program that writes to a physical file and we want to add environment variable support.
Here’s the “before” version of the RPG – it simply writes some test data to a file called QTEMP/OUTPUT:
ctl-opt dftactgrp(*no) option(*srcstmt);
dcl-f OUTPUT disk(100) usage(*output) extfile(outfile) usropn;
dcl-pr qcmdexc extpgm(‘QSYS/QCMDEXC’);
command char(32767) const options(*varsize);
length packed(15: 5) const;
igc char(3) const options(*nopass);
dcl-s cmd varchar(200);
dcl-ds rec len(100) end-ds;
dcl-s outfile varchar(21) inz(‘QTEMP/OUTPUT’);
cmd = ‘DLTF FILE(‘ + outfile + ‘)’;
callp(e) qcmdexc(cmd: %len(cmd));
cmd = ‘CRTPF FILE(‘ + outfile + ‘) RCDLEN(100)’;
rec = ‘TEST DATA HERE’;
write OUTPUT rec;
*inlr = *on;
This program is simple, it deletes a file named QTEMP/OUTPUT (ignoring an error if it doesn’t exist) then creates a file with the same name and writes a string that says ‘TEST DATA HERE’ to that file. My next goal is to see if the OUTFILE environment variable is set. If it is set, then it should contain the name of the file to write to (instead of using QTEMP/OUTPUT.)
IBM has actually provided 3 or 4 different system APIs for retrieving environment variables, and I’m not going to try to demonstrate all of them, but will use the one that I find the simplest, which is getenv(). This is also the same routine you’d typically call from C/C++ programs to retrieve environment variables (both on IBM i and other platforms.)
The prototype to call getenv() looks like this:
dcl-pr getenv pointer extproc(*dclcase);
varname pointer value options(*string);
Notice that it returns a pointer. This will point to a C-style (null-terminated) character string. I can use the %STR BIF in RPG to convert it to an RPG-style CHAR or VARCHAR string, however. The other purpose of the pointer is that it will be set to *NULL if the variable isn’t found. Therefore, I can write code to check for OUTFILE a follows:
dcl-s varptr pointer;
dcl-s outfile varchar(21) inz(‘QTEMP/OUTPUT’);
varptr = getenv(‘OUTFILE’);
if varptr <> *NULL;
outfile = %str(varptr);
varptr is the pointer returned from getenv. If it is null, I’m not changing my RPG outfile string, I’m leaving it set to its original value (QTEMP/OUTPUT). However, if it isn’t null, that means that someone set the OUTFILE environment variable. In that case, I use %STR to retrieve the value that it’s set to. When the program proceeds to create the file and put test data into it, it’ll use the filename that was set in the OUTFILE environment variable.
I suspect some readers are seeing this and saying “so what? I could do that with OVRDBF!” – it’s important to understand that this was just meant as a simple example – you can use environment variables for just about any purpose you dream up, it doesn’t have to be used to control file output.
dcl-s taxrate packed(7: 3);
dcl-s varptr pointer;
taxrate = 1.09;
varptr = getenv(‘TAXRATE’);
if varptr <> *null;
taxrate = %dec(taxrate:7:3);
In this case, my program has a tax rate coded into it, but that rate might change. The caller can control the tax rate that the program uses by typing:
ADDENVVAR ENVVAR(TAXRATE) VALUE(‘1.11’)
Then when he/she calls the above RPG program, it’ll use 1.11 as the tax rate.
RPG can set environment variables as well as retrieve them, of course. This is done with the putenv() routine:
dcl-pr putenv int(10) extproc(*dclcase);
varname pointer value options(*string);
The putenv() API takes one parameter, a character string that tells it the name of the variable, followed by an equal sign (=), followed by whatever string is to be assigned to the variable. It works the same as ADDENVVAR with REPLACE(*YES), if the variable doesn’t exist, it’ll create it, and if it does exist, it’ll replace it.
In this case, the TAXRATE variable is set to 1.1, and then it calls the program that uses that tax rate to calculate an invoice.
The CL equivalent would be as follows:
ADDENVVAR ENVVAR(TAXRATE) VALUE(‘1.11’) REPLACE(*YES)
CALL PGM(CALCINV) PARM(&INVNO)
Personally, I find the putenv() routine to be nicer to work with than ADDENVVAR. It just feels simpler and cleaner to me.
Retrieving Environment Variables in CL
If you were paying attention, you may have noticed that IBM didn’t provide an easy way to retrieve the value of an environment variable in CL. They only commands to set, change or remove an environment variable, which frustrates me more than you’ll ever know.
Naturally, since ILE procedures can be called from any ILE language, the getenv() routine that we used from RPG will also work from CL. However, CL doesn’t have an equivalent of the OPTIONS(*STRING) keyword and %STR built-in function that RPG provides to convert from C-style (null-terminated) strings. This means we have to do a little mucking about to add the null character when appropriate, and to determine the proper length of a null-terminated string, we can call the __strlen() function.
That makes it awkward to get a variable in CL by calling the ILE procedure directly. But, we can use it to write a CL command so that it only needs to be coded once.
Here’s the command I came up with (I wrote this back in 2010) for the RTVENVVAR CL command:
/* RETRIEVE ENVIRONMENT VARIABLE (RTVENVVAR) COMMAND */
/* SCOTT KLEMENT, FEBRUARY 11, 2010 */
/* TO COMPILE: */
/*> CRTBNDCL RTVENVVAR SRCFILE(QCLSRC) DBGVIEW(*LIST) <*/
/*> CRTPNLGRP RTVENVVAR SRCFILE(QPNLSRC) <*/
/*> CRTCMD RTVENVVAR PGM(RTVENVVAR) – <*/
/*> ALLOW(*IPGM *BPGM *IMOD *BMOD) – <*/
/*> HLPPNLGRP(RTVENVVAR) HLPID(RTVENVVAR) <*/
CMD PROMPT(‘Retrieve Environment Variable’)
PARM KWD(ENVVAR) TYPE(*CHAR) LEN(128) +
MIN(1) EXPR(*YES) CASE(*MIXED) +
PARM KWD(VALUE) TYPE(*X) VARY(*YES) +
RTNVAL(*YES) MIN(1) +
PROMPT(‘CL VAR for returned value’)
Here’s the corresponding CPP code:
/* This is the CPP for the Retrieve Environment Variable */
/* (RTVENVVAR) command. It is used to retrieve the value */
/* of an environment variable from a CL program. */
/* Scott Klement, February 11, 2010 */
PGM PARM(&ENVVARV &RCVVAR)
DCL VAR(&ENVVARV) TYPE(*CHAR) LEN(128)
DCL VAR(&ENVVAR) TYPE(*CHAR) LEN(129)
DCL VAR(&RCVVAR) TYPE(*CHAR) LEN(32767)
DCL VAR(&RCVLEN) TYPE(*DEC) LEN(5 0)
DCL VAR(&NULLPTR) TYPE(*PTR)
DCL VAR(&NULL) TYPE(*CHAR) LEN(1) VALUE(x’00’)
DCL VAR(&PTR) TYPE(*PTR)
DCL VAR(&DATA) TYPE(*CHAR) LEN(32767) +
DCL VAR(&MSGDTA) TYPE(*CHAR) LEN(140)
DCL VAR(&LEN) TYPE(*UINT) LEN(4)
CHGVAR VAR(&RCVLEN) VALUE(%BIN(&RCVVAR 1 2))
CHGVAR VAR(&ENVVAR) VALUE(&ENVVARV *TCAT &NULL)
CALLPRC PRC(‘getenv’) PARM(&ENVVAR) RTNVAL(&PTR)
IF (&PTR *EQ &NULLPTR) DO
CALLPRC PRC(‘__strlen’) PARM(&ENVVAR) RTNVAL(&LEN)
CHGVAR VAR(%BIN(&MSGDTA 1 4)) VALUE(0)
CHGVAR VAR(%SST(&MSGDTA 5 4)) VALUE(*JOB)
CHGVAR VAR(%BIN(&MSGDTA 9 4)) VALUE(&LEN)
CHGVAR VAR(%SST(&MSGDTA 13 128)) VALUE(&ENVVARV)
SNDPGMMSG MSGID(CPFA981) MSGF(QCPFMSG) MSGTYPE(*ESCAPE) +
CALLPRC PRC(‘__strlen’) PARM((&PTR *BYVAL)) RTNVAL(&LEN)
IF (&LEN *EQ 0) DO
CHGVAR VAR(%SST(&RCVVAR 3 &RCVLEN)) VALUE(‘ ‘)
IF (&LEN *GT 32765) DO
CHGVAR VAR(&LEN) VALUE(32765)
IF (&LEN *GT &RCVLEN) DO
CHGVAR VAR(&LEN) VALUE(&RCVLEN)
CHGVAR VAR(%SST(&RCVVAR 3 &LEN)) +
VALUE(%SST(&DATA 1 &LEN))
With this command, I can now retrieve variables in a CL program quite easily.
DCL VAR(&TAXRATE) TYPE(*DEC) LEN(7 3) VALUE(1.09)
DCL VAR(&CHARRATE) TYPE(*CHAR) LEN(30)
DCL VAR(&VARFOUND) TYPE(*LGL)
CHGVAR &VARFOUND ‘1’
RTVENVVAR ENVVAR(‘TAXRATE’) VALUE(&CHARRATE)
MONMSG CPFA981 EXEC(DO)
CHGVAR &VARFOUND ‘0’
IF (&VARFOUND *EQ ‘1’) DO
CHGVAR &TAXRATE VALUE(%DEC(&CHARRATE 7 3))
If the RTVENVVAR command sounds useful to you, you may want to download it from my web site. It is easier than manually retyping the preceding code, and also includes the command help. You’ll find it here: https://www.scottklement.com/rtvenvvar/
System-Level Environment Variables
I mentioned earlier that there are also system-level environment variables. You may be thinking that these are variables that are set system-wide instead of being scoped to a job – that’s not exactly true, as I’ll explain in a moment.
System-level variables are a unique feature of IBM i that you won’t see on other platforms.
The concept: When you first use environment variables within your job, it needs to know what variables should be set and what their initial values are. Other platforms have start up scripts and similar options that can be used to set up the variables users will want within their jobs, and give them the right values for your particular organization, system or application. On IBM i, we have system-level variables instead. The idea is that when you set a variable at the system-level, it becomes the “initializer” for a job variable of the same name.
The first time you use environment variables in your job, all of the system variables are copied to be job-level variables with the same values. Once copied, it won’t access the system-level variables again (unless you use a set of APIs explicitly designed for the system level ones, that is.)
For example, perhaps I want all users to have a PATH environment variable that includes both the QShell commands (which are located in /usr/bin) and the PASE ones (which are typically in /QOpenSys/usr/bin and /QOpenSys/pkgs/bin.) I can set a system-level PATH variable that includes them:
ADDENVVAR ENVVAR(PATH) VALUE(‘/usr/bin:/QOpenSys/pkgs/bin:/QOpenSys/usr/bin’)
This is now set system-wide because I specified LEVEL(*SYS). But what happens if a user changes (or for that matter, a program) changes PATH within their job?
When they first use variables, it’ll be copied to be at the job level for the user. When they change it, however (unless they specify LEVEL(*SYS) or use special APIs) it will be changed only for their job. That means you can have a system-wide setting, but users can still override it within their own job if they want, which is very useful.
Similarly, when QShell or PASE starts up within a job, they’ll copy all of the job’s environment variables into their shell environment (which in turn may have come from the system-level ones.) But changes made within the QShell or PASE environment will only affect that copy of QShell or PASE, they will not cause them to be changed in the parent IBM i environment that called QShell or PASE.
Do You Get It?
Hopefully after reading this, you understand why I like environment variables so much. They are so much more versatile than data areas! They can also have much more meaningful names! Programs/jobs can change them for their own purposes and use them without affecting other programs. Developers can change them for testing purposes without affecting other developers or production users. You simply can’t do all of that with standard data areas. You could achieve a similar result with LDA or job switches, but these don’t have meaningful names and are shared by all programs, which makes them difficult to use without programs having conflicting uses. That’s never a problem with environment variables because you can name them whatever you like. We need to convince more people to use them!
Midrange Dynamics Development & Solutions Architect
Scott Klement is an IT professional with a passion for both programming and mentoring. He joined Midrange Dynamics at the beginning of October 2022. He formerly was the Director of Product Development and Support at Profound Logic.
Logic and the IT Manager and Senior Programmer at Klement’s Sausage Co., Inc. Scott also serves on the Board of Directors of COMMON, where he represents the Education, Innovation, and Certification teams. He is an IBM Champion for Power Systems.
Subscribe to our newsletter and join us next month to see what is happening in Scott’s Corner. Add a great dad joke to your arsenal and gain an even better IT insight from this recognized industry expert as he continues his quest to educate and support the IBM i community.