Add equivalent of mktemp as builtin commands.

This implements `path:temp-dir` and `path:temp-file`.

Resolves #1255
This commit is contained in:
Kurtis Rader 2021-03-08 20:31:23 -08:00 committed by Qi Xiao
parent 210da2abea
commit bc37099c92
7 changed files with 206 additions and 2 deletions

View File

@ -49,6 +49,10 @@ New features in the standard library:
- A new `file:` module contains utilities for manipulating files.
- Commands for creating, what are usually meant to be temporary, unique
directories, path:temp-dir, and regular files, path:temp-file
([#1255](https://b.elv.sh/1255)).
New features in the interactive editor:
- The editor now supports setting global bindings via `$edit:global-binding`.

View File

@ -43,6 +43,10 @@ type Result struct {
Exception error
}
// MatchingRegexp is used in a `Puts()` call when the value being matched should be a string
// matching a regexp rather than a literal string.
type MatchingRegexp struct{ Pattern string }
// The following functions and methods are used to build Test structs. They are
// supposed to read like English, so a test that "put x" should put "x" reads:
//
@ -231,12 +235,19 @@ func matchOut(want, got []interface{}) bool {
default:
return false
}
default:
switch w := want[i].(type) {
case MatchingRegexp:
if !vals.MatchesRegexp(got[i], w.Pattern) {
return false
}
default:
if !vals.Equal(got[i], want[i]) {
return false
}
}
}
}
return true
}

View File

@ -2,10 +2,12 @@
package path
import (
"io/ioutil"
"os"
"path/filepath"
"src.elv.sh/pkg/eval"
"src.elv.sh/pkg/eval/errs"
)
// Ns is the namespace for the re: module.
@ -21,6 +23,8 @@ var fns = map[string]interface{}{
"is-abs": filepath.IsAbs,
"is-dir": isDir,
"is-regular": isRegular,
"temp-dir": tempDir,
"temp-file": tempFile,
}
//elvdoc:fn abs
@ -176,3 +180,99 @@ func isRegular(path string) bool {
fi, err := os.Lstat(path)
return err == nil && fi.Mode().IsRegular()
}
//elvdoc:fn temp-dir
//
// ```elvish
// temp-dir &dir=$dir $pattern?
// ```
//
// Create a unique directory and output its name. The &dir option determines where the directory
// will be created, and its default value is appropriate for your system. The `$pattern` value is
// optional. If omitted it defaults to `elvish-*`. The last star in the pattern is replaced by a
// random string. It is your responsibility to remove the (presumably) temporary directory.
//
// ```elvish-transcript
// ~> path:temp-dir
// ▶ /tmp/elvish-RANDOMSTR
// ~> path:temp-dir x-
// ▶ /tmp/x-RANDOMSTR
// ~> path:temp-dir 'x-*.y'
// ▶ /tmp/x-RANDOMSTR.y
// ~> path:temp-dir &dir=.
// ▶ elvish-RANDOMSTR
// ~> path:temp-dir &dir=/some/dir
// ▶ /some/dir/elvish-RANDOMSTR
// ```
type mktempOpt struct{ Dir string }
func (o *mktempOpt) SetDefaultOptions() {}
func tempDir(opts mktempOpt, args ...string) (string, error) {
var pattern string
switch len(args) {
case 0:
pattern = "elvish-*"
case 1:
pattern = args[0]
default:
return "", errs.ArityMismatch{
What: "arguments here",
ValidLow: 0, ValidHigh: 1, Actual: len(args)}
}
return ioutil.TempDir(opts.Dir, pattern)
}
//elvdoc:fn temp-file
//
// ```elvish
// temp-file [&dir=$dir] [$pattern]
// ```
//
// Create a unique file and output a [file](language.html#file) object opened for reading and
// writing. The &dir option determines where the directory will be created, and its default value is
// appropriate for your system. The `$pattern` value is optional. If omitted it defaults to
// `elvish-*`. The last star in the pattern is replaced by a random string. It is your
// responsibility to remove the (presumably) temporary file.
//
// You can use [`fclose`](builtin.html#fclose) to close the file. You can use `$f[name]` to extract
// the name of the file so it can be used as an argument for another command; e.g., `rm`.
//
// ```elvish-transcript
// ~> f = path:temp-file
// ~> put $f[name]
// ▶ /tmp/elvish-RANDOMSTR
// ~> echo hello > $f
// ~> cat $f[name]
// hello
// ~> f = path:temp-file x-
// ~> put $f[name]
// ▶ /tmp/x-RANDOMSTR
// ~> f = path:temp-file 'x-*.y'
// ~> put $f[name]
// ▶ /tmp/x-RANDOMSTR.y
// ~> f = path:temp-file &dir=.
// ~> put $f[name]
// ▶ elvish-RANDOMSTR
// ~> f = path:temp-file &dir=/some/dir
// ~> put $f[name]
// ▶ /some/dir/elvish-RANDOMSTR
// ```
func tempFile(opts mktempOpt, args ...string) (*os.File, error) {
var pattern string
switch len(args) {
case 0:
pattern = "elvish-*"
case 1:
pattern = args[0]
default:
return nil, errs.ArityMismatch{
What: "arguments here",
ValidLow: 0, ValidHigh: 1, Actual: len(args)}
}
return ioutil.TempFile(opts.Dir, pattern)
}

View File

