:: krowemoh

Monday | 15 SEP 2025
Posts Links Other About Now

previous

Exporting Pick Records to Linux

2025-09-14
pick, universe, sysadmin, scaletdme

In the previous post I wrote about importing data from Linux into Pick. This is pretty much how you would do any sort of data import. It's relatively straightforward, the only part that needs some thought is structuring the dictionaries and how you want the data stored.

In this post, I'm going to export the LINUX-USER-FILE back out. The goal is to recreate /etc/passwd and /etc/shadow.

There is a github repo with all the code.

The Long and Short of it

Like most things in life, what you are handed will dictate how easy or hard something is.

We can make things relatively easy if we select the LINUX-USER-FILE ordered by the USER.ID.

EXECUTE 'SELECT LINUX-USER-FILE BY USER.ID'

Now that we have the SELECT statement we can then loop on the list and read in each entry.

   EXECUTE 'SELECT LINUX-USER-FILE BY USER.ID'
*
   LOOP
      READNEXT ITEM.ID ELSE ITEM.ID = ''
*
   UNTIL ITEM.ID = '' DO
      MATREAD LINUX.USER.ITEM FROM LINUX.USER.FILE, ITEM.ID THEN
*
         USERNAME = ITEM.ID
*
         USER.ID = LINUX.USER.ITEM(LINUX.USER.USER.ID.ATTRIBUTE)
         GROUP.ID = LINUX.USER.ITEM(LINUX.USER.GROUP.ID.ATTRIBUTE)
      END ELSE
         PRINT 'Failed to read ITEM.ID: ' : ITEM.ID
         STOP
      END
   REPEAT
*

This is the code shortened up but the core idea is here. The SELECT statement returned us an active list of item ids that we can loop on.

While in our loop, we read in each record and get the data that we need from the record.

If the record doesn't exist, this is a catastrophic error and we hard stop. This should only happen if someone went in deleted an item while we were in the middle of processing other things. This shouldn't happen.

We will need to pick off quite a few fields so the code will look long but it's all very simple.

Now let's first bring in the constants so that we have some naming set up.

*
   EQU TRUE TO 1
   EQU FALSE TO 0
*
* COMPILER DIRECTIVES
*
   $DEFINE DATABASE.QM
   $DEFINE PLATFORM.LINUX
*
   $IFDEF DATABASE.QM
      $CATALOGUE LOCAL
   $ENDIF
*
* %INCLUDE LINUX-USER-FILE
*
   OPEN '','LINUX-USER-FILE' TO LINUX.USER.FILE ELSE
      PRINT 'Failed to open file: LINUX-USER-FILE'
      STOP
   END
*
   DIM LINUX.USER.ITEM(12)
   MAT LINUX.USER.ITEM = ''
*
   EQU LINUX.USER.PASSWORD.ATTRIBUTE TO 1
   EQU LINUX.USER.USER.ID.ATTRIBUTE TO 2
   EQU LINUX.USER.GROUP.ID.ATTRIBUTE TO 3
   EQU LINUX.USER.GECOS.ATTRIBUTE TO 4
   EQU LINUX.USER.DIRECTORY.ATTRIBUTE TO 5
   EQU LINUX.USER.SHELL.ATTRIBUTE TO 6
   EQU LINUX.USER.LAST.CHANGED.ATTRIBUTE TO 7
   EQU LINUX.USER.MIN.AGE.ATTRIBUTE TO 8
   EQU LINUX.USER.MAX.AGE.ATTRIBUTE TO 9
   EQU LINUX.USER.WARNING.PERIOD.ATTRIBUTE TO 10
   EQU LINUX.USER.INACTIVITY.PERIOD.ATTRIBUTE TO 11
   EQU LINUX.USER.EXPIRY.DATE.ATTRIBUTE TO 12
*
   @USER1 = 'EXPORT.LINUX.FILE'
   @USER2 = 'EXPORT.LINUX.FILE'
*
* %END
*
   OPENPATH '/tmp' TO TMP.FILE ELSE
      PRINT 'Failed to open: /tmp'
      STOP
   END
*
   EXECUTE 'SELECT LINUX-USER-FILE BY USER.ID'
*

Here we have our constants which should look familiar. We also open the /tmp directory as we will be exporting the passwd and shadow here.

Now for the juicy part:

   PASSWORD.BUFFER = ''
   SHADOW.BUFFER = ''
*
   LOOP
      READNEXT ITEM.ID ELSE ITEM.ID = ''
*
   UNTIL ITEM.ID = '' DO
      MATREAD LINUX.USER.ITEM FROM LINUX.USER.FILE, ITEM.ID THEN
