Written by

Blockchain & Full Stack Developer, Teacher at DreamTeam, former InterSystems
Question Nikita Savchenko · Jun 14, 2016

Get variables list

Hello!

In Caché there is a way to print all current variables using write command without arguments:

USER>set Einstein = 1879
USER>set Mozart = 1756
USER>write
Einstein=1879
Mozart=1756

But is there a way to get a list of this variables? I am looking for something that would return value like $LB("Einstein", "Mozart") for this case.

Thanks!

Comments

Michael Braam · Jun 14, 2016

Something like this should do it:

set x=""

set x = $o(@x)

while x'="" {

 write !,x," ",@x

 set x = $o(@x)

}

0
Nikita Savchenko  Jun 14, 2016 to Michael Braam

This is awesome. It works! Wondering how many things COS has.

But it also outputs variable x. I wrapped your code into the method, and that's what I got:

ClassMethod GetVariables() As %List{  set UndefinedSpecialList = $LB()  set UndefinedSpecialInt = 0  set Undefined = ""  set Undefined = $ORDER(@Undefined)  while (Undefined '= "") {    quit:($ORDER(@Undefined) = "UndefinedSpecialList")    set $LIST(UndefinedSpecialList, $INCREMENT(UndefinedSpecialInt)) = Undefined    set Undefined = $ORDER(@Undefined)  }  return UndefinedSpecialList}

It works awesome until user specify variables named UndefinedSpecialList, UndefinedSpecialInt and Undefined. I suppose user won't ever name variables this way, of course, but the method which will consider this is the winner.

0
Timothy Leavitt  Jun 14, 2016 to Nikita Savchenko

You could always use process private globals instead of local variable names if you want to avoid collisions in local variable names.

ClassMethod GetVariables() As %List
{
    Set ^||VariableNameList = ""
    Set ^||VariableName = ""
    Set ^||VariableName = $Order(@^||VariableName)
    While (^||VariableName '= "") {
        Set ^||VariableNameList = ^||VariableNameList_$ListBuild(^||VariableName)
        Set ^||VariableName = $Order(@^||VariableName)
    }
    Quit ^||VariableNameList
}

This might do some things you don't expect depending on variable scope, though - possibly relevant depending on the use case you have in mind.

Class Demo.Variables
{

ClassMethod OuterMethod()
{
    Set x = 5
    Do ..InnerMethod()
}

ClassMethod InnerMethod() [ PublicList = y ]
{
    Set y = 10
    Write $ListToString(..GetVariables())
}

ClassMethod NoPublicListMethod()
{
    Set y = 10
    Write $ListToString(..GetVariables())
}

ClassMethod GetVariables() As %List
{
    Set ^||VariableNameList = ""
    Set ^||VariableName = ""
    Set ^||VariableName = $Order(@^||VariableName)
    While (^||VariableName '= "") {
        Set ^||VariableNameList = ^||VariableNameList_$ListBuild(^||VariableName)
        Set ^||VariableName = $Order(@^||VariableName)
    }
    Quit ^||VariableNameList
}

}

Results:

SAMPLES>kill  set z = 0 d ##class(Demo.Variables).OuterMethod()
y,z
SAMPLES>kill  set z = 0 d ##class(Demo.Variables).NoPublicListMethod()
z
0
Nikita Savchenko  Jun 14, 2016 to Timothy Leavitt

Thank you, Timothy! This is good, but what if user specify ^||VariableNameList process-private variable before calling the method? The value will be lost:

USER>set ^||VariableNameList = "TempVal" zwrite ##class(Test.Class).GetVariablesByTimothy()
$lb("Einstein","Mozart")
USER>write ^||VariableNameList
...TempVal is now gone!...

Maybe there is no absolutely save method to get the variables list without touching/rewriting other variables or globals. I hope it is enough just to name temporary variables as no one suggests to name.

0
Eduard Lebedyuk  Jun 14, 2016 to Nikita Savchenko

Check if variable is defined beforehand and generate/use a new variable name if required:

ClassMethod GetNewVarName() As %String
{
    Set Name = "Temp"
    While $Data(@Name) {
        Set Name = Name _ $Random(100)
    }
    Return Name
}
0
Nikita Savchenko  Jun 14, 2016 to Eduard Lebedyuk

OK :) But how to use this variable name generator? :)