@ -2,9 +2,11 @@ package path
import (
"path/filepath"
"regexp"
"testing"
"src.elv.sh/pkg/eval"
"src.elv.sh/pkg/eval/errs"
. "src.elv.sh/pkg/eval/evaltest"
"src.elv.sh/pkg/testutil"
)
@ -31,6 +33,10 @@ func TestPath(t *testing.T) {
panic("unable to convert a/b/c.png to an absolute path")
}
// This is needed for path tests that use a regexp for validating a path since Windows uses a
// backslash as the path separator and a backslash is special in a regexp.
sep := regexp.QuoteMeta(string(filepath.Separator))
setup := func(ev *eval.Evaler) {
ev.AddGlobal(eval.NsBuilder{}.AddNs("path", Ns).Ns())
}
@ -64,5 +70,32 @@ func TestPath(t *testing.T) {
That(`path:is-regular d1/f`).Puts(false),
That(`path:is-regular d1/d2/f`).Puts(true),
That(`path:is-regular s1/f`).Puts(true),
// Verify the commands for creating temporary filesystem objects work correctly.
That(`x = (path:temp-dir)`, `rmdir $x`, `put $x`).Puts(
MatchingRegexp{Pattern: `^.*` + sep + `elvish-.*$`}),
That(`x = (path:temp-dir 'x-*.y')`, `rmdir $x`, `put $x`).Puts(
MatchingRegexp{Pattern: `^.*` + sep + `x-.*\.y$`}),
That(`x = (path:temp-dir &dir=. 'x-*.y')`, `rmdir $x`, `put $x`).Puts(
MatchingRegexp{Pattern: `^x-.*\.y$`}),
That(`x = (path:temp-dir &dir=.)`, `rmdir $x`, `put $x`).Puts(
MatchingRegexp{Pattern: `^elvish-.*$`}),
That(`path:temp-dir a b`).Throws(
errs.ArityMismatch{What: "arguments here", ValidLow: 0, ValidHigh: 1, Actual: 2},
"path:temp-dir a b"),
That(`f = (path:temp-file)`, `fclose $f`, `put $f[fd]`, `rm $f[name]`).
Puts(-1),
That(`f = (path:temp-file)`, `put $f[name]`, `fclose $f`, `rm $f[name]`).
Puts(MatchingRegexp{Pattern: `^.*` + sep + `elvish-.*$`}),
That(`f = (path:temp-file 'x-*.y')`, `put $f[name]`, `fclose $f`, `rm $f[name]`).
Puts(MatchingRegexp{Pattern: `^.*` + sep + `x-.*\.y$`}),
That(`f = (path:temp-file &dir=. 'x-*.y')`, `put $f[name]`, `fclose $f`, `rm $f[name]`).
Puts(MatchingRegexp{Pattern: `^x-.*\.y$`}),
That(`f = (path:temp-file &dir=.)`, `put $f[name]`, `fclose $f`, `rm $f[name]`).
Puts(MatchingRegexp{Pattern: `^elvish-.*$`}),
That(`path:temp-file a b`).Throws(
errs.ArityMismatch{What: "arguments here", ValidLow: 0, ValidHigh: 1, Actual: 2},
"path:temp-file a b"),
)
}

View File

@ -2,6 +2,7 @@ package vals
import (
"errors"
"os"
"reflect"
)
@ -34,6 +35,26 @@ func (err noSuchKeyError) Error() string {
return "no such key: " + Repr(err.key, NoPretty)
}
// TODO: Replace this with a a generalized introspection mechanism based on PseudoStructMap for
// *os.File objects so that commands like `keys` also work on those objects.
var errInvalidOsFileIndex = errors.New("invalid index for a File object")
func indexOsFile(f *os.File, k interface{}) (interface{}, error) {
switch k := k.(type) {
case string:
switch {
case k == "fd":
return int(f.Fd()), nil
case k == "name":
return f.Name(), nil
default:
return nil, errInvalidOsFileIndex
}
default:
return nil, errInvalidOsFileIndex
}
}
// Index indexes a value with the given key. It is implemented for the builtin
// type string, the List type, StructMap types, and types satisfying the
// ErrIndexer or Indexer interface (the Map type satisfies Indexer). For other
@ -42,6 +63,8 @@ func Index(a, k interface{}) (interface{}, error) {
switch a := a.(type) {
case string:
return indexString(a, k)
case *os.File:
return indexOsFile(a, k)
case ErrIndexer:
return a.Index(k)
case Indexer:

24
pkg/eval/vals/match.go Normal file
View File

@ -0,0 +1,24 @@
package vals
import (
"regexp"
)
// MatchesRegexp returns whether The first value matches the second value interpreted as a regexp.
// Both bytes must be a string.
func MatchesRegexp(x, y interface{}) bool {
val, ok := x.(string)
if !ok {
return false
}
pat, ok := y.(string)
if !ok {
return false
}
matched, err := regexp.MatchString(pat, val)
if err != nil {
return false
}
return matched
}

View File

@ -476,6 +476,15 @@ Examples:
▶ [&cmd-name=false &exit-status=1 &pid=953421 &type=external-cmd/exited]
```
## File
There is no literal syntax for the file type. This type is returned by commands
such as [file:open](file.html#open) and [path:temp-file](path.html#temp-file).
It can be used as the target of a redirection rather than a filename.
A file object is a [pseudo-map](#pseudo-map) with fields `fd` (an int) and
`name` (a string). If the file is closed the fd will be -1.
## Function
A function encapsulates a piece of code that can be executed in an