1# -*- coding: utf-8 -*-
2
3"""
4Filesystem Operations
5"""
6
7import os
8import pathlib
9import shutil
10import tempfile
11import time
12
13import os.path as osp
14
15from mathics.version import __version__  # noqa used in loading to check consistency.
16
17from mathics.core.expression import (
18    Expression,
19    String,
20    Symbol,
21    SymbolFailed,
22    SymbolFalse,
23    SymbolNull,
24    SymbolTrue,
25    SymbolList,
26    from_python,
27    valid_context_name,
28)
29
30import mathics.core.streams
31from mathics.core.streams import (
32    HOME_DIR,
33    PATH_VAR,
34    ROOT_DIR,
35    create_temporary_file,
36    path_search,
37    urlsave_tmp,
38)
39
40from mathics.builtin.base import Builtin, Predefined
41from mathics.builtin.files_io.files import (
42    DIRECTORY_STACK,
43    INITIAL_DIR,  # noqa is used via global
44    mathics_open
45    )
46from mathics.builtin.numeric import Hash
47from mathics.builtin.strings import to_regex
48from mathics.builtin.base import MessageException
49import re
50
51SYS_ROOT_DIR = "/" if os.name == "posix" else "\\"
52TMP_DIR = tempfile.gettempdir()
53
54
55class AbsoluteFileName(Builtin):
56    """
57    <dl>
58    <dt>'AbsoluteFileName["$name$"]'
59      <dd>returns the absolute version of the given filename.
60    </dl>
61
62    >> AbsoluteFileName["ExampleData/sunflowers.jpg"]
63     = ...
64
65    #> AbsoluteFileName["Some/NonExistant/Path.ext"]
66     : File not found during AbsoluteFileName[Some/NonExistant/Path.ext].
67     = $Failed
68    """
69
70    attributes = "Protected"
71
72    messages = {
73        "fstr": ("File specification x is not a string of one or more characters."),
74        "nffil": "File not found during `1`.",
75    }
76
77    def apply(self, name, evaluation):
78        "AbsoluteFileName[name_]"
79
80        py_name = name.to_python()
81
82        if not (isinstance(py_name, str) and py_name[0] == py_name[-1] == '"'):
83            evaluation.message("AbsoluteFileName", "fstr", name)
84            return
85        py_name = py_name[1:-1]
86
87        result = path_search(py_name)
88
89        if result is None:
90            evaluation.message(
91                "AbsoluteFileName", "nffil", Expression("AbsoluteFileName", name)
92            )
93            return SymbolFailed
94
95        return String(osp.abspath(result))
96
97
98class BaseDirectory(Predefined):
99    """
100    <dl>
101    <dt>'$UserBaseDirectory'
102      <dd>returns the folder where user configurations are stored.
103    </dl>
104
105    >> $RootDirectory
106     = ...
107    """
108
109    name = "$BaseDirectory"
110
111    attributes = "Protected"
112
113    def evaluate(self, evaluation):
114        global ROOT_DIR
115        return String(ROOT_DIR)
116
117
118class CopyDirectory(Builtin):
119    """
120    <dl>
121    <dt>'CopyDirectory["$dir1$", "$dir2$"]'
122      <dd>copies directory $dir1$ to $dir2$.
123    </dl>
124    """
125
126    attributes = "Protected"
127
128    messages = {
129        "argr": "called with `1` argument; 2 arguments are expected.",
130        "fstr": (
131            "File specification `1` is not a string of " "one or more characters."
132        ),
133        "filex": "Cannot overwrite existing file `1`.",
134        "nodir": "Directory `1` not found.",
135    }
136
137    def apply(self, dirs, evaluation):
138        "CopyDirectory[dirs__]"
139
140        seq = dirs.get_sequence()
141        if len(seq) != 2:
142            evaluation.message("CopyDirectory", "argr", len(seq))
143            return
144        (dir1, dir2) = (s.to_python() for s in seq)
145
146        if not (isinstance(dir1, str) and dir1[0] == dir1[-1] == '"'):
147            evaluation.message("CopyDirectory", "fstr", seq[0])
148            return
149        dir1 = dir1[1:-1]
150
151        if not (isinstance(dir2, str) and dir2[0] == dir2[-1] == '"'):
152            evaluation.message("CopyDirectory", "fstr", seq[1])
153            return
154        dir2 = dir2[1:-1]
155
156        if not osp.isdir(dir1):
157            evaluation.message("CopyDirectory", "nodir", seq[0])
158            return SymbolFailed
159        if osp.isdir(dir2):
160            evaluation.message("CopyDirectory", "filex", seq[1])
161            return SymbolFailed
162
163        shutil.copytree(dir1, dir2)
164
165        return String(osp.abspath(dir2))
166
167
168class CopyFile(Builtin):
169    """
170    <dl>
171    <dt>'CopyFile["$file1$", "$file2$"]'
172      <dd>copies $file1$ to $file2$.
173    </dl>
174
175    X> CopyFile["ExampleData/sunflowers.jpg", "MathicsSunflowers.jpg"]
176     = MathicsSunflowers.jpg
177    X> DeleteFile["MathicsSunflowers.jpg"]
178    """
179
180    messages = {
181        "filex": "Cannot overwrite existing file `1`.",
182        "fstr": (
183            "File specification `1` is not a string of " "one or more characters."
184        ),
185        "nffil": "File not found during `1`.",
186    }
187
188    attributes = "Protected"
189
190    def apply(self, source, dest, evaluation):
191        "CopyFile[source_, dest_]"
192
193        py_source = source.to_python()
194        py_dest = dest.to_python()
195
196        # Check filenames
197        if not (isinstance(py_source, str) and py_source[0] == py_source[-1] == '"'):
198            evaluation.message("CopyFile", "fstr", source)
199            return
200        if not (isinstance(py_dest, str) and py_dest[0] == py_dest[-1] == '"'):
201            evaluation.message("CopyFile", "fstr", dest)
202            return
203
204        py_source = py_source[1:-1]
205        py_dest = py_dest[1:-1]
206
207        py_source = path_search(py_source)
208
209        if py_source is None:
210            evaluation.message("CopyFile", "filex", source)
211            return SymbolFailed
212
213        if osp.exists(py_dest):
214            evaluation.message("CopyFile", "filex", dest)
215            return SymbolFailed
216
217        try:
218            shutil.copy(py_source, py_dest)
219        except IOError:
220            evaluation.message(
221                "CopyFile", "nffil", Expression("CopyFile", source, dest)
222            )
223            return SymbolFailed
224
225        return dest
226
227
228class CreateDirectory(Builtin):
229    """
230    <dl>
231    <dt>'CreateDirectory["$dir$"]'
232      <dd>creates a directory called $dir$.
233    <dt>'CreateDirectory[]'
234      <dd>creates a temporary directory.
235    </dl>
236
237    >> dir = CreateDirectory[]
238     = ...
239    #> DirectoryQ[dir]
240     = True
241    #> DeleteDirectory[dir]
242    """
243
244    attributes = ("Listable", "Protected")
245
246    options = {
247        "CreateIntermediateDirectories": "True",
248    }
249
250    messages = {
251        "fstr": (
252            "File specification `1` is not a string of " "one or more characters."
253        ),
254        "nffil": "File not found during `1`.",
255        "filex": "`1` already exists.",
256    }
257
258    def apply(self, dirname, evaluation, options):
259        "CreateDirectory[dirname_, OptionsPattern[CreateDirectory]]"
260
261        expr = Expression("CreateDirectory", dirname)
262        py_dirname = dirname.to_python()
263
264        if not (isinstance(py_dirname, str) and py_dirname[0] == py_dirname[-1] == '"'):
265            evaluation.message("CreateDirectory", "fstr", dirname)
266            return
267
268        py_dirname = py_dirname[1:-1]
269
270        if osp.isdir(py_dirname):
271            evaluation.message("CreateDirectory", "filex", osp.abspath(py_dirname))
272            return
273
274        os.mkdir(py_dirname)
275
276        if not osp.isdir(py_dirname):
277            evaluation.message("CreateDirectory", "nffil", expr)
278            return
279
280        return String(osp.abspath(py_dirname))
281
282    def apply_empty(self, evaluation, options):
283        "CreateDirectory[OptionsPattern[CreateDirectory]]"
284        dirname = tempfile.mkdtemp(prefix="m", dir=TMP_DIR)
285        return String(dirname)
286
287
288class CreateFile(Builtin):
289    """
290    <dl>
291    <dt>'CreateFile["filename"]'
292        <dd>Creates a file named "filename" temporary file, but do not open it.
293    <dt>'CreateFile[]'
294        <dd>Creates a temporary file, but do not open it.
295    </dl>
296    """
297
298    rules = {
299        "CreateFile[]": "CreateTemporary[]",
300    }
301    options = {
302        "CreateIntermediateDirectories": "True",
303        "OverwriteTarget": "True",
304    }
305
306    attributes = ("Listable", "Protected")
307
308    def apply(self, filename, evaluation, **options):
309        "CreateFile[filename_String, OptionsPattern[CreateFile]]"
310        try:
311            # TODO: Implement options
312            if not osp.isfile(filename.value):
313                f = open(filename.value, "w")
314                res = f.name
315                f.close()
316                return String(res)
317            else:
318                return filename
319        except:
320            return SymbolFailed
321
322
323class CreateTemporary(Builtin):
324    """
325    <dl>
326    <dt>'CreateTemporary[]'
327        <dd>Creates a temporary file, but do not open it.
328    </dl>
329    """
330
331    def apply_0(self, evaluation):
332        "CreateTemporary[]"
333        try:
334            res = create_temporary_file()
335        except:
336            return SymbolFailed
337        return String(res)
338
339
340class DeleteDirectory(Builtin):
341    """
342    <dl>
343    <dt>'DeleteDirectory["$dir$"]'
344      <dd>deletes a directory called $dir$.
345    </dl>
346
347    >> dir = CreateDirectory[]
348     = ...
349    >> DeleteDirectory[dir]
350    >> DirectoryQ[dir]
351     = False
352    #> Quiet[DeleteDirectory[dir]]
353     = $Failed
354    """
355
356    attributes = "Protected"
357
358    options = {
359        "DeleteContents": "False",
360    }
361
362    messages = {
363        "strs": (
364            "String or non-empty list of strings expected at " "position 1 in `1`."
365        ),
366        "nodir": "Directory `1` not found.",
367        "dirne": "Directory `1` not empty.",
368        "optx": "Unknown option `1` in `2`",
369        "idcts": "DeleteContents expects either True or False.",  # MMA Bug
370    }
371
372    def apply(self, dirname, evaluation, options):
373        "DeleteDirectory[dirname_, OptionsPattern[DeleteDirectory]]"
374
375        expr = Expression("DeleteDirectory", dirname)
376        py_dirname = dirname.to_python()
377
378        delete_contents = options["System`DeleteContents"].to_python()
379        if delete_contents not in [True, False]:
380            evaluation.message("DeleteDirectory", "idcts")
381            return
382
383        if not (isinstance(py_dirname, str) and py_dirname[0] == py_dirname[-1] == '"'):
384            evaluation.message("DeleteDirectory", "strs", expr)
385            return
386
387        py_dirname = py_dirname[1:-1]
388
389        if not osp.isdir(py_dirname):
390            evaluation.message("DeleteDirectory", "nodir", dirname)
391            return SymbolFailed
392
393        if delete_contents:
394            shutil.rmtree(py_dirname)
395        else:
396            if os.listdir(py_dirname) != []:
397                evaluation.message("DeleteDirectory", "dirne", dirname)
398                return SymbolFailed
399            os.rmdir(py_dirname)
400
401        return SymbolNull
402
403
404class DeleteFile(Builtin):
405    """
406    <dl>
407    <dt>'Delete["$file$"]'
408      <dd>deletes $file$.
409    <dt>'Delete[{"$file1$", "$file2$", ...}]'
410      <dd>deletes a list of files.
411    </dl>
412
413    >> CopyFile["ExampleData/sunflowers.jpg", "MathicsSunflowers.jpg"];
414    >> DeleteFile["MathicsSunflowers.jpg"]
415
416    >> CopyFile["ExampleData/sunflowers.jpg", "MathicsSunflowers1.jpg"];
417    >> CopyFile["ExampleData/sunflowers.jpg", "MathicsSunflowers2.jpg"];
418    >> DeleteFile[{"MathicsSunflowers1.jpg", "MathicsSunflowers2.jpg"}]
419    """
420
421    messages = {
422        "filex": "Cannot overwrite existing file `1`.",
423        "strs": (
424            "String or non-empty list of strings expected at " "position `1` in `2`."
425        ),
426        "nffil": "File not found during `1`.",
427    }
428
429    attributes = "Protected"
430
431    def apply(self, filename, evaluation):
432        "DeleteFile[filename_]"
433
434        py_path = filename.to_python()
435        if not isinstance(py_path, list):
436            py_path = [py_path]
437
438        py_paths = []
439        for path in py_path:
440            # Check filenames
441            if not (isinstance(path, str) and path[0] == path[-1] == '"'):
442                evaluation.message(
443                    "DeleteFile", "strs", filename, Expression("DeleteFile", filename)
444                )
445                return
446
447            path = path[1:-1]
448            path = path_search(path)
449
450            if path is None:
451                evaluation.message(
452                    "DeleteFile", "nffil", Expression("DeleteFile", filename)
453                )
454                return SymbolFailed
455            py_paths.append(path)
456
457        for path in py_paths:
458            try:
459                os.remove(path)
460            except OSError:
461                return SymbolFailed
462
463        return SymbolNull
464
465
466class Directory(Builtin):
467    """
468    <dl>
469    <dt>'Directory[]'
470      <dd>returns the current working directory.
471    </dl>
472
473    >> Directory[]
474    = ...
475    """
476
477    attributes = "Protected"
478
479    def apply(self, evaluation):
480        "Directory[]"
481        result = os.getcwd()
482        return String(result)
483
484
485class DirectoryName(Builtin):
486    """
487    <dl>
488    <dt>'DirectoryName["$name$"]'
489      <dd>extracts the directory name from a filename.
490    </dl>
491
492    >> DirectoryName["a/b/c"]
493     = a/b
494
495    >> DirectoryName["a/b/c", 2]
496     = a
497
498    #> DirectoryName["a/b/c", 3] // InputForm
499     = ""
500    #> DirectoryName[""] // InputForm
501     = ""
502
503    #> DirectoryName["a/b/c", x]
504     : Positive machine-sized integer expected at position 2 in DirectoryName[a/b/c, x].
505     = DirectoryName[a/b/c, x]
506
507    #> DirectoryName["a/b/c", -1]
508     : Positive machine-sized integer expected at position 2 in DirectoryName[a/b/c, -1].
509     = DirectoryName[a/b/c, -1]
510
511    #> DirectoryName[x]
512     : String expected at position 1 in DirectoryName[x].
513     = DirectoryName[x]
514    """
515
516    attributes = "Protected"
517
518    options = {
519        "OperatingSystem": "$OperatingSystem",
520    }
521
522    messages = {
523        "string": "String expected at position 1 in `1`.",
524        "intpm": ("Positive machine-sized integer expected at " "position 2 in `1`."),
525    }
526
527    def apply_with_n(self, name, n, evaluation, options):
528        "DirectoryName[name_, n_, OptionsPattern[DirectoryName]]"
529
530        if n is None:
531            expr = Expression("DirectoryName", name)
532            py_n = 1
533        else:
534            expr = Expression("DirectoryName", name, n)
535            py_n = n.to_python()
536
537        if not (isinstance(py_n, int) and py_n > 0):
538            evaluation.message("DirectoryName", "intpm", expr)
539            return
540
541        py_name = name.to_python()
542        if not (isinstance(py_name, str) and py_name[0] == py_name[-1] == '"'):
543            evaluation.message("DirectoryName", "string", expr)
544            return
545        py_name = py_name[1:-1]
546
547        result = py_name
548        for i in range(py_n):
549            (result, tmp) = osp.split(result)
550
551        return String(result)
552
553    def apply(self, name, evaluation, options):
554        "DirectoryName[name_, OptionsPattern[DirectoryName]]"
555        return self.apply_with_n(name, None, evaluation, options)
556
557
558class DirectoryStack(Builtin):
559    """
560    <dl>
561    <dt>'DirectoryStack[]'
562      <dd>returns the directory stack.
563    </dl>
564
565    >> DirectoryStack[]
566    = ...
567    """
568
569    attributes = "Protected"
570
571    def apply(self, evaluation):
572        "DirectoryStack[]"
573        global DIRECTORY_STACK
574        return from_python(DIRECTORY_STACK)
575
576
577class DirectoryQ(Builtin):
578    """
579    <dl>
580    <dt>'DirectoryQ["$name$"]'
581      <dd>returns 'True' if the directory called $name$ exists and 'False' otherwise.
582    </dl>
583
584    >> DirectoryQ["ExampleData/"]
585     = True
586    >> DirectoryQ["ExampleData/MythicalSubdir/"]
587     = False
588
589    #> DirectoryQ["ExampleData"]
590     = True
591
592    #> DirectoryQ["ExampleData/MythicalSubdir/NestedDir/"]
593     = False
594    """
595
596    messages = {
597        "fstr": (
598            "File specification `1` is not a string of " "one or more characters."
599        ),
600    }
601
602    attributes = "Protected"
603
604    def apply(self, pathname, evaluation):
605        "DirectoryQ[pathname_]"
606        path = pathname.to_python()
607
608        if not (isinstance(path, str) and path[0] == path[-1] == '"'):
609            evaluation.message("DirectoryQ", "fstr", pathname)
610            return
611        path = path[1:-1]
612
613        path = path_search(path)
614
615        if path is not None and osp.isdir(path):
616            return SymbolTrue
617        return SymbolFalse
618
619
620class ExpandFileName(Builtin):
621    """
622    <dl>
623    <dt>'ExpandFileName["$name$"]'
624      <dd>expands $name$ to an absolute filename for your system.
625    </dl>
626
627    >> ExpandFileName["ExampleData/sunflowers.jpg"]
628     = ...
629    """
630
631    attributes = "Protected"
632
633    messages = {
634        "string": "String expected at position 1 in `1`.",
635    }
636
637    def apply(self, name, evaluation):
638        "ExpandFileName[name_]"
639
640        py_name = name.to_python()
641
642        if not (isinstance(py_name, str) and py_name[0] == py_name[-1] == '"'):
643            evaluation.message(
644                "ExpandFileName", "string", Expression("ExpandFileName", name)
645            )
646            return
647        py_name = py_name[1:-1]
648
649        return String(osp.abspath(py_name))
650
651
652class File(Builtin):
653    attributes = "Protected"
654
655
656class FileBaseName(Builtin):
657    """
658    <dl>
659    <dt>'FileBaseName["$file$"]'
660      <dd>gives the base name for the specified file name.
661    </dl>
662
663    >> FileBaseName["file.txt"]
664     = file
665
666    >> FileBaseName["file.tar.gz"]
667     = file.tar
668
669    #> FileBaseName["file."]
670     = file
671
672    #> FileBaseName["file"]
673     = file
674    """
675
676    attributes = "Protected"
677
678    options = {
679        "OperatingSystem": "$OperatingSystem",
680    }
681
682    def apply(self, filename, evaluation, options):
683        "FileBaseName[filename_String, OptionsPattern[FileBaseName]]"
684        path = filename.to_python()[1:-1]
685
686        filename_base, filename_ext = osp.splitext(path)
687        return from_python(filename_base)
688
689
690class FileByteCount(Builtin):
691    """
692    <dl>
693    <dt>'FileByteCount[$file$]'
694      <dd>returns the number of bytes in $file$.
695    </dl>
696
697    >> FileByteCount["ExampleData/sunflowers.jpg"]
698     = 142286
699    """
700
701    messages = {
702        "fstr": "File specification `1` is not a string of one or more characters.",
703    }
704
705    def apply(self, filename, evaluation):
706        "FileByteCount[filename_]"
707        py_filename = filename.to_python()
708        if not (
709            isinstance(py_filename, str) and py_filename[0] == py_filename[-1] == '"'
710        ):
711            evaluation.message("FileByteCount", "fstr", filename)
712            return
713        py_filename = py_filename[1:-1]
714
715        try:
716            with mathics_open(py_filename, "rb") as f:
717                count = 0
718                tmp = f.read(1)
719                while tmp != b"":
720                    count += 1
721                    tmp = f.read(1)
722
723        except IOError:
724            evaluation.message("General", "noopen", filename)
725            return
726        except MessageException as e:
727            e.message(evaluation)
728            return
729
730        return from_python(count)
731
732
733class FileDate(Builtin):
734    """
735    <dl>
736    <dt>'FileDate[$file$, $types$]'
737      <dd>returns the time and date at which the file was last modified.
738    </dl>
739
740    >> FileDate["ExampleData/sunflowers.jpg"]
741     = ...
742
743    >> FileDate["ExampleData/sunflowers.jpg", "Access"]
744     = ...
745
746    >> FileDate["ExampleData/sunflowers.jpg", "Creation"]
747     = ...
748
749    >> FileDate["ExampleData/sunflowers.jpg", "Change"]
750     = ...
751
752    >> FileDate["ExampleData/sunflowers.jpg", "Modification"]
753     = ...
754
755    >>  FileDate["ExampleData/sunflowers.jpg", "Rules"]
756     = ...
757
758    #>  FileDate["MathicsNonExistantExample"]
759     : File not found during FileDate[MathicsNonExistantExample].
760     = FileDate[MathicsNonExistantExample]
761    #>  FileDate["MathicsNonExistantExample", "Modification"]
762     : File not found during FileDate[MathicsNonExistantExample, Modification].
763     = FileDate[MathicsNonExistantExample, Modification]
764
765    #> FileDate["ExampleData/sunflowers.jpg", "Fail"]
766     : Date type Fail should be "Access", "Modification", "Creation" (Windows only), "Change" (Macintosh and Unix only), or "Rules".
767     = FileDate[ExampleData/sunflowers.jpg, Fail]
768    """
769
770    messages = {
771        "nffil": "File not found during `1`.",
772        "datetype": (
773            'Date type Fail should be "Access", "Modification", '
774            '"Creation" (Windows only), '
775            '"Change" (Macintosh and Unix only), or "Rules".'
776        ),
777    }
778
779    rules = {
780        'FileDate[filepath_String, "Rules"]': """{"Access" -> FileDate[filepath, "Access"],
781            "Creation" -> FileDate[filepath, "Creation"],
782            "Change" -> FileDate[filepath, "Change"],
783            "Modification" -> FileDate[filepath, "Modification"]}""",
784    }
785
786    attributes = "Protected"
787
788    def apply(self, path, timetype, evaluation):
789        "FileDate[path_, timetype_]"
790        py_path = path_search(path.to_python()[1:-1])
791
792        if py_path is None:
793            if timetype is None:
794                evaluation.message("FileDate", "nffil", Expression("FileDate", path))
795            else:
796                evaluation.message(
797                    "FileDate", "nffil", Expression("FileDate", path, timetype)
798                )
799            return
800
801        if timetype is None:
802            time_type = "Modification"
803        else:
804            time_type = timetype.to_python()[1:-1]
805
806        if time_type == "Access":
807            result = osp.getatime(py_path)
808        elif time_type == "Creation":
809            if os.name == "posix":
810                return Expression("Missing", "NotApplicable")
811            result = osp.getctime(py_path)
812        elif time_type == "Change":
813            if os.name != "posix":
814                return Expression("Missing", "NotApplicable")
815            result = osp.getctime(py_path)
816        elif time_type == "Modification":
817            result = osp.getmtime(py_path)
818        else:
819            evaluation.message("FileDate", "datetype")
820            return
821
822        # Offset for system epoch
823        epochtime = Expression(
824            "AbsoluteTime", time.strftime("%Y-%m-%d %H:%M", time.gmtime(0))
825        ).to_python(n_evaluation=evaluation)
826        result += epochtime
827
828        return Expression("DateList", from_python(result))
829
830    def apply_default(self, path, evaluation):
831        "FileDate[path_]"
832        return self.apply(path, None, evaluation)
833
834
835class FileExistsQ(Builtin):
836    """
837    <dl>
838    <dt>'FileExistsQ["$file$"]'
839      <dd>returns 'True' if $file$ exists and 'False' otherwise.
840    </dl>
841
842    >> FileExistsQ["ExampleData/sunflowers.jpg"]
843     = True
844    >> FileExistsQ["ExampleData/sunflowers.png"]
845     = False
846    """
847
848    messages = {
849        "fstr": (
850            "File specification `1` is not a string of " "one or more characters."
851        ),
852    }
853
854    attributes = "Protected"
855
856    def apply(self, filename, evaluation):
857        "FileExistsQ[filename_]"
858        path = filename.to_python()
859        if not (isinstance(path, str) and path[0] == path[-1] == '"'):
860            evaluation.message("FileExistsQ", "fstr", filename)
861            return
862        path = path[1:-1]
863
864        path = path_search(path)
865
866        if path is None:
867            return SymbolFalse
868        return SymbolTrue
869
870
871class FileExtension(Builtin):
872    """
873    <dl>
874    <dt>'FileExtension["$file$"]'
875      <dd>gives the extension for the specified file name.
876    </dl>
877
878    >> FileExtension["file.txt"]
879     = txt
880
881    >> FileExtension["file.tar.gz"]
882     = gz
883
884    #> FileExtension["file."]
885     = #<--#
886    #> FileExtension["file"]
887     = #<--#
888    """
889
890    attributes = "Protected"
891
892    options = {
893        "OperatingSystem": "$OperatingSystem",
894    }
895
896    def apply(self, filename, evaluation, options):
897        "FileExtension[filename_String, OptionsPattern[FileExtension]]"
898        path = filename.to_python()[1:-1]
899        filename_base, filename_ext = osp.splitext(path)
900        filename_ext = filename_ext.lstrip(".")
901        return from_python(filename_ext)
902
903
904class FileHash(Builtin):
905    """
906    <dl>
907    <dt>'FileHash[$file$]'
908      <dd>returns an integer hash for the given $file$.
909    <dt>'FileHash[$file$, $type$]'
910      <dd>returns an integer hash of the specified $type$ for the given $file$.</dd>
911      <dd>The types supported are "MD5", "Adler32", "CRC32", "SHA", "SHA224", "SHA256", "SHA384", and "SHA512".</dd>
912    <dt>'FileHash[$file$, $type$, $format$]'
913      <dd>gives a hash code in the specified format.</dd>
914    </dl>
915
916    >> FileHash["ExampleData/sunflowers.jpg"]
917     = 109937059621979839952736809235486742106
918
919    >> FileHash["ExampleData/sunflowers.jpg", "MD5"]
920     = 109937059621979839952736809235486742106
921
922    >> FileHash["ExampleData/sunflowers.jpg", "Adler32"]
923     = 1607049478
924
925    >> FileHash["ExampleData/sunflowers.jpg", "SHA256"]
926     = 111619807552579450300684600241129773909359865098672286468229443390003894913065
927
928    #> FileHash["ExampleData/sunflowers.jpg", "CRC32"]
929     = 933095683
930    #> FileHash["ExampleData/sunflowers.jpg", "SHA"]
931     = 851696818771101405642332645949480848295550938123
932    #> FileHash["ExampleData/sunflowers.jpg", "SHA224"]
933     = 8723805623766373862936267623913366865806344065103917676078120867011
934    #> FileHash["ExampleData/sunflowers.jpg", "SHA384"]
935     = 28288410602533803613059815846847184383722061845493818218404754864571944356226472174056863474016709057507799332611860
936    #> FileHash["ExampleData/sunflowers.jpg", "SHA512"]
937     = 10111462070211820348006107532340854103555369343736736045463376555356986226454343186097958657445421102793096729074874292511750542388324853755795387877480102
938
939    #> FileHash["ExampleData/sunflowers.jpg", xyzsymbol]
940     = FileHash[ExampleData/sunflowers.jpg, xyzsymbol]
941    #> FileHash["ExampleData/sunflowers.jpg", "xyzstr"]
942     = FileHash[ExampleData/sunflowers.jpg, xyzstr, Integer]
943    #> FileHash[xyzsymbol]
944     = FileHash[xyzsymbol]
945    """
946
947    rules = {
948        "FileHash[filename_String]": 'FileHash[filename, "MD5", "Integer"]',
949        "FileHash[filename_String, hashtype_String]": 'FileHash[filename, hashtype, "Integer"]',
950    }
951
952    attributes = ("Protected", "ReadProtected")
953
954    def apply(self, filename, hashtype, format, evaluation):
955        "FileHash[filename_String, hashtype_String, format_String]"
956        py_filename = filename.get_string_value()
957
958        try:
959            with mathics_open(py_filename, "rb") as f:
960                dump = f.read()
961        except IOError:
962            evaluation.message("General", "noopen", filename)
963            return
964        except MessageException as e:
965            e.message(evaluation)
966            return
967
968        return Hash.compute(
969            lambda update: update(dump),
970            hashtype.get_string_value(),
971            format.get_string_value(),
972        )
973
974
975class FileInformation(Builtin):
976    """
977    <dl>
978    <dt>'FileInformation["$file$"]'
979      <dd>returns information about $file$.
980    </dl>
981
982    This function is totally undocumented in MMA!
983
984    >> FileInformation["ExampleData/sunflowers.jpg"]
985     = {File -> ..., FileType -> File, ByteCount -> 142286, Date -> ...}
986
987    #> FileInformation["ExampleData/missing_file.jpg"]
988     = {}
989    """
990
991    rules = {
992        "FileInformation[name_String]": "If[FileExistsQ[name], {File -> ExpandFileName[name], FileType -> FileType[name], ByteCount -> FileByteCount[name], Date -> AbsoluteTime[FileDate[name]]}, {}]",
993    }
994
995
996class FileNameDepth(Builtin):
997    """
998    <dl>
999    <dt>'FileNameDepth["$name$"]'
1000      <dd>gives the number of path parts in the given filename.
1001    </dl>
1002
1003    >> FileNameDepth["a/b/c"]
1004     = 3
1005
1006    >> FileNameDepth["a/b/c/"]
1007     = 3
1008
1009    #> FileNameDepth[x]
1010     = FileNameDepth[x]
1011
1012    #> FileNameDepth[$RootDirectory]
1013     = 0
1014    """
1015
1016    attributes = "Protected"
1017
1018    options = {
1019        "OperatingSystem": "$OperatingSystem",
1020    }
1021
1022    rules = {
1023        "FileNameDepth[name_String]": "Length[FileNameSplit[name]]",
1024    }
1025
1026
1027class FileNameJoin(Builtin):
1028    """
1029    <dl>
1030      <dt>'FileNameJoin[{"$dir_1$", "$dir_2$", ...}]'
1031      <dd>joins the $dir_i$ together into one path.
1032
1033      <dt>'FileNameJoin[..., OperatingSystem->"os"]'
1034      <dd>yields a file name in the format for the specified operating system. Possible choices are "Windows", "MacOSX", and "Unix".
1035    </dl>
1036
1037    >> FileNameJoin[{"dir1", "dir2", "dir3"}]
1038     = ...
1039
1040    >> FileNameJoin[{"dir1", "dir2", "dir3"}, OperatingSystem -> "Unix"]
1041     = dir1/dir2/dir3
1042
1043    >> FileNameJoin[{"dir1", "dir2", "dir3"}, OperatingSystem -> "Windows"]
1044     = dir1\\dir2\\dir3
1045    """
1046
1047    attributes = "Protected"
1048
1049    options = {
1050        "OperatingSystem": "$OperatingSystem",
1051    }
1052
1053    messages = {
1054        "ostype": (
1055            "The value of option OperatingSystem -> `1` "
1056            'must be one of "MacOSX", "Windows", or "Unix".'
1057        ),
1058    }
1059
1060    def apply(self, pathlist, evaluation, options):
1061        "FileNameJoin[pathlist_?ListQ, OptionsPattern[FileNameJoin]]"
1062
1063        py_pathlist = pathlist.to_python()
1064        if not all(isinstance(p, str) and p[0] == p[-1] == '"' for p in py_pathlist):
1065            return
1066        py_pathlist = [p[1:-1] for p in py_pathlist]
1067
1068        operating_system = (
1069            options["System`OperatingSystem"].evaluate(evaluation).get_string_value()
1070        )
1071
1072        if operating_system not in ["MacOSX", "Windows", "Unix"]:
1073            evaluation.message(
1074                "FileNameSplit", "ostype", options["System`OperatingSystem"]
1075            )
1076            if os.name == "posix":
1077                operating_system = "Unix"
1078            elif os.name == "nt":
1079                operating_system = "Windows"
1080            elif os.name == "os2":
1081                operating_system = "MacOSX"
1082            else:
1083                return
1084
1085        if operating_system in ("Unix", "MacOSX"):
1086            import posixpath
1087
1088            result = posixpath.join(*py_pathlist)
1089        elif operating_system in ("Windows",):
1090            import ntpath
1091
1092            result = ntpath.join(*py_pathlist)
1093        else:
1094            result = osp.join(*py_pathlist)
1095
1096        return from_python(result)
1097
1098
1099class FileType(Builtin):
1100    """
1101    <dl>
1102    <dt>'FileType["$file$"]'
1103      <dd>gives the type of a file, a string. This is typically 'File', 'Directory' or 'None'.
1104    </dl>
1105
1106    >> FileType["ExampleData/sunflowers.jpg"]
1107     = File
1108    >> FileType["ExampleData"]
1109     = Directory
1110    >> FileType["ExampleData/nonexistant"]
1111     = None
1112
1113    #> FileType[x]
1114     : File specification x is not a string of one or more characters.
1115     = FileType[x]
1116    """
1117
1118    messages = {
1119        "fstr": (
1120            "File specification `1` is not a string of " "one or more characters."
1121        ),
1122    }
1123
1124    attributes = "Protected"
1125
1126    def apply(self, filename, evaluation):
1127        "FileType[filename_]"
1128        if not isinstance(filename, String):
1129            evaluation.message("FileType", "fstr", filename)
1130            return
1131        path = filename.to_python()[1:-1]
1132
1133        path = path_search(path)
1134
1135        if path is None:
1136            return Symbol("None")
1137
1138        if osp.isfile(path):
1139            return Symbol("File")
1140        else:
1141            return Symbol("Directory")
1142
1143
1144class FindFile(Builtin):
1145    """
1146    <dl>
1147    <dt>'FindFile[$name$]'
1148      <dd>searches '$Path' for the given filename.
1149    </dl>
1150
1151    >> FindFile["ExampleData/sunflowers.jpg"]
1152     = ...
1153
1154    >> FindFile["VectorAnalysis`"]
1155     = ...
1156
1157    >> FindFile["VectorAnalysis`VectorAnalysis`"]
1158     = ...
1159
1160    #> FindFile["SomeTypoPackage`"]
1161     = $Failed
1162    """
1163
1164    attributes = "Protected"
1165
1166    messages = {
1167        "string": "String expected at position 1 in `1`.",
1168    }
1169
1170    def apply(self, name, evaluation):
1171        "FindFile[name_]"
1172
1173        py_name = name.to_python()
1174
1175        if not (isinstance(py_name, str) and py_name[0] == py_name[-1] == '"'):
1176            evaluation.message("FindFile", "string", Expression("FindFile", name))
1177            return
1178        py_name = py_name[1:-1]
1179
1180        result = path_search(py_name)
1181
1182        if result is None:
1183            return SymbolFailed
1184
1185        return String(osp.abspath(result))
1186
1187
1188class FileNames(Builtin):
1189    r"""
1190    <dl>
1191    <dt>'FileNames[]'
1192        <dd>Returns a list with the filenames in the current working folder.
1193    <dt>'FileNames[$form$]'
1194        <dd>Returns a list with the filenames in the current working folder that matches with $form$.
1195    <dt>'FileNames[{$form_1$, $form_2$, ...}]'
1196        <dd>Returns a list with the filenames in the current working folder that matches with one of $form_1$, $form_2$, ....
1197    <dt>'FileNames[{$form_1$, $form_2$, ...},{$dir_1$, $dir_2$, ...}]'
1198        <dd>Looks into the directories $dir_1$, $dir_2$, ....
1199    <dt>'FileNames[{$form_1$, $form_2$, ...},{$dir_1$, $dir_2$, ...}]'
1200        <dd>Looks into the directories $dir_1$, $dir_2$, ....
1201    <dt>'FileNames[{$forms$, $dirs$, $n$]'
1202        <dd>Look for files up to the level $n$.
1203    </dl>
1204
1205    >> SetDirectory[$InstallationDirectory <> "/autoload"];
1206    >> FileNames["*.m", "formats"]//Length
1207     = 0
1208    >> FileNames["*.m", "formats", 3]//Length
1209     = 13
1210    >> FileNames["*.m", "formats", Infinity]//Length
1211     = 13
1212    """
1213    # >> FileNames[]//Length
1214    #  = 2
1215    fmtmaps = {Symbol("System`All"): "*"}
1216    options = {
1217        "IgnoreCase": "Automatic",
1218    }
1219
1220    messages = {
1221        "nofmtstr": "`1` is not a format or a list of formats.",
1222        "nodirstr": "`1` is not a directory name  or a list of directory names.",
1223        "badn": "`1` is not an integer number.",
1224    }
1225
1226    def apply_0(self, evaluation, **options):
1227        """FileNames[OptionsPattern[FileNames]]"""
1228        return self.apply_3(
1229            String("*"), String(os.getcwd()), None, evaluation, **options
1230        )
1231
1232    def apply_1(self, forms, evaluation, **options):
1233        """FileNames[forms_, OptionsPattern[FileNames]]"""
1234        return self.apply_3(forms, String(os.getcwd()), None, evaluation, **options)
1235
1236    def apply_2(self, forms, paths, evaluation, **options):
1237        """FileNames[forms_, paths_, OptionsPattern[FileNames]]"""
1238        return self.apply_3(forms, paths, None, evaluation, **options)
1239
1240    def apply_3(self, forms, paths, n, evaluation, **options):
1241        """FileNames[forms_, paths_, n_, OptionsPattern[FileNames]]"""
1242        filenames = set()
1243        # Building a list of forms
1244        if forms.get_head_name() == "System`List":
1245            str_forms = []
1246            for p in forms._leaves:
1247                if self.fmtmaps.get(p, None):
1248                    str_forms.append(self.fmtmaps[p])
1249                else:
1250                    str_forms.append(p)
1251        else:
1252            str_forms = [
1253                self.fmtmaps[forms] if self.fmtmaps.get(forms, None) else forms
1254            ]
1255        # Building a list of directories
1256        if paths.get_head_name() == "System`String":
1257            str_paths = [paths.value]
1258        elif paths.get_head_name() == "System`List":
1259            str_paths = []
1260            for p in paths._leaves:
1261                if p.get_head_name() == "System`String":
1262                    str_paths.append(p.value)
1263                else:
1264                    evaluation.message("FileNames", "nodirstr", paths)
1265                    return
1266        else:
1267            evaluation.message("FileNames", "nodirstr", paths)
1268            return
1269
1270        if n is not None:
1271            if n.get_head_name() == "System`Integer":
1272                n = n.get_int_value()
1273            elif n.get_head_name() == "System`DirectedInfinity":
1274                n = None
1275            else:
1276                evaluation.message("FileNames", "badn", n)
1277                return
1278        else:
1279            n = 1
1280
1281        # list the files
1282        if options.get("System`IgnoreCase", None) == SymbolTrue:
1283            patterns = [
1284                re.compile(
1285                    "^" + to_regex(p, evaluation, abbreviated_patterns=True),
1286                    re.IGNORECASE,
1287                )
1288                + "$"
1289                for p in str_forms
1290            ]
1291        else:
1292            patterns = [
1293                re.compile(
1294                    "^" + to_regex(p, evaluation, abbreviated_patterns=True) + "$"
1295                )
1296                for p in str_forms
1297            ]
1298
1299        for path in str_paths:
1300            if not osp.isdir(path):
1301                continue
1302            if n == 1:
1303                for fn in os.listdir(path):
1304                    fullname = osp.join(path, fn)
1305                    for pattern in patterns:
1306                        if pattern.match(fn):
1307                            filenames.add(fullname)
1308                            break
1309            else:
1310                pathlen = len(path)
1311                for root, dirs, files in os.walk(path):
1312                    # FIXME: This is an ugly and inefficient way
1313                    # to avoid looking deeper than the level n, but I do not realize
1314                    # how to do this better without a lot of code...
1315                    if n is not None and len(root[pathlen:].split(osp.sep)) > n:
1316                        continue
1317                    for fn in files + dirs:
1318                        for pattern in patterns:
1319                            if pattern.match(fn):
1320                                filenames.add(osp.join(root, fn))
1321                                break
1322
1323        return Expression("List", *[String(s) for s in sorted(filenames)])
1324
1325
1326class FileNameSplit(Builtin):
1327    """
1328    <dl>
1329    <dt>'FileNameSplit["$filenams$"]'
1330      <dd>splits a $filename$ into a list of parts.
1331    </dl>
1332
1333    >> FileNameSplit["example/path/file.txt"]
1334     = {example, path, file.txt}
1335
1336    #> FileNameSplit["example/path", OperatingSystem -> x]
1337     : The value of option OperatingSystem -> x must be one of "MacOSX", "Windows", or "Unix".
1338     = {example, path}
1339    """
1340
1341    attributes = "Protected"
1342
1343    options = {
1344        "OperatingSystem": "$OperatingSystem",
1345    }
1346
1347    messages = {
1348        "ostype": (
1349            "The value of option OperatingSystem -> `1` "
1350            'must be one of "MacOSX", "Windows", or "Unix".'
1351        ),
1352    }
1353
1354    def apply(self, filename, evaluation, options):
1355        "FileNameSplit[filename_String, OptionsPattern[FileNameSplit]]"
1356
1357        path = filename.to_python()[1:-1]
1358
1359        operating_system = (
1360            options["System`OperatingSystem"].evaluate(evaluation).to_python()
1361        )
1362
1363        if operating_system not in ['"MacOSX"', '"Windows"', '"Unix"']:
1364            evaluation.message(
1365                "FileNameSplit", "ostype", options["System`OperatingSystem"]
1366            )
1367            if os.name == "posix":
1368                operating_system = "Unix"
1369            elif os.name == "nt":
1370                operating_system = "Windows"
1371            elif os.name == "os2":
1372                operating_system = "MacOSX"
1373            else:
1374                return
1375
1376        # TODO Implement OperatingSystem Option
1377
1378        result = []
1379        while path not in ["", SYS_ROOT_DIR]:
1380            path, ext = osp.split(path)
1381            if ext != "":
1382                result.insert(0, ext)
1383
1384        return from_python(result)
1385
1386
1387class FileNameTake(Builtin):
1388    """
1389    <dl>
1390      <dt>'FileNameTake["$file$"]'
1391      <dd>returns the last path element in the file name $name$.
1392
1393      <dt>'FileNameTake["$file$", $n$]'
1394      <dd>returns the first $n$ path elements in the file name $name$.
1395
1396      <dt>'FileNameTake["$file$", $-n$]'
1397      <dd>returns the last $n$ path elements in the file name $name$.
1398    </dl>
1399
1400    """
1401
1402    # mmatura: please put in a pytest
1403    # >> FileNameTake["/tmp/file.txt"]
1404    #  = file.txt
1405    # >> FileNameTake["tmp/file.txt", 1]
1406    #  = tmp
1407    # >> FileNameTake["tmp/file.txt", -1]
1408    #  = file.txt
1409
1410    attributes = "Protected"
1411
1412    options = {
1413        "OperatingSystem": "$OperatingSystem",
1414    }
1415
1416    def apply(self, filename, evaluation, options):
1417        "FileNameTake[filename_String, OptionsPattern[FileBaseName]]"
1418        path = pathlib.Path(filename.to_python()[1:-1])
1419        return from_python(path.name)
1420
1421    def apply_n(self, filename, n, evaluation, options):
1422        "FileNameTake[filename_String, n_Integer, OptionsPattern[FileBaseName]]"
1423        n_int = n.get_int_value()
1424        parts = pathlib.Path(filename.to_python()[1:-1]).parts
1425        if n_int >= 0:
1426            subparts = parts[:n_int]
1427        else:
1428            subparts = parts[n_int:]
1429        return from_python(str(pathlib.PurePath(*subparts)))
1430
1431
1432class FindList(Builtin):
1433    """
1434    <dl>
1435    <dt>'FindList[$file$, $text$]'
1436      <dd>returns a list of all lines in $file$ that contain $text$.
1437    <dt>'FindList[$file$, {$text1$, $text2$, ...}]'
1438      <dd>returns a list of all lines in $file$ that contain any of the specified string.
1439    <dt>'FindList[{$file1$, $file2$, ...}, ...]'
1440      <dd>returns a list of all lines in any of the $filei$ that contain the specified strings.
1441    </dl>
1442
1443    >> stream = FindList["ExampleData/EinsteinSzilLetter.txt", "uranium"];
1444    #> Length[stream]
1445     = 7
1446
1447    >> FindList["ExampleData/EinsteinSzilLetter.txt", "uranium", 1]
1448     = {in manuscript, leads me to expect that the element uranium may be turned into}
1449
1450    #> FindList["ExampleData/EinsteinSzilLetter.txt", "project"]
1451     = {}
1452
1453    #> FindList["ExampleData/EinsteinSzilLetter.txt", "uranium", 0]
1454     = $Failed
1455    """
1456
1457    messages = {
1458        "strs": "String or non-empty list of strings expected at position `1` in `2`.",
1459        "intnm": "Non-negative machine-sized integer expected at position `1` in `2`.",
1460    }
1461
1462    attributes = "Protected"
1463
1464    options = {
1465        "AnchoredSearch": "False",
1466        "IgnoreCase": "False",
1467        "RecordSeparators": '{"\r\n", "\n", "\r"}',
1468        "WordSearch": "False",
1469        "WordSeparators": '{" ", "\t"}',
1470    }
1471
1472    # TODO: Extra options AnchoredSearch, IgnoreCase RecordSeparators,
1473    # WordSearch, WordSeparators this is probably best done with a regex
1474
1475    def apply_without_n(self, filename, text, evaluation, options):
1476        "FindList[filename_, text_, OptionsPattern[FindList]]"
1477        return self.apply(filename, text, None, evaluation, options)
1478
1479    def apply(self, filename, text, n, evaluation, options):
1480        "FindList[filename_, text_, n_, OptionsPattern[FindList]]"
1481        py_text = text.to_python()
1482        py_name = filename.to_python()
1483        if n is None:
1484            py_n = None
1485            expr = Expression("FindList", filename, text)
1486        else:
1487            py_n = n.to_python()
1488            expr = Expression("FindList", filename, text, n)
1489
1490        if not isinstance(py_text, list):
1491            py_text = [py_text]
1492
1493        if not isinstance(py_name, list):
1494            py_name = [py_name]
1495
1496        if not all(isinstance(t, str) and t[0] == t[-1] == '"' for t in py_name):
1497            evaluation.message("FindList", "strs", "1", expr)
1498            return SymbolFailed
1499
1500        if not all(isinstance(t, str) and t[0] == t[-1] == '"' for t in py_text):
1501            evaluation.message("FindList", "strs", "2", expr)
1502            return SymbolFailed
1503
1504        if not ((isinstance(py_n, int) and py_n >= 0) or py_n is None):
1505            evaluation.message("FindList", "intnm", "3", expr)
1506            return SymbolFailed
1507
1508        if py_n == 0:
1509            return SymbolFailed
1510
1511        py_text = [t[1:-1] for t in py_text]
1512        py_name = [t[1:-1] for t in py_name]
1513
1514        results = []
1515        for path in py_name:
1516            try:
1517                with mathics_open(path, "r") as f:
1518                    lines = f.readlines()
1519            except IOError:
1520                evaluation.message("General", "noopen", path)
1521                return
1522            except MessageException as e:
1523                e.message(evaluation)
1524                return
1525
1526            result = []
1527            for line in lines:
1528                for t in py_text:
1529                    if line.find(t) != -1:
1530                        result.append(line[:-1])
1531            results.append(result)
1532
1533        results = [r for result in results for r in result]
1534
1535        if isinstance(py_n, int):
1536            results = results[: min(py_n, len(results))]
1537
1538        return from_python(results)
1539
1540
1541class HomeDirectory(Predefined):
1542    """
1543    <dl>
1544    <dt>'$HomeDirectory'
1545      <dd>returns the users HOME directory.
1546    </dl>
1547
1548    >> $HomeDirectory
1549     = ...
1550    """
1551
1552    name = "$HomeDirectory"
1553
1554    attributes = "Protected"
1555
1556    def evaluate(self, evaluation):
1557        global HOME_DIR
1558        return String(HOME_DIR)
1559
1560
1561class InitialDirectory(Predefined):
1562    """
1563    <dl>
1564    <dt>'$InitialDirectory'
1565      <dd>returns the directory from which \\Mathics was started.
1566    </dl>
1567
1568    >> $InitialDirectory
1569     = ...
1570    """
1571
1572    name = "$InitialDirectory"
1573
1574    def evaluate(self, evaluation):
1575        global INITIAL_DIR
1576        return String(INITIAL_DIR)
1577
1578
1579class InstallationDirectory(Predefined):
1580    """
1581    <dl>
1582      <dt>'$InstallationDirectory'
1583      <dd>returns the top-level directory in which \\Mathics was installed.
1584    </dl>
1585    >> $InstallationDirectory
1586     = ...
1587    """
1588
1589    attributes = ("Unprotected",)
1590    name = "$InstallationDirectory"
1591
1592    def evaluate(self, evaluation):
1593        global ROOT_DIR
1594        return String(ROOT_DIR)
1595
1596
1597class Needs(Builtin):
1598    """
1599    <dl>
1600    <dt>'Needs["context`"]'
1601        <dd>loads the specified context if not already in '$Packages'.
1602    </dl>
1603
1604    >> Needs["VectorAnalysis`"]
1605    #> Needs["VectorAnalysis`"]
1606
1607    #> Needs["SomeFakePackageOrTypo`"]
1608     : Cannot open SomeFakePackageOrTypo`.
1609     : Context SomeFakePackageOrTypo` was not created when Needs was evaluated.
1610     = $Failed
1611
1612    #> Needs["VectorAnalysis"]
1613     : Invalid context specified at position 1 in Needs[VectorAnalysis]. A context must consist of valid symbol names separated by and ending with `.
1614     = Needs[VectorAnalysis]
1615
1616    ## --- VectorAnalysis ---
1617
1618    #> Needs["VectorAnalysis`"]
1619
1620    #> DotProduct[{1,2,3}, {4,5,6}]
1621     = 32
1622    #> DotProduct[{-1.4, 0.6, 0.2}, {0.1, 0.6, 1.7}]
1623     = 0.56
1624
1625    #> CrossProduct[{1,2,3}, {4,5,6}]
1626     = {-3, 6, -3}
1627    #> CrossProduct[{-1.4, 0.6, 0.2}, {0.1, 0.6, 1.7}]
1628     = {0.9, 2.4, -0.9}
1629
1630    #> ScalarTripleProduct[{-2,3,1},{0,4,0},{-1,3,3}]
1631     = -20
1632    #> ScalarTripleProduct[{-1.4,0.6,0.2}, {0.1,0.6,1.7}, {0.7,-1.5,-0.2}]
1633     = -2.79
1634
1635    #> CoordinatesToCartesian[{2, Pi, 3}, Spherical]
1636     = {0, 0, -2}
1637    #> CoordinatesFromCartesian[%, Spherical]
1638     = {2, Pi, 0}
1639    #> CoordinatesToCartesian[{2, Pi, 3}, Cylindrical]
1640     = {-2, 0, 3}
1641    #> CoordinatesFromCartesian[%, Cylindrical]
1642     = {2, Pi, 3}
1643    ## Needs Sin/Cos exact value (PR #100) for these tests to pass
1644    ## #> CoordinatesToCartesian[{2, Pi / 4, Pi / 3}, Spherical]
1645    ##  = {Sqrt[2] / 2, Sqrt[6] / 2, Sqrt[2]}
1646    ## #> CoordinatesFromCartesian[%, Spherical]
1647    ##  = {2, Pi / 4, Pi / 3}
1648    ## #> CoordinatesToCartesian[{2, Pi / 4, -1}, Cylindrical]
1649    ##  = {Sqrt[2], Sqrt[2], -1}
1650    ## #> CoordinatesFromCartesian[%, Cylindrical]
1651    ##  = {2, Pi / 4, -1}
1652    #> CoordinatesToCartesian[{0.27, 0.51, 0.92}, Cylindrical]
1653     = {0.235641, 0.131808, 0.92}
1654    #> CoordinatesToCartesian[{0.27, 0.51, 0.92}, Spherical]
1655     = {0.0798519, 0.104867, 0.235641}
1656
1657    #> Coordinates[]
1658     = {Xx, Yy, Zz}
1659    #> Coordinates[Spherical]
1660     = {Rr, Ttheta, Pphi}
1661    #> SetCoordinates[Cylindrical]
1662     = Cylindrical[Rr, Ttheta, Zz]
1663    #> Coordinates[]
1664     = {Rr, Ttheta, Zz}
1665    #> CoordinateSystem
1666     = Cylindrical
1667    #> Parameters[]
1668     = {}
1669    #> CoordinateRanges[]
1670    ## = {0 <= Rr < Infinity, -Pi < Ttheta <= Pi, -Infinity < Zz < Infinity}
1671     = {0 <= Rr && Rr < Infinity, -Pi < Ttheta && Ttheta <= Pi, -Infinity < Zz < Infinity}
1672    #> CoordinateRanges[Cartesian]
1673     = {-Infinity < Xx < Infinity, -Infinity < Yy < Infinity, -Infinity < Zz < Infinity}
1674    #> ScaleFactors[Cartesian]
1675     = {1, 1, 1}
1676    #> ScaleFactors[Spherical]
1677     = {1, Rr, Rr Sin[Ttheta]}
1678    #> ScaleFactors[Cylindrical]
1679     = {1, Rr, 1}
1680    #> ScaleFactors[{2, 1, 3}, Cylindrical]
1681     = {1, 2, 1}
1682    #> JacobianDeterminant[Cartesian]
1683     = 1
1684    #> JacobianDeterminant[Spherical]
1685     = Rr ^ 2 Sin[Ttheta]
1686    #> JacobianDeterminant[Cylindrical]
1687     = Rr
1688    #> JacobianDeterminant[{2, 1, 3}, Cylindrical]
1689     = 2
1690    #> JacobianMatrix[Cartesian]
1691     = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}
1692    #> JacobianMatrix[Spherical]
1693     = {{Cos[Pphi] Sin[Ttheta], Rr Cos[Pphi] Cos[Ttheta], -Rr Sin[Pphi] Sin[Ttheta]}, {Sin[Pphi] Sin[Ttheta], Rr Cos[Ttheta] Sin[Pphi], Rr Cos[Pphi] Sin[Ttheta]}, {Cos[Ttheta], -Rr Sin[Ttheta], 0}}
1694    #> JacobianMatrix[Cylindrical]
1695     = {{Cos[Ttheta], -Rr Sin[Ttheta], 0}, {Sin[Ttheta], Rr Cos[Ttheta], 0}, {0, 0, 1}}
1696    """
1697
1698    messages = {
1699        "ctx": (
1700            "Invalid context specified at position `2` in `1`. "
1701            "A context must consist of valid symbol names separated by "
1702            "and ending with `3`."
1703        ),
1704        "nocont": "Context `1` was not created when Needs was evaluated.",
1705    }
1706
1707    def apply(self, context, evaluation):
1708        "Needs[context_String]"
1709        contextstr = context.get_string_value()
1710        if contextstr == "":
1711            return SymbolNull
1712        if contextstr[0] == "`":
1713            curr_ctxt = evaluation.definitions.get_current_context()
1714            contextstr = curr_ctxt + contextstr[1:]
1715            context = String(contextstr)
1716        if not valid_context_name(contextstr):
1717            evaluation.message("Needs", "ctx", Expression("Needs", context), 1, "`")
1718            return
1719        test_loaded = Expression("MemberQ", Symbol("$Packages"), context)
1720        test_loaded = test_loaded.evaluate(evaluation)
1721        if test_loaded.is_true():
1722            # Already loaded
1723            return SymbolNull
1724        result = Expression("Get", context).evaluate(evaluation)
1725
1726        if result == SymbolFailed:
1727            evaluation.message("Needs", "nocont", context)
1728            return SymbolFailed
1729
1730        return SymbolNull
1731
1732
1733class OperatingSystem(Predefined):
1734    """
1735    <dl>
1736    <dt>'$OperatingSystem'
1737      <dd>gives the type of operating system running Mathics.
1738    </dl>
1739
1740    >> $OperatingSystem
1741     = ...
1742    """
1743
1744    attributes = ("Locked", "Protected")
1745    name = "$OperatingSystem"
1746
1747    def evaluate(self, evaluation):
1748        if os.name == "posix":
1749            return String("Unix")
1750        elif os.name == "nt":
1751            return String("Windows")
1752        elif os.name == "os2":
1753            return String("MacOSX")
1754        else:
1755            return String("Unknown")
1756
1757
1758class ParentDirectory(Builtin):
1759    """
1760    <dl>
1761    <dt>'ParentDirectory[]'
1762      <dd>returns the parent of the current working directory.
1763    <dt>'ParentDirectory["$dir$"]'
1764      <dd>returns the parent $dir$.
1765    </dl>
1766
1767    >> ParentDirectory[]
1768     = ...
1769    """
1770
1771    rules = {
1772        "ParentDirectory[]": "ParentDirectory[Directory[]]",
1773    }
1774
1775    messages = {
1776        "fstr": (
1777            "File specification `1` is not a string of " "one or more characters."
1778        ),
1779    }
1780
1781    attributes = "Protected"
1782
1783    def apply(self, path, evaluation):
1784        "ParentDirectory[path_]"
1785
1786        if not isinstance(path, String):
1787            evaluation.message("ParentDirectory", "fstr", path)
1788            return
1789
1790        pypath = path.to_python()[1:-1]
1791
1792        result = osp.abspath(osp.join(pypath, osp.pardir))
1793        return String(result)
1794
1795
1796class Path(Predefined):
1797    """
1798    <dl>
1799    <dt>'$Path'
1800      <dd>returns the list of directories to search when looking for a file.
1801    </dl>
1802
1803    >> $Path
1804     = ...
1805    """
1806
1807    attributes = ("Unprotected",)
1808    name = "$Path"
1809
1810    def evaluate(self, evaluation):
1811        return Expression(SymbolList, *[String(p) for p in PATH_VAR])
1812
1813
1814class PathnameSeparator(Predefined):
1815    """
1816    <dl>
1817    <dt>'$PathnameSeparator'
1818      <dd>returns a string for the seperator in paths.
1819    </dl>
1820
1821    >> $PathnameSeparator
1822     = ...
1823    """
1824
1825    name = "$PathnameSeparator"
1826
1827    def evaluate(self, evaluation):
1828        return String(os.sep)
1829
1830
1831class RenameDirectory(Builtin):
1832    """
1833    <dl>
1834    <dt>'RenameDirectory["$dir1$", "$dir2$"]'
1835      <dd>renames directory $dir1$ to $dir2$.
1836    </dl>
1837    """
1838
1839    attributes = "Protected"
1840
1841    messages = {
1842        "argr": "called with `1` argument; 2 arguments are expected.",
1843        "fstr": (
1844            "File specification `1` is not a string of " "one or more characters."
1845        ),
1846        "filex": "Cannot overwrite existing file `1`.",
1847        "nodir": "Directory `1` not found.",
1848    }
1849
1850    def apply(self, dirs, evaluation):
1851        "RenameDirectory[dirs__]"
1852
1853        seq = dirs.get_sequence()
1854        if len(seq) != 2:
1855            evaluation.message("RenameDirectory", "argr", len(seq))
1856            return
1857        (dir1, dir2) = (s.to_python() for s in seq)
1858
1859        if not (isinstance(dir1, str) and dir1[0] == dir1[-1] == '"'):
1860            evaluation.message("RenameDirectory", "fstr", seq[0])
1861            return
1862        dir1 = dir1[1:-1]
1863
1864        if not (isinstance(dir2, str) and dir2[0] == dir2[-1] == '"'):
1865            evaluation.message("RenameDirectory", "fstr", seq[1])
1866            return
1867        dir2 = dir2[1:-1]
1868
1869        if not osp.isdir(dir1):
1870            evaluation.message("RenameDirectory", "nodir", seq[0])
1871            return SymbolFailed
1872        if osp.isdir(dir2):
1873            evaluation.message("RenameDirectory", "filex", seq[1])
1874            return SymbolFailed
1875
1876        shutil.move(dir1, dir2)
1877
1878        return String(osp.abspath(dir2))
1879
1880
1881class RenameFile(Builtin):
1882    """
1883    <dl>
1884    <dt>'RenameFile["$file1$", "$file2$"]'
1885      <dd>renames $file1$ to $file2$.
1886    </dl>
1887
1888    >> CopyFile["ExampleData/sunflowers.jpg", "MathicsSunflowers.jpg"]
1889     = MathicsSunflowers.jpg
1890    >> RenameFile["MathicsSunflowers.jpg", "MathicsSunnyFlowers.jpg"]
1891     = MathicsSunnyFlowers.jpg
1892    >> DeleteFile["MathicsSunnyFlowers.jpg"]
1893    """
1894
1895    messages = {
1896        "filex": "Cannot overwrite existing file `1`.",
1897        "fstr": (
1898            "File specification `1` is not a string of " "one or more characters."
1899        ),
1900        "nffil": "File not found during `1`.",
1901    }
1902
1903    attributes = "Protected"
1904
1905    def apply(self, source, dest, evaluation):
1906        "RenameFile[source_, dest_]"
1907
1908        py_source = source.to_python()
1909        py_dest = dest.to_python()
1910
1911        # Check filenames
1912        if not (isinstance(py_source, str) and py_source[0] == py_source[-1] == '"'):
1913            evaluation.message("RenameFile", "fstr", source)
1914            return
1915        if not (isinstance(py_dest, str) and py_dest[0] == py_dest[-1] == '"'):
1916            evaluation.message("RenameFile", "fstr", dest)
1917            return
1918
1919        py_source = py_source[1:-1]
1920        py_dest = py_dest[1:-1]
1921
1922        py_source = path_search(py_source)
1923
1924        if py_source is None:
1925            evaluation.message("RenameFile", "filex", source)
1926            return SymbolFailed
1927
1928        if osp.exists(py_dest):
1929            evaluation.message("RenameFile", "filex", dest)
1930            return SymbolFailed
1931
1932        try:
1933            shutil.move(py_source, py_dest)
1934        except IOError:
1935            evaluation.message("RenameFile", "nffil", dest)
1936            return SymbolFailed
1937
1938        return dest
1939
1940
1941class ResetDirectory(Builtin):
1942    """
1943    <dl>
1944    <dt>'ResetDirectory[]'
1945      <dd>pops a directory from the directory stack and returns it.
1946    </dl>
1947
1948    >> ResetDirectory[]
1949    = ...
1950    """
1951
1952    messages = {
1953        "dtop": "Directory stack is empty.",
1954    }
1955
1956    attributes = "Protected"
1957
1958    def apply(self, evaluation):
1959        "ResetDirectory[]"
1960        try:
1961            tmp = DIRECTORY_STACK.pop()
1962        except IndexError:
1963            tmp = os.getcwd()
1964            evaluation.message("ResetDirectory", "dtop")
1965        else:
1966            os.chdir(tmp)
1967        return String(tmp)
1968
1969
1970class RootDirectory(Predefined):
1971    """
1972    <dl>
1973    <dt>'$RootDirectory'
1974      <dd>returns the system root directory.
1975    </dl>
1976
1977    >> $RootDirectory
1978     = ...
1979    """
1980
1981    name = "$RootDirectory"
1982
1983    attributes = "Protected"
1984
1985    def evaluate(self, evaluation):
1986        global SYS_ROOT_DIR
1987        return String(SYS_ROOT_DIR)
1988
1989
1990class SetDirectory(Builtin):
1991    """
1992    <dl>
1993    <dt>'SetDirectory[$dir$]'
1994      <dd>sets the current working directory to $dir$.
1995    </dl>
1996
1997    S> SetDirectory[]
1998    = ...
1999
2000    #> SetDirectory["MathicsNonExample"]
2001     : Cannot set current directory to MathicsNonExample.
2002     = $Failed
2003    """
2004
2005    rules = {
2006        "SetDirectory[]": "SetDirectory[$HomeDirectory]",
2007    }
2008
2009    messages = {
2010        "fstr": (
2011            "File specification `1` is not a string of " "one or more characters."
2012        ),
2013        "cdir": "Cannot set current directory to `1`.",
2014    }
2015
2016    attributes = "Protected"
2017
2018    def apply(self, path, evaluation):
2019        "SetDirectory[path_]"
2020
2021        if not isinstance(path, String):
2022            evaluation.message("SetDirectory", "fstr", path)
2023            return
2024
2025        py_path = path.__str__()[1:-1]
2026
2027        if py_path is None or not osp.isdir(py_path):
2028            evaluation.message("SetDirectory", "cdir", path)
2029            return SymbolFailed
2030
2031        try:
2032            os.chdir(py_path)
2033        except:
2034            return SymbolFailed
2035
2036        DIRECTORY_STACK.append(os.getcwd())
2037        return String(os.getcwd())
2038
2039
2040class SetFileDate(Builtin):
2041    """
2042    <dl>
2043    <dt>'SetFileDate["$file$"]'
2044      <dd>set the file access and modification dates of $file$ to the current date.
2045    <dt>'SetFileDate["$file$", $date$]'
2046      <dd>set the file access and modification dates of $file$ to the specified date list.
2047    <dt>'SetFileDate["$file$", $date$, "$type$"]'
2048      <dd>set the file date of $file$ to the specified date list.
2049      The "$type$" can be one of "$Access$", "$Creation$", "$Modification$", or 'All'.
2050    </dl>
2051
2052    Create a temporary file (for example purposes)
2053    >> tmpfilename = $TemporaryDirectory <> "/tmp0";
2054    >> Close[OpenWrite[tmpfilename]];
2055
2056    >> SetFileDate[tmpfilename, {2002, 1, 1, 0, 0, 0.}, "Access"];
2057
2058    >> FileDate[tmpfilename, "Access"]
2059     = {2002, 1, 1, 0, 0, 0.}
2060
2061    #> SetFileDate[tmpfilename, {2002, 1, 1, 0, 0, 0.}];
2062    #> FileDate[tmpfilename, "Access"]
2063     = {2002, 1, 1, 0, 0, 0.}
2064
2065    #> SetFileDate[tmpfilename]
2066    #> FileDate[tmpfilename, "Access"]
2067     = {...}
2068
2069    #> DeleteFile[tmpfilename]
2070
2071    #> SetFileDate["MathicsNonExample"]
2072     : File not found during SetFileDate[MathicsNonExample].
2073     = $Failed
2074    """
2075
2076    messages = {
2077        "fstr": (
2078            "File specification `1` is not a string of one or " "more characters."
2079        ),
2080        "nffil": "File not found during `1`.",
2081        "fdate": (
2082            "Date specification should be either the number of seconds "
2083            "since January 1, 1900 or a {y, m, d, h, m, s} list."
2084        ),
2085        "datetype": (
2086            'Date type a should be "Access", "Modification", '
2087            '"Creation" (Windows only), or All.'
2088        ),
2089        "nocreationunix": (
2090            "The Creation date of a file cannot be set on " "Macintosh or Unix."
2091        ),
2092    }
2093
2094    attributes = "Protected"
2095
2096    def apply(self, filename, datelist, attribute, evaluation):
2097        "SetFileDate[filename_, datelist_, attribute_]"
2098
2099        py_filename = filename.to_python()
2100
2101        if datelist is None:
2102            py_datelist = Expression("DateList").evaluate(evaluation).to_python()
2103            expr = Expression("SetFileDate", filename)
2104        else:
2105            py_datelist = datelist.to_python()
2106
2107        if attribute is None:
2108            py_attr = "All"
2109            if datelist is not None:
2110                expr = Expression("SetFileDate", filename, datelist)
2111        else:
2112            py_attr = attribute.to_python()
2113            expr = Expression("SetFileDate", filename, datelist, attribute)
2114
2115        # Check filename
2116        if not (
2117            isinstance(py_filename, str) and py_filename[0] == py_filename[-1] == '"'
2118        ):
2119            evaluation.message("SetFileDate", "fstr", filename)
2120            return
2121        py_filename = path_search(py_filename[1:-1])
2122
2123        if py_filename is None:
2124            evaluation.message("SetFileDate", "nffil", expr)
2125            return SymbolFailed
2126
2127        # Check datelist
2128        if not (
2129            isinstance(py_datelist, list)
2130            and len(py_datelist) == 6
2131            and all(isinstance(d, int) for d in py_datelist[:-1])
2132            and isinstance(py_datelist[-1], float)
2133        ):
2134            evaluation.message("SetFileDate", "fdate", expr)
2135
2136        # Check attribute
2137        if py_attr not in ['"Access"', '"Creation"', '"Modification"', "All"]:
2138            evaluation.message("SetFileDate", "datetype")
2139            return
2140
2141        epochtime = (
2142            Expression("AbsoluteTime", time.strftime("%Y-%m-%d %H:%M", time.gmtime(0)))
2143            .evaluate(evaluation)
2144            .to_python()
2145        )
2146
2147        stattime = Expression("AbsoluteTime", from_python(py_datelist))
2148        stattime = stattime.to_python(n_evaluation=evaluation)
2149
2150        stattime -= epochtime
2151
2152        try:
2153            os.stat(py_filename)
2154            if py_attr == '"Access"':
2155                os.utime(py_filename, (stattime, osp.getatime(py_filename)))
2156            if py_attr == '"Creation"':
2157                if os.name == "posix":
2158                    evaluation.message("SetFileDate", "nocreationunix")
2159                    return SymbolFailed
2160                else:
2161                    # TODO: Note: This is windows only
2162                    return SymbolFailed
2163            if py_attr == '"Modification"':
2164                os.utime(py_filename, (osp.getatime(py_filename), stattime))
2165            if py_attr == "All":
2166                os.utime(py_filename, (stattime, stattime))
2167        except OSError as e:
2168            # evaluation.message(...)
2169            return SymbolFailed
2170
2171        return SymbolNull
2172
2173    def apply_1arg(self, filename, evaluation):
2174        "SetFileDate[filename_]"
2175        return self.apply(filename, None, None, evaluation)
2176
2177    def apply_2arg(self, filename, datelist, evaluation):
2178        "SetFileDate[filename_, datelist_]"
2179        return self.apply(filename, datelist, None, evaluation)
2180
2181
2182class TemporaryDirectory(Predefined):
2183    """
2184    <dl>
2185    <dt>'$TemporaryDirectory'
2186      <dd>returns the directory used for temporary files.
2187    </dl>
2188
2189    >> $TemporaryDirectory
2190     = ...
2191    """
2192
2193    name = "$TemporaryDirectory"
2194
2195    def evaluate(self, evaluation):
2196        return String(TMP_DIR)
2197
2198
2199class ToFileName(Builtin):
2200    """
2201    <dl>
2202    <dt>'ToFileName[{"$dir_1$", "$dir_2$", ...}]'
2203      <dd>joins the $dir_i$ together into one path.
2204    </dl>
2205
2206    'ToFileName' has been superseded by 'FileNameJoin'.
2207
2208    >> ToFileName[{"dir1", "dir2"}, "file"]
2209     = dir1...dir2...file
2210
2211    >> ToFileName["dir1", "file"]
2212     = dir1...file
2213
2214    >> ToFileName[{"dir1", "dir2", "dir3"}]
2215     = dir1...dir2...dir3
2216    """
2217
2218    rules = {
2219        "ToFileName[dir_String, name_String]": "FileNameJoin[{dir, name}]",
2220        "ToFileName[dirs_?ListQ, name_String]": "FileNameJoin[Append[dirs, name]]",
2221        "ToFileName[dirs_?ListQ]": "FileNameJoin[dirs]",
2222    }
2223
2224
2225class UserBaseDirectory(Predefined):
2226    """
2227    <dl>
2228    <dt>'$UserBaseDirectory'
2229      <dd>returns the folder where user configurations are stored.
2230    </dl>
2231
2232    >> $RootDirectory
2233     = ...
2234    """
2235
2236    name = "$UserBaseDirectory"
2237
2238    attributes = "Protected"
2239
2240    def evaluate(self, evaluation):
2241        global HOME_DIR
2242        return String(HOME_DIR + os.sep + ".mathics")
2243
2244
2245class URLSave(Builtin):
2246    """
2247    <dl>
2248    <dt>'URLSave["url"]'
2249        <dd>Save "url" in a temporary file.
2250    <dt>'URLSave["url", $filename$]'
2251        <dd>Save "url" in $filename$.
2252    </dl>
2253    """
2254
2255    messages = {
2256        "invfile": "`1` is not a valid Filename",
2257        "invhttp": "`1` is not a valid URL",
2258    }
2259
2260    def apply_1(self, url, evaluation, **options):
2261        "URLSave[url_String, OptionsPattern[URLSave]]"
2262        return self.apply_2(url, None, evaluation, **options)
2263
2264    def apply_2(self, url, filename, evaluation, **options):
2265        "URLSave[url_String, filename_, OptionsPattern[URLSave]]"
2266        url = url.value
2267        if filename is None:
2268            result = urlsave_tmp(url, None, **options)
2269        elif filename.get_head_name() == "String":
2270            filename = filename.value
2271            result = urlsave_tmp(url, filename, **options)
2272        else:
2273            evaluation.message("URLSave", "invfile", filename)
2274            return SymbolFailed
2275        if result is None:
2276            return SymbolFailed
2277        return String(result)
2278
2279
2280# To placate import
2281ROOT_DIR, HOME_DIR
2282