*
         USERNAME = ITEM.ID
*
         USER.ID = LINUX.USER.ITEM(LINUX.USER.USER.ID.ATTRIBUTE)
         GROUP.ID = LINUX.USER.ITEM(LINUX.USER.GROUP.ID.ATTRIBUTE)
*
         GECOS = LINUX.USER.ITEM(LINUX.USER.GECOS.ATTRIBUTE)
         CONVERT @VM TO ',' IN GECOS
*
         DIRECTORY = LINUX.USER.ITEM(LINUX.USER.DIRECTORY.ATTRIBUTE)
         SHELL = LINUX.USER.ITEM(LINUX.USER.SHELL.ATTRIBUTE)
*
         PASSWORD = LINUX.USER.ITEM(LINUX.USER.PASSWORD.ATTRIBUTE)
         LAST.CHANGED = LINUX.USER.ITEM(LINUX.USER.LAST.CHANGED.ATTRIBUTE)
         MIN.AGE = LINUX.USER.ITEM(LINUX.USER.MIN.AGE.ATTRIBUTE)
         MAX.AGE = LINUX.USER.ITEM(LINUX.USER.MAX.AGE.ATTRIBUTE)
         WARNING.PERIOD = LINUX.USER.ITEM(LINUX.USER.WARNING.PERIOD.ATTRIBUTE)
         INACTIVITY.PERIOD = LINUX.USER.ITEM(LINUX.USER.INACTIVITY.PERIOD.ATTRIBUTE)
         EXPIRY.DATE = LINUX.USER.ITEM(LINUX.USER.EXPIRY.DATE.ATTRIBUTE)
*
         PASSWD = ''
         PASSWD<1> = USERNAME
         PASSWD<2> = 'x'
         PASSWD<3> = USER.ID
         PASSWD<4> = GROUP.ID
         PASSWD<5> = GECOS
         PASSWD<6> = DIRECTORY
         PASSWD<7> = SHELL
*
         SHADOW = ''
         SHADOW<1> = USERNAME
         SHADOW<2> = PASSWORD
         SHADOW<3> = LAST.CHANGED
         SHADOW<4> = MIN.AGE
         SHADOW<5> = MAX.AGE
         SHADOW<6> = WARNING.PERIOD
         SHADOW<7> = INACTIVITY.PERIOD
         SHADOW<8> = EXPIRY.DATE
*
         CONVERT @AM TO ':' IN PASSWD
         CONVERT @AM TO ':' IN SHADOW
*
         PRINT PASSWD
         PRINT SHADOW
         PRINT
*
         PASSWORD.BUFFER<-1> = PASSWD
*
         IF EXPIRY.DATE = '' THEN
            SHADOW.BUFFER<-1> = SHADOW : ':'
         END ELSE
            SHADOW.BUFFER<-1> = SHADOW
         END
*
      END ELSE
         PRINT 'Failed to read ITEM.ID: ' : ITEM.ID
         STOP
      END
   REPEAT
*

Here we have the core loop. We loop and read the record in and set up variables for each part of the record. In my case every field is going to be used so every field gets a variable.

We then build the PASSWD and SHADOW record and then we can do a simple convert to get them into the right format.

This will then get printed out to the screen and added to a BUFFER variable.

The one thing to note is that the GECOS field gets it's value marks converted to commas as we had done that conversion on the import side.

The most noteworthy part of this code is:

         IF EXPIRY.DATE = '' THEN
            SHADOW.BUFFER<-1> = SHADOW : ':'
         END ELSE
            SHADOW.BUFFER<-1> = SHADOW
         END

We append : to each line if the EXPIRY.DATE is blank as blanks at the end will get collapsed. This means that a single attribute mark at the end is not enough information to tell if there was a single blank at the end or if ther was any number of blanks at the end.

This has to do with the way multivalue works and is a gotcha that will come to bite you. There are flags that will force the attribute marks to populate even when there are blanks but I can't seem to find it right now.

For example the following example of a record, where @ is the attribute mark:

1@1@@@

will show up as:

1@1@

However if there is even a single non-null, then all the multivalue marks will appear:

1@1@@@1

Now the final step is to write out the BUFFERs to the /tmp directory.

   WRITE PASSWORD.BUFFER ON TMP.FILE, 'export.passwd'
   PRINT 'Exported /tmp/export.passwd'
*
   WRITE SHADOW.BUFFER ON TMP.FILE, 'export.shadow'
   PRINT 'Exported /tmp/export.shadow'
*
* END OF PROGRAM.
*
   END
*

With that we are done! We should be able to run our routine and have it actually print out each line to the screen and also have the two export files read to be migrated.