SDSF ST via REXX - mainframe

I've got an interesting idea. I want to see JCL in SDSF via REXX.
Currently I can see necessary job names, using:
Address SDSF "ISFEXEC ST"
Maybe anybody has some idea of what to add to my script to make analog of:
command s
And totally get the same output

Using SDSF with the REXX programming language - Examples of REXX execs, as per the comment, has example code such as:-
List action characters
Set the ISFACTIONS special variable to ON, which causes the action characters to be returned in the ISFRESP variables. Then access the ST panel, and list the valid action characters for that panel.
/* REXX */
rc=isfcalls('ON')
/* Set isfactions special variable to */
/* the equivalent of SET ACTION ON */
isfactions="ON"
/* Invoke the ST panel */
Address SDSF "ISFEXEC ST"
if rc<>0 then
Exit rc
/* List each of the valid action characters */
/* for the panel. */
Say "Actions valid on the panel are:"
do ix=1 to isfresp.0
Say " " isfresp.ix
end
rc=isfcalls('OFF')
and
Access an SDSF panel
Access the ST panel, then list the column variables.
/* REXX */
rc=isfcalls('ON')
/* Access the ST panel */
Address SDSF "ISFEXEC ST"
if rc<>0 then
Exit rc
/* Get fixed field name from first word */
/* of isfcols special variable */
fixedField = word(isfcols,1)
Say "Number of rows returned:" isfrows
/* Process all rows */
do ix=1 to isfrows
Say "Now processing job:" value(fixedField"."ix)
/* List all columns for row */
do jx=1 to words(isfcols)
col = word(isfcols,jx)
Say " Column" col"."ix "has the value:" value(col"."ix)
end
end
rc=isfcalls('OFF')
An IBM Red Book, a PDF Downlaod, Implementing Rexx Support in SDSF may also be of use.

Related

Is it possible to map CDialog RADIO controls to an enum class object instead of int?