set myNewVarName = ..GetNewVarName() // oops, no
 set @..GetNewVarName() = ""// now it's OK, but I need to call this function once more
 set @..GetNewVarName() = $ORDER(@@..GetNewVarName()) // oops, another variable names

Things get complicated here. I think we can stop on the solutions Michael and Timothy proposed.

Thanks!

0
John Murray  Jun 14, 2016 to Nikita Savchenko

Here's a sneaky way to use a NEWable and SETtable system variable to cache your "safe" variable name in:

 new $etrap
 set $etrap=";"_..GetNewVarName() ; semicolon prefix makes the $ETRAP into a valid line of code (i.e. a comment)
 set @$extract($etrap,2,*)=""
 set @$extract($etrap,2,*)=$order(@@$extract($etrap,2,*))

You need more than one "safe" variable? Use subscripts of the one you've grabbed:

 set @$extract($etrap,2,*)@(1)="Some more info I need to store"
0
John Murray  Jun 14, 2016 to Timothy Leavitt

As well as the suggestion of using a process-private global, Tim flags up an important point here. The $ORDER technique only enumerates public local variables.

0
Richard Rael · Jun 15, 2016

a quick on-liner I use for debugging some times sends variabls to a global so that I can examine later:

s xtest="" f i=1:1 s xtest=$o(@xtest)  q:xtest=""  s ^ZZZZZ(xtest)=@xtest

0
Nikita Savchenko  Jun 15, 2016 to Richard Rael

Nice! Will wonder if you have some sort of more-liner, which also saves and restores objects (orefs). But I believe it is not too useful.

0
Timur Safin  Jun 16, 2016 to Fabio Goncalves

yes Old school!

P.S.

Almost entirely forgotten about spool device

0
Nikita Savchenko  Jun 30, 2016 to Fabio Goncalves

Thanks, Fabio!

Good to know about SPOOL device. Thank you! However, this method touches ^SPOOL global. Being set before the call, ^SPOOL global changes it's value, which is not secure for my needs.

P.S. Recently, Stuart gave me a complete answer. Thanks for your help!

0
Stuart Salzer · Jun 30, 2016

If you are really sensitive as to not defining new local variables, and having them pollute your variable list, use the variable named % (yes, just a percent sign is a valid variable name, and happens to sort first. Here is the simple case:

    WRITE:$DATA(%) !,"%"
    NEW % SET %="%" FOR SET %=$ORDER(@%) QUIT:%="" WRITE !,%

Putting that into a routine requires one more temporary variable. %0 sorts next, then %00, %000, and so on.

VARLIST() ; SRS 2016-06-30, Returns list of local variables.
    IF $DATA(%) ; This places the existence of variable % into $TEST.
    NEW % SET %=$SELECT($TEST:$LISTBUILD("%"),1:"")
    SET:$DATA(%0) %=%_$LISTBUILD("%0")
    NEW %0 SET %0="%0"
    FOR SET %0=$ORDER(@%0) QUIT:%0="" SET %=%_$LISTBUILD(%0)
    QUIT %

Many programmers consider it acceptable, if not desirable, to use variables %, and % followed by any digits for introspection only, and therefore not worry about if % and %digits are predefined, Using %, %1, %2, %3, etc, and starting loops with SET %="%9" or something like that.

0
Nikita Savchenko  Jun 30, 2016 to Stuart Salzer

Thank you,  Stuart!

I think this is a complete solution. I wrapped it to a class method and it works just as expected:

ClassMethod VarList() As %String
{
    IF $DATA(%) ; This places the existence of variable % into $TEST.
    NEW SET %=$SELECT($TEST:$LISTBUILD("%"),1:"")
    SET:$DATA(%0) %=%_$LISTBUILD("%0")
    NEW %0 SET %0="%0"
    FOR {
        SET %0=$ORDER(@%0)
        QUIT:%0=""
        SET %=%_$LISTBUILD(%0)
    }
    QUIT %
}

Results with

USER > write
%=11
a=<OBJECT REFERENCE>[1@DevProject.Robot]
i=5
USER > zw ##class(DevProject.Robot).VarList()
$lb("%","a","i")
USER > kill %
USER > zw ##class(DevProject.Robot).VarList()
$lb("a","i")
0