Macros for SAS Application Developers
https://github.com/sasjs/core
Loading...
Searching...
No Matches
mp_dirlist.sas
Go to the documentation of this file.
1/**
2 @file
3 @brief Returns all files and subdirectories within a specified parent
4 @details When used with getattrs=NO, is not OS specific (uses dopen / dread).
5
6 Usage:
7
8 %mp_dirlist(path=/some/location, outds=myTable, maxdepth=MAX)
9
10 %mp_dirlist(outds=cwdfileprops, getattrs=YES)
11
12 %mp_dirlist(fref=MYFREF)
13
14 Entries are classified with the dopen function alone. The previous
15 implementation also used fopen - which blocks when the entry is a named pipe
16 (fifo), so a single fifo in a folder was enough to hang the whole session.
17 With getattrs=NO that hazard is gone; with getattrs=YES entries are opened to
18 read their attributes, so a fifo can still block that mode.
19
20 @note Symlinks are followed. A symlink that points at one of its own parents
21 produces an endless path (a/b/link/b/link/...), so a browse service must
22 always pass a finite <code>maxdepth=</code>.
23
24 @param [in] path= (%sysfunc(pathname(work))) Path for which to return contents.
25 Pass a blank value to list the platform roots instead - the drives that
26 exist on Windows, / on Unix.
27 @param [in] fref= (0) Provide a DISK engine fileref as an alternative to PATH
28 @param [in] maxdepth= (0) Set to a positive integer to indicate the level of
29 subdirectory scan recursion - eg 3, to go `./3/levels/deep`. For unlimited
30 recursion, set to MAX.
31 @param [in] maxobs= (0) Maximum number of rows to return. Zero means no
32 limit. Useful when the folder is expected to be huge and the caller only
33 needs a slice of it - no further levels are scanned once the cap is hit.
34 @param [in] showparent= (NO) By default, the initial parent directory is not
35 part of the results. Set to YES to include it. For this record only,
36 directory=filepath.
37 @param [out] outds= (work.mp_dirlist) The output dataset to create
38 @param [out] getattrs= (NO) If getattrs=YES then the doptname / foptname
39 functions are used to scan all properties - any characters that are not
40 valid in a SAS name (v7) are simply stripped, and the table is transposed
41 so theat each property is a column and there is one file per row. An
42 attempt is made to get all properties whether a file or folder, but some
43 files/folders cannot be accessed, and so not all properties can / will be
44 populated.
45 @param [in] level= (0) Ignored - retained so that callers of previous versions
46 do not break. Recursion is now done in a loop rather than by re-invoking
47 the macro.
48
49 @returns outds contains the following variables:
50 - directory (containing folder)
51 - file_or_folder. folder when it can be opened as a directory, file when
52 it is not a directory (or exists but is not readable, which the copy,
53 hash and zip tools treat as a file), and locked when it is listed but
54 cannot be opened at all - a broken link, for instance
55 - filepath (path/to/file.name)
56 - filename (just the file name)
57 - ext (.extension)
58 - msg (system message if any issues)
59 - level (depth of folder)
60 - OS SPECIFIC variables, if <code>getattrs=</code> is used.
61
62 <h4> SAS Macros </h4>
63 @li mf_getuniquefileref.sas
64 @li mf_nobs.sas
65 @li mp_dropmembers.sas
66
67 <h4> Related Macros </h4>
68 @li mp_dirlist.test.sas
69
70 @version 9.2
71 @sasjslint {"maxLineLength": 128}
72**/
73
74%macro mp_dirlist(path=%sysfunc(pathname(work))
75 , fref=0
76 , outds=work.mp_dirlist
77 , getattrs=NO
78 , showparent=NO
79 , maxdepth=0
80 , maxobs=0
81 , level=0 /* deprecated - ignored, kept for backwards compatibility */
82)/*/STORE SOURCE*/;
83%let getattrs=%upcase(&getattrs)XX;
84%let showparent=%upcase(&showparent)XX;
85
86/* the separator belongs to the platform, not to the path text - a unix
87 directory name may contain a backslash without that being the separator */
88%if "%upcase(&sysscp)"="WIN" or "%upcase(&sysscp)"="DNTHOST" %then %let mpsep=\;
89%else %let mpsep=/;
90
91%local lvl nfront total room done mp_roots mpsep;
92%local fref1 fref2;
93
94/* the parent can be supplied as a fileref rather than a path */
95%if &fref ne 0 %then %let path=%sysfunc(pathname(&fref));
96
97/* MAX is not a number, so convert it (and anything not a digit) to an int */
98%if %length(&maxdepth)=0 or %sysfunc(verify(&maxdepth,0123456789))>0 %then %do;
99 %if "%upcase(&maxdepth)"="MAX" %then %let maxdepth=2147483647;
100 %else %let maxdepth=0;
101%end;
102%if %length(&maxobs)=0 or %sysfunc(verify(&maxobs,0123456789))>0 %then %let maxobs=0;
103
104/* drop main (top) table if it exists */
105%mp_dropmembers(%scan(&outds,-1,.), libref=WORK)
106
107/* an empty table with the target structure - each level is appended to it */
108data &outds;
109 length directory filepath $2000 file_or_folder $6 filename $255 ext $20
110 msg $200 level 8;
111 call missing(of _all_);
112 stop;
113run;
114
115/* a unique fileref per purpose, generated once - only macro vars are local */
116%let fref1=%mf_getuniquefileref();
117%let fref2=%mf_getuniquefileref();
118
119%let mp_roots=0;
120%if %length(&path)=0 %then %let mp_roots=1;
121
122%if &mp_roots=1 %then %do;
123 /*
124 A blank path is the platform roots: there is no filesystem root on Windows,
125 so the drives that exist are the top level, and / is that level on Unix.
126 */
127 data work.__mp_dirlist_level;
128 length directory filepath $2000 file_or_folder $6 filename $255
129 ext $20 msg $200 level 8;
130 length drive $3 letters $26;
131 call missing(of _all_);
132 %if "%upcase(&sysscp)"="WIN" or "%upcase(&sysscp)"="DNTHOST" %then %do;
133 letters='ABCDEFGHIJKLMNOPQRSTUVWXYZ';
134 do i=1 to 26;
135 drive=cats(substr(letters,i,1),':');
136 rc=filename("&fref1",cats(drive,'\'));
137 did=dopen("&fref1");
138 if did>0 then do;
139 rc=dclose(did);
140 directory=drive;
141 filepath=cats(drive,'\');
142 filename=filepath;
143 file_or_folder='folder';
144 level=0;
145 output;
146 end;
147 rc=filename("&fref1");
148 end;
149 %end;
150 %else %do;
151 directory='';
152 filepath='/';
153 filename='/';
154 file_or_folder='folder';
155 level=0;
156 output;
157 %end;
158 keep directory filepath file_or_folder filename ext msg level;
159 run;
160 proc append base=&outds data=work.__mp_dirlist_level nowarn force;
161 run;
162 /* the roots are folders, so they are what the next level expands */
163 data work.__mp_dirlist_frontier;
164 set work.__mp_dirlist_level(where=(file_or_folder='folder'));
165 level=1;
166 keep filepath level;
167 run;
168 %let lvl=1;
169%end;
170%else %do;
171 /* level 0 is the parent itself; the path is read with symget so that quotes
172 and macro triggers in a folder name are never evaluated as SAS code */
173 data work.__mp_dirlist_frontier;
174 length filepath $2000 level 8;
175 filepath=symget('path');
176 level=0;
177 run;
178 %let lvl=0;
179%end;
180
181%let nfront=%sysfunc(coalesce(%mf_nobs(work.__mp_dirlist_frontier),0));
182%let done=0;
183
184%do %until(&nfront=0 or &lvl>&maxdepth or &done=1);
185
186 data work.__mp_dirlist_level;
187 set work.__mp_dirlist_frontier;
188 length directory filepath $2000 file_or_folder $6 filename $255 ext $20
189 msg $200 sep $1 prefix $2000 fname $255 dmsg $200;
190 drop sep prefix fname dmsg rc did mid i;
191 directory=filepath;
192 msg='';
193 file_or_folder='';
194 ext='';
195 filename='';
196 rc=filename("&fref1", directory);
197 did=dopen("&fref1");
198 dmsg=sysmsg();
199 if did=0 then do;
200 /* nothing to list - a parent that vanished between levels, or a path
201 that does not exist; the entries that pointed here are already marked
202 locked by the level that produced them */
203 end;
204 else do;
205 /* strip any trailing separator, keeping / and drive roots intact */
206 do while(length(directory)>1);
207 if substr(directory,length(directory),1) not in ('/','\') then leave;
208 if length(directory)=3 and substr(directory,2,1)=':' then leave;
209 directory=substr(directory,1,length(directory)-1);
210 end;
211 sep=symget('mpsep');
212 if directory='/' or (length(directory)=3 and substr(directory,2,1)=':')
213 then prefix=directory;
214 else prefix=cats(directory,sep);
215 do i=1 to dnum(did);
216 fname=dread(did,i);
217 filename=fname;
218 filepath=cats(prefix,fname);
219 msg='';
220 ext='';
221 /* dopen alone is enough to tell a folder from anything else, and it
222 returns immediately for a fifo - fopen would block until a writer
223 appeared */
224 rc=filename("&fref2",filepath);
225 mid=dopen("&fref2");
226 if mid>0 then do;
227 file_or_folder='folder';
228 rc=dclose(mid);
229 end;
230 else do;
231 dmsg=sysmsg();
232 if index(dmsg,'is not a directory')>0 then do;
233 file_or_folder='file';
234 ext=prxchange('s/.*\.{1,1}(.*)/$1/',1,filename);
235 if filename=ext then ext=' ';
236 end;
237 else if index(dmsg,'uthorization')>0 then do;
238 /* present but not readable. It stays a file, as it was before the
239 entry test was fixed - the copy, hash and zip tools skip anything
240 that is not a file, and a backup that silently omits an unreadable
241 file is worse than one that fails on it */
242 file_or_folder='file';
243 ext=prxchange('s/.*\.{1,1}(.*)/$1/',1,filename);
244 if filename=ext then ext=' ';
245 msg=dmsg;
246 end;
247 else do;
248 /* listed but not openable at all - a broken link, for instance */
249 file_or_folder='locked';
250 msg=dmsg;
251 end;
252 end;
253 rc=filename("&fref2");
254 output;
255 end;
256 rc=dclose(did);
257 end;
258 rc=filename("&fref1");
259 run;
260
261 /* trim the level to what is left of the maxobs budget before appending */
262 %let total=%sysfunc(coalesce(%mf_nobs(&outds),0));
263 %let room=2147483647;
264 %if &maxobs>0 %then %let room=%eval(&maxobs-&total);
265 %if &room>0 %then %do;
266 data work.__mp_dirlist_level;
267 set work.__mp_dirlist_level(obs=&room);
268 run;
269 proc append base=&outds data=work.__mp_dirlist_level force nowarn;
270 run;
271 %end;
272
273 %let total=%sysfunc(coalesce(%mf_nobs(&outds),0));
274 %if &maxobs>0 and &total>=&maxobs %then %let done=1;
275
276 /* the folders of this level become the parents of the next one */
277 %let nfront=0;
278 %if &done=0 and &lvl<&maxdepth %then %do;
279 data work.__mp_dirlist_frontier;
280 set work.__mp_dirlist_level(where=(file_or_folder='folder'));
281 level=level+1;
282 keep filepath level;
283 run;
284 %let nfront=%sysfunc(coalesce(%mf_nobs(work.__mp_dirlist_frontier),0));
285 %end;
286
287 %let lvl=%eval(&lvl+1);
288
289%end;
290
291/* include the starting folder itself, when asked */
292%if %substr(&showparent,1,1)=Y %then %do;
293 data work.__mp_dirlist_level;
294 length directory filepath $2000 file_or_folder $6 filename $255 ext $20
295 msg $200 level 8;
296 /* the parent is only listed when it can be opened, so a path that does
297 not exist stays an empty result even with showparent=YES */
298 filepath=symget('path');
299 rc=filename("&fref1",filepath);
300 did=dopen("&fref1");
301 if did>0 then rc=dclose(did);
302 rc=filename("&fref1");
303 if did=0 then stop;
304 directory=filepath;
305 do while(length(directory)>1);
306 if substr(directory,length(directory),1) not in ('/','\') then leave;
307 if length(directory)=3 and substr(directory,2,1)=':' then leave;
308 directory=substr(directory,1,length(directory)-1);
309 end;
310 filepath=directory;
311 file_or_folder='folder';
312 ext='';
313 filename=scan(directory,-1,'/\');
314 msg='';
315 level=0;
316 output;
317 run;
318 proc append base=&outds data=work.__mp_dirlist_level force nowarn;
319 run;
320%end;
321
322%if %substr(&getattrs,1,1)=Y %then %do;
323 data &outds;
324 set &outds;
325 length infoname infoval $60 fref $8;
326 if _n_=1 then call missing(fref);
327 rc=filename(fref,filepath);
328 drop rc infoname fid i close fref;
329 if file_or_folder='file' then do;
330 fid=fopen(fref);
331 if fid le 0 then do;
332 msg=sysmsg();
333 putlog "Could not open file:" filepath fid= ;
334 sasname='_MCNOTVALID_';
335 output;
336 end;
337 else do i=1 to foptnum(fid);
338 infoname=foptname(fid,i);
339 infoval=finfo(fid,infoname);
340 sasname=compress(infoname, '_', 'adik');
341 if anydigit(sasname)=1 then sasname=substr(sasname,anyalpha(sasname));
342 if upcase(sasname) ne 'FILENAME' then output;
343 end;
344 close=fclose(fid);
345 end;
346 else do;
347 fid=dopen(fref);
348 if fid le 0 then do;
349 msg=sysmsg();
350 putlog "Could not open folder:" filepath fid= ;
351 sasname='_MCNOTVALID_';
352 output;
353 end;
354 else do i=1 to doptnum(fid);
355 infoname=doptname(fid,i);
356 infoval=dinfo(fid,infoname);
357 sasname=compress(infoname, '_', 'adik');
358 if anydigit(sasname)=1 then sasname=substr(sasname,anyalpha(sasname));
359 if upcase(sasname) ne 'FILENAME' then output;
360 end;
361 close=dclose(fid);
362 end;
363 run;
364 proc sort;
365 by filepath sasname;
366 proc transpose data=&outds out=&outds(drop=_:);
367 id sasname;
368 var infoval;
369 by filepath file_or_folder filename ext ;
370 run;
371%end;
372
373data &outds;
374 set &outds(where=(filepath ne ''));
375run;
376
377/* tidy up */
378proc sql noprint;
379drop table work.__mp_dirlist_frontier, work.__mp_dirlist_level;
380quit;
381
382%mend mp_dirlist;