I have a standard dialog resource which has some radio controls on it.
At the moment it is all done the normal way so the first radio is mapped to a int variable.
DDX_Radio(pDX, IDC_RADIO_DISPLAY_EVERYONE, m_iDisplayMode);
DDX_Radio(pDX, IDC_RADIO_SELECT_EVERYONE, m_iSelectMode);
Here is the thing ... I have these associated enumerations:
enum class DisplayMode { Everyone = 0, Brother, Sister };
enum class SelectMode { Everyone = 0, Elders, MinisterialServants, Appointed, Custom, None };
Therefore, whenever I need to do some comparisons of the mapped variable I have to do it like this:
Example 1:
m_iDisplayMode = to_underlying(DisplayMode::Everyone);
m_iSelectMode = to_underlying(SelectMode::None);
Example 2:
if (m_iDisplayMode == to_underlying(DisplayMode::Everyone))
bInclude = true;
else if (m_iDisplayMode == to_underlying(DisplayMode::Brother) && mapPublisher.second.eGender == Gender::Male)
bInclude = true;
else if (m_iDisplayMode == to_underlying(DisplayMode::Sister) && mapPublisher.second.eGender == Gender::Female)
bInclude = true;
The to_underlying function is a helper function that has been suggested previously to me here on SO and has been invaluable:
template <typename E>
constexpr auto to_underlying(E e) noexcept
{
return static_cast<std::underlying_type_t<E>>(e);
}
What I want to know is whether it is possible to map those radio controls directly to the DisplayMode or SelectMode objects? So instead of mapping to 1 etc. it maps to DisplayMode::Everyone etc. This would simplfy the code in this context and avoid the need for all the to_underlying calls.
This is the MFC source for DDX_Radio:
void AFXAPI DDX_Radio(CDataExchange* pDX, int nIDC, int& value)
// must be first in a group of auto radio buttons
{
pDX->PrepareCtrl(nIDC);
HWND hWndCtrl;
pDX->m_pDlgWnd->GetDlgItem(nIDC, &hWndCtrl);
ASSERT(::GetWindowLong(hWndCtrl, GWL_STYLE) & WS_GROUP);
ASSERT(::SendMessage(hWndCtrl, WM_GETDLGCODE, 0, 0L) & DLGC_RADIOBUTTON);
if (pDX->m_bSaveAndValidate)
value = -1; // value if none found
// walk all children in group
int iButton = 0;
do
{
if (::SendMessage(hWndCtrl, WM_GETDLGCODE, 0, 0L) & DLGC_RADIOBUTTON)
{
// control in group is a radio button
if (pDX->m_bSaveAndValidate)
{
if (::SendMessage(hWndCtrl, BM_GETCHECK, 0, 0L) != 0)
{
ASSERT(value == -1); // only set once
value = iButton;
}
}
else
{
// select button
::SendMessage(hWndCtrl, BM_SETCHECK, (iButton == value), 0L);
}
iButton++;
}
else
{
TRACE(traceAppMsg, 0, "Warning: skipping non-radio button in group.\n");
}
hWndCtrl = ::GetWindow(hWndCtrl, GW_HWNDNEXT);
} while (hWndCtrl != NULL &&
!(GetWindowLong(hWndCtrl, GWL_STYLE) & WS_GROUP));
}
I am trying to use the code in the answer but get this error:
MFC supports mapping between data (class members) and UI state. The standard mechanism is called Dialog Data Exchange (DDX) which the code in the question is using already (DDX_Radio). The data exchange is two-way, triggered by a call to UpdateData, where an argument of TRUE translates the UI state into values, and FALSE reads the associated values and adjusts the UI appropriately.
There are a number of standard dialog data exchange routines provided by MFC already, but clients can provide their own in case none of them fit the immediate use case. The question falls into this category, and conveniently provides the implementation of DDX_Radio to serve as a starting point.
The implementation looks a fair bit intimidating, though things start to make sense once the code has been augmented with a few comments here and there:
CustomDDX.h:
template<typename E>
void AFXAPI DDX_RadioEnum(CDataExchange* pDX, int nIDC, E& value)
{
// (1) Prepare the control for data exchange
pDX->PrepareCtrl(nIDC);
HWND hWndCtrl;
pDX->m_pDlgWnd->GetDlgItem(nIDC, &hWndCtrl);
// (2) Make sure this routine is associated with the first
// radio button in a radio button group
ASSERT(::GetWindowLong(hWndCtrl, GWL_STYLE) & WS_GROUP);
// And verify, that it is indeed a radio button
ASSERT(::SendMessage(hWndCtrl, WM_GETDLGCODE, 0, 0L) & DLGC_RADIOBUTTON);
// (3) Iterate over all radio buttons in this group
using value_t = std::underlying_type_t<E>;
value_t rdbtn_index {};
do {
if (::SendMessage(hWndCtrl, WM_GETDLGCODE, 0, 0L) & DLGC_RADIOBUTTON) {
// (4) Control is a radio button
if (pDX->m_bSaveAndValidate) {
// (5) Transfer data from UI to class member
if (::SendMessage(hWndCtrl, BM_GETCHECK, 0, 0L) != 0) {
value = static_cast<E>(rdbtn_index);
}
} else {
// (6) Transfer data from class member to UI
::SendMessage(hWndCtrl, BM_SETCHECK,
(static_cast<E>(rdbtn_index) == value), 0L);
}
++rdbtn_index;
} else {
// (7) Not a radio button -> Issue warning
TRACE(traceAppMsg, 0,
"Warning: skipping non-radio button in group.\n");
}
// (8) Move to next control in tab order
hWndCtrl = ::GetWindow(hWndCtrl, GW_HWNDNEXT);
}
// (9) Until there are no more, or we moved to the next group
while (hWndCtrl != NULL && !(GetWindowLong(hWndCtrl, GWL_STYLE) & WS_GROUP));
}
This declares a function template that can be instantiated for arbitrary scoped enum types, and implements the logic to translate between UI state and enum value. The integral underlying value of the enum serves as the zero-based index into the radio button group selection.
The implementation needs a bit of explanation, though. The following list provides a bit more information regarding the numbered // (n) code comments:
This initializes internal state used by the framework. The precise details aren't very important, as long as the correct function is called. There are 3 implementations, one for OLE controls, one for edit controls, and one for every thing else. We're in the "everything else" category.
Perform sanity checks. This verifies that the control identified by nIDC is the first control in a radio button group (WS_GROUP), and that it is indeed a radio button control. This helps weed out bugs early when running a debug build.
Initialize the radio button index counter (rdbtn_index), and start iterating over radio buttons.
Make sure the control we're operating on in this iteration is a radio button control (if not, see 7.).
When translating UI state back to member variables, verify whether the current control is checked, and store its index in the group as a scoped enum value.
Otherwise (i.e. when translating data to UI state) set the check mark if the numeric value of the enum matches the control index, and uncheck it otherwise. The latter is not strictly required when using BS_AUTORADIOBUTTON controls, but it's not harmful either.
If we encounter a control that isn't a radio button control, issue a warning. Closely watch the debug output for this message; it designates a bug in the dialog template. Make sure to set the WS_GROUP style on the first control following this radio button group (in tab order).
Move on to the next control in tab order.
Terminate the loop if either there is no trailing control, or the control starts a new group, designated by the WS_GROUP style.
That's a fair bit to digest. Luckily, use of this function template is far less cumbersome. For purposes of illustration, let's use the following scoped enums:
enum class Season {
Spring,
Summer,
Fall,
Winter
};
enum class Color {
Red,
Green,
Blue
};
and add the following class members to the dialog class:
private:
Season season_ {};
Color color_ { Color::Green };
All that's left is setting up the DDX associations, i.e.:
void CRadioEnumDlg::DoDataExchange(CDataExchange* pDX) {
CDialogEx::DoDataExchange(pDX);
DDX_RadioEnum(pDX, IDC_RADIO_SPRING, season_);
DDX_RadioEnum(pDX, IDC_RADIO_RED, color_);
}
(with CRadioEnumDlg deriving from CDialogEx). All the template machinery is neatly hidden, with the template type argument getting inferred from the final argument.
For completeness, here is the dialog template used:
IDD_RADIOENUM_DIALOG DIALOGEX 0, 0, 178, 107
STYLE DS_SETFONT | DS_FIXEDSYS | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME
EXSTYLE WS_EX_APPWINDOW
FONT 8, "MS Shell Dlg", 0, 0, 0x1
BEGIN
DEFPUSHBUTTON "OK",IDOK,59,86,50,14
PUSHBUTTON "Cancel",IDCANCEL,121,86,50,14
CONTROL "Spring",IDC_RADIO_SPRING,"Button",BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,7,7,39,10
CONTROL "Summer",IDC_RADIO_SUMMER,"Button",BS_AUTORADIOBUTTON,7,20,39,10
CONTROL "Fall",IDC_RADIO_FALL,"Button",BS_AUTORADIOBUTTON,7,33,39,10
CONTROL "Winter",IDC_RADIO_WINTER,"Button",BS_AUTORADIOBUTTON,7,46,39,10
CONTROL "Red",IDC_RADIO_RED,"Button",BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,54,7,39,10
CONTROL "Green",IDC_RADIO_GREEN,"Button",BS_AUTORADIOBUTTON,54,20,39,10
CONTROL "Blue",IDC_RADIO_BLUE,"Button",BS_AUTORADIOBUTTON,54,33,39,10
END
as well as its accompanying resource.h:
#define IDD_RADIOENUM_DIALOG 102
#define IDC_RADIO_SPRING 1000
#define IDC_RADIO_SUMMER 1001
#define IDC_RADIO_FALL 1002
#define IDC_RADIO_WINTER 1003
#define IDC_RADIO_RED 1004
#define IDC_RADIO_GREEN 1005
#define IDC_RADIO_BLUE 1006
Adjusting a default-generated MFC application (dialog-based) with the above produces the following result when launched:
That's sweet, actually. Note in particular that the second row of radio buttons has the second item checked, which matches the initial value set in the dialog class' implementation (Color color_ { Color::Green }).
So all's good then?
Well, yeah. I guess. Sort of, anyway. Let's talk about the things that aren't quite as cool, things to watch out for, and problems that simply don't have a solution.
The implementation provided above makes a number of assumptions, none of which can be verified at compile time, and only some of them can (and are) verified at run time:
Enum values need to be backed by integral values, starting at 0, and counting up without any gaps. To my knowledge, there's no way to enforce this today (C++20), and the most effective way to ensure this is a code comment.
The order of enum values must match the tab order of radio button controls. Again, this is nothing that can be enforced nor verified.
The control ID specified in the DDX_RadioEnum call must be the start of a radio button group. This is verified at run time (the first ASSERT).
The control ID specified in the DDX_RadioEnum call must identify a radio button control. Again, this is verified at run time (the second ASSERT).
The first control following the radio button group (in tab order) must have the WS_GROUP style set. This is verified at run time, in part. If the control following is not a radio button control, a warning is issued. If the control happens to be a radio button, then this is not something that can be verified.
Those assumptions certainly aren't impossible to match. The hard part is keeping those invariants valid over time. If you can, then this implementation is worth a try.

