src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/mods/os/os.go (about)

     1  // Package os exposes functionality from Go's os package as an Elvish module.
     2  package os
     3  
     4  import (
     5  	"io/fs"
     6  	"os"
     7  	"path/filepath"
     8  	"strconv"
     9  
    10  	"src.elv.sh/pkg/eval"
    11  	"src.elv.sh/pkg/eval/errs"
    12  	"src.elv.sh/pkg/eval/vals"
    13  	"src.elv.sh/pkg/eval/vars"
    14  )
    15  
    16  // Ns is the Elvish namespace for this module.
    17  var Ns = eval.BuildNsNamed("os").
    18  	AddVars(map[string]vars.Var{
    19  		"dev-null": vars.NewReadOnly(os.DevNull),
    20  		"dev-tty":  vars.NewReadOnly(DevTTY),
    21  	}).
    22  	AddGoFns(map[string]any{
    23  		"-is-exist":     isExist,
    24  		"-is-not-exist": isNotExist,
    25  
    26  		// File CRUD.
    27  		"mkdir":      mkdir,
    28  		"mkdir-all":  mkdirAll,
    29  		"symlink":    os.Symlink,
    30  		"remove":     remove,
    31  		"remove-all": removeAll,
    32  		"rename":     os.Rename,
    33  		"chmod":      chmod,
    34  
    35  		// File query.
    36  		"stat":       stat,
    37  		"exists":     exists,
    38  		"is-dir":     IsDir,
    39  		"is-regular": IsRegular,
    40  
    41  		"eval-symlinks": filepath.EvalSymlinks,
    42  
    43  		// Temp file/dir.
    44  		"temp-dir":  TempDir,
    45  		"temp-file": TempFile,
    46  	}).Ns()
    47  
    48  // Wraps [os.IsNotExist] to operate on Exception values.
    49  func isExist(e eval.Exception) bool {
    50  	return os.IsExist(e.Reason())
    51  }
    52  
    53  // Wraps [os.IsNotExist] to operate on Exception values.
    54  func isNotExist(e eval.Exception) bool {
    55  	return os.IsNotExist(e.Reason())
    56  }
    57  
    58  type mkdirOpts struct{ Perm int }
    59  
    60  func (opts *mkdirOpts) SetDefaultOptions() { opts.Perm = 0755 }
    61  
    62  func mkdir(opts mkdirOpts, path string) error {
    63  	return os.Mkdir(path, os.FileMode(opts.Perm))
    64  }
    65  
    66  func mkdirAll(opts mkdirOpts, path string) error {
    67  	return os.MkdirAll(path, os.FileMode(opts.Perm))
    68  }
    69  
    70  // ErrEmptyPath is thrown by remove and remove-all when given an empty path.
    71  var ErrEmptyPath = errs.BadValue{
    72  	What: "path", Valid: "non-empty string", Actual: "empty string"}
    73  
    74  // Wraps [os.Remove] to reject empty paths.
    75  func remove(path string) error {
    76  	if path == "" {
    77  		return ErrEmptyPath
    78  	}
    79  	return os.Remove(path)
    80  }
    81  
    82  // Wraps [os.RemoveAll] to reject empty paths, and resolve relative paths to
    83  // absolute paths first. The latter is necessary since the working directory
    84  // could be changed while [os.RemoveAll] is running.
    85  func removeAll(path string) error {
    86  	if path == "" {
    87  		return ErrEmptyPath
    88  	}
    89  	if !filepath.IsAbs(path) {
    90  		absPath, err := filepath.Abs(path)
    91  		if err != nil {
    92  			return err
    93  		}
    94  		path = absPath
    95  	}
    96  	return os.RemoveAll(path)
    97  }
    98  
    99  type chmodOpts struct {
   100  	SpecialModes any
   101  }
   102  
   103  func (*chmodOpts) SetDefaultOptions() {}
   104  
   105  func chmod(opts chmodOpts, perm int, path string) error {
   106  	if perm < 0 || perm > 0x777 {
   107  		return errs.OutOfRange{What: "permission bits",
   108  			ValidLow: "0", ValidHigh: "0o777", Actual: strconv.Itoa(perm)}
   109  	}
   110  	mode := fs.FileMode(perm)
   111  	if opts.SpecialModes != nil {
   112  		special, err := specialModesFromIterable(opts.SpecialModes)
   113  		if err != nil {
   114  			return err
   115  		}
   116  		mode |= special
   117  	}
   118  	return os.Chmod(path, mode)
   119  }
   120  
   121  type statOpts struct{ FollowSymlink bool }
   122  
   123  func (opts *statOpts) SetDefaultOptions() {}
   124  
   125  func stat(opts statOpts, path string) (vals.Map, error) {
   126  	fi, err := statOrLstat(path, opts.FollowSymlink)
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  	return statMap(fi), nil
   131  }
   132  
   133  func exists(opts statOpts, path string) bool {
   134  	_, err := statOrLstat(path, opts.FollowSymlink)
   135  	return err == nil
   136  }
   137  
   138  // IsDir is exported so that the implementation may be shared by the path:
   139  // module.
   140  func IsDir(opts statOpts, path string) bool {
   141  	fi, err := statOrLstat(path, opts.FollowSymlink)
   142  	return err == nil && fi.Mode().IsDir()
   143  }
   144  
   145  // IsRegular is exported so that the implementation may be shared by the path:
   146  // module.
   147  func IsRegular(opts statOpts, path string) bool {
   148  	fi, err := statOrLstat(path, opts.FollowSymlink)
   149  	return err == nil && fi.Mode().IsRegular()
   150  }
   151  
   152  func statOrLstat(path string, followSymlink bool) (os.FileInfo, error) {
   153  	if followSymlink {
   154  		return os.Stat(path)
   155  	} else {
   156  		return os.Lstat(path)
   157  	}
   158  }
   159  
   160  type mktempOpt struct{ Dir string }
   161  
   162  func (o *mktempOpt) SetDefaultOptions() {}
   163  
   164  // TempDir is exported so that the implementation may be shared by the path:
   165  // module.
   166  func TempDir(opts mktempOpt, args ...string) (string, error) {
   167  	pattern, err := optionalTempPattern(args)
   168  	if err != nil {
   169  		return "", err
   170  	}
   171  	return os.MkdirTemp(opts.Dir, pattern)
   172  }
   173  
   174  // TempFile is exported so that the implementation may be shared by the path:
   175  // module.
   176  func TempFile(opts mktempOpt, args ...string) (*os.File, error) {
   177  	pattern, err := optionalTempPattern(args)
   178  	if err != nil {
   179  		return nil, err
   180  	}
   181  	return os.CreateTemp(opts.Dir, pattern)
   182  }
   183  
   184  func optionalTempPattern(args []string) (string, error) {
   185  	switch len(args) {
   186  	case 0:
   187  		return "elvish-*", nil
   188  	case 1:
   189  		return args[0], nil
   190  	default:
   191  		return "", errs.ArityMismatch{What: "arguments",
   192  			ValidLow: 0, ValidHigh: 1, Actual: len(args)}
   193  	}
   194  }