1# encoding: utf-8
2"""
3Utilities for path handling.
4"""
5
6# Copyright (c) IPython Development Team.
7# Distributed under the terms of the Modified BSD License.
8
9import os
10import sys
11import errno
12import shutil
13import random
14import glob
15import warnings
16
17from IPython.utils.process import system
18
19#-----------------------------------------------------------------------------
20# Code
21#-----------------------------------------------------------------------------
22fs_encoding = sys.getfilesystemencoding()
23
24def _writable_dir(path):
25 """Whether `path` is a directory, to which the user has write access."""
26 return os.path.isdir(path) and os.access(path, os.W_OK)
27
28if sys.platform == 'win32':
29 def _get_long_path_name(path):
30 """Get a long path name (expand ~) on Windows using ctypes.
31
32 Examples
33 --------
34
35 >>> get_long_path_name('c:\\\\docume~1')
36 'c:\\\\Documents and Settings'
37
38 """
39 try:
40 import ctypes
41 except ImportError as e:
42 raise ImportError('you need to have ctypes installed for this to work') from e
43 _GetLongPathName = ctypes.windll.kernel32.GetLongPathNameW
44 _GetLongPathName.argtypes = [ctypes.c_wchar_p, ctypes.c_wchar_p,
45 ctypes.c_uint ]
46
47 buf = ctypes.create_unicode_buffer(260)
48 rv = _GetLongPathName(path, buf, 260)
49 if rv == 0 or rv > 260:
50 return path
51 else:
52 return buf.value
53else:
54 def _get_long_path_name(path):
55 """Dummy no-op."""
56 return path
57
58
59
60def get_long_path_name(path):
61 """Expand a path into its long form.
62
63 On Windows this expands any ~ in the paths. On other platforms, it is
64 a null operation.
65 """
66 return _get_long_path_name(path)
67
68
69def compress_user(path: str) -> str:
70 """Reverse of :func:`os.path.expanduser`"""
71 home = os.path.expanduser("~")
72 if path.startswith(home):
73 path = "~" + path[len(home):]
74 return path
75
76def get_py_filename(name):
77 """Return a valid python filename in the current directory.
78
79 If the given name is not a file, it adds '.py' and searches again.
80 Raises IOError with an informative message if the file isn't found.
81 """
82
83 name = os.path.expanduser(name)
84 if os.path.isfile(name):
85 return name
86 if not name.endswith(".py"):
87 py_name = name + ".py"
88 if os.path.isfile(py_name):
89 return py_name
90 raise IOError("File `%r` not found." % name)
91
92
93def filefind(filename: str, path_dirs=None) -> str:
94 """Find a file by looking through a sequence of paths.
95
96 This iterates through a sequence of paths looking for a file and returns
97 the full, absolute path of the first occurrence of the file. If no set of
98 path dirs is given, the filename is tested as is, after running through
99 :func:`expandvars` and :func:`expanduser`. Thus a simple call::
100
101 filefind('myfile.txt')
102
103 will find the file in the current working dir, but::
104
105 filefind('~/myfile.txt')
106
107 Will find the file in the users home directory. This function does not
108 automatically try any paths, such as the cwd or the user's home directory.
109
110 Parameters
111 ----------
112 filename : str
113 The filename to look for.
114 path_dirs : str, None or sequence of str
115 The sequence of paths to look for the file in. If None, the filename
116 need to be absolute or be in the cwd. If a string, the string is
117 put into a sequence and the searched. If a sequence, walk through
118 each element and join with ``filename``, calling :func:`expandvars`
119 and :func:`expanduser` before testing for existence.
120
121 Returns
122 -------
123 path : str
124 returns absolute path to file.
125
126 Raises
127 ------
128 IOError
129 """
130
131 # If paths are quoted, abspath gets confused, strip them...
132 filename = filename.strip('"').strip("'")
133 # If the input is an absolute path, just check it exists
134 if os.path.isabs(filename) and os.path.isfile(filename):
135 return filename
136
137 if path_dirs is None:
138 path_dirs = ("",)
139 elif isinstance(path_dirs, str):
140 path_dirs = (path_dirs,)
141
142 for path in path_dirs:
143 if path == '.': path = os.getcwd()
144 testname = expand_path(os.path.join(path, filename))
145 if os.path.isfile(testname):
146 return os.path.abspath(testname)
147
148 raise IOError("File %r does not exist in any of the search paths: %r" %
149 (filename, path_dirs) )
150
151
152class HomeDirError(Exception):
153 pass
154
155
156def get_home_dir(require_writable=False) -> str:
157 """Return the 'home' directory, as a unicode string.
158
159 Uses os.path.expanduser('~'), and checks for writability.
160
161 See stdlib docs for how this is determined.
162 For Python <3.8, $HOME is first priority on *ALL* platforms.
163 For Python >=3.8 on Windows, %HOME% is no longer considered.
164
165 Parameters
166 ----------
167 require_writable : bool [default: False]
168 if True:
169 guarantees the return value is a writable directory, otherwise
170 raises HomeDirError
171 if False:
172 The path is resolved, but it is not guaranteed to exist or be writable.
173 """
174
175 homedir = os.path.expanduser('~')
176 # Next line will make things work even when /home/ is a symlink to
177 # /usr/home as it is on FreeBSD, for example
178 homedir = os.path.realpath(homedir)
179
180 if not _writable_dir(homedir) and os.name == 'nt':
181 # expanduser failed, use the registry to get the 'My Documents' folder.
182 try:
183 import winreg as wreg
184 with wreg.OpenKey(
185 wreg.HKEY_CURRENT_USER,
186 r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
187 ) as key:
188 homedir = wreg.QueryValueEx(key,'Personal')[0]
189 except:
190 pass
191
192 if (not require_writable) or _writable_dir(homedir):
193 assert isinstance(homedir, str), "Homedir should be unicode not bytes"
194 return homedir
195 else:
196 raise HomeDirError('%s is not a writable dir, '
197 'set $HOME environment variable to override' % homedir)
198
199def get_xdg_dir():
200 """Return the XDG_CONFIG_HOME, if it is defined and exists, else None.
201
202 This is only for non-OS X posix (Linux,Unix,etc.) systems.
203 """
204
205 env = os.environ
206
207 if os.name == "posix":
208 # Linux, Unix, AIX, etc.
209 # use ~/.config if empty OR not set
210 xdg = env.get("XDG_CONFIG_HOME", None) or os.path.join(get_home_dir(), '.config')
211 if xdg and _writable_dir(xdg):
212 assert isinstance(xdg, str)
213 return xdg
214
215 return None
216
217
218def get_xdg_cache_dir():
219 """Return the XDG_CACHE_HOME, if it is defined and exists, else None.
220
221 This is only for non-OS X posix (Linux,Unix,etc.) systems.
222 """
223
224 env = os.environ
225
226 if os.name == "posix":
227 # Linux, Unix, AIX, etc.
228 # use ~/.cache if empty OR not set
229 xdg = env.get("XDG_CACHE_HOME", None) or os.path.join(get_home_dir(), '.cache')
230 if xdg and _writable_dir(xdg):
231 assert isinstance(xdg, str)
232 return xdg
233
234 return None
235
236
237def expand_path(s):
238 """Expand $VARS and ~names in a string, like a shell
239
240 :Examples:
241
242 In [2]: os.environ['FOO']='test'
243
244 In [3]: expand_path('variable FOO is $FOO')
245 Out[3]: 'variable FOO is test'
246 """
247 # This is a pretty subtle hack. When expand user is given a UNC path
248 # on Windows (\\server\share$\%username%), os.path.expandvars, removes
249 # the $ to get (\\server\share\%username%). I think it considered $
250 # alone an empty var. But, we need the $ to remains there (it indicates
251 # a hidden share).
252 if os.name=='nt':
253 s = s.replace('$\\', 'IPYTHON_TEMP')
254 s = os.path.expandvars(os.path.expanduser(s))
255 if os.name=='nt':
256 s = s.replace('IPYTHON_TEMP', '$\\')
257 return s
258
259
260def unescape_glob(string):
261 """Unescape glob pattern in `string`."""
262 def unescape(s):
263 for pattern in '*[]!?':
264 s = s.replace(r'\{0}'.format(pattern), pattern)
265 return s
266 return '\\'.join(map(unescape, string.split('\\\\')))
267
268
269def shellglob(args):
270 """
271 Do glob expansion for each element in `args` and return a flattened list.
272
273 Unmatched glob pattern will remain as-is in the returned list.
274
275 """
276 expanded = []
277 # Do not unescape backslash in Windows as it is interpreted as
278 # path separator:
279 unescape = unescape_glob if sys.platform != 'win32' else lambda x: x
280 for a in args:
281 expanded.extend(glob.glob(a) or [unescape(a)])
282 return expanded
283
284ENOLINK = 1998
285
286def link(src, dst):
287 """Hard links ``src`` to ``dst``, returning 0 or errno.
288
289 Note that the special errno ``ENOLINK`` will be returned if ``os.link`` isn't
290 supported by the operating system.
291 """
292
293 if not hasattr(os, "link"):
294 return ENOLINK
295 link_errno = 0
296 try:
297 os.link(src, dst)
298 except OSError as e:
299 link_errno = e.errno
300 return link_errno
301
302
303def link_or_copy(src, dst):
304 """Attempts to hardlink ``src`` to ``dst``, copying if the link fails.
305
306 Attempts to maintain the semantics of ``shutil.copy``.
307
308 Because ``os.link`` does not overwrite files, a unique temporary file
309 will be used if the target already exists, then that file will be moved
310 into place.
311 """
312
313 if os.path.isdir(dst):
314 dst = os.path.join(dst, os.path.basename(src))
315
316 link_errno = link(src, dst)
317 if link_errno == errno.EEXIST:
318 if os.stat(src).st_ino == os.stat(dst).st_ino:
319 # dst is already a hard link to the correct file, so we don't need
320 # to do anything else. If we try to link and rename the file
321 # anyway, we get duplicate files - see http://bugs.python.org/issue21876
322 return
323
324 new_dst = dst + "-temp-%04X" %(random.randint(1, 16**4), )
325 try:
326 link_or_copy(src, new_dst)
327 except:
328 try:
329 os.remove(new_dst)
330 except OSError:
331 pass
332 raise
333 os.rename(new_dst, dst)
334 elif link_errno != 0:
335 # Either link isn't supported, or the filesystem doesn't support
336 # linking, or 'src' and 'dst' are on different filesystems.
337 shutil.copy(src, dst)
338
339def ensure_dir_exists(path, mode=0o755):
340 """ensure that a directory exists
341
342 If it doesn't exist, try to create it and protect against a race condition
343 if another process is doing the same.
344
345 The default permissions are 755, which differ from os.makedirs default of 777.
346 """
347 if not os.path.exists(path):
348 try:
349 os.makedirs(path, mode=mode)
350 except OSError as e:
351 if e.errno != errno.EEXIST:
352 raise
353 elif not os.path.isdir(path):
354 raise IOError("%r exists but is not a directory" % path)