How to display default value from prompt macro in value prompt CA11?

I was wondering if it is possible to display the default value from a prompt macro in a Value prompt. My prompt macro looks like this "#prompt('pMonth','MUN','[Previous Month]')#"
so my goal in the value prompt would be to have 202103 displayed instead of header text name which I have named "Previous Month"
I tried with an old javascript from Cognos 10 where you desc the Months and specify what index it should pick but the issue with that code is that everytime you try to change to a different month the report re-runs and loops back to to the same Index value.
<script>
var theSpan = document.getElementById("A1");
var a = theSpan.getElementsByTagName("select"); /* This stmt return an array of all value prompts within span */
for( var i = a.length-1; i >= 0; i-- ) /* now loop through the elements */
{ var prompts = a[i];
if( prompts.id.match(/PRMT_SV_/))
{ prompts.selectedIndex = 2; } /* This selects the second options from top */
canSubmitPrompt();
}
</script>
All solutions, tips and ideas are highly appreciated.
Best regards,
Rubrix
For Cognos Analytics, running with full interactivity, you probably need a Page Module. Check out IBM's Scriptable Reports documentation for Cognos Analytics. You'll want to craft your script to use the current parameter value as default (if you can get it), then fail over to your default value from the data. You will probably need to integrate this with a Custom Control to be able to get that default value from the data.

