mirror of
https://github.com/go-sylixos/elvish.git
synced 2024-12-05 03:17:50 +08:00
Add equivalent of mktemp
as builtin commands.
This implements `path:temp-dir` and `path:temp-file`. Resolves #1255
This commit is contained in:
parent
210da2abea
commit
bc37099c92
|
@ -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`.
|
||||
|
|
|
@ -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:
|
||||
//
|
||||
|
@ -232,8 +236,15 @@ func matchOut(want, got []interface{}) bool {
|
|||
return false
|
||||
}
|
||||
default:
|
||||
if !vals.Equal(got[i], want[i]) {
|
||||
return false
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
24
pkg/eval/vals/match.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user