src/eric7/SystemUtilities/FileSystemUtilities.py

branch
eric7
changeset 9624
b47dfa7a137d
child 9639
9e66fd88193c
equal deleted inserted replaced
9623:9c1f429cb56b 9624:b47dfa7a137d
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing file system related utility functions.
8 """
9
10 import contextlib
11 import ctypes
12 import fnmatch
13 import os
14 import pathlib
15 import subprocess
16
17 from eric7.SystemUtilities import OSUtilities
18
19
20 def toNativeSeparators(path):
21 """
22 Function returning a path, that is using native separator characters.
23
24 @param path path to be converted
25 @type str
26 @return path with converted separator characters
27 @rtype str
28 """
29 return str(pathlib.PurePath(path)) if bool(path) else ""
30
31
32 def fromNativeSeparators(path):
33 """
34 Function returning a path, that is using "/" separator characters.
35
36 @param path path to be converted
37 @type str
38 @return path with converted separator characters
39 @rtype str
40 """
41 return pathlib.PurePath(path).as_posix() if bool(path) else ""
42
43
44 def normcasepath(path):
45 """
46 Function returning a path, that is normalized with respect to its case
47 and references.
48
49 @param path file path (string)
50 @return case normalized path (string)
51 """
52 return os.path.normcase(os.path.normpath(path))
53
54
55 def normcaseabspath(path):
56 """
57 Function returning an absolute path, that is normalized with respect to
58 its case and references.
59
60 @param path file path (string)
61 @return absolute, normalized path (string)
62 """
63 return os.path.normcase(os.path.abspath(path))
64
65
66 def normjoinpath(a, *p):
67 """
68 Function returning a normalized path of the joined parts passed into it.
69
70 @param a first path to be joined (string)
71 @param p variable number of path parts to be joined (string)
72 @return normalized path (string)
73 """
74 return os.path.normpath(os.path.join(a, *p))
75
76
77 def normabsjoinpath(a, *p):
78 """
79 Function returning a normalized, absolute path of the joined parts passed
80 into it.
81
82 @param a first path to be joined (string)
83 @param p variable number of path parts to be joind (string)
84 @return absolute, normalized path (string)
85 """
86 return os.path.abspath(os.path.join(a, *p))
87
88
89 def isinpath(file):
90 """
91 Function to check for an executable file.
92
93 @param file filename of the executable to check (string)
94 @return flag to indicate, if the executable file is accessible
95 via the searchpath defined by the PATH environment variable.
96 """
97 if os.path.isabs(file):
98 return os.access(file, os.X_OK)
99
100 if os.path.exists(os.path.join(os.curdir, file)):
101 return os.access(os.path.join(os.curdir, file), os.X_OK)
102
103 path = OSUtilities.getEnvironmentEntry("PATH")
104
105 # environment variable not defined
106 if path is None:
107 return False
108
109 dirs = path.split(os.pathsep)
110 return any(os.access(os.path.join(directory, file), os.X_OK) for directory in dirs)
111
112
113 def startswithPath(path, start):
114 """
115 Function to check, if a path starts with a given start path.
116
117 @param path path to be checked
118 @type str
119 @param start start path
120 @type str
121 @return flag indicating that the path starts with the given start
122 path
123 @rtype bool
124 """
125 return bool(start) and (
126 path == start or normcasepath(path).startswith(normcasepath(start + "/"))
127 )
128
129
130 def relativeUniversalPath(path, start):
131 """
132 Function to convert a file path to a path relative to a start path
133 with universal separators.
134
135 @param path file or directory name to convert (string)
136 @param start start path (string)
137 @return relative path or unchanged path, if path does not start with
138 the start path with universal separators (string)
139 """
140 return fromNativeSeparators(os.path.relpath(path, start))
141
142
143 def absolutePath(path, start):
144 """
145 Public method to convert a path relative to a start path to an
146 absolute path.
147
148 @param path file or directory name to convert (string)
149 @param start start path (string)
150 @return absolute path (string)
151 """
152 if not os.path.isabs(path):
153 path = os.path.normpath(os.path.join(start, path))
154 return path
155
156
157 def absoluteUniversalPath(path, start):
158 """
159 Public method to convert a path relative to a start path with
160 universal separators to an absolute path.
161
162 @param path file or directory name to convert (string)
163 @param start start path (string)
164 @return absolute path with native separators (string)
165 """
166 if not os.path.isabs(path):
167 path = toNativeSeparators(os.path.normpath(os.path.join(start, path)))
168 return path
169
170
171 def getExecutablePath(file):
172 """
173 Function to build the full path of an executable file from the environment.
174
175 @param file filename of the executable to check (string)
176 @return full executable name, if the executable file is accessible
177 via the searchpath defined by the PATH environment variable, or an
178 empty string otherwise.
179 """
180 if os.path.isabs(file):
181 if os.access(file, os.X_OK):
182 return file
183 else:
184 return ""
185
186 cur_path = os.path.join(os.curdir, file)
187 if os.path.exists(cur_path) and os.access(cur_path, os.X_OK):
188 return cur_path
189
190 path = os.getenv("PATH")
191
192 # environment variable not defined
193 if path is None:
194 return ""
195
196 dirs = path.split(os.pathsep)
197 for directory in dirs:
198 exe = os.path.join(directory, file)
199 if os.access(exe, os.X_OK):
200 return exe
201
202 return ""
203
204
205 def getExecutablePaths(file):
206 """
207 Function to build all full path of an executable file from the environment.
208
209 @param file filename of the executable (string)
210 @return list of full executable names (list of strings), if the executable
211 file is accessible via the searchpath defined by the PATH environment
212 variable, or an empty list otherwise.
213 """
214 paths = []
215
216 if os.path.isabs(file):
217 if os.access(file, os.X_OK):
218 return [file]
219 else:
220 return []
221
222 cur_path = os.path.join(os.curdir, file)
223 if os.path.exists(cur_path) and os.access(cur_path, os.X_OK):
224 paths.append(cur_path)
225
226 path = os.getenv("PATH")
227
228 # environment variable not defined
229 if path is not None:
230 dirs = path.split(os.pathsep)
231 for directory in dirs:
232 exe = os.path.join(directory, file)
233 if os.access(exe, os.X_OK) and exe not in paths:
234 paths.append(exe)
235
236 return paths
237
238
239 def getWindowsExecutablePath(file):
240 """
241 Function to build the full path of an executable file from the environment
242 on Windows platforms.
243
244 First an executable with the extension .exe is searched for, thereafter
245 such with the extensions .cmd or .bat and finally the given file name as
246 is. The first match is returned.
247
248 @param file filename of the executable to check (string)
249 @return full executable name, if the executable file is accessible
250 via the searchpath defined by the PATH environment variable, or an
251 empty string otherwise.
252 """
253 if os.path.isabs(file):
254 if os.access(file, os.X_OK):
255 return file
256 else:
257 return ""
258
259 filenames = [file + ".exe", file + ".cmd", file + ".bat", file]
260
261 for filename in filenames:
262 cur_path = os.path.join(os.curdir, filename)
263 if os.path.exists(cur_path) and os.access(cur_path, os.X_OK):
264 return os.path.abspath(cur_path)
265
266 path = os.getenv("PATH")
267
268 # environment variable not defined
269 if path is None:
270 return ""
271
272 dirs = path.split(os.pathsep)
273 for directory in dirs:
274 for filename in filenames:
275 exe = os.path.join(directory, filename)
276 if os.access(exe, os.X_OK):
277 return exe
278
279 return ""
280
281
282 def isExecutable(exe):
283 """
284 Function to check, if a file is executable.
285
286 @param exe filename of the executable to check (string)
287 @return flag indicating executable status (boolean)
288 """
289 return os.access(exe, os.X_OK)
290
291
292 def isDrive(path):
293 """
294 Function to check, if a path is a Windows drive.
295
296 @param path path name to be checked
297 @type str
298 @return flag indicating a Windows drive
299 @rtype bool
300 """
301 isWindowsDrive = False
302 drive, directory = os.path.splitdrive(path)
303 if (
304 drive
305 and len(drive) == 2
306 and drive.endswith(":")
307 and directory in ["", "\\", "/"]
308 ):
309 isWindowsDrive = True
310
311 return isWindowsDrive
312
313
314 def samepath(f1, f2):
315 """
316 Function to compare two paths.
317
318 @param f1 first path for the compare (string)
319 @param f2 second path for the compare (string)
320 @return flag indicating whether the two paths represent the
321 same path on disk.
322 """
323 if f1 is None or f2 is None:
324 return False
325
326 if normcaseabspath(os.path.realpath(f1)) == normcaseabspath(os.path.realpath(f2)):
327 return True
328
329 return False
330
331
332 def samefilepath(f1, f2):
333 """
334 Function to compare two paths. Strips the filename.
335
336 @param f1 first filepath for the compare (string)
337 @param f2 second filepath for the compare (string)
338 @return flag indicating whether the two paths represent the
339 same path on disk.
340 """
341 if f1 is None or f2 is None:
342 return False
343
344 if normcaseabspath(os.path.dirname(os.path.realpath(f1))) == normcaseabspath(
345 os.path.dirname(os.path.realpath(f2))
346 ):
347 return True
348
349 return False
350
351
352 try:
353 EXTSEP = os.extsep
354 except AttributeError:
355 EXTSEP = "."
356
357
358 def splitPath(name):
359 """
360 Function to split a pathname into a directory part and a file part.
361
362 @param name path name (string)
363 @return a tuple of 2 strings (dirname, filename).
364 """
365 if os.path.isdir(name):
366 dn = os.path.abspath(name)
367 fn = "."
368 else:
369 dn, fn = os.path.split(name)
370 return (dn, fn)
371
372
373 def joinext(prefix, ext):
374 """
375 Function to join a file extension to a path.
376
377 The leading "." of ext is replaced by a platform specific extension
378 separator if necessary.
379
380 @param prefix the basepart of the filename (string)
381 @param ext the extension part (string)
382 @return the complete filename (string)
383 """
384 if ext[0] != ".":
385 ext = ".{0}".format(ext)
386 # require leading separator to match os.path.splitext
387 return prefix + EXTSEP + ext[1:]
388
389
390 def compactPath(path, width, measure=len):
391 """
392 Function to return a compacted path fitting inside the given width.
393
394 @param path path to be compacted (string)
395 @param width width for the compacted path (integer)
396 @param measure reference to a function used to measure the length of the
397 string
398 @return compacted path (string)
399 """
400 if measure(path) <= width:
401 return path
402
403 ellipsis = "..."
404
405 head, tail = os.path.split(path)
406 mid = len(head) // 2
407 head1 = head[:mid]
408 head2 = head[mid:]
409 while head1:
410 # head1 is same size as head2 or one shorter
411 path = os.path.join("{0}{1}{2}".format(head1, ellipsis, head2), tail)
412 if measure(path) <= width:
413 return path
414 head1 = head1[:-1]
415 head2 = head2[1:]
416 path = os.path.join(ellipsis, tail)
417 if measure(path) <= width:
418 return path
419 while tail:
420 path = "{0}{1}".format(ellipsis, tail)
421 if measure(path) <= width:
422 return path
423 tail = tail[1:]
424 return ""
425
426
427 def direntries(
428 path, filesonly=False, pattern=None, followsymlinks=True, checkStop=None
429 ):
430 """
431 Function returning a list of all files and directories.
432
433 @param path root of the tree to check
434 @type str
435 @param filesonly flag indicating that only files are wanted
436 @type bool
437 @param pattern a filename pattern or list of filename patterns to check
438 against
439 @type str or list of str
440 @param followsymlinks flag indicating whether symbolic links
441 should be followed
442 @type bool
443 @param checkStop function to be called to check for a stop
444 @type function
445 @return list of all files and directories in the tree rooted
446 at path. The names are expanded to start with path.
447 @rtype list of strs
448 """
449 patterns = pattern if isinstance(pattern, list) else [pattern]
450 files = [] if filesonly else [path]
451 try:
452 entries = os.listdir(path)
453 for entry in entries:
454 if checkStop and checkStop():
455 break
456
457 if entry in [
458 ".svn",
459 ".hg",
460 ".git",
461 ".ropeproject",
462 ".eric7project",
463 ".jedi",
464 ]:
465 continue
466
467 fentry = os.path.join(path, entry)
468 if (
469 pattern
470 and not os.path.isdir(fentry)
471 and not any(fnmatch.fnmatch(entry, p) for p in patterns)
472 ):
473 # entry doesn't fit the given pattern
474 continue
475
476 if os.path.isdir(fentry):
477 if os.path.islink(fentry) and not followsymlinks:
478 continue
479 files += direntries(
480 fentry, filesonly, pattern, followsymlinks, checkStop
481 )
482 else:
483 files.append(fentry)
484 except OSError:
485 pass
486 except UnicodeDecodeError:
487 pass
488 return files
489
490
491 def getDirs(path, excludeDirs):
492 """
493 Function returning a list of all directories below path.
494
495 @param path root of the tree to check
496 @param excludeDirs basename of directories to ignore
497 @return list of all directories found
498 """
499 try:
500 names = os.listdir(path)
501 except OSError:
502 return []
503
504 dirs = []
505 for name in names:
506 if os.path.isdir(os.path.join(path, name)) and not os.path.islink(
507 os.path.join(path, name)
508 ):
509 exclude = 0
510 for e in excludeDirs:
511 if name.split(os.sep, 1)[0] == e:
512 exclude = 1
513 break
514 if not exclude:
515 dirs.append(os.path.join(path, name))
516
517 for name in dirs[:]:
518 if not os.path.islink(name):
519 dirs += getDirs(name, excludeDirs)
520
521 return dirs
522
523
524 def findVolume(volumeName, findAll=False):
525 """
526 Function to find the directory belonging to a given volume name.
527
528 @param volumeName name of the volume to search for
529 @type str
530 @param findAll flag indicating to get the directories for all volumes
531 starting with the given name (defaults to False)
532 @type bool (optional)
533 @return directory path or list of directory paths for the given volume
534 name
535 @rtype str or list of str
536 """
537 volumeDirectories = []
538 volumeDirectory = None
539
540 if OSUtilities.isWindowsPlatform():
541 # we are on a Windows platform
542 def getVolumeName(diskName):
543 """
544 Local function to determine the volume of a disk or device.
545
546 Each disk or external device connected to windows has an
547 attribute called "volume name". This function returns the
548 volume name for the given disk/device.
549
550 Code from http://stackoverflow.com/a/12056414
551 """
552 volumeNameBuffer = ctypes.create_unicode_buffer(1024)
553 ctypes.windll.kernel32.GetVolumeInformationW(
554 ctypes.c_wchar_p(diskName),
555 volumeNameBuffer,
556 ctypes.sizeof(volumeNameBuffer),
557 None,
558 None,
559 None,
560 None,
561 0,
562 )
563 return volumeNameBuffer.value
564
565 #
566 # In certain circumstances, volumes are allocated to USB
567 # storage devices which cause a Windows popup to raise if their
568 # volume contains no media. Wrapping the check in SetErrorMode
569 # with SEM_FAILCRITICALERRORS (1) prevents this popup.
570 #
571 oldMode = ctypes.windll.kernel32.SetErrorMode(1)
572 try:
573 for disk in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
574 dirpath = "{0}:\\".format(disk)
575 if os.path.exists(dirpath):
576 if findAll:
577 if getVolumeName(dirpath).startswith(volumeName):
578 volumeDirectories.append(dirpath)
579 else:
580 if getVolumeName(dirpath) == volumeName:
581 volumeDirectory = dirpath
582 break
583 finally:
584 ctypes.windll.kernel32.SetErrorMode(oldMode)
585 else:
586 # we are on a Linux or macOS platform
587 for mountCommand in ["mount", "/sbin/mount", "/usr/sbin/mount"]:
588 with contextlib.suppress(FileNotFoundError):
589 mountOutput = subprocess.run( # secok
590 mountCommand, check=True, capture_output=True, text=True
591 ).stdout.splitlines()
592 mountedVolumes = [
593 x.split(" type")[0].split(maxsplit=2)[2] for x in mountOutput
594 ]
595 if findAll:
596 for volume in mountedVolumes:
597 if volumeName in volume:
598 volumeDirectories.append(volume)
599 if volumeDirectories:
600 break
601 else:
602 for volume in mountedVolumes:
603 if volume.endswith(volumeName):
604 volumeDirectory = volume
605 break
606 if volumeDirectory:
607 break
608
609 if findAll:
610 return volumeDirectories
611 else:
612 return volumeDirectory

eric ide

mercurial