Reusableforms Set from email to posted email

I have implemented Reusableforms on my site, everything is working fine apart from when I view received emails in the client, the emails are always from 'contact form' with the email 'forms#domain.com'.
How can I change this to display the senders name and email that is posted via the form? Here is the code from the handler.php included with reusableforms.
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
/*
Tested working with PHP5.4 and above (including PHP 7 )
*/
require_once './vendor/autoload.php';
use FormGuide\Handlx\FormHandler;
$pp = new FormHandler();
$validator = $pp->getValidator();
$validator->fields(['Name','Email'])->areRequired()->maxLength(50);
$validator->field('Email')->isEmail();
$validator->field('Message')->maxLength(6000);
$pp->requireReCaptcha();
$pp->getReCaptcha()->initSecretKey('0000000000000000000000000000000');
$pp->sendEmailTo('orders#domain'); // ← Your email here
echo $pp->process($_POST);
I too been looking for help on this very subject.
I have got this to work - by chance - by having two instances of
'$pp = new FormHandler();'.
See my version of the code below.
I do not understand why it works.
<?php
require_once './vendor/autoload.php';
use FormGuide\Handlx\FormHandler;
$pp = new FormHandler();
/* The next 7 lines must be in this 1st section */
$mailer = $pp->getMailer();
$mailer->IsSMTP();
$mailer->SMTPAuth = true;
$mailer->SMTPSecure ="ssl";
$mailer->Host = "serverxxx.xxxx.com";
$mailer->Username = "contact#xxxxxxx.com";
$mailer->Password = "xxxxxxxxxxxxxx";
/* The next 2 lines work whether in the 1st or 2nd section */
$pp->requireReCaptcha();
$pp->getReCaptcha()->initSecretKey('xxxxxxxxxxxx');
/* Section 2
I do not understand it, but without the 2nd '$pp = new FormHandler()' below,
the SMTP send fails. The reported error is:
Sorry there was an error sending your form.
mail:SMTP connect() failed.
https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting
I have tested changing the 2nd $pp to $pp2 and it also works.
*/
$pp = new FormHandler();
/* The next line has to be in this 2nd section.
If it is the last line in top section then it appears to be sent OK
but the email does not actually arrive.
Checking the SMTP logs shows it was never sent.*/
$pp->sendEmailTo(['leslie#xxxxxxxxxxx.com']);
/* The next 2 lines also must be here rather than in 1st section above. */
$mailer = $pp->getMailer();
$mailer->setFrom('contact#xxxxxxx.com','xxxxxxx Contact Form');
echo $pp->process($_POST);
To send a reply to the user email address on a ReusableForm, you'll need to get the value from the handler.php and add that to FormHandler.php above the 'setFrom' function using the addReplyTo function & $_POST global variable:
$this->mailer->addReplyTo($_POST['Email']);
$this->mailer->setFrom($from_email,'Contact Form',false);
These forms still work well in 2021 using PHP 7.3

KeSetSystemAffinityThread behavior

Some questions about KeSetSystemAffinityThread function, since MSDN is quite laconic.
NOTE: I can't use the more complete KeSetSystemAffinityThreadEx because I must still support Windows XP.
1) How can I restore the previous affinity? The function does not return the old value, how can I obtain it?
2) Is it true that passing 0 to the function restores the default system affinity? I have found such assertion in some forums, but I can't find it in official MS documentation.
3) Is the new thread's system affinity mask maintained after a return to user mode, or is it restored to the default each time the thread enters in system mode?
4) What happens if previous system affinity mask is not restored?
(I'd rather post four different questions, but they seem to me too interdependent)
Use the undocumented KeRevertToUserAffinityThread(void) in WinXP. A quick search yields little information about the API but I found an implementation of the same function in ReastOS :
ReactOS KeRevertToUserAffinityThread
It is rather simple so I copy & paste it here:
VOID NTAPI KeRevertToUserAffinityThread ( VOID )
{
KIRQL OldIrql;
PKPRCB Prcb;
PKTHREAD NextThread, CurrentThread = KeGetCurrentThread();
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);
ASSERT(CurrentThread->SystemAffinityActive != FALSE);
/* Lock the Dispatcher Database */
OldIrql = KiAcquireDispatcherLock();
/* Set the user affinity and processor and disable system affinity */
CurrentThread->Affinity = CurrentThread->UserAffinity;
CurrentThread->IdealProcessor = CurrentThread->UserIdealProcessor;
CurrentThread->SystemAffinityActive = FALSE;
/* Get the current PRCB and check if it doesn't match this affinity */
Prcb = KeGetCurrentPrcb();
if (!(Prcb->SetMember & CurrentThread->Affinity))
{
/* Lock the PRCB */
KiAcquirePrcbLock(Prcb);
/* Check if there's no next thread scheduled */
if (!Prcb->NextThread)
{
/* Select a new thread and set it on standby */
NextThread = KiSelectNextThread(Prcb);
NextThread->State = Standby;
Prcb->NextThread = NextThread;
}
/* Release the PRCB lock */
KiReleasePrcbLock(Prcb);
}
/* Unlock dispatcher database */
KiReleaseDispatcherLock(OldIrql);
}
Note the function takes no argument and just restore the affinity from some elements in the currrent KTHREAD struct. I guess this answer your question 1 & 2. Just call this function with no argument. I have done a test in 32bit WinXP and confirmed this. Question 4 is simple, your thread will continue to run using the processor affinity your've set.
I have no idea to your question 3. But most likely a switch between user and kernel mode has no effect on the current processor affinity in effect since this is something stored in the KTHREAD struct.

SAS script to list all SAS server users from metadata

I need a way to import a list of all SAS users from SAS metadata into an Excel worksheet. I was considering doing this using the SAS plugin for Microsoft Office to create a dynamic data source the retrieves the list of users dynamically from the SAS server. If I am to do this I need to know how to do this in SAS code.
Does anyone know how I would write a SAS script to display a list of all users in SAS metadata, or if this is even possible?
I've been trying to find something online but haven't had any luck.
I have full administrator privileges, so no problem there.
Thanks!
Thanks to Joe in the comments I found the answer I need:
http://support.sas.com/documentation/cdl/en/lrmeta/63180/HTML/default/viewer.htm#p1k9zipe59ha2an1pq34gu143lay.htm
I used the very last example in this page, modified to do a PROC PRINT instead of exporting to an Excel sheet. In Enterprise Guide I created a new program as follows:
/*Connect to the metadata server using the metadata system options as
shown in the first example. */
data work.Identities;
/* The LENGTH statement defines the lengths of variables for function arguments. */
length IdentId IdentName DispName ExtLogin IntLogin DomainName $32
uri uri2 uri3 uri4 $256;
/* The LABEL statement assigns descriptive labels to variables. */
label
IdentId = "Identity Id"
IdentName = "Identity Name"
DispName = "Display Name"
ExtLogin = "External Login"
IntLogin = "Is Account Internal?"
DomainName = "Authentication Domain";
/* The CALL MISSING statement initializes the output variables to missing values. */
call missing(IdentId, IdentName, DispName, ExtLogin, IntLogin, DomainName,
uri, uri2, uri3, uri4);
n=1;
n2=1;
/* The METADATA_GETNOBJ function specifies to get the Person objects in the repository.
The n argument specifies to get the first person object that is returned.
The uri argument will return the actual uri of the Person object. The program prints an
informational message if no objects are found. */
rc=metadata_getnobj("omsobj:Person?#Id contains '.'",n,uri);
if rc<=0 then put "NOTE: rc=" rc
"There are no identities defined in this repository"
" or there was an error reading the repository.";
/* The DO statement specifies a group of statements to be executed as a unit.
The METADATA_GETATTR function gets the values of the Person object's Id, Name,
and DisplayName attributes. */
do while(rc>0);
objrc=metadata_getattr(uri,"Id",IdentId);
objrc=metadata_getattr(uri,"Name",IdentName);
objrc=metadata_getattr(uri,"DisplayName",DispName);
/* The METADATA_GETNASN function gets objects associated via the
InternalLoginInfo association. The InternalLoginInfo association returns
internal logins. The n2 argument specifies to return the first associated object
for that association name. The URI of the associated object is returned in
the uri2 variable. */
objrc=metadata_getnasn(uri,"InternalLoginInfo",n2,uri2);
/* If a Person does not have any internal logins, set their IntLogin
variable to 'No' Otherwise, set to 'Yes'. */
IntLogin="Yes";
DomainName="**None**";
if objrc<=0 then
do;
put "NOTE: There are no internal Logins defined for " IdentName +(-1)".";
IntLogin="No";
end;
/* The METADATA_GETNASN function gets objects associated via the Logins association.
The Logins association returns external logins. The n2 argument specifies to return
the first associated object for that association name. The URI of the associated
object is returned in the uri3 variable. */
objrc=metadata_getnasn(uri,"Logins",n2,uri3);
/* If a Person does not have any logins, set their ExtLogin
variable to '**None**' and output their name. */
if objrc<=0 then
do;
put "NOTE: There are no external Logins defined for " IdentName +(-1)".";
ExtLogin="**None**";
output;
end;
/* If a Person has many logins, loop through the list and retrieve the name of
each login. */
do while(objrc>0);
objrc=metadata_getattr(uri3,"UserID",ExtLogin);
/* If a Login is associated to an authentication domain, get the domain name. */
DomainName="**None**";
objrc2=metadata_getnasn(uri3,"Domain",1,uri4);
if objrc2 >0 then
do;
objrc2=metadata_getattr(uri4,"Name",DomainName);
end;
/*Output the record. */
output;
n2+1;
/* Retrieve the next Login's information */
objrc=metadata_getnasn(uri,"Logins",n2,uri3);
end; /*do while objrc*/
/* Retrieve the next Person's information */
n+1;
n2=1;
rc=metadata_getnobj("omsobj:Person?#Id contains '.'",n,uri);
end; /*do while rc*/
/* The KEEP statement specifies the variables to include in the output data set. */
keep IdentId IdentName DispName ExtLogin IntLogin DomainName;
run;
/* The PROC PRINT statement writes a basic listing of the data. */
proc print data=work.Identities label;
run;
/* The PROC EXPORT statement can be used to write the data to an Excel spreadsheet. */
/* Change DATA= to the data set name you specified above. */
/* Change OUTFILE= to an appropriate path for your system. */
/*
proc export data=work.Identities
dbms=EXCE
outfile="C:\temp\Identities.xls"
replace;
run;
*/
PROC PRINT DATA=work.Identities;
When this was executed it created a SAS Report. I exported that Report as a .srx file and then used the SAS Plugin for Microsoft Office to add the report into an Excel worksheet (the "Reports" button).
I then right-clicked on the cell where the report was added and clicked Properties, and then set it to automatically update whenever the document is opened.
It's a great way to review users as an administrator. Rather than having to check each system individually to see if a user exists (when they leave the company for example) I have a sheet for each of our SAS systems, a sheet for each of our Teradata systems (auto-updated using a query run through ODBC), and another sheet auto-updated from a separate spreadsheet that contains the list of our MicroStrategy users. It makes checking all systems as simple as a single Ctrl + F.
If you just want to extract the list of users in SAS you can compile this macro and run:
%mm_getusers(outds=myusers)
Disclaimer - I wrote it, and we use it in our commercial product (https://datacontroller.io) so users can see group memberships when applying permissions within the tool.

Resources