pax_global_header00006660000000000000000000000064141547110400014507gustar00rootroot0000000000000052 comment=b8132a26e62f8eca12945e20f88ed58c7c8d5334 elvish-0.17.0/000077500000000000000000000000001415471104000130665ustar00rootroot00000000000000elvish-0.17.0/.cirrus.yml000066400000000000000000000013021415471104000151720ustar00rootroot00000000000000test_task: env: ELVISH_TEST_TIME_SCALE: 20 go_modules_cache: fingerprint_script: cat go.sum folder: $GOPATH/pkg/mod matrix: - name: Test on gccgo container: image: debian:unstable-slim setup_script: - apt-get -y update - apt-get -y install ca-certificates gccgo-11 - ln -sf /usr/bin/go-11 /usr/local/bin/go env: # gccgo doesn't support race test TEST_FLAG: "" - name: Test on FreeBSD freebsd_instance: image_family: freebsd-12-1 setup_script: pkg install -y go env: GOPATH: $HOME/go TEST_FLAG: -race go_version_script: go version test_script: go test $TEST_FLAG ./... elvish-0.17.0/.codecov.yml000066400000000000000000000010321415471104000153050ustar00rootroot00000000000000coverage: status: project: default: threshold: 0.1% patch: off comment: false ignore: # Exclude test helpers from coverage calculation. # # The following patterns are also consumed by a hacky sed script in tools/prune-cover.sh. - "pkg/cli/clitest" - "pkg/eval/evaltest" - "pkg/eval/vals/testutils.go" - "pkg/prog/progtest" - "pkg/store/storetest" - "pkg/testutil/must.go" # Exclude the copied rpc package. - "pkg/rpc" # The web UI is not being worked on now, also exclude it. - "pkg/web" elvish-0.17.0/.codespellrc000066400000000000000000000001251415471104000153640ustar00rootroot00000000000000[codespell] ignore-words-list = upto,nd,ba,doas,fo,struc,shouldbe,iterm,lates,testof elvish-0.17.0/.dockerignore000066400000000000000000000000041415471104000155340ustar00rootroot00000000000000/_* elvish-0.17.0/.gitattributes000066400000000000000000000000261415471104000157570ustar00rootroot00000000000000*.go filter=goimports elvish-0.17.0/.github/000077500000000000000000000000001415471104000144265ustar00rootroot00000000000000elvish-0.17.0/.github/workflows/000077500000000000000000000000001415471104000164635ustar00rootroot00000000000000elvish-0.17.0/.github/workflows/check_cirrus.yml000066400000000000000000000017641415471104000216620ustar00rootroot00000000000000name: Check Cirrus CI on: check_suite: type: ['completed'] jobs: notify-failure: name: Notify failure if: github.event.check_suite.app.name == 'Cirrus CI' && github.event.check_suite.conclusion == 'failure' runs-on: ubuntu-latest steps: - uses: octokit/request-action@v2.x id: get_failed_check_run with: route: GET /repos/${{ github.repository }}/check-suites/${{ github.event.check_suite.id }}/check-runs?status=completed mediaType: '{"previews": ["antiope"]}' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | echo "Cirrus CI ${{ github.event.check_suite.conclusion }} on ${{ github.event.check_suite.head_branch }} branch!" echo "SHA ${{ github.event.check_suite.head_sha }}" echo $MESSAGE echo "##[error]See $CHECK_RUN_URL for details" false env: CHECK_RUN_URL: ${{ fromJson(steps.get_failed_check_run.outputs.data).check_runs[0].html_url }} elvish-0.17.0/.github/workflows/check_website.yml000066400000000000000000000013371415471104000220110ustar00rootroot00000000000000name: Check website on: push: branches: - master jobs: check_website: name: Check freshness runs-on: ubuntu-latest strategy: matrix: host: [cdg, hkg] steps: - name: Checkout code uses: actions/checkout@v2 - name: Compare timestamp run: | ts=$(git show -s --format=%ct HEAD) for i in `seq 30`; do website_ts=$(curl https://${{ matrix.host }}.elv.sh/commit-ts.txt) if test "$website_ts" -ge "$ts"; then echo "website ($website_ts) >= current ($ts)" exit 0 else echo "website ($website_ts) < current ($ts)" fi sleep 60 done echo "Timeout" exit 1 elvish-0.17.0/.github/workflows/ci.yml000066400000000000000000000135401415471104000176040ustar00rootroot00000000000000name: CI on: push: pull_request: defaults: run: # PowerShell's behavior for -flag=value is undesirable, so run all commands with bash. shell: bash jobs: test: name: Run tests strategy: matrix: os: [ubuntu, macos, windows] go-version: [1.17.x] include: # Test old supported Go version - os: ubuntu go-version: 1.16.x env: ELVISH_TEST_TIME_SCALE: 20 runs-on: ${{ matrix.os }}-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Set up cache uses: actions/cache@v2 with: path: | ~/go/pkg/mod ~/.cache/go-build ~/Library/Caches/go-build ~/AppData/Local/go-build key: test/${{ matrix.os }}/${{ matrix.go-version }}/${{ hashFiles('go.sum') }}/${{ github.sha }} restore-keys: test/${{ matrix.os }}/${{ matrix.go-version }}/${{ hashFiles('go.sum') }}/ - name: Set up Go uses: actions/setup-go@v2 with: go-version: ${{ matrix.go-version }} - name: Test with race detection run: | go test -race ./... cd website; go test -race ./... - name: Set ostype to ${{ matrix.os }} run: echo ostype=${{ matrix.os }} >> $GITHUB_ENV - name: Set ostype to linux if: matrix.os == 'ubuntu' run: echo ostype=linux >> $GITHUB_ENV - name: Generate test coverage if: matrix.go-version == '1.17.x' run: go test -coverprofile=cover -coverpkg=./pkg/... ./pkg/... - name: Save test coverage if: matrix.go-version == '1.17.x' uses: actions/upload-artifact@v2 with: name: cover-${{ env.ostype }} path: cover # The purpose of running benchmarks in GitHub Actions is primarily to ensure # that the benchmark code runs and doesn't crash. GitHub Action runners don't # have a stable enough environment to produce reliable benchmark numbers. benchmark: name: Run benchmarks runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Set up Go uses: actions/setup-go@v2 with: go-version: 1.17.x - name: Run benchmarks run: go test -bench=. -run='^$' ./... upload-coverage: name: Upload test coverage strategy: matrix: ostype: [linux, macos, windows] needs: test runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Download test coverage uses: actions/download-artifact@v2 with: name: cover-${{ matrix.ostype }} - name: Upload coverage to codecov uses: codecov/codecov-action@v1 with: files: ./cover flags: ${{ matrix.ostype }} buildall: name: Build binaries runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Set up cache uses: actions/cache@v2 with: path: | ~/go/pkg/mod ~/.cache/go-build key: buildall/${{ matrix.os }}/1.17.x/${{ hashFiles('go.sum') }}/${{ github.sha }} restore-keys: buildall/${{ matrix.os }}/1.17.x/${{ hashFiles('go.sum') }} - name: Set up Go uses: actions/setup-go@v2 with: go-version: 1.17.x - name: Build binaries # TODO: Use PR number for suffix when running for PR run: ELVISH_REPRODUCIBLE=dev ./tools/buildall.sh . bin HEAD - name: Upload binaries uses: actions/upload-artifact@v2 with: name: bin path: bin/**/* checkstyle-go: name: Check style of **.go runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Set up Go uses: actions/setup-go@v2 with: go-version: 1.17.x - name: Set up goimports run: go install golang.org/x/tools/cmd/goimports@latest - name: Check style run: ./tools/checkstyle-go.sh checkstyle-md: name: Check style of **.md runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Set up environment run: | echo "NPM_PREFIX=$HOME/npm" >> $GITHUB_ENV echo "PATH=$HOME/npm/bin:$PATH" >> $GITHUB_ENV - name: Set up Node uses: actions/setup-node@v2 - name: Set up Node prefix run: npm config set prefix $NPM_PREFIX - name: Set up prettier run: npm install --global prettier@2.3.1 - name: Check style run: ./tools/checkstyle-md.sh codespell: name: Check spelling runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 - name: Install codespell run: pip install codespell==2.1.0 - name: Run codespell run: codespell check-rellinks: name: Check relative links runs-on: ubuntu-latest container: image: theelves/up options: --user 0 defaults: run: shell: sh env: CGO_ENABLED: 0 steps: - name: Checkout code uses: actions/checkout@v2 - name: Check relative links run: make -C website check-rellinks lint: name: Run linters runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Set up Go uses: actions/setup-go@v2 with: go-version: 1.17.x - name: Set up staticcheck run: go install honnef.co/go/tools/cmd/staticcheck@2021.1 - name: Run linters run: ./tools/lint.sh lsif: name: Upload SourceGraph LSIF if: github.repository == 'elves/elvish' && github.event_name == 'push' runs-on: ubuntu-latest container: sourcegraph/lsif-go:latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Generate LSIF data run: lsif-go - name: Upload LSIF data run: src lsif upload -github-token=${{ secrets.GITHUB_TOKEN }} -ignore-upload-failure elvish-0.17.0/.gitignore000066400000000000000000000004601415471104000150560ustar00rootroot00000000000000# Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe /.vscode/ # Project specific cover /_bin/ /elvish elvish-0.17.0/.gitlab-ci.yml000066400000000000000000000004301415471104000155170ustar00rootroot00000000000000image: golang:alpine variables: REPO_NAME: gitlab.com/elves/elvish before_script: - go version test: script: - apk add gcc musl-dev - go test -race ./... build: script: - apk add zip - ./tools/buildall.sh . bin HEAD artifacts: paths: - bin elvish-0.17.0/0.17.0-release-notes.md000066400000000000000000000060201415471104000166750ustar00rootroot00000000000000This is the draft release notes for 0.17.0, scheduled to be released on 2022-01-01. # Breaking changes - Attempting to assign to a read-only variable (e.g. `set nil = x`) is now a compile-time error rather than an exception. # Deprecated features Deprecated features will be removed in 0.18.0. The following deprecated features trigger a warning whenever the code is parsed or compiled, even if it is not executed: - The `dir-history` command is deprecated. Use `store:dirs` instead. - The legacy assignment form is deprecated. Depending on whether the left-hand variable already exists or not, use `var` or `set` instead. Use the [upgrader](https://go.elv.sh/u0.17) to migrate scripts. - The lambda syntax that declares arguments and options within `[]` before `{` has been deprecated. The new syntax now declares arguments and options within a pair of `|`, after `{`. Use the [upgrader](https://go.elv.sh/u0.17) to migrate scripts. See ([#664](https://b.elv.sh/664)). - Use of the special namespace `local:` is deprecated. - If you are using `local:` to reference variables (e.g. `echo $local:x`), `local:` is never necessary in the first place since Elvish always resolves local variables first, so just remove it. - If you are using `local:` when assigning variables (e.g. `local:x = foo`), `local:` makes sure that a new variable is created; use the `var` special command instead. - Use of the special namespace `up:` is deprecated. - If you are using `up:` to access a non-shadowed variable in an outer scope, `up:` is not necessary; just remove it. - If you are using `up:` to access a shadowed variable in an outer scope, rename the variables to have different names. - Use of a leading empty namespace in a variable name (e.g. `$:x`) is deprecated. Since `$:x` is always equivalent to `$x` anyway, just remove the `:` prefix. # Notable new features New features in the language: - A new special command `pragma`. The only supported pragma now is `unknown command`; using `pragma unknown command = disallow` turns off the default behavior of treating unknown commands as external commands. - A new special command `coalesce`. New features in the interactive editor: - Editor modes now form a stack, instead of being mutually exclusive. For example, it is now possible to start a minibuf mode within a completion mode, and vice versa. New features in the standard library: - The `store:` module now exposes all functionalities of Elvish's persistent store. - New `compare` command to compare numbers, strings, and lists ([#1347](https://b.elv.sh/1347)), in a consistent way as the `order` builtin. Performance improvements: - The overhead of executing pipelines consisting of a single form (i.e. a simple command with no pipes) has been reduced. A code chunk containing just `nop` command now executes 4 times as fast as before. Thanks to kolbycrouch for suggesting this optimization! elvish-0.17.0/CONTRIBUTING.md000066400000000000000000000137171415471104000153300ustar00rootroot00000000000000# Contributor's Manual ## Human communication The project lead is @xiaq, who is reachable in the user group most of the time. If you intend to make user-visible changes to Elvish's behavior, it is good idea to talk to him first; this will make it easier to review your changes. On the other hand, if you find it easier to express your thoughts directly in code, it is also completely fine to directly send a pull request, as long as you don't mind the risk of the PR being rejected due to lack of prior discussion. ## Testing changes Write comprehensive unit tests for your code, and make sure that existing tests are passing. Tests are run on CI automatically for PRs; you can also run `make test` in the repo root yourself. Respect established patterns of how unit tests are written. Some packages unfortunately have competing patterns, which usually reflects a still-evolving idea of how to best test the code. Worse, parts of the codebase are poorly tested, or even untestable. In either case, discuss with the project lead on the best way forward. ### ELVISH_TEST_TIME_SCALE Some unit tests depend on time thresholds. The default values of these time thresholds are suitable for a reasonably powerful laptop, but on resource-constraint environments (virtual machines, embedded systems) they might not be enough. Set the `ELVISH_TEST_TIME_SCALE` environment variable to a number greater than 1 to scale up the time thresholds used in tests. The CI environments use `ELVISH_TEST_TIME_SCALE = 10`. ## Documenting changes Always document user-visible changes. ### Release notes Add a brief list item to the release note of the next release, in the appropriate section. You can find the document at the root of the repo (called `$version-release-notes.md`). ### Reference docs Reference docs are interspersed in Go sources as comments blocks whose first line starts with `//elvdoc` (and are hence called _elvdocs_). They can use [Github Flavored Markdown](https://github.github.com/gfm/). Elvdocs for functions look like the following: ````go //elvdoc:fn name-of-fn // // ```elvish // name-of-fn $arg &opt=default // ``` // // Does something. // // Example: // // ```elvish-transcript // ~> name-of-fn something // ▶ some-value-output // ``` func nameOfFn() { ... } ```` Generally, elvdocs for functions have the following structure: - A line starting with `//elvdoc:fn`, followed by the name of the function. Note that there should be no space after `//`, unlike all the other lines. - An `elvish` code block describing the signature of the function, following the convention [here](website/ref/builtin.md#usage-notation). - Description of the function, which can be one or more paragraphs. The first sentence of the description should start with a verb in 3rd person singular (i.e. ending with a "s"), as if there is an implicit subject "this function". - One or more `elvish-transcript` code blocks showing example usages, which are transcripts of actual REPL input and output. Transcripts must use the default prompt `~>` and default value output indicator `▶`. You can use `elvish -norc` if you have customized either in your `rc.elv`. Place the comment block before the implementation of the function. If the function has no implementation (e.g. it is a simple wrapper of a function from the Go standard library), place it before the top-level declaration of the namespace. Similarly, reference docs for variables start with `//elvdoc:var`: ```go //elvdoc:var name-of-var // // Something. ``` Variables do not have signatures, and are described using a noun phrase. Examples are not always needed; if they are, they can be given in the same format as examples for functions. ### Comment for unexported Go types and functions In the doc comment for exported types and functions, it's customary to use the symbol itself as the first word of the comment. For unexported types and functions, this becomes a bit awkward as their names don't start with a capital letter, so don't repeat the symbol. Examples: ```go // Foo does foo. func Foo() { } // Does foo. func foo() { } ``` ## Generating code Elvish uses generated code in a few places. As is the usual case with Go projects, they are committed into the repo, and if you change the input of a generated file you should re-generate it. Use the standard command, `go generate ./...` to regenerate all files. Some of the generation rules depend on the `stringer` tool. Install with `go install golang.org/x/tools/cmd/stringer@latest`. ## Code hygiene Some basic aspects of code hygiene are checked in the CI. ### Formatting Install [goimports](https://pkg.go.dev/golang.org/x/tools/cmd/goimports) to format Go files, and [prettier](https://prettier.io/) to format Markdown files. ```sh go install golang.org/x/tools/cmd/goimports@latest npm install --global prettier@2.3.1 ``` Once you have installed the tools, use `make style` to format Go and Markdown files. If you prefer, you can also configure your editor to run these commands automatically when saving Go or Markdown sources. Use `make checkstyle` to check if all Go and Markdown files are properly formatted. ### Linting Install [staticcheck](https://staticcheck.io): ```sh go install honnef.co/go/tools/cmd/staticcheck@2021.1 ``` The other linter Elvish uses is the standard `go vet` command. Elvish doesn't use golint since it is [deprecated and frozen](https://github.com/golang/go/issues/38968). Use `make lint` to run `staticcheck` and `go vet`. ### Spell checking Install [codespell](https://github.com/codespell-project/codespell) to check spelling: ```sh pip install --user codespell==2.1.0 ``` Use `make codespell` to run it. ### Running all checks Use this command to run all checks: ```sh make test checkstyle lint codespell ``` You can put this in `.git/hooks/pre-push` to ensure that your published commits pass all the checks. ## Licensing By contributing, you agree to license your code under the same license as existing source code of elvish. See the LICENSE file. elvish-0.17.0/Dockerfile000066400000000000000000000005551415471104000150650ustar00rootroot00000000000000FROM golang:1.16-alpine as builder RUN apk update && \ apk add --virtual build-deps make git # Build Elvish COPY . /go/src/src.elv.sh RUN make -C /go/src/src.elv.sh get FROM alpine:3.13 COPY --from=builder /go/bin/elvish /bin/elvish RUN adduser -D elf RUN apk update && apk add tmux mandoc man-pages vim curl git USER elf WORKDIR /home/elf CMD ["/bin/elvish"] elvish-0.17.0/LICENSE000066400000000000000000000024241415471104000140750ustar00rootroot00000000000000Copyright (c) elvish developers and contributors, All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. elvish-0.17.0/Makefile000066400000000000000000000036231415471104000145320ustar00rootroot00000000000000ELVISH_MAKE_BIN ?= $(shell go env GOPATH)/bin/elvish ELVISH_PLUGIN_SUPPORT ?= 0 # Treat 0 as false and everything else as true (consistent with CGO_ENABLED). ifeq ($(ELVISH_PLUGIN_SUPPORT), 0) REPRODUCIBLE := true else REPRODUCIBLE := false endif default: test get get: export CGO_ENABLED=$(ELVISH_PLUGIN_SUPPORT); \ if go env GOOS GOARCH | egrep -qx '(windows .*|linux (amd64|arm64))'; then \ export GOFLAGS=-buildmode=pie; \ fi; \ mkdir -p $(shell dirname $(ELVISH_MAKE_BIN)) go build -o $(ELVISH_MAKE_BIN) -trimpath -ldflags \ "-X src.elv.sh/pkg/buildinfo.VersionSuffix=-dev.$$(git rev-parse HEAD)$$(git diff HEAD --quiet || printf +%s `uname -n`) \ -X src.elv.sh/pkg/buildinfo.Reproducible=$(REPRODUCIBLE)" ./cmd/elvish generate: go generate ./... # Run unit tests, with race detection if the platform supports it. test: go test $(shell ./tools/run-race.sh) ./... cd website; go test $(shell ./tools/run-race.sh) ./... # Generate a basic test coverage report, and open it in the browser. See also # https://apps.codecov.io/gh/elves/elvish/. cover: go test -coverprofile=cover -coverpkg=./pkg/... ./pkg/... ./tools/prune-cover.sh .codecov.yml cover go tool cover -html=cover go tool cover -func=cover | tail -1 | awk '{ print "Overall coverage:", $$NF }' # Ensure the style of Go and Markdown source files is consistent. style: find . -name '*.go' | xargs goimports -w find . -name '*.go' | xargs gofmt -s -w find . -name '*.md' | xargs prettier --tab-width 4 --prose-wrap always --write # Check if the style of the Go and Markdown files is correct without modifying # those files. checkstyle: checkstyle-go checkstyle-md checkstyle-go: ./tools/checkstyle-go.sh checkstyle-md: ./tools/checkstyle-md.sh lint: ./tools/lint.sh codespell: codespell --skip .git .SILENT: checkstyle-go checkstyle-md lint .PHONY: default get generate test style checkstyle checkstyle-go checkstyle-md lint cover elvish-0.17.0/PACKAGING.md000066400000000000000000000111161415471104000146740ustar00rootroot00000000000000# Packager's Manual **Note**: The guidance here applies to the current development version and release versions starting from 0.16.0. The details for earlier versions are different. Elvish is a normal Go application, and doesn't require any special attention. Build the main package of `cmd/elvish`, and you should get a fully working binary. If you don't care about accurate version information or reproducible builds, you can now stop reading. If you do, there is a small amount of extra work to get them. ## Accurate version information The `pkg/buildinfo` package contains a constant, `Version`, and a variable, `VersionSuffix`, which are concatenated to form the full version used in the output of `elvish -version` and `elvish -buildinfo`. Their values are set as follows: - At release tags, `Version` contains the version of the release, which is identical to the tag name. `VersionSuffix` is empty. - At development commits, `Version` contains the version of the next release. `VersionSuffix` is set to `-dev.unknown`. The `VersionSuffix` variable can be overridden at build time, by passing `-ldflags "-X src.elv.sh/pkg/buildinfo.VersionSuffix=-foobar"` to `go build`, `go install` or `go get`. This is necessary in several scenarios, which are documented below. ### Packaging release versions If you are using the standard Go toolchain and not applying any patches, there is nothing more to do; the default empty `VersionSuffix` suffices. If you are using a non-standard toolchain, or have applied any patches that can affect the resulting binary, you **must** override `VersionSuffix` with a string that starts with `+` and can uniquely identify your toolchain and patch. For official Linux distribution builds, this should identify your distribution, plus the version of the patch. Example: ```sh go build -ldflags "-X src.elv.sh/pkg/buildinfo.VersionSuffix=+deb1" ./cmd/elvish ``` ### Packaging development builds If you are packaging development builds, the default value of `VersionSuffix`, which is `-dev.unknown`, is likely not good enough, as it does not identify the commit Elvish is built from. You should override `VersionSuffix` with `-dev.$commit_hash`, where `$commit_hash` is the full commit hash, which can be obtained with `git rev-parse HEAD`. Example: ```sh go build -ldflags \ "-X src.elv.sh/pkg/buildinfo.VersionSuffix=-dev.$(git rev-parse HEAD)" \ ./cmd/elvish ``` If you have applied any patches that is not committed as a Git commit, you should also append a string that starts with `+` and can uniquely identify your patch. ## Reproducible builds The idea of [reproducible build](https://en.wikipedia.org/wiki/Reproducible_builds) is that an Elvish binary from two different sources should be bit-to-bit identical, as long as they are built from the same version of the source code using the same version of the Go compiler. To make reproducible builds, you must do the following: - Pass `-trimpath` to the Go compiler. - For the following platforms, also pass `-buildmode=pie` to the Go compiler: - `GOOS=windows`, any `GOARCH` - `GOOS=linux`, `GOARCH=amd64` or `GOARCH=arm64` - Disable cgo by setting the `CGO_ENABLED` environment variable to 0. - Follow the requirements above for putting [accurate version information](#accurate-version-information) into the binary, so that the user is able to uniquely identify the build by running `elvish -version`. The recommendation for how to set `VersionSuffix` when [packaging development builds](#packaging-development-builds) becomes hard requirements when packaging reproducible builds. In addition, if your distribution uses a patched version of the Go compiler that changes its output, or if the build command uses any additional flags (either via the command line or via any environment variables), you must treat this as a patch on Elvish itself, and supply a version suffix accordingly. If you follow these requirements when building Elvish, you can mark the build as a reproducible one by overriding `src.elv.sh/pkg/buildinfo.Reproducible` to `"true"`. Example when building a release version without any patches, on a platform where PIE is applicable: ```sh go build -buildmode=pie -trimpath \ -ldflags "-X src.elv.sh/pkg/buildinfo.Reproducible=true" \ ./cmd/elvish ``` Example when building a development version with a patch, on a platform where PIE is application: ```sh go build -buildmode=pie -trimpath \ -ldflags "-X src.elv.sh/pkg/buildinfo.VersionSuffix=-dev.$(git rev-parse HEAD)+deb0 \ -X src.elv.sh/pkg/buildinfo.Reproducible=true" \ ./cmd/elvish ``` elvish-0.17.0/README.md000066400000000000000000000067561415471104000143630ustar00rootroot00000000000000# Elvish: Expressive Programming Language + Versatile Interactive Shell [![CI status](https://github.com/elves/elvish/workflows/CI/badge.svg)](https://github.com/elves/elvish/actions?query=workflow%3ACI) [![FreeBSD & gccgo test status](https://img.shields.io/cirrus/github/elves/elvish?logo=Cirrus%20CI&label=CI2)](https://cirrus-ci.com/github/elves/elvish/master) [![Test Coverage](https://img.shields.io/codecov/c/github/elves/elvish/master.svg?logo=Codecov&label=coverage)](https://app.codecov.io/gh/elves/elvish/branch/master) [![Go Reference](https://pkg.go.dev/badge/src.elv.sh@master.svg)](https://pkg.go.dev/src.elv.sh@master) [![Packaging status](https://repology.org/badge/tiny-repos/elvish.svg)](https://repology.org/project/elvish/versions) [![Twitter](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/ElvishShell) Elvish is an expressive programming language and a versatile interactive shell, combined into one seamless package. It runs on Linux, BSDs, macOS and Windows. Despite its pre-1.0 status, it is already suitable for most daily interactive use. **Visit the official website https://elv.sh for prebuilt binaries, blog posts, documentation and other resources.** User groups (all connected thanks to [Matrix](https://matrix.org)): [![Gitter](https://img.shields.io/badge/gitter-elves/elvish-blue.svg?logo=gitter-white)](https://gitter.im/elves/elvish) [![Telegram Group](https://img.shields.io/badge/telegram-@elvish-blue.svg)](https://telegram.me/elvish) [![#elvish on libera.chat](https://img.shields.io/badge/libera.chat-%23elvish-blue.svg)](https://web.libera.chat/#elvish) [![#users:elves.sh](https://img.shields.io/badge/matrix-%23users:elv.sh-blue.svg)](https://matrix.to/#/#users:elves.sh) ## Building Elvish Most users do not need to build Elvish from source. Prebuilt binaries for the latest commit are provided for [Linux amd64](https://dl.elv.sh/linux-amd64/elvish-HEAD.tar.gz), [macOS amd64](https://dl.elv.sh/darwin-amd64/elvish-HEAD.tar.gz), [Windows amd64](https://dl.elv.sh/windows-amd64/elvish-HEAD.zip) and [many other platforms](https://elv.sh/get). To build Elvish from source, you need - A supported OS: Linux, {Free,Net,Open}BSD, macOS, or Windows 10. **NOTE**: Windows 10 support is experimental. - Go >= 1.16. To build Elvish from source, follow these steps: ```sh # 1. Start from any directory you want to store Elvish's source code # 2. Clone the Git repository git clone https://github.com/elves/elvish # 3. Change into the repository cd elvish # 4. Build and install Elvish make get ``` This will install Elvish to `~/go/bin` (or `$GOPATH/bin` if you have set `$GOPATH`). You might want to add the directory to your `PATH`. To install it elsewhere, override `ELVISH_MAKE_BIN` in the `make` command: ```sh make get ELVISH_MAKE_BIN=./elvish # Install to the repo root make get ELVISH_MAKE_BIN=/usr/local/bin/elvish # Install to /usr/local/bin ``` ### Experimental plugin support Elvish has experimental support for building and importing plugins, modules written in Go. However, since plugin support relies on dynamic linking, it is not enabled in the official prebuilt binaries. You need to build Elvish from source, with `ELVISH_PLUGIN_SUPPORT=1`: ```sh make get ELVISH_PLUGIN_SUPPORT=1 ``` To build a plugin, see this [example](https://github.com/elves/sample-plugin). ## Packaging Elvish See [PACKAGING.md](PACKAGING.md) for notes for packagers. ## Contributing to Elvish See [CONTRIBUTING.md](CONTRIBUTING.md) for notes for contributors. elvish-0.17.0/SECURITY.md000066400000000000000000000007131415471104000146600ustar00rootroot00000000000000# Security Policy ## Supported Versions Only the HEAD and the last release is supported by the developers of Elvish. However, since some operating systems contain outdated Elvish packages, please also feel free to get in touch for security issues in unsupported versions. You can check the versions of Elvish packages on [Repology](https://repology.org/project/elvish/versions). ## Reporting a Vulnerability Please contact Qi Xiao at xiaqqaix@gmail.com. elvish-0.17.0/cmd/000077500000000000000000000000001415471104000136315ustar00rootroot00000000000000elvish-0.17.0/cmd/elvish/000077500000000000000000000000001415471104000151235ustar00rootroot00000000000000elvish-0.17.0/cmd/elvish/main.go000066400000000000000000000011761415471104000164030ustar00rootroot00000000000000// Elvish is a cross-platform shell, supporting Linux, BSDs and Windows. It // features an expressive programming language, with features like namespacing // and anonymous functions, and a fully programmable user interface with // friendly defaults. It is suitable for both interactive use and scripting. package main import ( "os" "src.elv.sh/pkg/buildinfo" "src.elv.sh/pkg/daemon" "src.elv.sh/pkg/prog" "src.elv.sh/pkg/shell" ) func main() { os.Exit(prog.Run( [3]*os.File{os.Stdin, os.Stdout, os.Stderr}, os.Args, prog.Composite( buildinfo.Program, daemon.Program, shell.Program{ActivateDaemon: daemon.Activate}))) } elvish-0.17.0/cmd/examples/000077500000000000000000000000001415471104000154475ustar00rootroot00000000000000elvish-0.17.0/cmd/examples/e3bc/000077500000000000000000000000001415471104000162635ustar00rootroot00000000000000elvish-0.17.0/cmd/examples/e3bc/bc/000077500000000000000000000000001415471104000166475ustar00rootroot00000000000000elvish-0.17.0/cmd/examples/e3bc/bc/bc.go000066400000000000000000000017401415471104000175640ustar00rootroot00000000000000package bc import ( "io" "log" "os" "os/exec" ) type Bc interface { Exec(string) error Quit() } type bc struct { cmd *exec.Cmd stdin io.WriteCloser stdout io.ReadCloser } func Start() Bc { cmd := exec.Command("bc") stdin, err := cmd.StdinPipe() if err != nil { log.Fatal(err) } stdout, err := cmd.StdoutPipe() if err != nil { log.Fatal(err) } cmd.Stderr = os.Stderr cmd.Start() return &bc{cmd, stdin, stdout} } // TODO: Use a more robust marker var inputSuffix = []byte("\n\"\x04\"\n") func (bc *bc) Exec(code string) error { bc.stdin.Write([]byte(code)) bc.stdin.Write(inputSuffix) for { b, err := readByte(bc.stdout) if err != nil { return err } if b == 0x04 { break } os.Stdout.Write([]byte{b}) } return nil } func readByte(r io.Reader) (byte, error) { var buf [1]byte _, err := r.Read(buf[:]) if err != nil { return 0, err } return buf[0], nil } func (bc *bc) Quit() { bc.stdin.Close() bc.cmd.Wait() bc.stdout.Close() } elvish-0.17.0/cmd/examples/e3bc/completion.go000066400000000000000000000010771415471104000207700ustar00rootroot00000000000000package main import "src.elv.sh/pkg/cli/modes" var items = []string{ // Functions "length(", "read(", "scale(", "sqrt(", // Functions in math library "s(", "c(", "a(", "l(", "e(", "j(", // Statements "print ", "if ", "while (", "for (", "break", "continue", "halt", "return", "return (", // Pseudo statements "limits", "quit", "warranty", } func candidates() []modes.CompletionItem { candidates := make([]modes.CompletionItem, len(items)) for i, item := range items { candidates[i] = modes.CompletionItem{ToShow: item, ToInsert: item} } return candidates } elvish-0.17.0/cmd/examples/e3bc/main.go000066400000000000000000000032731415471104000175430ustar00rootroot00000000000000// Command e3bc ("Elvish-editor-enhanced bc") is a wrapper for the bc command // that uses Elvish's cli library for an enhanced CLI experience. package main import ( "fmt" "io" "unicode" "src.elv.sh/cmd/examples/e3bc/bc" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/modes" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/ui" ) // A highlighter for bc code. Currently this just makes all digits green. // // TODO: Highlight more syntax of bc. type highlighter struct{} func (highlighter) Get(code string) (ui.Text, []error) { t := ui.Text{} for _, r := range code { var style ui.Styling if unicode.IsDigit(r) { style = ui.FgGreen } t = append(t, ui.T(string(r), style)...) } return t, nil } func (highlighter) LateUpdates() <-chan struct{} { return nil } func main() { var app cli.App app = cli.NewApp(cli.AppSpec{ Prompt: cli.NewConstPrompt(ui.T("bc> ")), Highlighter: highlighter{}, CodeAreaBindings: tk.MapBindings{ term.K('D', ui.Ctrl): func(tk.Widget) { app.CommitEOF() }, term.K(ui.Tab): func(w tk.Widget) { codearea := w.(tk.CodeArea) if codearea.CopyState().Buffer.Content != "" { // Only complete with an empty buffer return } w, err := modes.NewCompletion(app, modes.CompletionSpec{ Replace: diag.Ranging{From: 0, To: 0}, Items: candidates(), }) if err == nil { app.PushAddon(w) } }, }, GlobalBindings: tk.MapBindings{ term.K('[', ui.Ctrl): func(tk.Widget) { app.PopAddon() }, }, }) bc := bc.Start() defer bc.Quit() for { code, err := app.ReadCode() if err != nil { if err != io.EOF { fmt.Println("error:", err) } break } bc.Exec(code) } } elvish-0.17.0/cmd/examples/nav/000077500000000000000000000000001415471104000162335ustar00rootroot00000000000000elvish-0.17.0/cmd/examples/nav/main.go000066400000000000000000000010031415471104000175000ustar00rootroot00000000000000// Command nav runs the navigation mode of the line editor. package main import ( "fmt" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/modes" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/cli/tk" ) func main() { app := cli.NewApp(cli.AppSpec{}) w, _ := modes.NewNavigation(app, modes.NavigationSpec{ Bindings: tk.MapBindings{ term.K('x'): func(tk.Widget) { app.CommitCode() }, }, }) app.PushAddon(w) code, err := app.ReadCode() fmt.Println("code:", code) if err != nil { fmt.Println("err", err) } } elvish-0.17.0/cmd/examples/widget/000077500000000000000000000000001415471104000167325ustar00rootroot00000000000000elvish-0.17.0/cmd/examples/widget/main.go000066400000000000000000000030771415471104000202140ustar00rootroot00000000000000// Command widget allows manually testing a single widget. package main import ( "flag" "fmt" "os" "strconv" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/ui" ) var ( maxHeight = flag.Int("max-height", 10, "maximum height") horizontal = flag.Bool("horizontal", false, "use horizontal listbox layout") ) func makeWidget() tk.Widget { items := tk.TestItems{Prefix: "list item "} w := tk.NewComboBox(tk.ComboBoxSpec{ CodeArea: tk.CodeAreaSpec{ Prompt: func() ui.Text { return ui.Concat(ui.T(" NUMBER ", ui.Bold, ui.BgMagenta), ui.T(" ")) }, }, ListBox: tk.ListBoxSpec{ State: tk.ListBoxState{Items: &items}, Placeholder: ui.T("(no items)"), Horizontal: *horizontal, }, OnFilter: func(w tk.ComboBox, filter string) { if n, err := strconv.Atoi(filter); err == nil { items.NItems = n } }, }) return w } func main() { flag.Parse() widget := makeWidget() tty := cli.NewTTY(os.Stdin, os.Stderr) restore, err := tty.Setup() if err != nil { fmt.Fprintln(os.Stderr, err) return } defer restore() defer tty.CloseReader() for { h, w := tty.Size() if h > *maxHeight { h = *maxHeight } tty.UpdateBuffer(nil, widget.Render(w, h), false) event, err := tty.ReadEvent() if err != nil { errBuf := term.NewBufferBuilder(w).Write(err.Error(), ui.FgRed).Buffer() tty.UpdateBuffer(nil, errBuf, true) break } handled := widget.Handle(event) if !handled && event == term.K('D', ui.Ctrl) { tty.UpdateBuffer(nil, term.NewBufferBuilder(w).Buffer(), true) break } } } elvish-0.17.0/cmd/examples/win_tty/000077500000000000000000000000001415471104000171445ustar00rootroot00000000000000elvish-0.17.0/cmd/examples/win_tty/main_windows.go000066400000000000000000000043621415471104000221760ustar00rootroot00000000000000package main import ( "log" "os" "strings" "unicode" "golang.org/x/sys/windows" "src.elv.sh/pkg/sys/ewindows" ) func main() { restore := setup(os.Stdin, os.Stdout) defer restore() log.Println("ready") console, err := windows.GetStdHandle(windows.STD_INPUT_HANDLE) if err != nil { log.Fatalf("GetStdHandle(STD_INPUT_HANDLE): %v", err) } for { var buf [1]ewindows.InputRecord nr, err := ewindows.ReadConsoleInput(console, buf[:]) if nr == 0 { log.Fatal("no event read") } if err != nil { log.Fatal(err) } event := buf[0].GetEvent() switch event := event.(type) { case *ewindows.KeyEvent: typ := "up" if event.BKeyDown != 0 { typ = "down" } r := rune(event.UChar[0]) + rune(event.UChar[1])<<8 rs := "(" + string(r) + ")" if unicode.IsControl(r) { rs = " " } var mods []string testMod := func(mask uint32, name string) { if event.DwControlKeyState&mask != 0 { mods = append(mods, name) } } testMod(0x1, "right alt") testMod(0x2, "left alt") testMod(0x4, "right ctrl") testMod(0x8, "left ctrl") testMod(0x10, "shift") // testMod(0x20, "numslock") testMod(0x40, "scrolllock") testMod(0x80, "capslock") testMod(0x100, "enhanced") log.Printf("%4s, key code = %02x, char = %04x %s, mods = %s\n", typ, event.WVirtualKeyCode, r, rs, strings.Join(mods, ", ")) } } } const ( wantedInMode = windows.ENABLE_WINDOW_INPUT | windows.ENABLE_MOUSE_INPUT | windows.ENABLE_PROCESSED_INPUT wantedOutMode = windows.ENABLE_PROCESSED_OUTPUT | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING ) func setup(in, out *os.File) func() { hIn := windows.Handle(in.Fd()) hOut := windows.Handle(out.Fd()) var oldInMode, oldOutMode uint32 err := windows.GetConsoleMode(hIn, &oldInMode) if err != nil { log.Fatal(err) } err = windows.GetConsoleMode(hOut, &oldOutMode) if err != nil { log.Fatal(err) } err = windows.SetConsoleMode(hIn, wantedInMode) if err != nil { log.Fatal(err) } err = windows.SetConsoleMode(hOut, wantedOutMode) if err != nil { log.Fatal(err) } return func() { err := windows.SetConsoleMode(hIn, oldInMode) if err != nil { log.Fatal(err) } err = windows.SetConsoleMode(hOut, oldOutMode) if err != nil { log.Fatal(err) } } } elvish-0.17.0/cmd/nodaemon/000077500000000000000000000000001415471104000154315ustar00rootroot00000000000000elvish-0.17.0/cmd/nodaemon/elvish/000077500000000000000000000000001415471104000167235ustar00rootroot00000000000000elvish-0.17.0/cmd/nodaemon/elvish/main.go000066400000000000000000000012131415471104000201730ustar00rootroot00000000000000// Command elvish is an alternative main program of Elvish that does not include // the daemon subprogram. package main import ( "errors" "os" "src.elv.sh/pkg/buildinfo" "src.elv.sh/pkg/prog" "src.elv.sh/pkg/shell" ) func main() { os.Exit(prog.Run( [3]*os.File{os.Stdin, os.Stdout, os.Stderr}, os.Args, prog.Composite(buildinfo.Program, daemonStub{}, shell.Program{}))) } var errNoDaemon = errors.New("daemon is not supported in this build") type daemonStub struct{} func (daemonStub) ShouldRun(f *prog.Flags) bool { return f.Daemon } func (daemonStub) Run(fds [3]*os.File, f *prog.Flags, args []string) error { return errNoDaemon } elvish-0.17.0/go.mod000066400000000000000000000002711415471104000141740ustar00rootroot00000000000000module src.elv.sh require ( github.com/creack/pty v1.1.15 github.com/mattn/go-isatty v0.0.13 go.etcd.io/bbolt v1.3.6 golang.org/x/sys v0.0.0-20210820121016-41cdb8703e55 ) go 1.16 elvish-0.17.0/go.sum000066400000000000000000000016141415471104000142230ustar00rootroot00000000000000github.com/creack/pty v1.1.15 h1:cKRCLMj3Ddm54bKSpemfQ8AtYFBhAI2MPmdys22fBdc= github.com/creack/pty v1.1.15/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210820121016-41cdb8703e55 h1:rw6UNGRMfarCepjI8qOepea/SXwIBVfTKjztZ5gBbq4= golang.org/x/sys v0.0.0-20210820121016-41cdb8703e55/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= elvish-0.17.0/pkg/000077500000000000000000000000001415471104000136475ustar00rootroot00000000000000elvish-0.17.0/pkg/buildinfo/000077500000000000000000000000001415471104000156225ustar00rootroot00000000000000elvish-0.17.0/pkg/buildinfo/buildinfo.go000066400000000000000000000036321415471104000201300ustar00rootroot00000000000000// Package buildinfo contains build information. // // Build information should be set during compilation by passing // -ldflags "-X src.elv.sh/pkg/buildinfo.Var=value" to "go build" or // "go get". package buildinfo import ( "encoding/json" "fmt" "os" "runtime" "src.elv.sh/pkg/prog" ) // Version identifies the version of Elvish. On development commits, it // identifies the next release. const Version = "0.17.0" // VersionSuffix is appended to Version to build the full version string. It is public so it can be // overridden when building Elvish; see PACKAGING.md for details. var VersionSuffix = "" // Reproducible identifies whether the build is reproducible. This can be // overridden when building Elvish; see PACKAGING.md for details. var Reproducible = "false" // Program is the buildinfo subprogram. var Program prog.Program = program{} // Type contains all the build information fields. type Type struct { Version string `json:"version"` Reproducible bool `json:"reproducible"` GoVersion string `json:"goversion"` } func (Type) IsStructMap() {} // Value contains all the build information. var Value = Type{ Version: Version + VersionSuffix, Reproducible: Reproducible == "true", GoVersion: runtime.Version(), } type program struct{} func (program) Run(fds [3]*os.File, f *prog.Flags, _ []string) error { switch { case f.BuildInfo: if f.JSON { fmt.Fprintln(fds[1], mustToJSON(Value)) } else { fmt.Fprintln(fds[1], "Version:", Value.Version) fmt.Fprintln(fds[1], "Go version:", Value.GoVersion) fmt.Fprintln(fds[1], "Reproducible build:", Value.Reproducible) } case f.Version: if f.JSON { fmt.Fprintln(fds[1], mustToJSON(Value.Version)) } else { fmt.Fprintln(fds[1], Value.Version) } default: return prog.ErrNotSuitable } return nil } func mustToJSON(v interface{}) string { b, err := json.Marshal(v) if err != nil { panic(err) } return string(b) } elvish-0.17.0/pkg/buildinfo/buildinfo_test.go000066400000000000000000000011511415471104000211610ustar00rootroot00000000000000package buildinfo import ( "fmt" "testing" . "src.elv.sh/pkg/prog/progtest" ) func TestProgram(t *testing.T) { Test(t, Program, ThatElvish("-version").WritesStdout(Value.Version+"\n"), ThatElvish("-version", "-json").WritesStdout(mustToJSON(Value.Version)+"\n"), ThatElvish("-buildinfo").WritesStdout( fmt.Sprintf( "Version: %v\nGo version: %v\nReproducible build: %v\n", Value.Version, Value.GoVersion, Value.Reproducible)), ThatElvish("-buildinfo", "-json").WritesStdout(mustToJSON(Value)+"\n"), ThatElvish().ExitsWith(2).WritesStderr("internal error: no suitable subprogram\n"), ) } elvish-0.17.0/pkg/cli/000077500000000000000000000000001415471104000144165ustar00rootroot00000000000000elvish-0.17.0/pkg/cli/app.go000066400000000000000000000275041415471104000155350ustar00rootroot00000000000000// Package cli implements a generic interactive line editor. package cli import ( "io" "os" "sort" "sync" "syscall" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/sys" ) // App represents a CLI app. type App interface { // ReadCode requests the App to read code from the terminal by running an // event loop. This function is not re-entrant. ReadCode() (string, error) // MutateState mutates the state of the app. MutateState(f func(*State)) // CopyState returns a copy of the a state. CopyState() State // PushAddon pushes a widget to the addon stack. PushAddon(w tk.Widget) // PopAddon pops the last widget from the addon stack. If the widget // implements interface{ Dismiss() }, the Dismiss method is called // first. This method does nothing if the addon stack is empty. PopAddon() // ActiveWidget returns the currently active widget. If the addon stack is // non-empty, it returns the last addon. Otherwise it returns the main code // area widget. ActiveWidget() tk.Widget // FocusedWidget returns the currently focused widget. It is searched like // ActiveWidget, but skips widgets that implement interface{ Focus() bool } // and return false when .Focus() is called. FocusedWidget() tk.Widget // CommitEOF causes the main loop to exit with EOF. If this method is called // when an event is being handled, the main loop will exit after the handler // returns. CommitEOF() // CommitCode causes the main loop to exit with the current code content. If // this method is called when an event is being handled, the main loop will // exit after the handler returns. CommitCode() // Redraw requests a redraw. It never blocks and can be called regardless of // whether the App is active or not. Redraw() // RedrawFull requests a full redraw. It never blocks and can be called // regardless of whether the App is active or not. RedrawFull() // Notify adds a note and requests a redraw. Notify(note string) } type app struct { loop *loop reqRead chan struct{} TTY TTY MaxHeight func() int RPromptPersistent func() bool BeforeReadline []func() AfterReadline []func(string) Highlighter Highlighter Prompt Prompt RPrompt Prompt GlobalBindings tk.Bindings StateMutex sync.RWMutex State State codeArea tk.CodeArea } // State represents mutable state of an App. type State struct { // Notes that have been added since the last redraw. Notes []string // The addon stack. All widgets are shown under the codearea widget. The // last widget handles terminal events. Addons []tk.Widget } // NewApp creates a new App from the given specification. func NewApp(spec AppSpec) App { lp := newLoop() a := app{ loop: lp, TTY: spec.TTY, MaxHeight: spec.MaxHeight, RPromptPersistent: spec.RPromptPersistent, BeforeReadline: spec.BeforeReadline, AfterReadline: spec.AfterReadline, Highlighter: spec.Highlighter, Prompt: spec.Prompt, RPrompt: spec.RPrompt, GlobalBindings: spec.GlobalBindings, State: spec.State, } if a.TTY == nil { a.TTY = NewTTY(os.Stdin, os.Stderr) } if a.MaxHeight == nil { a.MaxHeight = func() int { return -1 } } if a.RPromptPersistent == nil { a.RPromptPersistent = func() bool { return false } } if a.Highlighter == nil { a.Highlighter = dummyHighlighter{} } if a.Prompt == nil { a.Prompt = NewConstPrompt(nil) } if a.RPrompt == nil { a.RPrompt = NewConstPrompt(nil) } if a.GlobalBindings == nil { a.GlobalBindings = tk.DummyBindings{} } lp.HandleCb(a.handle) lp.RedrawCb(a.redraw) a.codeArea = tk.NewCodeArea(tk.CodeAreaSpec{ Bindings: spec.CodeAreaBindings, Highlighter: a.Highlighter.Get, Prompt: a.Prompt.Get, RPrompt: a.RPrompt.Get, Abbreviations: spec.Abbreviations, QuotePaste: spec.QuotePaste, OnSubmit: a.CommitCode, State: spec.CodeAreaState, SmallWordAbbreviations: spec.SmallWordAbbreviations, }) return &a } func (a *app) MutateState(f func(*State)) { a.StateMutex.Lock() defer a.StateMutex.Unlock() f(&a.State) } func (a *app) CopyState() State { a.StateMutex.RLock() defer a.StateMutex.RUnlock() return State{ append([]string(nil), a.State.Notes...), append([]tk.Widget(nil), a.State.Addons...), } } type dismisser interface { Dismiss() } func (a *app) PushAddon(w tk.Widget) { a.StateMutex.Lock() defer a.StateMutex.Unlock() a.State.Addons = append(a.State.Addons, w) } func (a *app) PopAddon() { a.StateMutex.Lock() defer a.StateMutex.Unlock() if len(a.State.Addons) == 0 { return } if d, ok := a.State.Addons[len(a.State.Addons)-1].(dismisser); ok { d.Dismiss() } a.State.Addons = a.State.Addons[:len(a.State.Addons)-1] } func (a *app) ActiveWidget() tk.Widget { a.StateMutex.Lock() defer a.StateMutex.Unlock() if len(a.State.Addons) > 0 { return a.State.Addons[len(a.State.Addons)-1] } return a.codeArea } func (a *app) FocusedWidget() tk.Widget { a.StateMutex.Lock() defer a.StateMutex.Unlock() addons := a.State.Addons for i := len(addons) - 1; i >= 0; i-- { if hasFocus(addons[i]) { return addons[i] } } return a.codeArea } func (a *app) resetAllStates() { a.MutateState(func(s *State) { *s = State{} }) a.codeArea.MutateState( func(s *tk.CodeAreaState) { *s = tk.CodeAreaState{} }) } func (a *app) handle(e event) { switch e := e.(type) { case os.Signal: switch e { case syscall.SIGHUP: a.loop.Return("", io.EOF) case syscall.SIGINT: a.resetAllStates() a.triggerPrompts(true) case sys.SIGWINCH: a.RedrawFull() } case term.Event: target := a.ActiveWidget() handled := target.Handle(e) if !handled { a.GlobalBindings.Handle(target, e) } if !a.loop.HasReturned() { a.triggerPrompts(false) a.reqRead <- struct{}{} } } } func (a *app) triggerPrompts(force bool) { a.Prompt.Trigger(force) a.RPrompt.Trigger(force) } func (a *app) redraw(flag redrawFlag) { // Get the dimensions available. height, width := a.TTY.Size() if maxHeight := a.MaxHeight(); maxHeight > 0 && maxHeight < height { height = maxHeight } var notes []string var addons []tk.Widget a.MutateState(func(s *State) { notes = s.Notes s.Notes = nil addons = append([]tk.Widget(nil), s.Addons...) }) bufNotes := renderNotes(notes, width) isFinalRedraw := flag&finalRedraw != 0 if isFinalRedraw { hideRPrompt := !a.RPromptPersistent() if hideRPrompt { a.codeArea.MutateState(func(s *tk.CodeAreaState) { s.HideRPrompt = true }) } bufMain := renderApp([]tk.Widget{a.codeArea /* no addon */}, width, height) if hideRPrompt { a.codeArea.MutateState(func(s *tk.CodeAreaState) { s.HideRPrompt = false }) } // Insert a newline after the buffer and position the cursor there. bufMain.Extend(term.NewBuffer(width), true) a.TTY.UpdateBuffer(bufNotes, bufMain, flag&fullRedraw != 0) a.TTY.ResetBuffer() } else { bufMain := renderApp(append([]tk.Widget{a.codeArea}, addons...), width, height) a.TTY.UpdateBuffer(bufNotes, bufMain, flag&fullRedraw != 0) } } // Renders notes. This does not respect height so that overflow notes end up in // the scrollback buffer. func renderNotes(notes []string, width int) *term.Buffer { if len(notes) == 0 { return nil } bb := term.NewBufferBuilder(width) for i, note := range notes { if i > 0 { bb.Newline() } bb.Write(note) } return bb.Buffer() } // Renders the codearea, and uses the rest of the height for the listing. func renderApp(widgets []tk.Widget, width, height int) *term.Buffer { heights, focus := distributeHeight(widgets, width, height) var buf *term.Buffer for i, w := range widgets { if heights[i] == 0 { continue } buf2 := w.Render(width, heights[i]) if buf == nil { buf = buf2 } else { buf.Extend(buf2, i == focus) } } return buf } // Distributes the height among all the widgets. Returns the height for each // widget, and the index of the widget currently focused. func distributeHeight(widgets []tk.Widget, width, height int) ([]int, int) { var focus int for i, w := range widgets { if hasFocus(w) { focus = i } } n := len(widgets) heights := make([]int, n) if height <= n { // Not enough (or just enough) height to render every widget with a // height of 1. remain := height // Start from the focused widget, and extend downwards as much as // possible. for i := focus; i < n && remain > 0; i++ { heights[i] = 1 remain-- } // If there is still space remaining, start from the focused widget // again, and extend upwards as much as possible. for i := focus - 1; i >= 0 && remain > 0; i-- { heights[i] = 1 remain-- } return heights, focus } maxHeights := make([]int, n) for i, w := range widgets { maxHeights[i] = w.MaxHeight(width, height) } // The algorithm below achieves the following goals: // // 1. If maxHeights[u] > maxHeights[v], heights[u] >= heights[v]; // // 2. While achieving goal 1, have as many widgets u s.t. heights[u] == // maxHeights[u]. // // This is done by allocating the height among the widgets following an // non-decreasing order of maxHeights. At each step: // // - If it's possible to allocate maxHeights[u] to all remaining widgets, // then allocate maxHeights[u] to widget u; // // - If not, allocate the remaining budget evenly - rounding down at each // step, so the widgets with smaller maxHeights gets smaller heights. indices := make([]int, n) for i := range indices { indices[i] = i } sort.Slice(indices, func(i, j int) bool { return maxHeights[indices[i]] < maxHeights[indices[j]] }) remain := height for rank, idx := range indices { if remain >= maxHeights[idx] { heights[idx] = maxHeights[idx] } else { heights[idx] = remain / (n - rank) } remain -= heights[idx] } return heights, focus } func hasFocus(w interface{}) bool { if f, ok := w.(interface{ Focus() bool }); ok { return f.Focus() } return true } func (a *app) ReadCode() (string, error) { for _, f := range a.BeforeReadline { f() } defer func() { content := a.codeArea.CopyState().Buffer.Content for _, f := range a.AfterReadline { f(content) } a.resetAllStates() }() restore, err := a.TTY.Setup() if err != nil { return "", err } defer restore() var wg sync.WaitGroup defer wg.Wait() // Relay input events. a.reqRead = make(chan struct{}, 1) a.reqRead <- struct{}{} defer close(a.reqRead) defer a.TTY.CloseReader() wg.Add(1) go func() { defer wg.Done() for range a.reqRead { event, err := a.TTY.ReadEvent() if err == nil { a.loop.Input(event) } else if err == term.ErrStopped { return } else if term.IsReadErrorRecoverable(err) { a.loop.Input(term.NonfatalErrorEvent{Err: err}) } else { a.loop.Input(term.FatalErrorEvent{Err: err}) return } } }() // Relay signals. sigCh := a.TTY.NotifySignals() defer a.TTY.StopSignals() wg.Add(1) go func() { for sig := range sigCh { a.loop.Input(sig) } wg.Done() }() // Relay late updates from prompt, rprompt and highlighter. stopRelayLateUpdates := make(chan struct{}) defer close(stopRelayLateUpdates) relayLateUpdates := func(ch <-chan struct{}) { if ch == nil { return } wg.Add(1) go func() { defer wg.Done() for { select { case <-ch: a.Redraw() case <-stopRelayLateUpdates: return } } }() } relayLateUpdates(a.Prompt.LateUpdates()) relayLateUpdates(a.RPrompt.LateUpdates()) relayLateUpdates(a.Highlighter.LateUpdates()) // Trigger an initial prompt update. a.triggerPrompts(true) return a.loop.Run() } func (a *app) Redraw() { a.loop.Redraw(false) } func (a *app) RedrawFull() { a.loop.Redraw(true) } func (a *app) CommitEOF() { a.loop.Return("", io.EOF) } func (a *app) CommitCode() { code := a.codeArea.CopyState().Buffer.Content a.loop.Return(code, nil) } func (a *app) Notify(note string) { a.MutateState(func(s *State) { s.Notes = append(s.Notes, note) }) a.Redraw() } elvish-0.17.0/pkg/cli/app_spec.go000066400000000000000000000037431415471104000165460ustar00rootroot00000000000000package cli import ( "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/ui" ) // AppSpec specifies the configuration and initial state for an App. type AppSpec struct { TTY TTY MaxHeight func() int RPromptPersistent func() bool BeforeReadline []func() AfterReadline []func(string) Highlighter Highlighter Prompt Prompt RPrompt Prompt GlobalBindings tk.Bindings CodeAreaBindings tk.Bindings Abbreviations func(f func(abbr, full string)) QuotePaste func() bool SmallWordAbbreviations func(f func(abbr, full string)) CodeAreaState tk.CodeAreaState State State } // Highlighter represents a code highlighter whose result can be delivered // asynchronously. type Highlighter interface { // Get returns the highlighted code and any static errors. Get(code string) (ui.Text, []error) // LateUpdates returns a channel for delivering late updates. LateUpdates() <-chan struct{} } // A Highlighter implementation that always returns plain text. type dummyHighlighter struct{} func (dummyHighlighter) Get(code string) (ui.Text, []error) { return ui.T(code), nil } func (dummyHighlighter) LateUpdates() <-chan struct{} { return nil } // Prompt represents a prompt whose result can be delivered asynchronously. type Prompt interface { // Trigger requests a re-computation of the prompt. The force flag is set // when triggered for the first time during a ReadCode session or after a // SIGINT that resets the editor. Trigger(force bool) // Get returns the current prompt. Get() ui.Text // LastUpdates returns a channel for notifying late updates. LateUpdates() <-chan struct{} } // NewConstPrompt returns a Prompt that always shows the given text. func NewConstPrompt(t ui.Text) Prompt { return constPrompt{t} } type constPrompt struct{ Content ui.Text } func (constPrompt) Trigger(force bool) {} func (p constPrompt) Get() ui.Text { return p.Content } func (constPrompt) LateUpdates() <-chan struct{} { return nil } elvish-0.17.0/pkg/cli/app_test.go000066400000000000000000000356141415471104000165750ustar00rootroot00000000000000package cli_test import ( "errors" "io" "strings" "syscall" "testing" "time" . "src.elv.sh/pkg/cli" . "src.elv.sh/pkg/cli/clitest" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/sys" "src.elv.sh/pkg/testutil" "src.elv.sh/pkg/ui" ) // Lifecycle aspects. func TestReadCode_AbortsWhenTTYSetupReturnsError(t *testing.T) { ttySetupErr := errors.New("a fake error") f := Setup(WithTTY(func(tty TTYCtrl) { tty.SetSetup(func() {}, ttySetupErr) })) _, err := f.Wait() if err != ttySetupErr { t.Errorf("ReadCode returns error %v, want %v", err, ttySetupErr) } } func TestReadCode_RestoresTTYBeforeReturning(t *testing.T) { restoreCalled := 0 f := Setup(WithTTY(func(tty TTYCtrl) { tty.SetSetup(func() { restoreCalled++ }, nil) })) f.Stop() if restoreCalled != 1 { t.Errorf("Restore callback called %d times, want once", restoreCalled) } } func TestReadCode_ResetsStateBeforeReturning(t *testing.T) { f := Setup(WithSpec(func(spec *AppSpec) { spec.CodeAreaState.Buffer.Content = "some code" })) f.Stop() if code := f.App.ActiveWidget().(tk.CodeArea).CopyState().Buffer; code != (tk.CodeBuffer{}) { t.Errorf("Editor state has CodeBuffer %v, want empty", code) } } func TestReadCode_CallsBeforeReadline(t *testing.T) { callCh := make(chan bool, 1) f := Setup(WithSpec(func(spec *AppSpec) { spec.BeforeReadline = []func(){func() { callCh <- true }} })) defer f.Stop() select { case <-callCh: // OK, do nothing. case <-time.After(time.Second): t.Errorf("BeforeReadline not called") } } func TestReadCode_CallsBeforeReadlineBeforePromptTrigger(t *testing.T) { callCh := make(chan string, 2) f := Setup(WithSpec(func(spec *AppSpec) { spec.BeforeReadline = []func(){func() { callCh <- "hook" }} spec.Prompt = testPrompt{trigger: func(bool) { callCh <- "prompt" }} })) defer f.Stop() if first := <-callCh; first != "hook" { t.Errorf("BeforeReadline hook not called before prompt trigger") } } func TestReadCode_CallsAfterReadline(t *testing.T) { callCh := make(chan string, 1) f := Setup(WithSpec(func(spec *AppSpec) { spec.AfterReadline = []func(string){func(s string) { callCh <- s }} })) feedInput(f.TTY, "abc\n") f.Wait() select { case calledWith := <-callCh: wantCalledWith := "abc" if calledWith != wantCalledWith { t.Errorf("AfterReadline hook called with %v, want %v", calledWith, wantCalledWith) } case <-time.After(time.Second): t.Errorf("AfterReadline not called") } } func TestReadCode_FinalRedraw(t *testing.T) { f := Setup(WithSpec(func(spec *AppSpec) { spec.CodeAreaState.Buffer.Content = "code" spec.State.Addons = []tk.Widget{tk.Label{Content: ui.T("addon")}} })) // Wait until the stable state. wantBuf := bb(). Write("code"). Newline().SetDotHere().Write("addon").Buffer() f.TTY.TestBuffer(t, wantBuf) f.Stop() // Final redraw hides the addon, and puts the cursor on a new line. wantFinalBuf := bb(). Write("code").Newline().SetDotHere().Buffer() f.TTY.TestBuffer(t, wantFinalBuf) } // Signals. func TestReadCode_ReturnsEOFOnSIGHUP(t *testing.T) { f := Setup() f.TTY.Inject(term.K('a')) // Wait until the initial redraw. f.TTY.TestBuffer(t, bb().Write("a").SetDotHere().Buffer()) f.TTY.InjectSignal(syscall.SIGHUP) _, err := f.Wait() if err != io.EOF { t.Errorf("want ReadCode to return io.EOF on SIGHUP, got %v", err) } } func TestReadCode_ResetsStateOnSIGINT(t *testing.T) { f := Setup() defer f.Stop() // Ensure that the terminal shows an non-empty state. feedInput(f.TTY, "code") f.TTY.TestBuffer(t, bb().Write("code").SetDotHere().Buffer()) f.TTY.InjectSignal(syscall.SIGINT) // Verify that the state has now reset. f.TTY.TestBuffer(t, bb().Buffer()) } func TestReadCode_RedrawsOnSIGWINCH(t *testing.T) { f := Setup() defer f.Stop() // Ensure that the terminal shows the input with the initial width. feedInput(f.TTY, "1234567890") f.TTY.TestBuffer(t, bb().Write("1234567890").SetDotHere().Buffer()) // Emulate a window size change. f.TTY.SetSize(24, 4) f.TTY.InjectSignal(sys.SIGWINCH) // Test that the editor has redrawn using the new width. f.TTY.TestBuffer(t, term.NewBufferBuilder(4). Write("1234567890").SetDotHere().Buffer()) } // Code area. func TestReadCode_LetsCodeAreaHandleEvents(t *testing.T) { f := Setup() defer f.Stop() feedInput(f.TTY, "code") f.TTY.TestBuffer(t, bb().Write("code").SetDotHere().Buffer()) } func TestReadCode_ShowsHighlightedCode(t *testing.T) { f := Setup(withHighlighter( testHighlighter{ get: func(code string) (ui.Text, []error) { return ui.T(code, ui.FgRed), nil }, })) defer f.Stop() feedInput(f.TTY, "code") wantBuf := bb().Write("code", ui.FgRed).SetDotHere().Buffer() f.TTY.TestBuffer(t, wantBuf) } func TestReadCode_ShowsErrorsFromHighlighter(t *testing.T) { f := Setup(withHighlighter( testHighlighter{ get: func(code string) (ui.Text, []error) { errors := []error{errors.New("ERR 1"), errors.New("ERR 2")} return ui.T(code), errors }, })) defer f.Stop() feedInput(f.TTY, "code") wantBuf := bb(). Write("code").SetDotHere().Newline(). Write("ERR 1").Newline(). Write("ERR 2").Buffer() f.TTY.TestBuffer(t, wantBuf) } func TestReadCode_RedrawsOnLateUpdateFromHighlighter(t *testing.T) { var styling ui.Styling hl := testHighlighter{ get: func(code string) (ui.Text, []error) { return ui.T(code, styling), nil }, lateUpdates: make(chan struct{}), } f := Setup(withHighlighter(hl)) defer f.Stop() feedInput(f.TTY, "code") f.TTY.TestBuffer(t, bb().Write("code").SetDotHere().Buffer()) styling = ui.FgRed hl.lateUpdates <- struct{}{} f.TTY.TestBuffer(t, bb().Write("code", ui.FgRed).SetDotHere().Buffer()) } func withHighlighter(hl Highlighter) func(*AppSpec, TTYCtrl) { return WithSpec(func(spec *AppSpec) { spec.Highlighter = hl }) } func TestReadCode_ShowsPrompt(t *testing.T) { f := Setup(WithSpec(func(spec *AppSpec) { spec.Prompt = NewConstPrompt(ui.T("> ")) })) defer f.Stop() f.TTY.Inject(term.K('a')) f.TTY.TestBuffer(t, bb().Write("> a").SetDotHere().Buffer()) } func TestReadCode_CallsPromptTrigger(t *testing.T) { triggerCh := make(chan bool, 1) f := Setup(WithSpec(func(spec *AppSpec) { spec.Prompt = testPrompt{trigger: func(bool) { triggerCh <- true }} })) defer f.Stop() select { case <-triggerCh: // Good, test passes case <-time.After(time.Second): t.Errorf("Trigger not called within 1s") } } func TestReadCode_RedrawsOnLateUpdateFromPrompt(t *testing.T) { promptContent := "old" prompt := testPrompt{ get: func() ui.Text { return ui.T(promptContent) }, lateUpdates: make(chan struct{}), } f := Setup(WithSpec(func(spec *AppSpec) { spec.Prompt = prompt })) defer f.Stop() // Wait until old prompt is rendered f.TTY.TestBuffer(t, bb().Write("old").SetDotHere().Buffer()) promptContent = "new" prompt.lateUpdates <- struct{}{} f.TTY.TestBuffer(t, bb().Write("new").SetDotHere().Buffer()) } func TestReadCode_ShowsRPrompt(t *testing.T) { f := Setup(WithSpec(func(spec *AppSpec) { spec.RPrompt = NewConstPrompt(ui.T("R")) })) defer f.Stop() f.TTY.Inject(term.K('a')) wantBuf := bb(). Write("a").SetDotHere(). Write(strings.Repeat(" ", FakeTTYWidth-2)). Write("R").Buffer() f.TTY.TestBuffer(t, wantBuf) } func TestReadCode_ShowsRPromptInFinalRedrawIfPersistent(t *testing.T) { f := Setup(WithSpec(func(spec *AppSpec) { spec.CodeAreaState.Buffer.Content = "code" spec.RPrompt = NewConstPrompt(ui.T("R")) spec.RPromptPersistent = func() bool { return true } })) defer f.Stop() f.TTY.Inject(term.K('\n')) wantBuf := bb(). Write("code" + strings.Repeat(" ", FakeTTYWidth-5) + "R"). Newline().SetDotHere(). // cursor on newline in final redraw Buffer() f.TTY.TestBuffer(t, wantBuf) } func TestReadCode_HidesRPromptInFinalRedrawIfNotPersistent(t *testing.T) { f := Setup(WithSpec(func(spec *AppSpec) { spec.CodeAreaState.Buffer.Content = "code" spec.RPrompt = NewConstPrompt(ui.T("R")) spec.RPromptPersistent = func() bool { return false } })) defer f.Stop() f.TTY.Inject(term.K('\n')) wantBuf := bb(). Write("code"). // no rprompt Newline().SetDotHere(). // cursor on newline in final redraw Buffer() f.TTY.TestBuffer(t, wantBuf) } // Addon. func TestReadCode_LetsLastWidgetHandleEvents(t *testing.T) { f := Setup(WithSpec(func(spec *AppSpec) { spec.State.Addons = []tk.Widget{ tk.NewCodeArea(tk.CodeAreaSpec{ Prompt: func() ui.Text { return ui.T("addon1> ") }, }), tk.NewCodeArea(tk.CodeAreaSpec{ Prompt: func() ui.Text { return ui.T("addon2> ") }, }), } })) defer f.Stop() feedInput(f.TTY, "input") wantBuf := bb().Newline(). // empty main code area Write("addon1> ").Newline(). // addon1 did not handle inputs Write("addon2> input").SetDotHere(). // addon2 handled inputs Buffer() f.TTY.TestBuffer(t, wantBuf) } func TestReadCode_PutsCursorOnLastWidgetWithFocus(t *testing.T) { f := Setup(WithSpec(func(spec *AppSpec) { spec.State.Addons = []tk.Widget{ testAddon{tk.Label{Content: ui.T("addon1> ")}, true}, testAddon{tk.Label{Content: ui.T("addon2> ")}, false}, } })) defer f.Stop() f.TestTTY(t, "\n", // main code area is empty term.DotHere, "addon1> ", "\n", // addon 1 has focus "addon2> ", // addon 2 has no focus ) } func TestPushAddonPopAddon(t *testing.T) { f := Setup() defer f.Stop() f.TestTTY(t /* nothing */) f.App.PushAddon(tk.Label{Content: ui.T("addon1> ")}) f.App.Redraw() f.TestTTY(t, "\n", term.DotHere, "addon1> ") f.App.PushAddon(tk.Label{Content: ui.T("addon2> ")}) f.App.Redraw() f.TestTTY(t, "\n", "addon1> \n", term.DotHere, "addon2> ") f.App.PopAddon() f.App.Redraw() f.TestTTY(t, "\n", term.DotHere, "addon1> ") f.App.PopAddon() f.App.Redraw() f.TestTTY(t /* nothing */) // Popping addon when there is no addon does nothing f.App.PopAddon() // Add something to the codearea to ensure that we're not just looking at // the previous buffer f.TTY.Inject(term.K(' ')) f.TestTTY(t, " ", term.DotHere) } func TestReadCode_HidesAddonsWhenNotEnoughSpace(t *testing.T) { f := Setup( func(spec *AppSpec, tty TTYCtrl) { spec.State.Addons = []tk.Widget{ tk.Label{Content: ui.T("addon1> ")}, tk.Label{Content: ui.T("addon2> ")}, // no space for this } tty.SetSize(2, 40) }) defer f.Stop() f.TestTTY(t, "addon1> \n", term.DotHere, "addon2> ") } type testAddon struct { tk.Label focus bool } func (a testAddon) Focus() bool { return a.focus } // Misc features. func TestReadCode_UsesGlobalBindingsWithCodeAreaTarget(t *testing.T) { testGlobalBindings(t, nil) } func TestReadCode_UsesGlobalBindingsWithAddonTarget(t *testing.T) { testGlobalBindings(t, []tk.Widget{tk.Empty{}}) } func testGlobalBindings(t *testing.T, addons []tk.Widget) { gotWidgetCh := make(chan tk.Widget, 1) f := Setup(WithSpec(func(spec *AppSpec) { spec.GlobalBindings = tk.MapBindings{ term.K('X', ui.Ctrl): func(w tk.Widget) { gotWidgetCh <- w }, } spec.State.Addons = addons })) defer f.Stop() f.TTY.Inject(term.K('X', ui.Ctrl)) select { case gotWidget := <-gotWidgetCh: if gotWidget != f.App.ActiveWidget() { t.Error("global binding not called with the active widget") } case <-time.After(testutil.Scaled(100 * time.Millisecond)): t.Error("global binding not called") } } func TestReadCode_DoesNotUseGlobalBindingsIfHandledByWidget(t *testing.T) { f := Setup(WithSpec(func(spec *AppSpec) { spec.GlobalBindings = tk.MapBindings{ term.K('a'): func(w tk.Widget) {}, } })) defer f.Stop() f.TTY.Inject(term.K('a')) // Still handled by code area instead of global binding f.TestTTY(t, "a", term.DotHere) } func TestReadCode_TrimsBufferToMaxHeight(t *testing.T) { f := Setup(func(spec *AppSpec, tty TTYCtrl) { spec.MaxHeight = func() int { return 2 } // The code needs 3 lines to completely show. spec.CodeAreaState.Buffer.Content = strings.Repeat("a", 15) tty.SetSize(10, 5) // Width = 5 to make it easy to test }) defer f.Stop() wantBuf := term.NewBufferBuilder(5). Write(strings.Repeat("a", 10)). // Only show 2 lines due to MaxHeight. Buffer() f.TTY.TestBuffer(t, wantBuf) } func TestReadCode_ShowNotes(t *testing.T) { // Set up with a binding where 'a' can block indefinitely. This is useful // for testing the behavior of writing multiple notes. inHandler := make(chan struct{}) unblock := make(chan struct{}) f := Setup(WithSpec(func(spec *AppSpec) { spec.CodeAreaBindings = tk.MapBindings{ term.K('a'): func(tk.Widget) { inHandler <- struct{}{} <-unblock }, } })) defer f.Stop() // Wait until initial draw. f.TTY.TestBuffer(t, bb().Buffer()) // Make sure that the app is blocked within an event handler. f.TTY.Inject(term.K('a')) <-inHandler // Write two notes, and unblock the event handler f.App.Notify("note") f.App.Notify("note 2") unblock <- struct{}{} // Test that the note is rendered onto the notes buffer. wantNotesBuf := bb().Write("note").Newline().Write("note 2").Buffer() f.TTY.TestNotesBuffer(t, wantNotesBuf) // Test that notes are flushed after being rendered. if n := len(f.App.CopyState().Notes); n > 0 { t.Errorf("State.Notes has %d elements after redrawing, want 0", n) } } func TestReadCode_DoesNotCrashWithNilTTY(t *testing.T) { f := Setup(WithSpec(func(spec *AppSpec) { spec.TTY = nil })) defer f.Stop() } // Other properties. func TestReadCode_DoesNotLockWithALotOfInputsWithNewlines(t *testing.T) { // Regression test for #887 f := Setup(WithTTY(func(tty TTYCtrl) { for i := 0; i < 1000; i++ { tty.Inject(term.K('#'), term.K('\n')) } })) terminated := make(chan struct{}) go func() { f.Wait() close(terminated) }() select { case <-terminated: // OK case <-time.After(time.Second): t.Errorf("ReadCode did not terminate within 1s") } } func TestReadCode_DoesNotReadMoreEventsThanNeeded(t *testing.T) { f := Setup() defer f.Stop() f.TTY.Inject(term.K('a'), term.K('\n'), term.K('b')) code, err := f.Wait() if code != "a" || err != nil { t.Errorf("got (%q, %v), want (%q, nil)", code, err, "a") } if event := <-f.TTY.EventCh(); event != term.K('b') { t.Errorf("got event %v, want %v", event, term.K('b')) } } // Test utilities. func bb() *term.BufferBuilder { return term.NewBufferBuilder(FakeTTYWidth) } func feedInput(ttyCtrl TTYCtrl, input string) { for _, r := range input { ttyCtrl.Inject(term.K(r)) } } // A Highlighter implementation useful for testing. type testHighlighter struct { get func(code string) (ui.Text, []error) lateUpdates chan struct{} } func (hl testHighlighter) Get(code string) (ui.Text, []error) { return hl.get(code) } func (hl testHighlighter) LateUpdates() <-chan struct{} { return hl.lateUpdates } // A Prompt implementation useful for testing. type testPrompt struct { trigger func(force bool) get func() ui.Text lateUpdates chan struct{} } func (p testPrompt) Trigger(force bool) { if p.trigger != nil { p.trigger(force) } } func (p testPrompt) Get() ui.Text { if p.get != nil { return p.get() } return nil } func (p testPrompt) LateUpdates() <-chan struct{} { return p.lateUpdates } elvish-0.17.0/pkg/cli/clitest/000077500000000000000000000000001415471104000160655ustar00rootroot00000000000000elvish-0.17.0/pkg/cli/clitest/apptest.go000066400000000000000000000062501415471104000200770ustar00rootroot00000000000000// Package clitest provides utilities for testing cli.App. package clitest import ( "testing" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/ui" ) // Styles defines a common stylesheet for unit tests. var Styles = ui.RuneStylesheet{ '_': ui.Underlined, 'b': ui.Bold, '*': ui.Stylings(ui.Bold, ui.FgWhite, ui.BgMagenta), '+': ui.Inverse, '/': ui.FgBlue, '#': ui.Stylings(ui.Inverse, ui.FgBlue), '!': ui.FgRed, '?': ui.Stylings(ui.FgBrightWhite, ui.BgRed), '-': ui.FgMagenta, 'X': ui.Stylings(ui.Inverse, ui.FgMagenta), 'v': ui.FgGreen, 'V': ui.Stylings(ui.Underlined, ui.FgGreen), '$': ui.FgMagenta, 'c': ui.FgCyan, // mnemonic "Comment" } // Fixture is a test fixture. type Fixture struct { App cli.App TTY TTYCtrl width int codeCh <-chan string errCh <-chan error } // Setup sets up a test fixture. It contains an App whose ReadCode method has // been started asynchronously. func Setup(fns ...func(*cli.AppSpec, TTYCtrl)) *Fixture { tty, ttyCtrl := NewFakeTTY() spec := cli.AppSpec{TTY: tty} for _, fn := range fns { fn(&spec, ttyCtrl) } app := cli.NewApp(spec) codeCh, errCh := StartReadCode(app.ReadCode) _, width := tty.Size() return &Fixture{app, ttyCtrl, width, codeCh, errCh} } // WithSpec takes a function that operates on *cli.AppSpec, and wraps it into a // form suitable for passing to Setup. func WithSpec(f func(*cli.AppSpec)) func(*cli.AppSpec, TTYCtrl) { return func(spec *cli.AppSpec, _ TTYCtrl) { f(spec) } } // WithTTY takes a function that operates on TTYCtrl, and wraps it to a form // suitable for passing to Setup. func WithTTY(f func(TTYCtrl)) func(*cli.AppSpec, TTYCtrl) { return func(_ *cli.AppSpec, tty TTYCtrl) { f(tty) } } // Wait waits for ReaCode to finish, and returns its return values. func (f *Fixture) Wait() (string, error) { return <-f.codeCh, <-f.errCh } // Stop stops ReadCode and waits for it to finish. If ReadCode has already been // stopped, it is a no-op. func (f *Fixture) Stop() { f.App.CommitEOF() f.Wait() } // MakeBuffer is a helper for building a buffer. It is equivalent to // term.NewBufferBuilder(width of terminal).MarkLines(args...).Buffer(). func (f *Fixture) MakeBuffer(args ...interface{}) *term.Buffer { return term.NewBufferBuilder(f.width).MarkLines(args...).Buffer() } // TestTTY is equivalent to f.TTY.TestBuffer(f.MakeBuffer(args...)). func (f *Fixture) TestTTY(t *testing.T, args ...interface{}) { t.Helper() f.TTY.TestBuffer(t, f.MakeBuffer(args...)) } // TestTTYNotes is equivalent to f.TTY.TestNotesBuffer(f.MakeBuffer(args...)). func (f *Fixture) TestTTYNotes(t *testing.T, args ...interface{}) { t.Helper() f.TTY.TestNotesBuffer(t, f.MakeBuffer(args...)) } // StartReadCode starts the readCode function asynchronously, and returns two // channels that deliver its return values. The two channels are closed after // return values are delivered, so that subsequent reads will return zero values // and not block. func StartReadCode(readCode func() (string, error)) (<-chan string, <-chan error) { codeCh := make(chan string, 1) errCh := make(chan error, 1) go func() { code, err := readCode() codeCh <- code errCh <- err close(codeCh) close(errCh) }() return codeCh, errCh } elvish-0.17.0/pkg/cli/clitest/apptest_test.go000066400000000000000000000016271415471104000211410ustar00rootroot00000000000000package clitest import ( "testing" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/cli/tk" ) func TestFixture(t *testing.T) { f := Setup( WithSpec(func(spec *cli.AppSpec) { spec.CodeAreaState.Buffer = tk.CodeBuffer{Content: "test", Dot: 4} }), WithTTY(func(tty TTYCtrl) { tty.SetSize(20, 30) // h = 20, w = 30 }), ) defer f.Stop() // Verify that the functions passed to Setup have taken effect. if f.App.ActiveWidget().(tk.CodeArea).CopyState().Buffer.Content != "test" { t.Errorf("WithSpec did not work") } buf := f.MakeBuffer() // Verify that the WithTTY function has taken effect. if buf.Width != 30 { t.Errorf("WithTTY did not work") } f.TestTTY(t, "test", term.DotHere) f.App.Notify("something") f.TestTTYNotes(t, "something") f.App.CommitCode() if code, err := f.Wait(); code != "test" || err != nil { t.Errorf("Wait returned %q, %v", code, err) } } elvish-0.17.0/pkg/cli/clitest/fake_tty.go000066400000000000000000000167251415471104000202350ustar00rootroot00000000000000package clitest import ( "os" "reflect" "sync" "testing" "time" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/testutil" ) const ( // Maximum number of buffer updates FakeTTY expect to see. fakeTTYBufferUpdates = 4096 // Maximum number of events FakeTTY produces. fakeTTYEvents = 4096 // Maximum number of signals FakeTTY produces. fakeTTYSignals = 4096 ) // An implementation of the cli.TTY interface that is useful in tests. type fakeTTY struct { setup func() (func(), error) // Channel that StartRead returns. Can be used to inject additional events. eventCh chan term.Event // Whether eventCh has been closed. eventChClosed bool // Mutex for synchronizing writing and closing eventCh. eventChMutex sync.Mutex // Channel for publishing updates of the main buffer and notes buffer. bufCh, notesBufCh chan *term.Buffer // Records history of the main buffer and notes buffer. bufs, notesBufs []*term.Buffer // Mutexes for guarding bufs and notesBufs. bufMutex sync.RWMutex // Channel that NotifySignals returns. Can be used to inject signals. sigCh chan os.Signal // Argument that SetRawInput got. raw int // Number of times the TTY screen has been cleared, incremented in // ClearScreen. cleared int sizeMutex sync.RWMutex // Predefined sizes. height, width int } // Initial size of fake TTY. const ( FakeTTYHeight = 20 FakeTTYWidth = 50 ) // NewFakeTTY creates a new FakeTTY and a handle for controlling it. The initial // size of the terminal is FakeTTYHeight and FakeTTYWidth. func NewFakeTTY() (cli.TTY, TTYCtrl) { tty := &fakeTTY{ eventCh: make(chan term.Event, fakeTTYEvents), sigCh: make(chan os.Signal, fakeTTYSignals), bufCh: make(chan *term.Buffer, fakeTTYBufferUpdates), notesBufCh: make(chan *term.Buffer, fakeTTYBufferUpdates), height: FakeTTYHeight, width: FakeTTYWidth, } return tty, TTYCtrl{tty} } // Delegates to the setup function specified using the SetSetup method of // TTYCtrl, or return a nop function and a nil error. func (t *fakeTTY) Setup() (func(), error) { if t.setup == nil { return func() {}, nil } return t.setup() } // Returns the size specified by using the SetSize method of TTYCtrl. func (t *fakeTTY) Size() (h, w int) { t.sizeMutex.RLock() defer t.sizeMutex.RUnlock() return t.height, t.width } // Returns next event from t.eventCh. func (t *fakeTTY) ReadEvent() (term.Event, error) { return <-t.eventCh, nil } // Records the argument. func (t *fakeTTY) SetRawInput(n int) { t.raw = n } // Closes eventCh. func (t *fakeTTY) CloseReader() { t.eventChMutex.Lock() defer t.eventChMutex.Unlock() close(t.eventCh) t.eventChClosed = true } // Returns the last recorded buffer. func (t *fakeTTY) Buffer() *term.Buffer { t.bufMutex.RLock() defer t.bufMutex.RUnlock() return t.bufs[len(t.bufs)-1] } // Records a nil buffer. func (t *fakeTTY) ResetBuffer() { t.bufMutex.Lock() defer t.bufMutex.Unlock() t.recordBuf(nil) } // UpdateBuffer records a new pair of buffers, i.e. sending them to their // respective channels and appending them to their respective slices. func (t *fakeTTY) UpdateBuffer(bufNotes, buf *term.Buffer, _ bool) error { t.bufMutex.Lock() defer t.bufMutex.Unlock() t.recordNotesBuf(bufNotes) t.recordBuf(buf) return nil } func (t *fakeTTY) HideCursor() { } func (t *fakeTTY) ShowCursor() { } func (t *fakeTTY) ClearScreen() { t.cleared++ } func (t *fakeTTY) NotifySignals() <-chan os.Signal { return t.sigCh } func (t *fakeTTY) StopSignals() { close(t.sigCh) } func (t *fakeTTY) recordBuf(buf *term.Buffer) { t.bufs = append(t.bufs, buf) t.bufCh <- buf } func (t *fakeTTY) recordNotesBuf(buf *term.Buffer) { t.notesBufs = append(t.notesBufs, buf) t.notesBufCh <- buf } // TTYCtrl is an interface for controlling a fake terminal. type TTYCtrl struct{ *fakeTTY } // GetTTYCtrl takes a TTY and returns a TTYCtrl and true, if the TTY is a fake // terminal. Otherwise it returns an invalid TTYCtrl and false. func GetTTYCtrl(t cli.TTY) (TTYCtrl, bool) { fake, ok := t.(*fakeTTY) return TTYCtrl{fake}, ok } // SetSetup sets the return values of the Setup method of the fake terminal. func (t TTYCtrl) SetSetup(restore func(), err error) { t.setup = func() (func(), error) { return restore, err } } // SetSize sets the size of the fake terminal. func (t TTYCtrl) SetSize(h, w int) { t.sizeMutex.Lock() defer t.sizeMutex.Unlock() t.height, t.width = h, w } // Inject injects events to the fake terminal. func (t TTYCtrl) Inject(events ...term.Event) { for _, event := range events { t.inject(event) } } func (t TTYCtrl) inject(event term.Event) { t.eventChMutex.Lock() defer t.eventChMutex.Unlock() if !t.eventChClosed { t.eventCh <- event } } // EventCh returns the underlying channel for delivering events. func (t TTYCtrl) EventCh() chan term.Event { return t.eventCh } // InjectSignal injects signals. func (t TTYCtrl) InjectSignal(sigs ...os.Signal) { for _, sig := range sigs { t.sigCh <- sig } } // RawInput returns the argument in the last call to the SetRawInput method of // the TTY. func (t TTYCtrl) RawInput() int { return t.raw } // ScreenCleared returns the number of times ClearScreen has been called on the // TTY. func (t TTYCtrl) ScreenCleared() int { return t.cleared } // TestBuffer verifies that a buffer will appear within the timeout of 4 // seconds, and fails the test if it doesn't func (t TTYCtrl) TestBuffer(tt *testing.T, b *term.Buffer) { tt.Helper() ok := testBuffer(tt, b, t.bufCh) if !ok { t.bufMutex.RLock() defer t.bufMutex.RUnlock() lastBuf := t.LastBuffer() tt.Logf("Last buffer: %s", lastBuf.TTYString()) if lastBuf == nil { bufs := t.BufferHistory() for i := len(bufs) - 1; i >= 0; i-- { if bufs[i] != nil { tt.Logf("Last non-nil buffer: %s", bufs[i].TTYString()) return } } } } } // TestNotesBuffer verifies that a notes buffer will appear within the timeout of 4 // seconds, and fails the test if it doesn't func (t TTYCtrl) TestNotesBuffer(tt *testing.T, b *term.Buffer) { tt.Helper() ok := testBuffer(tt, b, t.notesBufCh) if !ok { t.bufMutex.RLock() defer t.bufMutex.RUnlock() bufs := t.NotesBufferHistory() tt.Logf("There has been %d notes buffers. None-nil ones are:", len(bufs)) for i, buf := range bufs { if buf != nil { tt.Logf("#%d:\n%s", i, buf.TTYString()) } } } } // BufferHistory returns a slice of all buffers that have appeared. func (t TTYCtrl) BufferHistory() []*term.Buffer { t.bufMutex.RLock() defer t.bufMutex.RUnlock() return t.bufs } // LastBuffer returns the last buffer that has appeared. func (t TTYCtrl) LastBuffer() *term.Buffer { t.bufMutex.RLock() defer t.bufMutex.RUnlock() if len(t.bufs) == 0 { return nil } return t.bufs[len(t.bufs)-1] } // NotesBufferHistory returns a slice of all notes buffers that have appeared. func (t TTYCtrl) NotesBufferHistory() []*term.Buffer { t.bufMutex.RLock() defer t.bufMutex.RUnlock() return t.notesBufs } func (t TTYCtrl) LastNotesBuffer() *term.Buffer { t.bufMutex.RLock() defer t.bufMutex.RUnlock() if len(t.notesBufs) == 0 { return nil } return t.notesBufs[len(t.notesBufs)-1] } // Tests that an expected buffer will appear within the timeout. func testBuffer(t *testing.T, want *term.Buffer, ch <-chan *term.Buffer) bool { t.Helper() timeout := time.After(testutil.Scaled(100 * time.Millisecond)) for { select { case buf := <-ch: if reflect.DeepEqual(buf, want) { return true } case <-timeout: t.Errorf("Wanted buffer not shown") t.Logf("Want: %s", want.TTYString()) return false } } } elvish-0.17.0/pkg/cli/clitest/fake_tty_test.go000066400000000000000000000077761415471104000213020ustar00rootroot00000000000000package clitest import ( "os" "reflect" "testing" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/term" ) func TestFakeTTY_Setup(t *testing.T) { tty, ttyCtrl := NewFakeTTY() restoreCalled := 0 ttyCtrl.SetSetup(func() { restoreCalled++ }, nil) restore, err := tty.Setup() if err != nil { t.Errorf("Setup -> error %v, want nil", err) } restore() if restoreCalled != 1 { t.Errorf("Setup did not return restore") } } func TestFakeTTY_Size(t *testing.T) { tty, ttyCtrl := NewFakeTTY() ttyCtrl.SetSize(20, 30) h, w := tty.Size() if h != 20 || w != 30 { t.Errorf("Size -> (%v, %v), want (20, 30)", h, w) } } func TestFakeTTY_SetRawInput(t *testing.T) { tty, ttyCtrl := NewFakeTTY() tty.SetRawInput(2) if raw := ttyCtrl.RawInput(); raw != 2 { t.Errorf("RawInput() -> %v, want 2", raw) } } func TestFakeTTY_Events(t *testing.T) { tty, ttyCtrl := NewFakeTTY() ttyCtrl.Inject(term.K('a'), term.K('b')) if event, err := tty.ReadEvent(); event != term.K('a') || err != nil { t.Errorf("Got (%v, %v), want (%v, nil)", event, err, term.K('a')) } if event := <-ttyCtrl.EventCh(); event != term.K('b') { t.Errorf("Got event %v, want K('b')", event) } } func TestFakeTTY_Signals(t *testing.T) { tty, ttyCtrl := NewFakeTTY() signals := tty.NotifySignals() ttyCtrl.InjectSignal(os.Interrupt, os.Kill) signal := <-signals if signal != os.Interrupt { t.Errorf("Got signal %v, want %v", signal, os.Interrupt) } signal = <-signals if signal != os.Kill { t.Errorf("Got signal %v, want %v", signal, os.Kill) } } func TestFakeTTY_Buffer(t *testing.T) { bufNotes1 := term.NewBufferBuilder(10).Write("notes 1").Buffer() buf1 := term.NewBufferBuilder(10).Write("buf 1").Buffer() bufNotes2 := term.NewBufferBuilder(10).Write("notes 2").Buffer() buf2 := term.NewBufferBuilder(10).Write("buf 2").Buffer() bufNotes3 := term.NewBufferBuilder(10).Write("notes 3").Buffer() buf3 := term.NewBufferBuilder(10).Write("buf 3").Buffer() tty, ttyCtrl := NewFakeTTY() if ttyCtrl.LastNotesBuffer() != nil { t.Errorf("LastNotesBuffer -> %v, want nil", ttyCtrl.LastNotesBuffer()) } if ttyCtrl.LastBuffer() != nil { t.Errorf("LastBuffer -> %v, want nil", ttyCtrl.LastBuffer()) } tty.UpdateBuffer(bufNotes1, buf1, true) if ttyCtrl.LastNotesBuffer() != bufNotes1 { t.Errorf("LastBuffer -> %v, want %v", ttyCtrl.LastNotesBuffer(), bufNotes1) } if ttyCtrl.LastBuffer() != buf1 { t.Errorf("LastBuffer -> %v, want %v", ttyCtrl.LastBuffer(), buf1) } ttyCtrl.TestBuffer(t, buf1) ttyCtrl.TestNotesBuffer(t, bufNotes1) tty.UpdateBuffer(bufNotes2, buf2, true) if ttyCtrl.LastNotesBuffer() != bufNotes2 { t.Errorf("LastBuffer -> %v, want %v", ttyCtrl.LastNotesBuffer(), bufNotes2) } if ttyCtrl.LastBuffer() != buf2 { t.Errorf("LastBuffer -> %v, want %v", ttyCtrl.LastBuffer(), buf2) } ttyCtrl.TestBuffer(t, buf2) ttyCtrl.TestNotesBuffer(t, bufNotes2) // Test Test{,Notes}Buffer tty.UpdateBuffer(bufNotes3, buf3, true) ttyCtrl.TestBuffer(t, buf3) ttyCtrl.TestNotesBuffer(t, bufNotes3) // Cannot test the failure branch as that will fail the test wantBufs := []*term.Buffer{buf1, buf2, buf3} wantNotesBufs := []*term.Buffer{bufNotes1, bufNotes2, bufNotes3} if !reflect.DeepEqual(ttyCtrl.BufferHistory(), wantBufs) { t.Errorf("BufferHistory did not return {buf1, buf2}") } if !reflect.DeepEqual(ttyCtrl.NotesBufferHistory(), wantNotesBufs) { t.Errorf("NotesBufferHistory did not return {bufNotes1, bufNotes2}") } } func TestFakeTTY_ClearScreen(t *testing.T) { fakeTTY, ttyCtrl := NewFakeTTY() for i := 0; i < 5; i++ { if cleared := ttyCtrl.ScreenCleared(); cleared != i { t.Errorf("ScreenCleared -> %v, want %v", cleared, i) } fakeTTY.ClearScreen() } } func TestGetTTYCtrl_FakeTTY(t *testing.T) { fakeTTY, ttyCtrl := NewFakeTTY() if got, ok := GetTTYCtrl(fakeTTY); got != ttyCtrl || !ok { t.Errorf("-> %v, %v, want %v, %v", got, ok, ttyCtrl, true) } } func TestGetTTYCtrl_RealTTY(t *testing.T) { realTTY := cli.NewTTY(os.Stdin, os.Stderr) if _, ok := GetTTYCtrl(realTTY); ok { t.Errorf("-> _, true, want _, false") } } elvish-0.17.0/pkg/cli/histutil/000077500000000000000000000000001415471104000162635ustar00rootroot00000000000000elvish-0.17.0/pkg/cli/histutil/db.go000066400000000000000000000046311415471104000172030ustar00rootroot00000000000000package histutil import ( "strings" "src.elv.sh/pkg/store/storedefs" ) // DB is the interface of the storage database. type DB interface { NextCmdSeq() (int, error) AddCmd(cmd string) (int, error) CmdsWithSeq(from, upto int) ([]storedefs.Cmd, error) PrevCmd(upto int, prefix string) (storedefs.Cmd, error) NextCmd(from int, prefix string) (storedefs.Cmd, error) } // FaultyInMemoryDB is an in-memory DB implementation that can be injected // one-off errors. It is useful in tests. type FaultyInMemoryDB interface { DB // SetOneOffError causes the next operation on the database to return the // given error. SetOneOffError(err error) } // NewFaultyInMemoryDB creates a new FaultyInMemoryDB with the given commands. func NewFaultyInMemoryDB(cmds ...string) FaultyInMemoryDB { return &testDB{cmds: cmds} } // Implementation of FaultyInMemoryDB. type testDB struct { cmds []string oneOffError error } func (s *testDB) SetOneOffError(err error) { s.oneOffError = err } func (s *testDB) error() error { err := s.oneOffError s.oneOffError = nil return err } func (s *testDB) NextCmdSeq() (int, error) { return len(s.cmds), s.error() } func (s *testDB) AddCmd(cmd string) (int, error) { if s.oneOffError != nil { return -1, s.error() } s.cmds = append(s.cmds, cmd) return len(s.cmds) - 1, nil } func (s *testDB) CmdsWithSeq(from, upto int) ([]storedefs.Cmd, error) { if err := s.error(); err != nil { return nil, err } if from < 0 { from = 0 } if upto < 0 || upto > len(s.cmds) { upto = len(s.cmds) } var cmds []storedefs.Cmd for i := from; i < upto; i++ { cmds = append(cmds, storedefs.Cmd{Text: s.cmds[i], Seq: i}) } return cmds, nil } func (s *testDB) PrevCmd(upto int, prefix string) (storedefs.Cmd, error) { if s.oneOffError != nil { return storedefs.Cmd{}, s.error() } if upto < 0 || upto > len(s.cmds) { upto = len(s.cmds) } for i := upto - 1; i >= 0; i-- { if strings.HasPrefix(s.cmds[i], prefix) { return storedefs.Cmd{Text: s.cmds[i], Seq: i}, nil } } return storedefs.Cmd{}, storedefs.ErrNoMatchingCmd } func (s *testDB) NextCmd(from int, prefix string) (storedefs.Cmd, error) { if s.oneOffError != nil { return storedefs.Cmd{}, s.error() } if from < 0 { from = 0 } for i := from; i < len(s.cmds); i++ { if strings.HasPrefix(s.cmds[i], prefix) { return storedefs.Cmd{Text: s.cmds[i], Seq: i}, nil } } return storedefs.Cmd{}, storedefs.ErrNoMatchingCmd } elvish-0.17.0/pkg/cli/histutil/db_store.go000066400000000000000000000030471415471104000204170ustar00rootroot00000000000000package histutil import ( "src.elv.sh/pkg/store/storedefs" ) // NewDBStore returns a Store backed by a database with the view of all // commands frozen at creation. func NewDBStore(db DB) (Store, error) { upper, err := db.NextCmdSeq() if err != nil { return nil, err } return dbStore{db, upper}, nil } type dbStore struct { db DB upper int } func (s dbStore) AllCmds() ([]storedefs.Cmd, error) { return s.db.CmdsWithSeq(0, s.upper) } func (s dbStore) AddCmd(cmd storedefs.Cmd) (int, error) { return s.db.AddCmd(cmd.Text) } func (s dbStore) Cursor(prefix string) Cursor { return &dbStoreCursor{ s.db, prefix, s.upper, storedefs.Cmd{Seq: s.upper}, ErrEndOfHistory} } type dbStoreCursor struct { db DB prefix string upper int cmd storedefs.Cmd err error } func (c *dbStoreCursor) Prev() { if c.cmd.Seq < 0 { return } cmd, err := c.db.PrevCmd(c.cmd.Seq, c.prefix) c.set(cmd, err, -1) } func (c *dbStoreCursor) Next() { if c.cmd.Seq >= c.upper { return } cmd, err := c.db.NextCmd(c.cmd.Seq+1, c.prefix) if cmd.Seq < c.upper { c.set(cmd, err, c.upper) } if cmd.Seq >= c.upper { c.cmd = storedefs.Cmd{Seq: c.upper} c.err = ErrEndOfHistory } } func (c *dbStoreCursor) set(cmd storedefs.Cmd, err error, endSeq int) { if err == nil { c.cmd = cmd c.err = nil } else if err.Error() == storedefs.ErrNoMatchingCmd.Error() { c.cmd = storedefs.Cmd{Seq: endSeq} c.err = ErrEndOfHistory } else { // Don't change c.cmd c.err = err } } func (c *dbStoreCursor) Get() (storedefs.Cmd, error) { return c.cmd, c.err } elvish-0.17.0/pkg/cli/histutil/db_store_test.go000066400000000000000000000017531415471104000214600ustar00rootroot00000000000000package histutil import ( "testing" "src.elv.sh/pkg/store/storedefs" ) func TestDBStore_Cursor(t *testing.T) { db := NewFaultyInMemoryDB("+ 1", "- 2", "+ 3") s, err := NewDBStore(db) if err != nil { panic(err) } testCursorIteration(t, s.Cursor("+"), []storedefs.Cmd{ {Text: "+ 1", Seq: 0}, {Text: "+ 3", Seq: 2}, }) // Test error conditions. c := s.Cursor("+") expect := func(wantCmd storedefs.Cmd, wantErr error) { t.Helper() cmd, err := c.Get() if cmd != wantCmd { t.Errorf("Get -> %v, want %v", cmd, wantCmd) } if err != wantErr { t.Errorf("Get -> error %v, want %v", err, wantErr) } } db.SetOneOffError(errMock) c.Prev() expect(storedefs.Cmd{Seq: 3}, errMock) c.Prev() expect(storedefs.Cmd{Text: "+ 3", Seq: 2}, nil) db.SetOneOffError(errMock) c.Prev() expect(storedefs.Cmd{Text: "+ 3", Seq: 2}, errMock) db.SetOneOffError(errMock) c.Next() expect(storedefs.Cmd{Text: "+ 3", Seq: 2}, errMock) } // Remaining methods tested with HybridStore. elvish-0.17.0/pkg/cli/histutil/dedup_cursor.go000066400000000000000000000016771415471104000213230ustar00rootroot00000000000000package histutil import "src.elv.sh/pkg/store/storedefs" // NewDedupCursor returns a cursor that skips over all duplicate entries. func NewDedupCursor(c Cursor) Cursor { return &dedupCursor{c, 0, nil, make(map[string]bool)} } type dedupCursor struct { c Cursor current int stack []storedefs.Cmd occ map[string]bool } func (c *dedupCursor) Prev() { if c.current < len(c.stack)-1 { c.current++ return } for { c.c.Prev() cmd, err := c.c.Get() if err != nil { c.current = len(c.stack) break } if !c.occ[cmd.Text] { c.current = len(c.stack) c.stack = append(c.stack, cmd) c.occ[cmd.Text] = true break } } } func (c *dedupCursor) Next() { if c.current >= 0 { c.current-- } } func (c *dedupCursor) Get() (storedefs.Cmd, error) { switch { case c.current < 0: return storedefs.Cmd{}, ErrEndOfHistory case c.current < len(c.stack): return c.stack[c.current], nil default: return c.c.Get() } } elvish-0.17.0/pkg/cli/histutil/dedup_cursor_test.go000066400000000000000000000012401415471104000223440ustar00rootroot00000000000000package histutil import ( "testing" "src.elv.sh/pkg/store/storedefs" ) func TestDedupCursor(t *testing.T) { s := NewMemStore("0", "1", "2") c := NewDedupCursor(s.Cursor("")) wantCmds := []storedefs.Cmd{ {Text: "0", Seq: 0}, {Text: "1", Seq: 1}, {Text: "2", Seq: 2}} testCursorIteration(t, c, wantCmds) // Go back again, this time with a full stack testCursorIteration(t, c, wantCmds) c = NewDedupCursor(s.Cursor("")) // Should be a no-op c.Next() testCursorIteration(t, c, wantCmds) c = NewDedupCursor(s.Cursor("")) c.Prev() c.Next() _, err := c.Get() if err != ErrEndOfHistory { t.Errorf("Get -> error %v, want ErrEndOfHistory", err) } } elvish-0.17.0/pkg/cli/histutil/doc.go000066400000000000000000000001321415471104000173530ustar00rootroot00000000000000// Package histutil provides utilities for working with command history. package histutil elvish-0.17.0/pkg/cli/histutil/hybrid_store.go000066400000000000000000000032131415471104000213060ustar00rootroot00000000000000package histutil import "src.elv.sh/pkg/store/storedefs" // NewHybridStore returns a store that provides a view of all the commands that // exists in the database, plus a in-memory session history. func NewHybridStore(db DB) (Store, error) { if db == nil { return NewMemStore(), nil } dbStore, err := NewDBStore(db) if err != nil { return NewMemStore(), err } return hybridStore{dbStore, NewMemStore()}, nil } type hybridStore struct { shared, session Store } func (s hybridStore) AddCmd(cmd storedefs.Cmd) (int, error) { seq, err := s.shared.AddCmd(cmd) s.session.AddCmd(storedefs.Cmd{Text: cmd.Text, Seq: seq}) return seq, err } func (s hybridStore) AllCmds() ([]storedefs.Cmd, error) { shared, err := s.shared.AllCmds() session, err2 := s.session.AllCmds() if err == nil { err = err2 } if len(shared) == 0 { return session, err } return append(shared, session...), err } func (s hybridStore) Cursor(prefix string) Cursor { return &hybridStoreCursor{ s.shared.Cursor(prefix), s.session.Cursor(prefix), false} } type hybridStoreCursor struct { shared Cursor session Cursor useShared bool } func (c *hybridStoreCursor) Prev() { if c.useShared { c.shared.Prev() return } c.session.Prev() if _, err := c.session.Get(); err == ErrEndOfHistory { c.useShared = true c.shared.Prev() } } func (c *hybridStoreCursor) Next() { if !c.useShared { c.session.Next() return } c.shared.Next() if _, err := c.shared.Get(); err == ErrEndOfHistory { c.useShared = false c.session.Next() } } func (c *hybridStoreCursor) Get() (storedefs.Cmd, error) { if c.useShared { return c.shared.Get() } return c.session.Get() } elvish-0.17.0/pkg/cli/histutil/hybrid_store_test.go000066400000000000000000000123431415471104000223510ustar00rootroot00000000000000package histutil import ( "errors" "reflect" "testing" "src.elv.sh/pkg/store/storedefs" ) var errMock = errors.New("mock error") func TestNewHybridStore_ReturnsMemStoreIfDBIsNil(t *testing.T) { store, err := NewHybridStore(nil) if _, ok := store.(*memStore); !ok { t.Errorf("NewHybridStore -> %v, want memStore", store) } if err != nil { t.Errorf("NewHybridStore -> error %v, want nil", err) } } func TestNewHybridStore_ReturnsMemStoreOnDBError(t *testing.T) { db := NewFaultyInMemoryDB() db.SetOneOffError(errMock) store, err := NewHybridStore(db) if _, ok := store.(*memStore); !ok { t.Errorf("NewHybridStore -> %v, want memStore", store) } if err != errMock { t.Errorf("NewHybridStore -> error %v, want %v", err, errMock) } } func TestFusuer_AddCmd_AddsBothToDBAndSession(t *testing.T) { db := NewFaultyInMemoryDB("shared 1") f := mustNewHybridStore(db) f.AddCmd(storedefs.Cmd{Text: "session 1"}) wantDBCmds := []storedefs.Cmd{ {Text: "shared 1", Seq: 0}, {Text: "session 1", Seq: 1}} if dbCmds, _ := db.CmdsWithSeq(-1, -1); !reflect.DeepEqual(dbCmds, wantDBCmds) { t.Errorf("DB commands = %v, want %v", dbCmds, wantDBCmds) } allCmds, err := f.AllCmds() if err != nil { panic(err) } wantAllCmds := []storedefs.Cmd{ {Text: "shared 1", Seq: 0}, {Text: "session 1", Seq: 1}} if !reflect.DeepEqual(allCmds, wantAllCmds) { t.Errorf("AllCmd -> %v, want %v", allCmds, wantAllCmds) } } func TestHybridStore_AddCmd_AddsToSessionEvenIfDBErrors(t *testing.T) { db := NewFaultyInMemoryDB() f := mustNewHybridStore(db) db.SetOneOffError(errMock) _, err := f.AddCmd(storedefs.Cmd{Text: "haha"}) if err != errMock { t.Errorf("AddCmd -> error %v, want %v", err, errMock) } allCmds, err := f.AllCmds() if err != nil { panic(err) } wantAllCmds := []storedefs.Cmd{{Text: "haha", Seq: 1}} if !reflect.DeepEqual(allCmds, wantAllCmds) { t.Errorf("AllCmd -> %v, want %v", allCmds, wantAllCmds) } } func TestHybridStore_AllCmds_IncludesFrozenSharedAndNewlyAdded(t *testing.T) { db := NewFaultyInMemoryDB("shared 1") f := mustNewHybridStore(db) // Simulate adding commands from both the current session and other sessions. f.AddCmd(storedefs.Cmd{Text: "session 1"}) db.AddCmd("other session 1") db.AddCmd("other session 2") f.AddCmd(storedefs.Cmd{Text: "session 2"}) db.AddCmd("other session 3") // AllCmds should return all commands from the storage when the HybridStore // was created, plus session commands. The session commands should have // sequence numbers consistent with the DB. allCmds, err := f.AllCmds() if err != nil { t.Errorf("AllCmds -> error %v, want nil", err) } wantAllCmds := []storedefs.Cmd{ {Text: "shared 1", Seq: 0}, {Text: "session 1", Seq: 1}, {Text: "session 2", Seq: 4}} if !reflect.DeepEqual(allCmds, wantAllCmds) { t.Errorf("AllCmds -> %v, want %v", allCmds, wantAllCmds) } } func TestHybridStore_AllCmds_ReturnsSessionIfDBErrors(t *testing.T) { db := NewFaultyInMemoryDB("shared 1") f := mustNewHybridStore(db) f.AddCmd(storedefs.Cmd{Text: "session 1"}) db.SetOneOffError(errMock) allCmds, err := f.AllCmds() if err != errMock { t.Errorf("AllCmds -> error %v, want %v", err, errMock) } wantAllCmds := []storedefs.Cmd{{Text: "session 1", Seq: 1}} if !reflect.DeepEqual(allCmds, wantAllCmds) { t.Errorf("AllCmd -> %v, want %v", allCmds, wantAllCmds) } } func TestHybridStore_Cursor_OnlySession(t *testing.T) { db := NewFaultyInMemoryDB() f := mustNewHybridStore(db) db.AddCmd("+ other session") f.AddCmd(storedefs.Cmd{Text: "+ session 1"}) f.AddCmd(storedefs.Cmd{Text: "- no match"}) testCursorIteration(t, f.Cursor("+"), []storedefs.Cmd{{Text: "+ session 1", Seq: 1}}) } func TestHybridStore_Cursor_OnlyShared(t *testing.T) { db := NewFaultyInMemoryDB("- no match", "+ shared 1") f := mustNewHybridStore(db) db.AddCmd("+ other session") f.AddCmd(storedefs.Cmd{Text: "- no match"}) testCursorIteration(t, f.Cursor("+"), []storedefs.Cmd{{Text: "+ shared 1", Seq: 1}}) } func TestHybridStore_Cursor_SharedAndSession(t *testing.T) { db := NewFaultyInMemoryDB("- no match", "+ shared 1") f := mustNewHybridStore(db) db.AddCmd("+ other session") db.AddCmd("- no match") f.AddCmd(storedefs.Cmd{Text: "+ session 1"}) f.AddCmd(storedefs.Cmd{Text: "- no match"}) testCursorIteration(t, f.Cursor("+"), []storedefs.Cmd{ {Text: "+ shared 1", Seq: 1}, {Text: "+ session 1", Seq: 4}}) } func testCursorIteration(t *testing.T, cursor Cursor, wantCmds []storedefs.Cmd) { expectEndOfHistory := func() { t.Helper() if _, err := cursor.Get(); err != ErrEndOfHistory { t.Errorf("Get -> error %v, want ErrEndOfHistory", err) } } expectCmd := func(i int) { t.Helper() wantCmd := wantCmds[i] cmd, err := cursor.Get() if cmd != wantCmd { t.Errorf("Get -> %v, want %v", cmd, wantCmd) } if err != nil { t.Errorf("Get -> error %v, want nil", err) } } expectEndOfHistory() for i := len(wantCmds) - 1; i >= 0; i-- { cursor.Prev() expectCmd(i) } cursor.Prev() expectEndOfHistory() cursor.Prev() expectEndOfHistory() for i := range wantCmds { cursor.Next() expectCmd(i) } cursor.Next() expectEndOfHistory() cursor.Next() expectEndOfHistory() } func mustNewHybridStore(db DB) Store { f, err := NewHybridStore(db) if err != nil { panic(err) } return f } elvish-0.17.0/pkg/cli/histutil/mem_store.go000066400000000000000000000025401415471104000206050ustar00rootroot00000000000000package histutil import ( "strings" "src.elv.sh/pkg/store/storedefs" ) // NewMemStore returns a Store that stores command history in memory. func NewMemStore(texts ...string) Store { cmds := make([]storedefs.Cmd, len(texts)) for i, text := range texts { cmds[i] = storedefs.Cmd{Text: text, Seq: i} } return &memStore{cmds} } type memStore struct{ cmds []storedefs.Cmd } func (s *memStore) AllCmds() ([]storedefs.Cmd, error) { return s.cmds, nil } func (s *memStore) AddCmd(cmd storedefs.Cmd) (int, error) { if cmd.Seq < 0 { cmd.Seq = len(s.cmds) + 1 } s.cmds = append(s.cmds, cmd) return cmd.Seq, nil } func (s *memStore) Cursor(prefix string) Cursor { return &memStoreCursor{s.cmds, prefix, len(s.cmds)} } type memStoreCursor struct { cmds []storedefs.Cmd prefix string index int } func (c *memStoreCursor) Prev() { if c.index < 0 { return } for c.index--; c.index >= 0; c.index-- { if strings.HasPrefix(c.cmds[c.index].Text, c.prefix) { return } } } func (c *memStoreCursor) Next() { if c.index >= len(c.cmds) { return } for c.index++; c.index < len(c.cmds); c.index++ { if strings.HasPrefix(c.cmds[c.index].Text, c.prefix) { return } } } func (c *memStoreCursor) Get() (storedefs.Cmd, error) { if c.index < 0 || c.index >= len(c.cmds) { return storedefs.Cmd{}, ErrEndOfHistory } return c.cmds[c.index], nil } elvish-0.17.0/pkg/cli/histutil/mem_store_test.go000066400000000000000000000005001415471104000216360ustar00rootroot00000000000000package histutil import ( "testing" "src.elv.sh/pkg/store/storedefs" ) func TestMemStore_Cursor(t *testing.T) { s := NewMemStore("+ 0", "- 1", "+ 2") testCursorIteration(t, s.Cursor("+"), []storedefs.Cmd{ {Text: "+ 0", Seq: 0}, {Text: "+ 2", Seq: 2}, }) } // Remaining methods tested along with HybridStore elvish-0.17.0/pkg/cli/histutil/store.go000066400000000000000000000022761415471104000177550ustar00rootroot00000000000000package histutil import ( "errors" "src.elv.sh/pkg/store/storedefs" ) // Store is an abstract interface for history store. type Store interface { // AddCmd adds a new command history entry and returns its sequence number. // Depending on the implementation, the Store might respect cmd.Seq and // return it as is, or allocate another sequence number. AddCmd(cmd storedefs.Cmd) (int, error) // AllCmds returns all commands kept in the store. AllCmds() ([]storedefs.Cmd, error) // Cursor returns a cursor that iterating through commands with the given // prefix. The cursor is initially placed just after the last command in the // store. Cursor(prefix string) Cursor } // Cursor is used to navigate a Store. type Cursor interface { // Prev moves the cursor to the previous command. Prev() // Next moves the cursor to the next command. Next() // Get returns the command the cursor is currently at, or any error if the // cursor is in an invalid state. If the cursor is "over the edge", the // error is ErrEndOfHistory. Get() (storedefs.Cmd, error) } // ErrEndOfHistory is returned by Cursor.Get if the cursor is currently over the // edge. var ErrEndOfHistory = errors.New("end of history") elvish-0.17.0/pkg/cli/loop.go000066400000000000000000000070701415471104000157220ustar00rootroot00000000000000package cli import "sync" // Buffer size of the input channel. The value is chosen for no particular // reason. const inputChSize = 128 // A generic main loop manager. type loop struct { inputCh chan event handleCb handleCb redrawCb redrawCb redrawCh chan struct{} redrawFull bool redrawMutex *sync.Mutex returnCh chan loopReturn } type loopReturn struct { buffer string err error } // A placeholder type for events. type event interface{} // Callback for redrawing the editor UI to the terminal. type redrawCb func(flag redrawFlag) func dummyRedrawCb(redrawFlag) {} // Flag to redrawCb. type redrawFlag uint // Bit flags for redrawFlag. const ( // fullRedraw signals a "full redraw". This is set on the first RedrawCb // call or when Redraw has been called with full = true. fullRedraw redrawFlag = 1 << iota // finalRedraw signals that this is the final redraw in the event loop. finalRedraw ) // Callback for handling a terminal event. type handleCb func(event) func dummyHandleCb(event) {} // newLoop creates a new Loop instance. func newLoop() *loop { return &loop{ inputCh: make(chan event, inputChSize), handleCb: dummyHandleCb, redrawCb: dummyRedrawCb, redrawCh: make(chan struct{}, 1), redrawFull: false, redrawMutex: new(sync.Mutex), returnCh: make(chan loopReturn, 1), } } // HandleCb sets the handle callback. It must be called before any Read call. func (lp *loop) HandleCb(cb handleCb) { lp.handleCb = cb } // RedrawCb sets the redraw callback. It must be called before any Read call. func (lp *loop) RedrawCb(cb redrawCb) { lp.redrawCb = cb } // Redraw requests a redraw. If full is true, a full redraw is requested. It // never blocks. func (lp *loop) Redraw(full bool) { lp.redrawMutex.Lock() defer lp.redrawMutex.Unlock() if full { lp.redrawFull = true } select { case lp.redrawCh <- struct{}{}: default: } } // Input provides an input event. It may block if the internal event buffer is // full. func (lp *loop) Input(ev event) { lp.inputCh <- ev } // Return requests the main loop to return. It never blocks. If Return has been // called before during the current loop iteration, it has no effect. func (lp *loop) Return(buffer string, err error) { select { case lp.returnCh <- loopReturn{buffer, err}: default: } } // HasReturned returns whether Return has been called during the current loop // iteration. func (lp *loop) HasReturned() bool { return len(lp.returnCh) == 1 } // Run runs the event loop, until the Return method is called. It is generic // and delegates all concrete work to callbacks. It is fully serial: it does // not spawn any goroutines and never calls two callbacks in parallel, so the // callbacks may manipulate shared states without synchronization. func (lp *loop) Run() (buffer string, err error) { for { var flag redrawFlag if lp.extractRedrawFull() { flag |= fullRedraw } lp.redrawCb(flag) select { case event := <-lp.inputCh: // Consume all events in the channel to minimize redraws. consumeAllEvents: for { lp.handleCb(event) select { case ret := <-lp.returnCh: lp.redrawCb(finalRedraw) return ret.buffer, ret.err default: } select { case event = <-lp.inputCh: // Continue the loop of consuming all events. default: break consumeAllEvents } } case ret := <-lp.returnCh: lp.redrawCb(finalRedraw) return ret.buffer, ret.err case <-lp.redrawCh: } } } func (lp *loop) extractRedrawFull() bool { lp.redrawMutex.Lock() defer lp.redrawMutex.Unlock() full := lp.redrawFull lp.redrawFull = false return full } elvish-0.17.0/pkg/cli/loop_test.go000066400000000000000000000077431415471104000167700ustar00rootroot00000000000000package cli import ( "fmt" "io" "reflect" "testing" ) func TestRead_PassesInputEventsToHandler(t *testing.T) { var handlerGotEvents []event lp := newLoop() lp.HandleCb(func(e event) { handlerGotEvents = append(handlerGotEvents, e) if e == "^D" { lp.Return("", nil) } }) inputPassedEvents := []event{"foo", "bar", "lorem", "ipsum", "^D"} supplyInputs(lp, inputPassedEvents...) _, _ = lp.Run() if !reflect.DeepEqual(handlerGotEvents, inputPassedEvents) { t.Errorf("Handler got events %v, expect same as events passed to input (%v)", handlerGotEvents, inputPassedEvents) } } func TestLoop_RunReturnsAfterReturnCalled(t *testing.T) { lp := newLoop() lp.HandleCb(func(event) { lp.Return("buffer", io.EOF) }) supplyInputs(lp, "x") buf, err := lp.Run() if buf != "buffer" || err != io.EOF { fmt.Printf("Run -> (%v, %v), want (%v, %v)", buf, err, "buffer", io.EOF) } } func TestRead_CallsDrawWhenRedrawRequestedBeforeRead(t *testing.T) { testReadCallsDrawWhenRedrawRequestedBeforeRead(t, true, fullRedraw) testReadCallsDrawWhenRedrawRequestedBeforeRead(t, false, 0) } func testReadCallsDrawWhenRedrawRequestedBeforeRead(t *testing.T, full bool, wantRedrawFlag redrawFlag) { t.Helper() var gotRedrawFlag redrawFlag drawSeq := 0 doneCh := make(chan struct{}) drawer := func(full redrawFlag) { if drawSeq == 0 { gotRedrawFlag = full close(doneCh) } drawSeq++ } lp := newLoop() lp.HandleCb(quitOn(lp, "^D", "", nil)) go func() { <-doneCh lp.Input("^D") }() lp.RedrawCb(drawer) lp.Redraw(full) _, _ = lp.Run() if gotRedrawFlag != wantRedrawFlag { t.Errorf("Drawer got flag %v, want %v", gotRedrawFlag, wantRedrawFlag) } } func TestRead_callsDrawWhenRedrawRequestedAfterFirstDraw(t *testing.T) { testReadCallsDrawWhenRedrawRequestedAfterFirstDraw(t, true, fullRedraw) testReadCallsDrawWhenRedrawRequestedAfterFirstDraw(t, false, 0) } func testReadCallsDrawWhenRedrawRequestedAfterFirstDraw(t *testing.T, full bool, wantRedrawFlag redrawFlag) { t.Helper() var gotRedrawFlag redrawFlag drawSeq := 0 firstDrawCalledCh := make(chan struct{}) doneCh := make(chan struct{}) drawer := func(flag redrawFlag) { if drawSeq == 0 { close(firstDrawCalledCh) } else if drawSeq == 1 { gotRedrawFlag = flag close(doneCh) } drawSeq++ } lp := newLoop() lp.HandleCb(quitOn(lp, "^D", "", nil)) go func() { <-doneCh lp.Input("^D") }() lp.RedrawCb(drawer) go func() { <-firstDrawCalledCh lp.Redraw(full) }() _, _ = lp.Run() if gotRedrawFlag != wantRedrawFlag { t.Errorf("Drawer got flag %v, want %v", gotRedrawFlag, wantRedrawFlag) } } // Helpers. func supplyInputs(lp *loop, events ...event) { for _, event := range events { lp.Input(event) } } // Returns a HandleCb that quits on a trigger event. func quitOn(lp *loop, retTrigger event, ret string, err error) handleCb { return func(e event) { if e == retTrigger { lp.Return(ret, err) } } } func TestLoop_FullLifecycle(t *testing.T) { // A test for the entire lifecycle of a loop. var initialBuffer, finalBuffer string buffer := "" firstDrawerCall := true drawer := func(flag redrawFlag) { // Because the consumption of events is batched, calls to the drawer is // nondeterministic except for the first and final calls. switch { case firstDrawerCall: initialBuffer = buffer firstDrawerCall = false case flag&finalRedraw != 0: finalBuffer = buffer } } lp := newLoop() lp.HandleCb(func(e event) { if e == '\n' { lp.Return(buffer, nil) return } buffer += string(e.(rune)) }) go func() { for _, event := range "echo\n" { lp.Input(event) } }() lp.RedrawCb(drawer) returnedBuffer, err := lp.Run() if initialBuffer != "" { t.Errorf("got initial buffer %q, want %q", initialBuffer, "") } if finalBuffer != "echo" { t.Errorf("got final buffer %q, want %q", finalBuffer, "echo") } if returnedBuffer != "echo" { t.Errorf("got returned buffer %q, want %q", returnedBuffer, "echo") } if err != nil { t.Errorf("got error %v, want nil", err) } } elvish-0.17.0/pkg/cli/lscolors/000077500000000000000000000000001415471104000162565ustar00rootroot00000000000000elvish-0.17.0/pkg/cli/lscolors/feature.go000066400000000000000000000047721415471104000202520ustar00rootroot00000000000000package lscolors import ( "os" ) type feature int const ( featureInvalid feature = iota featureOrphanedSymlink featureSymlink featureMultiHardLink featureNamedPipe featureSocket featureDoor featureBlockDevice featureCharDevice featureWorldWritableStickyDirectory featureWorldWritableDirectory featureStickyDirectory featureDirectory featureCapability featureSetuid featureSetgid featureExecutable featureRegular ) // Weirdly, permission masks for group and other are missing on platforms other // than linux, darwin and netbsd. So we replicate some of them here. const ( worldWritable = 02 // Writable by other executable = 0111 // Executable ) func determineFeature(fname string, mh bool) (feature, error) { stat, err := os.Lstat(fname) if err != nil { return featureInvalid, err } m := stat.Mode() // Symlink and OrphanedSymlink has highest precedence if is(m, os.ModeSymlink) { _, err := os.Stat(fname) if err != nil { return featureOrphanedSymlink, nil } return featureSymlink, nil } // featureMultiHardLink if mh && isMultiHardlink(stat) { return featureMultiHardLink, nil } // type bits features switch { case is(m, os.ModeNamedPipe): return featureNamedPipe, nil case is(m, os.ModeSocket): // Never on Windows return featureSocket, nil case isDoor(stat): return featureDoor, nil case is(m, os.ModeCharDevice): return featureCharDevice, nil case is(m, os.ModeDevice): // There is no dedicated os.Mode* flag for block device. On all // supported Unix platforms, when os.ModeDevice is set but // os.ModeCharDevice is not, the file is a block device (i.e. // syscall.S_IFBLK is set). On Windows, this branch is unreachable. // // On Plan9, this in inaccurate. return featureBlockDevice, nil case is(m, os.ModeDir): // Perm bits features for directory perm := m.Perm() switch { case is(m, os.ModeSticky) && is(perm, worldWritable): return featureWorldWritableStickyDirectory, nil case is(perm, worldWritable): return featureWorldWritableDirectory, nil case is(m, os.ModeSticky): return featureStickyDirectory, nil default: return featureDirectory, nil } } // TODO(xiaq): Support featureCapacity // Perm bits features for regular files switch { case is(m, os.ModeSetuid): return featureSetuid, nil case is(m, os.ModeSetgid): return featureSetgid, nil case m&executable != 0: return featureExecutable, nil } // Check extension return featureRegular, nil } func is(m, p os.FileMode) bool { return m&p == p } elvish-0.17.0/pkg/cli/lscolors/feature_nonunix_test.go000066400000000000000000000003771415471104000230640ustar00rootroot00000000000000//go:build windows || plan9 || js // +build windows plan9 js package lscolors import ( "errors" ) var errNotSupportedOnNonUNIX = errors.New("not supported on non-UNIX OS") func createNamedPipe(fname string) error { return errNotSupportedOnNonUNIX } elvish-0.17.0/pkg/cli/lscolors/feature_test.go000066400000000000000000000077641415471104000213150ustar00rootroot00000000000000package lscolors import ( "fmt" "net" "os" "runtime" "testing" "src.elv.sh/pkg/testutil" ) type opt struct { setupErr error mh bool wantErr bool } func TestDetermineFeature(t *testing.T) { testutil.InTempDir(t) testutil.Umask(t, 0) test := func(name, fname string, wantFeature feature, o opt) { t.Helper() t.Run(name, func(t *testing.T) { t.Helper() if o.setupErr != nil { t.Skip("skipped due to setup error:", o.setupErr) } feature, err := determineFeature(fname, o.mh) wantErr := "nil" if o.wantErr { wantErr = "non-nil" } if (err != nil) != o.wantErr { t.Errorf("determineFeature(%q, %v) returns error %v, want %v", fname, o.mh, err, wantErr) } if feature != wantFeature { t.Errorf("determineFeature(%q, %v) returns feature %v, want %v", fname, o.mh, feature, wantFeature) } }) } err := create("a") test("regular file", "a", featureRegular, opt{setupErr: err}) test("regular file mh=true", "a", featureRegular, opt{setupErr: err, mh: true}) err = os.Symlink("a", "l") test("symlink", "l", featureSymlink, opt{setupErr: err}) err = os.Symlink("aaaa", "lbad") test("broken symlink", "lbad", featureOrphanedSymlink, opt{setupErr: err}) if runtime.GOOS != "windows" { err := os.Link("a", "a2") test("multi hard link", "a", featureMultiHardLink, opt{mh: true, setupErr: err}) test("multi hard link with mh=false", "a", featureRegular, opt{setupErr: err}) } err = createNamedPipe("fifo") test("named pipe", "fifo", featureNamedPipe, opt{setupErr: err}) if runtime.GOOS != "windows" { l, err := net.Listen("unix", "sock") if err == nil { defer l.Close() } test("socket", "sock", featureSocket, opt{setupErr: err}) } // TODO: Test featureDoor on Solaris chr, err := findDevice(os.ModeDevice | os.ModeCharDevice) test("char device", chr, featureCharDevice, opt{setupErr: err}) blk, err := findDevice(os.ModeDevice) test("block device", blk, featureBlockDevice, opt{setupErr: err}) err = mkdirMode("d", 0700) test("normal dir", "d", featureDirectory, opt{setupErr: err}) err = mkdirMode("d-wws", 0777|os.ModeSticky) test("world-writable sticky dir", "d-wws", featureWorldWritableStickyDirectory, opt{setupErr: err}) err = mkdirMode("d-ww", 0777) test("world-writable dir", "d-ww", featureWorldWritableDirectory, opt{setupErr: err}) err = mkdirMode("d-s", 0700|os.ModeSticky) test("sticky dir", "d-s", featureStickyDirectory, opt{setupErr: err}) err = createMode("xu", 0100) test("executable by user", "xu", featureExecutable, opt{setupErr: err}) err = createMode("xg", 0010) test("executable by group", "xg", featureExecutable, opt{setupErr: err}) err = createMode("xo", 0001) test("executable by other", "xo", featureExecutable, opt{setupErr: err}) err = createMode("su", 0600|os.ModeSetuid) test("setuid", "su", featureSetuid, opt{setupErr: err}) err = createMode("sg", 0600|os.ModeSetgid) test("setgid", "sg", featureSetgid, opt{setupErr: err}) test("nonexistent file", "nonexistent", featureInvalid, opt{wantErr: true}) } func create(fname string) error { f, err := os.Create(fname) if err == nil { f.Close() } return err } func createMode(fname string, mode os.FileMode) error { f, err := os.OpenFile(fname, os.O_CREATE, mode) if err != nil { return err } f.Close() return checkMode(fname, mode) } func findDevice(typ os.FileMode) (string, error) { entries, err := os.ReadDir("/dev") if err != nil { return "", err } for _, entry := range entries { if entry.Type() == typ { return "/dev/" + entry.Name(), nil } } return "", fmt.Errorf("can't find %v device under /dev", typ) } func mkdirMode(fname string, mode os.FileMode) error { if err := os.Mkdir(fname, mode); err != nil { return err } return checkMode(fname, mode|os.ModeDir) } func checkMode(fname string, wantMode os.FileMode) error { info, err := os.Lstat(fname) if err != nil { return err } if mode := info.Mode(); mode != wantMode { return fmt.Errorf("created file has mode %v, want %v", mode, wantMode) } return nil } elvish-0.17.0/pkg/cli/lscolors/feature_unix_test.go000066400000000000000000000003111415471104000223350ustar00rootroot00000000000000//go:build !windows && !plan9 && !js // +build !windows,!plan9,!js package lscolors import ( "golang.org/x/sys/unix" ) func createNamedPipe(fname string) error { return unix.Mkfifo(fname, 0600) } elvish-0.17.0/pkg/cli/lscolors/lscolors.go000066400000000000000000000110131415471104000204410ustar00rootroot00000000000000// Package lscolors provides styling of filenames based on file features. // // This is a reverse-engineered implementation of the parsing and // interpretation of the LS_COLORS environmental variable used by GNU // coreutils. package lscolors import ( "os" "path" "strings" "sync" "src.elv.sh/pkg/env" "src.elv.sh/pkg/testutil" ) // Colorist styles filenames based on the features of the file. type Colorist interface { // GetStyle returns the style for the named file. GetStyle(fname string) string } type colorist struct { styleForFeature map[feature]string styleForExt map[string]string } const defaultLsColorString = `rs=:di=01;34:ln=01;36:mh=:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.axv=01;35:*.anx=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=36:*.au=36:*.flac=36:*.mid=36:*.midi=36:*.mka=36:*.mp3=36:*.mpc=36:*.ogg=36:*.ra=36:*.wav=36:*.axa=36:*.oga=36:*.spx=36:*.xspf=36:` var ( lastColorist *colorist lastColoristMutex sync.Mutex lastLsColors string ) func init() { lastColorist = parseLsColor(defaultLsColorString) } func GetColorist() Colorist { lastColoristMutex.Lock() defer lastColoristMutex.Unlock() s := getLsColors() if lastLsColors != s { lastLsColors = s lastColorist = parseLsColor(s) } return lastColorist } func getLsColors() string { lsColorString := os.Getenv(env.LS_COLORS) if len(lsColorString) == 0 { return defaultLsColorString } return lsColorString } var featureForName = map[string]feature{ "rs": featureRegular, "di": featureDirectory, "ln": featureSymlink, "mh": featureMultiHardLink, "pi": featureNamedPipe, "so": featureSocket, "do": featureDoor, "bd": featureBlockDevice, "cd": featureCharDevice, "or": featureOrphanedSymlink, "su": featureSetuid, "sg": featureSetgid, "ca": featureCapability, "tw": featureWorldWritableStickyDirectory, "ow": featureWorldWritableDirectory, "st": featureStickyDirectory, "ex": featureExecutable, } // parseLsColor parses a string in the LS_COLORS format into lsColor. Erroneous // fields are silently ignored. func parseLsColor(s string) *colorist { lc := &colorist{make(map[feature]string), make(map[string]string)} for _, spec := range strings.Split(s, ":") { words := strings.Split(spec, "=") if len(words) != 2 { continue } key, value := words[0], words[1] filterValues := []string{} for _, splitValue := range strings.Split(value, ";") { if strings.Count(splitValue, "0") == len(splitValue) { continue } filterValues = append(filterValues, splitValue) } if len(filterValues) == 0 { continue } value = strings.Join(filterValues, ";") if strings.HasPrefix(key, "*.") { lc.styleForExt[key[1:]] = value } else { feature, ok := featureForName[key] if !ok { continue } lc.styleForFeature[feature] = value } } return lc } func (lc *colorist) GetStyle(fname string) string { mh := strings.Trim(lc.styleForFeature[featureMultiHardLink], "0") != "" // TODO Handle error from determineFeature feature, _ := determineFeature(fname, mh) if feature == featureRegular { if ext := path.Ext(fname); ext != "" { if style, ok := lc.styleForExt[ext]; ok { return style } } } return lc.styleForFeature[feature] } // SetTestLsColors sets LS_COLORS to a value where directories are blue and // .png files are red for the duration of a test. func SetTestLsColors(c testutil.Cleanuper) { // ow (world-writable directory) needed for Windows. testutil.Setenv(c, "LS_COLORS", "di=34:ow=34:*.png=31") } elvish-0.17.0/pkg/cli/lscolors/lscolors_test.go000066400000000000000000000011071415471104000215030ustar00rootroot00000000000000package lscolors import ( "os" "testing" "src.elv.sh/pkg/testutil" ) func TestLsColors(t *testing.T) { testutil.InTempDir(t) SetTestLsColors(t) // Test both feature-based and extension-based coloring. colorist := GetColorist() os.Mkdir("dir", 0755) create("a.png") wantDirStyle := "34" if style := colorist.GetStyle("dir"); style != wantDirStyle { t.Errorf("Got dir style %q, want %q", style, wantDirStyle) } wantPngStyle := "31" if style := colorist.GetStyle("a.png"); style != wantPngStyle { t.Errorf("Got dir style %q, want %q", style, wantPngStyle) } } elvish-0.17.0/pkg/cli/lscolors/stat_notsolaris.go000066400000000000000000000002451415471104000220360ustar00rootroot00000000000000//go:build !solaris // +build !solaris package lscolors import "os" func isDoor(info os.FileInfo) bool { // Doors are only supported on Solaris. return false } elvish-0.17.0/pkg/cli/lscolors/stat_solaris.go000066400000000000000000000003161415471104000213140ustar00rootroot00000000000000package lscolors import ( "os" "syscall" ) // Taken from Illumos header file. const sIFDOOR = 0xD000 func isDoor(info os.FileInfo) bool { return info.Sys().(*syscall.Stat_t).Mode&sIFDOOR == sIFDOOR } elvish-0.17.0/pkg/cli/lscolors/stat_unix.go000066400000000000000000000003071415471104000206230ustar00rootroot00000000000000//go:build !windows && !plan9 // +build !windows,!plan9 package lscolors import ( "os" "syscall" ) func isMultiHardlink(info os.FileInfo) bool { return info.Sys().(*syscall.Stat_t).Nlink > 1 } elvish-0.17.0/pkg/cli/lscolors/stat_windows.go000066400000000000000000000003431415471104000213320ustar00rootroot00000000000000package lscolors import "os" func isMultiHardlink(info os.FileInfo) bool { // Windows supports hardlinks, but it is not exposed directly. We omit the // implementation for now. // TODO: Maybe implement it? return false } elvish-0.17.0/pkg/cli/modes/000077500000000000000000000000001415471104000155255ustar00rootroot00000000000000elvish-0.17.0/pkg/cli/modes/completion.go000066400000000000000000000047741415471104000202410ustar00rootroot00000000000000package modes import ( "errors" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/ui" ) // Completion is a mode specialized for viewing and inserting completion // candidates. It is based on the ComboBox widget. type Completion interface { tk.ComboBox } // CompletionSpec specifies the configuration for the completion mode. type CompletionSpec struct { Bindings tk.Bindings Name string Replace diag.Ranging Items []CompletionItem Filter FilterSpec } // CompletionItem represents a completion item, also known as a candidate. type CompletionItem struct { // Used in the UI and for filtering. ToShow string // Style to use in the UI. ShowStyle ui.Style // Used when inserting a candidate. ToInsert string } type completion struct { tk.ComboBox attached tk.CodeArea } var errNoCandidates = errors.New("no candidates") // NewCompletion starts the completion UI. func NewCompletion(app cli.App, cfg CompletionSpec) (Completion, error) { codeArea, err := FocusedCodeArea(app) if err != nil { return nil, err } if len(cfg.Items) == 0 { return nil, errNoCandidates } w := tk.NewComboBox(tk.ComboBoxSpec{ CodeArea: tk.CodeAreaSpec{ Prompt: modePrompt(" COMPLETING "+cfg.Name+" ", true), Highlighter: cfg.Filter.Highlighter, }, ListBox: tk.ListBoxSpec{ Horizontal: true, Bindings: cfg.Bindings, OnSelect: func(it tk.Items, i int) { text := it.(completionItems)[i].ToInsert codeArea.MutateState(func(s *tk.CodeAreaState) { s.Pending = tk.PendingCode{ From: cfg.Replace.From, To: cfg.Replace.To, Content: text} }) }, OnAccept: func(it tk.Items, i int) { codeArea.MutateState((*tk.CodeAreaState).ApplyPending) app.PopAddon() }, ExtendStyle: true, }, OnFilter: func(w tk.ComboBox, p string) { w.ListBox().Reset(filterCompletionItems(cfg.Items, cfg.Filter.makePredicate(p)), 0) }, }) return completion{w, codeArea}, nil } func (w completion) Dismiss() { w.attached.MutateState(func(s *tk.CodeAreaState) { s.Pending = tk.PendingCode{} }) } type completionItems []CompletionItem func filterCompletionItems(all []CompletionItem, p func(string) bool) completionItems { var filtered []CompletionItem for _, candidate := range all { if p(candidate.ToShow) { filtered = append(filtered, candidate) } } return filtered } func (it completionItems) Show(i int) ui.Text { return ui.Text{&ui.Segment{Style: it[i].ShowStyle, Text: it[i].ToShow}} } func (it completionItems) Len() int { return len(it) } elvish-0.17.0/pkg/cli/modes/completion_test.go000066400000000000000000000034001415471104000212610ustar00rootroot00000000000000package modes import ( "testing" "src.elv.sh/pkg/cli" . "src.elv.sh/pkg/cli/clitest" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/ui" ) func TestCompletion_Filter(t *testing.T) { f := setupStartedCompletion(t) defer f.Stop() f.TTY.Inject(term.K('b'), term.K('a')) f.TestTTY(t, "'foo bar'\n", Styles, "_________", " COMPLETING WORD ba", Styles, "***************** ", term.DotHere, "\n", "foo bar", Styles, "#######", ) } func TestCompletion_Accept(t *testing.T) { f := setupStartedCompletion(t) defer f.Stop() f.TTY.Inject(term.K(ui.Enter)) f.TestTTY(t, "foo", term.DotHere) } func TestCompletion_Dismiss(t *testing.T) { f := setupStartedCompletion(t) defer f.Stop() f.App.PopAddon() f.App.Redraw() f.TestTTY(t /* nothing */) } func TestNewCompletion_NoItems(t *testing.T) { f := Setup() defer f.Stop() _, err := NewCompletion(f.App, CompletionSpec{Items: []CompletionItem{}}) if err != errNoCandidates { t.Errorf("should return errNoCandidates") } } func TestNewCompletion_FocusedWidgetNotCodeArea(t *testing.T) { testFocusedWidgetNotCodeArea(t, func(app cli.App) error { _, err := NewCompletion(app, CompletionSpec{Items: []CompletionItem{{}}}) return err }) } func setupStartedCompletion(t *testing.T) *Fixture { f := Setup() w, _ := NewCompletion(f.App, CompletionSpec{ Name: "WORD", Replace: diag.Ranging{From: 0, To: 0}, Items: []CompletionItem{ {ToShow: "foo", ToInsert: "foo"}, {ToShow: "foo bar", ToInsert: "'foo bar'", ShowStyle: ui.Style{Foreground: ui.Blue}}, }, }) f.App.PushAddon(w) f.App.Redraw() f.TestTTY(t, "foo\n", Styles, "___", " COMPLETING WORD ", Styles, "***************** ", term.DotHere, "\n", "foo foo bar", Styles, "+++ ///////", ) return f } elvish-0.17.0/pkg/cli/modes/filter_spec.go000066400000000000000000000011371415471104000203550ustar00rootroot00000000000000package modes import ( "strings" "src.elv.sh/pkg/ui" ) // FilterSpec specifies the configuration for the filter in listing modes. type FilterSpec struct { // Called with the filter text to get the filter predicate. If nil, the // predicate performs substring match. Maker func(string) func(string) bool // Highlighter for the filter. If nil, the filter will not be highlighted. Highlighter func(string) (ui.Text, []error) } func (f FilterSpec) makePredicate(p string) func(string) bool { if f.Maker == nil { return func(s string) bool { return strings.Contains(s, p) } } return f.Maker(p) } elvish-0.17.0/pkg/cli/modes/histlist.go000066400000000000000000000052161415471104000177230ustar00rootroot00000000000000package modes import ( "fmt" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/store/storedefs" "src.elv.sh/pkg/ui" ) // Histlist is a mode for browsing history and selecting entries to insert. It // is based on the ComboBox widget. type Histlist interface { tk.ComboBox } // HistlistSpec specifies the configuration for the histlist mode. type HistlistSpec struct { // Key bindings. Bindings tk.Bindings // AllCmds is called to retrieve all commands. AllCmds func() ([]storedefs.Cmd, error) // Dedup is called to determine whether deduplication should be done. // Defaults to true if unset. Dedup func() bool // Configuration for the filter. Filter FilterSpec } // NewHistlist creates a new histlist mode. func NewHistlist(app cli.App, spec HistlistSpec) (Histlist, error) { codeArea, err := FocusedCodeArea(app) if err != nil { return nil, err } if spec.AllCmds == nil { return nil, errNoHistoryStore } if spec.Dedup == nil { spec.Dedup = func() bool { return true } } cmds, err := spec.AllCmds() if err != nil { return nil, fmt.Errorf("db error: %v", err.Error()) } last := map[string]int{} for i, cmd := range cmds { last[cmd.Text] = i } cmdItems := histlistItems{cmds, last} w := tk.NewComboBox(tk.ComboBoxSpec{ CodeArea: tk.CodeAreaSpec{ Prompt: func() ui.Text { content := " HISTORY " if spec.Dedup() { content += "(dedup on) " } return modeLine(content, true) }, Highlighter: spec.Filter.Highlighter, }, ListBox: tk.ListBoxSpec{ Bindings: spec.Bindings, OnAccept: func(it tk.Items, i int) { text := it.(histlistItems).entries[i].Text codeArea.MutateState(func(s *tk.CodeAreaState) { buf := &s.Buffer if buf.Content == "" { buf.InsertAtDot(text) } else { buf.InsertAtDot("\n" + text) } }) app.PopAddon() }, }, OnFilter: func(w tk.ComboBox, p string) { it := cmdItems.filter(spec.Filter.makePredicate(p), spec.Dedup()) w.ListBox().Reset(it, it.Len()-1) }, }) return w, nil } type histlistItems struct { entries []storedefs.Cmd last map[string]int } func (it histlistItems) filter(p func(string) bool, dedup bool) histlistItems { var filtered []storedefs.Cmd for i, entry := range it.entries { text := entry.Text if dedup && it.last[text] != i { continue } if p(text) { filtered = append(filtered, entry) } } return histlistItems{filtered, nil} } func (it histlistItems) Show(i int) ui.Text { entry := it.entries[i] // TODO: The alignment of the index works up to 10000 entries. return ui.T(fmt.Sprintf("%4d %s", entry.Seq, entry.Text)) } func (it histlistItems) Len() int { return len(it.entries) } elvish-0.17.0/pkg/cli/modes/histlist_test.go000066400000000000000000000075451415471104000207710ustar00rootroot00000000000000package modes import ( "regexp" "testing" "src.elv.sh/pkg/cli" . "src.elv.sh/pkg/cli/clitest" "src.elv.sh/pkg/cli/histutil" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/store/storedefs" "src.elv.sh/pkg/ui" ) func TestNewHistlist_NoStore(t *testing.T) { f := Setup() defer f.Stop() _, err := NewHistlist(f.App, HistlistSpec{}) if err != errNoHistoryStore { t.Errorf("want errNoHistoryStore") } } func TestNewHistlist_FocusedWidgetNotCodeArea(t *testing.T) { testFocusedWidgetNotCodeArea(t, func(app cli.App) error { st := histutil.NewMemStore("foo") _, err := NewHistlist(app, HistlistSpec{AllCmds: st.AllCmds}) return err }) } type faultyStore struct{} func (s faultyStore) AllCmds() ([]storedefs.Cmd, error) { return nil, errMock } func TestNewHistlist_StoreError(t *testing.T) { f := Setup() defer f.Stop() _, err := NewHistlist(f.App, HistlistSpec{AllCmds: faultyStore{}.AllCmds}) if err.Error() != "db error: mock error" { t.Errorf("want db error") } } func TestHistlist(t *testing.T) { f := Setup() defer f.Stop() st := histutil.NewMemStore( // 0 1 2 "foo", "bar", "baz") startHistlist(f.App, HistlistSpec{AllCmds: st.AllCmds}) // Test initial UI - last item selected f.TestTTY(t, "\n", " HISTORY (dedup on) ", Styles, "******************** ", term.DotHere, "\n", " 0 foo\n", " 1 bar\n", " 2 baz ", Styles, "++++++++++++++++++++++++++++++++++++++++++++++++++") // Test filtering. f.TTY.Inject(term.K('b')) f.TestTTY(t, "\n", " HISTORY (dedup on) b", Styles, "******************** ", term.DotHere, "\n", " 1 bar\n", " 2 baz ", Styles, "++++++++++++++++++++++++++++++++++++++++++++++++++") // Test accepting. f.TTY.Inject(term.K(ui.Enter)) f.TestTTY(t, "baz", term.DotHere) // Test accepting when there is already some text. st.AddCmd(storedefs.Cmd{Text: "baz2"}) startHistlist(f.App, HistlistSpec{AllCmds: st.AllCmds}) f.TTY.Inject(term.K(ui.Enter)) f.TestTTY(t, "baz", // codearea now contains newly inserted entry on a separate line "\n", "baz2", term.DotHere) } func TestHistlist_Dedup(t *testing.T) { f := Setup() defer f.Stop() st := histutil.NewMemStore( // 0 1 2 "ls", "echo", "ls") // No dedup startHistlist(f.App, HistlistSpec{AllCmds: st.AllCmds, Dedup: func() bool { return false }}) f.TestTTY(t, "\n", " HISTORY ", Styles, "********* ", term.DotHere, "\n", " 0 ls\n", " 1 echo\n", " 2 ls ", Styles, "++++++++++++++++++++++++++++++++++++++++++++++++++") f.App.PopAddon() // With dedup startHistlist(f.App, HistlistSpec{AllCmds: st.AllCmds, Dedup: func() bool { return true }}) f.TestTTY(t, "\n", " HISTORY (dedup on) ", Styles, "******************** ", term.DotHere, "\n", " 1 echo\n", " 2 ls ", Styles, "++++++++++++++++++++++++++++++++++++++++++++++++++") } func TestHistlist_CustomFilter(t *testing.T) { f := Setup() defer f.Stop() st := histutil.NewMemStore( // 0 1 2 "vi", "elvish", "nvi") startHistlist(f.App, HistlistSpec{ AllCmds: st.AllCmds, Filter: FilterSpec{ Maker: func(p string) func(string) bool { re, _ := regexp.Compile(p) return func(s string) bool { return re != nil && re.MatchString(s) } }, Highlighter: func(p string) (ui.Text, []error) { return ui.T(p, ui.Inverse), nil }, }, }) f.TTY.Inject(term.K('v'), term.K('i'), term.K('$')) f.TestTTY(t, "\n", " HISTORY (dedup on) vi$", Styles, "******************** +++", term.DotHere, "\n", " 0 vi\n", " 2 nvi ", Styles, "++++++++++++++++++++++++++++++++++++++++++++++++++") } func startHistlist(app cli.App, spec HistlistSpec) { w, err := NewHistlist(app, spec) startMode(app, w, err) } elvish-0.17.0/pkg/cli/modes/histwalk.go000066400000000000000000000054241415471104000177070ustar00rootroot00000000000000package modes import ( "errors" "fmt" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/histutil" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/cli/tk" ) // Histwalk is a mode for walking through history. type Histwalk interface { tk.Widget // Walk to the previous entry in history. Prev() error // Walk to the next entry in history. Next() error } // HistwalkSpec specifies the configuration for the histwalk mode. type HistwalkSpec struct { // Key bindings. Bindings tk.Bindings // History store to walk. Store histutil.Store // Only walk through items with this prefix. Prefix string } type histwalk struct { app cli.App attachedTo tk.CodeArea cursor histutil.Cursor HistwalkSpec } func (w *histwalk) Render(width, height int) *term.Buffer { buf := w.render(width) buf.TrimToLines(0, height) return buf } func (w *histwalk) MaxHeight(width, height int) int { return len(w.render(width).Lines) } func (w *histwalk) render(width int) *term.Buffer { cmd, _ := w.cursor.Get() content := modeLine(fmt.Sprintf(" HISTORY #%d ", cmd.Seq), false) return term.NewBufferBuilder(width).WriteStyled(content).Buffer() } func (w *histwalk) Handle(event term.Event) bool { handled := w.Bindings.Handle(w, event) if handled { return true } w.attachedTo.MutateState((*tk.CodeAreaState).ApplyPending) w.app.PopAddon() return w.attachedTo.Handle(event) } func (w *histwalk) Focus() bool { return false } var errNoHistoryStore = errors.New("no history store") // NewHistwalk creates a new Histwalk mode. func NewHistwalk(app cli.App, cfg HistwalkSpec) (Histwalk, error) { codeArea, err := FocusedCodeArea(app) if err != nil { return nil, err } if cfg.Store == nil { return nil, errNoHistoryStore } if cfg.Bindings == nil { cfg.Bindings = tk.DummyBindings{} } cursor := cfg.Store.Cursor(cfg.Prefix) cursor.Prev() if _, err := cursor.Get(); err != nil { return nil, err } w := histwalk{app: app, attachedTo: codeArea, HistwalkSpec: cfg, cursor: cursor} w.updatePending() return &w, nil } func (w *histwalk) Prev() error { return w.walk(histutil.Cursor.Prev, histutil.Cursor.Next) } func (w *histwalk) Next() error { return w.walk(histutil.Cursor.Next, histutil.Cursor.Prev) } func (w *histwalk) walk(f func(histutil.Cursor), undo func(histutil.Cursor)) error { f(w.cursor) _, err := w.cursor.Get() if err == nil { w.updatePending() } else if err == histutil.ErrEndOfHistory { undo(w.cursor) } return err } func (w *histwalk) Dismiss() { w.attachedTo.MutateState(func(s *tk.CodeAreaState) { s.Pending = tk.PendingCode{} }) } func (w *histwalk) updatePending() { cmd, _ := w.cursor.Get() w.attachedTo.MutateState(func(s *tk.CodeAreaState) { s.Pending = tk.PendingCode{ From: len(w.Prefix), To: len(s.Buffer.Content), Content: cmd.Text[len(w.Prefix):], } }) } elvish-0.17.0/pkg/cli/modes/histwalk_test.go000066400000000000000000000060121415471104000207400ustar00rootroot00000000000000package modes import ( "testing" "src.elv.sh/pkg/cli" . "src.elv.sh/pkg/cli/clitest" "src.elv.sh/pkg/cli/histutil" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/ui" ) func TestHistWalk(t *testing.T) { f := Setup(WithSpec(func(spec *cli.AppSpec) { spec.CodeAreaState.Buffer = tk.CodeBuffer{Content: "ls", Dot: 2} })) defer f.Stop() f.App.Redraw() buf0 := f.MakeBuffer("ls", term.DotHere) f.TTY.TestBuffer(t, buf0) getCfg := func() HistwalkSpec { store := histutil.NewMemStore( // 0 1 2 3 4 5 "echo", "ls -l", "echo a", "echo", "echo a", "ls -a") return HistwalkSpec{ Store: store, Prefix: "ls", Bindings: tk.MapBindings{ term.K(ui.Up): func(w tk.Widget) { w.(Histwalk).Prev() }, term.K(ui.Down): func(w tk.Widget) { w.(Histwalk).Next() }, term.K('[', ui.Ctrl): func(tk.Widget) { f.App.PopAddon() }, }, } } startHistwalk(f.App, getCfg()) buf5 := f.MakeBuffer( "ls -a", Styles, " ___", term.DotHere, "\n", " HISTORY #5 ", Styles, "************", ) f.TTY.TestBuffer(t, buf5) f.TTY.Inject(term.K(ui.Up)) buf1 := f.MakeBuffer( "ls -l", Styles, " ___", term.DotHere, "\n", " HISTORY #1 ", Styles, "************", ) f.TTY.TestBuffer(t, buf1) f.TTY.Inject(term.K(ui.Down)) f.TTY.TestBuffer(t, buf5) f.TTY.Inject(term.K('[', ui.Ctrl)) f.TTY.TestBuffer(t, buf0) // Start over and accept. startHistwalk(f.App, getCfg()) f.TTY.TestBuffer(t, buf5) f.TTY.Inject(term.K(' ')) f.TestTTY(t, "ls -a ", term.DotHere) } func TestHistWalk_FocusedWidgetNotCodeArea(t *testing.T) { testFocusedWidgetNotCodeArea(t, func(app cli.App) error { store := histutil.NewMemStore("foo") _, err := NewHistwalk(app, HistwalkSpec{Store: store}) return err }) } func TestHistWalk_NoWalker(t *testing.T) { f := Setup() defer f.Stop() startHistwalk(f.App, HistwalkSpec{}) f.TestTTYNotes(t, "no history store") } func TestHistWalk_NoMatch(t *testing.T) { f := Setup(WithSpec(func(spec *cli.AppSpec) { spec.CodeAreaState.Buffer = tk.CodeBuffer{Content: "ls", Dot: 2} })) defer f.Stop() f.App.Redraw() buf0 := f.MakeBuffer("ls", term.DotHere) f.TTY.TestBuffer(t, buf0) store := histutil.NewMemStore("echo 1", "echo 2") cfg := HistwalkSpec{Store: store, Prefix: "ls"} startHistwalk(f.App, cfg) // Test that an error message has been written to the notes buffer. f.TestTTYNotes(t, "end of history") // Test that buffer has not changed - histwalk addon is not active. f.TTY.TestBuffer(t, buf0) } func TestHistWalk_FallbackHandler(t *testing.T) { f := Setup() defer f.Stop() store := histutil.NewMemStore("ls") startHistwalk(f.App, HistwalkSpec{Store: store, Prefix: ""}) f.TestTTY(t, "ls", Styles, "__", term.DotHere, "\n", " HISTORY #0 ", Styles, "************", ) f.TTY.Inject(term.K(ui.Backspace)) f.TestTTY(t, "l", term.DotHere) } func startHistwalk(app cli.App, cfg HistwalkSpec) { w, err := NewHistwalk(app, cfg) if err != nil { app.Notify(err.Error()) return } app.PushAddon(w) app.Redraw() } elvish-0.17.0/pkg/cli/modes/instant.go000066400000000000000000000044561415471104000175450ustar00rootroot00000000000000package modes import ( "errors" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/ui" ) // Instant is a mode that executes code whenever it changes and shows the // result. type Instant interface { tk.Widget } // InstantSpec specifies the configuration for the instant mode. type InstantSpec struct { // Key bindings. Bindings tk.Bindings // The function to execute code and returns the output. Execute func(code string) ([]string, error) } type instant struct { InstantSpec attachedTo tk.CodeArea textView tk.TextView lastCode string lastErr error } func (w *instant) Render(width, height int) *term.Buffer { buf := w.render(width, height) buf.TrimToLines(0, height) return buf } func (w *instant) MaxHeight(width, height int) int { return len(w.render(width, height).Lines) } func (w *instant) render(width, height int) *term.Buffer { bb := term.NewBufferBuilder(width). WriteStyled(modeLine(" INSTANT ", false)).SetDotHere() if w.lastErr != nil { bb.Newline().Write(w.lastErr.Error(), ui.FgRed) } buf := bb.Buffer() if len(buf.Lines) < height { bufTextView := w.textView.Render(width, height-len(buf.Lines)) buf.Extend(bufTextView, false) } return buf } func (w *instant) Focus() bool { return false } func (w *instant) Handle(event term.Event) bool { handled := w.Bindings.Handle(w, event) if !handled { handled = w.attachedTo.Handle(event) } w.update(false) return handled } func (w *instant) update(force bool) { code := w.attachedTo.CopyState().Buffer.Content if code == w.lastCode && !force { return } w.lastCode = code output, err := w.Execute(code) w.lastErr = err if err == nil { w.textView.MutateState(func(s *tk.TextViewState) { *s = tk.TextViewState{Lines: output, First: 0} }) } } var errExecutorIsRequired = errors.New("executor is required") // NewInstant creates a new instant mode. func NewInstant(app cli.App, cfg InstantSpec) (Instant, error) { codeArea, err := FocusedCodeArea(app) if err != nil { return nil, err } if cfg.Execute == nil { return nil, errExecutorIsRequired } if cfg.Bindings == nil { cfg.Bindings = tk.DummyBindings{} } w := instant{ InstantSpec: cfg, attachedTo: codeArea, textView: tk.NewTextView(tk.TextViewSpec{Scrollable: true}), } w.update(true) return &w, nil } elvish-0.17.0/pkg/cli/modes/instant_test.go000066400000000000000000000031371415471104000205770ustar00rootroot00000000000000package modes import ( "errors" "testing" "src.elv.sh/pkg/cli" . "src.elv.sh/pkg/cli/clitest" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/ui" ) func setupStartedInstant(t *testing.T) *Fixture { f := Setup() w, err := NewInstant(f.App, InstantSpec{ Execute: func(code string) ([]string, error) { var err error if code == "!" { err = errors.New("error") } return []string{"result of", code}, err }, }) startMode(f.App, w, err) f.TestTTY(t, term.DotHere, "\n", " INSTANT \n", Styles, "*********", "result of\n", "", ) return f } func TestInstant_ShowsResult(t *testing.T) { f := setupStartedInstant(t) defer f.Stop() f.TTY.Inject(term.K('a')) bufA := f.MakeBuffer( "a", term.DotHere, "\n", " INSTANT \n", Styles, "*********", "result of\n", "a", ) f.TTY.TestBuffer(t, bufA) f.TTY.Inject(term.K(ui.Right)) f.TTY.TestBuffer(t, bufA) } func TestInstant_ShowsError(t *testing.T) { f := setupStartedInstant(t) defer f.Stop() f.TTY.Inject(term.K('!')) f.TestTTY(t, "!", term.DotHere, "\n", " INSTANT \n", Styles, "*********", // Error shown. "error\n", Styles, "!!!!!", // Buffer not updated. "result of\n", "", ) } func TestNewInstant_NoExecutor(t *testing.T) { f := Setup() _, err := NewInstant(f.App, InstantSpec{}) if err != errExecutorIsRequired { t.Error("expect errExecutorIsRequired") } } func TestNewInstant_FocusedWidgetNotCodeArea(t *testing.T) { testFocusedWidgetNotCodeArea(t, func(app cli.App) error { _, err := NewInstant(app, InstantSpec{ Execute: func(string) ([]string, error) { return nil, nil }}) return err }) } elvish-0.17.0/pkg/cli/modes/lastcmd.go000066400000000000000000000062031415471104000175040ustar00rootroot00000000000000package modes import ( "fmt" "strconv" "strings" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/histutil" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/ui" ) // Lastcmd is a mode for inspecting the last command, and inserting part of all // of it. It is based on the ComboBox widget. type Lastcmd interface { tk.ComboBox } // LastcmdSpec specifies the configuration for the lastcmd mode. type LastcmdSpec struct { // Key bindings. Bindings tk.Bindings // Store provides the source for the last command. Store LastcmdStore // Wordifier breaks a command into words. Wordifier func(string) []string } // LastcmdStore is a subset of histutil.Store used in lastcmd mode. type LastcmdStore interface { Cursor(prefix string) histutil.Cursor } var _ = LastcmdStore(histutil.Store(nil)) // NewLastcmd creates a new lastcmd mode. func NewLastcmd(app cli.App, cfg LastcmdSpec) (Lastcmd, error) { codeArea, err := FocusedCodeArea(app) if err != nil { return nil, err } if cfg.Store == nil { return nil, errNoHistoryStore } c := cfg.Store.Cursor("") c.Prev() cmd, err := c.Get() if err != nil { return nil, fmt.Errorf("db error: %v", err) } wordifier := cfg.Wordifier if wordifier == nil { wordifier = strings.Fields } cmdText := cmd.Text words := wordifier(cmdText) entries := make([]lastcmdEntry, len(words)+1) entries[0] = lastcmdEntry{content: cmdText} for i, word := range words { entries[i+1] = lastcmdEntry{strconv.Itoa(i), strconv.Itoa(i - len(words)), word} } accept := func(text string) { codeArea.MutateState(func(s *tk.CodeAreaState) { s.Buffer.InsertAtDot(text) }) app.PopAddon() } w := tk.NewComboBox(tk.ComboBoxSpec{ CodeArea: tk.CodeAreaSpec{Prompt: modePrompt(" LASTCMD ", true)}, ListBox: tk.ListBoxSpec{ Bindings: cfg.Bindings, OnAccept: func(it tk.Items, i int) { accept(it.(lastcmdItems).entries[i].content) }, }, OnFilter: func(w tk.ComboBox, p string) { items := filterLastcmdItems(entries, p) if len(items.entries) == 1 { accept(items.entries[0].content) } else { w.ListBox().Reset(items, 0) } }, }) return w, nil } type lastcmdItems struct { negFilter bool entries []lastcmdEntry } type lastcmdEntry struct { posIndex string negIndex string content string } func filterLastcmdItems(allEntries []lastcmdEntry, p string) lastcmdItems { if p == "" { return lastcmdItems{false, allEntries} } var entries []lastcmdEntry negFilter := strings.HasPrefix(p, "-") for _, entry := range allEntries { if (negFilter && strings.HasPrefix(entry.negIndex, p)) || (!negFilter && strings.HasPrefix(entry.posIndex, p)) { entries = append(entries, entry) } } return lastcmdItems{negFilter, entries} } func (it lastcmdItems) Show(i int) ui.Text { index := "" entry := it.entries[i] if it.negFilter { index = entry.negIndex } else { index = entry.posIndex } // NOTE: We now use a hardcoded width of 3 for the index, which will work as // long as the command has less than 1000 words (when filter is positive) or // 100 words (when filter is negative). return ui.T(fmt.Sprintf("%3s %s", index, entry.content)) } func (it lastcmdItems) Len() int { return len(it.entries) } elvish-0.17.0/pkg/cli/modes/lastcmd_test.go000066400000000000000000000052151415471104000205450ustar00rootroot00000000000000package modes import ( "strings" "testing" "src.elv.sh/pkg/cli" . "src.elv.sh/pkg/cli/clitest" "src.elv.sh/pkg/cli/histutil" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/store/storedefs" "src.elv.sh/pkg/ui" ) func TestNewLastcmd_NoStore(t *testing.T) { f := Setup() defer f.Stop() _, err := NewLastcmd(f.App, LastcmdSpec{}) if err != errNoHistoryStore { t.Error("expect errNoHistoryStore") } } func TestNewLastcmd_FocusedWidgetNotCodeArea(t *testing.T) { testFocusedWidgetNotCodeArea(t, func(app cli.App) error { st := histutil.NewMemStore("foo") _, err := NewLastcmd(app, LastcmdSpec{Store: st}) return err }) } func TestNewLastcmd_StoreError(t *testing.T) { f := Setup() defer f.Stop() db := histutil.NewFaultyInMemoryDB() store, err := histutil.NewDBStore(db) if err != nil { panic(err) } db.SetOneOffError(errMock) _, err = NewLastcmd(f.App, LastcmdSpec{Store: store}) if err.Error() != "db error: mock error" { t.Error("expect db error") } } func TestLastcmd(t *testing.T) { f := Setup() defer f.Stop() st := histutil.NewMemStore("foo,bar,baz") startLastcmd(f.App, LastcmdSpec{ Store: st, Wordifier: func(cmd string) []string { return strings.Split(cmd, ",") }, }) // Test UI. f.TestTTY(t, "\n", // empty code area " LASTCMD ", Styles, "********* ", term.DotHere, "\n", " foo,bar,baz \n", Styles, "++++++++++++++++++++++++++++++++++++++++++++++++++", " 0 foo\n", " 1 bar\n", " 2 baz", ) // Test negative filtering. f.TTY.Inject(term.K('-')) f.TestTTY(t, "\n", // empty code area " LASTCMD -", Styles, "********* ", term.DotHere, "\n", " -3 foo \n", Styles, "++++++++++++++++++++++++++++++++++++++++++++++++++", " -2 bar\n", " -1 baz", ) // Test automatic submission. f.TTY.Inject(term.K('2')) // -2 bar f.TestTTY(t, "bar", term.DotHere) // Test submission by Enter. f.App.ActiveWidget().(tk.CodeArea).MutateState(func(s *tk.CodeAreaState) { *s = tk.CodeAreaState{} }) startLastcmd(f.App, LastcmdSpec{ Store: st, Wordifier: func(cmd string) []string { return strings.Split(cmd, ",") }, }) f.TTY.Inject(term.K(ui.Enter)) f.TestTTY(t, "foo,bar,baz", term.DotHere) // Default wordifier. f.App.ActiveWidget().(tk.CodeArea).MutateState(func(s *tk.CodeAreaState) { *s = tk.CodeAreaState{} }) st.AddCmd(storedefs.Cmd{Text: "foo bar baz", Seq: 1}) startLastcmd(f.App, LastcmdSpec{Store: st}) f.TTY.Inject(term.K('0')) f.TestTTY(t, "foo", term.DotHere) } func startLastcmd(app cli.App, spec LastcmdSpec) { w, err := NewLastcmd(app, spec) startMode(app, w, err) } elvish-0.17.0/pkg/cli/modes/listing.go000066400000000000000000000044171415471104000175330ustar00rootroot00000000000000package modes import ( "errors" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/ui" ) // Listing is a customizable mode for browsing through a list of items. It is // based on the ComboBox widget. type Listing interface { tk.ComboBox } // ListingSpec specifies the configuration for the listing mode. type ListingSpec struct { // Key bindings. Bindings tk.Bindings // Caption of the listing. If empty, defaults to " LISTING ". Caption string // A function that takes the query string and returns a list of Item's and // the index of the Item to select. Required. GetItems func(query string) (items []ListingItem, selected int) // A function to call when the user has accepted the selected item. If the // return value is true, the listing will not be closed after accpeting. // If unspecified, the Accept function default to a function that does // nothing other than returning false. Accept func(string) // Whether to automatically accept when there is only one item. AutoAccept bool } // ListingItem is an item to show in the listing. type ListingItem struct { // Passed to the Accept callback in Config. ToAccept string // How the item is shown. ToShow ui.Text } var errGetItemsMustBeSpecified = errors.New("GetItems must be specified") // NewListing creates a new listing mode. func NewListing(app cli.App, spec ListingSpec) (Listing, error) { if spec.GetItems == nil { return nil, errGetItemsMustBeSpecified } if spec.Accept == nil { spec.Accept = func(string) {} } if spec.Caption == "" { spec.Caption = " LISTING " } accept := func(s string) { app.PopAddon() spec.Accept(s) } w := tk.NewComboBox(tk.ComboBoxSpec{ CodeArea: tk.CodeAreaSpec{ Prompt: modePrompt(spec.Caption, true), }, ListBox: tk.ListBoxSpec{ Bindings: spec.Bindings, OnAccept: func(it tk.Items, i int) { accept(it.(listingItems)[i].ToAccept) }, ExtendStyle: true, }, OnFilter: func(w tk.ComboBox, q string) { it, selected := spec.GetItems(q) w.ListBox().Reset(listingItems(it), selected) if spec.AutoAccept && len(it) == 1 { accept(it[0].ToAccept) } }, }) return w, nil } type listingItems []ListingItem func (it listingItems) Len() int { return len(it) } func (it listingItems) Show(i int) ui.Text { return it[i].ToShow } elvish-0.17.0/pkg/cli/modes/listing_test.go000066400000000000000000000043641415471104000205730ustar00rootroot00000000000000package modes import ( "testing" "src.elv.sh/pkg/cli" . "src.elv.sh/pkg/cli/clitest" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/ui" ) func fooAndGreenBar(string) ([]ListingItem, int) { return []ListingItem{{"foo", ui.T("foo")}, {"bar", ui.T("bar", ui.FgGreen)}}, 0 } func TestListing_BasicUI(t *testing.T) { f := Setup() defer f.Stop() startListing(f.App, ListingSpec{ Caption: " TEST ", GetItems: fooAndGreenBar, }) f.TestTTY(t, "\n", " TEST ", Styles, "****** ", term.DotHere, "\n", "foo \n", Styles, "++++++++++++++++++++++++++++++++++++++++++++++++++", "bar ", Styles, "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv", ) } func TestListing_Accept_ClosingListing(t *testing.T) { f := Setup() defer f.Stop() startListing(f.App, ListingSpec{ GetItems: fooAndGreenBar, Accept: func(t string) { f.App.ActiveWidget().(tk.CodeArea).MutateState(func(s *tk.CodeAreaState) { s.Buffer.InsertAtDot(t) }) }, }) // foo will be selected f.TTY.Inject(term.K('\n')) f.TestTTY(t, "foo", term.DotHere) } func TestListing_Accept_DefaultNop(t *testing.T) { f := Setup() defer f.Stop() startListing(f.App, ListingSpec{GetItems: fooAndGreenBar}) f.TTY.Inject(term.K('\n')) f.TestTTY(t /* nothing */) } func TestListing_AutoAccept(t *testing.T) { f := Setup() defer f.Stop() startListing(f.App, ListingSpec{ GetItems: func(query string) ([]ListingItem, int) { if query == "" { // Return two items initially. return []ListingItem{ {"foo", ui.T("foo")}, {"bar", ui.T("bar")}, }, 0 } return []ListingItem{{"bar", ui.T("bar")}}, 0 }, Accept: func(t string) { f.App.ActiveWidget().(tk.CodeArea).MutateState(func(s *tk.CodeAreaState) { s.Buffer.InsertAtDot(t) }) }, AutoAccept: true, }) f.TTY.Inject(term.K('a')) f.TestTTY(t, "bar", term.DotHere) } func TestNewListing_NoGetItems(t *testing.T) { f := Setup() defer f.Stop() _, err := NewListing(f.App, ListingSpec{}) if err != errGetItemsMustBeSpecified { t.Error("expect errGetItemsMustBeSpecified") } } func startListing(app cli.App, spec ListingSpec) { w, err := NewListing(app, spec) startMode(app, w, err) } elvish-0.17.0/pkg/cli/modes/location.go000066400000000000000000000113271415471104000176700ustar00rootroot00000000000000package modes import ( "errors" "fmt" "math" "path/filepath" "regexp" "strings" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/fsutil" "src.elv.sh/pkg/store/storedefs" "src.elv.sh/pkg/ui" ) // Location is a mode for viewing location history and changing to a selected // directory. It is based on the ComboBox widget. type Location interface { tk.ComboBox } // LocationSpec is the configuration to start the location history feature. type LocationSpec struct { // Key bindings. Bindings tk.Bindings // Store provides the directory history and the function to change directory. Store LocationStore // IteratePinned specifies pinned directories by calling the given function // with all pinned directories. IteratePinned func(func(string)) // IterateHidden specifies hidden directories by calling the given function // with all hidden directories. IterateHidden func(func(string)) // IterateWorksapce specifies workspace configuration. IterateWorkspaces LocationWSIterator // Configuration for the filter. Filter FilterSpec } // LocationStore defines the interface for interacting with the directory history. type LocationStore interface { Dirs(blacklist map[string]struct{}) ([]storedefs.Dir, error) Chdir(dir string) error Getwd() (string, error) } // A special score for pinned directories. var pinnedScore = math.Inf(1) var errNoDirectoryHistoryStore = errors.New("no directory history store") // NewLocation creates a new location mode. func NewLocation(app cli.App, cfg LocationSpec) (Location, error) { if cfg.Store == nil { return nil, errNoDirectoryHistoryStore } dirs := []storedefs.Dir{} blacklist := map[string]struct{}{} wsKind, wsRoot := "", "" if cfg.IteratePinned != nil { cfg.IteratePinned(func(s string) { blacklist[s] = struct{}{} dirs = append(dirs, storedefs.Dir{Score: pinnedScore, Path: s}) }) } if cfg.IterateHidden != nil { cfg.IterateHidden(func(s string) { blacklist[s] = struct{}{} }) } wd, err := cfg.Store.Getwd() if err == nil { blacklist[wd] = struct{}{} if cfg.IterateWorkspaces != nil { wsKind, wsRoot = cfg.IterateWorkspaces.Parse(wd) } } storedDirs, err := cfg.Store.Dirs(blacklist) if err != nil { return nil, fmt.Errorf("db error: %v", err) } for _, dir := range storedDirs { if filepath.IsAbs(dir.Path) { dirs = append(dirs, dir) } else if wsKind != "" && hasPathPrefix(dir.Path, wsKind) { dirs = append(dirs, dir) } } l := locationList{dirs} w := tk.NewComboBox(tk.ComboBoxSpec{ CodeArea: tk.CodeAreaSpec{ Prompt: modePrompt(" LOCATION ", true), Highlighter: cfg.Filter.Highlighter, }, ListBox: tk.ListBoxSpec{ Bindings: cfg.Bindings, OnAccept: func(it tk.Items, i int) { path := it.(locationList).dirs[i].Path if strings.HasPrefix(path, wsKind) { path = wsRoot + path[len(wsKind):] } err := cfg.Store.Chdir(path) if err != nil { app.Notify(err.Error()) } app.PopAddon() }, }, OnFilter: func(w tk.ComboBox, p string) { w.ListBox().Reset(l.filter(cfg.Filter.makePredicate(p)), 0) }, }) return w, nil } func hasPathPrefix(path, prefix string) bool { return path == prefix || strings.HasPrefix(path, prefix+string(filepath.Separator)) } // LocationWSIterator is a function that iterates all workspaces by calling // the passed function with the name and pattern of each kind of workspace. // Iteration should stop when the called function returns false. type LocationWSIterator func(func(kind, pattern string) bool) // Parse returns whether the path matches any kind of workspace. If there is // a match, it returns the kind of the workspace and the root. It there is no // match, it returns "", "". func (ws LocationWSIterator) Parse(path string) (kind, root string) { var foundKind, foundRoot string ws(func(kind, pattern string) bool { if !strings.HasPrefix(pattern, "^") { pattern = "^" + pattern } re, err := regexp.Compile(pattern) if err != nil { // TODO(xiaq): Surface the error. return true } if root := re.FindString(path); root != "" { foundKind, foundRoot = kind, root return false } return true }) return foundKind, foundRoot } type locationList struct { dirs []storedefs.Dir } func (l locationList) filter(p func(string) bool) locationList { var filteredDirs []storedefs.Dir for _, dir := range l.dirs { if p(fsutil.TildeAbbr(dir.Path)) { filteredDirs = append(filteredDirs, dir) } } return locationList{filteredDirs} } func (l locationList) Show(i int) ui.Text { return ui.T(fmt.Sprintf("%s %s", showScore(l.dirs[i].Score), fsutil.TildeAbbr(l.dirs[i].Path))) } func (l locationList) Len() int { return len(l.dirs) } func showScore(f float64) string { if f == pinnedScore { return " *" } return fmt.Sprintf("%3.0f", f) } elvish-0.17.0/pkg/cli/modes/location_test.go000066400000000000000000000135161415471104000207310ustar00rootroot00000000000000package modes import ( "errors" "fmt" "path/filepath" "runtime" "strings" "testing" "time" "src.elv.sh/pkg/cli" . "src.elv.sh/pkg/cli/clitest" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/store/storedefs" "src.elv.sh/pkg/testutil" "src.elv.sh/pkg/ui" ) type locationStore struct { storedDirs []storedefs.Dir dirsError error chdir func(dir string) error wd string } func (ts locationStore) Dirs(blacklist map[string]struct{}) ([]storedefs.Dir, error) { dirs := []storedefs.Dir{} for _, dir := range ts.storedDirs { if _, ok := blacklist[dir.Path]; ok { continue } dirs = append(dirs, dir) } return dirs, ts.dirsError } func (ts locationStore) Chdir(dir string) error { if ts.chdir == nil { return nil } return ts.chdir(dir) } func (ts locationStore) Getwd() (string, error) { return ts.wd, nil } func TestNewLocation_NoStore(t *testing.T) { f := Setup() defer f.Stop() _, err := NewLocation(f.App, LocationSpec{}) if err != errNoDirectoryHistoryStore { t.Error("want errNoDirectoryHistoryStore") } } func TestNewLocation_StoreError(t *testing.T) { f := Setup() defer f.Stop() _, err := NewLocation(f.App, LocationSpec{Store: locationStore{dirsError: errors.New("ERROR")}}) if err.Error() != "db error: ERROR" { t.Error("want db error") } } func TestLocation_FullWorkflow(t *testing.T) { home := testutil.InTempHome(t) f := Setup() defer f.Stop() errChdir := errors.New("mock chdir error") chdirCh := make(chan string, 100) dirs := []storedefs.Dir{ {Path: filepath.Join(home, "go"), Score: 200}, {Path: home, Score: 100}, {Path: fixPath("/tmp/foo/bar/lorem/ipsum"), Score: 50}, } startLocation(f.App, LocationSpec{Store: locationStore{ storedDirs: dirs, chdir: func(dir string) error { chdirCh <- dir; return errChdir }, }}) // Test UI. wantBuf := locationBuf( "", "200 "+filepath.Join("~", "go"), "100 ~", " 50 "+fixPath("/tmp/foo/bar/lorem/ipsum")) f.TTY.TestBuffer(t, wantBuf) // Test filtering. f.TTY.Inject(term.K('f'), term.K('o')) wantBuf = locationBuf( "fo", " 50 "+fixPath("/tmp/foo/bar/lorem/ipsum")) f.TTY.TestBuffer(t, wantBuf) // Test accepting. f.TTY.Inject(term.K(ui.Enter)) // There should be no change to codearea after accepting. f.TestTTY(t /* nothing */) // Error from Chdir should be sent to notes. f.TestTTYNotes(t, "mock chdir error") // Chdir should be called. wantChdir := fixPath("/tmp/foo/bar/lorem/ipsum") select { case got := <-chdirCh: if got != wantChdir { t.Errorf("Chdir called with %s, want %s", got, wantChdir) } case <-time.After(testutil.Scaled(time.Second)): t.Errorf("Chdir not called") } } func TestLocation_Hidden(t *testing.T) { f := Setup() defer f.Stop() dirs := []storedefs.Dir{ {Path: fixPath("/usr/bin"), Score: 200}, {Path: fixPath("/usr"), Score: 100}, {Path: fixPath("/tmp"), Score: 50}, } startLocation(f.App, LocationSpec{ Store: locationStore{storedDirs: dirs}, IterateHidden: func(f func(string)) { f(fixPath("/usr")) }, }) // Test UI. wantBuf := locationBuf( "", "200 "+fixPath("/usr/bin"), " 50 "+fixPath("/tmp")) f.TTY.TestBuffer(t, wantBuf) } func TestLocation_Pinned(t *testing.T) { f := Setup() defer f.Stop() dirs := []storedefs.Dir{ {Path: fixPath("/usr/bin"), Score: 200}, {Path: fixPath("/usr"), Score: 100}, {Path: fixPath("/tmp"), Score: 50}, } startLocation(f.App, LocationSpec{ Store: locationStore{storedDirs: dirs}, IteratePinned: func(f func(string)) { f(fixPath("/home")); f(fixPath("/usr")) }, }) // Test UI. wantBuf := locationBuf( "", " * "+fixPath("/home"), " * "+fixPath("/usr"), "200 "+fixPath("/usr/bin"), " 50 "+fixPath("/tmp")) f.TTY.TestBuffer(t, wantBuf) } func TestLocation_HideWd(t *testing.T) { f := Setup() defer f.Stop() dirs := []storedefs.Dir{ {Path: fixPath("/home"), Score: 200}, {Path: fixPath("/tmp"), Score: 50}, } startLocation(f.App, LocationSpec{Store: locationStore{storedDirs: dirs, wd: fixPath("/home")}}) // Test UI. wantBuf := locationBuf( "", " 50 "+fixPath("/tmp")) f.TTY.TestBuffer(t, wantBuf) } func TestLocation_Workspace(t *testing.T) { f := Setup() defer f.Stop() chdir := "" dirs := []storedefs.Dir{ {Path: fixPath("home/src"), Score: 200}, {Path: fixPath("ws1/src"), Score: 150}, {Path: fixPath("ws2/bin"), Score: 100}, {Path: fixPath("/tmp"), Score: 50}, } startLocation(f.App, LocationSpec{ Store: locationStore{ storedDirs: dirs, wd: fixPath("/home/elf/bin"), chdir: func(dir string) error { chdir = dir return nil }, }, IterateWorkspaces: func(f func(kind, pattern string) bool) { if runtime.GOOS == "windows" { // Invalid patterns are ignored. f("ws1", `C:\\usr\\[^\\+`) f("home", `C:\\home\\[^\\]+`) f("ws2", `C:\\tmp\[^\]+`) } else { // Invalid patterns are ignored. f("ws1", "/usr/[^/+") f("home", "/home/[^/]+") f("ws2", "/tmp/[^/]+") } }, }) wantBuf := locationBuf( "", "200 "+fixPath("home/src"), " 50 "+fixPath("/tmp")) f.TTY.TestBuffer(t, wantBuf) f.TTY.Inject(term.K(ui.Enter)) f.TestTTY(t /* nothing */) wantChdir := fixPath("/home/elf/src") if chdir != wantChdir { t.Errorf("got chdir %q, want %q", chdir, wantChdir) } } func locationBuf(filter string, lines ...string) *term.Buffer { b := term.NewBufferBuilder(50). Newline(). // empty code area WriteStyled(modeLine(" LOCATION ", true)). Write(filter).SetDotHere() for i, line := range lines { b.Newline() if i == 0 { b.WriteStyled(ui.T(fmt.Sprintf("%-50s", line), ui.Inverse)) } else { b.Write(line) } } return b.Buffer() } func fixPath(path string) string { if runtime.GOOS != "windows" { return path } if path[0] == '/' { path = "C:" + path } return strings.ReplaceAll(path, "/", "\\") } func startLocation(app cli.App, spec LocationSpec) { w, err := NewLocation(app, spec) startMode(app, w, err) } elvish-0.17.0/pkg/cli/modes/mode.go000066400000000000000000000022221415471104000167760ustar00rootroot00000000000000// Package mode implements modes, which are widgets tailored for a specific // task. package modes import ( "errors" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/ui" ) // ErrFocusedWidgetNotCodeArea is returned when an operation requires the // focused widget to be a code area but it is not. var ErrFocusedWidgetNotCodeArea = errors.New("focused widget is not a code area") // FocusedCodeArea returns a CodeArea widget if the currently focused widget is // a CodeArea. Otherwise it returns the error ErrFocusedWidgetNotCodeArea. func FocusedCodeArea(a cli.App) (tk.CodeArea, error) { if w, ok := a.FocusedWidget().(tk.CodeArea); ok { return w, nil } return nil, ErrFocusedWidgetNotCodeArea } // Returns text styled as a modeline. func modeLine(content string, space bool) ui.Text { t := ui.T(content, ui.Bold, ui.FgWhite, ui.BgMagenta) if space { t = ui.Concat(t, ui.T(" ")) } return t } func modePrompt(content string, space bool) func() ui.Text { p := modeLine(content, space) return func() ui.Text { return p } } // Prompt returns a callback suitable as the prompt in the codearea of a // mode widget. var Prompt = modePrompt elvish-0.17.0/pkg/cli/modes/mode_test.go000066400000000000000000000024051415471104000200400ustar00rootroot00000000000000package modes import ( "errors" "testing" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/clitest" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/tt" "src.elv.sh/pkg/ui" ) func TestModeLine(t *testing.T) { testModeLine(t, tt.Fn("Line", modeLine)) } func TestModePrompt(t *testing.T) { testModeLine(t, tt.Fn("Prompt", func(s string, b bool) ui.Text { return modePrompt(s, b)() })) } func testModeLine(t *testing.T, fn *tt.FnToTest) { tt.Test(t, fn, tt.Table{ tt.Args("TEST", false).Rets( ui.T("TEST", ui.Bold, ui.FgWhite, ui.BgMagenta)), tt.Args("TEST", true).Rets( ui.Concat( ui.T("TEST", ui.Bold, ui.FgWhite, ui.BgMagenta), ui.T(" "))), }) } // Common test utilities. var errMock = errors.New("mock error") var withNonCodeAreaAddon = clitest.WithSpec(func(spec *cli.AppSpec) { spec.State.Addons = []tk.Widget{tk.Label{}} }) func startMode(app cli.App, w tk.Widget, err error) { if w != nil { app.PushAddon(w) app.Redraw() } if err != nil { app.Notify(err.Error()) } } func testFocusedWidgetNotCodeArea(t *testing.T, fn func(cli.App) error) { t.Helper() f := clitest.Setup(withNonCodeAreaAddon) defer f.Stop() if err := fn(f.App); err != ErrFocusedWidgetNotCodeArea { t.Errorf("should return ErrFocusedWidgetNotCodeArea, got %v", err) } } elvish-0.17.0/pkg/cli/modes/navigation.go000066400000000000000000000212531415471104000202160ustar00rootroot00000000000000package modes import ( "sort" "strings" "sync" "unicode" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/ui" ) type Navigation interface { tk.Widget // SelectedName returns the currently selected name. It returns an empty // string if there is no selected name, which can happen if the current // directory is empty. SelectedName() string // Select changes the selection. Select(f func(tk.ListBoxState) int) // ScrollPreview scrolls the preview. ScrollPreview(delta int) // Ascend ascends to the parent directory. Ascend() // Descend descends into the currently selected child directory. Descend() // MutateFiltering changes the filtering status. MutateFiltering(f func(bool) bool) // MutateShowHidden changes whether hidden files - files whose names start // with ".", should be shown. MutateShowHidden(f func(bool) bool) } // NavigationSpec specifieis the configuration for the navigation mode. type NavigationSpec struct { // Key bindings. Bindings tk.Bindings // Underlying filesystem. Cursor NavigationCursor // A function that returns the relative weights of the widths of the 3 // columns. If unspecified, the ratio is 1:3:4. WidthRatio func() [3]int // Configuration for the filter. Filter FilterSpec } type navigationState struct { Filtering bool ShowHidden bool } type navigation struct { NavigationSpec app cli.App attachedTo tk.CodeArea codeArea tk.CodeArea colView tk.ColView lastFilter string stateMutex sync.RWMutex state navigationState } func (w *navigation) MutateState(f func(*navigationState)) { w.stateMutex.Lock() defer w.stateMutex.Unlock() f(&w.state) } func (w *navigation) CopyState() navigationState { w.stateMutex.RLock() defer w.stateMutex.RUnlock() return w.state } func (w *navigation) Handle(event term.Event) bool { if w.colView.Handle(event) { return true } if w.CopyState().Filtering { if w.codeArea.Handle(event) { filter := w.codeArea.CopyState().Buffer.Content if filter != w.lastFilter { w.lastFilter = filter updateState(w, "") } return true } return false } return w.attachedTo.Handle(event) } func (w *navigation) Render(width, height int) *term.Buffer { buf := w.codeArea.Render(width, height) bufColView := w.colView.Render(width, height-len(buf.Lines)) buf.Extend(bufColView, false) return buf } func (w *navigation) MaxHeight(width, height int) int { return w.codeArea.MaxHeight(width, height) + w.colView.MaxHeight(width, height) } func (w *navigation) Focus() bool { return w.CopyState().Filtering } func (w *navigation) ascend() { // Remember the name of the current directory before ascending. currentName := "" current, err := w.Cursor.Current() if err == nil { currentName = current.Name() } err = w.Cursor.Ascend() if err != nil { w.app.Notify(err.Error()) } else { w.codeArea.MutateState(func(s *tk.CodeAreaState) { s.Buffer = tk.CodeBuffer{} }) updateState(w, currentName) } } func (w *navigation) descend() { currentCol, ok := w.colView.CopyState().Columns[1].(tk.ListBox) if !ok { return } state := currentCol.CopyState() if state.Items.Len() == 0 { return } selected := state.Items.(fileItems)[state.Selected] if !selected.IsDirDeep() { return } err := w.Cursor.Descend(selected.Name()) if err != nil { w.app.Notify(err.Error()) } else { w.codeArea.MutateState(func(s *tk.CodeAreaState) { s.Buffer = tk.CodeBuffer{} }) updateState(w, "") } } // NewNavigation creates a new navigation mode. func NewNavigation(app cli.App, spec NavigationSpec) (Navigation, error) { codeArea, err := FocusedCodeArea(app) if err != nil { return nil, err } if spec.Cursor == nil { spec.Cursor = NewOSNavigationCursor() } if spec.WidthRatio == nil { spec.WidthRatio = func() [3]int { return [3]int{1, 3, 4} } } var w *navigation w = &navigation{ NavigationSpec: spec, app: app, attachedTo: codeArea, codeArea: tk.NewCodeArea(tk.CodeAreaSpec{ Prompt: func() ui.Text { if w.CopyState().ShowHidden { return modeLine(" NAVIGATING (show hidden) ", true) } return modeLine(" NAVIGATING ", true) }, Highlighter: spec.Filter.Highlighter, }), colView: tk.NewColView(tk.ColViewSpec{ Bindings: spec.Bindings, Weights: func(int) []int { a := spec.WidthRatio() return a[:] }, OnLeft: func(tk.ColView) { w.ascend() }, OnRight: func(tk.ColView) { w.descend() }, }), } updateState(w, "") return w, nil } func (w *navigation) SelectedName() string { col, ok := w.colView.CopyState().Columns[1].(tk.ListBox) if !ok { return "" } state := col.CopyState() if 0 <= state.Selected && state.Selected < state.Items.Len() { return state.Items.(fileItems)[state.Selected].Name() } return "" } func updateState(w *navigation, selectName string) { colView := w.colView cursor := w.Cursor filter := w.lastFilter showHidden := w.CopyState().ShowHidden var parentCol, currentCol tk.Widget colView.MutateState(func(s *tk.ColViewState) { *s = tk.ColViewState{ Columns: []tk.Widget{ tk.Empty{}, tk.Empty{}, tk.Empty{}}, FocusColumn: 1, } }) parent, err := cursor.Parent() if err == nil { parentCol = makeCol(parent, showHidden) } else { parentCol = makeErrCol(err) } current, err := cursor.Current() if err == nil { currentCol = makeColInner( current, w.Filter.makePredicate(filter), showHidden, func(it tk.Items, i int) { previewCol := makeCol(it.(fileItems)[i], showHidden) colView.MutateState(func(s *tk.ColViewState) { s.Columns[2] = previewCol }) }) tryToSelectName(parentCol, current.Name()) if selectName != "" { tryToSelectName(currentCol, selectName) } } else { currentCol = makeErrCol(err) tryToSelectNothing(parentCol) } colView.MutateState(func(s *tk.ColViewState) { s.Columns[0] = parentCol s.Columns[1] = currentCol }) } // Selects nothing if the widget is a listbox. func tryToSelectNothing(w tk.Widget) { list, ok := w.(tk.ListBox) if !ok { return } list.Select(func(tk.ListBoxState) int { return -1 }) } // Selects the item with the given name, if the widget is a listbox with // fileItems and has such an item. func tryToSelectName(w tk.Widget, name string) { list, ok := w.(tk.ListBox) if !ok { // Do nothing return } list.Select(func(state tk.ListBoxState) int { items, ok := state.Items.(fileItems) if !ok { return 0 } for i, file := range items { if file.Name() == name { return i } } return 0 }) } func makeCol(f NavigationFile, showHidden bool) tk.Widget { return makeColInner(f, func(string) bool { return true }, showHidden, nil) } func makeColInner(f NavigationFile, filter func(string) bool, showHidden bool, onSelect func(tk.Items, int)) tk.Widget { files, content, err := f.Read() if err != nil { return makeErrCol(err) } if files != nil { var filtered []NavigationFile for _, file := range files { name := file.Name() hidden := len(name) > 0 && name[0] == '.' if filter(name) && (showHidden || !hidden) { filtered = append(filtered, file) } } files = filtered sort.Slice(files, func(i, j int) bool { return files[i].Name() < files[j].Name() }) return tk.NewListBox(tk.ListBoxSpec{ Padding: 1, ExtendStyle: true, OnSelect: onSelect, State: tk.ListBoxState{Items: fileItems(files)}, }) } lines := strings.Split(sanitize(string(content)), "\n") return tk.NewTextView(tk.TextViewSpec{ State: tk.TextViewState{Lines: lines}, Scrollable: true, }) } func makeErrCol(err error) tk.Widget { return tk.Label{Content: ui.T(err.Error(), ui.FgRed)} } type fileItems []NavigationFile func (it fileItems) Show(i int) ui.Text { return it[i].ShowName() } func (it fileItems) Len() int { return len(it) } func sanitize(content string) string { // Remove unprintable characters, and replace tabs with 4 spaces. var sb strings.Builder for _, r := range content { if r == '\t' { sb.WriteString(" ") } else if r == '\n' || unicode.IsGraphic(r) { sb.WriteRune(r) } } return sb.String() } func (w *navigation) Select(f func(tk.ListBoxState) int) { if listBox, ok := w.colView.CopyState().Columns[1].(tk.ListBox); ok { listBox.Select(f) } } func (w *navigation) ScrollPreview(delta int) { if textView, ok := w.colView.CopyState().Columns[2].(tk.TextView); ok { textView.ScrollBy(delta) } } func (w *navigation) Ascend() { w.colView.Left() } func (w *navigation) Descend() { w.colView.Right() } func (w *navigation) MutateFiltering(f func(bool) bool) { w.MutateState(func(s *navigationState) { s.Filtering = f(s.Filtering) }) } func (w *navigation) MutateShowHidden(f func(bool) bool) { w.MutateState(func(s *navigationState) { s.ShowHidden = f(s.ShowHidden) }) updateState(w, w.SelectedName()) } elvish-0.17.0/pkg/cli/modes/navigation_fs.go000066400000000000000000000103451415471104000207060ustar00rootroot00000000000000package modes import ( "errors" "io" "os" "path/filepath" "unicode/utf8" "src.elv.sh/pkg/cli/lscolors" "src.elv.sh/pkg/ui" ) // NavigationCursor represents a cursor for navigating in a potentially virtual // filesystem. type NavigationCursor interface { // Current returns a File that represents the current directory. Current() (NavigationFile, error) // Parent returns a File that represents the parent directory. It may return // nil if the current directory is the root of the filesystem. Parent() (NavigationFile, error) // Ascend navigates to the parent directory. Ascend() error // Descend navigates to the named child directory. Descend(name string) error } // NavigationFile represents a potentially virtual file. type NavigationFile interface { // Name returns the name of the file. Name() string // ShowName returns a styled filename. ShowName() ui.Text // IsDirDeep returns whether the file is itself a directory or a symlink to // a directory. IsDirDeep() bool // Read returns either a list of File's if the File represents a directory, // a (possibly incomplete) slice of bytes if the File represents a normal // file, or an error if the File cannot be read. Read() ([]NavigationFile, []byte, error) } // NewOSNavigationCursor returns a NavigationCursor backed by the OS. func NewOSNavigationCursor() NavigationCursor { return osCursor{lscolors.GetColorist()} } type osCursor struct{ colorist lscolors.Colorist } func (c osCursor) Current() (NavigationFile, error) { abs, err := filepath.Abs(".") if err != nil { return nil, err } return file{filepath.Base(abs), abs, os.ModeDir, c.colorist}, nil } func (c osCursor) Parent() (NavigationFile, error) { if abs, _ := filepath.Abs("."); abs == "/" { return emptyDir{}, nil } abs, err := filepath.Abs("..") if err != nil { return nil, err } return file{filepath.Base(abs), abs, os.ModeDir, c.colorist}, nil } func (c osCursor) Ascend() error { return os.Chdir("..") } func (c osCursor) Descend(name string) error { return os.Chdir(name) } type emptyDir struct{} func (emptyDir) Name() string { return "" } func (emptyDir) ShowName() ui.Text { return nil } func (emptyDir) IsDirDeep() bool { return true } func (emptyDir) Read() ([]NavigationFile, []byte, error) { return []NavigationFile{}, nil, nil } type file struct { name string path string mode os.FileMode colorist lscolors.Colorist } func (f file) Name() string { return f.name } func (f file) ShowName() ui.Text { sgrStyle := f.colorist.GetStyle(f.path) return ui.Text{&ui.Segment{ Style: ui.StyleFromSGR(sgrStyle), Text: f.name}} } func (f file) IsDirDeep() bool { if f.mode.IsDir() { // File itself is a directory; return true and save a stat call. return true } info, err := os.Stat(f.path) return err == nil && info.IsDir() } const previewBytes = 64 * 1024 var ( errDevice = errors.New("no preview for device file") errNamedPipe = errors.New("no preview for named pipe") errSocket = errors.New("no preview for socket file") errCharDevice = errors.New("no preview for char device") errNonUTF8 = errors.New("no preview for non-utf8 file") ) var specialFileModes = []struct { mode os.FileMode err error }{ {os.ModeDevice, errDevice}, {os.ModeNamedPipe, errNamedPipe}, {os.ModeSocket, errSocket}, {os.ModeCharDevice, errCharDevice}, } func (f file) Read() ([]NavigationFile, []byte, error) { ff, err := os.Open(f.path) if err != nil { return nil, nil, err } defer ff.Close() info, err := ff.Stat() if err != nil { return nil, nil, err } if info.IsDir() { infos, err := ff.Readdir(0) if err != nil { return nil, nil, err } files := make([]NavigationFile, len(infos)) for i, info := range infos { files[i] = file{ info.Name(), filepath.Join(f.path, info.Name()), info.Mode(), f.colorist, } } return files, nil, err } for _, special := range specialFileModes { if info.Mode()&special.mode != 0 { return nil, nil, special.err } } var buf [previewBytes]byte nr, err := ff.Read(buf[:]) if err != nil && err != io.EOF { return nil, nil, err } content := buf[:nr] if !utf8.Valid(content) { return nil, nil, errNonUTF8 } return nil, content, nil } elvish-0.17.0/pkg/cli/modes/navigation_fs_test.go000066400000000000000000000046471415471104000217550ustar00rootroot00000000000000package modes import ( "errors" "strings" "src.elv.sh/pkg/testutil" "src.elv.sh/pkg/ui" ) var ( errCannotCd = errors.New("cannot cd") errNoSuchFile = errors.New("no such file") errNoSuchDir = errors.New("no such directory") ) type testCursor struct { root testutil.Dir pwd []string currentErr, parentErr, ascendErr, descendErr error } func (c *testCursor) Current() (NavigationFile, error) { if c.currentErr != nil { return nil, c.currentErr } return getDirFile(c.root, c.pwd) } func (c *testCursor) Parent() (NavigationFile, error) { if c.parentErr != nil { return nil, c.parentErr } parent := c.pwd if len(parent) > 0 { parent = parent[:len(parent)-1] } return getDirFile(c.root, parent) } func (c *testCursor) Ascend() error { if c.ascendErr != nil { return c.ascendErr } if len(c.pwd) > 0 { c.pwd = c.pwd[:len(c.pwd)-1] } return nil } func (c *testCursor) Descend(name string) error { if c.descendErr != nil { return c.descendErr } pwdCopy := append([]string{}, c.pwd...) childPath := append(pwdCopy, name) if _, err := getDirFile(c.root, childPath); err == nil { c.pwd = childPath return nil } return errCannotCd } func getFile(root testutil.Dir, path []string) (NavigationFile, error) { var f interface{} = root for _, p := range path { d, ok := f.(testutil.Dir) if !ok { return nil, errNoSuchFile } f = d[p] } name := "" if len(path) > 0 { name = path[len(path)-1] } return testFile{name, f}, nil } func getDirFile(root testutil.Dir, path []string) (NavigationFile, error) { f, err := getFile(root, path) if err != nil { return nil, err } if !f.IsDirDeep() { return nil, errNoSuchDir } return f, nil } type testFile struct { name string data interface{} } func (f testFile) Name() string { return f.name } func (f testFile) ShowName() ui.Text { // The style matches that of LS_COLORS in the test code. switch { case f.IsDirDeep(): return ui.T(f.name, ui.FgBlue) case strings.HasSuffix(f.name, ".png"): return ui.T(f.name, ui.FgRed) default: return ui.T(f.name) } } func (f testFile) IsDirDeep() bool { _, ok := f.data.(testutil.Dir) return ok } func (f testFile) Read() ([]NavigationFile, []byte, error) { if dir, ok := f.data.(testutil.Dir); ok { files := make([]NavigationFile, 0, len(dir)) for name, data := range dir { files = append(files, testFile{name, data}) } return files, nil, nil } return nil, []byte(f.data.(string)), nil } elvish-0.17.0/pkg/cli/modes/navigation_test.go000066400000000000000000000201001415471104000212430ustar00rootroot00000000000000package modes import ( "errors" "testing" "src.elv.sh/pkg/cli" . "src.elv.sh/pkg/cli/clitest" "src.elv.sh/pkg/cli/lscolors" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/testutil" "src.elv.sh/pkg/ui" ) var testDir = testutil.Dir{ "a": "", "d": testutil.Dir{ "d1": "content\td1\nline 2", "d2": testutil.Dir{ "d21": "content d21", "d22": "content d22", "d23.png": "", }, "d3": testutil.Dir{}, ".dh": "hidden", }, "f": "", } func TestErrorInAscend(t *testing.T) { f := Setup() defer f.Stop() c := getTestCursor() c.ascendErr = errors.New("cannot ascend") startNavigation(f.App, NavigationSpec{Cursor: c}) f.TTY.Inject(term.K(ui.Left)) f.TestTTYNotes(t, "cannot ascend") } func TestErrorInDescend(t *testing.T) { f := Setup() defer f.Stop() c := getTestCursor() c.descendErr = errors.New("cannot descend") startNavigation(f.App, NavigationSpec{Cursor: c}) f.TTY.Inject(term.K(ui.Down)) f.TTY.Inject(term.K(ui.Right)) f.TestTTYNotes(t, "cannot descend") } func TestErrorInCurrent(t *testing.T) { f := setupNav(t) defer f.Stop() c := getTestCursor() c.currentErr = errors.New("ERR") startNavigation(f.App, NavigationSpec{Cursor: c}) f.TestTTY(t, "", term.DotHere, "\n", " NAVIGATING \n", Styles, "************ ", " a ERR \n", Styles, " !!!", " d \n", Styles, "////", " f ", ) // Test that Right does nothing. f.TTY.Inject(term.K(ui.Right)) // We can't just test that the buffer hasn't changed, because that might // capture the state of the buffer before the Right key is handled. Instead // we inject a key and test the result of that instead, to ensure that the // Right key had no effect. f.TTY.Inject(term.K('x')) f.TestTTY(t, "x", term.DotHere, "\n", " NAVIGATING \n", Styles, "************ ", " a ERR \n", Styles, " !!!", " d \n", Styles, "////", " f ", ) } func TestErrorInParent(t *testing.T) { f := setupNav(t) defer f.Stop() c := getTestCursor() c.parentErr = errors.New("ERR") startNavigation(f.App, NavigationSpec{Cursor: c}) f.TestTTY(t, "", term.DotHere, "\n", " NAVIGATING \n", Styles, "************ ", "ERR d1 content d1\n", Styles, "!!! ++++++++++++++", " d2 line 2\n", Styles, " //////////////", " d3 ", Styles, " //////////////", ) } func TestWidthRatio(t *testing.T) { f := setupNav(t) defer f.Stop() c := getTestCursor() startNavigation(f.App, NavigationSpec{ Cursor: c, WidthRatio: func() [3]int { return [3]int{1, 1, 1} }, }) f.TestTTY(t, "", term.DotHere, "\n", " NAVIGATING \n", Styles, "************ ", " a d1 content d1\n", Styles, " +++++++++++++", " d d2 line 2\n", Styles, "############ /////////////", " f d3 ", Styles, " /////////////", ) } func TestGetSelectedName(t *testing.T) { f := Setup() defer f.Stop() w := startNavigation(f.App, NavigationSpec{Cursor: getTestCursor()}) wantName := "d1" if name := w.SelectedName(); name != wantName { t.Errorf("Got name %q, want %q", name, wantName) } } func TestNavigation_FakeFS(t *testing.T) { cursor := getTestCursor() testNavigation(t, cursor) } func TestNavigation_RealFS(t *testing.T) { testutil.InTempDir(t) testutil.ApplyDir(testDir) testutil.MustChdir("d") testNavigation(t, nil) } func testNavigation(t *testing.T, c NavigationCursor) { f := setupNav(t) defer f.Stop() w := startNavigation(f.App, NavigationSpec{Cursor: c}) // Test initial UI and file preview. // NOTE: Buffers are named after the file that is now being selected. d1Buf := f.MakeBuffer( "", term.DotHere, "\n", " NAVIGATING \n", Styles, "************ ", " a d1 content d1\n", Styles, " ++++++++++++++", " d d2 line 2\n", Styles, "#### //////////////", " f d3 ", Styles, " //////////////", ) f.TTY.TestBuffer(t, d1Buf) // Test scrolling of preview. w.ScrollPreview(1) f.App.Redraw() d1Buf2 := f.MakeBuffer( "", term.DotHere, "\n", " NAVIGATING \n", Styles, "************ ", " a d1 line 2 │\n", Styles, " ++++++++++++++ -", " d d2 │\n", Styles, "#### ////////////// -", " f d3 ", Styles, " ////////////// X", ) f.TTY.TestBuffer(t, d1Buf2) // Test handling of selection change and directory preview. Also test // LS_COLORS. w.Select(tk.Next) f.App.Redraw() d2Buf := f.MakeBuffer( "", term.DotHere, "\n", " NAVIGATING \n", Styles, "************ ", " a d1 d21 \n", Styles, " ++++++++++++++++++++", " d d2 d22 \n", Styles, "#### ##############", " f d3 d23.png ", Styles, " ////////////// !!!!!!!!!!!!!!!!!!!!", ) f.TTY.TestBuffer(t, d2Buf) // Test handling of Descend. w.Descend() f.App.Redraw() d21Buf := f.MakeBuffer( "", term.DotHere, "\n", " NAVIGATING \n", Styles, "************ ", " d1 d21 content d21\n", Styles, " ++++++++++++++", " d2 d22 \n", Styles, "####", " d3 d23.png ", Styles, "//// !!!!!!!!!!!!!!", ) f.TTY.TestBuffer(t, d21Buf) // Test handling of Ascend, and that the current column selects the // directory we just ascended from, thus reverting to wantBuf1. w.Ascend() f.App.Redraw() f.TTY.TestBuffer(t, d2Buf) // Test handling of Descend on a regular file, i.e. do nothing. First move // the cursor to d1, which is a regular file. w.Select(tk.Prev) f.App.Redraw() f.TTY.TestBuffer(t, d1Buf) // Now descend, and verify that the buffer has not changed. w.Descend() f.App.Redraw() f.TTY.TestBuffer(t, d1Buf) // Test showing hidden. w.MutateShowHidden(func(bool) bool { return true }) f.App.Redraw() f.TestTTY(t, "", term.DotHere, "\n", " NAVIGATING (show hidden) \n", Styles, "************************** ", " a .dh content d1\n", " d d1 line 2\n", Styles, "#### ++++++++++++++", " f d2 \n", Styles, " //////////////", " d3 ", Styles, " //////////////", ) w.MutateShowHidden(func(bool) bool { return false }) // Test filtering; current column shows d1, d2, d3 before filtering. w.MutateFiltering(func(bool) bool { return true }) f.TTY.Inject(term.K('3')) f.TestTTY(t, "\n", " NAVIGATING 3", Styles, "************ ", term.DotHere, "\n", " a d3 \n", Styles, " ##############", " d \n", Styles, "####", " f ", ) w.MutateFiltering(func(bool) bool { return false }) // Now move into d3, an empty directory. Test that the filter has been // cleared. w.Select(tk.Next) w.Select(tk.Next) w.Descend() f.App.Redraw() d3NoneBuf := f.MakeBuffer( "", term.DotHere, "\n", " NAVIGATING \n", Styles, "************ ", " d1 \n", " d2 \n", Styles, "////", " d3 ", Styles, "####", ) f.TTY.TestBuffer(t, d3NoneBuf) // Test that selecting the previous does nothing in an empty directory. w.Select(tk.Prev) f.App.Redraw() f.TTY.TestBuffer(t, d3NoneBuf) // Test that selecting the next does nothing in an empty directory. w.Select(tk.Next) f.App.Redraw() f.TTY.TestBuffer(t, d3NoneBuf) // Test that Descend does nothing in an empty directory. w.Descend() f.App.Redraw() f.TTY.TestBuffer(t, d3NoneBuf) } func TestNewNavigation_FocusedWidgetNotCodeArea(t *testing.T) { testFocusedWidgetNotCodeArea(t, func(app cli.App) error { _, err := NewNavigation(app, NavigationSpec{}) return err }) } func setupNav(c testutil.Cleanuper) *Fixture { lscolors.SetTestLsColors(c) // Use a small TTY size to make the test buffer easier to build. return Setup(WithTTY(func(tty TTYCtrl) { tty.SetSize(6, 40) })) } func startNavigation(app cli.App, spec NavigationSpec) Navigation { w, _ := NewNavigation(app, spec) startMode(app, w, nil) return w } func getTestCursor() *testCursor { return &testCursor{root: testDir, pwd: []string{"d"}} } elvish-0.17.0/pkg/cli/modes/stub.go000066400000000000000000000021311415471104000170260ustar00rootroot00000000000000package modes import ( "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/cli/tk" ) // Stub is a mode that just shows a modeline and keeps the focus on the code // area. It is mainly useful to apply some special non-default bindings. type Stub interface { tk.Widget } // StubSpec specifies the configuration for the stub mode. type StubSpec struct { // Key bindings. Bindings tk.Bindings // Name to show in the modeline. Name string } type stub struct { StubSpec } func (w stub) Render(width, height int) *term.Buffer { buf := w.render(width) buf.TrimToLines(0, height) return buf } func (w stub) MaxHeight(width, height int) int { return len(w.render(width).Lines) } func (w stub) render(width int) *term.Buffer { return term.NewBufferBuilder(width). WriteStyled(modeLine(w.Name, false)).SetDotHere().Buffer() } func (w stub) Handle(event term.Event) bool { return w.Bindings.Handle(w, event) } func (w stub) Focus() bool { return false } // NewStub creates a new Stub mode. func NewStub(cfg StubSpec) Stub { if cfg.Bindings == nil { cfg.Bindings = tk.DummyBindings{} } return stub{cfg} } elvish-0.17.0/pkg/cli/modes/stub_test.go000066400000000000000000000014631415471104000200740ustar00rootroot00000000000000package modes import ( "testing" "time" "src.elv.sh/pkg/cli" . "src.elv.sh/pkg/cli/clitest" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/cli/tk" ) func TestStub_Rendering(t *testing.T) { f := Setup() defer f.Stop() startStub(f.App, StubSpec{Name: " STUB "}) f.TestTTY(t, "", term.DotHere, "\n", " STUB ", Styles, "******", ) } func TestStub_Handling(t *testing.T) { f := Setup() defer f.Stop() bindingCalled := make(chan bool) startStub(f.App, StubSpec{ Bindings: tk.MapBindings{ term.K('a'): func(tk.Widget) { bindingCalled <- true }}, }) f.TTY.Inject(term.K('a')) select { case <-bindingCalled: // OK case <-time.After(time.Second): t.Errorf("Handler not called after 1s") } } func startStub(app cli.App, spec StubSpec) { w := NewStub(spec) app.PushAddon(w) app.Redraw() } elvish-0.17.0/pkg/cli/prompt/000077500000000000000000000000001415471104000157375ustar00rootroot00000000000000elvish-0.17.0/pkg/cli/prompt/prompt.go000066400000000000000000000066241415471104000176170ustar00rootroot00000000000000// Package prompt provides an implementation of the cli.Prompt interface. package prompt import ( "os" "sync" "time" "src.elv.sh/pkg/ui" ) // Prompt implements a prompt that is executed asynchronously. type Prompt struct { config Config // Working directory when prompt was last updated. lastWd string // Channel for update requests. updateReq chan struct{} // Channel on which prompt contents are delivered. ch chan struct{} // Last computed prompt content. last ui.Text // Mutex for guarding access to the last field. lastMutex sync.RWMutex } // Config keeps configurations for the prompt. type Config struct { // The function that computes the prompt. Compute func() ui.Text // Function to transform stale prompts. StaleTransform func(ui.Text) ui.Text // Threshold for a prompt to be considered as stale. StaleThreshold func() time.Duration // How eager the prompt should be updated. When >= 5, updated when directory // is changed. When >= 10, always update. Default is 5. Eagerness func() int } func defaultStaleTransform(t ui.Text) ui.Text { return ui.StyleText(t, ui.Inverse) } const defaultStaleThreshold = 200 * time.Millisecond const defaultEagerness = 5 var unknownContent = ui.T("???> ") // New makes a new prompt. func New(cfg Config) *Prompt { if cfg.Compute == nil { cfg.Compute = func() ui.Text { return unknownContent } } if cfg.StaleTransform == nil { cfg.StaleTransform = defaultStaleTransform } if cfg.StaleThreshold == nil { cfg.StaleThreshold = func() time.Duration { return defaultStaleThreshold } } if cfg.Eagerness == nil { cfg.Eagerness = func() int { return defaultEagerness } } p := &Prompt{ cfg, "", make(chan struct{}, 1), make(chan struct{}, 1), unknownContent, sync.RWMutex{}} // TODO: Don't keep a goroutine running. go p.loop() return p } func (p *Prompt) loop() { content := unknownContent ch := make(chan ui.Text) for range p.updateReq { go func() { ch <- p.config.Compute() }() select { case <-time.After(p.config.StaleThreshold()): // The prompt callback did not finish within the threshold. Send the // previous content, marked as stale. p.update(p.config.StaleTransform(content)) content = <-ch select { case <-p.updateReq: // If another update is already requested by the time we finish, // keep marking the prompt as stale. This reduces flickering. p.update(p.config.StaleTransform(content)) p.queueUpdate() default: p.update(content) } case content = <-ch: p.update(content) } } } // Trigger triggers an update to the prompt. func (p *Prompt) Trigger(force bool) { if force || p.shouldUpdate() { p.queueUpdate() } } // Get returns the current content of the prompt. func (p *Prompt) Get() ui.Text { p.lastMutex.RLock() defer p.lastMutex.RUnlock() return p.last } // LateUpdates returns a channel on which late updates are made available. func (p *Prompt) LateUpdates() <-chan struct{} { return p.ch } func (p *Prompt) queueUpdate() { select { case p.updateReq <- struct{}{}: default: } } func (p *Prompt) update(content ui.Text) { p.lastMutex.Lock() p.last = content p.lastMutex.Unlock() p.ch <- struct{}{} } func (p *Prompt) shouldUpdate() bool { eagerness := p.config.Eagerness() if eagerness >= 10 { return true } if eagerness >= 5 { wd, err := os.Getwd() if err != nil { wd = "error" } oldWd := p.lastWd p.lastWd = wd return wd != oldWd } return false } elvish-0.17.0/pkg/cli/prompt/prompt_test.go000066400000000000000000000104431415471104000206500ustar00rootroot00000000000000package prompt import ( "fmt" "reflect" "testing" "time" "src.elv.sh/pkg/testutil" "src.elv.sh/pkg/ui" ) func TestPrompt_DefaultCompute(t *testing.T) { prompt := New(Config{}) prompt.Trigger(false) testUpdate(t, prompt, ui.T("???> ")) } func TestPrompt_ShowsComputedPrompt(t *testing.T) { prompt := New(Config{ Compute: func() ui.Text { return ui.T(">>> ") }}) prompt.Trigger(false) testUpdate(t, prompt, ui.T(">>> ")) } func TestPrompt_StalePrompt(t *testing.T) { compute, unblock := blockedAutoIncPrompt() prompt := New(Config{ Compute: compute, StaleThreshold: func() time.Duration { return testutil.Scaled(10 * time.Millisecond) }, }) prompt.Trigger(true) // The compute function is blocked, so a stale version of the initial // "unknown" prompt will be shown. testUpdate(t, prompt, ui.T("???> ", ui.Inverse)) // The compute function will now return. unblock() // The returned prompt will now be used. testUpdate(t, prompt, ui.T("1> ")) // Force a refresh. prompt.Trigger(true) // The compute function will now be blocked again, so after a while a stale // version of the previous prompt will be shown. testUpdate(t, prompt, ui.T("1> ", ui.Inverse)) // Unblock the compute function. unblock() // The new prompt will now be shown. testUpdate(t, prompt, ui.T("2> ")) // Force a refresh. prompt.Trigger(true) // Make sure that the compute function is run and stuck. testUpdate(t, prompt, ui.T("2> ", ui.Inverse)) // Queue another two refreshes before the compute function can return. prompt.Trigger(true) prompt.Trigger(true) unblock() // Now the new prompt should be marked stale immediately. testUpdate(t, prompt, ui.T("3> ", ui.Inverse)) unblock() // However, the the two refreshes we requested early only trigger one // re-computation, because they are requested while the compute function is // stuck, so they can be safely merged. testUpdate(t, prompt, ui.T("4> ")) } func TestPrompt_Eagerness0(t *testing.T) { prompt := New(Config{ Compute: autoIncPrompt(), Eagerness: func() int { return 0 }, }) // A forced refresh is always respected. prompt.Trigger(true) testUpdate(t, prompt, ui.T("1> ")) // A unforced refresh is not respected. prompt.Trigger(false) testNoUpdate(t, prompt) // No update even if pwd has changed. testutil.InTempDir(t) prompt.Trigger(false) testNoUpdate(t, prompt) // Only force updates are respected. prompt.Trigger(true) testUpdate(t, prompt, ui.T("2> ")) } func TestPrompt_Eagerness5(t *testing.T) { prompt := New(Config{ Compute: autoIncPrompt(), Eagerness: func() int { return 5 }, }) // The initial trigger is respected because there was no previous pwd. prompt.Trigger(false) testUpdate(t, prompt, ui.T("1> ")) // No update because the pwd has not changed. prompt.Trigger(false) testNoUpdate(t, prompt) // Update because the pwd has changed. testutil.InTempDir(t) prompt.Trigger(false) testUpdate(t, prompt, ui.T("2> ")) } func TestPrompt_Eagerness10(t *testing.T) { prompt := New(Config{ Compute: autoIncPrompt(), Eagerness: func() int { return 10 }, }) // The initial trigger is respected. prompt.Trigger(false) testUpdate(t, prompt, ui.T("1> ")) // Subsequent triggers, force or not, are also respected. prompt.Trigger(false) testUpdate(t, prompt, ui.T("2> ")) prompt.Trigger(true) testUpdate(t, prompt, ui.T("3> ")) prompt.Trigger(false) testUpdate(t, prompt, ui.T("4> ")) } func blockedAutoIncPrompt() (func() ui.Text, func()) { unblockChan := make(chan struct{}) i := 0 compute := func() ui.Text { <-unblockChan i++ return ui.T(fmt.Sprintf("%d> ", i)) } unblock := func() { unblockChan <- struct{}{} } return compute, unblock } func autoIncPrompt() func() ui.Text { i := 0 return func() ui.Text { i++ return ui.T(fmt.Sprintf("%d> ", i)) } } func testUpdate(t *testing.T, p *Prompt, wantUpdate ui.Text) { t.Helper() select { case <-p.LateUpdates(): update := p.Get() if !reflect.DeepEqual(update, wantUpdate) { t.Errorf("got updated %v, want %v", update, wantUpdate) } case <-time.After(time.Second): t.Errorf("no late update after 1 second") } } func testNoUpdate(t *testing.T, p *Prompt) { t.Helper() select { case update := <-p.LateUpdates(): t.Errorf("unexpected update %v", update) case <-time.After(testutil.Scaled(10 * time.Millisecond)): // OK } } elvish-0.17.0/pkg/cli/term/000077500000000000000000000000001415471104000153655ustar00rootroot00000000000000elvish-0.17.0/pkg/cli/term/buffer.go000066400000000000000000000110111415471104000171570ustar00rootroot00000000000000package term import ( "fmt" "strings" "src.elv.sh/pkg/wcwidth" ) // Cell is an indivisible unit on the screen. It is not necessarily 1 column // wide. type Cell struct { Text string Style string } // Pos is a line/column position. type Pos struct { Line, Col int } // CellsWidth returns the total width of a Cell slice. func CellsWidth(cs []Cell) int { w := 0 for _, c := range cs { w += wcwidth.Of(c.Text) } return w } // CompareCells returns whether two Cell slices are equal, and when they are // not, the first index at which they differ. func CompareCells(r1, r2 []Cell) (bool, int) { for i, c := range r1 { if i >= len(r2) || c != r2[i] { return false, i } } if len(r1) < len(r2) { return false, len(r1) } return true, 0 } // Buffer reflects a continuous range of lines on the terminal. // // The Unix terminal API provides only awkward ways of querying the terminal // Buffer, so we keep an internal reflection and do one-way synchronizations // (Buffer -> terminal, and not the other way around). This requires us to // exactly match the terminal's idea of the width of characters (wcwidth) and // where to insert soft carriage returns, so there could be bugs. type Buffer struct { Width int // Lines the content of the buffer. Lines Lines // Dot is what the user perceives as the cursor. Dot Pos } // Lines stores multiple lines. type Lines [][]Cell // Line stores a single line. type Line []Cell // NewBuffer builds a new buffer, with one empty line. func NewBuffer(width int) *Buffer { return &Buffer{Width: width, Lines: [][]Cell{make([]Cell, 0, width)}} } // Col returns the column the cursor is in. func (b *Buffer) Col() int { return CellsWidth(b.Lines[len(b.Lines)-1]) } // Cursor returns the current position of the cursor. func (b *Buffer) Cursor() Pos { return Pos{len(b.Lines) - 1, b.Col()} } // BuffersHeight computes the combined height of a number of buffers. func BuffersHeight(bufs ...*Buffer) (l int) { for _, buf := range bufs { if buf != nil { l += len(buf.Lines) } } return } // TrimToLines trims a buffer to the lines [low, high). func (b *Buffer) TrimToLines(low, high int) { if low < 0 { low = 0 } if high > len(b.Lines) { high = len(b.Lines) } for i := 0; i < low; i++ { b.Lines[i] = nil } for i := high; i < len(b.Lines); i++ { b.Lines[i] = nil } b.Lines = b.Lines[low:high] b.Dot.Line -= low if b.Dot.Line < 0 { b.Dot.Line = 0 } } // Extend adds all lines from b2 to the bottom of this buffer. If moveDot is // true, the dot is updated to match the dot of b2. func (b *Buffer) Extend(b2 *Buffer, moveDot bool) { if b2 != nil && b2.Lines != nil { if moveDot { b.Dot.Line = b2.Dot.Line + len(b.Lines) b.Dot.Col = b2.Dot.Col } b.Lines = append(b.Lines, b2.Lines...) } } // ExtendRight extends bb to the right. It pads each line in b to be b.Width and // appends the corresponding line in b2 to it, making new lines when b2 has more // lines than bb. func (b *Buffer) ExtendRight(b2 *Buffer) { i := 0 w := b.Width b.Width += b2.Width for ; i < len(b.Lines) && i < len(b2.Lines); i++ { if w0 := CellsWidth(b.Lines[i]); w0 < w { b.Lines[i] = append(b.Lines[i], makeSpacing(w-w0)...) } b.Lines[i] = append(b.Lines[i], b2.Lines[i]...) } for ; i < len(b2.Lines); i++ { row := append(makeSpacing(w), b2.Lines[i]...) b.Lines = append(b.Lines, row) } } // Buffer returns itself. func (b *Buffer) Buffer() *Buffer { return b } // TTYString returns a string for representing the buffer on the terminal. func (b *Buffer) TTYString() string { if b == nil { return "nil" } sb := new(strings.Builder) fmt.Fprintf(sb, "Width = %d, Dot = (%d, %d)\n", b.Width, b.Dot.Line, b.Dot.Col) // Top border sb.WriteString("┌" + strings.Repeat("─", b.Width) + "┐\n") for _, line := range b.Lines { // Left border sb.WriteRune('│') // Content lastStyle := "" usedWidth := 0 for _, cell := range line { if cell.Style != lastStyle { switch { case lastStyle == "": sb.WriteString("\033[" + cell.Style + "m") case cell.Style == "": sb.WriteString("\033[m") default: sb.WriteString("\033[;" + cell.Style + "m") } lastStyle = cell.Style } sb.WriteString(cell.Text) usedWidth += wcwidth.Of(cell.Text) } if lastStyle != "" { sb.WriteString("\033[m") } if usedWidth < b.Width { sb.WriteString("$" + strings.Repeat(" ", b.Width-usedWidth-1)) } // Right border and newline sb.WriteString("│\n") } // Bottom border sb.WriteString("└" + strings.Repeat("─", b.Width) + "┘\n") return sb.String() } elvish-0.17.0/pkg/cli/term/buffer_builder.go000066400000000000000000000077511415471104000207050ustar00rootroot00000000000000package term import ( "strings" "src.elv.sh/pkg/ui" "src.elv.sh/pkg/wcwidth" ) // BufferBuilder supports building of Buffer. type BufferBuilder struct { Width, Col, Indent int // EagerWrap controls whether to wrap line as soon as the cursor reaches the // right edge of the terminal. This is not often desirable as it creates // unneessary line breaks, but is is useful when echoing the user input. // will otherwise EagerWrap bool // Lines the content of the buffer. Lines Lines // Dot is what the user perceives as the cursor. Dot Pos } // NewBufferBuilder makes a new BufferBuilder, initially with one empty line. func NewBufferBuilder(width int) *BufferBuilder { return &BufferBuilder{Width: width, Lines: [][]Cell{make([]Cell, 0, width)}} } func (bb *BufferBuilder) Cursor() Pos { return Pos{len(bb.Lines) - 1, bb.Col} } // Buffer returns a Buffer built by the BufferBuilder. func (bb *BufferBuilder) Buffer() *Buffer { return &Buffer{bb.Width, bb.Lines, bb.Dot} } func (bb *BufferBuilder) SetIndent(indent int) *BufferBuilder { bb.Indent = indent return bb } func (bb *BufferBuilder) SetEagerWrap(v bool) *BufferBuilder { bb.EagerWrap = v return bb } func (bb *BufferBuilder) setDot(dot Pos) *BufferBuilder { bb.Dot = dot return bb } func (bb *BufferBuilder) SetDotHere() *BufferBuilder { return bb.setDot(bb.Cursor()) } func (bb *BufferBuilder) appendLine() { bb.Lines = append(bb.Lines, make([]Cell, 0, bb.Width)) bb.Col = 0 } func (bb *BufferBuilder) appendCell(c Cell) { n := len(bb.Lines) bb.Lines[n-1] = append(bb.Lines[n-1], c) bb.Col += wcwidth.Of(c.Text) } // Newline starts a newline. func (bb *BufferBuilder) Newline() *BufferBuilder { bb.appendLine() if bb.Indent > 0 { for i := 0; i < bb.Indent; i++ { bb.appendCell(Cell{Text: " "}) } } return bb } // WriteRuneSGR writes a single rune to a buffer with an SGR style, wrapping the // line when needed. If the rune is a control character, it will be written // using the caret notation (like ^X) and gets the additional style of // styleForControlChar. func (bb *BufferBuilder) WriteRuneSGR(r rune, style string) *BufferBuilder { if r == '\n' { bb.Newline() return bb } c := Cell{string(r), style} if r < 0x20 || r == 0x7f { // Always show control characters in reverse video. if style != "" { style = style + ";7" } else { style = "7" } c = Cell{"^" + string(r^0x40), style} } if bb.Col+wcwidth.Of(c.Text) > bb.Width { bb.Newline() bb.appendCell(c) } else { bb.appendCell(c) if bb.Col == bb.Width && bb.EagerWrap { bb.Newline() } } return bb } // Write is equivalent to calling WriteStyled with ui.T(text, style...). func (bb *BufferBuilder) Write(text string, ts ...ui.Styling) *BufferBuilder { return bb.WriteStyled(ui.T(text, ts...)) } // WriteSpaces writes w spaces with the given styles. func (bb *BufferBuilder) WriteSpaces(w int, ts ...ui.Styling) *BufferBuilder { return bb.Write(strings.Repeat(" ", w), ts...) } // DotHere is a special argument to MarkLines to mark the position of the dot. var DotHere = struct{ x struct{} }{} // MarkLines is like calling WriteStyled with ui.MarkLines(args...), but accepts // an additional special parameter DotHere to mark the position of the dot. func (bb *BufferBuilder) MarkLines(args ...interface{}) *BufferBuilder { for i, arg := range args { if arg == DotHere { return bb.WriteStyled(ui.MarkLines(args[:i]...)). SetDotHere().WriteStyled(ui.MarkLines(args[i+1:]...)) } } return bb.WriteStyled(ui.MarkLines(args...)) } // WriteStringSGR writes a string to a buffer with a SGR style. func (bb *BufferBuilder) WriteStringSGR(text, style string) *BufferBuilder { for _, r := range text { bb.WriteRuneSGR(r, style) } return bb } // WriteStyled writes a styled text. func (bb *BufferBuilder) WriteStyled(t ui.Text) *BufferBuilder { for _, seg := range t { bb.WriteStringSGR(seg.Text, seg.Style.SGR()) } return bb } func makeSpacing(n int) []Cell { s := make([]Cell, n) for i := 0; i < n; i++ { s[i].Text = " " } return s } elvish-0.17.0/pkg/cli/term/buffer_builder_test.go000066400000000000000000000062601415471104000217360ustar00rootroot00000000000000package term import ( "reflect" "testing" "src.elv.sh/pkg/ui" ) var bufferBuilderWritesTests = []struct { bb *BufferBuilder text string style string want *Buffer }{ // Writing nothing. {NewBufferBuilder(10), "", "", &Buffer{Width: 10, Lines: Lines{Line{}}}}, // Writing a single rune. {NewBufferBuilder(10), "a", "1", &Buffer{Width: 10, Lines: Lines{Line{Cell{"a", "1"}}}}}, // Writing control character. {NewBufferBuilder(10), "\033", "", &Buffer{Width: 10, Lines: Lines{Line{Cell{"^[", "7"}}}}}, // Writing styled control character. {NewBufferBuilder(10), "a\033b", "1", &Buffer{Width: 10, Lines: Lines{Line{ Cell{"a", "1"}, Cell{"^[", "1;7"}, Cell{"b", "1"}}}}}, // Writing text containing a newline. {NewBufferBuilder(10), "a\nb", "1", &Buffer{Width: 10, Lines: Lines{ Line{Cell{"a", "1"}}, Line{Cell{"b", "1"}}}}}, // Writing text containing a newline when there is indent. {NewBufferBuilder(10).SetIndent(2), "a\nb", "1", &Buffer{Width: 10, Lines: Lines{ Line{Cell{"a", "1"}}, Line{Cell{" ", ""}, Cell{" ", ""}, Cell{"b", "1"}}, }}}, // Writing long text that triggers wrapping. {NewBufferBuilder(4), "aaaab", "1", &Buffer{Width: 4, Lines: Lines{ Line{Cell{"a", "1"}, Cell{"a", "1"}, Cell{"a", "1"}, Cell{"a", "1"}}, Line{Cell{"b", "1"}}}}}, // Writing long text that triggers wrapping when there is indent. {NewBufferBuilder(4).SetIndent(2), "aaaab", "1", &Buffer{Width: 4, Lines: Lines{ Line{Cell{"a", "1"}, Cell{"a", "1"}, Cell{"a", "1"}, Cell{"a", "1"}}, Line{Cell{" ", ""}, Cell{" ", ""}, Cell{"b", "1"}}}}}, // Writing long text that triggers eager wrapping. {NewBufferBuilder(4).SetIndent(2).SetEagerWrap(true), "aaaa", "1", &Buffer{Width: 4, Lines: Lines{ Line{Cell{"a", "1"}, Cell{"a", "1"}, Cell{"a", "1"}, Cell{"a", "1"}}, Line{Cell{" ", ""}, Cell{" ", ""}}}}}, } // TestBufferWrites tests BufferBuilder.Writes by calling Writes on a // BufferBuilder and see if the built Buffer matches what is expected. func TestBufferBuilderWrites(t *testing.T) { for _, test := range bufferBuilderWritesTests { bb := cloneBufferBuilder(test.bb) bb.WriteStringSGR(test.text, test.style) buf := bb.Buffer() if !reflect.DeepEqual(buf, test.want) { t.Errorf("buf.writes(%q, %q) makes it %v, want %v", test.text, test.style, buf, test.want) } } } var styles = ui.RuneStylesheet{ '-': ui.Underlined, } var bufferBuilderTests = []struct { name string builder *BufferBuilder wantBuf *Buffer }{ { "MarkLines", NewBufferBuilder(10).MarkLines( "foo ", styles, "-- ", DotHere, "\n", "", "bar", ), &Buffer{Width: 10, Dot: Pos{0, 4}, Lines: Lines{ Line{Cell{"f", "4"}, Cell{"o", "4"}, Cell{"o", ""}, Cell{" ", ""}}, Line{Cell{"b", ""}, Cell{"a", ""}, Cell{"r", ""}}, }}, }, } func TestBufferBuilder(t *testing.T) { for _, test := range bufferBuilderTests { t.Run(test.name, func(t *testing.T) { buf := test.builder.Buffer() if !reflect.DeepEqual(buf, test.wantBuf) { t.Errorf("Got buf %v, want %v", buf, test.wantBuf) } }) } } func cloneBufferBuilder(bb *BufferBuilder) *BufferBuilder { return &BufferBuilder{ bb.Width, bb.Col, bb.Indent, bb.EagerWrap, cloneLines(bb.Lines), bb.Dot} } elvish-0.17.0/pkg/cli/term/buffer_test.go000066400000000000000000000177151415471104000202370ustar00rootroot00000000000000package term import ( "reflect" "testing" ) var cellsWidthTests = []struct { cells []Cell wantWidth int }{ {[]Cell{}, 0}, {[]Cell{{"a", ""}, {"好", ""}}, 3}, } func TestCellsWidth(t *testing.T) { for _, test := range cellsWidthTests { if width := CellsWidth(test.cells); width != test.wantWidth { t.Errorf("cellsWidth(%v) = %v, want %v", test.cells, width, test.wantWidth) } } } var makeSpacingTests = []struct { n int want []Cell }{ {0, []Cell{}}, {1, []Cell{{" ", ""}}}, {4, []Cell{{" ", ""}, {" ", ""}, {" ", ""}, {" ", ""}}}, } func TestMakeSpacing(t *testing.T) { for _, test := range makeSpacingTests { if got := makeSpacing(test.n); !reflect.DeepEqual(got, test.want) { t.Errorf("makeSpacing(%v) = %v, want %v", test.n, got, test.want) } } } var compareCellsTests = []struct { cells1 []Cell cells2 []Cell wantEqual bool wantIndex int }{ {[]Cell{}, []Cell{}, true, 0}, {[]Cell{}, []Cell{{"a", ""}}, false, 0}, { []Cell{{"a", ""}, {"好", ""}, {"b", ""}}, []Cell{{"a", ""}, {"好", ""}, {"c", ""}}, false, 2, }, { []Cell{{"a", ""}, {"好", ""}, {"b", ""}}, []Cell{{"a", ""}, {"好", "1"}, {"c", ""}}, false, 1, }, } func TestCompareCells(t *testing.T) { for _, test := range compareCellsTests { equal, index := CompareCells(test.cells1, test.cells2) if equal != test.wantEqual || index != test.wantIndex { t.Errorf("compareCells(%v, %v) = (%v, %v), want (%v, %v)", test.cells1, test.cells2, equal, index, test.wantEqual, test.wantIndex) } } } var bufferCursorTests = []struct { buf *Buffer want Pos }{ { &Buffer{Width: 10, Lines: Lines{Line{}}}, Pos{0, 0}, }, { &Buffer{Width: 10, Lines: Lines{Line{Cell{"a", ""}}, Line{Cell{"好", ""}}}}, Pos{1, 2}, }, } func TestBufferCursor(t *testing.T) { for _, test := range bufferCursorTests { if p := test.buf.Cursor(); p != test.want { t.Errorf("(%v).cursor() = %v, want %v", test.buf, p, test.want) } } } var buffersHeighTests = []struct { buffers []*Buffer want int }{ {nil, 0}, {[]*Buffer{NewBuffer(10)}, 1}, { []*Buffer{ {Width: 10, Lines: Lines{Line{}, Line{}}}, {Width: 10, Lines: Lines{Line{}}}, {Width: 10, Lines: Lines{Line{}, Line{}}}, }, 5, }, } func TestBuffersHeight(t *testing.T) { for _, test := range buffersHeighTests { if h := BuffersHeight(test.buffers...); h != test.want { t.Errorf("buffersHeight(%v...) = %v, want %v", test.buffers, h, test.want) } } } var bufferTrimToLinesTests = []struct { buf *Buffer low int high int want *Buffer }{ { &Buffer{Width: 10, Lines: Lines{ Line{Cell{"a", ""}}, Line{Cell{"b", ""}}, Line{Cell{"c", ""}}, Line{Cell{"d", ""}}, }}, 0, 2, &Buffer{Width: 10, Lines: Lines{ Line{Cell{"a", ""}}, Line{Cell{"b", ""}}, }}, }, // Negative low is treated as 0. { &Buffer{Width: 10, Lines: Lines{ Line{Cell{"a", ""}}, Line{Cell{"b", ""}}, Line{Cell{"c", ""}}, Line{Cell{"d", ""}}, }}, -1, 2, &Buffer{Width: 10, Lines: Lines{ Line{Cell{"a", ""}}, Line{Cell{"b", ""}}, }}, }, // With dot. { &Buffer{Width: 10, Lines: Lines{ Line{Cell{"a", ""}}, Line{Cell{"b", ""}}, Line{Cell{"c", ""}}, Line{Cell{"d", ""}}, }, Dot: Pos{1, 1}}, 1, 3, &Buffer{Width: 10, Lines: Lines{ Line{Cell{"b", ""}}, Line{Cell{"c", ""}}, }, Dot: Pos{0, 1}}, }, // With dot that is going to be trimmed away. { &Buffer{Width: 10, Lines: Lines{ Line{Cell{"a", ""}}, Line{Cell{"b", ""}}, Line{Cell{"c", ""}}, Line{Cell{"d", ""}}, }, Dot: Pos{0, 1}}, 1, 3, &Buffer{Width: 10, Lines: Lines{ Line{Cell{"b", ""}}, Line{Cell{"c", ""}}, }, Dot: Pos{0, 1}}, }, } func TestBufferTrimToLines(t *testing.T) { for _, test := range bufferTrimToLinesTests { b := cloneBuffer(test.buf) b.TrimToLines(test.low, test.high) if !reflect.DeepEqual(b, test.want) { t.Errorf("buf.trimToLines(%v, %v) makes it %v, want %v", test.low, test.high, b, test.want) } } } var bufferExtendTests = []struct { buf *Buffer buf2 *Buffer moveDot bool want *Buffer }{ { &Buffer{Width: 10, Lines: Lines{ Line{Cell{"a", ""}}, Line{Cell{"b", ""}}}}, &Buffer{Width: 11, Lines: Lines{ Line{Cell{"c", ""}}, Line{Cell{"d", ""}}}}, false, &Buffer{Width: 10, Lines: Lines{ Line{Cell{"a", ""}}, Line{Cell{"b", ""}}, Line{Cell{"c", ""}}, Line{Cell{"d", ""}}}}, }, // Moving dot. { &Buffer{Width: 10, Lines: Lines{ Line{Cell{"a", ""}}, Line{Cell{"b", ""}}}}, &Buffer{ Width: 11, Lines: Lines{Line{Cell{"c", ""}}, Line{Cell{"d", ""}}}, Dot: Pos{1, 1}, }, true, &Buffer{ Width: 10, Lines: Lines{ Line{Cell{"a", ""}}, Line{Cell{"b", ""}}, Line{Cell{"c", ""}}, Line{Cell{"d", ""}}}, Dot: Pos{3, 1}, }, }, } func TestBufferExtend(t *testing.T) { for _, test := range bufferExtendTests { buf := cloneBuffer(test.buf) buf.Extend(test.buf2, test.moveDot) if !reflect.DeepEqual(buf, test.want) { t.Errorf("buf.extend(%v, %v) makes it %v, want %v", test.buf2, test.moveDot, buf, test.want) } } } var bufferExtendRightTests = []struct { buf *Buffer buf2 *Buffer want *Buffer }{ // No padding, equal height. { &Buffer{Width: 1, Lines: Lines{Line{Cell{"a", ""}}, Line{Cell{"b", ""}}}}, &Buffer{Width: 1, Lines: Lines{Line{Cell{"c", ""}}, Line{Cell{"d", ""}}}}, &Buffer{Width: 2, Lines: Lines{ Line{Cell{"a", ""}, Cell{"c", ""}}, Line{Cell{"b", ""}, Cell{"d", ""}}}}, }, // With padding, equal height. { &Buffer{Width: 2, Lines: Lines{Line{Cell{"a", ""}}, Line{Cell{"b", ""}}}}, &Buffer{Width: 1, Lines: Lines{Line{Cell{"c", ""}}, Line{Cell{"d", ""}}}}, &Buffer{Width: 3, Lines: Lines{ Line{Cell{"a", ""}, Cell{" ", ""}, Cell{"c", ""}}, Line{Cell{"b", ""}, Cell{" ", ""}, Cell{"d", ""}}}}, }, // buf is higher. { &Buffer{Width: 1, Lines: Lines{ Line{Cell{"a", ""}}, Line{Cell{"b", ""}}, Line{Cell{"x", ""}}}}, &Buffer{Width: 1, Lines: Lines{ Line{Cell{"c", ""}}, Line{Cell{"d", ""}}, }}, &Buffer{Width: 2, Lines: Lines{ Line{Cell{"a", ""}, Cell{"c", ""}}, Line{Cell{"b", ""}, Cell{"d", ""}}, Line{Cell{"x", ""}}}}, }, // buf2 is higher. { &Buffer{Width: 1, Lines: Lines{Line{Cell{"a", ""}}, Line{Cell{"b", ""}}}}, &Buffer{Width: 1, Lines: Lines{ Line{Cell{"c", ""}}, Line{Cell{"d", ""}}, Line{Cell{"e", ""}}, }}, &Buffer{Width: 2, Lines: Lines{ Line{Cell{"a", ""}, Cell{"c", ""}}, Line{Cell{"b", ""}, Cell{"d", ""}}, Line{Cell{" ", ""}, Cell{"e", ""}}}}, }, } func TestBufferExtendRight(t *testing.T) { for _, test := range bufferExtendRightTests { buf := cloneBuffer(test.buf) buf.ExtendRight(test.buf2) if !reflect.DeepEqual(buf, test.want) { t.Errorf("buf.extendRight(%v) makes it %v, want %v", test.buf2, buf, test.want) } } } func TestBufferBuffer(t *testing.T) { b := NewBufferBuilder(4).Write("text").Buffer() if b.Buffer() != b { t.Errorf("Buffer did not return itself") } } var bufferTTYStringTests = []struct { buf *Buffer want string }{ { nil, "nil", }, { NewBufferBuilder(4). Write("ABCD"). Newline(). Write("XY"). Buffer(), "Width = 4, Dot = (0, 0)\n" + "┌────┐\n" + "│ABCD│\n" + "│XY$ │\n" + "└────┘\n", }, { NewBufferBuilder(4). Write("A").SetDotHere(). WriteStringSGR("B", "1"). WriteStringSGR("C", "7"). Write("D"). Newline(). WriteStringSGR("XY", "7"). Buffer(), "Width = 4, Dot = (0, 1)\n" + "┌────┐\n" + "│A\033[1mB\033[;7mC\033[mD│\n" + "│\033[7mXY\033[m$ │\n" + "└────┘\n", }, } func TestBufferTTYString(t *testing.T) { for _, test := range bufferTTYStringTests { ttyString := test.buf.TTYString() if ttyString != test.want { t.Errorf("TTYString -> %q, want %q", ttyString, test.want) } } } func cloneBuffer(b *Buffer) *Buffer { return &Buffer{b.Width, cloneLines(b.Lines), b.Dot} } func cloneLines(lines Lines) Lines { newLines := make(Lines, len(lines)) for i, line := range lines { if line != nil { newLines[i] = make(Line, len(line)) copy(newLines[i], line) } } return newLines } elvish-0.17.0/pkg/cli/term/event.go000066400000000000000000000030131415471104000170320ustar00rootroot00000000000000package term import "src.elv.sh/pkg/ui" // Event represents an event that can be read from the terminal. type Event interface { isEvent() } // KeyEvent represents a key press. type KeyEvent ui.Key // K constructs a new KeyEvent. func K(r rune, mods ...ui.Mod) KeyEvent { return KeyEvent(ui.K(r, mods...)) } // MouseEvent represents a mouse event (either pressing or releasing). type MouseEvent struct { Pos Down bool // Number of the Button, 0-based. -1 for unknown. Button int Mod ui.Mod } // CursorPosition represents a report of the current cursor position from the // terminal driver, usually as a response from a cursor position request. type CursorPosition Pos // PasteSetting indicates the start or finish of pasted text. type PasteSetting bool // FatalErrorEvent represents an error that affects the Reader's ability to // continue reading events. After sending a FatalError, the Reader makes no more // attempts at continuing to read events and wait for Stop to be called. type FatalErrorEvent struct{ Err error } // NonfatalErrorEvent represents an error that can be gradually recovered. After // sending a NonfatalError, the Reader will continue to read events. Note that // one anamoly in the terminal might cause multiple NonfatalError events to be // sent. type NonfatalErrorEvent struct{ Err error } func (KeyEvent) isEvent() {} func (MouseEvent) isEvent() {} func (CursorPosition) isEvent() {} func (PasteSetting) isEvent() {} func (FatalErrorEvent) isEvent() {} func (NonfatalErrorEvent) isEvent() {} elvish-0.17.0/pkg/cli/term/file_reader_unix.go000066400000000000000000000031611415471104000212210ustar00rootroot00000000000000//go:build !windows && !plan9 // +build !windows,!plan9 package term import ( "io" "os" "sync" "syscall" "time" "src.elv.sh/pkg/sys/eunix" ) // A helper for reading from a file. type fileReader interface { byteReaderWithTimeout // Stop stops any outstanding read call. It blocks until the read returns. Stop() error // Close releases new resources allocated for the fileReader. It does not // close the underlying file. Close() } func newFileReader(file *os.File) (fileReader, error) { rStop, wStop, err := os.Pipe() if err != nil { return nil, err } return &bReader{file: file, rStop: rStop, wStop: wStop}, nil } type bReader struct { file *os.File rStop *os.File wStop *os.File // A mutex that is held when Read is in process. mutex sync.Mutex } func (r *bReader) ReadByteWithTimeout(timeout time.Duration) (byte, error) { r.mutex.Lock() defer r.mutex.Unlock() for { ready, err := eunix.WaitForRead(timeout, r.file, r.rStop) if err != nil { if err == syscall.EINTR { continue } return 0, err } if ready[1] { var b [1]byte r.rStop.Read(b[:]) return 0, ErrStopped } if !ready[0] { return 0, errTimeout } var b [1]byte nr, err := r.file.Read(b[:]) if err != nil { return 0, err } if nr != 1 { return 0, io.ErrNoProgress } return b[0], nil } } func (r *bReader) Stop() error { _, err := r.wStop.Write([]byte{'q'}) r.mutex.Lock() //lint:ignore SA2001 We only lock the mutex to make sure that // ReadByteWithTimeout has exited, so we unlock it immediately. r.mutex.Unlock() return err } func (r *bReader) Close() { r.rStop.Close() r.wStop.Close() } elvish-0.17.0/pkg/cli/term/file_reader_unix_test.go000066400000000000000000000032351415471104000222620ustar00rootroot00000000000000//go:build !windows && !plan9 // +build !windows,!plan9 package term import ( "fmt" "io" "os" "testing" "time" "src.elv.sh/pkg/testutil" ) func TestFileReader_ReadByteWithTimeout(t *testing.T) { r, w, cleanup := setupFileReader() defer cleanup() content := []byte("0123456789") w.Write(content) // Test successful ReadByteWithTimeout calls. for i := 0; i < len(content); i++ { t.Run(fmt.Sprintf("byte %d", i), func(t *testing.T) { b, err := r.ReadByteWithTimeout(-1) if err != nil { t.Errorf("got err %v, want nil", err) } if b != content[i] { t.Errorf("got byte %q, want %q", b, content[i]) } }) } } func TestFileReader_ReadByteWithTimeout_EOF(t *testing.T) { r, w, cleanup := setupFileReader() defer cleanup() w.Close() _, err := r.ReadByteWithTimeout(-1) if err != io.EOF { t.Errorf("got byte %v, want %v", err, io.EOF) } } func TestFileReader_ReadByteWithTimeout_Timeout(t *testing.T) { r, _, cleanup := setupFileReader() defer cleanup() _, err := r.ReadByteWithTimeout(testutil.Scaled(time.Millisecond)) if err != errTimeout { t.Errorf("got err %v, want %v", err, errTimeout) } } func TestFileReader_Stop(t *testing.T) { r, _, cleanup := setupFileReader() defer cleanup() errCh := make(chan error, 1) go func() { _, err := r.ReadByteWithTimeout(-1) errCh <- err }() r.Stop() if err := <-errCh; err != ErrStopped { t.Errorf("got err %v, want %v", err, ErrStopped) } } func setupFileReader() (reader fileReader, writer *os.File, cleanup func()) { pr, pw := testutil.MustPipe() r, err := newFileReader(pr) if err != nil { panic(err) } return r, pw, func() { r.Close() pr.Close() pw.Close() } } elvish-0.17.0/pkg/cli/term/read_rune.go000066400000000000000000000020051415471104000176550ustar00rootroot00000000000000package term import ( "time" ) type byteReaderWithTimeout interface { // ReadByteWithTimeout reads a single byte with a timeout. A negative // timeout means no timeout. ReadByteWithTimeout(timeout time.Duration) (byte, error) } const badRune = '\ufffd' var utf8SeqTimeout = 10 * time.Millisecond // Reads a rune from the reader. The timeout applies to the first byte; a // negative value means no timeout. func readRune(rd byteReaderWithTimeout, timeout time.Duration) (rune, error) { leader, err := rd.ReadByteWithTimeout(timeout) if err != nil { return badRune, err } var r rune pending := 0 switch { case leader>>7 == 0: r = rune(leader) case leader>>5 == 0x6: r = rune(leader & 0x1f) pending = 1 case leader>>4 == 0xe: r = rune(leader & 0xf) pending = 2 case leader>>3 == 0x1e: r = rune(leader & 0x7) pending = 3 } for i := 0; i < pending; i++ { b, err := rd.ReadByteWithTimeout(utf8SeqTimeout) if err != nil { return badRune, err } r = r<<6 + rune(b&0x3f) } return r, nil } elvish-0.17.0/pkg/cli/term/read_rune_test.go000066400000000000000000000023271415471104000207230ustar00rootroot00000000000000//go:build !windows && !plan9 // +build !windows,!plan9 package term import "testing" // TODO(xiaq): Do not depend on Unix for this test. var contents = []string{ "English", "Ελληνικά", "你好 こんにちは", "𐌰𐌱", } func TestReadRune(t *testing.T) { for _, content := range contents { t.Run(content, func(t *testing.T) { rd, w, cleanup := setupFileReader() defer cleanup() w.Write([]byte(content)) for _, wantRune := range content { r, err := readRune(rd, 0) if r != wantRune { t.Errorf("got rune %q, want %q", r, wantRune) } if err != nil { t.Errorf("got err %v, want nil", err) } } }) } } func TestReadRune_ErrorAtFirstByte(t *testing.T) { rd, _, cleanup := setupFileReader() defer cleanup() r, err := readRune(rd, 0) if r != '\ufffd' { t.Errorf("got rune %q, want %q", r, '\ufffd') } if err == nil { t.Errorf("got err %v, want non-nil", err) } } func TestReadRune_ErrorAtNonFirstByte(t *testing.T) { rd, w, cleanup := setupFileReader() defer cleanup() w.Write([]byte{0xe4}) r, err := readRune(rd, 0) if r != '\ufffd' { t.Errorf("got rune %q, want %q", r, '\ufffd') } if err == nil { t.Errorf("got err %v, want non-nil", err) } } elvish-0.17.0/pkg/cli/term/reader.go000066400000000000000000000027331415471104000171630ustar00rootroot00000000000000package term import ( "errors" "fmt" "os" ) // Reader reads events from the terminal. type Reader interface { // ReadEvent reads a single event from the terminal. ReadEvent() (Event, error) // ReadRawEvent reads a single raw event from the terminal. The concept of // raw events is applicable where terminal events are represented as escape // sequences sequences, such as most modern Unix terminal emulators. If // the concept is not applicable, such as in the Windows console, it is // equivalent to ReadEvent. ReadRawEvent() (Event, error) // Close releases resources associated with the Reader. Any outstanding // ReadEvent or ReadRawEvent call will be aborted, returning ErrStopped. Close() } // ErrStopped is returned by Reader when Close is called during a ReadEvent or // ReadRawEvent method. var ErrStopped = errors.New("stopped") var errTimeout = errors.New("timed out") type seqError struct { msg string seq string } func (err seqError) Error() string { return fmt.Sprintf("%s: %q", err.msg, err.seq) } // NewReader creates a new Reader on the given terminal file. // // TODO: NewReader should return an error as well. Right now failure to // initialize Reader panics. func NewReader(f *os.File) Reader { return newReader(f) } // IsReadErrorRecoverable returns whether an error returned by Reader is // recoverable. func IsReadErrorRecoverable(err error) bool { if _, ok := err.(seqError); ok { return true } return err == ErrStopped || err == errTimeout } elvish-0.17.0/pkg/cli/term/reader_test.go000066400000000000000000000005531415471104000202200ustar00rootroot00000000000000package term import ( "errors" "testing" "src.elv.sh/pkg/tt" ) var Args = tt.Args func TestIsReadErrorRecoverable(t *testing.T) { tt.Test(t, tt.Fn("IsReadErrorRecoverable", IsReadErrorRecoverable), tt.Table{ Args(seqError{}).Rets(true), Args(ErrStopped).Rets(true), Args(errTimeout).Rets(true), Args(errors.New("other error")).Rets(false), }) } elvish-0.17.0/pkg/cli/term/reader_unix.go000066400000000000000000000275451415471104000202360ustar00rootroot00000000000000//go:build !windows && !plan9 // +build !windows,!plan9 package term import ( "os" "time" "src.elv.sh/pkg/ui" ) // reader reads terminal escape sequences and decodes them into events. type reader struct { fr fileReader } func newReader(f *os.File) *reader { fr, err := newFileReader(f) if err != nil { // TODO(xiaq): Do not panic. panic(err) } return &reader{fr} } func (rd *reader) ReadEvent() (Event, error) { return readEvent(rd.fr) } func (rd *reader) ReadRawEvent() (Event, error) { r, err := readRune(rd.fr, -1) return K(r), err } func (rd *reader) Close() { rd.fr.Stop() rd.fr.Close() } // Used by readRune in readOne to signal end of current sequence. const runeEndOfSeq rune = -1 // Timeout for bytes in escape sequences. Modern terminal emulators send escape // sequences very fast, so 10ms is more than sufficient. SSH connections on a // slow link might be problematic though. var keySeqTimeout = 10 * time.Millisecond func readEvent(rd byteReaderWithTimeout) (event Event, err error) { var r rune r, err = readRune(rd, -1) if err != nil { return } currentSeq := string(r) // Attempts to read a rune within a timeout of keySeqTimeout. It returns // runeEndOfSeq if there is any error; the caller should terminate the // current sequence when it sees that value. readRune := func() rune { r, e := readRune(rd, keySeqTimeout) if e != nil { return runeEndOfSeq } currentSeq += string(r) return r } badSeq := func(msg string) { err = seqError{msg, currentSeq} } switch r { case 0x1b: // ^[ Escape r2 := readRune() // According to https://unix.stackexchange.com/a/73697, rxvt and derivatives // prepend another ESC to a CSI-style or G3-style sequence to signal Alt. // If that happens, remember this now; it will be later picked up when parsing // those two kinds of sequences. // // issue #181 hasTwoLeadingESC := false if r2 == 0x1b { hasTwoLeadingESC = true r2 = readRune() } if r2 == runeEndOfSeq { // TODO(xiaq): Error is swallowed. // Nothing follows. Taken as a lone Escape. event = KeyEvent{'[', ui.Ctrl} break } switch r2 { case '[': // A '[' follows. CSI style function key sequence. r = readRune() if r == runeEndOfSeq { event = KeyEvent{'[', ui.Alt} return } nums := make([]int, 0, 2) var starter rune // Read an optional starter. switch r { case '<': starter = r r = readRune() case 'M': // Mouse event. cb := readRune() if cb == runeEndOfSeq { badSeq("incomplete mouse event") return } cx := readRune() if cx == runeEndOfSeq { badSeq("incomplete mouse event") return } cy := readRune() if cy == runeEndOfSeq { badSeq("incomplete mouse event") return } down := true button := int(cb & 3) if button == 3 { down = false button = -1 } mod := mouseModify(int(cb)) event = MouseEvent{ Pos{int(cy) - 32, int(cx) - 32}, down, button, mod} return } CSISeq: for { switch { case r == ';': nums = append(nums, 0) case '0' <= r && r <= '9': if len(nums) == 0 { nums = append(nums, 0) } cur := len(nums) - 1 nums[cur] = nums[cur]*10 + int(r-'0') case r == runeEndOfSeq: // Incomplete CSI. badSeq("incomplete CSI") return default: // Treat as a terminator. break CSISeq } r = readRune() } if starter == 0 && r == 'R' { // Cursor position report. if len(nums) != 2 { badSeq("bad CPR") return } event = CursorPosition{nums[0], nums[1]} } else if starter == '<' && (r == 'm' || r == 'M') { // SGR-style mouse event. if len(nums) != 3 { badSeq("bad SGR mouse event") return } down := r == 'M' button := nums[0] & 3 mod := mouseModify(nums[0]) event = MouseEvent{Pos{nums[2], nums[1]}, down, button, mod} } else if r == '~' && len(nums) == 1 && (nums[0] == 200 || nums[0] == 201) { b := nums[0] == 200 event = PasteSetting(b) } else { k := parseCSI(nums, r, currentSeq) if k == (ui.Key{}) { badSeq("bad CSI") } else { if hasTwoLeadingESC { k.Mod |= ui.Alt } event = KeyEvent(k) } } case 'O': // An 'O' follows. G3 style function key sequence: read one rune. r = readRune() if r == runeEndOfSeq { // Nothing follows after 'O'. Taken as Alt-O. event = KeyEvent{'O', ui.Alt} return } k, ok := g3Seq[r] if ok { if hasTwoLeadingESC { k.Mod |= ui.Alt } event = KeyEvent(k) } else { badSeq("bad G3") } default: // Something other than '[' or 'O' follows. Taken as an // Alt-modified key, possibly also modified by Ctrl. k := ctrlModify(r2) k.Mod |= ui.Alt event = KeyEvent(k) } default: event = KeyEvent(ctrlModify(r)) } return } // Determines whether a rune corresponds to a Ctrl-modified key and returns the // ui.Key the rune represents. func ctrlModify(r rune) ui.Key { switch r { // TODO(xiaq): Are the following special cases universal? case 0x0: return ui.K('`', ui.Ctrl) // ^@ case 0x1e: return ui.K('6', ui.Ctrl) // ^^ case 0x1f: return ui.K('/', ui.Ctrl) // ^_ case ui.Tab, ui.Enter, ui.Backspace: // ^I ^J ^? // Ambiguous Ctrl keys; prefer the non-Ctrl form as they are more likely. return ui.K(r) default: // Regular ui.Ctrl sequences. if 0x1 <= r && r <= 0x1d { return ui.K(r+0x40, ui.Ctrl) } } return ui.K(r) } // Tables for key sequences. Comments document which terminal emulators are // known to generate which sequences. The terminal emulators tested are // categorized into xterm (including actual xterm, libvte-based terminals, // Konsole and Terminal.app unless otherwise noted), urxvt, tmux. // G3-style key sequences: \eO followed by exactly one character. For instance, // \eOP is F1. These are pretty limited in that they cannot be extended to // support modifier keys, other than a leading \e for Alt (e.g. \e\eOP is // Alt-F1). Terminals that send G3-style key sequences typically switch to // sending a CSI-style key sequence when a non-Alt modifier key is pressed. var g3Seq = map[rune]ui.Key{ // xterm, tmux -- only in Vim, depends on termios setting? // NOTE(xiaq): According to urxvt's manpage, \eO[ABCD] sequences are used for // Ctrl-Shift-modified arrow keys; however, this doesn't seem to be true for // urxvt 9.22 packaged by Debian; those keys simply send the same sequence // as Ctrl-modified keys (\eO[abcd]). 'A': ui.K(ui.Up), 'B': ui.K(ui.Down), 'C': ui.K(ui.Right), 'D': ui.K(ui.Left), 'H': ui.K(ui.Home), 'F': ui.K(ui.End), 'M': ui.K(ui.Insert), // urxvt 'a': ui.K(ui.Up, ui.Ctrl), 'b': ui.K(ui.Down, ui.Ctrl), 'c': ui.K(ui.Right, ui.Ctrl), 'd': ui.K(ui.Left, ui.Ctrl), // xterm, urxvt, tmux 'P': ui.K(ui.F1), 'Q': ui.K(ui.F2), 'R': ui.K(ui.F3), 'S': ui.K(ui.F4), } // Tables for CSI-style key sequences. A CSI sequence is \e[ followed by zero or // more numerical arguments (separated by semicolons), ending in a non-numeric, // non-semicolon rune. They are used for many purposes, and CSI-style key // sequences are a subset of them. // // There are several variants of CSI-style key sequences; see comments above the // respective tables. In all variants, modifier keys are encoded in numerical // arguments; see xtermModify. Note that although the set of possible sequences // make it possible to express a very complete set of key combinations, they are // not always sent by terminals. For instance, many (if not most) terminals will // send the same sequence for Up when Shift-Up is pressed, even if Shift-Up is // expressible using the escape sequences described below. // CSI-style key sequences identified by the last rune. For instance, \e[A is // Up. When modified, two numerical arguments are added, the first always beging // 1 and the second identifying the modifier. For instance, \e[1;5A is Ctrl-Up. var csiSeqByLast = map[rune]ui.Key{ // xterm, urxvt, tmux 'A': ui.K(ui.Up), 'B': ui.K(ui.Down), 'C': ui.K(ui.Right), 'D': ui.K(ui.Left), // urxvt 'a': ui.K(ui.Up, ui.Shift), 'b': ui.K(ui.Down, ui.Shift), 'c': ui.K(ui.Right, ui.Shift), 'd': ui.K(ui.Left, ui.Shift), // xterm (Terminal.app only sends those in alternate screen) 'H': ui.K(ui.Home), 'F': ui.K(ui.End), // xterm, urxvt, tmux 'Z': ui.K(ui.Tab, ui.Shift), } // CSI-style key sequences ending with '~' with by one or two numerical // arguments. The first argument identifies the key, and the optional second // argument identifies the modifier. For instance, \e[3~ is Delete, and \e[3;5~ // is Ctrl-Delete. // // An alternative encoding of the modifier key, only known to be used by urxvt // (or for that matter, likely also rxvt) is to change the last rune: '$' for // Shift, '^' for Ctrl, and '@' for Ctrl+Shift. The numeric argument is kept // unchanged. For instance, \e[3^ is Ctrl-Delete. var csiSeqTilde = map[int]rune{ // tmux (NOTE: urxvt uses the pair for Find/Select) 1: ui.Home, 4: ui.End, // xterm (Terminal.app sends ^M for Fn+Enter), urxvt, tmux 2: ui.Insert, // xterm, urxvt, tmux 3: ui.Delete, // xterm (Terminal.app only sends those in alternate screen), urxvt, tmux // NOTE: called Prior/Next in urxvt manpage 5: ui.PageUp, 6: ui.PageDown, // urxvt 7: ui.Home, 8: ui.End, // urxvt 11: ui.F1, 12: ui.F2, 13: ui.F3, 14: ui.F4, // xterm, urxvt, tmux // NOTE: 16 and 22 are unused 15: ui.F5, 17: ui.F6, 18: ui.F7, 19: ui.F8, 20: ui.F9, 21: ui.F10, 23: ui.F11, 24: ui.F12, } // CSI-style key sequences ending with '~', with the first argument always 27, // the second argument identifying the modifier, and the third argument // identifying the key. For instance, \e[27;5;9~ is Ctrl-Tab. // // NOTE(xiaq): The list is taken blindly from xterm-keys.c in the tmux source // tree. I do not have a keyboard-terminal combination that generate such // sequences, but assumably they are generated by some terminals for numpad // inputs. var csiSeqTilde27 = map[int]rune{ 9: '\t', 13: '\r', 33: '!', 35: '#', 39: '\'', 40: '(', 41: ')', 43: '+', 44: ',', 45: '-', 46: '.', 48: '0', 49: '1', 50: '2', 51: '3', 52: '4', 53: '5', 54: '6', 55: '7', 56: '8', 57: '9', 58: ':', 59: ';', 60: '<', 61: '=', 62: '>', 63: ';', } // parseCSI parses a CSI-style key sequence. See comments above for all the 3 // variants this function handles. func parseCSI(nums []int, last rune, seq string) ui.Key { if k, ok := csiSeqByLast[last]; ok { if len(nums) == 0 { // Unmodified: \e[A (Up) return k } else if len(nums) == 2 && nums[0] == 1 { // Modified: \e[1;5A (Ctrl-Up) return xtermModify(k, nums[1], seq) } else { return ui.Key{} } } switch last { case '~': if len(nums) == 1 || len(nums) == 2 { if r, ok := csiSeqTilde[nums[0]]; ok { k := ui.K(r) if len(nums) == 1 { // Unmodified: \e[5~ (e.g. PageUp) return k } // Modified: \e[5;5~ (e.g. Ctrl-PageUp) return xtermModify(k, nums[1], seq) } } else if len(nums) == 3 && nums[0] == 27 { if r, ok := csiSeqTilde27[nums[2]]; ok { k := ui.K(r) return xtermModify(k, nums[1], seq) } } case '$', '^', '@': // Modified by urxvt; see comment above csiSeqTilde. if len(nums) == 1 { if r, ok := csiSeqTilde[nums[0]]; ok { var mod ui.Mod switch last { case '$': mod = ui.Shift case '^': mod = ui.Ctrl case '@': mod = ui.Shift | ui.Ctrl } return ui.K(r, mod) } } } return ui.Key{} } func xtermModify(k ui.Key, mod int, seq string) ui.Key { if mod < 0 || mod > 16 { // Out of range return ui.Key{} } if mod == 0 { return k } modFlags := mod - 1 if modFlags&0x1 != 0 { k.Mod |= ui.Shift } if modFlags&0x2 != 0 { k.Mod |= ui.Alt } if modFlags&0x4 != 0 { k.Mod |= ui.Ctrl } if modFlags&0x8 != 0 { // This should be Meta, but we currently conflate Meta and Alt. k.Mod |= ui.Alt } return k } func mouseModify(n int) ui.Mod { var mod ui.Mod if n&4 != 0 { mod |= ui.Shift } if n&8 != 0 { mod |= ui.Alt } if n&16 != 0 { mod |= ui.Ctrl } return mod } elvish-0.17.0/pkg/cli/term/reader_unix_test.go000066400000000000000000000136441415471104000212700ustar00rootroot00000000000000//go:build !windows && !plan9 // +build !windows,!plan9 package term import ( "os" "strings" "testing" "src.elv.sh/pkg/testutil" "src.elv.sh/pkg/ui" ) var readEventTests = []struct { input string want Event }{ // Simple graphical key. {"x", K('x')}, {"X", K('X')}, {" ", K(' ')}, // Ctrl key. {"\001", K('A', ui.Ctrl)}, {"\033", K('[', ui.Ctrl)}, // Special Ctrl keys that do not obey the usual 0x40 rule. {"\000", K('`', ui.Ctrl)}, {"\x1e", K('6', ui.Ctrl)}, {"\x1f", K('/', ui.Ctrl)}, // Ambiguous Ctrl keys; the reader uses the non-Ctrl form as canonical. {"\n", K('\n')}, {"\t", K('\t')}, {"\x7f", K('\x7f')}, // backspace // Alt plus simple graphical key. {"\033a", K('a', ui.Alt)}, {"\033[", K('[', ui.Alt)}, // G3-style key. {"\033OA", K(ui.Up)}, {"\033OH", K(ui.Home)}, // G3-style key with leading Escape. {"\033\033OA", K(ui.Up, ui.Alt)}, {"\033\033OH", K(ui.Home, ui.Alt)}, // Alt-O. This is handled as a special case because it looks like a G3-style // key. {"\033O", K('O', ui.Alt)}, // CSI-sequence key identified by the ending rune. {"\033[A", K(ui.Up)}, {"\033[H", K(ui.Home)}, // Modifiers. {"\033[1;0A", K(ui.Up)}, {"\033[1;1A", K(ui.Up)}, {"\033[1;2A", K(ui.Up, ui.Shift)}, {"\033[1;3A", K(ui.Up, ui.Alt)}, {"\033[1;4A", K(ui.Up, ui.Shift, ui.Alt)}, {"\033[1;5A", K(ui.Up, ui.Ctrl)}, {"\033[1;6A", K(ui.Up, ui.Shift, ui.Ctrl)}, {"\033[1;7A", K(ui.Up, ui.Alt, ui.Ctrl)}, {"\033[1;8A", K(ui.Up, ui.Shift, ui.Alt, ui.Ctrl)}, // The modifiers below should be for Meta, but we conflate Alt and Meta. {"\033[1;9A", K(ui.Up, ui.Alt)}, {"\033[1;10A", K(ui.Up, ui.Shift, ui.Alt)}, {"\033[1;11A", K(ui.Up, ui.Alt)}, {"\033[1;12A", K(ui.Up, ui.Shift, ui.Alt)}, {"\033[1;13A", K(ui.Up, ui.Alt, ui.Ctrl)}, {"\033[1;14A", K(ui.Up, ui.Shift, ui.Alt, ui.Ctrl)}, {"\033[1;15A", K(ui.Up, ui.Alt, ui.Ctrl)}, {"\033[1;16A", K(ui.Up, ui.Shift, ui.Alt, ui.Ctrl)}, // CSI-sequence key with one argument, ending in '~'. {"\033[1~", K(ui.Home)}, {"\033[11~", K(ui.F1)}, // Modified. {"\033[1;2~", K(ui.Home, ui.Shift)}, // Urxvt-flavor modifier, shifting the '~' to reflect the modifier {"\033[1$", K(ui.Home, ui.Shift)}, {"\033[1^", K(ui.Home, ui.Ctrl)}, {"\033[1@", K(ui.Home, ui.Shift, ui.Ctrl)}, // With a leading Escape. {"\033\033[1~", K(ui.Home, ui.Alt)}, // CSI-sequence key with three arguments and ending in '~'. The first // argument is always 27, the second identifies the modifier and the last // identifies the key. {"\033[27;4;63~", K(';', ui.Shift, ui.Alt)}, // Cursor Position Report. {"\033[3;4R", CursorPosition{3, 4}}, // Paste setting. {"\033[200~", PasteSetting(true)}, {"\033[201~", PasteSetting(false)}, // Mouse event. {"\033[M\x00\x23\x24", MouseEvent{Pos{4, 3}, true, 0, 0}}, // Other buttons. {"\033[M\x01\x23\x24", MouseEvent{Pos{4, 3}, true, 1, 0}}, // Button up. {"\033[M\x03\x23\x24", MouseEvent{Pos{4, 3}, false, -1, 0}}, // Modified. {"\033[M\x04\x23\x24", MouseEvent{Pos{4, 3}, true, 0, ui.Shift}}, {"\033[M\x08\x23\x24", MouseEvent{Pos{4, 3}, true, 0, ui.Alt}}, {"\033[M\x10\x23\x24", MouseEvent{Pos{4, 3}, true, 0, ui.Ctrl}}, {"\033[M\x14\x23\x24", MouseEvent{Pos{4, 3}, true, 0, ui.Shift | ui.Ctrl}}, // SGR-style mouse event. {"\033[<0;3;4M", MouseEvent{Pos{4, 3}, true, 0, 0}}, // Other buttons. {"\033[<1;3;4M", MouseEvent{Pos{4, 3}, true, 1, 0}}, // Button up. {"\033[<0;3;4m", MouseEvent{Pos{4, 3}, false, 0, 0}}, // Modified. {"\033[<4;3;4M", MouseEvent{Pos{4, 3}, true, 0, ui.Shift}}, {"\033[<16;3;4M", MouseEvent{Pos{4, 3}, true, 0, ui.Ctrl}}, } func TestReader_ReadEvent(t *testing.T) { r, w := setupReader(t) for _, test := range readEventTests { t.Run(test.input, func(t *testing.T) { w.WriteString(test.input) ev, err := r.ReadEvent() if ev != test.want { t.Errorf("got event %v, want %v", ev, test.want) } if err != nil { t.Errorf("got err %v, want %v", err, nil) } }) } } var readEventBadSeqTests = []struct { input string wantErrMsg string }{ // mouse event should have exactly 3 bytes after \033[M {"\033[M", "incomplete mouse event"}, {"\033[M1", "incomplete mouse event"}, {"\033[M12", "incomplete mouse event"}, // CSI needs to be terminated by something that is not a parameter {"\033[1", "incomplete CSI"}, {"\033[;", "incomplete CSI"}, {"\033[1;", "incomplete CSI"}, // CPR should have exactly 2 parameters {"\033[1R", "bad CPR"}, {"\033[1;2;3R", "bad CPR"}, // SGR mouse event should have exactly 3 parameters {"\033[<1;2m", "bad SGR mouse event"}, // csiSeqByLast should have 0 or 2 parameters {"\033[1;2;3A", "bad CSI"}, // csiSeqByLast with 2 parameters should have first parameter = 1 {"\033[2;1A", "bad CSI"}, // xterm-style modifier should be 0 to 16 {"\033[1;17A", "bad CSI"}, // unknown CSI terminator {"\033[x", "bad CSI"}, // G3 allows a small list of allowed bytes after \033O {"\033Ox", "bad G3"}, } func TestReader_ReadEvent_BadSeq(t *testing.T) { r, w := setupReader(t) for _, test := range readEventBadSeqTests { t.Run(test.input, func(t *testing.T) { w.WriteString(test.input) ev, err := r.ReadEvent() if err == nil { t.Fatalf("got nil err with event %v, want non-nil error", ev) } errMsg := err.Error() if !strings.HasPrefix(errMsg, test.wantErrMsg) { t.Errorf("got err with message %v, want message starting with %v", errMsg, test.wantErrMsg) } }) } } func TestReader_ReadRawEvent(t *testing.T) { rd, w := setupReader(t) for _, test := range readEventTests { input := test.input t.Run(input, func(t *testing.T) { w.WriteString(input) for _, r := range input { ev, err := rd.ReadRawEvent() if err != nil { t.Errorf("got error %v, want nil", err) } if ev != K(r) { t.Errorf("got event %v, want %v", ev, K(r)) } } }) } } func setupReader(t *testing.T) (Reader, *os.File) { pr, pw := testutil.MustPipe() r := NewReader(pr) t.Cleanup(func() { r.Close() pr.Close() pw.Close() }) return r, pw } elvish-0.17.0/pkg/cli/term/reader_windows.go000066400000000000000000000130271415471104000207330ustar00rootroot00000000000000package term import ( "fmt" "io" "log" "os" "sync" "golang.org/x/sys/windows" "src.elv.sh/pkg/sys/ewindows" "src.elv.sh/pkg/ui" ) type reader struct { console windows.Handle stopEvent windows.Handle // A mutex that is held during ReadEvent. mutex sync.Mutex } // Creates a new Reader instance. func newReader(file *os.File) Reader { console, err := windows.GetStdHandle(windows.STD_INPUT_HANDLE) if err != nil { panic(fmt.Errorf("GetStdHandle(STD_INPUT_HANDLE): %v", err)) } stopEvent, err := windows.CreateEvent(nil, 0, 0, nil) if err != nil { panic(fmt.Errorf("CreateEvent: %v", err)) } return &reader{console: console, stopEvent: stopEvent} } func (r *reader) ReadEvent() (Event, error) { r.mutex.Lock() defer r.mutex.Unlock() handles := []windows.Handle{r.console, r.stopEvent} for { triggered, _, err := ewindows.WaitForMultipleObjects(handles, false, ewindows.INFINITE) if err != nil { return nil, err } if triggered == 1 { return nil, ErrStopped } var buf [1]ewindows.InputRecord nr, err := ewindows.ReadConsoleInput(r.console, buf[:]) if nr == 0 { return nil, io.ErrNoProgress } if err != nil { return nil, err } event := convertEvent(buf[0].GetEvent()) if event != nil { return event, nil } // Got an event that should be ignored; keep going. } } func (r *reader) ReadRawEvent() (Event, error) { return r.ReadEvent() } func (r *reader) Close() { err := windows.SetEvent(r.stopEvent) if err != nil { log.Println("SetEvent:", err) } r.mutex.Lock() r.mutex.Unlock() err = windows.CloseHandle(r.stopEvent) if err != nil { log.Println("Closing stopEvent handle for reader:", err) } } // A subset of virtual key codes listed in // https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx var keyCodeToRune = map[uint16]rune{ 0x08: ui.Backspace, 0x09: ui.Tab, 0x0d: ui.Enter, 0x1b: '\x1b', 0x20: ' ', 0x23: ui.End, 0x24: ui.Home, 0x25: ui.Left, 0x26: ui.Up, 0x27: ui.Right, 0x28: ui.Down, 0x2d: ui.Insert, 0x2e: ui.Delete, /* 0x30 - 0x39: digits, same with ASCII */ /* 0x41 - 0x5a: letters, same with ASCII */ /* 0x60 - 0x6f: numpads; currently ignored */ 0x70: ui.F1, 0x71: ui.F2, 0x72: ui.F3, 0x73: ui.F4, 0x74: ui.F5, 0x75: ui.F6, 0x76: ui.F7, 0x77: ui.F8, 0x78: ui.F9, 0x79: ui.F10, 0x7a: ui.F11, 0x7b: ui.F12, /* 0x7c - 0x87: F13 - F24; currently ignored */ 0xba: ';', 0xbb: '=', 0xbc: ',', 0xbd: '-', 0xbe: '.', 0xbf: '/', 0xc0: '`', 0xdb: '[', 0xdc: '\\', 0xdd: ']', 0xde: '\'', } // A subset of constants listed in // https://docs.microsoft.com/en-us/windows/console/key-event-record-str const ( leftAlt = 0x02 leftCtrl = 0x08 rightAlt = 0x01 rightCtrl = 0x04 shift = 0x10 ) // Converts the native ewindows.InputEvent type to a suitable Event type. It returns // nil if the event should be ignored. func convertEvent(event ewindows.InputEvent) Event { switch event := event.(type) { case *ewindows.KeyEvent: if event.BKeyDown == 0 { // Ignore keyup events. return nil } r := rune(event.UChar[0]) + rune(event.UChar[1])<<8 filteredMod := event.DwControlKeyState & (leftAlt | leftCtrl | rightAlt | rightCtrl | shift) if r >= 0x20 && r != 0x7f { // This key inputs a character. The flags present in // DwControlKeyState might indicate modifier keys that are needed to // input this character (e.g. the Shift key when inputting 'A'), or // modifier keys that are pressed in addition (e.g. the Alt key when // pressing Alt-A). There doesn't seem to be an easy way to tell // which is the case, so we rely on heuristics derived from // real-world observations. if filteredMod == 0 { // TODO: Handle surrogate pairs return KeyEvent(ui.Key{Rune: r}) } else if filteredMod == shift { // A lone Shift seems to be always part of the character. return KeyEvent(ui.Key{Rune: r}) } else if filteredMod == leftCtrl|rightAlt || filteredMod == leftCtrl|rightAlt|shift { // The combination leftCtrl|rightAlt is used to represent AltGr. // Furthermore, when the actual left Ctrl and right Alt are used // together, the UChar field seems to be always 0; so if we are // here, we can actually be sure that it's AltGr. // // Some characters require AltGr+Shift to input, such as the // upper-case sharp S on a German keyboard. return KeyEvent(ui.Key{Rune: r}) } } mod := convertMod(filteredMod) if mod == 0 && event.WVirtualKeyCode == 0x1b { // Special case: Normalize 0x1b to Ctrl-[. // // TODO(xiaq): This is Unix-centric. Maybe the normalized form // should be Escape. return KeyEvent(ui.Key{Rune: '[', Mod: ui.Ctrl}) } r = convertRune(event.WVirtualKeyCode, mod) if r == 0 { return nil } return KeyEvent(ui.Key{Rune: r, Mod: mod}) default: // Other events are ignored. return nil } } func convertRune(keyCode uint16, mod ui.Mod) rune { r, ok := keyCodeToRune[keyCode] if ok { return r } if '0' <= keyCode && keyCode <= '9' { return rune(keyCode) } if 'A' <= keyCode && keyCode <= 'Z' { // If Ctrl is involved, emulate UNIX's convention and use upper case; // otherwise use lower case. // // TODO(xiaq): This is quite Unix-centric. Maybe we should make the // base rune case-insensitive when there are modifiers involved. if mod&ui.Ctrl != 0 { return rune(keyCode) } return rune(keyCode - 'A' + 'a') } return 0 } func convertMod(state uint32) ui.Mod { mod := ui.Mod(0) if state&(leftAlt|rightAlt) != 0 { mod |= ui.Alt } if state&(leftCtrl|rightCtrl) != 0 { mod |= ui.Ctrl } if state&shift != 0 { mod |= ui.Shift } return mod } elvish-0.17.0/pkg/cli/term/reader_windows_test.go000066400000000000000000000027631415471104000217770ustar00rootroot00000000000000package term import ( "testing" "src.elv.sh/pkg/sys/ewindows" "src.elv.sh/pkg/tt" "src.elv.sh/pkg/ui" ) func TestConvertEvent(t *testing.T) { tt.Test(t, tt.Fn("convertEvent", convertEvent), tt.Table{ // Only convert KeyEvent Args(&ewindows.MouseEvent{}).Rets(nil), // Only convert KeyDown events Args(&ewindows.KeyEvent{BKeyDown: 0}).Rets(nil), Args(charKeyEvent('a', 0)).Rets(K('a')), Args(charKeyEvent('A', shift)).Rets(K('A')), Args(charKeyEvent('µ', leftCtrl|rightAlt)).Rets(K('µ')), Args(charKeyEvent('ẞ', leftCtrl|rightAlt|shift)).Rets(K('ẞ')), Args(funcKeyEvent(0x1b, 0)).Rets(K('[', ui.Ctrl)), // Functional key with modifiers Args(funcKeyEvent(0x08, 0)).Rets(K(ui.Backspace)), Args(funcKeyEvent(0x08, leftCtrl)).Rets(K(ui.Backspace, ui.Ctrl)), Args(funcKeyEvent(0x08, leftCtrl|leftAlt|shift)).Rets(K(ui.Backspace, ui.Ctrl, ui.Alt, ui.Shift)), // Functional keys with an alphanumeric base Args(funcKeyEvent('2', leftCtrl)).Rets(K('2', ui.Ctrl)), Args(funcKeyEvent('A', leftCtrl)).Rets(K('A', ui.Ctrl)), Args(funcKeyEvent('A', leftAlt)).Rets(K('a', ui.Alt)), // Unrecognized functional key Args(funcKeyEvent(0, 0)).Rets(nil), }) } func charKeyEvent(r uint16, mod uint32) *ewindows.KeyEvent { return &ewindows.KeyEvent{ BKeyDown: 1, DwControlKeyState: mod, UChar: [2]byte{byte(r), byte(r >> 8)}} } func funcKeyEvent(code uint16, mod uint32) *ewindows.KeyEvent { return &ewindows.KeyEvent{ BKeyDown: 1, DwControlKeyState: mod, WVirtualKeyCode: code} } elvish-0.17.0/pkg/cli/term/setup.go000066400000000000000000000056571415471104000170710ustar00rootroot00000000000000package term import ( "fmt" "os" "src.elv.sh/pkg/sys" "src.elv.sh/pkg/wcwidth" ) // Setup sets up the terminal so that it is suitable for the Reader and // Writer to use. It returns a function that can be used to restore the // original terminal config. func Setup(in, out *os.File) (func() error, error) { return setup(in, out) } // SetupGlobal sets up the terminal for the entire Elvish session. func SetupGlobal() func() { return setupGlobal() } // Sanitize sanitizes the terminal after an external command has executed. func Sanitize(in, out *os.File) { sanitize(in, out) } const ( lackEOLRune = '\u23ce' lackEOL = "\033[7m" + string(lackEOLRune) + "\033[m" enableSGRMouse = false ) // setupVT performs setup for VT-like terminals. func setupVT(out *os.File) error { _, width := sys.WinSize(out) s := "" /* Write a lackEOLRune if the cursor is not in the leftmost column. This is done as follows: 1. Turn on autowrap; 2. Write lackEOL along with enough padding, so that the total width is equal to the width of the screen. If the cursor was in the first column, we are still in the same line, just off the line boundary. Otherwise, we are now in the next line. 3. Rewind to the first column, write one space and rewind again. If the cursor was in the first column to start with, we have just erased the LackEOL character. Otherwise, we are now in the next line and this is a no-op. The LackEOL character remains. */ s += fmt.Sprintf("\033[?7h%s%*s\r \r", lackEOL, width-wcwidth.OfRune(lackEOLRune), "") /* Turn off autowrap. The terminals sometimes has different opinions about how wide some characters are (notably emojis and some dingbats) with elvish. When that happens, elvish becomes wrong about where the cursor is when it writes its output, and the effect can be disastrous. If we turn off autowrap, the terminal won't insert any newlines behind the scene, so elvish is always right about which line the cursor is. With a bit more caution, this can restrict the consequence of the mismatch within one line. */ s += "\033[?7l" // Turn on SGR-style mouse tracking. if enableSGRMouse { s += "\033[?1000;1006h" } // Enable bracketed paste. s += "\033[?2004h" _, err := out.WriteString(s) return err } // restoreVT performs restore for VT-like terminals. func restoreVT(out *os.File) error { s := "" // Turn on autowrap. s += "\033[?7h" // Turn off mouse tracking. if enableSGRMouse { s += "\033[?1000;1006l" } // Disable bracketed paste. s += "\033[?2004l" // Move the cursor to the first row, even if we haven't written anything // visible. This is because the terminal driver might not be smart enough to // recognize some escape sequences as invisible and wrongly assume that we // are not in the first column, which can mess up with tabs. See // https://src.elv.sh/pkg/issues/629 for an example. s += "\r" _, err := out.WriteString(s) return err } elvish-0.17.0/pkg/cli/term/setup_unix.go000066400000000000000000000026631415471104000201260ustar00rootroot00000000000000//go:build !windows && !plan9 // +build !windows,!plan9 package term import ( "fmt" "os" "golang.org/x/sys/unix" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/sys/eunix" ) func setup(in, out *os.File) (func() error, error) { // On Unix, use input file for changing termios. All fds pointing to the // same terminal are equivalent. fd := int(in.Fd()) term, err := eunix.TermiosForFd(fd) if err != nil { return nil, fmt.Errorf("can't get terminal attribute: %s", err) } savedTermios := term.Copy() term.SetICanon(false) term.SetIExten(false) term.SetEcho(false) term.SetVMin(1) term.SetVTime(0) // Enforcing crnl translation on readline. Assuming user won't set // inlcr or -onlcr, otherwise we have to hardcode all of them here. term.SetICRNL(true) err = term.ApplyToFd(fd) if err != nil { return nil, fmt.Errorf("can't set up terminal attribute: %s", err) } var errSetupVT error err = setupVT(out) if err != nil { errSetupVT = fmt.Errorf("can't setup VT: %s", err) } restore := func() error { return diag.Errors(savedTermios.ApplyToFd(fd), restoreVT(out)) } return restore, errSetupVT } func setupGlobal() func() { return func() {} } func sanitize(in, out *os.File) { // Some programs use non-blocking IO but do not correctly clear the // non-blocking flags after exiting, so we always clear the flag. See #822 // for an example. unix.SetNonblock(int(in.Fd()), false) unix.SetNonblock(int(out.Fd()), false) } elvish-0.17.0/pkg/cli/term/setup_unix_test.go000066400000000000000000000010251415471104000211540ustar00rootroot00000000000000//go:build !windows && !plan9 // +build !windows,!plan9 package term import ( "testing" "github.com/creack/pty" ) func TestSetupTerminal(t *testing.T) { pty, tty, err := pty.Open() if err != nil { t.Skip("cannot open pty for testing setupTerminal") } defer pty.Close() defer tty.Close() _, err = setup(tty, tty) if err != nil { t.Errorf("setupTerminal returns an error") } // TODO(xiaq): Test whether the interesting flags in the termios were indeed // set. // termios, err := sys.TermiosForFd(int(tty.Fd())) } elvish-0.17.0/pkg/cli/term/setup_windows.go000066400000000000000000000026371415471104000206360ustar00rootroot00000000000000package term import ( "os" "golang.org/x/sys/windows" "src.elv.sh/pkg/diag" ) const ( wantedInMode = windows.ENABLE_WINDOW_INPUT | windows.ENABLE_MOUSE_INPUT | windows.ENABLE_PROCESSED_INPUT wantedOutMode = windows.ENABLE_PROCESSED_OUTPUT | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING wantedGlobalOutMode = windows.ENABLE_PROCESSED_OUTPUT | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING ) func setup(in, out *os.File) (func() error, error) { hIn := windows.Handle(in.Fd()) hOut := windows.Handle(out.Fd()) var oldInMode, oldOutMode uint32 err := windows.GetConsoleMode(hIn, &oldInMode) if err != nil { return nil, err } err = windows.GetConsoleMode(hOut, &oldOutMode) if err != nil { return nil, err } errSetIn := windows.SetConsoleMode(hIn, wantedInMode) errSetOut := windows.SetConsoleMode(hOut, wantedOutMode) errVT := setupVT(out) return func() error { return diag.Errors( restoreVT(out), windows.SetConsoleMode(hOut, oldOutMode), windows.SetConsoleMode(hIn, oldInMode)) }, diag.Errors(errSetIn, errSetOut, errVT) } func setupGlobal() func() { hOut := windows.Handle(os.Stderr.Fd()) var oldOutMode uint32 err := windows.GetConsoleMode(hOut, &oldOutMode) if err != nil { return func() {} } err = windows.SetConsoleMode(hOut, wantedGlobalOutMode) if err != nil { return func() {} } return func() { windows.SetConsoleMode(hOut, oldOutMode) } } func sanitize(in, out *os.File) {} elvish-0.17.0/pkg/cli/term/term.go000066400000000000000000000002401415471104000166570ustar00rootroot00000000000000// Package term provides functionality for working with terminals. package term import "src.elv.sh/pkg/logutil" var logger = logutil.GetLogger("[cli/term] ") elvish-0.17.0/pkg/cli/term/writer.go000066400000000000000000000120231415471104000172260ustar00rootroot00000000000000package term import ( "bytes" "fmt" "io" ) var logWriterDetail = false // Writer represents the output to a terminal. type Writer interface { // Buffer returns the current buffer. Buffer() *Buffer // ResetBuffer resets the current buffer. ResetBuffer() // UpdateBuffer updates the terminal display to reflect current buffer. UpdateBuffer(bufNoti, buf *Buffer, fullRefresh bool) error // ClearScreen clears the terminal screen and places the cursor at the top // left corner. ClearScreen() // ShowCursor shows the cursor. ShowCursor() // HideCursor hides the cursor. HideCursor() } // writer renders the editor UI. type writer struct { file io.Writer curBuf *Buffer } // NewWriter returns a Writer that writes VT100 sequences to the given io.Writer. func NewWriter(f io.Writer) Writer { return &writer{f, &Buffer{}} } func (w *writer) Buffer() *Buffer { return w.curBuf } func (w *writer) ResetBuffer() { w.curBuf = &Buffer{} } // deltaPos calculates the escape sequence needed to move the cursor from one // position to another. It use relative movements to move to the destination // line and absolute movement to move to the destination column. func deltaPos(from, to Pos) []byte { buf := new(bytes.Buffer) if from.Line < to.Line { // move down fmt.Fprintf(buf, "\033[%dB", to.Line-from.Line) } else if from.Line > to.Line { // move up fmt.Fprintf(buf, "\033[%dA", from.Line-to.Line) } fmt.Fprint(buf, "\r") if to.Col > 0 { fmt.Fprintf(buf, "\033[%dC", to.Col) } return buf.Bytes() } const ( hideCursor = "\033[?25l" showCursor = "\033[?25h" ) // UpdateBuffer updates the terminal display to reflect current buffer. func (w *writer) UpdateBuffer(bufNoti, buf *Buffer, fullRefresh bool) error { if buf.Width != w.curBuf.Width && w.curBuf.Lines != nil { // Width change, force full refresh w.curBuf.Lines = nil fullRefresh = true } bytesBuf := new(bytes.Buffer) bytesBuf.WriteString(hideCursor) // Rewind cursor if pLine := w.curBuf.Dot.Line; pLine > 0 { fmt.Fprintf(bytesBuf, "\033[%dA", pLine) } bytesBuf.WriteString("\r") if fullRefresh { // Erase from here. We may be in the top right corner of the screen; if // we simply do an erase here, tmux will save the current screen in the // scrollback buffer (presumably as a heuristics to detect full-screen // applications), but that is not something we want. So we write a space // first, and then erase, before rewinding back. // // Source code for tmux behavior: // https://github.com/tmux/tmux/blob/5f5f029e3b3a782dc616778739b2801b00b17c0e/screen-write.c#L1139 bytesBuf.WriteString(" \033[J\r") } // style of last written cell. style := "" switchStyle := func(newstyle string) { if newstyle != style { fmt.Fprintf(bytesBuf, "\033[0;%sm", newstyle) style = newstyle } } writeCells := func(cs []Cell) { for _, c := range cs { switchStyle(c.Style) bytesBuf.WriteString(c.Text) } } if bufNoti != nil { if logWriterDetail { logger.Printf("going to write %d lines of notifications", len(bufNoti.Lines)) } // Write notifications for _, line := range bufNoti.Lines { writeCells(line) switchStyle("") bytesBuf.WriteString("\033[K\n") } // TODO(xiaq): This is hacky; try to improve it. if len(w.curBuf.Lines) > 0 { w.curBuf.Lines = w.curBuf.Lines[1:] } } if logWriterDetail { logger.Printf("going to write %d lines, oldBuf had %d", len(buf.Lines), len(w.curBuf.Lines)) } for i, line := range buf.Lines { if i > 0 { bytesBuf.WriteString("\n") } var j int // First column where buf and oldBuf differ // No need to update current line if !fullRefresh && i < len(w.curBuf.Lines) { var eq bool if eq, j = CompareCells(line, w.curBuf.Lines[i]); eq { continue } } // Move to the first differing column if necessary. firstCol := CellsWidth(line[:j]) if firstCol != 0 { fmt.Fprintf(bytesBuf, "\033[%dC", firstCol) } // Erase the rest of the line if necessary. if !fullRefresh && i < len(w.curBuf.Lines) && j < len(w.curBuf.Lines[i]) { switchStyle("") bytesBuf.WriteString("\033[K") } writeCells(line[j:]) } if len(w.curBuf.Lines) > len(buf.Lines) && !fullRefresh { // If the old buffer is higher, erase old content. // Note that we cannot simply write \033[J, because if the cursor is // just over the last column -- which is precisely the case if we have a // rprompt, \033[J will also erase the last column. switchStyle("") bytesBuf.WriteString("\n\033[J\033[A") } switchStyle("") cursor := buf.Cursor() bytesBuf.Write(deltaPos(cursor, buf.Dot)) // Show cursor. bytesBuf.WriteString(showCursor) if logWriterDetail { logger.Printf("going to write %q", bytesBuf.String()) } _, err := w.file.Write(bytesBuf.Bytes()) if err != nil { return err } w.curBuf = buf return nil } func (w *writer) HideCursor() { fmt.Fprint(w.file, hideCursor) } func (w *writer) ShowCursor() { fmt.Fprint(w.file, showCursor) } func (w *writer) ClearScreen() { fmt.Fprint(w.file, "\033[H", // move cursor to the top left corner "\033[2J", // clear entire buffer ) } elvish-0.17.0/pkg/cli/term/writer_test.go000066400000000000000000000007511415471104000202720ustar00rootroot00000000000000package term import ( "strings" "testing" ) func TestWriter(t *testing.T) { sb := &strings.Builder{} testOutput := func(want string) { t.Helper() if sb.String() != want { t.Errorf("got %q, want %q", sb.String(), want) } sb.Reset() } w := NewWriter(sb) w.UpdateBuffer( NewBufferBuilder(10).Write("note 1").Buffer(), NewBufferBuilder(10).Write("line 1").SetDotHere().Buffer(), false) testOutput(hideCursor + "\rnote 1\033[K\n" + "line 1\r\033[6C" + showCursor) } elvish-0.17.0/pkg/cli/tk/000077500000000000000000000000001415471104000150345ustar00rootroot00000000000000elvish-0.17.0/pkg/cli/tk/codearea.go000066400000000000000000000230251415471104000171300ustar00rootroot00000000000000package tk import ( "bytes" "strings" "sync" "unicode" "unicode/utf8" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/ui" ) // CodeArea is a Widget for displaying and editing code. type CodeArea interface { Widget // CopyState returns a copy of the state. CopyState() CodeAreaState // MutateState calls the given the function while locking StateMutex. MutateState(f func(*CodeAreaState)) // Submit triggers the OnSubmit callback. Submit() } // CodeAreaSpec specifies the configuration and initial state for CodeArea. type CodeAreaSpec struct { // Key bindings. Bindings Bindings // A function that highlights the given code and returns any errors it has // found when highlighting. If this function is not given, the Widget does // not highlight the code nor show any errors. Highlighter func(code string) (ui.Text, []error) // Prompt callback. Prompt func() ui.Text // Right-prompt callback. RPrompt func() ui.Text // A function that calls the callback with string pairs for abbreviations // and their expansions. If this function is not given, the Widget does not // expand any abbreviations. Abbreviations func(f func(abbr, full string)) SmallWordAbbreviations func(f func(abbr, full string)) // A function that returns whether pasted texts (from bracketed pastes) // should be quoted. If this function is not given, the Widget defaults to // not quoting pasted texts. QuotePaste func() bool // A function that is called on the submit event. OnSubmit func() // State. When used in New, this field specifies the initial state. State CodeAreaState } // CodeAreaState keeps the mutable state of the CodeArea widget. type CodeAreaState struct { Buffer CodeBuffer Pending PendingCode HideRPrompt bool } // CodeBuffer represents the buffer of the CodeArea widget. type CodeBuffer struct { // Content of the buffer. Content string // Position of the dot (more commonly known as the cursor), as a byte index // into Content. Dot int } // PendingCode represents pending code, such as during completion. type PendingCode struct { // Beginning index of the text area that the pending code replaces, as a // byte index into RawState.Code. From int // End index of the text area that the pending code replaces, as a byte // index into RawState.Code. To int // The content of the pending code. Content string } // ApplyPending applies pending code to the code buffer, and resets pending code. func (s *CodeAreaState) ApplyPending() { s.Buffer, _, _ = patchPending(s.Buffer, s.Pending) s.Pending = PendingCode{} } func (c *CodeBuffer) InsertAtDot(text string) { *c = CodeBuffer{ Content: c.Content[:c.Dot] + text + c.Content[c.Dot:], Dot: c.Dot + len(text), } } type codeArea struct { // Mutex for synchronizing access to State. StateMutex sync.RWMutex // Configuration and state. CodeAreaSpec // Consecutively inserted text. Used for expanding abbreviations. inserts string // Value of State.CodeBuffer when handleKeyEvent was last called. Used for // detecting whether insertion has been interrupted. lastCodeBuffer CodeBuffer // Whether the widget is in the middle of bracketed pasting. pasting bool // Buffer for keeping Pasted text during bracketed pasting. pasteBuffer bytes.Buffer } // NewCodeArea creates a new CodeArea from the given spec. func NewCodeArea(spec CodeAreaSpec) CodeArea { if spec.Bindings == nil { spec.Bindings = DummyBindings{} } if spec.Highlighter == nil { spec.Highlighter = func(s string) (ui.Text, []error) { return ui.T(s), nil } } if spec.Prompt == nil { spec.Prompt = func() ui.Text { return nil } } if spec.RPrompt == nil { spec.RPrompt = func() ui.Text { return nil } } if spec.Abbreviations == nil { spec.Abbreviations = func(func(a, f string)) {} } if spec.SmallWordAbbreviations == nil { spec.SmallWordAbbreviations = func(func(a, f string)) {} } if spec.QuotePaste == nil { spec.QuotePaste = func() bool { return false } } if spec.OnSubmit == nil { spec.OnSubmit = func() {} } return &codeArea{CodeAreaSpec: spec} } // Submit emits a submit event with the current code content. func (w *codeArea) Submit() { w.OnSubmit() } // Render renders the code area, including the prompt and rprompt, highlighted // code, the cursor, and compilation errors in the code content. func (w *codeArea) Render(width, height int) *term.Buffer { b := w.render(width) truncateToHeight(b, height) return b } func (w *codeArea) MaxHeight(width, height int) int { return len(w.render(width).Lines) } func (w *codeArea) render(width int) *term.Buffer { view := getView(w) bb := term.NewBufferBuilder(width) renderView(view, bb) return bb.Buffer() } // Handle handles KeyEvent's of non-function keys, as well as PasteSetting // events. func (w *codeArea) Handle(event term.Event) bool { switch event := event.(type) { case term.PasteSetting: return w.handlePasteSetting(bool(event)) case term.KeyEvent: return w.handleKeyEvent(ui.Key(event)) } return false } func (w *codeArea) MutateState(f func(*CodeAreaState)) { w.StateMutex.Lock() defer w.StateMutex.Unlock() f(&w.State) } func (w *codeArea) CopyState() CodeAreaState { w.StateMutex.RLock() defer w.StateMutex.RUnlock() return w.State } func (w *codeArea) resetInserts() { w.inserts = "" w.lastCodeBuffer = CodeBuffer{} } func (w *codeArea) handlePasteSetting(start bool) bool { w.resetInserts() if start { w.pasting = true } else { text := w.pasteBuffer.String() if w.QuotePaste() { text = parse.Quote(text) } w.MutateState(func(s *CodeAreaState) { s.Buffer.InsertAtDot(text) }) w.pasting = false w.pasteBuffer = bytes.Buffer{} } return true } // Tries to expand a simple abbreviation. This function assumes that the state // mutex is already being held. func (w *codeArea) expandSimpleAbbr() { var abbr, full string // Find the longest matching abbreviation. w.Abbreviations(func(a, f string) { if strings.HasSuffix(w.inserts, a) && len(a) > len(abbr) { abbr, full = a, f } }) if len(abbr) > 0 { c := &w.State.Buffer *c = CodeBuffer{ Content: c.Content[:c.Dot-len(abbr)] + full + c.Content[c.Dot:], Dot: c.Dot - len(abbr) + len(full), } w.resetInserts() } } // Tries to expand a word abbreviation. This function assumes that the state // mutex is already being held. func (w *codeArea) expandWordAbbr(trigger rune, categorizer func(rune) int) { c := &w.State.Buffer if c.Dot < len(c.Content) { // Word abbreviations are only expanded at the end of the buffer. return } triggerLen := len(string(trigger)) if triggerLen >= len(w.inserts) { // Only the trigger has been inserted, or a simple abbreviation was just // expanded. In either case, there is nothing to expand. return } // The trigger is only used to determine word boundary; when considering // what to expand, we only consider the part that was inserted before it. inserts := w.inserts[:len(w.inserts)-triggerLen] var abbr, full string // Find the longest matching abbreviation. w.SmallWordAbbreviations(func(a, f string) { if len(a) <= len(abbr) { // This abbreviation can't be the longest. return } if !strings.HasSuffix(inserts, a) { // This abbreviation was not inserted. return } // Verify the trigger rune creates a word boundary. r, _ := utf8.DecodeLastRuneInString(a) if categorizer(trigger) == categorizer(r) { return } // Verify the rune preceding the abbreviation, if any, creates a word // boundary. if len(c.Content) > len(a)+triggerLen { r1, _ := utf8.DecodeLastRuneInString(c.Content[:len(c.Content)-len(a)-triggerLen]) r2, _ := utf8.DecodeRuneInString(a) if categorizer(r1) == categorizer(r2) { return } } abbr, full = a, f }) if len(abbr) > 0 { *c = CodeBuffer{ Content: c.Content[:c.Dot-len(abbr)-triggerLen] + full + string(trigger), Dot: c.Dot - len(abbr) + len(full), } w.resetInserts() } } func (w *codeArea) handleKeyEvent(key ui.Key) bool { isFuncKey := key.Mod != 0 || key.Rune < 0 if w.pasting { if isFuncKey { // TODO: Notify the user of the error, or insert the original // character as is. } else { w.pasteBuffer.WriteRune(key.Rune) } return true } if w.Bindings.Handle(w, term.KeyEvent(key)) { return true } // We only implement essential keybindings here. Other keybindings can be // added via handler overlays. switch key { case ui.K('\n'): w.resetInserts() w.Submit() return true case ui.K(ui.Backspace), ui.K('H', ui.Ctrl): w.resetInserts() w.MutateState(func(s *CodeAreaState) { c := &s.Buffer // Remove the last rune. _, chop := utf8.DecodeLastRuneInString(c.Content[:c.Dot]) *c = CodeBuffer{ Content: c.Content[:c.Dot-chop] + c.Content[c.Dot:], Dot: c.Dot - chop, } }) return true default: if isFuncKey || !unicode.IsGraphic(key.Rune) { w.resetInserts() return false } w.StateMutex.Lock() defer w.StateMutex.Unlock() if w.lastCodeBuffer != w.State.Buffer { // Something has happened between the last insert and this one; // reset the state. w.resetInserts() } s := string(key.Rune) w.State.Buffer.InsertAtDot(s) w.inserts += s w.lastCodeBuffer = w.State.Buffer w.expandSimpleAbbr() w.expandWordAbbr(key.Rune, CategorizeSmallWord) return true } } // IsAlnum determines if the rune is an alphanumeric character. func IsAlnum(r rune) bool { return unicode.IsLetter(r) || unicode.IsNumber(r) } // CategorizeSmallWord determines if the rune is whitespace, alphanum, or // something else. func CategorizeSmallWord(r rune) int { switch { case unicode.IsSpace(r): return 0 case IsAlnum(r): return 1 default: return 2 } } elvish-0.17.0/pkg/cli/tk/codearea_render.go000066400000000000000000000054571415471104000205000ustar00rootroot00000000000000package tk import ( "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/ui" "src.elv.sh/pkg/wcwidth" ) // View model, calculated from State and used for rendering. type view struct { prompt ui.Text rprompt ui.Text code ui.Text dot int errors []error } var stylingForPending = ui.Underlined func getView(w *codeArea) *view { s := w.CopyState() code, pFrom, pTo := patchPending(s.Buffer, s.Pending) styledCode, errors := w.Highlighter(code.Content) if pFrom < pTo { // Apply stylingForPending to [pFrom, pTo) parts := styledCode.Partition(pFrom, pTo) pending := ui.StyleText(parts[1], stylingForPending) styledCode = ui.Concat(parts[0], pending, parts[2]) } var rprompt ui.Text if !s.HideRPrompt { rprompt = w.RPrompt() } return &view{w.Prompt(), rprompt, styledCode, code.Dot, errors} } func patchPending(c CodeBuffer, p PendingCode) (CodeBuffer, int, int) { if p.From > p.To || p.From < 0 || p.To > len(c.Content) { // Invalid Pending. return c, 0, 0 } if p.From == p.To && p.Content == "" { return c, 0, 0 } newContent := c.Content[:p.From] + p.Content + c.Content[p.To:] newDot := 0 switch { case c.Dot < p.From: // Dot is before the replaced region. Keep it. newDot = c.Dot case c.Dot >= p.From && c.Dot < p.To: // Dot is within the replaced region. Place the dot at the end. newDot = p.From + len(p.Content) case c.Dot >= p.To: // Dot is after the replaced region. Maintain the relative position of // the dot. newDot = c.Dot - (p.To - p.From) + len(p.Content) } return CodeBuffer{Content: newContent, Dot: newDot}, p.From, p.From + len(p.Content) } func renderView(v *view, buf *term.BufferBuilder) { buf.EagerWrap = true buf.WriteStyled(v.prompt) if len(buf.Lines) == 1 && buf.Col*2 < buf.Width { buf.Indent = buf.Col } parts := v.code.Partition(v.dot) buf. WriteStyled(parts[0]). SetDotHere(). WriteStyled(parts[1]) buf.EagerWrap = false buf.Indent = 0 // Handle rprompts with newlines. if rpromptWidth := styledWcswidth(v.rprompt); rpromptWidth > 0 { padding := buf.Width - buf.Col - rpromptWidth if padding >= 1 { buf.WriteSpaces(padding) buf.WriteStyled(v.rprompt) } } if len(v.errors) > 0 { for _, err := range v.errors { buf.Newline() buf.Write(err.Error()) } } } func truncateToHeight(b *term.Buffer, maxHeight int) { switch { case len(b.Lines) <= maxHeight: // We can show all line; do nothing. case b.Dot.Line < maxHeight: // We can show all lines before the cursor, and as many lines after the // cursor as we can, adding up to maxHeight. b.TrimToLines(0, maxHeight) default: // We can show maxHeight lines before and including the cursor line. b.TrimToLines(b.Dot.Line-maxHeight+1, b.Dot.Line+1) } } func styledWcswidth(t ui.Text) int { w := 0 for _, seg := range t { w += wcwidth.Of(seg.Text) } return w } elvish-0.17.0/pkg/cli/tk/codearea_test.go000066400000000000000000000355051415471104000201750ustar00rootroot00000000000000package tk import ( "errors" "reflect" "testing" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/tt" "src.elv.sh/pkg/ui" ) var bb = term.NewBufferBuilder func p(t ui.Text) func() ui.Text { return func() ui.Text { return t } } var codeAreaRenderTests = []renderTest{ { Name: "prompt only", Given: NewCodeArea(CodeAreaSpec{ Prompt: p(ui.T("~>", ui.Bold))}), Width: 10, Height: 24, Want: bb(10).WriteStringSGR("~>", "1").SetDotHere(), }, { Name: "rprompt only", Given: NewCodeArea(CodeAreaSpec{ RPrompt: p(ui.T("RP", ui.Inverse))}), Width: 10, Height: 24, Want: bb(10).SetDotHere().WriteSpaces(8).WriteStringSGR("RP", "7"), }, { Name: "code only with dot at beginning", Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{ Buffer: CodeBuffer{Content: "code", Dot: 0}}}), Width: 10, Height: 24, Want: bb(10).SetDotHere().Write("code"), }, { Name: "code only with dot at middle", Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{ Buffer: CodeBuffer{Content: "code", Dot: 2}}}), Width: 10, Height: 24, Want: bb(10).Write("co").SetDotHere().Write("de"), }, { Name: "code only with dot at end", Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{ Buffer: CodeBuffer{Content: "code", Dot: 4}}}), Width: 10, Height: 24, Want: bb(10).Write("code").SetDotHere(), }, { Name: "prompt, code and rprompt", Given: NewCodeArea(CodeAreaSpec{ Prompt: p(ui.T("~>")), RPrompt: p(ui.T("RP")), State: CodeAreaState{Buffer: CodeBuffer{Content: "code", Dot: 4}}}), Width: 10, Height: 24, Want: bb(10).Write("~>code").SetDotHere().Write(" RP"), }, { Name: "prompt explicitly hidden ", Given: NewCodeArea(CodeAreaSpec{ Prompt: p(ui.T("~>")), RPrompt: p(ui.T("RP")), State: CodeAreaState{Buffer: CodeBuffer{Content: "code", Dot: 4}, HideRPrompt: true}}), Width: 10, Height: 24, Want: bb(10).Write("~>code").SetDotHere(), }, { Name: "rprompt too long", Given: NewCodeArea(CodeAreaSpec{ Prompt: p(ui.T("~>")), RPrompt: p(ui.T("1234")), State: CodeAreaState{Buffer: CodeBuffer{Content: "code", Dot: 4}}}), Width: 10, Height: 24, Want: bb(10).Write("~>code").SetDotHere(), }, { Name: "highlighted code", Given: NewCodeArea(CodeAreaSpec{ Highlighter: func(code string) (ui.Text, []error) { return ui.T(code, ui.Bold), nil }, State: CodeAreaState{Buffer: CodeBuffer{Content: "code", Dot: 4}}}), Width: 10, Height: 24, Want: bb(10).WriteStringSGR("code", "1").SetDotHere(), }, { Name: "static errors in code", Given: NewCodeArea(CodeAreaSpec{ Prompt: p(ui.T("> ")), Highlighter: func(code string) (ui.Text, []error) { err := errors.New("static error") return ui.T(code), []error{err} }, State: CodeAreaState{Buffer: CodeBuffer{Content: "code", Dot: 4}}}), Width: 10, Height: 24, Want: bb(10).Write("> code").SetDotHere(). Newline().Write("static error"), }, { Name: "pending code inserting at the dot", Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{ Buffer: CodeBuffer{Content: "code", Dot: 4}, Pending: PendingCode{From: 4, To: 4, Content: "x"}, }}), Width: 10, Height: 24, Want: bb(10).Write("code").WriteStringSGR("x", "4").SetDotHere(), }, { Name: "pending code replacing at the dot", Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{ Buffer: CodeBuffer{Content: "code", Dot: 2}, Pending: PendingCode{From: 2, To: 4, Content: "x"}, }}), Width: 10, Height: 24, Want: bb(10).Write("co").WriteStringSGR("x", "4").SetDotHere(), }, { Name: "pending code to the left of the dot", Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{ Buffer: CodeBuffer{Content: "code", Dot: 4}, Pending: PendingCode{From: 1, To: 3, Content: "x"}, }}), Width: 10, Height: 24, Want: bb(10).Write("c").WriteStringSGR("x", "4").Write("e").SetDotHere(), }, { Name: "pending code to the right of the cursor", Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{ Buffer: CodeBuffer{Content: "code", Dot: 1}, Pending: PendingCode{From: 2, To: 3, Content: "x"}, }}), Width: 10, Height: 24, Want: bb(10).Write("c").SetDotHere().Write("o"). WriteStringSGR("x", "4").Write("e"), }, { Name: "ignore invalid pending code 1", Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{ Buffer: CodeBuffer{Content: "code", Dot: 4}, Pending: PendingCode{From: 2, To: 1, Content: "x"}, }}), Width: 10, Height: 24, Want: bb(10).Write("code").SetDotHere(), }, { Name: "ignore invalid pending code 2", Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{ Buffer: CodeBuffer{Content: "code", Dot: 4}, Pending: PendingCode{From: 5, To: 6, Content: "x"}, }}), Width: 10, Height: 24, Want: bb(10).Write("code").SetDotHere(), }, { Name: "prioritize lines before the cursor with small height", Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{ Buffer: CodeBuffer{Content: "a\nb\nc\nd", Dot: 3}, }}), Width: 10, Height: 2, Want: bb(10).Write("a").Newline().Write("b").SetDotHere(), }, { Name: "show only the cursor line when height is 1", Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{ Buffer: CodeBuffer{Content: "a\nb\nc\nd", Dot: 3}, }}), Width: 10, Height: 1, Want: bb(10).Write("b").SetDotHere(), }, { Name: "show lines after the cursor when all lines before the cursor are shown", Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{ Buffer: CodeBuffer{Content: "a\nb\nc\nd", Dot: 3}, }}), Width: 10, Height: 3, Want: bb(10).Write("a").Newline().Write("b").SetDotHere(). Newline().Write("c"), }, } func TestCodeArea_Render(t *testing.T) { testRender(t, codeAreaRenderTests) } var codeAreaHandleTests = []handleTest{ { Name: "simple inserts", Given: NewCodeArea(CodeAreaSpec{}), Events: []term.Event{term.K('c'), term.K('o'), term.K('d'), term.K('e')}, WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "code", Dot: 4}}, }, { Name: "unicode inserts", Given: NewCodeArea(CodeAreaSpec{}), Events: []term.Event{term.K('你'), term.K('好')}, WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "你好", Dot: 6}}, }, { Name: "unterminated paste", Given: NewCodeArea(CodeAreaSpec{}), Events: []term.Event{term.PasteSetting(true), term.K('"'), term.K('x')}, WantNewState: CodeAreaState{}, }, { Name: "literal paste", Given: NewCodeArea(CodeAreaSpec{}), Events: []term.Event{ term.PasteSetting(true), term.K('"'), term.K('x'), term.PasteSetting(false)}, WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "\"x", Dot: 2}}, }, { Name: "literal paste swallowing functional keys", Given: NewCodeArea(CodeAreaSpec{}), Events: []term.Event{ term.PasteSetting(true), term.K('a'), term.K(ui.F1), term.K('b'), term.PasteSetting(false)}, WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "ab", Dot: 2}}, }, { Name: "quoted paste", Given: NewCodeArea(CodeAreaSpec{QuotePaste: func() bool { return true }}), Events: []term.Event{ term.PasteSetting(true), term.K('"'), term.K('x'), term.PasteSetting(false)}, WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "'\"x'", Dot: 4}}, }, { Name: "backspace at end of code", Given: NewCodeArea(CodeAreaSpec{}), Events: []term.Event{ term.K('c'), term.K('o'), term.K('d'), term.K('e'), term.K(ui.Backspace)}, WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "cod", Dot: 3}}, }, { Name: "backspace at middle of buffer", Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{ Buffer: CodeBuffer{Content: "code", Dot: 2}}}), Events: []term.Event{term.K(ui.Backspace)}, WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "cde", Dot: 1}}, }, { Name: "backspace at beginning of buffer", Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{ Buffer: CodeBuffer{Content: "code", Dot: 0}}}), Events: []term.Event{term.K(ui.Backspace)}, WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "code", Dot: 0}}, }, { Name: "backspace deleting unicode character", Given: NewCodeArea(CodeAreaSpec{}), Events: []term.Event{ term.K('你'), term.K('好'), term.K(ui.Backspace)}, WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "你", Dot: 3}}, }, // Regression test for https://b.elv.sh/1178 { Name: "Ctrl-H being equivalent to backspace", Given: NewCodeArea(CodeAreaSpec{}), Events: []term.Event{ term.K('c'), term.K('o'), term.K('d'), term.K('e'), term.K('H', ui.Ctrl)}, WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "cod", Dot: 3}}, }, { Name: "abbreviation expansion", Given: NewCodeArea(CodeAreaSpec{ Abbreviations: func(f func(abbr, full string)) { f("dn", "/dev/null") }, }), Events: []term.Event{term.K('d'), term.K('n')}, WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "/dev/null", Dot: 9}}, }, { Name: "abbreviation expansion 2", Given: NewCodeArea(CodeAreaSpec{ Abbreviations: func(f func(abbr, full string)) { f("||", " | less") }, }), Events: []term.Event{term.K('x'), term.K('|'), term.K('|')}, WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "x | less", Dot: 8}}, }, { Name: "abbreviation expansion after other content", Given: NewCodeArea(CodeAreaSpec{ Abbreviations: func(f func(abbr, full string)) { f("||", " | less") }, }), Events: []term.Event{term.K('{'), term.K('e'), term.K('c'), term.K('h'), term.K('o'), term.K(' '), term.K('x'), term.K('}'), term.K('|'), term.K('|')}, WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "{echo x} | less", Dot: 15}}, }, { Name: "abbreviation expansion preferring longest", Given: NewCodeArea(CodeAreaSpec{ Abbreviations: func(f func(abbr, full string)) { f("n", "none") f("dn", "/dev/null") }, }), Events: []term.Event{term.K('d'), term.K('n')}, WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "/dev/null", Dot: 9}}, }, { Name: "abbreviation expansion interrupted by function key", Given: NewCodeArea(CodeAreaSpec{ Abbreviations: func(f func(abbr, full string)) { f("dn", "/dev/null") }, }), Events: []term.Event{term.K('d'), term.K(ui.F1), term.K('n')}, WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "dn", Dot: 2}}, }, { Name: "small word abbreviation expansion space trigger", Given: NewCodeArea(CodeAreaSpec{ SmallWordAbbreviations: func(f func(abbr, full string)) { f("eh", "echo hello") }, }), Events: []term.Event{term.K('e'), term.K('h'), term.K(' ')}, WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "echo hello ", Dot: 11}}, }, { Name: "small word abbreviation expansion non-space trigger", Given: NewCodeArea(CodeAreaSpec{ SmallWordAbbreviations: func(f func(abbr, full string)) { f("h", "hello") }, }), Events: []term.Event{term.K('x'), term.K('['), term.K('h'), term.K(']')}, WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "x[hello]", Dot: 8}}, }, { Name: "small word abbreviation expansion preceding char invalid", Given: NewCodeArea(CodeAreaSpec{ SmallWordAbbreviations: func(f func(abbr, full string)) { f("h", "hello") }, }), Events: []term.Event{term.K('g'), term.K('h'), term.K(' ')}, WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "gh ", Dot: 3}}, }, { Name: "small word abbreviation expansion after backspace preceding char invalid", Given: NewCodeArea(CodeAreaSpec{ SmallWordAbbreviations: func(f func(abbr, full string)) { f("h", "hello") }, }), Events: []term.Event{term.K('g'), term.K(' '), term.K(ui.Backspace), term.K('h'), term.K(' ')}, WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "gh ", Dot: 3}}, }, { Name: "key bindings", Given: NewCodeArea(CodeAreaSpec{Bindings: MapBindings{ term.K('a'): func(w Widget) { w.(*codeArea).State.Buffer.InsertAtDot("b") }}, }), Events: []term.Event{term.K('a')}, WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "b", Dot: 1}}, }, { // Regression test for #890. Name: "key bindings do not apply when pasting", Given: NewCodeArea(CodeAreaSpec{Bindings: MapBindings{ term.K('\n'): func(w Widget) {}}, }), Events: []term.Event{ term.PasteSetting(true), term.K('\n'), term.PasteSetting(false)}, WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "\n", Dot: 1}}, }, } func TestCodeArea_Handle(t *testing.T) { testHandle(t, codeAreaHandleTests) } var codeAreaUnhandledEvents = []term.Event{ // Mouse events are unhandled term.MouseEvent{}, // Function keys are unhandled (except Backspace) term.K(ui.F1), term.K('X', ui.Ctrl), } func TestCodeArea_Handle_UnhandledEvents(t *testing.T) { w := NewCodeArea(CodeAreaSpec{}) for _, event := range codeAreaUnhandledEvents { handled := w.Handle(event) if handled { t.Errorf("event %v got handled", event) } } } func TestCodeArea_Handle_AbbreviationExpansionInterruptedByExternalMutation(t *testing.T) { w := NewCodeArea(CodeAreaSpec{ Abbreviations: func(f func(abbr, full string)) { f("dn", "/dev/null") }, }) w.Handle(term.K('d')) w.MutateState(func(s *CodeAreaState) { s.Buffer.InsertAtDot("d") }) w.Handle(term.K('n')) wantState := CodeAreaState{Buffer: CodeBuffer{Content: "ddn", Dot: 3}} if state := w.CopyState(); !reflect.DeepEqual(state, wantState) { t.Errorf("got state %v, want %v", state, wantState) } } func TestCodeArea_Handle_EnterEmitsSubmit(t *testing.T) { submitted := false w := NewCodeArea(CodeAreaSpec{ OnSubmit: func() { submitted = true }, State: CodeAreaState{Buffer: CodeBuffer{Content: "code", Dot: 4}}}) w.Handle(term.K('\n')) if submitted != true { t.Errorf("OnSubmit not triggered") } } func TestCodeArea_Handle_DefaultNoopSubmit(t *testing.T) { w := NewCodeArea(CodeAreaSpec{State: CodeAreaState{ Buffer: CodeBuffer{Content: "code", Dot: 4}}}) w.Handle(term.K('\n')) // No panic, we are good } func TestCodeArea_State(t *testing.T) { w := NewCodeArea(CodeAreaSpec{}) w.MutateState(func(s *CodeAreaState) { s.Buffer.Content = "code" }) if w.CopyState().Buffer.Content != "code" { t.Errorf("state not mutated") } } func TestCodeAreaState_ApplyPending(t *testing.T) { applyPending := func(s CodeAreaState) CodeAreaState { s.ApplyPending() return s } tt.Test(t, tt.Fn("applyPending", applyPending), tt.Table{ tt.Args(CodeAreaState{Buffer: CodeBuffer{}, Pending: PendingCode{0, 0, "ls"}}). Rets(CodeAreaState{Buffer: CodeBuffer{Content: "ls", Dot: 2}, Pending: PendingCode{}}), tt.Args(CodeAreaState{Buffer: CodeBuffer{"x", 1}, Pending: PendingCode{0, 0, "ls"}}). Rets(CodeAreaState{Buffer: CodeBuffer{Content: "lsx", Dot: 3}, Pending: PendingCode{}}), // No-op when Pending is empty. tt.Args(CodeAreaState{Buffer: CodeBuffer{"x", 1}}). Rets(CodeAreaState{Buffer: CodeBuffer{Content: "x", Dot: 1}}), // HideRPrompt is kept intact. tt.Args(CodeAreaState{Buffer: CodeBuffer{"x", 1}, HideRPrompt: true}). Rets(CodeAreaState{Buffer: CodeBuffer{Content: "x", Dot: 1}, HideRPrompt: true}), }) } elvish-0.17.0/pkg/cli/tk/colview.go000066400000000000000000000123051415471104000170340ustar00rootroot00000000000000package tk import ( "sync" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/ui" ) // ColView is a Widget that arranges several widgets in a column. type ColView interface { Widget // MutateState mutates the state. MutateState(f func(*ColViewState)) // CopyState returns a copy of the state. CopyState() ColViewState // Left triggers the OnLeft callback. Left() // Right triggers the OnRight callback. Right() } // ColViewSpec specifies the configuration and initial state for ColView. type ColViewSpec struct { // Key bindings. Bindings Bindings // A function that takes the number of columns and return weights for the // widths of the columns. The returned slice must have a size of n. If this // function is nil, all the columns will have the same weight. Weights func(n int) []int // A function called when the Left method of Widget is called, or when Left // is pressed and unhandled. OnLeft func(w ColView) // A function called when the Right method of Widget is called, or when // Right is pressed and unhandled. OnRight func(w ColView) // State. Specifies the initial state when used in New. State ColViewState } // ColViewState keeps the mutable state of the ColView widget. type ColViewState struct { Columns []Widget FocusColumn int } type colView struct { // Mutex for synchronizing access to State. StateMutex sync.RWMutex ColViewSpec } // NewColView creates a new ColView from the given spec. func NewColView(spec ColViewSpec) ColView { if spec.Bindings == nil { spec.Bindings = DummyBindings{} } if spec.Weights == nil { spec.Weights = equalWeights } if spec.OnLeft == nil { spec.OnLeft = func(ColView) {} } if spec.OnRight == nil { spec.OnRight = func(ColView) {} } return &colView{ColViewSpec: spec} } func equalWeights(n int) []int { weights := make([]int, n) for i := 0; i < n; i++ { weights[i] = 1 } return weights } func (w *colView) MutateState(f func(*ColViewState)) { w.StateMutex.Lock() defer w.StateMutex.Unlock() f(&w.State) } func (w *colView) CopyState() ColViewState { w.StateMutex.RLock() defer w.StateMutex.RUnlock() copied := w.State copied.Columns = append([]Widget(nil), w.State.Columns...) return copied } const colViewColGap = 1 // Render renders all the columns side by side, putting the dot in the focused // column. func (w *colView) Render(width, height int) *term.Buffer { cols, widths := w.prepareRender(width) if len(cols) == 0 { return &term.Buffer{Width: width} } var buf term.Buffer for i, col := range cols { if i > 0 { buf.Width += colViewColGap } bufCol := col.Render(widths[i], height) buf.ExtendRight(bufCol) } return &buf } func (w *colView) MaxHeight(width, height int) int { cols, widths := w.prepareRender(width) max := 0 for i, col := range cols { colMax := col.MaxHeight(widths[i], height) if max < colMax { max = colMax } } return max } // Returns widgets in and widths of columns. func (w *colView) prepareRender(width int) ([]Widget, []int) { state := w.CopyState() ncols := len(state.Columns) if ncols == 0 { // No column. return nil, nil } if width < ncols { // To narrow; give up by rendering nothing. return nil, nil } widths := distribute(width-(ncols-1)*colViewColGap, w.Weights(ncols)) return state.Columns, widths } // Handle handles the event first by consulting the overlay handler, and then // delegating the event to the currently focused column. func (w *colView) Handle(event term.Event) bool { if w.Bindings.Handle(w, event) { return true } state := w.CopyState() if 0 <= state.FocusColumn && state.FocusColumn < len(state.Columns) { if state.Columns[state.FocusColumn].Handle(event) { return true } } switch event { case term.K(ui.Left): w.Left() return true case term.K(ui.Right): w.Right() return true default: return false } } func (w *colView) Left() { w.OnLeft(w) } func (w *colView) Right() { w.OnRight(w) } // Distributes fullWidth according to the weights, rounding to integers. // // This works iteratively each step by taking the sum of all remaining weights, // and using floor(remainedWidth * currentWeight / remainedAllWeights) for the // current column. // // A simpler alternative is to simply use floor(fullWidth * currentWeight / // allWeights) at each step, and also giving the remainder to the last column. // However, this means that the last column gets all the rounding errors from // flooring, which can be big. The more sophisticated algorithm distributes the // rounding errors among all the remaining elements and can result in a much // better distribution, and as a special upside, does not need to handle the // last column as a special case. // // As an extreme example, consider the case of fullWidth = 9, weights = {1, 1, // 1, 1, 1} (five 1's). Using the simplistic algorithm, the widths are {1, 1, 1, // 1, 5}. Using the more complex algorithm, the widths are {1, 2, 2, 2, 2}. func distribute(fullWidth int, weights []int) []int { remainedWidth := fullWidth remainedWeight := 0 for _, weight := range weights { remainedWeight += weight } widths := make([]int, len(weights)) for i, weight := range weights { widths[i] = remainedWidth * weight / remainedWeight remainedWidth -= widths[i] remainedWeight -= weight } return widths } elvish-0.17.0/pkg/cli/tk/colview_test.go000066400000000000000000000065641415471104000201050ustar00rootroot00000000000000package tk import ( "testing" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/tt" "src.elv.sh/pkg/ui" ) var colViewRenderTests = []renderTest{ { Name: "colview no column", Given: NewColView(ColViewSpec{}), Width: 10, Height: 24, Want: &term.Buffer{Width: 10}, }, { Name: "colview width < number of columns", Given: NewColView(ColViewSpec{State: ColViewState{ Columns: []Widget{ makeListbox("x", 2, 0), makeListbox("y", 1, 0), makeListbox("z", 3, 0), makeListbox("w", 1, 0), }, }}), Width: 3, Height: 24, Want: &term.Buffer{Width: 3}, }, { Name: "colview normal", Given: NewColView(ColViewSpec{State: ColViewState{ Columns: []Widget{ makeListbox("x", 2, 1), makeListbox("y", 1, 0), makeListbox("z", 3, -1), }, }}), Width: 11, Height: 24, Want: term.NewBufferBuilder(11). // first line Write("x0 "). Write("y0 ", ui.Inverse). Write(" z0"). // second line Newline().Write("x1 ", ui.Inverse). Write(" z1"). // third line Newline().Write(" z2"), }, } func makeListbox(prefix string, n, selected int) Widget { return NewListBox(ListBoxSpec{ State: ListBoxState{ Items: TestItems{Prefix: prefix, NItems: n}, Selected: selected, }}) } func TestColView_Render(t *testing.T) { testRender(t, colViewRenderTests) } func TestColView_Handle(t *testing.T) { // Channel for recording the place an event was handled. -1 for the widget // itself, column index for column. handledBy := make(chan int, 10) w := NewColView(ColViewSpec{ Bindings: MapBindings{ term.K('a'): func(Widget) { handledBy <- -1 }, }, State: ColViewState{ Columns: []Widget{ NewListBox(ListBoxSpec{ Bindings: MapBindings{ term.K('a'): func(Widget) { handledBy <- 0 }, term.K('b'): func(Widget) { handledBy <- 0 }, }}), NewListBox(ListBoxSpec{ Bindings: MapBindings{ term.K('a'): func(Widget) { handledBy <- 1 }, term.K('b'): func(Widget) { handledBy <- 1 }, }}), }, FocusColumn: 1, }, OnLeft: func(ColView) { handledBy <- 100 }, OnRight: func(ColView) { handledBy <- 101 }, }) expectHandled := func(event term.Event, wantBy int) { t.Helper() handled := w.Handle(event) if !handled { t.Errorf("Handle -> false, want true") } if by := <-handledBy; by != wantBy { t.Errorf("Handled by %d, want %d", by, wantBy) } } expectUnhandled := func(event term.Event) { t.Helper() handled := w.Handle(event) if handled { t.Errorf("Handle -> true, want false") } } // Event handled by widget's overlay handler. expectHandled(term.K('a'), -1) // Event handled by the focused column. expectHandled(term.K('b'), 1) // Fallback handler for Left expectHandled(term.K(ui.Left), 100) // Fallback handler for Left expectHandled(term.K(ui.Right), 101) // No one to handle the event. expectUnhandled(term.K('c')) // No focused column: event unhandled w.MutateState(func(s *ColViewState) { s.FocusColumn = -1 }) expectUnhandled(term.K('b')) } func TestDistribute(t *testing.T) { tt.Test(t, tt.Fn("distribute", distribute), tt.Table{ // Nice integer distributions. tt.Args(10, []int{1, 1}).Rets([]int{5, 5}), tt.Args(10, []int{2, 3}).Rets([]int{4, 6}), tt.Args(10, []int{1, 2, 2}).Rets([]int{2, 4, 4}), // Approximate integer distributions. tt.Args(10, []int{1, 1, 1}).Rets([]int{3, 3, 4}), tt.Args(5, []int{1, 1, 1}).Rets([]int{1, 2, 2}), }) } elvish-0.17.0/pkg/cli/tk/combobox.go000066400000000000000000000041031415471104000171710ustar00rootroot00000000000000package tk import ( "src.elv.sh/pkg/cli/term" ) // ComboBox is a Widget that combines a ListBox and a CodeArea. type ComboBox interface { Widget // Returns the embedded codearea widget. CodeArea() CodeArea // Returns the embedded listbox widget. ListBox() ListBox // Forces the filtering to rerun. Refilter() } // ComboBoxSpec specifies the configuration and initial state for ComboBox. type ComboBoxSpec struct { CodeArea CodeAreaSpec ListBox ListBoxSpec OnFilter func(ComboBox, string) } type comboBox struct { codeArea CodeArea listBox ListBox OnFilter func(ComboBox, string) // Last filter value. lastFilter string } // NewComboBox creates a new ComboBox from the given spec. func NewComboBox(spec ComboBoxSpec) ComboBox { if spec.OnFilter == nil { spec.OnFilter = func(ComboBox, string) {} } w := &comboBox{ codeArea: NewCodeArea(spec.CodeArea), listBox: NewListBox(spec.ListBox), OnFilter: spec.OnFilter, } w.OnFilter(w, "") return w } // Render renders the codearea and the listbox below it. func (w *comboBox) Render(width, height int) *term.Buffer { buf := w.codeArea.Render(width, height) bufListBox := w.listBox.Render(width, height-len(buf.Lines)) buf.Extend(bufListBox, false) return buf } func (w *comboBox) MaxHeight(width, height int) int { return w.codeArea.MaxHeight(width, height) + w.listBox.MaxHeight(width, height) } // Handle first lets the listbox handle the event, and if it is unhandled, lets // the codearea handle it. If the codearea has handled the event and the code // content has changed, it calls OnFilter with the new content. func (w *comboBox) Handle(event term.Event) bool { if w.listBox.Handle(event) { return true } if w.codeArea.Handle(event) { filter := w.codeArea.CopyState().Buffer.Content if filter != w.lastFilter { w.OnFilter(w, filter) w.lastFilter = filter } return true } return false } func (w *comboBox) Refilter() { w.OnFilter(w, w.codeArea.CopyState().Buffer.Content) } func (w *comboBox) CodeArea() CodeArea { return w.codeArea } func (w *comboBox) ListBox() ListBox { return w.listBox } elvish-0.17.0/pkg/cli/tk/combobox_test.go000066400000000000000000000053511415471104000202360ustar00rootroot00000000000000package tk import ( "testing" "time" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/ui" ) var comboBoxRenderTests = []renderTest{ { Name: "rendering codearea and listbox", Given: NewComboBox(ComboBoxSpec{ CodeArea: CodeAreaSpec{ State: CodeAreaState{ Buffer: CodeBuffer{Content: "filter", Dot: 6}}}, ListBox: ListBoxSpec{ State: ListBoxState{Items: TestItems{NItems: 2}}}}), Width: 10, Height: 24, Want: term.NewBufferBuilder(10). Write("filter").SetDotHere(). Newline().Write("item 0 ", ui.Inverse). Newline().Write("item 1"), }, { Name: "calling filter before rendering", Given: NewComboBox(ComboBoxSpec{ CodeArea: CodeAreaSpec{ State: CodeAreaState{ Buffer: CodeBuffer{Content: "filter", Dot: 6}}}, OnFilter: func(w ComboBox, filter string) { w.ListBox().Reset(TestItems{NItems: 2}, 0) }}), Width: 10, Height: 24, Want: term.NewBufferBuilder(10). Write("filter").SetDotHere(). Newline().Write("item 0 ", ui.Inverse). Newline().Write("item 1"), }, } func TestComboBox_Render(t *testing.T) { testRender(t, comboBoxRenderTests) } func TestComboBox_Handle(t *testing.T) { var onFilterCalled bool var lastFilter string w := NewComboBox(ComboBoxSpec{ OnFilter: func(w ComboBox, filter string) { onFilterCalled = true lastFilter = filter }, ListBox: ListBoxSpec{ State: ListBoxState{Items: TestItems{NItems: 2}}}}) handled := w.Handle(term.K(ui.Down)) if !handled { t.Errorf("listbox did not handle") } if w.ListBox().CopyState().Selected != 1 { t.Errorf("listbox state not changed") } handled = w.Handle(term.K('a')) if !handled { t.Errorf("codearea did not handle letter key") } if w.CodeArea().CopyState().Buffer.Content != "a" { t.Errorf("codearea state not changed") } if lastFilter != "a" { t.Errorf("OnFilter not called when codearea content changed") } onFilterCalled = false handled = w.Handle(term.PasteSetting(true)) if !handled { t.Errorf("codearea did not handle PasteSetting") } if onFilterCalled { t.Errorf("OnFilter called when codearea content did not change") } w.Handle(term.PasteSetting(false)) handled = w.Handle(term.K('D', ui.Ctrl)) if handled { t.Errorf("key unhandled by codearea and listbox got handled") } } func TestRefilter(t *testing.T) { onFilter := make(chan string, 100) w := NewComboBox(ComboBoxSpec{ OnFilter: func(w ComboBox, filter string) { onFilter <- filter }}) <-onFilter // Ignore the initial OnFilter call. w.CodeArea().MutateState(func(s *CodeAreaState) { s.Buffer.Content = "new" }) w.Refilter() select { case f := <-onFilter: if f != "new" { t.Errorf("OnFilter called with %q, want 'new'", f) } case <-time.After(time.Second): t.Errorf("OnFilter not called by Refilter") } } elvish-0.17.0/pkg/cli/tk/empty.go000066400000000000000000000007721415471104000165270ustar00rootroot00000000000000package tk import ( "src.elv.sh/pkg/cli/term" ) // Empty is an empty widget. type Empty struct{} // Render shows nothing, although the resulting Buffer still occupies one line. func (Empty) Render(width, height int) *term.Buffer { return term.NewBufferBuilder(width).Buffer() } // MaxHeight returns 1, since this widget always occupies one line. func (Empty) MaxHeight(width, height int) int { return 1 } // Handle always returns false. func (Empty) Handle(event term.Event) bool { return false } elvish-0.17.0/pkg/cli/tk/label.go000066400000000000000000000014711415471104000164450ustar00rootroot00000000000000package tk import ( "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/ui" ) // Label is a Renderer that writes out a text. type Label struct { Content ui.Text } // Render shows the content. If the given box is too small, the text is cropped. func (l Label) Render(width, height int) *term.Buffer { // TODO: Optimize by stopping as soon as $height rows are written. b := l.render(width) b.TrimToLines(0, height) return b } // MaxHeight returns the maximum height the Label can take when rendering within // a bound box. func (l Label) MaxHeight(width, height int) int { return len(l.render(width).Lines) } func (l Label) render(width int) *term.Buffer { return term.NewBufferBuilder(width).WriteStyled(l.Content).Buffer() } // Handle always returns false. func (l Label) Handle(event term.Event) bool { return false } elvish-0.17.0/pkg/cli/tk/layout_test.go000066400000000000000000000047111415471104000177420ustar00rootroot00000000000000package tk import ( "reflect" "testing" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/ui" ) var layoutRenderTests = []struct { name string renderer Renderer width int height int wantBuf *term.BufferBuilder }{ { "empty widget", Empty{}, 10, 24, bb(10), }, { "Label showing all", Label{ui.T("label")}, 10, 24, bb(10).Write("label"), }, { "Label cropping", Label{ui.T("label")}, 4, 1, bb(4).Write("labe"), }, { "VScrollbar showing full thumb", VScrollbar{4, 0, 3}, 10, 2, bb(1).WriteStyled(vscrollbarThumb).WriteStyled(vscrollbarThumb), }, { "VScrollbar showing thumb in first half", VScrollbar{4, 0, 1}, 10, 2, bb(1).WriteStyled(vscrollbarThumb).WriteStyled(vscrollbarTrough), }, { "VScrollbar showing a minimal 1-size thumb at beginning", VScrollbar{4, 0, 0}, 10, 2, bb(1).WriteStyled(vscrollbarThumb).WriteStyled(vscrollbarTrough), }, { "VScrollbar showing a minimal 1-size thumb at end", VScrollbar{4, 3, 3}, 10, 2, bb(1).WriteStyled(vscrollbarTrough).WriteStyled(vscrollbarThumb), }, { "VScrollbarContainer", VScrollbarContainer{Label{ui.T("abcd1234")}, VScrollbar{4, 0, 1}}, 5, 2, bb(5).Write("abcd").WriteStyled(vscrollbarThumb). Newline().Write("1234").WriteStyled(vscrollbarTrough), }, { "HScrollbar showing full thumb", HScrollbar{4, 0, 3}, 2, 10, bb(2).WriteStyled(hscrollbarThumb).WriteStyled(hscrollbarThumb), }, { "HScrollbar showing thumb in first half", HScrollbar{4, 0, 1}, 2, 10, bb(2).WriteStyled(hscrollbarThumb).WriteStyled(hscrollbarTrough), }, { "HScrollbar showing a minimal 1-size thumb at beginning", HScrollbar{4, 0, 0}, 2, 10, bb(2).WriteStyled(hscrollbarThumb).WriteStyled(hscrollbarTrough), }, { "HScrollbar showing a minimal 1-size thumb at end", HScrollbar{4, 3, 3}, 2, 10, bb(2).WriteStyled(hscrollbarTrough).WriteStyled(hscrollbarThumb), }, } func TestLayout_Render(t *testing.T) { for _, test := range layoutRenderTests { t.Run(test.name, func(t *testing.T) { buf := test.renderer.Render(test.width, test.height) wantBuf := test.wantBuf.Buffer() if !reflect.DeepEqual(buf, wantBuf) { t.Errorf("got buf %v, want %v", buf, wantBuf) } }) } } var nopHandlers = []Handler{ Empty{}, Label{ui.T("label")}, } func TestLayout_Handle(t *testing.T) { for _, handler := range nopHandlers { handled := handler.Handle(term.K('a')) if handled { t.Errorf("%v handles event when it shouldn't", handler) } } } elvish-0.17.0/pkg/cli/tk/listbox.go000066400000000000000000000252301415471104000170510ustar00rootroot00000000000000package tk import ( "strings" "sync" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/ui" ) // ListBox is a list for displaying and selecting from a list of items. type ListBox interface { Widget // CopyState returns a copy of the state. CopyState() ListBoxState // Reset resets the state of the widget with the given items and index of // the selected item. It triggers the OnSelect callback if the index is // valid. Reset(it Items, selected int) // Select changes the selection by calling f with the current state, and // using the return value as the new selection index. It triggers the // OnSelect callback if the selected index has changed and is valid. Select(f func(ListBoxState) int) // Accept accepts the currently selected item. Accept() } // ListBoxSpec specifies the configuration and initial state for ListBox. type ListBoxSpec struct { // Key bindings. Bindings Bindings // A placeholder to show when there are no items. Placeholder ui.Text // A function to call when the selected item has changed. OnSelect func(it Items, i int) // A function called on the accept event. OnAccept func(it Items, i int) // Whether the listbox should be rendered in a horizontal Note that // in the horizontal layout, items must have only one line. Horizontal bool // The minimal amount of space to reserve for left and right sides of each // entry. Padding int // If true, the left padding of each item will be styled the same as the // first segment of the item, and the right spacing and padding will be // styled the same as the last segment of the item. ExtendStyle bool // State. When used in New, this field specifies the initial state. State ListBoxState } type listBox struct { // Mutex for synchronizing access to the state. StateMutex sync.RWMutex // Configuration and state. ListBoxSpec } // NewListBox creates a new ListBox from the given spec. func NewListBox(spec ListBoxSpec) ListBox { if spec.Bindings == nil { spec.Bindings = DummyBindings{} } if spec.OnAccept == nil { spec.OnAccept = func(Items, int) {} } if spec.OnSelect == nil { spec.OnSelect = func(Items, int) {} } else { s := spec.State if s.Items != nil && 0 <= s.Selected && s.Selected < s.Items.Len() { spec.OnSelect(s.Items, s.Selected) } } return &listBox{ListBoxSpec: spec} } var stylingForSelected = ui.Inverse func (w *listBox) Render(width, height int) *term.Buffer { if w.Horizontal { return w.renderHorizontal(width, height) } return w.renderVertical(width, height) } func (w *listBox) MaxHeight(width, height int) int { s := w.CopyState() if s.Items == nil || s.Items.Len() == 0 { return 0 } if w.Horizontal { _, h := getHorizontalWindow(s, w.Padding, width, height) return h } h := 0 for i := 0; i < s.Items.Len(); i++ { h += s.Items.Show(i).CountLines() if h >= height { return height } } return h } const listBoxColGap = 2 func (w *listBox) renderHorizontal(width, height int) *term.Buffer { var state ListBoxState w.mutate(func(s *ListBoxState) { if s.Items == nil || s.Items.Len() == 0 { s.First = 0 } else { s.First, s.Height = getHorizontalWindow(*s, w.Padding, width, height) // Override height to the height required; we don't need the // original height later. height = s.Height } state = *s }) if state.Items == nil || state.Items.Len() == 0 { return Label{Content: w.Placeholder}.Render(width, height) } items, selected, first := state.Items, state.Selected, state.First n := items.Len() buf := term.NewBuffer(0) remainedWidth := width hasCropped := false last := first for i := first; i < n; i += height { selectedRow := -1 // Render the column starting from i. col := make([]ui.Text, 0, height) for j := i; j < i+height && j < n; j++ { last = j item := items.Show(j) if j == selected { selectedRow = j - i } col = append(col, item) } colWidth := maxWidth(items, w.Padding, i, i+height) if colWidth > remainedWidth { colWidth = remainedWidth hasCropped = true } colBuf := croppedLines{ lines: col, padding: w.Padding, selectFrom: selectedRow, selectTo: selectedRow + 1, extendStyle: w.ExtendStyle}.Render(colWidth, height) buf.ExtendRight(colBuf) remainedWidth -= colWidth if remainedWidth <= listBoxColGap { break } remainedWidth -= listBoxColGap buf.Width += listBoxColGap } // We may not have used all the width required; force buffer width. buf.Width = width if first != 0 || last != n-1 || hasCropped { scrollbar := HScrollbar{Total: n, Low: first, High: last + 1} buf.Extend(scrollbar.Render(width, 1), false) } return buf } func (w *listBox) renderVertical(width, height int) *term.Buffer { var state ListBoxState var firstCrop int w.mutate(func(s *ListBoxState) { if s.Items == nil || s.Items.Len() == 0 { s.First = 0 } else { s.First, firstCrop = getVerticalWindow(*s, height) } s.Height = height state = *s }) if state.Items == nil || state.Items.Len() == 0 { return Label{Content: w.Placeholder}.Render(width, height) } items, selected, first := state.Items, state.Selected, state.First n := items.Len() allLines := []ui.Text{} hasCropped := firstCrop > 0 var i, selectFrom, selectTo int for i = first; i < n && len(allLines) < height; i++ { item := items.Show(i) lines := item.SplitByRune('\n') if i == first { lines = lines[firstCrop:] } if i == selected { selectFrom, selectTo = len(allLines), len(allLines)+len(lines) } // TODO: Optionally, add underlines to the last line as a visual // separator between adjacent entries. if len(allLines)+len(lines) > height { lines = lines[:len(allLines)+len(lines)-height] hasCropped = true } allLines = append(allLines, lines...) } var rd Renderer = croppedLines{ lines: allLines, padding: w.Padding, selectFrom: selectFrom, selectTo: selectTo, extendStyle: w.ExtendStyle} if first > 0 || i < n || hasCropped { rd = VScrollbarContainer{ Content: rd, Scrollbar: VScrollbar{Total: n, Low: first, High: i}, } } return rd.Render(width, height) } type croppedLines struct { lines []ui.Text padding int selectFrom int selectTo int extendStyle bool } func (c croppedLines) Render(width, height int) *term.Buffer { bb := term.NewBufferBuilder(width) leftSpacing := ui.T(strings.Repeat(" ", c.padding)) rightSpacing := ui.T(strings.Repeat(" ", width-c.padding)) for i, line := range c.lines { if i > 0 { bb.Newline() } selected := c.selectFrom <= i && i < c.selectTo extendStyle := c.extendStyle && len(line) > 0 left := leftSpacing.Clone() if extendStyle { left[0].Style = line[0].Style } acc := ui.Concat(left, line.TrimWcwidth(width-2*c.padding)) if extendStyle || selected { right := rightSpacing.Clone() if extendStyle { right[0].Style = line[len(line)-1].Style } acc = ui.Concat(acc, right).TrimWcwidth(width) } if selected { acc = ui.StyleText(acc, stylingForSelected) } bb.WriteStyled(acc) } return bb.Buffer() } func (w *listBox) Handle(event term.Event) bool { if w.Bindings.Handle(w, event) { return true } switch event { case term.K(ui.Up): w.Select(Prev) return true case term.K(ui.Down): w.Select(Next) return true case term.K(ui.Enter): w.Accept() return true } return false } func (w *listBox) CopyState() ListBoxState { w.StateMutex.RLock() defer w.StateMutex.RUnlock() return w.State } func (w *listBox) Reset(it Items, selected int) { w.mutate(func(s *ListBoxState) { *s = ListBoxState{Items: it, Selected: selected} }) if 0 <= selected && selected < it.Len() { w.OnSelect(it, selected) } } func (w *listBox) Select(f func(ListBoxState) int) { var it Items var oldSelected, selected int w.mutate(func(s *ListBoxState) { oldSelected, it = s.Selected, s.Items selected = f(*s) s.Selected = selected }) if selected != oldSelected && 0 <= selected && selected < it.Len() { w.OnSelect(it, selected) } } // Prev moves the selection to the previous item, or does nothing if the // first item is currently selected. It is a suitable as an argument to // Widget.Select. func Prev(s ListBoxState) int { return fixIndex(s.Selected-1, s.Items.Len()) } // PrevPage moves the selection to the item one page before. It is only // meaningful in vertical layout and suitable as an argument to Widget.Select. // // TODO(xiaq): This does not correctly with multi-line items. func PrevPage(s ListBoxState) int { return fixIndex(s.Selected-s.Height, s.Items.Len()) } // Next moves the selection to the previous item, or does nothing if the // last item is currently selected. It is a suitable as an argument to // Widget.Select. func Next(s ListBoxState) int { return fixIndex(s.Selected+1, s.Items.Len()) } // NextPage moves the selection to the item one page after. It is only // meaningful in vertical layout and suitable as an argument to Widget.Select. // // TODO(xiaq): This does not correctly with multi-line items. func NextPage(s ListBoxState) int { return fixIndex(s.Selected+s.Height, s.Items.Len()) } // PrevWrap moves the selection to the previous item, or to the last item if // the first item is currently selected. It is a suitable as an argument to // Widget.Select. func PrevWrap(s ListBoxState) int { selected, n := s.Selected, s.Items.Len() switch { case selected >= n: return n - 1 case selected <= 0: return n - 1 default: return selected - 1 } } // NextWrap moves the selection to the previous item, or to the first item // if the last item is currently selected. It is a suitable as an argument to // Widget.Select. func NextWrap(s ListBoxState) int { selected, n := s.Selected, s.Items.Len() switch { case selected >= n-1: return 0 case selected < 0: return 0 default: return selected + 1 } } // Left moves the selection to the item to the left. It is only meaningful in // horizontal layout and suitable as an argument to Widget.Select. func Left(s ListBoxState) int { return horizontal(s.Selected, s.Items.Len(), -s.Height) } // Right moves the selection to the item to the right. It is only meaningful in // horizontal layout and suitable as an argument to Widget.Select. func Right(s ListBoxState) int { return horizontal(s.Selected, s.Items.Len(), s.Height) } func horizontal(selected, n, d int) int { selected = fixIndex(selected, n) newSelected := selected + d if newSelected < 0 || newSelected >= n { return selected } return newSelected } func fixIndex(i, n int) int { switch { case i < 0: return 0 case i >= n: return n - 1 default: return i } } func (w *listBox) Accept() { state := w.CopyState() if 0 <= state.Selected && state.Selected < state.Items.Len() { w.OnAccept(state.Items, state.Selected) } } func (w *listBox) mutate(f func(s *ListBoxState)) { w.StateMutex.Lock() defer w.StateMutex.Unlock() f(&w.State) } elvish-0.17.0/pkg/cli/tk/listbox_state.go000066400000000000000000000015651415471104000202560ustar00rootroot00000000000000package tk import ( "fmt" "src.elv.sh/pkg/ui" ) // ListBoxState keeps the mutable state ListBox. type ListBoxState struct { Items Items Selected int First int Height int } // Items is an interface for accessing multiple items. type Items interface { // Show renders the item at the given zero-based index. Show(i int) ui.Text // Len returns the number of items. Len() int } // TestItems is an implementation of Items useful for testing. type TestItems struct { Prefix string Style ui.Styling NItems int } // Show returns a plain text consisting of the prefix and i. If the prefix is // empty, it defaults to "item ". func (it TestItems) Show(i int) ui.Text { prefix := it.Prefix if prefix == "" { prefix = "item " } return ui.T(fmt.Sprintf("%s%d", prefix, i), it.Style) } // Len returns it.NItems. func (it TestItems) Len() int { return it.NItems } elvish-0.17.0/pkg/cli/tk/listbox_test.go000066400000000000000000000324451415471104000201160ustar00rootroot00000000000000package tk import ( "testing" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/ui" ) var listBoxRenderVerticalTests = []renderTest{ { Name: "placeholder when Items is nil", Given: NewListBox(ListBoxSpec{Placeholder: ui.T("nothing")}), Width: 10, Height: 3, Want: bb(10).Write("nothing"), }, { Name: "placeholder when NItems is 0", Given: NewListBox(ListBoxSpec{ Placeholder: ui.T("nothing"), State: ListBoxState{Items: TestItems{}}}), Width: 10, Height: 3, Want: bb(10).Write("nothing"), }, { Name: "all items when there is enough height", Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 2}, Selected: 0}}), Width: 10, Height: 3, Want: bb(10). Write("item 0 ", ui.Inverse). Newline().Write("item 1"), }, { Name: "long lines cropped", Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 2}, Selected: 0}}), Width: 4, Height: 3, Want: bb(4). Write("item", ui.Inverse). Newline().Write("item"), }, { Name: "scrollbar when not showing all items", Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 4}, Selected: 0}}), Width: 10, Height: 2, Want: bb(10). Write("item 0 ", ui.Inverse). Write(" ", ui.Inverse, ui.FgMagenta). Newline().Write("item 1 "). Write("│", ui.FgMagenta), }, { Name: "scrollbar when not showing last item in full", Given: NewListBox(ListBoxSpec{ State: ListBoxState{ Items: TestItems{Prefix: "item\n", NItems: 2}, Selected: 0}}), Width: 10, Height: 3, Want: bb(10). Write("item ", ui.Inverse). Write(" ", ui.Inverse, ui.FgMagenta). Newline().Write("0 ", ui.Inverse). Write(" ", ui.Inverse, ui.FgMagenta). Newline().Write("item "). Write(" ", ui.Inverse, ui.FgMagenta), }, { Name: "scrollbar when not showing only item in full", Given: NewListBox(ListBoxSpec{ State: ListBoxState{ Items: TestItems{Prefix: "item\n", NItems: 1}, Selected: 0}}), Width: 10, Height: 1, Want: bb(10). Write("item ", ui.Inverse). Write(" ", ui.Inverse, ui.FgMagenta), }, { Name: "padding", Given: NewListBox( ListBoxSpec{ Padding: 1, State: ListBoxState{ Items: TestItems{Prefix: "item\n", NItems: 2}, Selected: 0}}), Width: 4, Height: 4, Want: bb(4). Write(" it ", ui.Inverse).Newline(). Write(" 0 ", ui.Inverse).Newline(). Write(" it").Newline(). Write(" 1").Buffer(), }, { Name: "not extending style", Given: NewListBox(ListBoxSpec{ Padding: 1, State: ListBoxState{ Items: TestItems{ Prefix: "x", NItems: 2, Style: ui.Stylings(ui.FgBlue, ui.BgGreen)}}}), Width: 6, Height: 2, Want: bb(6). Write(" ", ui.Inverse). Write("x0", ui.FgBlue, ui.BgGreen, ui.Inverse). Write(" ", ui.Inverse). Newline(). Write(" "). Write("x1", ui.FgBlue, ui.BgGreen). Buffer(), }, { Name: "extending style", Given: NewListBox(ListBoxSpec{ Padding: 1, ExtendStyle: true, State: ListBoxState{Items: TestItems{ Prefix: "x", NItems: 2, Style: ui.Stylings(ui.FgBlue, ui.BgGreen)}}}), Width: 6, Height: 2, Want: bb(6). Write(" x0 ", ui.FgBlue, ui.BgGreen, ui.Inverse). Newline(). Write(" x1 ", ui.FgBlue, ui.BgGreen). Buffer(), }, } func TestListBox_Render_Vertical(t *testing.T) { testRender(t, listBoxRenderVerticalTests) } func TestListBox_Render_Vertical_MutatesState(t *testing.T) { // Calling Render alters the First field to reflect the first item rendered. w := NewListBox(ListBoxSpec{ State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 4, First: 0}}) // Items shown will be 3, 4, 5 w.Render(10, 3) state := w.CopyState() if first := state.First; first != 3 { t.Errorf("State.First = %d, want 3", first) } if height := state.Height; height != 3 { t.Errorf("State.Height = %d, want 3", height) } } var listBoxRenderHorizontalTests = []renderTest{ { Name: "placeholder when Items is nil", Given: NewListBox(ListBoxSpec{Horizontal: true, Placeholder: ui.T("nothing")}), Width: 10, Height: 3, Want: bb(10).Write("nothing"), }, { Name: "placeholder when NItems is 0", Given: NewListBox(ListBoxSpec{ Horizontal: true, Placeholder: ui.T("nothing"), State: ListBoxState{Items: TestItems{}}}), Width: 10, Height: 3, Want: bb(10).Write("nothing"), }, { Name: "all items when there is enough space, using minimal height", Given: NewListBox(ListBoxSpec{ Horizontal: true, State: ListBoxState{Items: TestItems{NItems: 4}, Selected: 0}}), Width: 14, Height: 3, // Available height is 3, but only need 2 lines. Want: bb(14). Write("item 0", ui.Inverse). Write(" "). Write("item 2"). Newline().Write("item 1 item 3"), }, { Name: "padding", Given: NewListBox(ListBoxSpec{ Horizontal: true, Padding: 1, State: ListBoxState{Items: TestItems{NItems: 4, Prefix: "x"}, Selected: 0}}), Width: 14, Height: 3, Want: bb(14). Write(" x0 ", ui.Inverse). Write(" "). Write(" x2"). Newline().Write(" x1 x3"), }, { Name: "extending style", Given: NewListBox(ListBoxSpec{ Horizontal: true, Padding: 1, ExtendStyle: true, State: ListBoxState{Items: TestItems{ NItems: 2, Prefix: "x", Style: ui.Stylings(ui.FgBlue, ui.BgGreen)}}}), Width: 14, Height: 3, Want: bb(14). Write(" x0 ", ui.FgBlue, ui.BgGreen, ui.Inverse). Write(" "). Write(" x1 ", ui.FgBlue, ui.BgGreen), }, { Name: "long lines cropped, with full scrollbar", Given: NewListBox(ListBoxSpec{ Horizontal: true, State: ListBoxState{Items: TestItems{NItems: 2}, Selected: 0}}), Width: 4, Height: 3, Want: bb(4). Write("item", ui.Inverse). Newline().Write("item"). Newline().Write(" ", ui.FgMagenta, ui.Inverse), }, { Name: "scrollbar when not showing all items", Given: NewListBox(ListBoxSpec{ Horizontal: true, State: ListBoxState{Items: TestItems{NItems: 4}, Selected: 0}}), Width: 6, Height: 3, Want: bb(6). Write("item 0", ui.Inverse). Newline().Write("item 1"). Newline(). Write(" ", ui.Inverse, ui.FgMagenta). Write("━━━", ui.FgMagenta), }, { Name: "scrollbar when not showing all items", Given: NewListBox(ListBoxSpec{ Horizontal: true, State: ListBoxState{Items: TestItems{NItems: 4}, Selected: 0}}), Width: 10, Height: 3, Want: bb(10). Write("item 0", ui.Inverse).Write(" it"). Newline().Write("item 1 it"). Newline(). Write(" ", ui.Inverse, ui.FgMagenta), }, } func TestListBox_Render_Horizontal(t *testing.T) { testRender(t, listBoxRenderHorizontalTests) } func TestListBox_Render_Horizontal_MutatesState(t *testing.T) { // Calling Render alters the First field to reflect the first item rendered. w := NewListBox(ListBoxSpec{ Horizontal: true, State: ListBoxState{ Items: TestItems{Prefix: "x", NItems: 10}, Selected: 4, First: 0}}) // Only a single column of 3 items shown: x3-x5 w.Render(2, 4) state := w.CopyState() if first := state.First; first != 3 { t.Errorf("State.First = %d, want 3", first) } if height := state.Height; height != 3 { t.Errorf("State.Height = %d, want 3", height) } } var listBoxHandleTests = []handleTest{ { Name: "up moving selection up", Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 1}}), Event: term.K(ui.Up), WantNewState: ListBoxState{Items: TestItems{NItems: 10}, Selected: 0}, }, { Name: "up stopping at 0", Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 0}}), Event: term.K(ui.Up), WantNewState: ListBoxState{Items: TestItems{NItems: 10}, Selected: 0}, }, { Name: "up moving to last item when selecting after boundary", Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 11}}), Event: term.K(ui.Up), WantNewState: ListBoxState{Items: TestItems{NItems: 10}, Selected: 9}, }, { Name: "down moving selection down", Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 1}}), Event: term.K(ui.Down), WantNewState: ListBoxState{Items: TestItems{NItems: 10}, Selected: 2}, }, { Name: "down stopping at n-1", Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 9}}), Event: term.K(ui.Down), WantNewState: ListBoxState{Items: TestItems{NItems: 10}, Selected: 9}, }, { Name: "down moving to first item when selecting before boundary", Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 10}, Selected: -2}}), Event: term.K(ui.Down), WantNewState: ListBoxState{Items: TestItems{NItems: 10}, Selected: 0}, }, { Name: "enter triggering default no-op accept", Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 5}}), Event: term.K(ui.Enter), WantNewState: ListBoxState{Items: TestItems{NItems: 10}, Selected: 5}, }, { Name: "other keys not handled", Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 5}}), Event: term.K('a'), WantUnhandled: true, }, { Name: "bindings", Given: NewListBox(ListBoxSpec{ State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 5}, Bindings: MapBindings{ term.K('a'): func(w Widget) { w.(*listBox).State.Selected = 0 }, }, }), Event: term.K('a'), WantNewState: ListBoxState{Items: TestItems{NItems: 10}, Selected: 0}, }, } func TestListBox_Handle(t *testing.T) { testHandle(t, listBoxHandleTests) } func TestListBox_Handle_EnterEmitsAccept(t *testing.T) { var acceptedItems Items var acceptedIndex int w := NewListBox(ListBoxSpec{ OnAccept: func(it Items, i int) { acceptedItems = it acceptedIndex = i }, State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 5}}) w.Handle(term.K(ui.Enter)) if acceptedItems != (TestItems{NItems: 10}) { t.Errorf("OnAccept not passed current Items") } if acceptedIndex != 5 { t.Errorf("OnAccept not passed current selected index") } } func TestListBox_Select_ChangeState(t *testing.T) { // number of items = 10, height = 3 var tests = []struct { name string before int f func(ListBoxState) int after int }{ {"Next from -1", -1, Next, 0}, {"Next from 0", 0, Next, 1}, {"Next from 9", 9, Next, 9}, {"Next from 10", 10, Next, 9}, {"NextWrap from -1", -1, NextWrap, 0}, {"NextWrap from 0", 0, NextWrap, 1}, {"NextWrap from 9", 9, NextWrap, 0}, {"NextWrap from 10", 10, NextWrap, 0}, {"NextPage from -1", -1, NextPage, 2}, {"NextPage from 0", 0, NextPage, 3}, {"NextPage from 9", 9, NextPage, 9}, {"NextPage from 10", 10, NextPage, 9}, {"Prev from -1", -1, Prev, 0}, {"Prev from 0", 0, Prev, 0}, {"Prev from 9", 9, Prev, 8}, {"Prev from 10", 10, Prev, 9}, {"PrevWrap from -1", -1, PrevWrap, 9}, {"PrevWrap from 0", 0, PrevWrap, 9}, {"PrevWrap from 9", 9, PrevWrap, 8}, {"PrevWrap from 10", 10, PrevWrap, 9}, {"PrevPage from -1", -1, PrevPage, 0}, {"PrevPage from 0", 0, PrevPage, 0}, {"PrevPage from 9", 9, PrevPage, 6}, {"PrevPage from 10", 10, PrevPage, 7}, {"Left from -1", -1, Left, 0}, {"Left from 0", 0, Left, 0}, {"Left from 9", 9, Left, 6}, {"Left from 10", 10, Left, 6}, {"Right from -1", -1, Right, 3}, {"Right from 0", 0, Right, 3}, {"Right from 9", 9, Right, 9}, {"Right from 10", 10, Right, 9}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { w := NewListBox(ListBoxSpec{ State: ListBoxState{ Items: TestItems{NItems: 10}, Height: 3, Selected: test.before}}) w.Select(test.f) if selected := w.CopyState().Selected; selected != test.after { t.Errorf("selected = %d, want %d", selected, test.after) } }) } } func TestListBox_Select_CallOnSelect(t *testing.T) { it := TestItems{NItems: 10} gotItemsCh := make(chan Items, 10) gotSelectedCh := make(chan int, 10) w := NewListBox(ListBoxSpec{ OnSelect: func(it Items, i int) { gotItemsCh <- it gotSelectedCh <- i }, State: ListBoxState{Items: it, Selected: 5}}) verifyOnSelect := func(wantSelected int) { if gotItems := <-gotItemsCh; gotItems != it { t.Errorf("Got it = %v, want %v", gotItems, it) } if gotSelected := <-gotSelectedCh; gotSelected != wantSelected { t.Errorf("Got selected = %v, want %v", gotSelected, wantSelected) } } // Test that OnSelect is called during initialization. verifyOnSelect(5) // Test that OnSelect is called when changing selection. w.Select(Next) verifyOnSelect(6) // Test that OnSelect is not called when index is invalid. Instead of // waiting a fixed time to make sure that nothing is sent in the channel, we // immediately does another Select with a valid index, and verify that only // the valid index is sent. w.Select(func(ListBoxState) int { return -1 }) w.Select(func(ListBoxState) int { return 0 }) verifyOnSelect(0) } func TestListBox_Accept_IndexCheck(t *testing.T) { tests := []struct { name string nItems int selected int shouldAccept bool }{ {"index in range", 1, 0, true}, {"index exceeds left boundary", 1, -1, false}, {"index exceeds right boundary", 0, 0, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { w := NewListBox(ListBoxSpec{ OnAccept: func(it Items, i int) { if !tt.shouldAccept { t.Error("should not accept this state") } }, State: ListBoxState{ Items: TestItems{NItems: tt.nItems}, Selected: tt.selected, }, }) w.Accept() }) } } elvish-0.17.0/pkg/cli/tk/listbox_window.go000066400000000000000000000112011415471104000204310ustar00rootroot00000000000000package tk import "src.elv.sh/pkg/wcwidth" // The number of lines the listing mode keeps between the current selected item // and the top and bottom edges of the window, unless the available height is // too small or if the selected item is near the top or bottom of the list. var respectDistance = 2 // Determines the index of the first item to show in vertical mode. // // This function does not return the full window, but just the first item to // show, and how many initial lines to crop. The window determined by this // algorithm has the following properties: // // * It always includes the selected item. // // * The combined height of all the entries in the window is equal to // min(height, combined height of all entries). // // * There are at least respectDistance rows above the first row of the selected // item, as well as that many rows below the last row of the selected item, // unless the height is too small. // // * Among all values satisfying the above conditions, the value of first is // the one closest to lastFirst. func getVerticalWindow(state ListBoxState, height int) (first, crop int) { items, selected, lastFirst := state.Items, state.Selected, state.First n := items.Len() if selected < 0 { selected = 0 } else if selected >= n { selected = n - 1 } selectedHeight := items.Show(selected).CountLines() if height <= selectedHeight { // The height is not big enough (or just big enough) to fit the selected // item. Fit as much as the selected item as we can. return selected, 0 } // Determine the minimum amount of space required for the downward direction. budget := height - selectedHeight var needDown int if budget >= 2*respectDistance { // If we can afford maintaining the respect distance on both sides, then // the minimum amount of space required is the respect distance. needDown = respectDistance } else { // Otherwise we split the available space by half. The downward (no pun // intended) rounding here is an arbitrary choice. needDown = budget / 2 } // Calculate how much of the budget the downward direction can use. This is // used to 1) potentially shrink needDown 2) decide how much to expand // upward later. useDown := 0 for i := selected + 1; i < n; i++ { useDown += items.Show(i).CountLines() if useDown >= budget { break } } if needDown > useDown { // We reached the last item without using all of needDown. That means we // don't need so much in the downward direction. needDown = useDown } // The maximum amount of space we can use in the upward direction is the // entire budget minus the minimum amount of space we need in the downward // direction. budgetUp := budget - needDown useUp := 0 // Extend upwards until any of the following becomes true: // // * We have exhausted budgetUp; // // * We have reached item 0; // // * We have reached or passed lastFirst, satisfied the upward respect // distance, and will be able to use up the entire budget when expanding // downwards later. for i := selected - 1; i >= 0; i-- { useUp += items.Show(i).CountLines() if useUp >= budgetUp { return i, useUp - budgetUp } if i <= lastFirst && useUp >= respectDistance && useUp+useDown >= budget { return i, 0 } } return 0, 0 } // Determines the window to show in horizontal. Returns the first item to show // and the amount of height required. func getHorizontalWindow(state ListBoxState, padding, width, height int) (int, int) { items := state.Items n := items.Len() // Lower bound of number of items that can fit in a row. perRow := (width + listBoxColGap) / (maxWidth(items, padding, 0, n) + listBoxColGap) if perRow == 0 { // We trim items that are too wide, so there is at least one item per row. perRow = 1 } if height*perRow >= n { // All items can fit. return 0, (n + perRow - 1) / perRow } // Reduce the amount of available height by one because the last row will be // reserved for the scrollbar. height-- selected, lastFirst := state.Selected, state.First // Start with the column containing the selected item, move left until // either the width is exhausted, or lastFirst has been reached. first := selected / height * height usedWidth := maxWidth(items, padding, first, first+height) for ; first > lastFirst; first -= height { usedWidth += maxWidth(items, padding, first-height, first) + listBoxColGap if usedWidth > width { break } } return first, height } func maxWidth(items Items, padding, low, high int) int { n := items.Len() width := 0 for i := low; i < high && i < n; i++ { w := 0 for _, seg := range items.Show(i) { w += wcwidth.Of(seg.Text) } if width < w { width = w } } return width + 2*padding } elvish-0.17.0/pkg/cli/tk/listbox_window_test.go000066400000000000000000000107101415471104000214740ustar00rootroot00000000000000package tk import ( "testing" "src.elv.sh/pkg/tt" ) var Args = tt.Args func TestGetVerticalWindow(t *testing.T) { tt.Test(t, tt.Fn("getVerticalWindow", getVerticalWindow), tt.Table{ // selected = 0: always show a widow starting from 0, regardless of // the value of oldFirst Args(ListBoxState{Items: TestItems{NItems: 10}, Selected: 0, First: 0}, 6).Rets(0, 0), Args(ListBoxState{Items: TestItems{NItems: 10}, Selected: 0, First: 1}, 6).Rets(0, 0), // selected < 0 is treated as if = 0. Args(ListBoxState{Items: TestItems{NItems: 10}, Selected: -1, First: 0}, 6).Rets(0, 0), // selected = n-1: always show a window ending at n-1, regardless of the // value of oldFirst Args(ListBoxState{Items: TestItems{NItems: 10}, Selected: 9, First: 0}, 6).Rets(4, 0), Args(ListBoxState{Items: TestItems{NItems: 10}, Selected: 9, First: 8}, 6).Rets(4, 0), // selected >= n is treated as if = n-1. Args(ListBoxState{Items: TestItems{NItems: 10}, Selected: 10, First: 0}, 6).Rets(4, 0), // selected = 3, oldFirst = 2 (likely because previous selected = 4). // Adjust first -> 1 to satisfy the upward respect distance of 2. Args(ListBoxState{Items: TestItems{NItems: 10}, Selected: 3, First: 2}, 6).Rets(1, 0), // selected = 6, oldFirst = 2 (likely because previous selected = 7). // Adjust first -> 3 to satisfy the downward respect distance of 2. Args(ListBoxState{Items: TestItems{NItems: 10}, Selected: 6, First: 2}, 6).Rets(3, 0), // There is not enough budget to achieve respect distance on both sides. // Split the budget in half. Args(ListBoxState{Items: TestItems{NItems: 10}, Selected: 3, First: 1}, 3).Rets(2, 0), Args(ListBoxState{Items: TestItems{NItems: 10}, Selected: 3, First: 0}, 3).Rets(2, 0), // There is just enough distance to fit the selected item. Only show the // selected item. Args(ListBoxState{Items: TestItems{NItems: 10}, Selected: 2, First: 0}, 1).Rets(2, 0), }) } func TestGetHorizontalWindow(t *testing.T) { tt.Test(t, tt.Fn("getHorizontalWindow", getHorizontalWindow), tt.Table{ // All items fit in a single column. Item width is 6 ("item 0"). Args(ListBoxState{Items: TestItems{NItems: 10}, Selected: 4, First: 0}, 0, 6, 10).Rets(0, 10), // All items fit in multiple columns. Item width is 2 ("x0"). Args(ListBoxState{Items: TestItems{Prefix: "x", NItems: 10}, Selected: 4, First: 0}, 0, 6, 5).Rets(0, 5), // All items cannot fit, selected = 0; show a window from 0. Height // reduced to make room for scrollbar. Args(ListBoxState{Items: TestItems{Prefix: "x", NItems: 11}, Selected: 0, First: 0}, 0, 6, 5).Rets(0, 4), // All items cannot fit. Columns are 0-3, 4-7, 8-10 (height reduced from // 5 to 4 for scrollbar). Selecting last item, and showing last two // columns; height reduced to make room for scrollbar. Args(ListBoxState{Items: TestItems{Prefix: "x", NItems: 11}, Selected: 10, First: 0}, 0, 7, 5).Rets(4, 4), // Items are wider than terminal, and there is a single column. Show // them all. Args(ListBoxState{Items: TestItems{Prefix: "long prefix", NItems: 10}, Selected: 9, First: 0}, 0, 6, 10).Rets(0, 10), // Items are wider than terminal, and there are multiple columns. Treat // them as if each column occupies a full width. Columns are 0-4, 5-9. Args(ListBoxState{Items: TestItems{Prefix: "long prefix", NItems: 10}, Selected: 9, First: 0}, 0, 6, 6).Rets(5, 5), // The following cases only differ in State.First and shows that the // algorithm respects it. In all cases, the columns are 0-4, 5-9, // 10-14, 15-19, item 10 is selected, and the terminal can fit 2 columns. // First = 0. Try to reach as far as possible to that, ending up showing // columns 5-9 and 10-14. Args(ListBoxState{Items: TestItems{Prefix: "x", NItems: 20}, Selected: 10, First: 0}, 0, 8, 6).Rets(5, 5), // First = 2. Ditto. Args(ListBoxState{Items: TestItems{Prefix: "x", NItems: 20}, Selected: 10, First: 2}, 0, 8, 6).Rets(5, 5), // First = 5. Show columns 5-9 and 10-14. Args(ListBoxState{Items: TestItems{Prefix: "x", NItems: 20}, Selected: 10, First: 5}, 0, 8, 6).Rets(5, 5), // First = 7. Ditto. Args(ListBoxState{Items: TestItems{Prefix: "x", NItems: 20}, Selected: 10, First: 7}, 0, 8, 6).Rets(5, 5), // First = 10. No need to any columns to the left. Args(ListBoxState{Items: TestItems{Prefix: "x", NItems: 20}, Selected: 10, First: 10}, 0, 8, 6).Rets(10, 5), // First = 12. Ditto. Args(ListBoxState{Items: TestItems{Prefix: "x", NItems: 20}, Selected: 10, First: 12}, 0, 8, 6).Rets(10, 5), }) } elvish-0.17.0/pkg/cli/tk/scrollbar.go000066400000000000000000000040531415471104000173500ustar00rootroot00000000000000package tk import ( "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/ui" ) // VScrollbarContainer is a Renderer consisting of content and a vertical // scrollbar on the right. type VScrollbarContainer struct { Content Renderer Scrollbar VScrollbar } func (v VScrollbarContainer) Render(width, height int) *term.Buffer { buf := v.Content.Render(width-1, height) buf.ExtendRight(v.Scrollbar.Render(1, height)) return buf } // VScrollbar is a Renderer for a vertical scrollbar. type VScrollbar struct { Total int Low int High int } var ( vscrollbarThumb = ui.T(" ", ui.FgMagenta, ui.Inverse) vscrollbarTrough = ui.T("│", ui.FgMagenta) ) func (v VScrollbar) Render(width, height int) *term.Buffer { posLow, posHigh := findScrollInterval(v.Total, v.Low, v.High, height) bb := term.NewBufferBuilder(1) for i := 0; i < height; i++ { if i > 0 { bb.Newline() } if posLow <= i && i < posHigh { bb.WriteStyled(vscrollbarThumb) } else { bb.WriteStyled(vscrollbarTrough) } } return bb.Buffer() } // HScrollbar is a Renderer for a horizontal scrollbar. type HScrollbar struct { Total int Low int High int } var ( hscrollbarThumb = ui.T(" ", ui.FgMagenta, ui.Inverse) hscrollbarTrough = ui.T("━", ui.FgMagenta) ) func (h HScrollbar) Render(width, height int) *term.Buffer { posLow, posHigh := findScrollInterval(h.Total, h.Low, h.High, width) bb := term.NewBufferBuilder(width) for i := 0; i < width; i++ { if posLow <= i && i < posHigh { bb.WriteStyled(hscrollbarThumb) } else { bb.WriteStyled(hscrollbarTrough) } } return bb.Buffer() } func findScrollInterval(n, low, high, height int) (int, int) { f := func(i int) int { return int(float64(i)/float64(n)*float64(height) + 0.5) } scrollLow := f(low) // We use the following instead of f(high), so that the size of the // scrollbar remains the same as long as the window size remains the same. scrollHigh := scrollLow + f(high-low) if scrollLow == scrollHigh { if scrollHigh == height { scrollLow-- } else { scrollHigh++ } } return scrollLow, scrollHigh } elvish-0.17.0/pkg/cli/tk/textview.go000066400000000000000000000062101415471104000172410ustar00rootroot00000000000000package tk import ( "sync" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/ui" "src.elv.sh/pkg/wcwidth" ) // TextView is a Widget for displaying text, with support for vertical // scrolling. // // NOTE: This widget now always crops long lines. In future it should support // wrapping and horizontal scrolling. type TextView interface { Widget // ScrollBy scrolls the widget by the given delta. Positive values scroll // down, and negative values scroll up. ScrollBy(delta int) // MutateState mutates the state. MutateState(f func(*TextViewState)) // CopyState returns a copy of the State. CopyState() TextViewState } // TextViewSpec specifies the configuration and initial state for a Widget. type TextViewSpec struct { // Key bindings. Bindings Bindings // If true, a vertical scrollbar will be shown when there are more lines // that can be displayed, and the widget responds to Up and Down keys. Scrollable bool // State. Specifies the initial state if used in New. State TextViewState } // TextViewState keeps mutable state of TextView. type TextViewState struct { Lines []string First int } type textView struct { // Mutex for synchronizing access to the state. StateMutex sync.RWMutex TextViewSpec } // NewTextView builds a TextView from the given spec. func NewTextView(spec TextViewSpec) TextView { if spec.Bindings == nil { spec.Bindings = DummyBindings{} } return &textView{TextViewSpec: spec} } func (w *textView) Render(width, height int) *term.Buffer { lines, first := w.getStateForRender(height) needScrollbar := w.Scrollable && (first > 0 || first+height < len(lines)) textWidth := width if needScrollbar { textWidth-- } bb := term.NewBufferBuilder(textWidth) for i := first; i < first+height && i < len(lines); i++ { if i > first { bb.Newline() } bb.Write(wcwidth.Trim(lines[i], textWidth)) } buf := bb.Buffer() if needScrollbar { scrollbar := VScrollbar{ Total: len(lines), Low: first, High: first + height} buf.ExtendRight(scrollbar.Render(1, height)) } return buf } func (w *textView) MaxHeight(width, height int) int { return len(w.CopyState().Lines) } func (w *textView) getStateForRender(height int) (lines []string, first int) { w.MutateState(func(s *TextViewState) { if s.First > len(s.Lines)-height && len(s.Lines)-height >= 0 { s.First = len(s.Lines) - height } lines, first = s.Lines, s.First }) return } func (w *textView) Handle(event term.Event) bool { if w.Bindings.Handle(w, event) { return true } if w.Scrollable { switch event { case term.K(ui.Up): w.ScrollBy(-1) return true case term.K(ui.Down): w.ScrollBy(1) return true } } return false } func (w *textView) ScrollBy(delta int) { w.MutateState(func(s *TextViewState) { s.First += delta if s.First < 0 { s.First = 0 } if s.First >= len(s.Lines) { s.First = len(s.Lines) - 1 } }) } func (w *textView) MutateState(f func(*TextViewState)) { w.StateMutex.Lock() defer w.StateMutex.Unlock() f(&w.State) } // CopyState returns a copy of the State while r-locking the StateMutex. func (w *textView) CopyState() TextViewState { w.StateMutex.RLock() defer w.StateMutex.RUnlock() return w.State } elvish-0.17.0/pkg/cli/tk/textview_test.go000066400000000000000000000070431415471104000203050ustar00rootroot00000000000000package tk import ( "reflect" "testing" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/ui" ) var textViewRenderTests = []renderTest{ { Name: "text fits entirely", Given: NewTextView(TextViewSpec{State: TextViewState{ Lines: []string{"line 1", "line 2", "line 3"}}}), Width: 10, Height: 4, Want: bb(10). Write("line 1").Newline(). Write("line 2").Newline(). Write("line 3").Buffer(), }, { Name: "text cropped horizontally", Given: NewTextView(TextViewSpec{State: TextViewState{ Lines: []string{"a very long line"}}}), Width: 10, Height: 4, Want: bb(10). Write("a very lon").Buffer(), }, { Name: "text cropped vertically", Given: NewTextView(TextViewSpec{State: TextViewState{ Lines: []string{"line 1", "line 2", "line 3"}}}), Width: 10, Height: 2, Want: bb(10). Write("line 1").Newline(). Write("line 2").Buffer(), }, { Name: "text cropped vertically, with scrollbar", Given: NewTextView(TextViewSpec{ Scrollable: true, State: TextViewState{ Lines: []string{"line 1", "line 2", "line 3", "line 4"}}}), Width: 10, Height: 2, Want: bb(10). Write("line 1 "). Write(" ", ui.Inverse, ui.FgMagenta).Newline(). Write("line 2 "). Write("│", ui.FgMagenta).Buffer(), }, { Name: "State.First adjusted to fit text", Given: NewTextView(TextViewSpec{State: TextViewState{ First: 2, Lines: []string{"line 1", "line 2", "line 3"}}}), Width: 10, Height: 3, Want: bb(10). Write("line 1").Newline(). Write("line 2").Newline(). Write("line 3").Buffer(), }, } func TestTextView_Render(t *testing.T) { testRender(t, textViewRenderTests) } var textViewHandleTests = []handleTest{ { Name: "up doing nothing when not scrollable", Given: NewTextView(TextViewSpec{ State: TextViewState{Lines: []string{"1", "2", "3", "4"}, First: 1}}), Event: term.K(ui.Up), WantUnhandled: true, }, { Name: "up moving window up when scrollable", Given: NewTextView(TextViewSpec{ Scrollable: true, State: TextViewState{Lines: []string{"1", "2", "3", "4"}, First: 1}}), Event: term.K(ui.Up), WantNewState: TextViewState{Lines: []string{"1", "2", "3", "4"}, First: 0}, }, { Name: "up doing nothing when already at top", Given: NewTextView(TextViewSpec{ Scrollable: true, State: TextViewState{Lines: []string{"1", "2", "3", "4"}, First: 0}}), Event: term.K(ui.Up), WantNewState: TextViewState{Lines: []string{"1", "2", "3", "4"}, First: 0}, }, { Name: "down moving window down when scrollable", Given: NewTextView(TextViewSpec{ Scrollable: true, State: TextViewState{Lines: []string{"1", "2", "3", "4"}, First: 1}}), Event: term.K(ui.Down), WantNewState: TextViewState{Lines: []string{"1", "2", "3", "4"}, First: 2}, }, { Name: "down doing nothing when already at bottom", Given: NewTextView(TextViewSpec{ Scrollable: true, State: TextViewState{Lines: []string{"1", "2", "3", "4"}, First: 3}}), Event: term.K(ui.Down), WantNewState: TextViewState{Lines: []string{"1", "2", "3", "4"}, First: 3}, }, { Name: "bindings", Given: NewTextView(TextViewSpec{ Bindings: MapBindings{term.K('a'): func(Widget) {}}}), Event: term.K('a'), WantNewState: TextViewState{}, }, } func TestTextView_Handle(t *testing.T) { testHandle(t, textViewHandleTests) } func TestTextView_CopyState(t *testing.T) { state := TextViewState{Lines: []string{"a", "b", "c"}, First: 1} w := NewTextView(TextViewSpec{State: state}) copied := w.CopyState() if !reflect.DeepEqual(copied, state) { t.Errorf("Got copied state %v, want %v", copied, state) } } elvish-0.17.0/pkg/cli/tk/utils_test.go000066400000000000000000000065451415471104000175740ustar00rootroot00000000000000package tk import ( "reflect" "testing" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/ui" ) // renderTest is a test case to be used in TestRenderer. type renderTest struct { Name string Given Renderer Width int Height int Want interface{ Buffer() *term.Buffer } } // testRender runs the given Renderer tests. func testRender(t *testing.T, tests []renderTest) { t.Helper() for _, test := range tests { t.Run(test.Name, func(t *testing.T) { t.Helper() buf := test.Given.Render(test.Width, test.Height) wantBuf := test.Want.Buffer() if !reflect.DeepEqual(buf, wantBuf) { t.Errorf("Buffer mismatch") t.Logf("Got: %s", buf.TTYString()) t.Logf("Want: %s", wantBuf.TTYString()) } }) } } // handleTest is a test case to be used in testHandle. type handleTest struct { Name string Given Handler Event term.Event Events []term.Event WantNewState interface{} WantUnhandled bool } // testHandle runs the given Handler tests. func testHandle(t *testing.T, tests []handleTest) { t.Helper() for _, test := range tests { t.Run(test.Name, func(t *testing.T) { t.Helper() handler := test.Given oldState := getState(handler) defer setState(handler, oldState) var handled bool switch { case test.Event != nil && test.Events != nil: t.Fatal("Malformed test case: both Event and Events non-nil:", test.Event, test.Events) case test.Event == nil && test.Events == nil: t.Fatal("Malformed test case: both Event and Events nil") case test.Event != nil: handled = handler.Handle(test.Event) default: // test.Events != nil for _, event := range test.Events { handled = handler.Handle(event) } } if handled != !test.WantUnhandled { t.Errorf("Got handled %v, want %v", handled, !test.WantUnhandled) } if test.WantNewState != nil { state := getState(test.Given) if !reflect.DeepEqual(state, test.WantNewState) { t.Errorf("Got state %v, want %v", state, test.WantNewState) } } }) } } func getState(v interface{}) interface{} { return reflectState(v).Interface() } func setState(v, state interface{}) { reflectState(v).Set(reflect.ValueOf(state)) } func reflectState(v interface{}) reflect.Value { rv := reflect.ValueOf(v) if rv.Kind() == reflect.Ptr { rv = reflect.Indirect(rv) } return rv.FieldByName("State") } // Test for the test utilities. func TestTestRender(t *testing.T) { testRender(t, []renderTest{ { Name: "test", Given: &testWidget{text: ui.T("test")}, Width: 10, Height: 10, Want: term.NewBufferBuilder(10).Write("test"), }, }) } type testHandlerWithState struct { State testHandlerState } type testHandlerState struct { last term.Event total int } func (h *testHandlerWithState) Handle(e term.Event) bool { if e == term.K('x') { return false } h.State.last = e h.State.total++ return true } func TestTestHandle(t *testing.T) { testHandle(t, []handleTest{ { Name: "WantNewState", Given: &testHandlerWithState{}, Event: term.K('a'), WantNewState: testHandlerState{last: term.K('a'), total: 1}, }, { Name: "Multiple events", Given: &testHandlerWithState{}, Events: []term.Event{term.K('a'), term.K('b')}, WantNewState: testHandlerState{last: term.K('b'), total: 2}, }, { Name: "WantUnhaneld", Given: &testHandlerWithState{}, Event: term.K('x'), WantUnhandled: true, }, }) } elvish-0.17.0/pkg/cli/tk/widget.go000066400000000000000000000036331415471104000166530ustar00rootroot00000000000000// Package tk is the toolkit for the cli package. // // This package defines three basic interfaces - Renderer, Handler and Widget - // and numerous implementations of these interfaces. package tk import ( "src.elv.sh/pkg/cli/term" ) // Widget is the basic component of UI; it knows how to handle events and how to // render itself. type Widget interface { Renderer MaxHeighter Handler } // Renderer wraps the Render method. type Renderer interface { // Render renders onto a region of bound width and height. Render(width, height int) *term.Buffer } // MaxHeighter wraps the MaxHeight method. type MaxHeighter interface { // MaxHeight returns the maximum height needed when rendering onto a region // of bound width and height. The returned value may be larger than the // height argument. MaxHeight(width, height int) int } // Handler wraps the Handle method. type Handler interface { // Try to handle a terminal event and returns whether the event has been // handled. Handle(event term.Event) bool } // Bindings is the interface for key bindings. type Bindings interface { Handle(Widget, term.Event) bool } // DummyBindings is a trivial Bindings implementation. type DummyBindings struct{} // Handle always returns false. func (DummyBindings) Handle(w Widget, event term.Event) bool { return false } // MapBindings is a map-backed Bindings implementation. type MapBindings map[term.Event]func(Widget) // Handle handles the event by calling the function corresponding to the event // in the map. If there is no corresponding function, it returns false. func (m MapBindings) Handle(w Widget, event term.Event) bool { fn, ok := m[event] if ok { fn(w) } return ok } // FuncBindings is a function-based Bindings implementation. type FuncBindings func(Widget, term.Event) bool // Handle handles the event by calling the function. func (f FuncBindings) Handle(w Widget, event term.Event) bool { return f(w, event) } elvish-0.17.0/pkg/cli/tk/widget_test.go000066400000000000000000000040471415471104000177120ustar00rootroot00000000000000package tk import ( "testing" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/ui" ) type testWidget struct { // Text to render. text ui.Text // Which events to accept. accepted []term.Event // A record of events that have been handled. handled []term.Event } func (w *testWidget) Render(width, height int) *term.Buffer { buf := term.NewBufferBuilder(width).WriteStyled(w.text).Buffer() buf.TrimToLines(0, height) return buf } func (w *testWidget) Handle(e term.Event) bool { for _, accept := range w.accepted { if e == accept { w.handled = append(w.handled, e) return true } } return false } func TestDummyBindings(t *testing.T) { w := Empty{} b := DummyBindings{} for _, event := range []term.Event{term.K('a'), term.PasteSetting(true)} { if b.Handle(w, event) { t.Errorf("should not handle") } } } func TestMapBindings(t *testing.T) { widgetCh := make(chan Widget, 1) w := Empty{} b := MapBindings{term.K('a'): func(w Widget) { widgetCh <- w }} handled := b.Handle(w, term.K('a')) if !handled { t.Errorf("should handle") } if gotWidget := <-widgetCh; gotWidget != w { t.Errorf("function called with widget %v, want %v", gotWidget, w) } handled = b.Handle(w, term.K('b')) if handled { t.Errorf("should not handle") } } func TestFuncBindings(t *testing.T) { widgetCh := make(chan Widget, 1) eventCh := make(chan term.Event, 1) h := FuncBindings(func(w Widget, event term.Event) bool { widgetCh <- w eventCh <- event return event == term.K('a') }) w := Empty{} event := term.K('a') handled := h.Handle(w, event) if !handled { t.Errorf("should handle") } if gotWidget := <-widgetCh; gotWidget != w { t.Errorf("function called with widget %v, want %v", gotWidget, w) } if gotEvent := <-eventCh; gotEvent != event { t.Errorf("function called with event %v, want %v", gotEvent, event) } event = term.K('b') handled = h.Handle(w, event) if handled { t.Errorf("should not handle") } if gotEvent := <-eventCh; gotEvent != event { t.Errorf("function called with event %v, want %v", gotEvent, event) } } elvish-0.17.0/pkg/cli/tty.go000066400000000000000000000053011415471104000155640ustar00rootroot00000000000000package cli import ( "fmt" "os" "os/signal" "sync" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/sys" ) // TTY is the type the terminal dependency of the editor needs to satisfy. type TTY interface { // Setup sets up the terminal for the CLI app. // // This method returns a restore function that undoes the setup, and any // error during setup. It only returns fatal errors that make the terminal // unsuitable for later operations; non-fatal errors may be reported by // showing a warning message, but not returned. // // This method should be called before any other method is called. Setup() (restore func(), err error) // ReadEvent reads a terminal event. ReadEvent() (term.Event, error) // SetRawInput requests the next n ReadEvent calls to read raw events. It // is applicable to environments where events are represented as a special // sequences, such as VT100. It is a no-op if events are delivered as whole // units by the terminal, such as Windows consoles. SetRawInput(n int) // CloseReader releases resources allocated for reading terminal events. CloseReader() term.Writer // NotifySignals start relaying signals and returns a channel on which // signals are delivered. NotifySignals() <-chan os.Signal // StopSignals stops the relaying of signals. After this function returns, // the channel returned by NotifySignals will no longer deliver signals. StopSignals() // Size returns the height and width of the terminal. Size() (h, w int) } type aTTY struct { in, out *os.File r term.Reader term.Writer sigCh chan os.Signal rawMutex sync.Mutex raw int } // NewTTY returns a new TTY from input and output terminal files. func NewTTY(in, out *os.File) TTY { return &aTTY{in: in, out: out, Writer: term.NewWriter(out)} } func (t *aTTY) Setup() (func(), error) { restore, err := term.Setup(t.in, t.out) return func() { err := restore() if err != nil { fmt.Println(t.out, "failed to restore terminal properties:", err) } }, err } func (t *aTTY) Size() (h, w int) { return sys.WinSize(t.out) } func (t *aTTY) ReadEvent() (term.Event, error) { if t.r == nil { t.r = term.NewReader(t.in) } if t.consumeRaw() { return t.r.ReadRawEvent() } return t.r.ReadEvent() } func (t *aTTY) consumeRaw() bool { t.rawMutex.Lock() defer t.rawMutex.Unlock() if t.raw <= 0 { return false } t.raw-- return true } func (t *aTTY) SetRawInput(n int) { t.rawMutex.Lock() defer t.rawMutex.Unlock() t.raw = n } func (t *aTTY) CloseReader() { if t.r != nil { t.r.Close() } t.r = nil } func (t *aTTY) NotifySignals() <-chan os.Signal { t.sigCh = sys.NotifySignals() return t.sigCh } func (t *aTTY) StopSignals() { signal.Stop(t.sigCh) close(t.sigCh) t.sigCh = nil } elvish-0.17.0/pkg/cli/tty_unix_test.go000066400000000000000000000016771415471104000177020ustar00rootroot00000000000000//go:build !windows && !plan9 // +build !windows,!plan9 package cli_test import ( "os" "testing" "golang.org/x/sys/unix" . "src.elv.sh/pkg/cli" ) func TestTTYSignal(t *testing.T) { tty := NewTTY(os.Stdin, os.Stderr) sigch := tty.NotifySignals() err := unix.Kill(unix.Getpid(), unix.SIGUSR1) if err != nil { t.Skip("cannot send SIGUSR1 to myself:", err) } if sig := nextSig(sigch); sig != unix.SIGUSR1 { t.Errorf("Got signal %v, want SIGUSR1", sig) } tty.StopSignals() err = unix.Kill(unix.Getpid(), unix.SIGUSR2) if err != nil { t.Skip("cannot send SIGUSR2 to myself:", err) } if sig := nextSig(sigch); sig != nil { t.Errorf("Got signal %v, want nil", sig) } } // Gets the next signal from the channel, ignoring all SIGURG generated by the // Go runtime. See https://github.com/golang/go/issues/37942. func nextSig(sigch <-chan os.Signal) os.Signal { for { sig := <-sigch if sig != unix.SIGURG { return sig } } } elvish-0.17.0/pkg/daemon/000077500000000000000000000000001415471104000151125ustar00rootroot00000000000000elvish-0.17.0/pkg/daemon/activate.go000066400000000000000000000140621415471104000172440ustar00rootroot00000000000000package daemon import ( "errors" "fmt" "io" "os" "path/filepath" "time" "src.elv.sh/pkg/daemon/daemondefs" "src.elv.sh/pkg/daemon/internal/api" "src.elv.sh/pkg/fsutil" ) var ( daemonSpawnTimeout = time.Second daemonSpawnWaitPerLoop = 10 * time.Millisecond daemonKillTimeout = time.Second daemonKillWaitPerLoop = 10 * time.Millisecond ) type daemonStatus int const ( daemonOK daemonStatus = iota sockfileMissing sockfileOtherError connectionRefused connectionOtherError daemonOutdated ) const connectionRefusedFmt = "Socket file %s exists but refuses requests. This is likely because the daemon was terminated abnormally. Going to remove socket file and re-spawn the daemon.\n" // Activate returns a daemon client, either by connecting to an existing daemon, // or spawning a new one. It always returns a non-nil client, even if there was an error. func Activate(stderr io.Writer, spawnCfg *daemondefs.SpawnConfig) (daemondefs.Client, error) { sockpath := spawnCfg.SockPath cl := NewClient(sockpath) status, err := detectDaemon(sockpath, cl) shouldSpawn := false switch status { case daemonOK: case sockfileMissing: shouldSpawn = true case sockfileOtherError: return cl, fmt.Errorf("socket file %s inaccessible: %w", sockpath, err) case connectionRefused: fmt.Fprintf(stderr, connectionRefusedFmt, sockpath) err := os.Remove(sockpath) if err != nil { return cl, fmt.Errorf("failed to remove socket file: %w", err) } shouldSpawn = true case connectionOtherError: return cl, fmt.Errorf("unexpected RPC error on socket %s: %w", sockpath, err) case daemonOutdated: fmt.Fprintln(stderr, "Daemon is outdated; going to kill old daemon and re-spawn") err := killDaemon(sockpath, cl) if err != nil { return cl, fmt.Errorf("failed to kill old daemon: %w", err) } shouldSpawn = true default: return cl, fmt.Errorf("code bug: unknown daemon status %d", status) } if !shouldSpawn { return cl, nil } err = spawn(spawnCfg) if err != nil { return cl, fmt.Errorf("failed to spawn daemon: %w", err) } // Wait for daemon to come online start := time.Now() for time.Since(start) < daemonSpawnTimeout { cl.ResetConn() status, err := detectDaemon(sockpath, cl) switch status { case daemonOK: return cl, nil case sockfileMissing: // Continue waiting case sockfileOtherError: return cl, fmt.Errorf("socket file %s inaccessible: %w", sockpath, err) case connectionRefused: // Continue waiting case connectionOtherError: return cl, fmt.Errorf("unexpected RPC error on socket %s: %w", sockpath, err) case daemonOutdated: return cl, fmt.Errorf("code bug: newly spawned daemon is outdated") default: return cl, fmt.Errorf("code bug: unknown daemon status %d", status) } time.Sleep(daemonSpawnWaitPerLoop) } return cl, fmt.Errorf("daemon did not come up within %v", daemonSpawnTimeout) } func detectDaemon(sockpath string, cl daemondefs.Client) (daemonStatus, error) { _, err := os.Lstat(sockpath) if err != nil { if os.IsNotExist(err) { return sockfileMissing, err } return sockfileOtherError, err } version, err := cl.Version() if err != nil { if errors.Is(err, errConnRefused) { return connectionRefused, err } return connectionOtherError, err } if version < api.Version { return daemonOutdated, nil } return daemonOK, nil } func killDaemon(sockpath string, cl daemondefs.Client) error { pid, err := cl.Pid() if err != nil { return fmt.Errorf("kill daemon: %w", err) } process, err := os.FindProcess(pid) if err != nil { return fmt.Errorf("kill daemon: %w", err) } err = process.Signal(os.Interrupt) if err != nil { return fmt.Errorf("kill daemon: %w", err) } // Wait until the old daemon has removed the socket file, so that it doesn't // inadvertently remove the socket file of the new daemon we will start. start := time.Now() for time.Since(start) < daemonKillTimeout { _, err := os.Lstat(sockpath) if err == nil { time.Sleep(daemonKillWaitPerLoop) } else if os.IsNotExist(err) { return nil } else { return fmt.Errorf("kill daemon: %w", err) } } return fmt.Errorf("kill daemon: daemon did not remove socket within %v", daemonKillTimeout) } // Can be overridden in tests to avoid actual forking. var startProcess = func(name string, argv []string, attr *os.ProcAttr) error { _, err := os.StartProcess(name, argv, attr) return err } // Spawns a daemon process in the background by invoking BinPath, passing // BinPath, DbPath and SockPath as command-line arguments after resolving them // to absolute paths. The daemon log file is created in RunDir, and the stdout // and stderr of the daemon is redirected to the log file. // // A suitable ProcAttr is chosen depending on the OS and makes sure that the // daemon is detached from the current terminal, so that it is not affected by // I/O or signals in the current terminal and keeps running after the current // process quits. func spawn(cfg *daemondefs.SpawnConfig) error { binPath, err := os.Executable() if err != nil { return errors.New("cannot find elvish: " + err.Error()) } dbPath, err := abs("DbPath", cfg.DbPath) if err != nil { return err } sockPath, err := abs("SockPath", cfg.SockPath) if err != nil { return err } args := []string{ binPath, "-daemon", "-db", dbPath, "-sock", sockPath, } // The daemon does not read any input; open DevNull and use it for stdin. We // could also just close the stdin, but on Unix that would make the first // file opened by the daemon take FD 0. in, err := os.OpenFile(os.DevNull, os.O_RDONLY, 0) if err != nil { return err } defer in.Close() out, err := fsutil.ClaimFile(cfg.RunDir, "daemon-*.log") if err != nil { return err } defer out.Close() procattrs := procAttrForSpawn([]*os.File{in, out, out}) err = startProcess(binPath, args, procattrs) return err } func abs(name, path string) (string, error) { if path == "" { return "", fmt.Errorf("%s is required for spawning daemon", name) } absPath, err := filepath.Abs(path) if err != nil { return "", fmt.Errorf("cannot resolve %s to absolute path: %s", name, err) } return absPath, nil } elvish-0.17.0/pkg/daemon/activate_test.go000066400000000000000000000061321415471104000203020ustar00rootroot00000000000000package daemon import ( "io" "net" "os" "runtime" "testing" "time" "src.elv.sh/pkg/daemon/daemondefs" "src.elv.sh/pkg/testutil" ) func TestActivate_ConnectsToExistingServer(t *testing.T) { setup(t) startServer(t, cli("sock", "db")) _, err := Activate(io.Discard, &daemondefs.SpawnConfig{DbPath: "db", SockPath: "sock", RunDir: "."}) if err != nil { t.Errorf("got error %v, want nil", err) } } func TestActivate_SpawnsNewServer(t *testing.T) { activated := 0 setupForActivate(t, func(name string, argv []string, attr *os.ProcAttr) error { startServer(t, argv) activated++ return nil }) _, err := Activate(io.Discard, &daemondefs.SpawnConfig{DbPath: "db", SockPath: "sock", RunDir: "."}) if err != nil { t.Errorf("got error %v, want nil", err) } if activated != 1 { t.Errorf("got activated %v times, want 1", activated) } } func TestActivate_RemovesHangingSocketAndSpawnsNewServer(t *testing.T) { activated := 0 setupForActivate(t, func(name string, argv []string, attr *os.ProcAttr) error { startServer(t, argv) activated++ return nil }) makeHangingUNIXSocket(t, "sock") _, err := Activate(io.Discard, &daemondefs.SpawnConfig{DbPath: "db", SockPath: "sock", RunDir: "."}) if err != nil { t.Errorf("got error %v, want nil", err) } if activated != 1 { t.Errorf("got activated %v times, want 1", activated) } } func TestActivate_FailsIfCannotStatSock(t *testing.T) { setup(t) // Build a path for which Lstat will return a non-nil err such that // os.IsNotExist(err) is false. badSockPath := "" if runtime.GOOS != "windows" { // POSIX lstat(2) returns ENOTDIR instead of ENOENT if a path prefix is // not a directory. testutil.MustCreateEmpty("not-dir") badSockPath = "not-dir/sock" } else { // Use a syntactically invalid drive letter on Windows. badSockPath = `CD:\sock` } _, err := Activate(io.Discard, &daemondefs.SpawnConfig{DbPath: "db", SockPath: badSockPath, RunDir: "."}) if err == nil { t.Errorf("got error nil, want non-nil") } } func TestActivate_FailsIfCannotDialSock(t *testing.T) { setup(t) testutil.MustCreateEmpty("sock") _, err := Activate(io.Discard, &daemondefs.SpawnConfig{DbPath: "db", SockPath: "sock", RunDir: "."}) if err == nil { t.Errorf("got error nil, want non-nil") } } func setupForActivate(t *testing.T, f func(string, []string, *os.ProcAttr) error) { setup(t) saveStartProcess := startProcess t.Cleanup(func() { startProcess = saveStartProcess }) startProcess = f scaleDuration(t, &daemonSpawnTimeout) scaleDuration(t, &daemonKillTimeout) } func scaleDuration(t *testing.T, d *time.Duration) { save := *d t.Cleanup(func() { *d = save }) *d = testutil.Scaled(*d) } func makeHangingUNIXSocket(t *testing.T, path string) { t.Helper() l, err := net.Listen("unix", path) if err != nil { t.Fatal(err) } // We need to call l.Close() to make the socket hang, but that will // helpfully remove the socket file. Work around this by renaming the socket // file. err = os.Rename(path, path+".save") if err != nil { t.Fatal(err) } l.Close() err = os.Rename(path+".save", path) if err != nil { t.Fatal(err) } } elvish-0.17.0/pkg/daemon/activate_unix_test.go000066400000000000000000000030421415471104000213420ustar00rootroot00000000000000//go:build !windows && !plan9 && !js // +build !windows,!plan9,!js package daemon import ( "io" "os" "os/user" "testing" "src.elv.sh/pkg/daemon/daemondefs" "src.elv.sh/pkg/daemon/internal/api" "src.elv.sh/pkg/testutil" ) func TestActivate_InterruptsOutdatedServerAndSpawnsNewServer(t *testing.T) { activated := 0 setupForActivate(t, func(name string, argv []string, attr *os.ProcAttr) error { startServer(t, argv) activated++ return nil }) version := api.Version - 1 oldServer := startServerOpts(t, cli("sock", "db"), ServeOpts{Version: &version}) _, err := Activate(io.Discard, &daemondefs.SpawnConfig{DbPath: "db", SockPath: "sock", RunDir: "."}) if err != nil { t.Errorf("got error %v, want nil", err) } if activated != 1 { t.Errorf("got activated %v times, want 1", activated) } oldServer.WaitQuit() } func TestActivate_FailsIfUnableToRemoveHangingSocket(t *testing.T) { if u, err := user.Current(); err != nil || u.Uid == "0" { t.Skip("current user is root or unknown") } activated := 0 setupForActivate(t, func(name string, argv []string, attr *os.ProcAttr) error { activated++ return nil }) testutil.MustMkdirAll("d") makeHangingUNIXSocket(t, "d/sock") // Remove write permission so that removing d/sock will fail os.Chmod("d", 0600) defer os.Chmod("d", 0700) _, err := Activate(io.Discard, &daemondefs.SpawnConfig{DbPath: "db", SockPath: "d/sock", RunDir: "."}) if err == nil { t.Errorf("got error nil, want non-nil") } if activated != 0 { t.Errorf("got activated %v times, want 0", activated) } } elvish-0.17.0/pkg/daemon/client.go000066400000000000000000000116461415471104000167270ustar00rootroot00000000000000package daemon import ( "errors" "net" "sync" "src.elv.sh/pkg/daemon/daemondefs" "src.elv.sh/pkg/daemon/internal/api" "src.elv.sh/pkg/rpc" "src.elv.sh/pkg/store/storedefs" ) const retriesOnShutdown = 3 var ( // ErrDaemonUnreachable is returned when the daemon cannot be reached after // several retries. ErrDaemonUnreachable = errors.New("daemon offline") ) // Implementation of the Client interface. type client struct { sockPath string rpcClient *rpc.Client waits sync.WaitGroup } // NewClient creates a new Client instance that talks to the socket. Connection // creation is deferred to the first request. func NewClient(sockPath string) daemondefs.Client { return &client{sockPath, nil, sync.WaitGroup{}} } // SockPath returns the socket path that the Client talks to. If the client is // nil, it returns an empty string. func (c *client) SockPath() string { return c.sockPath } // ResetConn resets the current connection. A new connection will be established // the next time a request is made. If the client is nil, it does nothing. func (c *client) ResetConn() error { if c.rpcClient == nil { return nil } rc := c.rpcClient c.rpcClient = nil return rc.Close() } // Close waits for all outstanding requests to finish and close the connection. // If the client is nil, it does nothing and returns nil. func (c *client) Close() error { c.waits.Wait() return c.ResetConn() } func (c *client) call(f string, req, res interface{}) error { c.waits.Add(1) defer c.waits.Done() for attempt := 0; attempt < retriesOnShutdown; attempt++ { if c.rpcClient == nil { conn, err := net.Dial("unix", c.sockPath) if err != nil { return err } c.rpcClient = rpc.NewClient(conn) } err := c.rpcClient.Call(api.ServiceName+"."+f, req, res) if err == rpc.ErrShutdown { // Clear rpcClient so as to reconnect next time c.rpcClient = nil continue } else { return err } } return ErrDaemonUnreachable } // Convenience methods for RPC methods. These are quite repetitive; when the // number of RPC calls grow above some threshold, a code generator should be // written to generate them. func (c *client) Version() (int, error) { req := &api.VersionRequest{} res := &api.VersionResponse{} err := c.call("Version", req, res) return res.Version, err } func (c *client) Pid() (int, error) { req := &api.PidRequest{} res := &api.PidResponse{} err := c.call("Pid", req, res) return res.Pid, err } func (c *client) NextCmdSeq() (int, error) { req := &api.NextCmdRequest{} res := &api.NextCmdSeqResponse{} err := c.call("NextCmdSeq", req, res) return res.Seq, err } func (c *client) AddCmd(text string) (int, error) { req := &api.AddCmdRequest{Text: text} res := &api.AddCmdResponse{} err := c.call("AddCmd", req, res) return res.Seq, err } func (c *client) DelCmd(seq int) error { req := &api.DelCmdRequest{Seq: seq} res := &api.DelCmdResponse{} err := c.call("DelCmd", req, res) return err } func (c *client) Cmd(seq int) (string, error) { req := &api.CmdRequest{Seq: seq} res := &api.CmdResponse{} err := c.call("Cmd", req, res) return res.Text, err } func (c *client) CmdsWithSeq(from, upto int) ([]storedefs.Cmd, error) { req := &api.CmdsWithSeqRequest{From: from, Upto: upto} res := &api.CmdsWithSeqResponse{} err := c.call("CmdsWithSeq", req, res) return res.Cmds, err } func (c *client) NextCmd(from int, prefix string) (storedefs.Cmd, error) { req := &api.NextCmdRequest{From: from, Prefix: prefix} res := &api.NextCmdResponse{} err := c.call("NextCmd", req, res) return storedefs.Cmd{Text: res.Text, Seq: res.Seq}, err } func (c *client) PrevCmd(upto int, prefix string) (storedefs.Cmd, error) { req := &api.PrevCmdRequest{Upto: upto, Prefix: prefix} res := &api.PrevCmdResponse{} err := c.call("PrevCmd", req, res) return storedefs.Cmd{Text: res.Text, Seq: res.Seq}, err } func (c *client) AddDir(dir string, incFactor float64) error { req := &api.AddDirRequest{Dir: dir, IncFactor: incFactor} res := &api.AddDirResponse{} err := c.call("AddDir", req, res) return err } func (c *client) DelDir(dir string) error { req := &api.DelDirRequest{Dir: dir} res := &api.DelDirResponse{} err := c.call("DelDir", req, res) return err } func (c *client) Dirs(blacklist map[string]struct{}) ([]storedefs.Dir, error) { req := &api.DirsRequest{Blacklist: blacklist} res := &api.DirsResponse{} err := c.call("Dirs", req, res) return res.Dirs, err } func (c *client) SharedVar(name string) (string, error) { req := &api.SharedVarRequest{Name: name} res := &api.SharedVarResponse{} err := c.call("SharedVar", req, res) return res.Value, err } func (c *client) SetSharedVar(name, value string) error { req := &api.SetSharedVarRequest{Name: name, Value: value} res := &api.SetSharedVarResponse{} return c.call("SetSharedVar", req, res) } func (c *client) DelSharedVar(name string) error { req := &api.DelSharedVarRequest{Name: name} res := &api.DelSharedVarResponse{} return c.call("DelSharedVar", req, res) } elvish-0.17.0/pkg/daemon/daemondefs/000077500000000000000000000000001415471104000172175ustar00rootroot00000000000000elvish-0.17.0/pkg/daemon/daemondefs/daemondefs.go000066400000000000000000000017351415471104000216610ustar00rootroot00000000000000// Package daemondefs contains definitions used for the daemon. // // It is a separate package so that packages that only depend on the daemon // API does not need to depend on the concrete implementation. package daemondefs import ( "io" "src.elv.sh/pkg/store/storedefs" ) // Client represents a daemon client. type Client interface { storedefs.Store ResetConn() error Close() error Pid() (int, error) SockPath() string Version() (int, error) } // ActivateFunc is a function that activates a daemon client, possibly by // spawning a new daemon and connecting to it. type ActivateFunc func(stderr io.Writer, spawnCfg *SpawnConfig) (Client, error) // SpawnConfig keeps configurations for spawning the daemon. type SpawnConfig struct { // DbPath is the path to the database. DbPath string // SockPath is the path to the socket on which the daemon will serve // requests. SockPath string // RunDir is the directory in which to place the daemon log file. RunDir string } elvish-0.17.0/pkg/daemon/internal/000077500000000000000000000000001415471104000167265ustar00rootroot00000000000000elvish-0.17.0/pkg/daemon/internal/api/000077500000000000000000000000001415471104000174775ustar00rootroot00000000000000elvish-0.17.0/pkg/daemon/internal/api/api.go000066400000000000000000000035521415471104000206040ustar00rootroot00000000000000// Package api defines types and constants useful for the API between the daemon // service and client. package api import ( "src.elv.sh/pkg/store/storedefs" ) // Version is the API version. It should be bumped any time the API changes. const Version = -93 // ServiceName is the name of the RPC service exposed by the daemon. const ServiceName = "Daemon" // Basic requests. type VersionRequest struct{} type VersionResponse struct { Version int } type PidRequest struct{} type PidResponse struct { Pid int } // Cmd requests. type NextCmdSeqRequest struct{} type NextCmdSeqResponse struct { Seq int } type AddCmdRequest struct { Text string } type AddCmdResponse struct { Seq int } type DelCmdRequest struct { Seq int } type DelCmdResponse struct { } type CmdRequest struct { Seq int } type CmdResponse struct { Text string } type CmdsRequest struct { From int Upto int } type CmdsResponse struct { Cmds []string } type CmdsWithSeqRequest struct { From int Upto int } type CmdsWithSeqResponse struct { Cmds []storedefs.Cmd } type NextCmdRequest struct { From int Prefix string } type NextCmdResponse struct { Seq int Text string } type PrevCmdRequest struct { Upto int Prefix string } type PrevCmdResponse struct { Seq int Text string } // Dir requests. type AddDirRequest struct { Dir string IncFactor float64 } type AddDirResponse struct{} type DelDirRequest struct { Dir string } type DelDirResponse struct{} type DirsRequest struct { Blacklist map[string]struct{} } type DirsResponse struct { Dirs []storedefs.Dir } // SharedVar requests. type SharedVarRequest struct { Name string } type SharedVarResponse struct { Value string } type SetSharedVarRequest struct { Name string Value string } type SetSharedVarResponse struct{} type DelSharedVarRequest struct { Name string } type DelSharedVarResponse struct{} elvish-0.17.0/pkg/daemon/server.go000066400000000000000000000076631415471104000167630ustar00rootroot00000000000000// Package daemon implements a service for mediating access to the data store, // and its client. // // Most RPCs exposed by the service correspond to the methods of Store in the // store package and are not documented here. package daemon import ( "net" "net/rpc" "os" "os/signal" "syscall" "src.elv.sh/pkg/daemon/internal/api" "src.elv.sh/pkg/logutil" "src.elv.sh/pkg/prog" "src.elv.sh/pkg/store" ) var logger = logutil.GetLogger("[daemon] ") // Program is the daemon subprogram. var Program prog.Program = program{} type program struct { ServeOpts ServeOpts } func (p program) Run(fds [3]*os.File, f *prog.Flags, args []string) error { if !f.Daemon { return prog.ErrNotSuitable } if len(args) > 0 { return prog.BadUsage("arguments are not allowed with -daemon") } setUmaskForDaemon() exit := Serve(f.Sock, f.DB, p.ServeOpts) return prog.Exit(exit) } // ServeOpts keeps options that can be passed to Serve. type ServeOpts struct { // If not nil, will be closed when the daemon is ready to serve requests. Ready chan<- struct{} // Causes the daemon to abort if closed or sent any date. If nil, Serve will // set up its own signal channel by listening to SIGINT and SIGTERM. Signals <-chan os.Signal // If not nil, overrides the response of the Version RPC. Version *int } // Serve runs the daemon service, listening on the socket specified by sockpath // and serving data from dbpath until all clients have exited. See doc for // ServeChans for additional options. func Serve(sockpath, dbpath string, opts ServeOpts) int { logger.Println("pid is", syscall.Getpid()) logger.Println("going to listen", sockpath) listener, err := net.Listen("unix", sockpath) if err != nil { logger.Printf("failed to listen on %s: %v", sockpath, err) logger.Println("aborting") return 2 } st, err := store.NewStore(dbpath) if err != nil { logger.Printf("failed to create storage: %v", err) logger.Printf("serving anyway") } server := rpc.NewServer() version := api.Version if opts.Version != nil { version = *opts.Version } server.RegisterName(api.ServiceName, &service{version, st, err}) connCh := make(chan net.Conn, 10) listenErrCh := make(chan error, 1) go func() { for { conn, err := listener.Accept() if err != nil { listenErrCh <- err close(listenErrCh) return } connCh <- conn } }() sigCh := opts.Signals if sigCh == nil { ch := make(chan os.Signal, 1) signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT) sigCh = ch } conns := make(map[net.Conn]struct{}) connDoneCh := make(chan net.Conn, 10) interrupt := func() { if len(conns) == 0 { logger.Println("exiting since there are no clients") } logger.Printf("going to close %v active connections", len(conns)) for conn := range conns { err := conn.Close() if err != nil { logger.Println("failed to close connection:", err) } } } if opts.Ready != nil { close(opts.Ready) } loop: for { select { case sig := <-sigCh: logger.Printf("received signal %v", sig) interrupt() break loop case err := <-listenErrCh: logger.Println("could not listen:", err) if len(conns) == 0 { logger.Println("exiting since there are no clients") break loop } logger.Println("continuing to serve until all existing clients exit") case conn := <-connCh: conns[conn] = struct{}{} go func() { server.ServeConn(conn) connDoneCh <- conn }() case conn := <-connDoneCh: delete(conns, conn) if len(conns) == 0 { logger.Println("all clients disconnected, exiting") break loop } } } err = os.Remove(sockpath) if err != nil { logger.Printf("failed to remove socket %s: %v", sockpath, err) } if st != nil { err = st.Close() if err != nil { logger.Printf("failed to close storage: %v", err) } } err = listener.Close() if err != nil { logger.Printf("failed to close listener: %v", err) } // Ensure that the listener goroutine has exited before returning <-listenErrCh return 0 } elvish-0.17.0/pkg/daemon/server_test.go000066400000000000000000000101451415471104000200070ustar00rootroot00000000000000package daemon import ( "os" "syscall" "testing" "time" "src.elv.sh/pkg/daemon/daemondefs" "src.elv.sh/pkg/daemon/internal/api" . "src.elv.sh/pkg/prog/progtest" "src.elv.sh/pkg/store/storetest" "src.elv.sh/pkg/testutil" ) func TestProgram_TerminatesIfCannotListen(t *testing.T) { setup(t) testutil.MustCreateEmpty("sock") Test(t, Program, ThatElvish("-daemon", "-sock", "sock", "-db", "db"). ExitsWith(2). WritesStdoutContaining("failed to listen on sock"), ) } func TestProgram_ServesClientRequests(t *testing.T) { setup(t) startServer(t, cli("sock", "db")) client := startClient(t, "sock") // Test server state requests. gotVersion, err := client.Version() if gotVersion != api.Version || err != nil { t.Errorf(".Version() -> (%v, %v), want (%v, nil)", gotVersion, err, api.Version) } gotPid, err := client.Pid() wantPid := syscall.Getpid() if gotPid != wantPid || err != nil { t.Errorf(".Pid() -> (%v, %v), want (%v, nil)", gotPid, err, wantPid) } // Test store requests. storetest.TestCmd(t, client) storetest.TestDir(t, client) storetest.TestSharedVar(t, client) } func TestProgram_StillServesIfCannotOpenDB(t *testing.T) { setup(t) testutil.MustWriteFile("db", "not a valid bolt database") startServer(t, cli("sock", "db")) client := startClient(t, "sock") _, err := client.AddCmd("cmd") if err == nil { t.Errorf("got nil error, want non-nil") } } func TestProgram_QuitsOnSignalChannelWithNoClient(t *testing.T) { setup(t) sigCh := make(chan os.Signal) startServerOpts(t, cli("sock", "db"), ServeOpts{Signals: sigCh}) close(sigCh) // startServerSigCh will wait for server to terminate at cleanup } func TestProgram_QuitsOnSignalChannelWithClients(t *testing.T) { setup(t) sigCh := make(chan os.Signal) server := startServerOpts(t, cli("sock", "db"), ServeOpts{Signals: sigCh}) client := startClient(t, "sock") close(sigCh) server.WaitQuit() _, err := client.Version() if err == nil { t.Errorf("client.Version() returns nil error, want non-nil") } } func TestProgram_BadCLI(t *testing.T) { Test(t, Program, ThatElvish(). ExitsWith(2). WritesStderr("internal error: no suitable subprogram\n"), ThatElvish("-daemon", "x"). ExitsWith(2). WritesStderrContaining("arguments are not allowed with -daemon"), ) } func setup(t *testing.T) { testutil.Umask(t, 0) testutil.InTempDir(t) } // Calls startServerOpts with a Signals channel that gets closed during cleanup. func startServer(t *testing.T, args []string) server { t.Helper() sigCh := make(chan os.Signal) s := startServerOpts(t, args, ServeOpts{Signals: sigCh}) // Cleanup functions added later are run earlier. This will be run before // the cleanup function added by startServerOpts that waits for the server // to terminate. t.Cleanup(func() { close(sigCh) }) return s } // Start server with custom ServeOpts (opts.Ready is ignored). Makes sure that // the server terminates during cleanup. func startServerOpts(t *testing.T, args []string, opts ServeOpts) server { t.Helper() readyCh := make(chan struct{}) opts.Ready = readyCh doneCh := make(chan serverResult) go func() { exit, stdout, stderr := Run(program{opts}, args...) doneCh <- serverResult{exit, stdout, stderr} close(doneCh) }() select { case <-readyCh: case <-time.After(testutil.Scaled(2 * time.Second)): t.Fatal("timed out waiting for daemon to start") } s := server{t, doneCh} t.Cleanup(func() { s.WaitQuit() }) return s } type server struct { t *testing.T ch <-chan serverResult } type serverResult struct { exit int stdout, stderr string } func (s server) WaitQuit() (serverResult, bool) { s.t.Helper() select { case r := <-s.ch: return r, true case <-time.After(testutil.Scaled(2 * time.Second)): s.t.Error("timed out waiting for daemon to quit") return serverResult{}, false } } func cli(sock, db string) []string { return []string{"elvish", "-daemon", "-sock", sock, "-db", db} } func startClient(t *testing.T, sock string) daemondefs.Client { cl := NewClient("sock") if _, err := cl.Version(); err != nil { t.Errorf("failed to start client: %v", err) } t.Cleanup(func() { cl.Close() }) return cl } elvish-0.17.0/pkg/daemon/server_unix_test.go000066400000000000000000000012561415471104000210550ustar00rootroot00000000000000//go:build !windows && !plan9 && !js // +build !windows,!plan9,!js package daemon import ( "os" "syscall" "testing" ) func TestProgram_QuitsOnSystemSignal_SIGINT(t *testing.T) { testProgram_QuitsOnSystemSignal(t, syscall.SIGINT) } func TestProgram_QuitsOnSystemSignal_SIGTERM(t *testing.T) { testProgram_QuitsOnSystemSignal(t, syscall.SIGTERM) } func testProgram_QuitsOnSystemSignal(t *testing.T, sig os.Signal) { t.Helper() setup(t) startServerOpts(t, cli("sock", "db"), ServeOpts{Signals: nil}) p, err := os.FindProcess(os.Getpid()) if err != nil { t.Fatalf("FindProcess: %v", err) } p.Signal(sig) // startServerOpts will wait for server to terminate at cleanup } elvish-0.17.0/pkg/daemon/service.go000066400000000000000000000060021415471104000170770ustar00rootroot00000000000000package daemon import ( "syscall" "src.elv.sh/pkg/daemon/internal/api" "src.elv.sh/pkg/store/storedefs" ) // A net/rpc service for the daemon. type service struct { version int store storedefs.Store err error } // Implementations of RPC methods. // Version returns the API version number. func (s *service) Version(req *api.VersionRequest, res *api.VersionResponse) error { res.Version = s.version return nil } // Pid returns the process ID of the daemon. func (s *service) Pid(req *api.PidRequest, res *api.PidResponse) error { res.Pid = syscall.Getpid() return nil } func (s *service) NextCmdSeq(req *api.NextCmdSeqRequest, res *api.NextCmdSeqResponse) error { if s.err != nil { return s.err } seq, err := s.store.NextCmdSeq() res.Seq = seq return err } func (s *service) AddCmd(req *api.AddCmdRequest, res *api.AddCmdResponse) error { if s.err != nil { return s.err } seq, err := s.store.AddCmd(req.Text) res.Seq = seq return err } func (s *service) DelCmd(req *api.DelCmdRequest, res *api.DelCmdResponse) error { if s.err != nil { return s.err } err := s.store.DelCmd(req.Seq) return err } func (s *service) Cmd(req *api.CmdRequest, res *api.CmdResponse) error { if s.err != nil { return s.err } text, err := s.store.Cmd(req.Seq) res.Text = text return err } func (s *service) CmdsWithSeq(req *api.CmdsWithSeqRequest, res *api.CmdsWithSeqResponse) error { if s.err != nil { return s.err } cmds, err := s.store.CmdsWithSeq(req.From, req.Upto) res.Cmds = cmds return err } func (s *service) NextCmd(req *api.NextCmdRequest, res *api.NextCmdResponse) error { if s.err != nil { return s.err } cmd, err := s.store.NextCmd(req.From, req.Prefix) res.Seq, res.Text = cmd.Seq, cmd.Text return err } func (s *service) PrevCmd(req *api.PrevCmdRequest, res *api.PrevCmdResponse) error { if s.err != nil { return s.err } cmd, err := s.store.PrevCmd(req.Upto, req.Prefix) res.Seq, res.Text = cmd.Seq, cmd.Text return err } func (s *service) AddDir(req *api.AddDirRequest, res *api.AddDirResponse) error { if s.err != nil { return s.err } return s.store.AddDir(req.Dir, req.IncFactor) } func (s *service) DelDir(req *api.DelDirRequest, res *api.DelDirResponse) error { if s.err != nil { return s.err } return s.store.DelDir(req.Dir) } func (s *service) Dirs(req *api.DirsRequest, res *api.DirsResponse) error { if s.err != nil { return s.err } dirs, err := s.store.Dirs(req.Blacklist) res.Dirs = dirs return err } func (s *service) SharedVar(req *api.SharedVarRequest, res *api.SharedVarResponse) error { if s.err != nil { return s.err } value, err := s.store.SharedVar(req.Name) res.Value = value return err } func (s *service) SetSharedVar(req *api.SetSharedVarRequest, res *api.SetSharedVarResponse) error { if s.err != nil { return s.err } return s.store.SetSharedVar(req.Name, req.Value) } func (s *service) DelSharedVar(req *api.DelSharedVarRequest, res *api.DelSharedVarResponse) error { if s.err != nil { return s.err } return s.store.DelSharedVar(req.Name) } elvish-0.17.0/pkg/daemon/sys_unix.go000066400000000000000000000010151415471104000173170ustar00rootroot00000000000000//go:build !windows && !plan9 && !js // +build !windows,!plan9,!js package daemon import ( "os" "syscall" "golang.org/x/sys/unix" ) var errConnRefused = syscall.ECONNREFUSED // Make sure that files created by the daemon is not accessible to other users. func setUmaskForDaemon() { unix.Umask(0077) } func procAttrForSpawn(files []*os.File) *os.ProcAttr { return &os.ProcAttr{ Dir: "/", Env: []string{}, Files: files, Sys: &syscall.SysProcAttr{ Setsid: true, // detach from current terminal }, } } elvish-0.17.0/pkg/daemon/sys_windows.go000066400000000000000000000015561415471104000200400ustar00rootroot00000000000000package daemon import ( "os" "syscall" ) // https://docs.microsoft.com/en-us/windows/win32/winsock/windows-sockets-error-codes-2 var errConnRefused = syscall.Errno(10061) // No-op on Windows. func setUmaskForDaemon() {} // A subset of possible process creation flags, value taken from // https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863(v=vs.85).aspx const ( createBreakwayFromJob = 0x01000000 createNewProcessGroup = 0x00000200 detachedProcess = 0x00000008 daemonCreationFlags = createBreakwayFromJob | createNewProcessGroup | detachedProcess ) func procAttrForSpawn(files []*os.File) *os.ProcAttr { return &os.ProcAttr{ Dir: `C:\`, Env: []string{"SystemRoot=" + os.Getenv("SystemRoot")}, // SystemRoot is needed for net.Listen for some reason Files: files, Sys: &syscall.SysProcAttr{CreationFlags: daemonCreationFlags}, } } elvish-0.17.0/pkg/diag/000077500000000000000000000000001415471104000145535ustar00rootroot00000000000000elvish-0.17.0/pkg/diag/context.go000066400000000000000000000101241415471104000165640ustar00rootroot00000000000000package diag import ( "bytes" "fmt" "strings" "src.elv.sh/pkg/wcwidth" ) // Context is a range of text in a source code. It is typically used for // errors that can be associated with a part of the source code, like parse // errors and a traceback entry. type Context struct { Name string Source string Ranging savedShowInfo *rangeShowInfo } // NewContext creates a new Context. func NewContext(name, source string, r Ranger) *Context { return &Context{name, source, r.Range(), nil} } // Information about the source range that are needed for showing. type rangeShowInfo struct { // Head is the piece of text immediately before Culprit, extending to, but // not including the closest line boundary. If Culprit already starts after // a line boundary, Head is an empty string. Head string // Culprit is Source[Begin:End], with any trailing newlines stripped. Culprit string // Tail is the piece of text immediately after Culprit, extending to, but // not including the closet line boundary. If Culprit already ends before a // line boundary, Tail is an empty string. Tail string // BeginLine is the (1-based) line number that the first character of Culprit is on. BeginLine int // EndLine is the (1-based) line number that the last character of Culprit is on. EndLine int } // Variables controlling the style of the culprit. var ( culpritLineBegin = "\033[1;4m" culpritLineEnd = "\033[m" culpritPlaceHolder = "^" ) func (c *Context) RelevantString() string { return c.Source[c.From:c.To] } func (c *Context) showInfo() *rangeShowInfo { if c.savedShowInfo != nil { return c.savedShowInfo } before := c.Source[:c.From] culprit := c.Source[c.From:c.To] after := c.Source[c.To:] head := lastLine(before) beginLine := strings.Count(before, "\n") + 1 // If the culprit ends with a newline, stripe it. Otherwise, tail is nonempty. var tail string if strings.HasSuffix(culprit, "\n") { culprit = culprit[:len(culprit)-1] } else { tail = firstLine(after) } endLine := beginLine + strings.Count(culprit, "\n") c.savedShowInfo = &rangeShowInfo{head, culprit, tail, beginLine, endLine} return c.savedShowInfo } // Show shows a SourceContext. func (c *Context) Show(sourceIndent string) string { if err := c.checkPosition(); err != nil { return err.Error() } return (c.Name + ", " + c.lineRange() + "\n" + sourceIndent + c.relevantSource(sourceIndent)) } // ShowCompact shows a SourceContext, with no line break between the // source position range description and relevant source excerpt. func (c *Context) ShowCompact(sourceIndent string) string { if err := c.checkPosition(); err != nil { return err.Error() } desc := c.Name + ", " + c.lineRange() + " " // Extra indent so that following lines line up with the first line. descIndent := strings.Repeat(" ", wcwidth.Of(desc)) return desc + c.relevantSource(sourceIndent+descIndent) } func (c *Context) checkPosition() error { if c.From == -1 { return fmt.Errorf("%s, unknown position", c.Name) } else if c.From < 0 || c.To > len(c.Source) || c.From > c.To { return fmt.Errorf("%s, invalid position %d-%d", c.Name, c.From, c.To) } return nil } func (c *Context) lineRange() string { info := c.showInfo() if info.BeginLine == info.EndLine { return fmt.Sprintf("line %d:", info.BeginLine) } return fmt.Sprintf("line %d-%d:", info.BeginLine, info.EndLine) } func (c *Context) relevantSource(sourceIndent string) string { info := c.showInfo() var buf bytes.Buffer buf.WriteString(info.Head) culprit := info.Culprit if culprit == "" { culprit = culpritPlaceHolder } for i, line := range strings.Split(culprit, "\n") { if i > 0 { buf.WriteByte('\n') buf.WriteString(sourceIndent) } buf.WriteString(culpritLineBegin) buf.WriteString(line) buf.WriteString(culpritLineEnd) } buf.WriteString(info.Tail) return buf.String() } func firstLine(s string) string { i := strings.IndexByte(s, '\n') if i == -1 { return s } return s[:i] } func lastLine(s string) string { // When s does not contain '\n', LastIndexByte returns -1, which happens to // be what we want. return s[strings.LastIndexByte(s, '\n')+1:] } elvish-0.17.0/pkg/diag/context_test.go000066400000000000000000000033651415471104000176340ustar00rootroot00000000000000package diag import ( "strings" "testing" ) var sourceRangeTests = []struct { Name string Context *Context Indent string WantShow string WantShowCompact string }{ { Name: "single-line culprit", Context: parseContext("echo (bad)", "(", ")", true), Indent: "_", WantShow: lines( "[test], line 1:", "_echo <(bad)>", ), WantShowCompact: "[test], line 1: echo <(bad)>", }, { Name: "multi-line culprit", Context: parseContext("echo (bad\nbad)", "(", ")", true), Indent: "_", WantShow: lines( "[test], line 1-2:", "_echo <(bad>", "_", ), WantShowCompact: lines( "[test], line 1-2: echo <(bad>", "_ ", ), }, { Name: "empty culprit", Context: parseContext("echo x", "x", "x", false), Indent: "", WantShow: lines( "[test], line 1:", "echo <^>x", ), WantShowCompact: "[test], line 1: echo <^>x", }, } func TestContext(t *testing.T) { culpritLineBegin = "<" culpritLineEnd = ">" for _, test := range sourceRangeTests { t.Run(test.Name, func(t *testing.T) { gotShow := test.Context.Show(test.Indent) if gotShow != test.WantShow { t.Errorf("Show() -> %q, want %q", gotShow, test.WantShow) } gotShowCompact := test.Context.ShowCompact(test.Indent) if gotShowCompact != test.WantShowCompact { t.Errorf("ShowCompact() -> %q, want %q", gotShowCompact, test.WantShowCompact) } }) } } // Parse a string into a source range, using the first appearance of certain // texts as start and end positions. func parseContext(s, starter, ender string, endAfter bool) *Context { end := strings.Index(s, ender) if endAfter { end += len(ender) } return NewContext("[test]", s, Ranging{From: strings.Index(s, starter), To: end}) } elvish-0.17.0/pkg/diag/doc.go000066400000000000000000000001571415471104000156520ustar00rootroot00000000000000// Package diag contains building blocks for formatting and processing // diagnostic information. package diag elvish-0.17.0/pkg/diag/error.go000066400000000000000000000012311415471104000162300ustar00rootroot00000000000000package diag import "fmt" // Error represents an error with context that can be showed. type Error struct { Type string Message string Context Context } // Error returns a plain text representation of the error. func (e *Error) Error() string { return fmt.Sprintf("%s: %d-%d in %s: %s", e.Type, e.Context.From, e.Context.To, e.Context.Name, e.Message) } // Range returns the range of the error. func (e *Error) Range() Ranging { return e.Context.Range() } // Show shows the error. func (e *Error) Show(indent string) string { header := fmt.Sprintf("%s: \033[31;1m%s\033[m\n", e.Type, e.Message) return header + e.Context.ShowCompact(indent+" ") } elvish-0.17.0/pkg/diag/error_test.go000066400000000000000000000016011415471104000172700ustar00rootroot00000000000000package diag import ( "strings" "testing" ) func TestError(t *testing.T) { err := &Error{ Type: "some error", Message: "bad list", Context: *parseContext("echo [x]", "[", "]", true), } wantErrorString := "some error: 5-8 in [test]: bad list" if gotErrorString := err.Error(); gotErrorString != wantErrorString { t.Errorf("Error() -> %q, want %q", gotErrorString, wantErrorString) } wantRanging := Ranging{From: 5, To: 8} if gotRanging := err.Range(); gotRanging != wantRanging { t.Errorf("Range() -> %v, want %v", gotRanging, wantRanging) } culpritLineBegin = "<" culpritLineEnd = ">" wantShow := lines( "some error: \033[31;1mbad list\033[m", "[test], line 1: echo <[x]>", ) if gotShow := err.Show(""); gotShow != wantShow { t.Errorf("Show() -> %q, want %q", gotShow, wantShow) } } func lines(lines ...string) string { return strings.Join(lines, "\n") } elvish-0.17.0/pkg/diag/multierror.go000066400000000000000000000021131415471104000173030ustar00rootroot00000000000000package diag import "bytes" // MultiError pack multiple errors into one error. type MultiError struct { Errors []error } func (es MultiError) Error() string { switch len(es.Errors) { case 0: return "no error" case 1: return es.Errors[0].Error() default: var buf bytes.Buffer buf.WriteString("multiple errors: ") for i, e := range es.Errors { if i > 0 { buf.WriteString("; ") } buf.WriteString(e.Error()) } return buf.String() } } // Errors concatenate multiple errors into one. If all errors are nil, it // returns nil. If there is one non-nil error, it is returned. Otherwise the // return value is a MultiError containing all the non-nil arguments. Arguments // of the type MultiError are flattened. func Errors(errs ...error) error { var nonNil []error for _, err := range errs { if err != nil { if multi, ok := err.(MultiError); ok { nonNil = append(nonNil, multi.Errors...) } else { nonNil = append(nonNil, err) } } } switch len(nonNil) { case 0: return nil case 1: return nonNil[0] default: return MultiError{nonNil} } } elvish-0.17.0/pkg/diag/multierror_test.go000066400000000000000000000017171415471104000203530ustar00rootroot00000000000000package diag import ( "errors" "testing" ) var ( err1 = errors.New("error 1") err2 = errors.New("error 2") err3 = errors.New("error 3") ) var errorsTests = []struct { e error wantString string }{ {Errors(), ""}, {MultiError{}, "no error"}, {Errors(errors.New("some error")), "some error"}, { Errors(err1, err2), "multiple errors: error 1; error 2", }, { Errors(err1, err2, err3), "multiple errors: error 1; error 2; error 3", }, { Errors(err1, Errors(err2, err3)), "multiple errors: error 1; error 2; error 3", }, { Errors(Errors(err1, err2), err3), "multiple errors: error 1; error 2; error 3", }, } func TestErrors(t *testing.T) { for _, test := range errorsTests { if test.e == nil { if test.wantString != "" { t.Errorf("got nil, want %q", test.wantString) } } else { gotString := test.e.Error() if gotString != test.wantString { t.Errorf("got %q, want %q", gotString, test.wantString) } } } } elvish-0.17.0/pkg/diag/range.go000066400000000000000000000013131415471104000161740ustar00rootroot00000000000000package diag // Ranger wraps the Range method. type Ranger interface { // Range returns the range associated with the value. Range() Ranging } // Ranging represents a range [From, To) within an indexable sequence. Structs // can embed Ranging to satisfy the Ranger interface. type Ranging struct { From int To int } // Range returns the Ranging itself. func (r Ranging) Range() Ranging { return r } // PointRanging returns a zero-width Ranging at the given point. func PointRanging(p int) Ranging { return Ranging{p, p} } // MixedRanging returns a Ranging from the start position of a to the end // position of b. func MixedRanging(a, b Ranger) Ranging { return Ranging{a.Range().From, b.Range().To} } elvish-0.17.0/pkg/diag/range_test.go000066400000000000000000000012271415471104000172370ustar00rootroot00000000000000package diag import ( "testing" "src.elv.sh/pkg/tt" ) var Args = tt.Args type aRanger struct { Ranging } func TestEmbeddingRangingImplementsRanger(t *testing.T) { r := Ranging{1, 10} s := Ranger(aRanger{Ranging{1, 10}}) if s.Range() != r { t.Errorf("s.Range() = %v, want %v", s.Range(), r) } } func TestPointRanging(t *testing.T) { tt.Test(t, tt.Fn("PointRanging", PointRanging), tt.Table{ Args(1).Rets(Ranging{1, 1}), }) } func TestMixedRanging(t *testing.T) { tt.Test(t, tt.Fn("MixedRanging", MixedRanging), tt.Table{ Args(Ranging{1, 2}, Ranging{0, 4}).Rets(Ranging{1, 4}), Args(Ranging{0, 4}, Ranging{1, 2}).Rets(Ranging{0, 2}), }) } elvish-0.17.0/pkg/diag/show_error.go000066400000000000000000000012631415471104000172750ustar00rootroot00000000000000package diag import ( "fmt" "io" ) // ShowError shows an error. It uses the Show method if the error // implements Shower, and uses Complain to print the error message otherwise. func ShowError(w io.Writer, err error) { if shower, ok := err.(Shower); ok { fmt.Fprintln(w, shower.Show("")) } else { Complain(w, err.Error()) } } // Complain prints a message to w in bold and red, adding a trailing newline. func Complain(w io.Writer, msg string) { fmt.Fprintf(w, "\033[31;1m%s\033[m\n", msg) } // Complainf is like Complain, but accepts a format string and arguments. func Complainf(w io.Writer, format string, args ...interface{}) { Complain(w, fmt.Sprintf(format, args...)) } elvish-0.17.0/pkg/diag/show_error_test.go000066400000000000000000000012541415471104000203340ustar00rootroot00000000000000package diag import ( "errors" "strings" "testing" ) type showerError struct{} func (showerError) Error() string { return "error" } func (showerError) Show(_ string) string { return "show" } var showErrorTests = []struct { name string err error wantBuf string }{ {"A Shower error", showerError{}, "show\n"}, {"A errors.New error", errors.New("ERROR"), "\033[31;1mERROR\033[m\n"}, } func TestShowError(t *testing.T) { for _, test := range showErrorTests { t.Run(test.name, func(t *testing.T) { sb := &strings.Builder{} ShowError(sb, test.err) if sb.String() != test.wantBuf { t.Errorf("Wrote %q, want %q", sb.String(), test.wantBuf) } }) } } elvish-0.17.0/pkg/diag/shower.go000066400000000000000000000002271415471104000164120ustar00rootroot00000000000000package diag // Shower wraps the Show function. type Shower interface { // Show takes an indentation string and shows. Show(indent string) string } elvish-0.17.0/pkg/edit/000077500000000000000000000000001415471104000145745ustar00rootroot00000000000000elvish-0.17.0/pkg/edit/binding_map.go000066400000000000000000000050061415471104000173730ustar00rootroot00000000000000package edit import ( "errors" "sort" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/persistent/hashmap" "src.elv.sh/pkg/ui" ) var errValueShouldBeFn = errors.New("value should be function") // A special Map that converts its key to ui.Key and ensures that its values // satisfy eval.CallableValue. type bindingsMap struct { hashmap.Map } var emptyBindingsMap = bindingsMap{vals.EmptyMap} // Repr returns the representation of the binding table as if it were an // ordinary map keyed by strings. func (bt bindingsMap) Repr(indent int) string { var keys ui.Keys for it := bt.Map.Iterator(); it.HasElem(); it.Next() { k, _ := it.Elem() keys = append(keys, k.(ui.Key)) } sort.Sort(keys) builder := vals.NewMapReprBuilder(indent) for _, k := range keys { v, _ := bt.Map.Index(k) builder.WritePair(parse.Quote(k.String()), indent+2, vals.Repr(v, indent+2)) } return builder.String() } // Index converts the index to ui.Key and uses the Index of the inner Map. func (bt bindingsMap) Index(index interface{}) (interface{}, error) { key, err := toKey(index) if err != nil { return nil, err } return vals.Index(bt.Map, key) } func (bt bindingsMap) HasKey(k interface{}) bool { _, ok := bt.Map.Index(k) return ok } func (bt bindingsMap) GetKey(k ui.Key) eval.Callable { v, ok := bt.Map.Index(k) if !ok { panic("get called when key not present") } return v.(eval.Callable) } // Assoc converts the index to ui.Key, ensures that the value is CallableValue, // uses the Assoc of the inner Map and converts the result to a BindingTable. func (bt bindingsMap) Assoc(k, v interface{}) (interface{}, error) { key, err := toKey(k) if err != nil { return nil, err } f, ok := v.(eval.Callable) if !ok { return nil, errValueShouldBeFn } map2 := bt.Map.Assoc(key, f) return bindingsMap{map2}, nil } // Dissoc converts the key to ui.Key and calls the Dissoc method of the inner // map. func (bt bindingsMap) Dissoc(k interface{}) interface{} { key, err := toKey(k) if err != nil { // Key is invalid; dissoc is no-op. return bt } return bindingsMap{bt.Map.Dissoc(key)} } func makeBindingMap(raw hashmap.Map) (bindingsMap, error) { converted := vals.EmptyMap for it := raw.Iterator(); it.HasElem(); it.Next() { k, v := it.Elem() f, ok := v.(eval.Callable) if !ok { return emptyBindingsMap, errValueShouldBeFn } key, err := toKey(k) if err != nil { return bindingsMap{}, err } converted = converted.Assoc(key, f) } return bindingsMap{converted}, nil } elvish-0.17.0/pkg/edit/binding_map_test.go000066400000000000000000000027311415471104000204340ustar00rootroot00000000000000package edit import ( "testing" "src.elv.sh/pkg/eval" . "src.elv.sh/pkg/eval/evaltest" ) func TestBindingMap(t *testing.T) { // TODO TestWithSetup(t, func(ev *eval.Evaler) { ev.ExtendBuiltin(eval.BuildNs().AddGoFn("binding-map", makeBindingMap)) }, // Checking key and value when constructing That("binding-map [&[]={ }]"). Throws(ErrorWithMessage("must be key or string")), That("binding-map [&foo={ }]"). Throws(ErrorWithMessage("bad key: foo")), That("binding-map [&a=string]"). Throws(ErrorWithMessage("value should be function")), // repr prints a binding-map like an ordinary map That("repr (binding-map [&])").Prints("[&]\n"), // Keys are always sorted That("repr (binding-map [&a=$nop~ &b=$nop~ &c=$nop~])"). Prints("[&a= &b= &c=]\n"), // Indexing That("eq $nop~ (binding-map [&a=$nop~])[a]").Puts(true), // Checking key That("put (binding-map [&a=$nop~])[foo]"). Throws(ErrorWithMessage("bad key: foo")), // Assoc That("count (assoc (binding-map [&a=$nop~]) b $nop~)").Puts(2), // Checking key That("(assoc (binding-map [&a=$nop~]) foo $nop~)"). Throws(ErrorWithMessage("bad key: foo")), // Checking value That("(assoc (binding-map [&a=$nop~]) b foo)"). Throws(ErrorWithMessage("value should be function")), // Dissoc That("count (dissoc (binding-map [&a=$nop~]) a)").Puts(0), // Allows bad key - no op That("count (dissoc (binding-map [&a=$nop~]) foo)").Puts(1), ) } elvish-0.17.0/pkg/edit/buf_to_html.go000066400000000000000000000015131415471104000174250ustar00rootroot00000000000000package edit import ( "fmt" "html" "strings" "src.elv.sh/pkg/cli/term" ) // TODO(xiaq): Move this into the ui package. func bufToHTML(b *term.Buffer) string { var sb strings.Builder for _, line := range b.Lines { style := "" openedSpan := false for _, c := range line { if c.Style != style { if openedSpan { sb.WriteString("") } if c.Style == "" { openedSpan = false } else { var classes []string for _, c := range strings.Split(c.Style, ";") { classes = append(classes, "sgr-"+c) } fmt.Fprintf(&sb, ``, strings.Join(classes, " ")) openedSpan = true } style = c.Style } fmt.Fprintf(&sb, "%s", html.EscapeString(c.Text)) } if openedSpan { sb.WriteString("") } sb.WriteString("\n") } return sb.String() } elvish-0.17.0/pkg/edit/buf_to_html_test.go000066400000000000000000000017711415471104000204720ustar00rootroot00000000000000package edit import ( "testing" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/tt" ) func TestBufToHTML(t *testing.T) { tt.Test(t, tt.Fn("bufToHTML", bufToHTML), tt.Table{ // Just plain text. tt.Args( bb().Write("abc").Buffer(), ).Rets( `abc` + "\n", ), // Just styled text. tt.Args( bb().WriteStringSGR("abc", "31").Buffer(), ).Rets( `abc` + "\n", ), // Mixing plain and styled texts. tt.Args( bb(). WriteStringSGR("abc", "31"). Write(" def "). WriteStringSGR("xyz", "1"). Buffer(), ).Rets( `abc def xyz` + "\n", ), // Multiple lines. tt.Args( bb(). WriteStringSGR("abc", "31"). Newline().Write("def"). Newline().WriteStringSGR("xyz", "1"). Buffer(), ).Rets( `abc` + "\n" + `def` + "\n" + `xyz` + "\n", ), }) } func bb() *term.BufferBuilder { return term.NewBufferBuilder(20) } elvish-0.17.0/pkg/edit/buffer_builtins.go000066400000000000000000000404721415471104000203140ustar00rootroot00000000000000package edit import ( "strings" "unicode" "unicode/utf8" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/strutil" "src.elv.sh/pkg/wcwidth" ) func initBufferBuiltins(app cli.App, nb eval.NsBuilder) { m := make(map[string]interface{}) for name, fn := range bufferBuiltinsData { // Make a lexically scoped copy of fn. fn := fn m[name] = func() { codeArea, ok := focusedCodeArea(app) if !ok { return } codeArea.MutateState(func(s *tk.CodeAreaState) { fn(&s.Buffer) }) } } nb.AddGoFns(m) } var bufferBuiltinsData = map[string]func(*tk.CodeBuffer){ "move-dot-left": makeMove(moveDotLeft), "move-dot-right": makeMove(moveDotRight), "move-dot-left-word": makeMove(moveDotLeftWord), "move-dot-right-word": makeMove(moveDotRightWord), "move-dot-left-small-word": makeMove(moveDotLeftSmallWord), "move-dot-right-small-word": makeMove(moveDotRightSmallWord), "move-dot-left-alnum-word": makeMove(moveDotLeftAlnumWord), "move-dot-right-alnum-word": makeMove(moveDotRightAlnumWord), "move-dot-sol": makeMove(moveDotSOL), "move-dot-eol": makeMove(moveDotEOL), "move-dot-up": makeMove(moveDotUp), "move-dot-down": makeMove(moveDotDown), "kill-rune-left": makeKill(moveDotLeft), "kill-rune-right": makeKill(moveDotRight), "kill-word-left": makeKill(moveDotLeftWord), "kill-word-right": makeKill(moveDotRightWord), "kill-small-word-left": makeKill(moveDotLeftSmallWord), "kill-small-word-right": makeKill(moveDotRightSmallWord), "kill-left-alnum-word": makeKill(moveDotLeftAlnumWord), "kill-right-alnum-word": makeKill(moveDotRightAlnumWord), "kill-line-left": makeKill(moveDotSOL), "kill-line-right": makeKill(moveDotEOL), "transpose-rune": makeTransform(transposeRunes), "transpose-word": makeTransform(transposeWord), "transpose-small-word": makeTransform(transposeSmallWord), "transpose-alnum-word": makeTransform(transposeAlnumWord), } // A pure function that takes the current buffer and dot, and returns a new // value for the dot. Used to derive move- and kill- functions that operate on // the editor state. type pureMover func(buffer string, dot int) int func makeMove(m pureMover) func(*tk.CodeBuffer) { return func(buf *tk.CodeBuffer) { buf.Dot = m(buf.Content, buf.Dot) } } func makeKill(m pureMover) func(*tk.CodeBuffer) { return func(buf *tk.CodeBuffer) { newDot := m(buf.Content, buf.Dot) if newDot < buf.Dot { // Dot moved to the left: remove text between new dot and old dot, // and move the dot itself buf.Content = buf.Content[:newDot] + buf.Content[buf.Dot:] buf.Dot = newDot } else if newDot > buf.Dot { // Dot moved to the right: remove text between old dot and new dot. buf.Content = buf.Content[:buf.Dot] + buf.Content[newDot:] } } } // A pure function that takes the current buffer and dot, and returns a new // value for the buffer and dot. type pureTransformer func(buffer string, dot int) (string, int) func makeTransform(t pureTransformer) func(*tk.CodeBuffer) { return func(buf *tk.CodeBuffer) { buf.Content, buf.Dot = t(buf.Content, buf.Dot) } } // Implementation of pure movers. //elvdoc:fn move-dot-left // // Moves the dot left one rune. Does nothing if the dot is at the beginning of // the buffer. //elvdoc:fn kill-rune-left // // Kills one rune left of the dot. Does nothing if the dot is at the beginning of // the buffer. func moveDotLeft(buffer string, dot int) int { _, w := utf8.DecodeLastRuneInString(buffer[:dot]) return dot - w } //elvdoc:fn move-dot-right // // Moves the dot right one rune. Does nothing if the dot is at the end of the // buffer. //elvdoc:fn kill-rune-left // // Kills one rune right of the dot. Does nothing if the dot is at the end of the // buffer. func moveDotRight(buffer string, dot int) int { _, w := utf8.DecodeRuneInString(buffer[dot:]) return dot + w } //elvdoc:fn move-dot-sol // // Moves the dot to the start of the current line. //elvdoc:fn kill-line-left // // Deletes the text between the dot and the start of the current line. func moveDotSOL(buffer string, dot int) int { return strutil.FindLastSOL(buffer[:dot]) } //elvdoc:fn move-dot-eol // // Moves the dot to the end of the current line. //elvdoc:fn kill-line-right // // Deletes the text between the dot and the end of the current line. func moveDotEOL(buffer string, dot int) int { return strutil.FindFirstEOL(buffer[dot:]) + dot } //elvdoc:fn move-dot-up // // Moves the dot up one line, trying to preserve the visual horizontal position. // Does nothing if dot is already on the first line of the buffer. func moveDotUp(buffer string, dot int) int { sol := strutil.FindLastSOL(buffer[:dot]) if sol == 0 { // Already in the first line. return dot } prevEOL := sol - 1 prevSOL := strutil.FindLastSOL(buffer[:prevEOL]) width := wcwidth.Of(buffer[sol:dot]) return prevSOL + len(wcwidth.Trim(buffer[prevSOL:prevEOL], width)) } //elvdoc:fn move-dot-down // // Moves the dot down one line, trying to preserve the visual horizontal // position. Does nothing if dot is already on the last line of the buffer. func moveDotDown(buffer string, dot int) int { eol := strutil.FindFirstEOL(buffer[dot:]) + dot if eol == len(buffer) { // Already in the last line. return dot } nextSOL := eol + 1 nextEOL := strutil.FindFirstEOL(buffer[nextSOL:]) + nextSOL sol := strutil.FindLastSOL(buffer[:dot]) width := wcwidth.Of(buffer[sol:dot]) return nextSOL + len(wcwidth.Trim(buffer[nextSOL:nextEOL], width)) } //elvdoc:fn transpose-rune // // Swaps the runes to the left and right of the dot. If the dot is at the // beginning of the buffer, swaps the first two runes, and if the dot is at the // end, it swaps the last two. func transposeRunes(buffer string, dot int) (string, int) { if len(buffer) == 0 { return buffer, dot } var newBuffer string var newDot int // transpose at the beginning of the buffer transposes the first two // characters, and at the end the last two if dot == 0 { first, firstLen := utf8.DecodeRuneInString(buffer) if firstLen == len(buffer) { return buffer, dot } second, secondLen := utf8.DecodeRuneInString(buffer[firstLen:]) newBuffer = string(second) + string(first) + buffer[firstLen+secondLen:] newDot = firstLen + secondLen } else if dot == len(buffer) { second, secondLen := utf8.DecodeLastRuneInString(buffer) if secondLen == len(buffer) { return buffer, dot } first, firstLen := utf8.DecodeLastRuneInString(buffer[:len(buffer)-secondLen]) newBuffer = buffer[:len(buffer)-firstLen-secondLen] + string(second) + string(first) newDot = len(newBuffer) } else { first, firstLen := utf8.DecodeLastRuneInString(buffer[:dot]) second, secondLen := utf8.DecodeRuneInString(buffer[dot:]) newBuffer = buffer[:dot-firstLen] + string(second) + string(first) + buffer[dot+secondLen:] newDot = dot + secondLen } return newBuffer, newDot } //elvdoc:fn move-dot-left-word // // Moves the dot to the beginning of the last word to the left of the dot. //elvdoc:fn kill-word-left // // Deletes the the last word to the left of the dot. func moveDotLeftWord(buffer string, dot int) int { return moveDotLeftGeneralWord(categorizeWord, buffer, dot) } //elvdoc:fn move-dot-right-word // // Moves the dot to the beginning of the first word to the right of the dot. //elvdoc:fn kill-word-right // // Deletes the the first word to the right of the dot. func moveDotRightWord(buffer string, dot int) int { return moveDotRightGeneralWord(categorizeWord, buffer, dot) } //elvdoc:fn transpose-word // // Swaps the words to the left and right of the dot. If the dot is at the // beginning of the buffer, swaps the first two words, and the dot is at the // end, it swaps the last two. func transposeWord(buffer string, dot int) (string, int) { return transposeGeneralWord(categorizeWord, buffer, dot) } func categorizeWord(r rune) int { switch { case unicode.IsSpace(r): return 0 default: return 1 } } //elvdoc:fn move-dot-left-small-word // // Moves the dot to the beginning of the last small word to the left of the dot. //elvdoc:fn kill-small-word-left // // Deletes the the last small word to the left of the dot. func moveDotLeftSmallWord(buffer string, dot int) int { return moveDotLeftGeneralWord(tk.CategorizeSmallWord, buffer, dot) } //elvdoc:fn move-dot-right-small-word // // Moves the dot to the beginning of the first small word to the right of the dot. //elvdoc:fn kill-small-word-right // // Deletes the the first small word to the right of the dot. func moveDotRightSmallWord(buffer string, dot int) int { return moveDotRightGeneralWord(tk.CategorizeSmallWord, buffer, dot) } //elvdoc:fn transpose-small-word // // Swaps the small words to the left and right of the dot. If the dot is at the // beginning of the buffer, it swaps the first two small words, and if the dot // is at the end, it swaps the last two. func transposeSmallWord(buffer string, dot int) (string, int) { return transposeGeneralWord(tk.CategorizeSmallWord, buffer, dot) } //elvdoc:fn move-dot-left-alnum-word // // Moves the dot to the beginning of the last alnum word to the left of the dot. //elvdoc:fn kill-alnum-word-left // // Deletes the the last alnum word to the left of the dot. func moveDotLeftAlnumWord(buffer string, dot int) int { return moveDotLeftGeneralWord(categorizeAlnum, buffer, dot) } //elvdoc:fn move-dot-right-alnum-word // // Moves the dot to the beginning of the first alnum word to the right of the dot. //elvdoc:fn kill-alnum-word-right // // Deletes the the first alnum word to the right of the dot. func moveDotRightAlnumWord(buffer string, dot int) int { return moveDotRightGeneralWord(categorizeAlnum, buffer, dot) } //elvdoc:fn transpose-alnum-word // // Swaps the alnum words to the left and right of the dot. If the dot is at the // beginning of the buffer, it swaps the first two alnum words, and if the dot // is at the end, it swaps the last two. func transposeAlnumWord(buffer string, dot int) (string, int) { return transposeGeneralWord(categorizeAlnum, buffer, dot) } func categorizeAlnum(r rune) int { switch { case tk.IsAlnum(r): return 1 default: return 0 } } // Word movements are are more complex than one may expect. There are also // several flavors of word movements supported by Elvish. // // To understand word movements, we first need to categorize runes into several // categories: a whitespace category, plus one or more word category. The // flavors of word movements are described by their different categorization: // // * Plain word: two categories: whitespace, and non-whitespace. This flavor // corresponds to WORD in vi. // // * Small word: whitespace, alphanumeric, and everything else. This flavor // corresponds to word in vi. // // * Alphanumeric word: non-alphanumeric (all treated as whitespace) and // alphanumeric. This flavor corresponds to word in readline and zsh (when // moving left; see below for the difference in behavior when moving right). // // After fixing the flavor, a "word" is a run of runes in the same // non-whitespace category. For instance, the text "cd ~/tmp" has: // // * Two plain words: "cd" and "~/tmp". // // * Three small words: "cd", "~/" and "tmp". // // * Two alphanumeric words: "cd" and "tmp". // // To move left one word, we always move to the beginning of the last word to // the left of the dot (excluding the dot). That is: // // * If we are in the middle of a word, we will move to its beginning. // // * If we are already at the beginning of a word, we will move to the beginning // of the word before that. // // * If we are in a run of whitespaces, we will move to the beginning of the // word before the run of whitespaces. // // Moving right one word works similarly: we move to the beginning of the first // word to the right of the dot (excluding the dot). This behavior is the same // as vi and zsh, but differs from GNU readline (used by bash) and fish, which // moves the dot to one point after the end of the first word to the right of // the dot. // // See the test case for a real-world example of how the different flavors of // word movements work. // // A remark: This definition of "word movement" is general enough to include // single-rune movements as a special case, where each rune is in its own word // category (even whitespace runes). Single-rune movements are not implemented // as such though, to avoid making things unnecessarily complex. // A function that describes a word flavor by categorizing runes. The return // value of 0 represents the whitespace category while other values represent // different word categories. type categorizer func(rune) int // Move the dot left one word, using the word flavor described by the // categorizer. func moveDotLeftGeneralWord(categorize categorizer, buffer string, dot int) int { // skip trailing whitespaces left of dot pos := skipWsLeft(categorize, buffer, dot) // skip this word pos = skipSameCatLeft(categorize, buffer, pos) return pos } // Move the dot right one word, using the word flavor described by the // categorizer. func moveDotRightGeneralWord(categorize categorizer, buffer string, dot int) int { // skip leading whitespaces right of dot pos := skipWsRight(categorize, buffer, dot) if pos > dot { // Dot was within whitespaces, and we have now moved to the start of the // next word. return pos } // Dot was within a word; skip both the word and whitespaces // skip this word pos = skipSameCatRight(categorize, buffer, pos) // skip remaining whitespace pos = skipWsRight(categorize, buffer, pos) return pos } // Transposes the words around the cursor, using the word flavor described // by the categorizer. func transposeGeneralWord(categorize categorizer, buffer string, dot int) (string, int) { if strings.TrimFunc(buffer, func(r rune) bool { return categorize(r) == 0 }) == "" { // buffer contains only whitespace return buffer, dot } // after skipping whitespace, find the end of the right word pos := skipWsRight(categorize, buffer, dot) var rightEnd int if pos == len(buffer) { // there is only whitespace to the right of the dot rightEnd = skipWsLeft(categorize, buffer, pos) } else { rightEnd = skipSameCatRight(categorize, buffer, pos) } // if the dot started in the middle of a word, 'pos' is the same as dot, // so we should skip word characters to the left to find the start of the // word rightStart := skipSameCatLeft(categorize, buffer, rightEnd) leftEnd := skipWsLeft(categorize, buffer, rightStart) var leftStart int if leftEnd == 0 { // right word is the first word, use it as the left word and find a // new right word leftStart = rightStart leftEnd = rightEnd rightStart = skipWsRight(categorize, buffer, leftEnd) if rightStart == len(buffer) { // there is only one word in the buffer return buffer, dot } rightEnd = skipSameCatRight(categorize, buffer, rightStart) } else { leftStart = skipSameCatLeft(categorize, buffer, leftEnd) } return buffer[:leftStart] + buffer[rightStart:rightEnd] + buffer[leftEnd:rightStart] + buffer[leftStart:leftEnd] + buffer[rightEnd:], rightEnd } // Skips all runes to the left of the dot that belongs to the the same category. func skipSameCatLeft(categorize categorizer, buffer string, pos int) int { if pos == 0 { return pos } r, _ := utf8.DecodeLastRuneInString(buffer[:pos]) cat := categorize(r) return skipCatLeft(categorize, cat, buffer, pos) } // Skips whitespaces to the left of the dot. func skipWsLeft(categorize categorizer, buffer string, pos int) int { return skipCatLeft(categorize, 0, buffer, pos) } func skipCatLeft(categorize categorizer, cat int, buffer string, pos int) int { left := strings.TrimRightFunc(buffer[:pos], func(r rune) bool { return categorize(r) == cat }) return len(left) } // Skips all runes to the right of the dot that belongs to the the same // category. func skipSameCatRight(categorize categorizer, buffer string, pos int) int { if pos == len(buffer) { return pos } r, _ := utf8.DecodeRuneInString(buffer[pos:]) cat := categorize(r) return skipCatRight(categorize, cat, buffer, pos) } // Skips whitespaces to the right of the dot. func skipWsRight(categorize categorizer, buffer string, pos int) int { return skipCatRight(categorize, 0, buffer, pos) } func skipCatRight(categorize categorizer, cat int, buffer string, pos int) int { right := strings.TrimLeftFunc(buffer[pos:], func(r rune) bool { return categorize(r) == cat }) return len(buffer) - len(right) } elvish-0.17.0/pkg/edit/builtins.go000066400000000000000000000125231415471104000167570ustar00rootroot00000000000000package edit import ( "errors" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/modes" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/parse/parseutil" "src.elv.sh/pkg/ui" ) //elvdoc:fn binding-table // // Converts a normal map into a binding map. //elvdoc:fn -dump-buf // // Dumps the current UI buffer as HTML. This command is used to generate // "ttyshots" on the [website](https://elv.sh). // // Example: // // ```elvish // ttyshot = ~/a.html // edit:insert:binding[Ctrl-X] = { edit:-dump-buf > $tty } // ``` func dumpBuf(tty cli.TTY) string { return bufToHTML(tty.Buffer()) } //elvdoc:fn close-mode // // Closes the current active mode. func closeMode(app cli.App) { app.PopAddon() } //elvdoc:fn end-of-history // // Adds a notification saying "End of history". func endOfHistory(app cli.App) { app.Notify("End of history") } //elvdoc:fn redraw // // ```elvish // edit:redraw &full=$false // ``` // // Triggers a redraw. // // The `&full` option controls whether to do a full redraw. By default, all // redraws performed by the line editor are incremental redraws, updating only // the part of the screen that has changed from the last redraw. A full redraw // updates the entire command line. type redrawOpts struct{ Full bool } func (redrawOpts) SetDefaultOptions() {} func redraw(app cli.App, opts redrawOpts) { if opts.Full { app.RedrawFull() } else { app.Redraw() } } //elvdoc:fn clear // // ```elvish // edit:clear // ``` // // Clears the screen. // // This command should be used in place of the external `clear` command to clear // the screen. func clear(app cli.App, tty cli.TTY) { tty.HideCursor() tty.ClearScreen() app.RedrawFull() tty.ShowCursor() } //elvdoc:fn insert-raw // // Requests the next terminal input to be inserted uninterpreted. func insertRaw(app cli.App, tty cli.TTY) { codeArea, ok := focusedCodeArea(app) if !ok { return } tty.SetRawInput(1) w := modes.NewStub(modes.StubSpec{ Bindings: tk.FuncBindings(func(w tk.Widget, event term.Event) bool { switch event := event.(type) { case term.KeyEvent: codeArea.MutateState(func(s *tk.CodeAreaState) { s.Buffer.InsertAtDot(string(event.Rune)) }) app.PopAddon() return true default: return false } }), Name: " RAW ", }) app.PushAddon(w) } //elvdoc:fn key // // ```elvish // edit:key $string // ``` // // Parses a string into a key. var errMustBeKeyOrString = errors.New("must be key or string") func toKey(v interface{}) (ui.Key, error) { switch v := v.(type) { case ui.Key: return v, nil case string: return ui.ParseKey(v) default: return ui.Key{}, errMustBeKeyOrString } } //elvdoc:fn notify // // ```elvish // edit:notify $message // ``` // // Prints a notification message. // // If called while the editor is active, this will print the message above the // editor, and redraw the editor. // // If called while the editor is inactive, the message will be queued, and shown // once the editor becomes active. //elvdoc:fn return-line // // Causes the Elvish REPL to end the current read iteration and evaluate the // code it just read. If called from a key binding, takes effect after the key // binding returns. //elvdoc:fn return-eof // // Causes the Elvish REPL to terminate. If called from a key binding, takes // effect after the key binding returns. //elvdoc:fn smart-enter // // Inserts a literal newline if the current code is not syntactically complete // Elvish code. Accepts the current line otherwise. func smartEnter(app cli.App) { codeArea, ok := focusedCodeArea(app) if !ok { return } commit := false codeArea.MutateState(func(s *tk.CodeAreaState) { buf := &s.Buffer if isSyntaxComplete(buf.Content) { commit = true } else { buf.InsertAtDot("\n") } }) if commit { app.CommitCode() } } func isSyntaxComplete(code string) bool { _, err := parse.Parse(parse.Source{Code: code}, parse.Config{}) if err != nil { for _, e := range err.(*parse.Error).Entries { if e.Context.From == len(code) { return false } } } return true } //elvdoc:fn wordify // // // ```elvish // edit:wordify $code // ``` // Breaks Elvish code into words. func wordify(fm *eval.Frame, code string) error { out := fm.ValueOutput() for _, s := range parseutil.Wordify(code) { err := out.Put(s) if err != nil { return err } } return nil } func initTTYBuiltins(app cli.App, tty cli.TTY, nb eval.NsBuilder) { nb.AddGoFns(map[string]interface{}{ "-dump-buf": func() string { return dumpBuf(tty) }, "insert-raw": func() { insertRaw(app, tty) }, "clear": func() { clear(app, tty) }, }) } func initMiscBuiltins(app cli.App, nb eval.NsBuilder) { nb.AddGoFns(map[string]interface{}{ "binding-table": makeBindingMap, "close-mode": func() { closeMode(app) }, "end-of-history": func() { endOfHistory(app) }, "key": toKey, "notify": app.Notify, "redraw": func(opts redrawOpts) { redraw(app, opts) }, "return-line": app.CommitCode, "return-eof": app.CommitEOF, "smart-enter": func() { smartEnter(app) }, "wordify": wordify, }) } // Like mode.FocusedCodeArea, but handles the error by writing a notification. func focusedCodeArea(app cli.App) (tk.CodeArea, bool) { codeArea, err := modes.FocusedCodeArea(app) if err != nil { app.Notify(err.Error()) return nil, false } return codeArea, true } elvish-0.17.0/pkg/edit/builtins_test.go000066400000000000000000000362601415471104000200220ustar00rootroot00000000000000package edit import ( "io" "strings" "testing" "src.elv.sh/pkg/cli/modes" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/eval" . "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/tt" "src.elv.sh/pkg/ui" ) func TestBindingTable(t *testing.T) { f := setup(t) evals(f.Evaler, `called = $false`) evals(f.Evaler, `m = (edit:binding-table [&a={ called = $true }])`) _, ok := getGlobal(f.Evaler, "m").(bindingsMap) if !ok { t.Errorf("edit:binding-table did not create BindingMap variable") } } func TestCloseMode(t *testing.T) { f := setup(t) f.Editor.app.PushAddon(tk.Empty{}) evals(f.Evaler, `edit:close-mode`) if addons := f.Editor.app.CopyState().Addons; len(addons) > 0 { t.Errorf("got addons %v, want nil or empty slice", addons) } } func TestDumpBuf(t *testing.T) { f := setup(t) feedInput(f.TTYCtrl, "echo") // Wait until the buffer we want has shown up. f.TestTTY(t, "~> echo", Styles, " vvvv", term.DotHere, ) evals(f.Evaler, `html = (edit:-dump-buf)`) testGlobal(t, f.Evaler, "html", `~> echo`+"\n") } func TestInsertRaw(t *testing.T) { f := setup(t) f.TTYCtrl.Inject(term.K('V', ui.Ctrl)) wantBuf := f.MakeBuffer( "~> ", term.DotHere, "\n", " RAW ", Styles, "*****", ) f.TTYCtrl.TestBuffer(t, wantBuf) // Since we do not use real terminals in the test, we cannot have a // realistic test case against actual raw inputs. However, we can still // check that the builtin command does call the SetRawInput method with 1. if raw := f.TTYCtrl.RawInput(); raw != 1 { t.Errorf("RawInput() -> %d, want 1", raw) } // Raw mode does not respond to non-key events. f.TTYCtrl.Inject(term.MouseEvent{}) f.TTYCtrl.TestBuffer(t, wantBuf) // Raw mode is dismissed after a single key event. f.TTYCtrl.Inject(term.K('+')) f.TestTTY(t, "~> +", Styles, " v", term.DotHere, ) } func TestEndOfHistory(t *testing.T) { f := setup(t) evals(f.Evaler, `edit:end-of-history`) f.TestTTYNotes(t, "End of history") } func TestKey(t *testing.T) { f := setup(t) evals(f.Evaler, `k = (edit:key a)`) wantK := ui.K('a') if k, _ := f.Evaler.Global().Index("k"); k != wantK { t.Errorf("$k is %v, want %v", k, wantK) } } func TestRedraw(t *testing.T) { f := setup(t) evals(f.Evaler, `edit:current-command = echo`, `edit:redraw`) f.TestTTY(t, "~> echo", Styles, " vvvv", term.DotHere) evals(f.Evaler, `edit:redraw &full=$true`) // TODO(xiaq): Test that this is actually a full redraw. f.TestTTY(t, "~> echo", Styles, " vvvv", term.DotHere) } func TestClear(t *testing.T) { f := setup(t) evals(f.Evaler, `edit:current-command = echo`, `edit:clear`) f.TestTTY(t, "~> echo", Styles, " vvvv", term.DotHere) if cleared := f.TTYCtrl.ScreenCleared(); cleared != 1 { t.Errorf("screen cleared %v times, want 1", cleared) } } func TestReturnCode(t *testing.T) { f := setup(t) codeArea(f.Editor.app).MutateState(func(s *tk.CodeAreaState) { s.Buffer.Content = "test code" }) evals(f.Evaler, `edit:return-line`) code, err := f.Wait() if code != "test code" { t.Errorf("got code %q, want %q", code, "test code") } if err != nil { t.Errorf("got err %v, want nil", err) } } func TestReturnEOF(t *testing.T) { f := setup(t) evals(f.Evaler, `edit:return-eof`) if _, err := f.Wait(); err != io.EOF { t.Errorf("got err %v, want %v", err, io.EOF) } } func TestSmartEnter_InsertsNewlineWhenIncomplete(t *testing.T) { f := setup(t) f.SetCodeBuffer(tk.CodeBuffer{Content: "put [", Dot: 5}) evals(f.Evaler, `edit:smart-enter`) wantBuf := tk.CodeBuffer{Content: "put [\n", Dot: 6} if buf := codeArea(f.Editor.app).CopyState().Buffer; buf != wantBuf { t.Errorf("got code buffer %v, want %v", buf, wantBuf) } } func TestSmartEnter_AcceptsCodeWhenWholeBufferIsComplete(t *testing.T) { f := setup(t) f.SetCodeBuffer(tk.CodeBuffer{Content: "put []", Dot: 5}) evals(f.Evaler, `edit:smart-enter`) wantCode := "put []" if code, _ := f.Wait(); code != wantCode { t.Errorf("got return code %q, want %q", code, wantCode) } } func TestWordify(t *testing.T) { TestWithSetup(t, setupWordify, That("wordify 'ls str [list]'").Puts("ls", "str", "[list]"), That("wordify foo >&-").Throws(AnyError), ) } func setupWordify(ev *eval.Evaler) { ev.ExtendBuiltin(eval.BuildNs().AddGoFn("wordify", wordify)) } var bufferBuiltinsTests = []struct { name string bufBefore tk.CodeBuffer bufAfter tk.CodeBuffer }{ { "move-dot-left", tk.CodeBuffer{Content: "ab", Dot: 1}, tk.CodeBuffer{Content: "ab", Dot: 0}, }, { "move-dot-right", tk.CodeBuffer{Content: "ab", Dot: 1}, tk.CodeBuffer{Content: "ab", Dot: 2}, }, { "kill-rune-left", tk.CodeBuffer{Content: "ab", Dot: 1}, tk.CodeBuffer{Content: "b", Dot: 0}, }, { "kill-rune-right", tk.CodeBuffer{Content: "ab", Dot: 1}, tk.CodeBuffer{Content: "a", Dot: 1}, }, { "transpose-rune with empty buffer", tk.CodeBuffer{Content: "", Dot: 0}, tk.CodeBuffer{Content: "", Dot: 0}, }, { "transpose-rune with dot at beginning", tk.CodeBuffer{Content: "abc", Dot: 0}, tk.CodeBuffer{Content: "bac", Dot: 2}, }, { "transpose-rune with dot in middle", tk.CodeBuffer{Content: "abc", Dot: 1}, tk.CodeBuffer{Content: "bac", Dot: 2}, }, { "transpose-rune with dot at end", tk.CodeBuffer{Content: "abc", Dot: 3}, tk.CodeBuffer{Content: "acb", Dot: 3}, }, { "transpose-rune with one character and dot at end", tk.CodeBuffer{Content: "a", Dot: 1}, tk.CodeBuffer{Content: "a", Dot: 1}, }, { "transpose-rune with one character and dot at beginning", tk.CodeBuffer{Content: "a", Dot: 0}, tk.CodeBuffer{Content: "a", Dot: 0}, }, { "transpose-word with dot at beginning", tk.CodeBuffer{Content: "ab bc cd", Dot: 0}, tk.CodeBuffer{Content: "bc ab cd", Dot: 6}, }, { "transpose-word with dot in between words", tk.CodeBuffer{Content: "ab bc cd", Dot: 6}, tk.CodeBuffer{Content: "ab cd bc", Dot: 9}, }, { "transpose-word with dot at end", tk.CodeBuffer{Content: "ab bc cd", Dot: 9}, tk.CodeBuffer{Content: "ab cd bc", Dot: 9}, }, { "transpose-word with dot in the middle of a word", tk.CodeBuffer{Content: "ab bc cd", Dot: 5}, tk.CodeBuffer{Content: "bc ab cd", Dot: 6}, }, { "transpose-word with one word", tk.CodeBuffer{Content: " ab ", Dot: 4}, tk.CodeBuffer{Content: " ab ", Dot: 4}, }, { "transpose-word with no words", tk.CodeBuffer{Content: " \t\n ", Dot: 4}, tk.CodeBuffer{Content: " \t\n ", Dot: 4}, }, { "transpose-word with complex input", tk.CodeBuffer{Content: "cd ~/downloads;", Dot: 4}, tk.CodeBuffer{Content: "~/downloads; cd", Dot: 15}, }, { "transpose-small-word", tk.CodeBuffer{Content: "cd ~/downloads;", Dot: 4}, tk.CodeBuffer{Content: "~/ cddownloads;", Dot: 5}, }, { "transpose-alnum-word", tk.CodeBuffer{Content: "cd ~/downloads;", Dot: 4}, tk.CodeBuffer{Content: "downloads ~/cd;", Dot: 14}, }, } func TestBufferBuiltins(t *testing.T) { f := setup(t) app := f.Editor.app for _, test := range bufferBuiltinsTests { t.Run(test.name, func(t *testing.T) { codeArea(app).MutateState(func(s *tk.CodeAreaState) { s.Buffer = test.bufBefore }) cmd := strings.Split(test.name, " ")[0] evals(f.Evaler, "edit:"+cmd) if buf := codeArea(app).CopyState().Buffer; buf != test.bufAfter { t.Errorf("got buf %v, want %v", buf, test.bufAfter) } }) } } // Builtins that expect the focused widget to be code areas. This // includes some builtins defined in files other than builtins.go. var focusedWidgetNotCodeAreaTests = []string{ "edit:insert-raw", "edit:smart-enter", "edit:move-dot-right", // other buffer builtins not tested "edit:completion:start", "edit:history:start", } func TestBuiltins_FocusedWidgetNotCodeArea(t *testing.T) { for _, code := range focusedWidgetNotCodeAreaTests { t.Run(code, func(t *testing.T) { f := setup(t) f.Editor.app.PushAddon(tk.Label{}) evals(f.Evaler, code) f.TestTTYNotes(t, modes.ErrFocusedWidgetNotCodeArea.Error()) }) } } // Tests for pure movers. func TestMoveDotLeftRight(t *testing.T) { tt.Test(t, tt.Fn("moveDotLeft", moveDotLeft), tt.Table{ tt.Args("foo", 0).Rets(0), tt.Args("bar", 3).Rets(2), tt.Args("精灵", 0).Rets(0), tt.Args("精灵", 3).Rets(0), tt.Args("精灵", 6).Rets(3), }) tt.Test(t, tt.Fn("moveDotRight", moveDotRight), tt.Table{ tt.Args("foo", 0).Rets(1), tt.Args("bar", 3).Rets(3), tt.Args("精灵", 0).Rets(3), tt.Args("精灵", 3).Rets(6), tt.Args("精灵", 6).Rets(6), }) } func TestMoveDotSOLEOL(t *testing.T) { buffer := "abc\ndef" // Index: // 012 34567 tt.Test(t, tt.Fn("moveDotSOL", moveDotSOL), tt.Table{ tt.Args(buffer, 0).Rets(0), tt.Args(buffer, 1).Rets(0), tt.Args(buffer, 2).Rets(0), tt.Args(buffer, 3).Rets(0), tt.Args(buffer, 4).Rets(4), tt.Args(buffer, 5).Rets(4), tt.Args(buffer, 6).Rets(4), tt.Args(buffer, 7).Rets(4), }) tt.Test(t, tt.Fn("moveDotEOL", moveDotEOL), tt.Table{ tt.Args(buffer, 0).Rets(3), tt.Args(buffer, 1).Rets(3), tt.Args(buffer, 2).Rets(3), tt.Args(buffer, 3).Rets(3), tt.Args(buffer, 4).Rets(7), tt.Args(buffer, 5).Rets(7), tt.Args(buffer, 6).Rets(7), tt.Args(buffer, 7).Rets(7), }) } func TestMoveDotUpDown(t *testing.T) { buffer := "abc\n精灵语\ndef" // Index: // 012 34 7 0 34567 // + 10 * 0 1 tt.Test(t, tt.Fn("moveDotUp", moveDotUp), tt.Table{ tt.Args(buffer, 0).Rets(0), // a -> a tt.Args(buffer, 1).Rets(1), // b -> b tt.Args(buffer, 2).Rets(2), // c -> c tt.Args(buffer, 3).Rets(3), // EOL1 -> EOL1 tt.Args(buffer, 4).Rets(0), // 精 -> a tt.Args(buffer, 7).Rets(2), // 灵 -> c tt.Args(buffer, 10).Rets(3), // 语 -> EOL1 tt.Args(buffer, 13).Rets(3), // EOL2 -> EOL1 tt.Args(buffer, 14).Rets(4), // d -> 精 tt.Args(buffer, 15).Rets(4), // e -> 精 (jump left half width) tt.Args(buffer, 16).Rets(7), // f -> 灵 tt.Args(buffer, 17).Rets(7), // EOL3 -> 灵 (jump left half width) }) tt.Test(t, tt.Fn("moveDotDown", moveDotDown), tt.Table{ tt.Args(buffer, 0).Rets(4), // a -> 精 tt.Args(buffer, 1).Rets(4), // b -> 精 (jump left half width) tt.Args(buffer, 2).Rets(7), // c -> 灵 tt.Args(buffer, 3).Rets(7), // EOL1 -> 灵 (jump left half width) tt.Args(buffer, 4).Rets(14), // 精 -> d tt.Args(buffer, 7).Rets(16), // 灵 -> f tt.Args(buffer, 10).Rets(17), // 语 -> EOL3 tt.Args(buffer, 13).Rets(17), // EOL2 -> EOL3 tt.Args(buffer, 14).Rets(14), // d -> d tt.Args(buffer, 15).Rets(15), // e -> e tt.Args(buffer, 16).Rets(16), // f -> f tt.Args(buffer, 17).Rets(17), // EOL3 -> EOL3 }) } // Word movement tests. // The string below is carefully chosen to test all word, small-word, and // alnum-word move/kill functions, because it contains features to set the // different movement behaviors apart. // // The string is annotated with carets (^) to indicate the beginning of words, // and periods (.) to indicate trailing runes of words. Indices are also // annotated. // // cd ~/downloads; rm -rf 2018aug07-pics/*; // ^. ^........... ^. ^.. ^................ (word) // ^. ^.^........^ ^. ^^. ^........^^...^.. (small-word) // ^. ^........ ^. ^. ^........ ^... (alnum-word) // 01234567890123456789012345678901234567890 // 0 1 2 3 4 // // word boundaries: 0 3 16 19 23 // small-word boundaries: 0 3 5 14 16 19 20 23 32 33 37 // alnum-word boundaries: 0 5 16 20 23 33 // var wordMoveTestBuffer = "cd ~/downloads; rm -rf 2018aug07-pics/*;" var ( // word boundaries: 0 3 16 19 23 moveDotLeftWordTests = tt.Table{ tt.Args(wordMoveTestBuffer, 0).Rets(0), tt.Args(wordMoveTestBuffer, 1).Rets(0), tt.Args(wordMoveTestBuffer, 2).Rets(0), tt.Args(wordMoveTestBuffer, 3).Rets(0), tt.Args(wordMoveTestBuffer, 4).Rets(3), tt.Args(wordMoveTestBuffer, 16).Rets(3), tt.Args(wordMoveTestBuffer, 19).Rets(16), tt.Args(wordMoveTestBuffer, 23).Rets(19), tt.Args(wordMoveTestBuffer, 40).Rets(23), } moveDotRightWordTests = tt.Table{ tt.Args(wordMoveTestBuffer, 0).Rets(3), tt.Args(wordMoveTestBuffer, 1).Rets(3), tt.Args(wordMoveTestBuffer, 2).Rets(3), tt.Args(wordMoveTestBuffer, 3).Rets(16), tt.Args(wordMoveTestBuffer, 16).Rets(19), tt.Args(wordMoveTestBuffer, 19).Rets(23), tt.Args(wordMoveTestBuffer, 23).Rets(40), } // small-word boundaries: 0 3 5 14 16 19 20 23 32 33 37 moveDotLeftSmallWordTests = tt.Table{ tt.Args(wordMoveTestBuffer, 0).Rets(0), tt.Args(wordMoveTestBuffer, 1).Rets(0), tt.Args(wordMoveTestBuffer, 2).Rets(0), tt.Args(wordMoveTestBuffer, 3).Rets(0), tt.Args(wordMoveTestBuffer, 4).Rets(3), tt.Args(wordMoveTestBuffer, 5).Rets(3), tt.Args(wordMoveTestBuffer, 14).Rets(5), tt.Args(wordMoveTestBuffer, 16).Rets(14), tt.Args(wordMoveTestBuffer, 19).Rets(16), tt.Args(wordMoveTestBuffer, 20).Rets(19), tt.Args(wordMoveTestBuffer, 23).Rets(20), tt.Args(wordMoveTestBuffer, 32).Rets(23), tt.Args(wordMoveTestBuffer, 33).Rets(32), tt.Args(wordMoveTestBuffer, 37).Rets(33), tt.Args(wordMoveTestBuffer, 40).Rets(37), } moveDotRightSmallWordTests = tt.Table{ tt.Args(wordMoveTestBuffer, 0).Rets(3), tt.Args(wordMoveTestBuffer, 1).Rets(3), tt.Args(wordMoveTestBuffer, 2).Rets(3), tt.Args(wordMoveTestBuffer, 3).Rets(5), tt.Args(wordMoveTestBuffer, 5).Rets(14), tt.Args(wordMoveTestBuffer, 14).Rets(16), tt.Args(wordMoveTestBuffer, 16).Rets(19), tt.Args(wordMoveTestBuffer, 19).Rets(20), tt.Args(wordMoveTestBuffer, 20).Rets(23), tt.Args(wordMoveTestBuffer, 23).Rets(32), tt.Args(wordMoveTestBuffer, 32).Rets(33), tt.Args(wordMoveTestBuffer, 33).Rets(37), tt.Args(wordMoveTestBuffer, 37).Rets(40), } // alnum-word boundaries: 0 5 16 20 23 33 moveDotLeftAlnumWordTests = tt.Table{ tt.Args(wordMoveTestBuffer, 0).Rets(0), tt.Args(wordMoveTestBuffer, 1).Rets(0), tt.Args(wordMoveTestBuffer, 2).Rets(0), tt.Args(wordMoveTestBuffer, 3).Rets(0), tt.Args(wordMoveTestBuffer, 4).Rets(0), tt.Args(wordMoveTestBuffer, 5).Rets(0), tt.Args(wordMoveTestBuffer, 6).Rets(5), tt.Args(wordMoveTestBuffer, 16).Rets(5), tt.Args(wordMoveTestBuffer, 20).Rets(16), tt.Args(wordMoveTestBuffer, 23).Rets(20), tt.Args(wordMoveTestBuffer, 33).Rets(23), tt.Args(wordMoveTestBuffer, 40).Rets(33), } moveDotRightAlnumWordTests = tt.Table{ tt.Args(wordMoveTestBuffer, 0).Rets(5), tt.Args(wordMoveTestBuffer, 1).Rets(5), tt.Args(wordMoveTestBuffer, 2).Rets(5), tt.Args(wordMoveTestBuffer, 3).Rets(5), tt.Args(wordMoveTestBuffer, 4).Rets(5), tt.Args(wordMoveTestBuffer, 5).Rets(16), tt.Args(wordMoveTestBuffer, 16).Rets(20), tt.Args(wordMoveTestBuffer, 20).Rets(23), tt.Args(wordMoveTestBuffer, 23).Rets(33), tt.Args(wordMoveTestBuffer, 33).Rets(40), } ) func TestMoveDotWord(t *testing.T) { tt.Test(t, tt.Fn("moveDotLeftWord", moveDotLeftWord), moveDotLeftWordTests) tt.Test(t, tt.Fn("moveDotRightWord", moveDotRightWord), moveDotRightWordTests) } func TestMoveDotSmallWord(t *testing.T) { tt.Test(t, tt.Fn("moveDotLeftSmallWord", moveDotLeftSmallWord), moveDotLeftSmallWordTests, ) tt.Test(t, tt.Fn("moveDotRightSmallWord", moveDotRightSmallWord), moveDotRightSmallWordTests, ) } func TestMoveDotAlnumWord(t *testing.T) { tt.Test(t, tt.Fn("moveDotLeftAlnumWord", moveDotLeftAlnumWord), moveDotLeftAlnumWordTests, ) tt.Test(t, tt.Fn("moveDotRightAlnumWord", moveDotRightAlnumWord), moveDotRightAlnumWordTests, ) } elvish-0.17.0/pkg/edit/command_api.go000066400000000000000000000016371415471104000174010ustar00rootroot00000000000000package edit // Implementation of the editor "command" mode. import ( "src.elv.sh/pkg/cli/modes" "src.elv.sh/pkg/eval" ) //elvdoc:var command:binding // // Key bindings for command mode. This is currently a very small subset of Vi // command mode bindings. // // @cf edit:command:start //elvdoc:fn command:start // // Enter command mode. This mode is intended to emulate Vi's command mode, but // it is very incomplete right now. // // @cf edit:command:binding func initCommandAPI(ed *Editor, ev *eval.Evaler, nb eval.NsBuilder) { bindingVar := newBindingVar(emptyBindingsMap) bindings := newMapBindings(ed, ev, bindingVar) nb.AddNs("command", eval.BuildNsNamed("edit:command"). AddVar("binding", bindingVar). AddGoFns(map[string]interface{}{ "start": func() { w := modes.NewStub(modes.StubSpec{ Bindings: bindings, Name: " COMMAND ", }) ed.app.PushAddon(w) }, })) } elvish-0.17.0/pkg/edit/command_api_test.go000066400000000000000000000010251415471104000204270ustar00rootroot00000000000000package edit import ( "testing" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/ui" ) func TestCommandMode(t *testing.T) { f := setup(t) evals(f.Evaler, `edit:insert:binding[Ctrl-'['] = $edit:command:start~`) feedInput(f.TTYCtrl, "echo") f.TTYCtrl.Inject(term.K('[', ui.Ctrl)) f.TestTTY(t, "~> echo", Styles, " vvvv", term.DotHere, "\n", " COMMAND ", Styles, "*********", ) f.TTYCtrl.Inject(term.K('b')) f.TestTTY(t, "~> ", term.DotHere, "echo\n", Styles, "vvvv", " COMMAND ", Styles, "*********", ) } elvish-0.17.0/pkg/edit/complete/000077500000000000000000000000001415471104000164045ustar00rootroot00000000000000elvish-0.17.0/pkg/edit/complete/complete.go000066400000000000000000000071501415471104000205460ustar00rootroot00000000000000// Package complete implements the code completion algorithm for Elvish. package complete import ( "errors" "sort" "src.elv.sh/pkg/cli/modes" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/parse/parseutil" ) type item = modes.CompletionItem // An error returned by Complete if the config has not supplied a PureEvaler. var errNoPureEvaler = errors.New("no PureEvaler supplied") // An error returned by Complete as well as the completers if there is no // applicable completion. var errNoCompletion = errors.New("no completion") // Config stores the configuration required for code completion. type Config struct { // An interface to access the runtime. Complete will return an error if this // is nil. PureEvaler PureEvaler // A function for filtering raw candidates. If nil, no filtering is done. Filterer Filterer // Used to generate candidates for a command argument. Defaults to // Filenames. ArgGenerator ArgGenerator } // Filterer is the type of functions that filter raw candidates. type Filterer func(ctxName, seed string, rawItems []RawItem) []RawItem // ArgGenerator is the type of functions that generate raw candidates for a // command argument. It takes all the existing arguments, the last being the // argument to complete, and returns raw candidates or an error. type ArgGenerator func(args []string) ([]RawItem, error) // Result keeps the result of the completion algorithm. type Result struct { Name string Replace diag.Ranging Items []modes.CompletionItem } // RawItem represents completion items before the quoting pass. type RawItem interface { String() string Cook(parse.PrimaryType) modes.CompletionItem } // PureEvaler encapsulates the functionality the completion algorithm needs from // the language runtime. type PureEvaler interface { EachExternal(func(cmd string)) EachSpecial(func(special string)) EachNs(func(string)) EachVariableInNs(string, func(string)) PurelyEvalPrimary(pn *parse.Primary) interface{} PurelyEvalCompound(*parse.Compound) (string, bool) PurelyEvalPartialCompound(*parse.Compound, int) (string, bool) } // CodeBuffer is the same the type in src.elv.sh/pkg/el/codearea, // replicated here to avoid an unnecessary dependency. type CodeBuffer struct { Content string Dot int } // Complete runs the code completion algorithm in the given context, and returns // the completion type, items and any error encountered. func Complete(code CodeBuffer, cfg Config) (*Result, error) { if cfg.PureEvaler == nil { return nil, errNoPureEvaler } if cfg.Filterer == nil { cfg.Filterer = FilterPrefix } if cfg.ArgGenerator == nil { cfg.ArgGenerator = GenerateFileNames } // Ignore the error; the function always returns a valid *ChunkNode. tree, _ := parse.Parse(parse.Source{Name: "[interactive]", Code: code.Content}, parse.Config{}) leaf := parseutil.FindLeafNode(tree.Root, code.Dot) for _, completer := range completers { ctx, rawItems, err := completer(leaf, cfg) if err == errNoCompletion { continue } rawItems = cfg.Filterer(ctx.name, ctx.seed, rawItems) items := make([]modes.CompletionItem, len(rawItems)) for i, rawCand := range rawItems { items[i] = rawCand.Cook(ctx.quote) } sort.Slice(items, func(i, j int) bool { return items[i].ToShow < items[j].ToShow }) items = dedup(items) return &Result{Name: ctx.name, Items: items, Replace: ctx.interval}, nil } return nil, errNoCompletion } func dedup(items []modes.CompletionItem) []modes.CompletionItem { var result []modes.CompletionItem for i, item := range items { if i == 0 || item.ToInsert != items[i-1].ToInsert { result = append(result, item) } } return result } elvish-0.17.0/pkg/edit/complete/complete_test.go000066400000000000000000000173521415471104000216120ustar00rootroot00000000000000package complete import ( "fmt" "os" "runtime" "testing" "src.elv.sh/pkg/cli/lscolors" "src.elv.sh/pkg/cli/modes" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/testutil" "src.elv.sh/pkg/tt" "src.elv.sh/pkg/ui" ) var Args = tt.Args // An implementation of PureEvaler useful in tests. type testEvaler struct { externals []string specials []string namespaces []string variables map[string][]string } func feed(f func(string), ss []string) { for _, s := range ss { f(s) } } func (ev testEvaler) EachExternal(f func(string)) { feed(f, ev.externals) } func (ev testEvaler) EachSpecial(f func(string)) { feed(f, ev.specials) } func (ev testEvaler) EachNs(f func(string)) { feed(f, ev.namespaces) } func (ev testEvaler) EachVariableInNs(ns string, f func(string)) { feed(f, ev.variables[ns]) } func (ev testEvaler) PurelyEvalPartialCompound(cn *parse.Compound, upto int) (string, bool) { return (*eval.Evaler)(nil).PurelyEvalPartialCompound(cn, upto) } func (ev testEvaler) PurelyEvalCompound(cn *parse.Compound) (string, bool) { return (*eval.Evaler)(nil).PurelyEvalCompound(cn) } func (ev testEvaler) PurelyEvalPrimary(pn *parse.Primary) interface{} { return (*eval.Evaler)(nil).PurelyEvalPrimary(pn) } func TestComplete(t *testing.T) { lscolors.SetTestLsColors(t) testutil.InTempDir(t) testutil.ApplyDir(testutil.Dir{ "a.exe": testutil.File{Perm: 0755, Content: ""}, "non-exe": "", "d": testutil.Dir{ "a.exe": testutil.File{Perm: 0755, Content: ""}, }, }) var cfg Config cfg = Config{ Filterer: FilterPrefix, PureEvaler: testEvaler{ externals: []string{"ls", "make"}, specials: []string{"if", "for"}, variables: map[string][]string{ "": {"foo", "bar", "fn~", "ns:"}, "ns1:": {"lorem"}, "ns2:": {"ipsum"}, }, namespaces: []string{"ns1:", "ns2:"}, }, ArgGenerator: func(args []string) ([]RawItem, error) { if len(args) >= 2 && args[0] == "sudo" { return GenerateForSudo(cfg, args) } return GenerateFileNames(args) }, } argGeneratorDebugCfg := Config{ PureEvaler: cfg.PureEvaler, Filterer: func(ctxName, seed string, items []RawItem) []RawItem { return items }, ArgGenerator: func(args []string) ([]RawItem, error) { item := noQuoteItem(fmt.Sprintf("%#v", args)) return []RawItem{item}, nil }, } dupCfg := Config{ PureEvaler: cfg.PureEvaler, ArgGenerator: func([]string) ([]RawItem, error) { return []RawItem{PlainItem("a"), PlainItem("b"), PlainItem("a")}, nil }, } allFileNameItems := []modes.CompletionItem{ fc("a.exe", " "), fc("d"+string(os.PathSeparator), ""), fc("non-exe", " "), } allCommandItems := []modes.CompletionItem{ c("bar = "), c("fn"), c("foo = "), c("for"), c("if"), c("ls"), c("make"), c("ns:"), } tt.Test(t, tt.Fn("Complete", Complete), tt.Table{ // No PureEvaler. Args(cb(""), Config{}).Rets( (*Result)(nil), errNoPureEvaler), // Candidates are deduplicated. Args(cb("ls "), dupCfg).Rets( &Result{ Name: "argument", Replace: r(3, 3), Items: []modes.CompletionItem{ c("a"), c("b"), }, }, nil), // Complete arguments using GenerateFileNames. Args(cb("ls "), cfg).Rets( &Result{ Name: "argument", Replace: r(3, 3), Items: allFileNameItems}, nil), Args(cb("ls a"), cfg).Rets( &Result{ Name: "argument", Replace: r(3, 4), Items: []modes.CompletionItem{fc("a.exe", " ")}}, nil), // GenerateForSudo completing external commands. Args(cb("sudo "), cfg).Rets( &Result{ Name: "argument", Replace: r(5, 5), Items: []modes.CompletionItem{c("ls"), c("make")}}, nil), // GenerateForSudo completing non-command arguments. Args(cb("sudo ls "), cfg).Rets( &Result{ Name: "argument", Replace: r(8, 8), Items: allFileNameItems}, nil), // Custom arg completer, new argument Args(cb("ls a "), argGeneratorDebugCfg).Rets( &Result{ Name: "argument", Replace: r(5, 5), Items: []modes.CompletionItem{c(`[]string{"ls", "a", ""}`)}}, nil), Args(cb("ls a b"), argGeneratorDebugCfg).Rets( &Result{ Name: "argument", Replace: r(5, 6), Items: []modes.CompletionItem{c(`[]string{"ls", "a", "b"}`)}}, nil), // Complete commands at an empty buffer, generating special forms, // externals, functions, namespaces and variable assignments. Args(cb(""), cfg).Rets( &Result{Name: "command", Replace: r(0, 0), Items: allCommandItems}, nil), // Complete at an empty closure. Args(cb("{ "), cfg).Rets( &Result{Name: "command", Replace: r(2, 2), Items: allCommandItems}, nil), // Complete after a newline. Args(cb("a\n"), cfg).Rets( &Result{Name: "command", Replace: r(2, 2), Items: allCommandItems}, nil), // Complete after a semicolon. Args(cb("a;"), cfg).Rets( &Result{Name: "command", Replace: r(2, 2), Items: allCommandItems}, nil), // Complete after a pipe. Args(cb("a|"), cfg).Rets( &Result{Name: "command", Replace: r(2, 2), Items: allCommandItems}, nil), // Complete at the beginning of output capture. Args(cb("a ("), cfg).Rets( &Result{Name: "command", Replace: r(3, 3), Items: allCommandItems}, nil), // Complete at the beginning of exception capture. Args(cb("a ?("), cfg).Rets( &Result{Name: "command", Replace: r(4, 4), Items: allCommandItems}, nil), // Complete external commands with the e: prefix. Args(cb("e:"), cfg).Rets( &Result{ Name: "command", Replace: r(0, 2), Items: []modes.CompletionItem{c("e:ls"), c("e:make")}}, nil), // TODO(xiaq): Add tests for completing indices. // Complete filenames for redirection. Args(cb("p >"), cfg).Rets( &Result{Name: "redir", Replace: r(3, 3), Items: allFileNameItems}, nil), Args(cb("p > a"), cfg).Rets( &Result{ Name: "redir", Replace: r(4, 5), Items: []modes.CompletionItem{fc("a.exe", " ")}}, nil), // Completing variables. Args(cb("p $"), cfg).Rets( &Result{ Name: "variable", Replace: r(3, 3), Items: []modes.CompletionItem{ c("bar"), c("fn~"), c("foo"), c("ns1:"), c("ns2:"), c("ns:")}}, nil), Args(cb("p $f"), cfg).Rets( &Result{ Name: "variable", Replace: r(3, 4), Items: []modes.CompletionItem{c("fn~"), c("foo")}}, nil), // 0123456 Args(cb("p $ns1:"), cfg).Rets( &Result{ Name: "variable", Replace: r(7, 7), Items: []modes.CompletionItem{c("lorem")}}, nil), }) // Symlinks and executable bits are not available on Windows. if goos := runtime.GOOS; goos != "windows" { err := os.Symlink("d", "d2") if err != nil { panic(err) } allLocalCommandItems := []modes.CompletionItem{ fc("./a.exe", " "), fc("./d/", ""), fc("./d2/", ""), } tt.Test(t, tt.Fn("Complete", Complete), tt.Table{ // Filename completion treats symlink to directories as directories. // 01234 Args(cb("p > d"), cfg).Rets( &Result{ Name: "redir", Replace: r(4, 5), Items: []modes.CompletionItem{fc("d/", ""), fc("d2/", "")}}, nil, ), // Complete local external commands. // // TODO(xiaq): Make this test applicable to Windows by using a // different criteria for executable files on Window. Args(cb("./"), cfg).Rets( &Result{ Name: "command", Replace: r(0, 2), Items: allLocalCommandItems}, nil), // After sudo. Args(cb("sudo ./"), cfg).Rets( &Result{ Name: "argument", Replace: r(5, 7), Items: allLocalCommandItems}, nil), }) } } func cb(s string) CodeBuffer { return CodeBuffer{s, len(s)} } func c(s string) modes.CompletionItem { return modes.CompletionItem{ToShow: s, ToInsert: s} } func fc(s, suffix string) modes.CompletionItem { return modes.CompletionItem{ToShow: s, ToInsert: parse.Quote(s) + suffix, ShowStyle: ui.StyleFromSGR(lscolors.GetColorist().GetStyle(s))} } func r(i, j int) diag.Ranging { return diag.Ranging{From: i, To: j} } elvish-0.17.0/pkg/edit/complete/completers.go000066400000000000000000000145521415471104000211170ustar00rootroot00000000000000package complete import ( "strings" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/parse" ) var parent = parse.Parent var completers = []completer{ completeCommand, completeIndex, completeRedir, completeVariable, completeArg, } type completer func(parse.Node, Config) (*context, []RawItem, error) type context struct { name string seed string quote parse.PrimaryType interval diag.Ranging } func completeArg(n parse.Node, cfg Config) (*context, []RawItem, error) { ev := cfg.PureEvaler if sep, ok := n.(*parse.Sep); ok { if form, ok := parent(sep).(*parse.Form); ok && form.Head != nil { // Case 1: starting a new argument. ctx := &context{"argument", "", parse.Bareword, range0(n.Range().To)} args := purelyEvalForm(form, "", n.Range().To, ev) items, err := cfg.ArgGenerator(args) return ctx, items, err } } if primary, ok := n.(*parse.Primary); ok { if compound, seed := primaryInSimpleCompound(primary, ev); compound != nil { if form, ok := parent(compound).(*parse.Form); ok { if form.Head != nil && form.Head != compound { // Case 2: in an incomplete argument. ctx := &context{"argument", seed, primary.Type, compound.Range()} args := purelyEvalForm(form, seed, compound.Range().From, ev) items, err := cfg.ArgGenerator(args) return ctx, items, err } } } } return nil, nil, errNoCompletion } func completeCommand(n parse.Node, cfg Config) (*context, []RawItem, error) { ev := cfg.PureEvaler generateForEmpty := func(pos int) (*context, []RawItem, error) { ctx := &context{"command", "", parse.Bareword, range0(pos)} items, err := generateCommands("", ev) return ctx, items, err } if is(n, aChunk) { // Case 1: The leaf is a Chunk. That means that the chunk is empty // (nothing entered at all) and it is a correct place for completing a // command. return generateForEmpty(n.Range().To) } if is(n, aSep) { parent := parent(n) switch { case is(parent, aChunk), is(parent, aPipeline): // Case 2: Just after a newline, semicolon, or a pipe. return generateForEmpty(n.Range().To) case is(parent, aPrimary): ptype := parent.(*parse.Primary).Type if ptype == parse.OutputCapture || ptype == parse.ExceptionCapture || ptype == parse.Lambda { // Case 3: At the beginning of output, exception capture or lambda. // // TODO: Don't trigger after "{|". return generateForEmpty(n.Range().To) } } } if primary, ok := n.(*parse.Primary); ok { if compound, seed := primaryInSimpleCompound(primary, ev); compound != nil { if form, ok := parent(compound).(*parse.Form); ok { if form.Head == compound { // Case 4: At an already started command. ctx := &context{ "command", seed, primary.Type, compound.Range()} items, err := generateCommands(seed, ev) return ctx, items, err } } } } return nil, nil, errNoCompletion } // NOTE: This now only supports a single level of indexing; for instance, // $a[ is supported, but $a[x][ is not. func completeIndex(n parse.Node, cfg Config) (*context, []RawItem, error) { ev := cfg.PureEvaler generateForEmpty := func(v interface{}, pos int) (*context, []RawItem, error) { ctx := &context{"index", "", parse.Bareword, range0(pos)} return ctx, generateIndices(v), nil } if is(n, aSep) { if is(parent(n), aIndexing) { // We are just after an opening bracket. indexing := parent(n).(*parse.Indexing) if len(indexing.Indices) == 1 { if indexee := ev.PurelyEvalPrimary(indexing.Head); indexee != nil { return generateForEmpty(indexee, n.Range().To) } } } if is(parent(n), aArray) { array := parent(n) if is(parent(array), aIndexing) { // We are after an existing index and spaces. indexing := parent(array).(*parse.Indexing) if len(indexing.Indices) == 1 { if indexee := ev.PurelyEvalPrimary(indexing.Head); indexee != nil { return generateForEmpty(indexee, n.Range().To) } } } } } if is(n, aPrimary) { primary := n.(*parse.Primary) compound, seed := primaryInSimpleCompound(primary, ev) if compound != nil { if is(parent(compound), aArray) { array := parent(compound) if is(parent(array), aIndexing) { // We are just after an incomplete index. indexing := parent(array).(*parse.Indexing) if len(indexing.Indices) == 1 { if indexee := ev.PurelyEvalPrimary(indexing.Head); indexee != nil { ctx := &context{ "index", seed, primary.Type, compound.Range()} return ctx, generateIndices(indexee), nil } } } } } } return nil, nil, errNoCompletion } func completeRedir(n parse.Node, cfg Config) (*context, []RawItem, error) { ev := cfg.PureEvaler if is(n, aSep) { if is(parent(n), aRedir) { // Empty redirection target. ctx := &context{"redir", "", parse.Bareword, range0(n.Range().To)} items, err := generateFileNames("", false) return ctx, items, err } } if primary, ok := n.(*parse.Primary); ok { if compound, seed := primaryInSimpleCompound(primary, ev); compound != nil { if is(parent(compound), &parse.Redir{}) { // Non-empty redirection target. ctx := &context{ "redir", seed, primary.Type, compound.Range()} items, err := generateFileNames(seed, false) return ctx, items, err } } } return nil, nil, errNoCompletion } func completeVariable(n parse.Node, cfg Config) (*context, []RawItem, error) { ev := cfg.PureEvaler primary, ok := n.(*parse.Primary) if !ok || primary.Type != parse.Variable { return nil, nil, errNoCompletion } sigil, qname := eval.SplitSigil(primary.Value) ns, nameSeed := eval.SplitIncompleteQNameNs(qname) // Move past "$", "@" and ":". begin := primary.Range().From + 1 + len(sigil) + len(ns) ctx := &context{ "variable", nameSeed, parse.Bareword, diag.Ranging{From: begin, To: primary.Range().To}} var items []RawItem ev.EachVariableInNs(ns, func(varname string) { items = append(items, noQuoteItem(parse.QuoteVariableName(varname))) }) ev.EachNs(func(thisNs string) { // This is to match namespaces that are "nested" under the current // namespace. if hasProperPrefix(thisNs, ns) { items = append(items, noQuoteItem(parse.QuoteVariableName(thisNs[len(ns):]))) } }) return ctx, items, nil } func range0(pos int) diag.Ranging { return diag.Ranging{From: pos, To: pos} } func hasProperPrefix(s, p string) bool { return len(s) > len(p) && strings.HasPrefix(s, p) } elvish-0.17.0/pkg/edit/complete/filterers.go000066400000000000000000000005441415471104000207350ustar00rootroot00000000000000package complete import "strings" // FilterPrefix filters raw items by prefix. It can be used as a Filterer in // Config. func FilterPrefix(ctxName, seed string, items []RawItem) []RawItem { var filtered []RawItem for _, cand := range items { if strings.HasPrefix(cand.String(), seed) { filtered = append(filtered, cand) } } return filtered } elvish-0.17.0/pkg/edit/complete/generators.go000066400000000000000000000100311415471104000210770ustar00rootroot00000000000000package complete import ( "fmt" "os" "path/filepath" "strings" "src.elv.sh/pkg/cli/lscolors" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/fsutil" "src.elv.sh/pkg/ui" ) var pathSeparator = string(filepath.Separator) // GenerateFileNames returns filename candidates that are suitable for completing // the last argument. It can be used in Config.ArgGenerator. func GenerateFileNames(args []string) ([]RawItem, error) { return generateFileNames(args[len(args)-1], false) } // GenerateForSudo generates candidates for sudo. func GenerateForSudo(cfg Config, args []string) ([]RawItem, error) { switch { case len(args) < 2: return nil, errNoCompletion case len(args) == 2: // Complete external commands. return generateExternalCommands(args[1], cfg.PureEvaler) default: return cfg.ArgGenerator(args[1:]) } } // Internal generators, used from completers. func generateExternalCommands(seed string, ev PureEvaler) ([]RawItem, error) { if fsutil.DontSearch(seed) { // Completing a local external command name. return generateFileNames(seed, true) } var items []RawItem ev.EachExternal(func(s string) { items = append(items, PlainItem(s)) }) return items, nil } func generateCommands(seed string, ev PureEvaler) ([]RawItem, error) { if fsutil.DontSearch(seed) { // Completing a local external command name. return generateFileNames(seed, true) } var cands []RawItem addPlainItem := func(s string) { cands = append(cands, PlainItem(s)) } if strings.HasPrefix(seed, "e:") { // Generate all external commands with the e: prefix, and be done. ev.EachExternal(func(command string) { addPlainItem("e:" + command) }) return cands, nil } // Generate all special forms. ev.EachSpecial(addPlainItem) // Generate all external commands (without the e: prefix). ev.EachExternal(addPlainItem) sigil, qname := eval.SplitSigil(seed) ns, _ := eval.SplitIncompleteQNameNs(qname) if sigil == "" { // Generate functions, namespaces, and variable assignments. ev.EachVariableInNs(ns, func(varname string) { switch { case strings.HasSuffix(varname, eval.FnSuffix): addPlainItem( ns + varname[:len(varname)-len(eval.FnSuffix)]) case strings.HasSuffix(varname, eval.NsSuffix): addPlainItem(ns + varname) default: cands = append(cands, noQuoteItem(ns+varname+" = ")) } }) } return cands, nil } func generateFileNames(seed string, onlyExecutable bool) ([]RawItem, error) { var items []RawItem dir, fileprefix := filepath.Split(seed) dirToRead := dir if dirToRead == "" { dirToRead = "." } files, err := os.ReadDir(dirToRead) if err != nil { return nil, fmt.Errorf("cannot list directory %s: %v", dirToRead, err) } lsColor := lscolors.GetColorist() // Make candidates out of elements that match the file component. for _, file := range files { name := file.Name() info, err := file.Info() if err != nil { continue } // Show dot files iff file part of pattern starts with dot, and vice // versa. if dotfile(fileprefix) != dotfile(name) { continue } // Only accept searchable directories and executable files if // executableOnly is true. if onlyExecutable && (info.Mode()&0111) == 0 { continue } // Full filename for source and getStyle. full := dir + name // Will be set to an empty space for non-directories suffix := " " if info.IsDir() { full += pathSeparator suffix = "" } else if info.Mode()&os.ModeSymlink != 0 { stat, err := os.Stat(full) if err == nil && stat.IsDir() { // Symlink to directory. full += pathSeparator suffix = "" } } items = append(items, ComplexItem{ Stem: full, CodeSuffix: suffix, DisplayStyle: ui.StyleFromSGR(lsColor.GetStyle(full)), }) } return items, nil } func generateIndices(v interface{}) []RawItem { var items []RawItem vals.IterateKeys(v, func(k interface{}) bool { if kstring, ok := k.(string); ok { items = append(items, PlainItem(kstring)) } return true }) return items } func dotfile(fname string) bool { return strings.HasPrefix(fname, ".") } elvish-0.17.0/pkg/edit/complete/node_utils.go000066400000000000000000000027101415471104000211000ustar00rootroot00000000000000package complete import ( "reflect" "src.elv.sh/pkg/parse" ) // Reports whether a and b have the same dynamic type. Useful as a more succinct // alternative to type assertions. func is(a, b parse.Node) bool { return reflect.TypeOf(a) == reflect.TypeOf(b) } // Useful as arguments to is. var ( aChunk = &parse.Chunk{} aPipeline = &parse.Pipeline{} aArray = &parse.Array{} aIndexing = &parse.Indexing{} aPrimary = &parse.Primary{} aRedir = &parse.Redir{} aSep = &parse.Sep{} ) func primaryInSimpleCompound(pn *parse.Primary, ev PureEvaler) (*parse.Compound, string) { indexing, ok := parent(pn).(*parse.Indexing) if !ok { return nil, "" } compound, ok := parent(indexing).(*parse.Compound) if !ok { return nil, "" } head, ok := ev.PurelyEvalPartialCompound(compound, indexing.To) if !ok { return nil, "" } return compound, head } func purelyEvalForm(form *parse.Form, seed string, upto int, ev PureEvaler) []string { // Find out head of the form and preceding arguments. // If form.Head is not a simple compound, head will be "", just what we want. head, _ := ev.PurelyEvalPartialCompound(form.Head, -1) words := []string{head} for _, compound := range form.Args { if compound.Range().From >= upto { break } if arg, ok := ev.PurelyEvalCompound(compound); ok { // TODO(xiaq): Arguments that are not simple compounds are simply ignored. words = append(words, arg) } } words = append(words, seed) return words } elvish-0.17.0/pkg/edit/complete/raw_item.go000066400000000000000000000027141415471104000205460ustar00rootroot00000000000000package complete import ( "src.elv.sh/pkg/cli/modes" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/ui" ) // PlainItem is a simple implementation of RawItem. type PlainItem string func (p PlainItem) String() string { return string(p) } func (p PlainItem) Cook(q parse.PrimaryType) modes.CompletionItem { s := string(p) quoted, _ := parse.QuoteAs(s, q) return modes.CompletionItem{ToInsert: quoted, ToShow: s} } // noQuoteItem is a RawItem implementation that does not quote when cooked. This // type is not exposed, since argument generators never need this. type noQuoteItem string func (nq noQuoteItem) String() string { return string(nq) } func (nq noQuoteItem) Cook(parse.PrimaryType) modes.CompletionItem { s := string(nq) return modes.CompletionItem{ToInsert: s, ToShow: s} } // ComplexItem is an implementation of RawItem that offers customization options. type ComplexItem struct { Stem string // Used in the code and the menu. CodeSuffix string // Appended to the code. Display string // How the item is displayed. If empty, defaults to Stem. DisplayStyle ui.Style // Use for displaying. } func (c ComplexItem) String() string { return c.Stem } func (c ComplexItem) Cook(q parse.PrimaryType) modes.CompletionItem { quoted, _ := parse.QuoteAs(c.Stem, q) display := c.Display if display == "" { display = c.Stem } return modes.CompletionItem{ ToInsert: quoted + c.CodeSuffix, ToShow: display, ShowStyle: c.DisplayStyle, } } elvish-0.17.0/pkg/edit/complete_getopt.go000066400000000000000000000240311415471104000203150ustar00rootroot00000000000000package edit import ( "errors" "fmt" "strings" "unicode/utf8" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/getopt" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/persistent/hashmap" ) //elvdoc:fn complete-getopt // // ```elvish // edit:complete-getopt $args $opt-specs $arg-handlers // ``` // Produces completions according to a specification of accepted command-line // options (both short and long options are handled), positional handler // functions for each command position, and the current arguments in the command // line. The arguments are as follows: // // * `$args` is an array containing the current arguments in the command line // (without the command itself). These are the arguments as passed to the // [Argument Completer](#argument-completer) function. // // * `$opt-specs` is an array of maps, each one containing the definition of // one possible command-line option. Matching options will be provided as // completions when the last element of `$args` starts with a dash, but not // otherwise. Each map can contain the following keys (at least one of `short` // or `long` needs to be specified): // // - `short` contains the one-letter short option, if any, without the dash. // // - `long` contains the long option name, if any, without the initial two // dashes. // // - `arg-optional`, if set to `$true`, specifies that the option receives an // optional argument. // // - `arg-required`, if set to `$true`, specifies that the option receives a // mandatory argument. Only one of `arg-optional` or `arg-required` can be // set to `$true`. // // - `desc` can be set to a human-readable description of the option which // will be displayed in the completion menu. // // - `completer` can be set to a function to generate possible completions for // the option argument. The function receives as argument the element at // that position and return zero or more candidates. // // * `$arg-handlers` is an array of functions, each one returning the possible // completions for that position in the arguments. Each function receives // as argument the last element of `$args`, and should return zero or more // possible values for the completions at that point. The returned values can // be plain strings or the output of `edit:complex-candidate`. If the last // element of the list is the string `...`, then the last handler is reused // for all following arguments. // // Example: // // ```elvish-transcript // ~> fn complete {|@args| // opt-specs = [ [&short=a &long=all &desc="Show all"] // [&short=n &desc="Set name" &arg-required=$true // &completer= {|_| put name1 name2 }] ] // arg-handlers = [ {|_| put first1 first2 } // {|_| put second1 second2 } ... ] // edit:complete-getopt $args $opt-specs $arg-handlers // } // ~> complete '' // ▶ first1 // ▶ first2 // ~> complete '-' // ▶ (edit:complex-candidate -a &display='-a (Show all)') // ▶ (edit:complex-candidate --all &display='--all (Show all)') // ▶ (edit:complex-candidate -n &display='-n (Set name)') // ~> complete -n '' // ▶ name1 // ▶ name2 // ~> complete -a '' // ▶ first1 // ▶ first2 // ~> complete arg1 '' // ▶ second1 // ▶ second2 // ~> complete arg1 arg2 '' // ▶ second1 // ▶ second2 // ``` func completeGetopt(fm *eval.Frame, vArgs, vOpts, vArgHandlers interface{}) error { args, err := parseGetoptArgs(vArgs) if err != nil { return err } opts, err := parseGetoptOptSpecs(vOpts) if err != nil { return err } argHandlers, variadic, err := parseGetoptArgHandlers(vArgHandlers) if err != nil { return err } // TODO(xiaq): Make the Config field configurable g := getopt.Getopt{Options: opts.opts, Config: getopt.GNUGetoptLong} _, parsedArgs, ctx := g.Parse(args) out := fm.ValueOutput() putShortOpt := func(opt *getopt.Option) error { c := complexItem{Stem: "-" + string(opt.Short)} if d, ok := opts.desc[opt]; ok { if e, ok := opts.argDesc[opt]; ok { c.Display = c.Stem + " " + e + " (" + d + ")" } else { c.Display = c.Stem + " (" + d + ")" } } return out.Put(c) } putLongOpt := func(opt *getopt.Option) error { c := complexItem{Stem: "--" + opt.Long} if d, ok := opts.desc[opt]; ok { if e, ok := opts.argDesc[opt]; ok { c.Display = c.Stem + " " + e + " (" + d + ")" } else { c.Display = c.Stem + " (" + d + ")" } } return out.Put(c) } call := func(fn eval.Callable, args ...interface{}) error { return fn.Call(fm, args, eval.NoOpts) } switch ctx.Type { case getopt.NewOptionOrArgument, getopt.Argument: // Find argument handler. var argHandler eval.Callable if len(parsedArgs) < len(argHandlers) { argHandler = argHandlers[len(parsedArgs)] } else if variadic { argHandler = argHandlers[len(argHandlers)-1] } if argHandler != nil { return call(argHandler, ctx.Text) } // TODO(xiaq): Notify that there is no suitable argument completer. case getopt.NewOption: for _, opt := range opts.opts { if opt.Short != 0 { err := putShortOpt(opt) if err != nil { return err } } if opt.Long != "" { err := putLongOpt(opt) if err != nil { return err } } } case getopt.NewLongOption: for _, opt := range opts.opts { if opt.Long != "" { err := putLongOpt(opt) if err != nil { return err } } } case getopt.LongOption: for _, opt := range opts.opts { if strings.HasPrefix(opt.Long, ctx.Text) { err := putLongOpt(opt) if err != nil { return err } } } case getopt.ChainShortOption: for _, opt := range opts.opts { if opt.Short != 0 { // TODO(xiaq): Loses chained options. err := putShortOpt(opt) if err != nil { return err } } } case getopt.OptionArgument: gen := opts.argGenerator[ctx.Option.Option] if gen != nil { return call(gen, ctx.Option.Argument) } } return nil } // TODO(xiaq): Simplify most of the parsing below with reflection. func parseGetoptArgs(v interface{}) ([]string, error) { var args []string var err error errIterate := vals.Iterate(v, func(v interface{}) bool { arg, ok := v.(string) if !ok { err = fmt.Errorf("arg should be string, got %s", vals.Kind(v)) return false } args = append(args, arg) return true }) if errIterate != nil { err = errIterate } return args, err } type parsedOptSpecs struct { opts []*getopt.Option desc map[*getopt.Option]string argDesc map[*getopt.Option]string argGenerator map[*getopt.Option]eval.Callable } func parseGetoptOptSpecs(v interface{}) (parsedOptSpecs, error) { result := parsedOptSpecs{ nil, map[*getopt.Option]string{}, map[*getopt.Option]string{}, map[*getopt.Option]eval.Callable{}} var err error errIterate := vals.Iterate(v, func(v interface{}) bool { m, ok := v.(hashmap.Map) if !ok { err = fmt.Errorf("opt should be map, got %s", vals.Kind(v)) return false } opt := &getopt.Option{} getStringField := func(k string) (string, bool, error) { v, ok := m.Index(k) if !ok { return "", false, nil } if vs, ok := v.(string); ok { return vs, true, nil } return "", false, fmt.Errorf("%s should be string, got %s", k, vals.Kind(v)) } getCallableField := func(k string) (eval.Callable, bool, error) { v, ok := m.Index(k) if !ok { return nil, false, nil } if vb, ok := v.(eval.Callable); ok { return vb, true, nil } return nil, false, fmt.Errorf("%s should be fn, got %s", k, vals.Kind(v)) } getBoolField := func(k string) (bool, bool, error) { v, ok := m.Index(k) if !ok { return false, false, nil } if vb, ok := v.(bool); ok { return vb, true, nil } return false, false, fmt.Errorf("%s should be bool, got %s", k, vals.Kind(v)) } if s, ok, errGet := getStringField("short"); ok { r, size := utf8.DecodeRuneInString(s) if r == utf8.RuneError || size != len(s) { err = fmt.Errorf( "short should be exactly one rune, got %v", parse.Quote(s)) return false } opt.Short = r } else if errGet != nil { err = errGet return false } if s, ok, errGet := getStringField("long"); ok { opt.Long = s } else if errGet != nil { err = errGet return false } if opt.Short == 0 && opt.Long == "" { err = errors.New( "opt should have at least one of short and long forms") return false } argRequired, _, errGet := getBoolField("arg-required") if errGet != nil { err = errGet return false } argOptional, _, errGet := getBoolField("arg-optional") if errGet != nil { err = errGet return false } switch { case argRequired && argOptional: err = errors.New( "opt cannot have both arg-required and arg-optional") return false case argRequired: opt.HasArg = getopt.RequiredArgument case argOptional: opt.HasArg = getopt.OptionalArgument } if s, ok, errGet := getStringField("desc"); ok { result.desc[opt] = s } else if errGet != nil { err = errGet return false } if s, ok, errGet := getStringField("arg-desc"); ok { result.argDesc[opt] = s } else if errGet != nil { err = errGet return false } if f, ok, errGet := getCallableField("completer"); ok { result.argGenerator[opt] = f } else if errGet != nil { err = errGet return false } result.opts = append(result.opts, opt) return true }) if errIterate != nil { err = errIterate } return result, err } func parseGetoptArgHandlers(v interface{}) ([]eval.Callable, bool, error) { var argHandlers []eval.Callable var variadic bool var err error errIterate := vals.Iterate(v, func(v interface{}) bool { sv, ok := v.(string) if ok { if sv == "..." { variadic = true return true } err = fmt.Errorf( "string except for ... not allowed as argument handler, got %s", parse.Quote(sv)) return false } argHandler, ok := v.(eval.Callable) if !ok { err = fmt.Errorf( "argument handler should be fn, got %s", vals.Kind(v)) } argHandlers = append(argHandlers, argHandler) return true }) if errIterate != nil { err = errIterate } return argHandlers, variadic, err } elvish-0.17.0/pkg/edit/complete_getopt_test.go000066400000000000000000000103431415471104000213550ustar00rootroot00000000000000package edit import ( "testing" "src.elv.sh/pkg/eval" . "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/parse" ) func setupCompleteGetopt(ev *eval.Evaler) { ev.ExtendBuiltin(eval.BuildNs().AddGoFn("complete-getopt", completeGetopt)) code := `fn complete {|@args| opt-specs = [ [&short=a &long=all &desc="Show all"] [&short=n &long=name &desc="Set name" &arg-required=$true &arg-desc='new-name' &completer= {|_| put name1 name2 }] ] arg-handlers = [ {|_| put first1 first2 } {|_| put second1 second2 } ... ] complete-getopt $args $opt-specs $arg-handlers }` ev.Eval(parse.Source{Name: "[test init]", Code: code}, eval.EvalCfg{}) } func TestCompleteGetopt(t *testing.T) { TestWithSetup(t, setupCompleteGetopt, // Complete argument That("complete ''").Puts("first1", "first2"), That("complete '' >&-").Throws(eval.ErrNoValueOutput), // Complete option That("complete -").Puts( complexItem{Stem: "-a", Display: "-a (Show all)"}, complexItem{Stem: "--all", Display: "--all (Show all)"}, complexItem{Stem: "-n", Display: "-n new-name (Set name)"}, complexItem{Stem: "--name", Display: "--name new-name (Set name)"}), That("complete - >&-").Throws(eval.ErrNoValueOutput), // Complete long option That("complete --").Puts( complexItem{Stem: "--all", Display: "--all (Show all)"}, complexItem{Stem: "--name", Display: "--name new-name (Set name)"}), That("complete --a").Puts( complexItem{Stem: "--all", Display: "--all (Show all)"}), That("complete -- >&-").Throws(eval.ErrNoValueOutput), // Complete argument of short option That("complete -n ''").Puts("name1", "name2"), That("complete -n '' >&-").Throws(eval.ErrNoValueOutput), // Complete argument of long option That("complete --name ''").Puts("name1", "name2"), That("complete --name '' >&-").Throws(eval.ErrNoValueOutput), // Complete (normal) argument after option that doesn't take an argument That("complete -a ''").Puts("first1", "first2"), That("complete -a '' >&-").Throws(eval.ErrNoValueOutput), // Complete second argument That("complete arg1 ''").Puts("second1", "second2"), That("complete arg1 '' >&-").Throws(eval.ErrNoValueOutput), // Complete variadic argument That("complete arg1 arg2 ''").Puts("second1", "second2"), That("complete arg1 arg2 '' >&-").Throws(eval.ErrNoValueOutput), ) } func TestCompleteGetopt_TypeCheck(t *testing.T) { TestWithSetup(t, setupCompleteGetopt, That("complete-getopt [foo []] [] []"). Throws(ErrorWithMessage("arg should be string, got list")), That("complete-getopt [] [foo] []"). Throws(ErrorWithMessage("opt should be map, got string")), That("complete-getopt [] [[&short=[]]] []"). Throws(ErrorWithMessage("short should be string, got list")), That("complete-getopt [] [[&short=foo]] []"). Throws(ErrorWithMessage("short should be exactly one rune, got foo")), That("complete-getopt [] [[&long=[]]] []"). Throws(ErrorWithMessage("long should be string, got list")), That("complete-getopt [] [[&]] []"). Throws(ErrorWithMessage("opt should have at least one of short and long forms")), That("complete-getopt [] [[&short=a &arg-required=foo]] []"). Throws(ErrorWithMessage("arg-required should be bool, got string")), That("complete-getopt [] [[&short=a &arg-optional=foo]] []"). Throws(ErrorWithMessage("arg-optional should be bool, got string")), That("complete-getopt [] [[&short=a &arg-required=$true &arg-optional=$true]] []"). Throws(ErrorWithMessage("opt cannot have both arg-required and arg-optional")), That("complete-getopt [] [[&short=a &desc=[]]] []"). Throws(ErrorWithMessage("desc should be string, got list")), That("complete-getopt [] [[&short=a &arg-desc=[]]] []"). Throws(ErrorWithMessage("arg-desc should be string, got list")), That("complete-getopt [] [[&short=a &completer=[]]] []"). Throws(ErrorWithMessage("completer should be fn, got list")), That("complete-getopt [] [] [foo]"). Throws(ErrorWithMessage("string except for ... not allowed as argument handler, got foo")), That("complete-getopt [] [] [[]]"). Throws(ErrorWithMessage("argument handler should be fn, got list")), ) } elvish-0.17.0/pkg/edit/completion.go000066400000000000000000000361041415471104000173000ustar00rootroot00000000000000package edit import ( "bufio" "fmt" "os" "strings" "sync" "unicode/utf8" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/modes" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/edit/complete" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" "src.elv.sh/pkg/fsutil" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/persistent/hash" "src.elv.sh/pkg/strutil" ) //elvdoc:var completion:arg-completer // // A map containing argument completers. //elvdoc:var completion:binding // // Keybinding for the completion mode. //elvdoc:var completion:matcher // // A map mapping from context names to matcher functions. See the // [Matcher](#matcher) section. //elvdoc:fn complete-filename // // ```elvish // edit:complete-filename $args... // ``` // // Produces a list of filenames found in the directory of the last argument. All // other arguments are ignored. If the last argument does not contain a path // (either absolute or relative to the current directory), then the current // directory is used. Relevant files are output as `edit:complex-candidate` // objects. // // This function is the default handler for any commands without // explicit handlers in `$edit:completion:arg-completer`. See [Argument // Completer](#argument-completer). // // Example: // // ```elvish-transcript // ~> edit:complete-filename '' // ▶ (edit:complex-candidate Applications &code-suffix=/ &style='01;34') // ▶ (edit:complex-candidate Books &code-suffix=/ &style='01;34') // ▶ (edit:complex-candidate Desktop &code-suffix=/ &style='01;34') // ▶ (edit:complex-candidate Docsafe &code-suffix=/ &style='01;34') // ▶ (edit:complex-candidate Documents &code-suffix=/ &style='01;34') // ... // ~> edit:complete-filename .elvish/ // ▶ (edit:complex-candidate .elvish/aliases &code-suffix=/ &style='01;34') // ▶ (edit:complex-candidate .elvish/db &code-suffix=' ' &style='') // ▶ (edit:complex-candidate .elvish/epm-installed &code-suffix=' ' &style='') // ▶ (edit:complex-candidate .elvish/lib &code-suffix=/ &style='01;34') // ▶ (edit:complex-candidate .elvish/rc.elv &code-suffix=' ' &style='') // ``` //elvdoc:fn complex-candidate // // ```elvish // edit:complex-candidate $stem &display='' &code-suffix='' // ``` // // Builds a complex candidate. This is mainly useful in [argument // completers](#argument-completer). // // The `&display` option controls how the candidate is shown in the UI. It can // be a string or a [styled](builtin.html#styled) text. If it is empty, `$stem` // is used. // // The `&code-suffix` option affects how the candidate is inserted into the code // when it is accepted. By default, a quoted version of `$stem` is inserted. If // `$code-suffix` is non-empty, it is added to that text, and the suffix is not // quoted. type complexCandidateOpts struct { CodeSuffix string Display string } func (*complexCandidateOpts) SetDefaultOptions() {} func complexCandidate(fm *eval.Frame, opts complexCandidateOpts, stem string) complexItem { display := opts.Display if display == "" { display = stem } return complexItem{ Stem: stem, CodeSuffix: opts.CodeSuffix, Display: display, } } //elvdoc:fn match-prefix // // ```elvish // edit:match-prefix $seed $inputs? // ``` // // For each input, outputs whether the input has $seed as a prefix. Uses the // result of `to-string` for non-string inputs. // // Roughly equivalent to the following Elvish function, but more efficient: // // ```elvish // use str // fn match-prefix {|seed @input| // each {|x| str:has-prefix (to-string $x) $seed } $@input // } // ``` //elvdoc:fn match-subseq // // ```elvish // edit:match-subseq $seed $inputs? // ``` // // For each input, outputs whether the input has $seed as a // [subsequence](https://en.wikipedia.org/wiki/Subsequence). Uses the result of // `to-string` for non-string inputs. //elvdoc:fn match-substr // // ```elvish // edit:match-substr $seed $inputs? // ``` // // For each input, outputs whether the input has $seed as a substring. Uses the // result of `to-string` for non-string inputs. // // Roughly equivalent to the following Elvish function, but more efficient: // // ```elvish // use str // fn match-substr {|seed @input| // each {|x| str:has-contains (to-string $x) $seed } $@input // } // ``` //elvdoc:fn completion:start // // Start the completion mode. //elvdoc:fn completion:smart-start // // Starts the completion mode. However, if all the candidates share a non-empty // prefix and that prefix starts with the seed, inserts the prefix instead. func completionStart(app cli.App, bindings tk.Bindings, cfg complete.Config, smart bool) { codeArea, ok := focusedCodeArea(app) if !ok { return } buf := codeArea.CopyState().Buffer result, err := complete.Complete( complete.CodeBuffer{Content: buf.Content, Dot: buf.Dot}, cfg) if err != nil { app.Notify(err.Error()) return } if smart { prefix := "" for i, item := range result.Items { if i == 0 { prefix = item.ToInsert continue } prefix = commonPrefix(prefix, item.ToInsert) if prefix == "" { break } } if prefix != "" { insertedPrefix := false codeArea.MutateState(func(s *tk.CodeAreaState) { rep := s.Buffer.Content[result.Replace.From:result.Replace.To] if len(prefix) > len(rep) && strings.HasPrefix(prefix, rep) { s.Pending = tk.PendingCode{ Content: prefix, From: result.Replace.From, To: result.Replace.To} s.ApplyPending() insertedPrefix = true } }) if insertedPrefix { return } } } w, err := modes.NewCompletion(app, modes.CompletionSpec{ Name: result.Name, Replace: result.Replace, Items: result.Items, Filter: filterSpec, Bindings: bindings, }) if w != nil { app.PushAddon(w) } if err != nil { app.Notify(err.Error()) } } //elvdoc:fn completion:close // // Closes the completion mode UI. func initCompletion(ed *Editor, ev *eval.Evaler, nb eval.NsBuilder) { bindingVar := newBindingVar(emptyBindingsMap) bindings := newMapBindings(ed, ev, bindingVar) matcherMapVar := newMapVar(vals.EmptyMap) argGeneratorMapVar := newMapVar(vals.EmptyMap) cfg := func() complete.Config { return complete.Config{ PureEvaler: pureEvaler{ev}, Filterer: adaptMatcherMap( ed, ev, matcherMapVar.Get().(vals.Map)), ArgGenerator: adaptArgGeneratorMap( ev, argGeneratorMapVar.Get().(vals.Map)), } } generateForSudo := func(args []string) ([]complete.RawItem, error) { return complete.GenerateForSudo(cfg(), args) } nb.AddGoFns(map[string]interface{}{ "complete-filename": wrapArgGenerator(complete.GenerateFileNames), "complete-getopt": completeGetopt, "complete-sudo": wrapArgGenerator(generateForSudo), "complex-candidate": complexCandidate, "match-prefix": wrapMatcher(strings.HasPrefix), "match-subseq": wrapMatcher(strutil.HasSubseq), "match-substr": wrapMatcher(strings.Contains), }) app := ed.app nb.AddNs("completion", eval.BuildNsNamed("edit:completion"). AddVars(map[string]vars.Var{ "arg-completer": argGeneratorMapVar, "binding": bindingVar, "matcher": matcherMapVar, }). AddGoFns(map[string]interface{}{ "accept": func() { listingAccept(app) }, "smart-start": func() { completionStart(app, bindings, cfg(), true) }, "start": func() { completionStart(app, bindings, cfg(), false) }, "up": func() { listingUp(app) }, "down": func() { listingDown(app) }, "up-cycle": func() { listingUpCycle(app) }, "down-cycle": func() { listingDownCycle(app) }, "left": func() { listingLeft(app) }, "right": func() { listingRight(app) }, })) } // A wrapper type implementing Elvish value methods. type complexItem complete.ComplexItem func (c complexItem) Index(k interface{}) (interface{}, bool) { switch k { case "stem": return c.Stem, true case "code-suffix": return c.CodeSuffix, true case "display": return c.Display, true } return nil, false } func (c complexItem) IterateKeys(f func(interface{}) bool) { vals.Feed(f, "stem", "code-suffix", "display") } func (c complexItem) Kind() string { return "map" } func (c complexItem) Equal(a interface{}) bool { rhs, ok := a.(complexItem) return ok && c.Stem == rhs.Stem && c.CodeSuffix == rhs.CodeSuffix && c.Display == rhs.Display } func (c complexItem) Hash() uint32 { h := hash.DJBInit h = hash.DJBCombine(h, hash.String(c.Stem)) h = hash.DJBCombine(h, hash.String(c.CodeSuffix)) h = hash.DJBCombine(h, hash.String(c.Display)) return h } func (c complexItem) Repr(indent int) string { // TODO(xiaq): Pretty-print when indent >= 0 return fmt.Sprintf("(edit:complex-candidate %s &code-suffix=%s &display=%s)", parse.Quote(c.Stem), parse.Quote(c.CodeSuffix), parse.Quote(c.Display)) } type wrappedArgGenerator func(*eval.Frame, ...string) error // Wraps an ArgGenerator into a function that can be then passed to // eval.NewGoFn. func wrapArgGenerator(gen complete.ArgGenerator) wrappedArgGenerator { return func(fm *eval.Frame, args ...string) error { rawItems, err := gen(args) if err != nil { return err } out := fm.ValueOutput() for _, rawItem := range rawItems { var v interface{} switch rawItem := rawItem.(type) { case complete.ComplexItem: v = complexItem(rawItem) case complete.PlainItem: v = string(rawItem) default: v = rawItem } err := out.Put(v) if err != nil { return err } } return nil } } func commonPrefix(s1, s2 string) string { for i, r := range s1 { if s2 == "" { break } r2, n2 := utf8.DecodeRuneInString(s2) if r2 != r { return s1[:i] } s2 = s2[n2:] } return s1 } // The type for a native Go matcher. This is not equivalent to the Elvish // counterpart, which streams input and output. This is because we can actually // afford calling a Go function for each item, so omitting the streaming // behavior makes the implementation simpler. // // Native Go matchers are wrapped into Elvish matchers, but never the other way // around. // // This type is satisfied by strings.Contains and strings.HasPrefix; they are // wrapped into match-substr and match-prefix respectively. type matcher func(text, seed string) bool type matcherOpts struct { IgnoreCase bool SmartCase bool } func (*matcherOpts) SetDefaultOptions() {} type wrappedMatcher func(fm *eval.Frame, opts matcherOpts, seed string, inputs eval.Inputs) error func wrapMatcher(m matcher) wrappedMatcher { return func(fm *eval.Frame, opts matcherOpts, seed string, inputs eval.Inputs) error { out := fm.ValueOutput() var errOut error if opts.IgnoreCase || (opts.SmartCase && seed == strings.ToLower(seed)) { if opts.IgnoreCase { seed = strings.ToLower(seed) } inputs(func(v interface{}) { if errOut != nil { return } errOut = out.Put(m(strings.ToLower(vals.ToString(v)), seed)) }) } else { inputs(func(v interface{}) { if errOut != nil { return } errOut = out.Put(m(vals.ToString(v), seed)) }) } return errOut } } // Adapts $edit:completion:matcher into a Filterer. func adaptMatcherMap(nt notifier, ev *eval.Evaler, m vals.Map) complete.Filterer { return func(ctxName, seed string, rawItems []complete.RawItem) []complete.RawItem { matcher, ok := lookupFn(m, ctxName) if !ok { nt.notifyf( "matcher for %s not a function, falling back to prefix matching", ctxName) } if matcher == nil { return complete.FilterPrefix(ctxName, seed, rawItems) } input := make(chan interface{}) stopInputFeeder := make(chan struct{}) defer close(stopInputFeeder) // Feed a string representing all raw candidates to the input channel. go func() { defer close(input) for _, rawItem := range rawItems { select { case input <- rawItem.String(): case <-stopInputFeeder: return } } }() // TODO: Supply the Chan component of port 2. port1, collect, err := eval.CapturePort() if err != nil { nt.notifyf("cannot create pipe to run completion matcher: %v", err) return nil } err = ev.Call(matcher, eval.CallCfg{Args: []interface{}{seed}, From: "[editor matcher]"}, eval.EvalCfg{Ports: []*eval.Port{ // TODO: Supply the Chan component of port 2. {Chan: input, File: eval.DevNull}, port1, {File: os.Stderr}}}) outputs := collect() if err != nil { nt.notifyError("matcher", err) // Continue with whatever values have been output } if len(outputs) != len(rawItems) { nt.notifyf( "matcher has output %v values, not equal to %v inputs", len(outputs), len(rawItems)) } filtered := []complete.RawItem{} for i := 0; i < len(rawItems) && i < len(outputs); i++ { if vals.Bool(outputs[i]) { filtered = append(filtered, rawItems[i]) } } return filtered } } func adaptArgGeneratorMap(ev *eval.Evaler, m vals.Map) complete.ArgGenerator { return func(args []string) ([]complete.RawItem, error) { gen, ok := lookupFn(m, args[0]) if !ok { return nil, fmt.Errorf("arg completer for %s not a function", args[0]) } if gen == nil { return complete.GenerateFileNames(args) } argValues := make([]interface{}, len(args)) for i, arg := range args { argValues[i] = arg } var output []complete.RawItem var outputMutex sync.Mutex collect := func(item complete.RawItem) { outputMutex.Lock() defer outputMutex.Unlock() output = append(output, item) } valueCb := func(ch <-chan interface{}) { for v := range ch { switch v := v.(type) { case string: collect(complete.PlainItem(v)) case complexItem: collect(complete.ComplexItem(v)) default: collect(complete.PlainItem(vals.ToString(v))) } } } bytesCb := func(r *os.File) { buffered := bufio.NewReader(r) for { line, err := buffered.ReadString('\n') if line != "" { collect(complete.PlainItem(strutil.ChopLineEnding(line))) } if err != nil { break } } } port1, done, err := eval.PipePort(valueCb, bytesCb) if err != nil { panic(err) } err = ev.Call(gen, eval.CallCfg{Args: argValues, From: "[editor arg generator]"}, eval.EvalCfg{Ports: []*eval.Port{ // TODO: Supply the Chan component of port 2. nil, port1, {File: os.Stderr}}}) done() return output, err } } func lookupFn(m vals.Map, ctxName string) (eval.Callable, bool) { val, ok := m.Index(ctxName) if !ok { val, ok = m.Index("") } if !ok { // No matcher, but not an error either return nil, true } fn, ok := val.(eval.Callable) if !ok { return nil, false } return fn, true } type pureEvaler struct{ ev *eval.Evaler } func (pureEvaler) EachExternal(f func(string)) { fsutil.EachExternal(f) } func (pureEvaler) EachSpecial(f func(string)) { for name := range eval.IsBuiltinSpecial { f(name) } } func (pe pureEvaler) EachNs(f func(string)) { eachNsInTop(pe.ev.Builtin(), pe.ev.Global(), f) } func (pe pureEvaler) EachVariableInNs(ns string, f func(string)) { eachVariableInTop(pe.ev.Builtin(), pe.ev.Global(), ns, f) } func (pe pureEvaler) PurelyEvalPrimary(pn *parse.Primary) interface{} { return pe.ev.PurelyEvalPrimary(pn) } func (pe pureEvaler) PurelyEvalCompound(cn *parse.Compound) (string, bool) { return pe.ev.PurelyEvalCompound(cn) } func (pe pureEvaler) PurelyEvalPartialCompound(cn *parse.Compound, upto int) (string, bool) { return pe.ev.PurelyEvalPartialCompound(cn, upto) } elvish-0.17.0/pkg/edit/completion_test.go000066400000000000000000000126641415471104000203440ustar00rootroot00000000000000package edit import ( "testing" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/eval" . "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/testutil" ) func TestCompletionAddon(t *testing.T) { f := setup(t) testutil.ApplyDir(testutil.Dir{"a": "", "b": ""}) feedInput(f.TTYCtrl, "echo \t") f.TestTTY(t, "~> echo a \n", Styles, " vvvv __", " COMPLETING argument ", Styles, "********************* ", term.DotHere, "\n", "a b", Styles, "+ ", ) } func TestCompletionAddon_CompletesLongestCommonPrefix(t *testing.T) { f := setup(t) testutil.ApplyDir(testutil.Dir{"foo1": "", "foo2": "", "foo": "", "fox": ""}) feedInput(f.TTYCtrl, "echo \t") f.TestTTY(t, "~> echo fo", Styles, " vvvv", term.DotHere, ) feedInput(f.TTYCtrl, "\t") f.TestTTY(t, "~> echo foo \n", Styles, " vvvv ____", " COMPLETING argument ", Styles, "********************* ", term.DotHere, "\n", "foo foo1 foo2 fox", Styles, "+++ ", ) } func TestCompleteFilename(t *testing.T) { f := setup(t) testutil.ApplyDir(testutil.Dir{"d": testutil.Dir{"a": "", "b": ""}}) evals(f.Evaler, `@cands = (edit:complete-filename ls ./d/a)`) testGlobal(t, f.Evaler, "cands", vals.MakeList( complexItem{Stem: "./d/a", CodeSuffix: " "}, complexItem{Stem: "./d/b", CodeSuffix: " "})) testThatOutputErrorIsBubbled(t, f, "edit:complete-filename ls ''") } func TestComplexCandidate(t *testing.T) { TestWithSetup(t, func(ev *eval.Evaler) { ev.ExtendGlobal(eval.BuildNs().AddGoFn("cc", complexCandidate)) }, That("kind-of (cc stem)").Puts("map"), That("keys (cc stem)").Puts("stem", "code-suffix", "display"), That("repr (cc a/b &code-suffix=' ' &display=A/B)").Prints( "(edit:complex-candidate a/b &code-suffix=' ' &display=A/B)\n"), That("eq (cc stem) (cc stem)").Puts(true), That("eq (cc stem &code-suffix=' ') (cc stem)").Puts(false), That("eq (cc stem &display=STEM) (cc stem)").Puts(false), That("put [&(cc stem)=value][(cc stem)]").Puts("value"), That("put (cc a/b &code-suffix=' ' &display=A/B)[stem code-suffix display]"). Puts("a/b", " ", "A/B"), ) } func TestComplexCandidate_InEditModule(t *testing.T) { // A sanity check that the complex-candidate command is part of the edit // module. f := setup(t) evals(f.Evaler, `stem = (edit:complex-candidate stem)[stem]`) testGlobal(t, f.Evaler, "stem", "stem") } func TestCompletionArgCompleter_ArgsAndValueOutput(t *testing.T) { f := setup(t) evals(f.Evaler, `foo-args = []`, `fn foo { }`, `edit:completion:arg-completer[foo] = {|@args| foo-args = $args put 1val edit:complex-candidate 2val &display=2VAL }`) feedInput(f.TTYCtrl, "foo foo1 foo2 \t") f.TestTTY(t, "~> foo foo1 foo2 1val\n", Styles, " vvv ____", " COMPLETING argument ", Styles, "********************* ", term.DotHere, "\n", "1val 2VAL", Styles, "++++ ", ) testGlobal(t, f.Evaler, "foo-args", vals.MakeList("foo", "foo1", "foo2", "")) } func TestCompletionArgCompleter_BytesOutput(t *testing.T) { f := setup(t) evals(f.Evaler, `fn foo { }`, `edit:completion:arg-completer[foo] = {|@args| echo 1val echo 2val }`) feedInput(f.TTYCtrl, "foo foo1 foo2 \t") f.TestTTY(t, "~> foo foo1 foo2 1val\n", Styles, " vvv ____", " COMPLETING argument ", Styles, "********************* ", term.DotHere, "\n", "1val 2val", Styles, "++++ ", ) } func TestCompleteSudo(t *testing.T) { f := setup(t) evals(f.Evaler, `fn foo { }`, `edit:completion:arg-completer[foo] = {|@args| echo val1 echo val2 }`, `@cands = (edit:complete-sudo sudo foo '')`) testGlobal(t, f.Evaler, "cands", vals.MakeList("val1", "val2")) } func TestCompletionMatcher(t *testing.T) { f := setup(t) testutil.ApplyDir(testutil.Dir{"foo": "", "oof": ""}) evals(f.Evaler, `edit:completion:matcher[''] = $edit:match-substr~`) feedInput(f.TTYCtrl, "echo f\t") f.TestTTY(t, "~> echo foo \n", Styles, " vvvv ____", " COMPLETING argument ", Styles, "********************* ", term.DotHere, "\n", "foo oof", Styles, "+++ ", ) } func TestBuiltinMatchers(t *testing.T) { f := setup(t) evals(f.Evaler, `@prefix = (edit:match-prefix ab [ab abc cab acb ba [ab] [a b] [b a]])`, `@substr = (edit:match-substr ab [ab abc cab acb ba [ab] [a b] [b a]])`, `@subseq = (edit:match-subseq ab [ab abc cab acb ba [ab] [a b] [b a]])`, ) testGlobals(t, f.Evaler, map[string]interface{}{ "prefix": vals.MakeList(true, true, false, false, false, false, false, false), "substr": vals.MakeList(true, true, true, false, false, true, false, false), "subseq": vals.MakeList(true, true, true, true, false, true, true, false), }) testThatOutputErrorIsBubbled(t, f, "edit:match-prefix ab [ab]") } func TestBuiltinMatchers_Options(t *testing.T) { f := setup(t) // The two options work identically on all the builtin matchers, so we only // test for match-prefix for simplicity. evals(f.Evaler, `@a = (edit:match-prefix &ignore-case ab [abc aBc AbC])`, `@b = (edit:match-prefix &ignore-case aB [abc aBc AbC])`, `@c = (edit:match-prefix &smart-case ab [abc aBc Abc])`, `@d = (edit:match-prefix &smart-case aB [abc aBc AbC])`, ) testGlobals(t, f.Evaler, map[string]interface{}{ "a": vals.MakeList(true, true, true), "b": vals.MakeList(true, true, true), "c": vals.MakeList(true, true, true), "d": vals.MakeList(false, true, false), }) testThatOutputErrorIsBubbled(t, f, "edit:match-prefix &ignore-case ab [ab]") } elvish-0.17.0/pkg/edit/config_api.go000066400000000000000000000131461415471104000172260ustar00rootroot00000000000000package edit import ( "fmt" "os" "strings" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/histutil" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" "src.elv.sh/pkg/store/storedefs" ) //elvdoc:var max-height // // Maximum height the editor is allowed to use, defaults to `+Inf`. // // By default, the height of the editor is only restricted by the terminal // height. Some modes like location mode can use a lot of lines; as a result, // it can often occupy the entire terminal, and push up your scrollback buffer. // Change this variable to a finite number to restrict the height of the editor. func initMaxHeight(appSpec *cli.AppSpec, nb eval.NsBuilder) { maxHeight := newIntVar(-1) appSpec.MaxHeight = func() int { return maxHeight.GetRaw().(int) } nb.AddVar("max-height", maxHeight) } func initReadlineHooks(appSpec *cli.AppSpec, ev *eval.Evaler, nb eval.NsBuilder) { initBeforeReadline(appSpec, ev, nb) initAfterReadline(appSpec, ev, nb) } //elvdoc:var before-readline // // A list of functions to call before each readline cycle. Each function is // called without any arguments. func initBeforeReadline(appSpec *cli.AppSpec, ev *eval.Evaler, nb eval.NsBuilder) { hook := newListVar(vals.EmptyList) nb.AddVar("before-readline", hook) appSpec.BeforeReadline = append(appSpec.BeforeReadline, func() { callHooks(ev, "$:before-readline", hook.Get().(vals.List)) }) } //elvdoc:var after-readline // // A list of functions to call after each readline cycle. Each function is // called with a single string argument containing the code that has been read. func initAfterReadline(appSpec *cli.AppSpec, ev *eval.Evaler, nb eval.NsBuilder) { hook := newListVar(vals.EmptyList) nb.AddVar("after-readline", hook) appSpec.AfterReadline = append(appSpec.AfterReadline, func(code string) { callHooks(ev, "$:after-readline", hook.Get().(vals.List), code) }) } //elvdoc:var add-cmd-filters // // List of filters to run before adding a command to history. // // A filter is a function that takes a command as argument and outputs // a boolean value. If any of the filters outputs `$false`, the // command is not saved to history, and the rest of the filters are // not run. The default value of this list contains a filter which // ignores command starts with space. func initAddCmdFilters(appSpec *cli.AppSpec, ev *eval.Evaler, nb eval.NsBuilder, s histutil.Store) { ignoreLeadingSpace := eval.NewGoFn("", func(s string) bool { return !strings.HasPrefix(s, " ") }) filters := newListVar(vals.MakeList(ignoreLeadingSpace)) nb.AddVar("add-cmd-filters", filters) appSpec.AfterReadline = append(appSpec.AfterReadline, func(code string) { if code != "" && callFilters(ev, "$:add-cmd-filters", filters.Get().(vals.List), code) { s.AddCmd(storedefs.Cmd{Text: code, Seq: -1}) } // TODO(xiaq): Handle the error. }) } //elvdoc:var global-binding // // Global keybindings, consulted for keys not handled by mode-specific bindings. // // See [Keybindings](#keybindings). func initGlobalBindings(appSpec *cli.AppSpec, nt notifier, ev *eval.Evaler, nb eval.NsBuilder) { bindingVar := newBindingVar(emptyBindingsMap) appSpec.GlobalBindings = newMapBindings(nt, ev, bindingVar) nb.AddVar("global-binding", bindingVar) } func callHooks(ev *eval.Evaler, name string, hook vals.List, args ...interface{}) { if hook.Len() == 0 { return } ports, cleanup := eval.PortsFromStdFiles(ev.ValuePrefix()) evalCfg := eval.EvalCfg{Ports: ports[:]} defer cleanup() i := -1 for it := hook.Iterator(); it.HasElem(); it.Next() { i++ name := fmt.Sprintf("%s[%d]", name, i) fn, ok := it.Elem().(eval.Callable) if !ok { // TODO(xiaq): This is not testable as it depends on stderr. // Make it testable. diag.Complainf(os.Stderr, "%s not function", name) continue } err := ev.Call(fn, eval.CallCfg{Args: args, From: name}, evalCfg) if err != nil { diag.ShowError(os.Stderr, err) } } } func callFilters(ev *eval.Evaler, name string, filters vals.List, args ...interface{}) bool { if filters.Len() == 0 { return true } i := -1 for it := filters.Iterator(); it.HasElem(); it.Next() { i++ name := fmt.Sprintf("%s[%d]", name, i) fn, ok := it.Elem().(eval.Callable) if !ok { // TODO(xiaq): This is not testable as it depends on stderr. // Make it testable. diag.Complainf(os.Stderr, "%s not function", name) continue } port1, collect, err := eval.CapturePort() if err != nil { diag.Complainf(os.Stderr, "cannot create pipe to run filter") return true } err = ev.Call(fn, eval.CallCfg{Args: args, From: name}, // TODO: Supply the Chan component of port 2. eval.EvalCfg{Ports: []*eval.Port{nil, port1, {File: os.Stderr}}}) out := collect() if err != nil { diag.Complainf(os.Stderr, "%s return error", name) continue } if len(out) != 1 { diag.Complainf(os.Stderr, "filter %s should only return $true or $false", name) continue } p, ok := out[0].(bool) if !ok { diag.Complainf(os.Stderr, "filter %s should return bool", name) continue } if !p { return false } } return true } func newIntVar(i int) vars.PtrVar { return vars.FromPtr(&i) } func newFloatVar(f float64) vars.PtrVar { return vars.FromPtr(&f) } func newBoolVar(b bool) vars.PtrVar { return vars.FromPtr(&b) } func newListVar(l vals.List) vars.PtrVar { return vars.FromPtr(&l) } func newMapVar(m vals.Map) vars.PtrVar { return vars.FromPtr(&m) } func newFnVar(c eval.Callable) vars.PtrVar { return vars.FromPtr(&c) } func newBindingVar(b bindingsMap) vars.PtrVar { return vars.FromPtr(&b) } elvish-0.17.0/pkg/edit/config_api_test.go000066400000000000000000000067341415471104000202720ustar00rootroot00000000000000package edit import ( "testing" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/store/storedefs" "src.elv.sh/pkg/ui" ) func TestBeforeReadline(t *testing.T) { f := setup(t, rc( `called = 0`, `edit:before-readline = [ { called = (+ $called 1) } ]`)) // Wait for UI to stabilize so that we can be sure that before-readline hooks // have been called. f.TestTTY(t, "~> ", term.DotHere) testGlobal(t, f.Evaler, "called", 1) } func TestAfterReadline(t *testing.T) { f := setup(t) evals(f.Evaler, `called = 0`, `called-with = ''`, `edit:after-readline = [ {|code| called = (+ $called 1); called-with = $code } ]`) // Wait for UI to stabilize so that we can be sure that after-readline hooks // are *not* called. f.TestTTY(t, "~> ", term.DotHere) testGlobal(t, f.Evaler, "called", "0") // Input "test code", press Enter and wait until the editor is done. feedInput(f.TTYCtrl, "test code\n") f.Wait() testGlobals(t, f.Evaler, map[string]interface{}{ "called": 1, "called-with": "test code", }) } func TestAddCmdFilters(t *testing.T) { cases := []struct { name string rc string input string wantHistory []storedefs.Cmd }{ // TODO: Enable the following two tests once error output can // be tested. // { // name: "non callable item", // rc: "edit:add-cmd-filters = [$false]", // input: "echo\n", // wantHistory: []string{"echo"}, // }, // { // name: "callback outputs nothing", // rc: "edit:add-cmd-filters = [{|_| }]", // input: "echo\n", // wantHistory: []string{"echo"}, // }, { name: "callback outputs true", rc: "edit:add-cmd-filters = [{|_| put $true }]", input: "echo\n", wantHistory: []storedefs.Cmd{{Text: "echo", Seq: 1}}, }, { name: "callback outputs false", rc: "edit:add-cmd-filters = [{|_| put $false }]", input: "echo\n", wantHistory: nil, }, { name: "false-true chain", rc: "edit:add-cmd-filters = [{|_| put $false } {|_| put $true }]", input: "echo\n", wantHistory: nil, }, { name: "true-false chain", rc: "edit:add-cmd-filters = [{|_| put $true } {|_| put $false }]", input: "echo\n", wantHistory: nil, }, { name: "positive", rc: "edit:add-cmd-filters = [{|cmd| ==s $cmd echo }]", input: "echo\n", wantHistory: []storedefs.Cmd{{Text: "echo", Seq: 1}}, }, { name: "negative", rc: "edit:add-cmd-filters = [{|cmd| ==s $cmd echo }]", input: "echo x\n", wantHistory: nil, }, { name: "default value", rc: "", input: " echo\n", wantHistory: nil, }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { f := setup(t, rc(c.rc)) feedInput(f.TTYCtrl, c.input) f.Wait() testCommands(t, f.Store, c.wantHistory...) }) } } func TestAddCmdFilters_SkipsRemainingOnFalse(t *testing.T) { f := setup(t, rc( `called = $false`, `@edit:add-cmd-filters = {|_| put $false } {|_| called = $true; put $true }`, )) feedInput(f.TTYCtrl, "echo\n") f.Wait() testCommands(t, f.Store) testGlobal(t, f.Evaler, "called", false) } func TestGlobalBindings(t *testing.T) { f := setup(t, rc( `var called = $false`, `edit:global-binding[Ctrl-X] = { set called = $true }`, )) f.TTYCtrl.Inject(term.K('X', ui.Ctrl)) f.TTYCtrl.Inject(term.K(ui.Enter)) f.Wait() testGlobal(t, f.Evaler, "called", true) } elvish-0.17.0/pkg/edit/editor.go000066400000000000000000000103501415471104000164100ustar00rootroot00000000000000// Package edit implements the line editor for Elvish. // // The line editor is based on the cli package, which implements a general, // Elvish-agnostic line editor, and multiple "addon" packages. This package // glues them together and provides Elvish bindings for them. package edit import ( _ "embed" "fmt" "sync" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/store/storedefs" ) // Editor is the interactive line editor for Elvish. type Editor struct { app cli.App ns *eval.Ns excMutex sync.RWMutex excList vals.List // Maybe move this to another type that represents the REPL cycle as a whole, not just the // read/edit portion represented by the Editor type. AfterCommand []func(src parse.Source, duration float64, err error) } // An interface that wraps notifyf and notifyError. It is only implemented by // the *Editor type; functions may take a notifier instead of *Editor argument // to make it clear that they do not depend on other parts of *Editor. type notifier interface { notifyf(format string, args ...interface{}) notifyError(ctx string, e error) } // NewEditor creates a new editor. The TTY is used for input and output. The // Evaler is used for syntax highlighting, completion, and calling callbacks. // The Store is used for saving and retrieving command and directory history. func NewEditor(tty cli.TTY, ev *eval.Evaler, st storedefs.Store) *Editor { // Declare the Editor with a nil App first; some initialization functions // require a notifier as an argument, but does not use it immediately. ed := &Editor{excList: vals.EmptyList} nb := eval.BuildNsNamed("edit") appSpec := cli.AppSpec{TTY: tty} hs, err := newHistStore(st) if err != nil { _ = err // TODO(xiaq): Report the error. } initHighlighter(&appSpec, ev) initMaxHeight(&appSpec, nb) initReadlineHooks(&appSpec, ev, nb) initAddCmdFilters(&appSpec, ev, nb, hs) initGlobalBindings(&appSpec, ed, ev, nb) initInsertAPI(&appSpec, ed, ev, nb) initPrompts(&appSpec, ed, ev, nb) ed.app = cli.NewApp(appSpec) initExceptionsAPI(ed, nb) initVarsAPI(ed, nb) initCommandAPI(ed, ev, nb) initListings(ed, ev, st, hs, nb) initNavigation(ed, ev, nb) initCompletion(ed, ev, nb) initHistWalk(ed, ev, hs, nb) initInstant(ed, ev, nb) initMinibuf(ed, ev, nb) initRepl(ed, ev, nb) initBufferBuiltins(ed.app, nb) initTTYBuiltins(ed.app, tty, nb) initMiscBuiltins(ed.app, nb) initStateAPI(ed.app, nb) initStoreAPI(ed.app, nb, hs) ed.ns = nb.Ns() initElvishState(ev, ed.ns) return ed } //elvdoc:var exceptions // // A list of exceptions thrown from callbacks such as prompts. Useful for // examining tracebacks and other metadata. func initExceptionsAPI(ed *Editor, nb eval.NsBuilder) { nb.AddVar("exceptions", vars.FromPtrWithMutex(&ed.excList, &ed.excMutex)) } //go:embed init.elv var initElv string // Initialize the `edit` module by executing the pre-defined Elvish code for the module. func initElvishState(ev *eval.Evaler, ns *eval.Ns) { src := parse.Source{Name: "[init.elv]", Code: initElv} err := ev.Eval(src, eval.EvalCfg{Global: ns}) if err != nil { panic(err) } } // ReadCode reads input from the user. func (ed *Editor) ReadCode() (string, error) { return ed.app.ReadCode() } // Notify adds a note to the notification buffer. func (ed *Editor) Notify(note string) { ed.app.Notify(note) } // RunAfterCommandHooks runs callbacks involving the interactive completion of a command line. func (ed *Editor) RunAfterCommandHooks(src parse.Source, duration float64, err error) { for _, f := range ed.AfterCommand { f(src, duration, err) } } // Ns returns a namespace for manipulating the editor from Elvish code. // // See https://elv.sh/ref/edit.html for the Elvish API. func (ed *Editor) Ns() *eval.Ns { return ed.ns } func (ed *Editor) notifyf(format string, args ...interface{}) { ed.app.Notify(fmt.Sprintf(format, args...)) } func (ed *Editor) notifyError(ctx string, e error) { if exc, ok := e.(eval.Exception); ok { ed.excMutex.Lock() defer ed.excMutex.Unlock() ed.excList = ed.excList.Cons(exc) ed.notifyf("[%v error] %v\n"+ `see stack trace with "show $edit:exceptions[%d]"`, ctx, e, ed.excList.Len()-1) } else { ed.notifyf("[%v error] %v", ctx, e) } } elvish-0.17.0/pkg/edit/editor_test.go000066400000000000000000000014541415471104000174540ustar00rootroot00000000000000package edit import ( "reflect" "testing" "src.elv.sh/pkg/store/storedefs" ) func TestEditor_AddsHistoryAfterAccepting(t *testing.T) { f := setup(t) feedInput(f.TTYCtrl, "echo x\n") f.Wait() testCommands(t, f.Store, storedefs.Cmd{Text: "echo x", Seq: 1}) } func TestEditor_DoesNotAddEmptyCommandToHistory(t *testing.T) { f := setup(t) feedInput(f.TTYCtrl, "\n") f.Wait() testCommands(t, f.Store /* no commands */) } func TestEditor_Notify(t *testing.T) { f := setup(t) f.Editor.Notify("note") f.TestTTYNotes(t, "note") } func testCommands(t *testing.T, store storedefs.Store, wantCmds ...storedefs.Cmd) { t.Helper() cmds, err := store.CmdsWithSeq(0, 1024) if err != nil { panic(err) } if !reflect.DeepEqual(cmds, wantCmds) { t.Errorf("got cmds %v, want %v", cmds, wantCmds) } } elvish-0.17.0/pkg/edit/filter/000077500000000000000000000000001415471104000160615ustar00rootroot00000000000000elvish-0.17.0/pkg/edit/filter/compile.go000066400000000000000000000053741415471104000200510ustar00rootroot00000000000000// Package filter implements the Elvish filter DSL. // // The filter DSL is a subset of Elvish's expression syntax, and is useful for // filtering a list of items. It is currently used in the listing modes of the // interactive editor. package filter import ( "errors" "regexp" "strings" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/parse/cmpd" ) // Compile parses and compiles a filter. func Compile(q string) (Filter, error) { qn, errParse := parseFilter(q) filter, errCompile := compileFilter(qn) return filter, diag.Errors(errParse, errCompile) } func parseFilter(q string) (*parse.Filter, error) { qn := &parse.Filter{} err := parse.ParseAs(parse.Source{Name: "filter", Code: q}, qn, parse.Config{}) return qn, err } func compileFilter(qn *parse.Filter) (Filter, error) { if len(qn.Opts) > 0 { return nil, notSupportedError{"option"} } qs, err := compileCompounds(qn.Args) if err != nil { return nil, err } return andFilter{qs}, nil } func compileCompounds(ns []*parse.Compound) ([]Filter, error) { qs := make([]Filter, len(ns)) for i, n := range ns { q, err := compileCompound(n) if err != nil { return nil, err } qs[i] = q } return qs, nil } func compileCompound(n *parse.Compound) (Filter, error) { if pn, ok := cmpd.Primary(n); ok { switch pn.Type { case parse.Bareword, parse.SingleQuoted, parse.DoubleQuoted: s := pn.Value ignoreCase := s == strings.ToLower(s) return substringFilter{s, ignoreCase}, nil case parse.List: return compileList(pn.Elements) } } return nil, notSupportedError{cmpd.Shape(n)} } var errEmptySubfilter = errors.New("empty subfilter") func compileList(elems []*parse.Compound) (Filter, error) { if len(elems) == 0 { return nil, errEmptySubfilter } head, ok := cmpd.StringLiteral(elems[0]) if !ok { return nil, notSupportedError{"non-literal subfilter head"} } switch head { case "re": if len(elems) == 1 { return nil, notSupportedError{"re subfilter with no argument"} } if len(elems) > 2 { return nil, notSupportedError{"re subfilter with two or more arguments"} } arg := elems[1] s, ok := cmpd.StringLiteral(arg) if !ok { return nil, notSupportedError{"re subfilter with " + cmpd.Shape(arg)} } p, err := regexp.Compile(s) if err != nil { return nil, err } return regexpFilter{p}, nil case "and": qs, err := compileCompounds(elems[1:]) if err != nil { return nil, err } return andFilter{qs}, nil case "or": qs, err := compileCompounds(elems[1:]) if err != nil { return nil, err } return orFilter{qs}, nil default: return nil, notSupportedError{"head " + parse.SourceText(elems[0])} } } type notSupportedError struct{ what string } func (err notSupportedError) Error() string { return err.what + " not supported" } elvish-0.17.0/pkg/edit/filter/compile_test.go000066400000000000000000000127331415471104000211050ustar00rootroot00000000000000package filter_test import ( "testing" "src.elv.sh/pkg/edit/filter" "src.elv.sh/pkg/parse" ) func TestCompile(t *testing.T) { test(t, That("empty filter matches anything"). Filter("").Matches("foo", "bar", " ", ""), That("bareword matches any string containing it"). Filter("foo").Matches("foobar", "afoo").DoesNotMatch("", "faoo"), That("bareword is case-insensitive is filter is all lower case"). Filter("foo").Matches("FOO", "Foo", "FOObar").DoesNotMatch("", "faoo"), That("bareword is case-sensitive is filter is not all lower case"). Filter("Foo").Matches("Foobar").DoesNotMatch("foo", "FOO"), That("double quoted string works like bareword"). Filter(`"foo"`).Matches("FOO", "Foo", "FOObar").DoesNotMatch("", "faoo"), That("single quoted string works like bareword"). Filter(`'foo'`).Matches("FOO", "Foo", "FOObar").DoesNotMatch("", "faoo"), That("space-separated words work like an AND filter"). Filter("foo bar"). Matches("foobar", "bar foo", "foo lorem ipsum bar"). DoesNotMatch("foo", "bar", ""), That("quoted string can be used when string contains spaces"). Filter(`"foo bar"`). Matches("__foo bar xyz"). DoesNotMatch("foobar"), That("AND filter matches if all components match"). Filter("[and foo bar]").Matches("foobar", "bar foo").DoesNotMatch("foo"), That("OR filter matches if any component matches"). Filter("[or foo bar]").Matches("foo", "bar", "foobar").DoesNotMatch(""), That("RE filter uses component as regular expression to match"). Filter("[re f..]").Matches("foo", "f..").DoesNotMatch("fo", ""), // Invalid queries That("empty list is invalid"). Filter("[]").DoesNotCompile("empty subfilter"), That("starting list with non-literal is invalid"). Filter("[[foo] bar]"). DoesNotCompile("non-literal subfilter head not supported"), That("RE filter with no argument is invalid"). Filter("[re]"). DoesNotCompile("re subfilter with no argument not supported"), That("RE filter with two or more arguments is invalid"). Filter("[re foo bar]"). DoesNotCompile("re subfilter with two or more arguments not supported"), That("RE filter with invalid regular expression is invalid"). Filter("[re '[']"). DoesNotCompile("error parsing regexp: missing closing ]: `[`"), That("invalid syntax results in parse error"). Filter("[and").DoesNotParse("parse error: 4-4 in filter: should be ']'"), // Unsupported for now, but may be in future That("options are not supported yet"). Filter("foo &k=v").DoesNotCompile("option not supported"), That("compound expressions are not supported yet"). Filter(`a"foo"`).DoesNotCompile("compound expression not supported"), That("indexing expressions are not supported yet"). Filter("foo[0]").DoesNotCompile("indexing expression not supported"), That("variable references are not supported yet"). Filter("$a"). DoesNotCompile("primary expression of type Variable not supported"), That("variable references in RE subfilter are not supported yet"). Filter("[re $a]"). DoesNotCompile("re subfilter with primary expression of type Variable not supported"), That("variable references in AND subfilter are not supported yet"). Filter("[and $a]"). DoesNotCompile("primary expression of type Variable not supported"), That("variable references in OR subfilter are not supported yet"). Filter("[or $a]"). DoesNotCompile("primary expression of type Variable not supported"), That("other subqueries are not supported yet"). Filter("[other foo bar]"). DoesNotCompile("head other not supported"), ) } func test(t *testing.T, tests ...testCase) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { q, err := filter.Compile(test.filter) if errType := getErrorType(err); errType != test.errorType { t.Errorf("%q should have %s, but has %s", test.filter, test.errorType, errType) } if err != nil { if err.Error() != test.errorMessage { t.Errorf("%q should have error message %q, but is %q", test.filter, test.errorMessage, err) } return } for _, s := range test.matches { ok := q.Match(s) if !ok { t.Errorf("%q should match %q, but doesn't", test.filter, s) } } for _, s := range test.doesntMatch { ok := q.Match(s) if ok { t.Errorf("%q shouldn't match %q, but does", test.filter, s) } } }) } } type testCase struct { name string filter string matches []string doesntMatch []string errorType errorType errorMessage string } func That(name string) testCase { return testCase{name: name} } func (t testCase) Filter(q string) testCase { t.filter = q return t } func (t testCase) DoesNotParse(message string) testCase { t.errorType = parseError t.errorMessage = message return t } func (t testCase) DoesNotCompile(message string) testCase { t.errorType = compileError t.errorMessage = message return t } func (t testCase) Matches(s ...string) testCase { t.matches = s return t } func (t testCase) DoesNotMatch(s ...string) testCase { t.doesntMatch = s return t } type errorType uint const ( noError errorType = iota parseError compileError ) func getErrorType(err error) errorType { switch err.(type) { case nil: return noError case *parse.Error: return parseError default: return compileError } } func (et errorType) String() string { switch et { case noError: return "no error" case parseError: return "parse error" case compileError: return "compile error" default: panic("unreachable") } } elvish-0.17.0/pkg/edit/filter/filter.go000066400000000000000000000015451415471104000177020ustar00rootroot00000000000000package filter import ( "regexp" "strings" ) // Filter represents a compiled filter, which can be used to match text. type Filter interface { Match(s string) bool } type andFilter struct { queries []Filter } func (aq andFilter) Match(s string) bool { for _, q := range aq.queries { if !q.Match(s) { return false } } return true } type orFilter struct { queries []Filter } func (oq orFilter) Match(s string) bool { for _, q := range oq.queries { if q.Match(s) { return true } } return false } type substringFilter struct { pattern string ignoreCase bool } func (sq substringFilter) Match(s string) bool { if sq.ignoreCase { s = strings.ToLower(s) } return strings.Contains(s, sq.pattern) } type regexpFilter struct { pattern *regexp.Regexp } func (rq regexpFilter) Match(s string) bool { return rq.pattern.MatchString(s) } elvish-0.17.0/pkg/edit/filter/highlight.go000066400000000000000000000030101415471104000203510ustar00rootroot00000000000000package filter import ( "strings" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/parse/cmpd" "src.elv.sh/pkg/ui" ) func Highlight(q string) (ui.Text, []error) { n, _ := parseFilter(q) w := walker{} w.walk(n) text := ui.StyleRegions(q, w.regions) // TODO: Add errors. return text, nil } type walker struct { regions []ui.StylingRegion } func (w *walker) emit(r diag.Ranger, s ui.Styling) { region := ui.StylingRegion{Ranging: r.Range(), Styling: s} w.regions = append(w.regions, region) } func (w *walker) walk(n parse.Node) { switch n := n.(type) { case *parse.Sep: w.walkSep(n) case *parse.Primary: w.walkPrimary(n) } for _, ch := range parse.Children(n) { w.walk(ch) } } func (w *walker) walkSep(n *parse.Sep) { text := parse.SourceText(n) trimmed := strings.TrimLeftFunc(text, parse.IsWhitespace) if trimmed == "" { // Whitespace; nothing to do. return } // Metacharacter; style it bold. w.emit(n, ui.Bold) } func (w *walker) walkPrimary(n *parse.Primary) { switch n.Type { case parse.Bareword: // Barewords are unstyled. case parse.SingleQuoted, parse.DoubleQuoted: w.emit(n, ui.FgYellow) case parse.List: if len(n.Elements) == 0 { w.emit(n, ui.FgRed) return } headNode := n.Elements[0] head, ok := cmpd.StringLiteral(headNode) if !ok { w.emit(headNode, ui.FgRed) } switch head { case "re", "and", "or": w.emit(headNode, ui.FgGreen) default: w.emit(headNode, ui.FgRed) } default: // Unsupported primary type. w.emit(n, ui.FgRed) } } elvish-0.17.0/pkg/edit/filter/highlight_test.go000066400000000000000000000021711415471104000214170ustar00rootroot00000000000000package filter_test import ( "reflect" "testing" "src.elv.sh/pkg/edit/filter" "src.elv.sh/pkg/ui" ) var highlightTests = []struct { name string q string want ui.Text }{ { name: "quoted string", q: `'a'`, want: ui.T(`'a'`, ui.FgYellow), }, { name: "unsupported primary", q: `$a`, want: ui.T(`$a`, ui.FgRed), }, { name: "supported list form", q: `[re a]`, want: ui.Concat( ui.T("[", ui.Bold), ui.T("re", ui.FgGreen), ui.T(" a"), ui.T("]", ui.Bold)), }, { name: "empty list form", q: `[]`, want: ui.T("[]", ui.FgRed), }, { name: "unsupported list form", q: `[bad]`, want: ui.Concat( ui.T("[", ui.Bold), ui.T("bad", ui.FgRed), ui.T("]", ui.Bold)), }, { name: "unsupported primary as head of list form", q: `[$a]`, want: ui.Concat( ui.T("[", ui.Bold), ui.T("$a", ui.FgRed), ui.T("]", ui.Bold)), }, } func TestHighlight(t *testing.T) { for _, test := range highlightTests { t.Run(test.name, func(t *testing.T) { got, _ := filter.Highlight(test.q) if !reflect.DeepEqual(got, test.want) { t.Errorf("got %s, want %s", got, test.want) } }) } } elvish-0.17.0/pkg/edit/highlight.go000066400000000000000000000044331415471104000170760ustar00rootroot00000000000000package edit import ( "os" "os/exec" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/edit/highlight" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/fsutil" "src.elv.sh/pkg/parse" ) func initHighlighter(appSpec *cli.AppSpec, ev *eval.Evaler) { appSpec.Highlighter = highlight.NewHighlighter(highlight.Config{ Check: func(tree parse.Tree) error { return check(ev, tree) }, HasCommand: func(cmd string) bool { return hasCommand(ev, cmd) }, }) } func check(ev *eval.Evaler, tree parse.Tree) error { err := ev.CheckTree(tree, nil) if err == nil { return nil } return err } func hasCommand(ev *eval.Evaler, cmd string) bool { if eval.IsBuiltinSpecial[cmd] { return true } if fsutil.DontSearch(cmd) { return isDirOrExecutable(cmd) || hasExternalCommand(cmd) } sigil, qname := eval.SplitSigil(cmd) if sigil != "" { // The @ sign is only valid when referring to external commands. return hasExternalCommand(cmd) } first, rest := eval.SplitQName(qname) switch { case rest == "": // Unqualified name; try builtin and global. if hasFn(ev.Builtin(), first) || hasFn(ev.Global(), first) { return true } case first == "e:": return hasExternalCommand(rest) default: // Qualified name. Find the top-level module first. if hasQualifiedFn(ev, first, rest) { return true } } // If all failed, it can still be an external command. return hasExternalCommand(cmd) } func hasQualifiedFn(ev *eval.Evaler, firstNs string, rest string) bool { if rest == "" { return false } modVal, ok := ev.Global().Index(firstNs) if !ok { modVal, ok = ev.Builtin().Index(firstNs) if !ok { return false } } mod, ok := modVal.(*eval.Ns) if !ok { return false } segs := eval.SplitQNameSegs(rest) for _, seg := range segs[:len(segs)-1] { modVal, ok = mod.Index(seg) if !ok { return false } mod, ok = modVal.(*eval.Ns) if !ok { return false } } return hasFn(mod, segs[len(segs)-1]) } func hasFn(ns *eval.Ns, name string) bool { fnVar, ok := ns.Index(name + eval.FnSuffix) if !ok { return false } _, ok = fnVar.(eval.Callable) return ok } func isDirOrExecutable(fname string) bool { stat, err := os.Stat(fname) return err == nil && (stat.IsDir() || stat.Mode()&0111 != 0) } func hasExternalCommand(cmd string) bool { _, err := exec.LookPath(cmd) return err == nil } elvish-0.17.0/pkg/edit/highlight/000077500000000000000000000000001415471104000165435ustar00rootroot00000000000000elvish-0.17.0/pkg/edit/highlight/highlight.go000066400000000000000000000064411415471104000210460ustar00rootroot00000000000000// Package highlight provides an Elvish syntax highlighter. package highlight import ( "time" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/ui" ) // Config keeps configuration for highlighting code. type Config struct { Check func(n parse.Tree) error HasCommand func(name string) bool } // Information collected about a command region, used for asynchronous // highlighting. type cmdRegion struct { seg int cmd string } // MaxBlockForLate specifies the maximum wait time to block for late results. // It can be changed for test cases. var MaxBlockForLate = 10 * time.Millisecond // Highlights a piece of Elvish code. func highlight(code string, cfg Config, lateCb func(ui.Text)) (ui.Text, []error) { var errors []error var errorRegions []region tree, errParse := parse.Parse(parse.Source{Name: "[tty]", Code: code}, parse.Config{}) if errParse != nil { for _, err := range errParse.(*parse.Error).Entries { if err.Context.From != len(code) { errors = append(errors, err) errorRegions = append(errorRegions, region{ err.Context.From, err.Context.To, semanticRegion, errorRegion}) } } } if cfg.Check != nil { err := cfg.Check(tree) if r, ok := err.(diag.Ranger); ok && r.Range().From != len(code) { errors = append(errors, err) errorRegions = append(errorRegions, region{ r.Range().From, r.Range().To, semanticRegion, errorRegion}) } } var text ui.Text regions := getRegionsInner(tree.Root) regions = append(regions, errorRegions...) regions = fixRegions(regions) lastEnd := 0 var cmdRegions []cmdRegion for _, r := range regions { if r.begin > lastEnd { // Add inter-region text. text = append(text, &ui.Segment{Text: code[lastEnd:r.begin]}) } regionCode := code[r.begin:r.end] var styling ui.Styling if r.typ == commandRegion { if cfg.HasCommand != nil { // Do not highlight now, but collect the index of the region and the // segment. cmdRegions = append(cmdRegions, cmdRegion{len(text), regionCode}) } else { // Treat all commands as good commands. styling = stylingForGoodCommand } } else { styling = stylingFor[r.typ] } seg := &ui.Segment{Text: regionCode} if styling != nil { seg = ui.StyleSegment(seg, styling) } text = append(text, seg) lastEnd = r.end } if len(code) > lastEnd { // Add text after the last region as unstyled. text = append(text, &ui.Segment{Text: code[lastEnd:]}) } if cfg.HasCommand != nil && len(cmdRegions) > 0 { // Launch a goroutine to style command regions asynchronously. lateCh := make(chan ui.Text) go func() { newText := text.Clone() for _, cmdRegion := range cmdRegions { var styling ui.Styling if cfg.HasCommand(cmdRegion.cmd) { styling = stylingForGoodCommand } else { styling = stylingForBadCommand } seg := &newText[cmdRegion.seg] *seg = ui.StyleSegment(*seg, styling) } lateCh <- newText }() // Block a short while for the late text to arrive, in order to reduce // flickering. Otherwise, return the text already computed, and pass the // late result to lateCb in another goroutine. select { case late := <-lateCh: return late, errors case <-time.After(MaxBlockForLate): go func() { lateCb(<-lateCh) }() return text, errors } } return text, errors } elvish-0.17.0/pkg/edit/highlight/highlighter.go000066400000000000000000000025341415471104000213740ustar00rootroot00000000000000package highlight import ( "sync" "src.elv.sh/pkg/ui" ) const latesBufferSize = 128 // Highlighter is a code highlighter that can deliver results asynchronously. type Highlighter struct { cfg Config state state lates chan struct{} } type state struct { sync.Mutex code string styledCode ui.Text errors []error } func NewHighlighter(cfg Config) *Highlighter { return &Highlighter{cfg, state{}, make(chan struct{}, latesBufferSize)} } // Get returns the highlighted code and static errors found in the code. func (hl *Highlighter) Get(code string) (ui.Text, []error) { hl.state.Lock() defer hl.state.Unlock() if code == hl.state.code { return hl.state.styledCode, hl.state.errors } lateCb := func(styledCode ui.Text) { hl.state.Lock() if hl.state.code != code { // Late result was delivered after code has changed. Unlock and // return. hl.state.Unlock() return } hl.state.styledCode = styledCode // The channel send below might block, so unlock the state first. hl.state.Unlock() hl.lates <- struct{}{} } styledCode, errors := highlight(code, hl.cfg, lateCb) hl.state.code = code hl.state.styledCode = styledCode hl.state.errors = errors return styledCode, errors } // LateUpdates returns a channel for notifying late updates. func (hl *Highlighter) LateUpdates() <-chan struct{} { return hl.lates } elvish-0.17.0/pkg/edit/highlight/highlighter_test.go000066400000000000000000000144571415471104000224420ustar00rootroot00000000000000package highlight import ( "reflect" "testing" "time" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/testutil" "src.elv.sh/pkg/tt" "src.elv.sh/pkg/ui" ) var any = anyMatcher{} var noErrors []error var styles = ui.RuneStylesheet{ '?': ui.Stylings(ui.FgBrightWhite, ui.BgRed), '$': ui.FgMagenta, '\'': ui.FgYellow, 'v': ui.FgGreen, } func TestHighlighter_HighlightRegions(t *testing.T) { // Force commands to be delivered synchronously. MaxBlockForLate = testutil.Scaled(100 * time.Millisecond) hl := NewHighlighter(Config{ HasCommand: func(name string) bool { return name == "ls" }, }) tt.Test(t, tt.Fn("hl.Get", hl.Get), tt.Table{ Args("ls").Rets( ui.MarkLines( "ls", styles, "vv", ), noErrors), Args(" ls\n").Rets( ui.MarkLines( " ls\n", styles, " vv"), noErrors), Args("ls $x 'y'").Rets( ui.MarkLines( "ls $x 'y'", styles, "vv $$ '''"), noErrors), // Non-bareword commands do not go through command highlighting. Args("'ls'").Rets(ui.T("'ls'", ui.FgYellow)), Args("a$x").Rets( ui.MarkLines( "a$x", styles, " $$"), noErrors, ), }) } func TestHighlighter_ParseErrors(t *testing.T) { hl := NewHighlighter(Config{}) tt.Test(t, tt.Fn("hl.Get", hl.Get), tt.Table{ // Parse error is highlighted and returned Args("ls ]").Rets( ui.MarkLines( "ls ]", styles, "vv ?"), matchErrors(parseErrorMatcher{3, 4})), // Errors at the end are ignored Args("ls $").Rets(any, noErrors), Args("ls [").Rets(any, noErrors), }) } func TestHighlighter_CheckErrors(t *testing.T) { var checkError error // Make a highlighter whose Check callback returns checkError. hl := NewHighlighter(Config{ Check: func(parse.Tree) error { return checkError }}) getWithCheckError := func(code string, err error) (ui.Text, []error) { checkError = err return hl.Get(code) } tt.Test(t, tt.Fn("getWithCheckError", getWithCheckError), tt.Table{ // Check error is highlighted and returned Args("code 1", fakeCheckError{5, 6}).Rets( ui.MarkLines( "code 1", styles, "vvvv ?"), []error{fakeCheckError{5, 6}}), // Check errors at the end are ignored Args("code 2", fakeCheckError{6, 6}). Rets(any, noErrors), }) } type c struct { given string wantInitial ui.Text wantLate ui.Text mustLate bool } var lateTimeout = testutil.Scaled(100 * time.Millisecond) func testThat(t *testing.T, hl *Highlighter, c c) { initial, _ := hl.Get(c.given) if !reflect.DeepEqual(c.wantInitial, initial) { t.Errorf("want %v from initial Get, got %v", c.wantInitial, initial) } if c.wantLate == nil { return } select { case <-hl.LateUpdates(): late, _ := hl.Get(c.given) if !reflect.DeepEqual(c.wantLate, late) { t.Errorf("want %v from late Get, got %v", c.wantLate, late) } case <-time.After(lateTimeout): t.Errorf("want %v from LateUpdates, but timed out after %v", c.wantLate, lateTimeout) } } func TestHighlighter_HasCommand_LateResult_Async(t *testing.T) { // When the HasCommand callback takes longer than maxBlockForLate, late // results are delivered asynchronously. MaxBlockForLate = testutil.Scaled(time.Millisecond) hl := NewHighlighter(Config{ // HasCommand is slow and only recognizes "ls". HasCommand: func(cmd string) bool { time.Sleep(testutil.Scaled(10 * time.Millisecond)) return cmd == "ls" }}) testThat(t, hl, c{ given: "ls", wantInitial: ui.T("ls"), wantLate: ui.T("ls", ui.FgGreen), }) testThat(t, hl, c{ given: "echo", wantInitial: ui.T("echo"), wantLate: ui.T("echo", ui.FgRed), }) } func TestHighlighter_HasCommand_LateResult_Sync(t *testing.T) { // When the HasCommand callback takes shorter than maxBlockForLate, late // results are delivered asynchronously. MaxBlockForLate = testutil.Scaled(100 * time.Millisecond) hl := NewHighlighter(Config{ // HasCommand is fast and only recognizes "ls". HasCommand: func(cmd string) bool { time.Sleep(testutil.Scaled(time.Millisecond)) return cmd == "ls" }}) testThat(t, hl, c{ given: "ls", wantInitial: ui.T("ls", ui.FgGreen), }) testThat(t, hl, c{ given: "echo", wantInitial: ui.T("echo", ui.FgRed), }) } func TestHighlighter_HasCommand_LateResultOutOfOrder(t *testing.T) { // When late results are delivered out of order, the ones that do not match // the current code are dropped. In this test, hl.Get is called with "l" // first and then "ls". The late result for "l" is delivered after that of // "ls" and is dropped. // Make sure that the HasCommand callback takes longer than maxBlockForLate. MaxBlockForLate = testutil.Scaled(time.Millisecond) hlSecond := make(chan struct{}) hl := NewHighlighter(Config{ HasCommand: func(cmd string) bool { if cmd == "l" { // Make sure that the second highlight has been requested before // returning. <-hlSecond time.Sleep(testutil.Scaled(10 * time.Millisecond)) return false } time.Sleep(testutil.Scaled(10 * time.Millisecond)) close(hlSecond) return cmd == "ls" }}) hl.Get("l") testThat(t, hl, c{ given: "ls", wantInitial: ui.T("ls"), wantLate: ui.T("ls", ui.FgGreen), mustLate: true, }) // Make sure that no more late updates are delivered. select { case late := <-hl.LateUpdates(): t.Errorf("want nothing from LateUpdates, got %v", late) case <-time.After(testutil.Scaled(50 * time.Millisecond)): // We have waited for 50 ms and there are no late updates; test passes. } } // Matchers. type anyMatcher struct{} func (anyMatcher) Match(tt.RetValue) bool { return true } type errorsMatcher struct{ matchers []tt.Matcher } func (m errorsMatcher) Match(v tt.RetValue) bool { errs := v.([]error) if len(errs) != len(m.matchers) { return false } for i, matcher := range m.matchers { if !matcher.Match(errs[i]) { return false } } return true } func matchErrors(m ...tt.Matcher) errorsMatcher { return errorsMatcher{m} } type parseErrorMatcher struct{ begin, end int } func (m parseErrorMatcher) Match(v tt.RetValue) bool { err := v.(*diag.Error) return m.begin == err.Context.From && m.end == err.Context.To } // Fake check error, used in tests for check callback. type fakeCheckError struct{ from, to int } func (e fakeCheckError) Range() diag.Ranging { return diag.Ranging{From: e.from, To: e.to} } func (fakeCheckError) Error() string { return "fake check error" } elvish-0.17.0/pkg/edit/highlight/regions.go000066400000000000000000000154101415471104000205410ustar00rootroot00000000000000package highlight import ( "sort" "strings" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/parse" ) var sourceText = parse.SourceText // Represents a region to be highlighted. type region struct { begin int end int // Regions can be lexical or semantic. Lexical regions always correspond to // a leaf node in the parse tree, either a parse.Primary node or a parse.Sep // node. Semantic regions may span several leaves and override all lexical // regions in it. kind regionKind // In lexical regions for Primary nodes, this field corresponds to the Type // field of the node (e.g. "bareword", "single-quoted"). In lexical regions // for Sep nodes, this field is simply the source text itself (e.g. "(", // "|"), except for comments, which have typ == "comment". // // In semantic regions, this field takes a value from a fixed list (see // below). typ string } type regionKind int // Region kinds. const ( lexicalRegion regionKind = iota semanticRegion ) // Lexical region types. const ( barewordRegion = "bareword" singleQuotedRegion = "single-quoted" doubleQuotedRegion = "double-quoted" variableRegion = "variable" // Could also be semantic. wildcardRegion = "wildcard" tildeRegion = "tilde" // A comment region. Note that this is the only type of Sep leaf node that // is not identified by its text. commentRegion = "comment" ) // Semantic region types. const ( // A region when a string literal (bareword, single-quoted or double-quoted) // appears as a command. commandRegion = "command" // A region for keywords in special forms, like "else" in an "if" form. keywordRegion = "keyword" // A region of parse or compilation error. errorRegion = "error" ) func getRegions(n parse.Node) []region { regions := getRegionsInner(n) regions = fixRegions(regions) return regions } func getRegionsInner(n parse.Node) []region { var regions []region emitRegions(n, func(n parse.Node, kind regionKind, typ string) { regions = append(regions, region{n.Range().From, n.Range().To, kind, typ}) }) return regions } func fixRegions(regions []region) []region { // Sort regions by the begin position, putting semantic regions before // lexical regions. sort.Slice(regions, func(i, j int) bool { if regions[i].begin < regions[j].begin { return true } if regions[i].begin == regions[j].begin { return regions[i].kind == semanticRegion && regions[j].kind == lexicalRegion } return false }) // Remove overlapping regions, preferring the ones that appear earlier. var newRegions []region lastEnd := 0 for _, r := range regions { if r.begin < lastEnd { continue } newRegions = append(newRegions, r) lastEnd = r.end } return newRegions } func emitRegions(n parse.Node, f func(parse.Node, regionKind, string)) { switch n := n.(type) { case *parse.Form: emitRegionsInForm(n, f) case *parse.Primary: emitRegionsInPrimary(n, f) case *parse.Sep: emitRegionsInSep(n, f) } for _, child := range parse.Children(n) { emitRegions(child, f) } } func emitRegionsInForm(n *parse.Form, f func(parse.Node, regionKind, string)) { // Left hands of temporary assignments. for _, an := range n.Assignments { if an.Left != nil && an.Left.Head != nil { f(an.Left.Head, semanticRegion, variableRegion) } } if n.Head == nil { return } // Special forms. // TODO: This only highlights bareword special commands, however currently // quoted special commands are also possible (e.g `"if" $true { }` is // accepted). head := sourceText(n.Head) switch head { case "var", "set": emitRegionsInVarSet(n, f) case "if": emitRegionsInIf(n, f) case "for": emitRegionsInFor(n, f) case "try": emitRegionsInTry(n, f) } if !eval.IsBuiltinSpecial[head] { for i, arg := range n.Args { if parse.SourceText(arg) == "=" { // Highlight left hands of legacy assignment form. emitVariableRegion(n.Head, f) for j := 0; j < i; j++ { emitVariableRegion(n.Args[j], f) } return } } } if isBarewordCompound(n.Head) { f(n.Head, semanticRegion, commandRegion) } } func emitRegionsInVarSet(n *parse.Form, f func(parse.Node, regionKind, string)) { // Highlight all LHS, and = as a keyword. for _, arg := range n.Args { if parse.SourceText(arg) == "=" { f(arg, semanticRegion, keywordRegion) break } emitVariableRegion(arg, f) } } func emitVariableRegion(n *parse.Compound, f func(parse.Node, regionKind, string)) { // Only handle valid LHS here. Invalid LHS will result in a compile error // and highlighted as an error accordingly. if n != nil && len(n.Indexings) == 1 && n.Indexings[0].Head != nil { f(n.Indexings[0].Head, semanticRegion, variableRegion) } } func isBarewordCompound(n *parse.Compound) bool { return len(n.Indexings) == 1 && len(n.Indexings[0].Indices) == 0 && n.Indexings[0].Head.Type == parse.Bareword } func emitRegionsInIf(n *parse.Form, f func(parse.Node, regionKind, string)) { // Highlight all "elif" and "else". for i := 2; i < len(n.Args); i += 2 { arg := n.Args[i] if s := sourceText(arg); s == "elif" || s == "else" { f(arg, semanticRegion, keywordRegion) } } } func emitRegionsInFor(n *parse.Form, f func(parse.Node, regionKind, string)) { // Highlight the iterating variable. if 0 < len(n.Args) && len(n.Args[0].Indexings) > 0 { f(n.Args[0].Indexings[0].Head, semanticRegion, variableRegion) } // Highlight "else". if 3 < len(n.Args) && sourceText(n.Args[3]) == "else" { f(n.Args[3], semanticRegion, keywordRegion) } } func emitRegionsInTry(n *parse.Form, f func(parse.Node, regionKind, string)) { // Highlight "except", the exception variable after it, "else" and // "finally". i := 1 matchKW := func(text string) bool { if i < len(n.Args) && sourceText(n.Args[i]) == text { f(n.Args[i], semanticRegion, keywordRegion) return true } return false } if matchKW("except") { if i+1 < len(n.Args) && len(n.Args[i+1].Indexings) > 0 { f(n.Args[i+1], semanticRegion, variableRegion) } i += 3 } if matchKW("else") { i += 2 } matchKW("finally") } func emitRegionsInPrimary(n *parse.Primary, f func(parse.Node, regionKind, string)) { switch n.Type { case parse.Bareword: f(n, lexicalRegion, barewordRegion) case parse.SingleQuoted: f(n, lexicalRegion, singleQuotedRegion) case parse.DoubleQuoted: f(n, lexicalRegion, doubleQuotedRegion) case parse.Variable: f(n, lexicalRegion, variableRegion) case parse.Wildcard: f(n, lexicalRegion, wildcardRegion) case parse.Tilde: f(n, lexicalRegion, tildeRegion) } } func emitRegionsInSep(n *parse.Sep, f func(parse.Node, regionKind, string)) { text := sourceText(n) trimmed := strings.TrimLeftFunc(text, parse.IsWhitespace) switch { case trimmed == "": // Don't do anything; whitespaces do not get highlighted. case strings.HasPrefix(trimmed, "#"): f(n, lexicalRegion, commentRegion) default: f(n, lexicalRegion, text) } } elvish-0.17.0/pkg/edit/highlight/regions_test.go000066400000000000000000000121741415471104000216040ustar00rootroot00000000000000package highlight import ( "testing" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/tt" ) var Args = tt.Args func TestGetRegions(t *testing.T) { lsCommand := region{0, 2, semanticRegion, commandRegion} tt.Test(t, tt.Fn("getRegionsFromString", getRegionsFromString), tt.Table{ Args("").Rets([]region(nil)), Args("ls").Rets([]region{ lsCommand, }), // Lexical regions. Args("ls a").Rets([]region{ lsCommand, {3, 4, lexicalRegion, barewordRegion}, // a }), Args("ls 'a'").Rets([]region{ lsCommand, {3, 6, lexicalRegion, singleQuotedRegion}, // 'a' }), Args(`ls "a"`).Rets([]region{ lsCommand, {3, 6, lexicalRegion, doubleQuotedRegion}, // 'a' }), Args("ls $x").Rets([]region{ lsCommand, {3, 5, lexicalRegion, variableRegion}, // $x }), Args("ls x*y").Rets([]region{ lsCommand, {3, 4, lexicalRegion, barewordRegion}, // x {4, 5, lexicalRegion, wildcardRegion}, // * {5, 6, lexicalRegion, barewordRegion}, // y }), Args("ls ~user/x").Rets([]region{ lsCommand, {3, 4, lexicalRegion, tildeRegion}, // ~ {4, 10, lexicalRegion, barewordRegion}, // user/x }), Args("ls # comment").Rets([]region{ lsCommand, {2, 12, lexicalRegion, commentRegion}, // # comment }), // LHS of assignments. Args("x y = foo bar").Rets([]region{ {0, 1, semanticRegion, variableRegion}, // x {2, 3, semanticRegion, variableRegion}, // y {4, 5, lexicalRegion, barewordRegion}, // = {6, 9, lexicalRegion, barewordRegion}, // foo {10, 13, lexicalRegion, barewordRegion}, // bar }), Args("x=foo ls").Rets([]region{ {0, 1, semanticRegion, variableRegion}, // x {1, 2, lexicalRegion, "="}, {2, 5, lexicalRegion, barewordRegion}, // foo {6, 8, semanticRegion, commandRegion}, // ls }), // The "var" special command Args("var x = foo").Rets([]region{ {0, 3, semanticRegion, commandRegion}, // var {4, 5, semanticRegion, variableRegion}, // x {6, 7, semanticRegion, keywordRegion}, // = {8, 11, lexicalRegion, barewordRegion}, // foo }), // The "set" special command Args("set x = foo").Rets([]region{ {0, 3, semanticRegion, commandRegion}, // var {4, 5, semanticRegion, variableRegion}, // x {6, 7, semanticRegion, keywordRegion}, // = {8, 11, lexicalRegion, barewordRegion}, // foo }), // The "if" special command. Args("if x { }").Rets([]region{ {0, 2, semanticRegion, commandRegion}, // if {3, 4, lexicalRegion, barewordRegion}, // x {5, 6, lexicalRegion, "{"}, {7, 8, lexicalRegion, "}"}, }), Args("if x { } else { }").Rets([]region{ {0, 2, semanticRegion, commandRegion}, // if {3, 4, lexicalRegion, barewordRegion}, // x {5, 6, lexicalRegion, "{"}, {7, 8, lexicalRegion, "}"}, {9, 13, semanticRegion, keywordRegion}, // else {14, 15, lexicalRegion, "{"}, {16, 17, lexicalRegion, "}"}, }), Args("if x { } elif y { }").Rets([]region{ {0, 2, semanticRegion, commandRegion}, // if {3, 4, lexicalRegion, barewordRegion}, // x {5, 6, lexicalRegion, "{"}, {7, 8, lexicalRegion, "}"}, {9, 13, semanticRegion, keywordRegion}, // elif {14, 15, lexicalRegion, barewordRegion}, // y {16, 17, lexicalRegion, "{"}, {18, 19, lexicalRegion, "}"}, }), // The "for" special command. Args("for x [] { }").Rets([]region{ {0, 3, semanticRegion, commandRegion}, // for {4, 5, semanticRegion, variableRegion}, // x {6, 7, lexicalRegion, "["}, {7, 8, lexicalRegion, "]"}, {9, 10, lexicalRegion, "{"}, {11, 12, lexicalRegion, "}"}, }), Args("for x [] { } else { }").Rets([]region{ {0, 3, semanticRegion, commandRegion}, // for {4, 5, semanticRegion, variableRegion}, // x {6, 7, lexicalRegion, "["}, {7, 8, lexicalRegion, "]"}, {9, 10, lexicalRegion, "{"}, {11, 12, lexicalRegion, "}"}, {13, 17, semanticRegion, keywordRegion}, // else {18, 19, lexicalRegion, "{"}, {20, 21, lexicalRegion, "}"}, }), // The "try" special command. Args("try { } except e { }").Rets([]region{ {0, 3, semanticRegion, commandRegion}, // try {4, 5, lexicalRegion, "{"}, {6, 7, lexicalRegion, "}"}, {8, 14, semanticRegion, keywordRegion}, // except {15, 16, semanticRegion, variableRegion}, // e {17, 18, lexicalRegion, "{"}, {19, 20, lexicalRegion, "}"}, }), Args("try { } except e { } else { }").Rets([]region{ {0, 3, semanticRegion, commandRegion}, // try {4, 5, lexicalRegion, "{"}, {6, 7, lexicalRegion, "}"}, {8, 14, semanticRegion, keywordRegion}, // except {15, 16, semanticRegion, variableRegion}, // e {17, 18, lexicalRegion, "{"}, {19, 20, lexicalRegion, "}"}, {21, 25, semanticRegion, keywordRegion}, // else {26, 27, lexicalRegion, "{"}, {28, 29, lexicalRegion, "}"}, }), Args("try { } finally { }").Rets([]region{ {0, 3, semanticRegion, commandRegion}, // try {4, 5, lexicalRegion, "{"}, {6, 7, lexicalRegion, "}"}, {8, 15, semanticRegion, keywordRegion}, // finally {16, 17, lexicalRegion, "{"}, {18, 19, lexicalRegion, "}"}, }), }) } func getRegionsFromString(code string) []region { // Ignore error. tree, _ := parse.Parse(parse.SourceForTest(code), parse.Config{}) return getRegions(tree.Root) } elvish-0.17.0/pkg/edit/highlight/theme.go000066400000000000000000000013141415471104000201730ustar00rootroot00000000000000package highlight import "src.elv.sh/pkg/ui" var stylingFor = map[string]ui.Styling{ barewordRegion: nil, singleQuotedRegion: ui.FgYellow, doubleQuotedRegion: ui.FgYellow, variableRegion: ui.FgMagenta, wildcardRegion: nil, tildeRegion: nil, commentRegion: ui.FgCyan, ">": ui.FgGreen, ">>": ui.FgGreen, "<": ui.FgGreen, "?>": ui.FgGreen, "|": ui.FgGreen, "?(": ui.Bold, "(": ui.Bold, ")": ui.Bold, "[": ui.Bold, "]": ui.Bold, "{": ui.Bold, "}": ui.Bold, "&": ui.Bold, commandRegion: ui.FgGreen, keywordRegion: ui.FgYellow, errorRegion: ui.Stylings(ui.FgBrightWhite, ui.BgRed), } var ( stylingForGoodCommand = ui.FgGreen stylingForBadCommand = ui.FgRed ) elvish-0.17.0/pkg/edit/highlight_test.go000066400000000000000000000066751415471104000201470ustar00rootroot00000000000000package edit import ( "os" "path/filepath" "runtime" "testing" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/env" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vars" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/testutil" "src.elv.sh/pkg/tt" ) // High-level sanity test. func TestHighlighter(t *testing.T) { f := setup(t) feedInput(f.TTYCtrl, "put $true") f.TestTTY(t, "~> put $true", Styles, " vvv $$$$$", term.DotHere, ) feedInput(f.TTYCtrl, "x") f.TestTTY(t, "~> put $truex", Styles, " vvv ??????", term.DotHere, "\n", "compilation error: 4-10 in [tty]: variable $truex not found", ) } // Fine-grained tests against the highlighter. func TestCheck(t *testing.T) { ev := eval.NewEvaler() ev.ExtendGlobal(eval.BuildNs().AddVar("good", vars.FromInit(0))) tt.Test(t, tt.Fn("check", check), tt.Table{ tt.Args(ev, mustParse("")).Rets(noError), tt.Args(ev, mustParse("echo $good")).Rets(noError), // TODO: Check the range of the returned error tt.Args(ev, mustParse("echo $bad")).Rets(anyError), }) } type anyErrorMatcher struct{} func (anyErrorMatcher) Match(ret tt.RetValue) bool { err, _ := ret.(error) return err != nil } var ( noError = error(nil) anyError anyErrorMatcher ) const colonInFilenameOk = runtime.GOOS != "windows" func TestMakeHasCommand(t *testing.T) { ev := eval.NewEvaler() // Set up global functions and modules in the evaler. goodFn := eval.NewGoFn("good", func() {}) ev.ExtendGlobal(eval.BuildNs(). AddFn("good", goodFn). AddNs("a", eval.BuildNs(). AddFn("good", goodFn). AddNs("b", eval.BuildNs().AddFn("good", goodFn)))) // Set up environment. testDir := testutil.InTempDir(t) testutil.Setenv(t, env.PATH, filepath.Join(testDir, "bin")) if runtime.GOOS == "windows" { testutil.Unsetenv(t, env.PATHEXT) // force default value } // Set up a directory in PATH. mustMkdirAll("bin") mustMkExecutable("bin/external") mustMkExecutable("bin/@external") if colonInFilenameOk { mustMkExecutable("bin/ex:tern:al") } // Set up a directory not in PATH. mustMkdirAll("a/b/c") mustMkExecutable("a/b/c/executable") tt.Test(t, tt.Fn("hasCommand", hasCommand), tt.Table{ // Builtin special form tt.Args(ev, "if").Rets(true), // Builtin function tt.Args(ev, "put").Rets(true), // User-defined function tt.Args(ev, "good").Rets(true), // Function in modules tt.Args(ev, "a:good").Rets(true), tt.Args(ev, "a:b:good").Rets(true), tt.Args(ev, "a:bad").Rets(false), tt.Args(ev, "a:b:bad").Rets(false), // Non-searching directory and external tt.Args(ev, "./a").Rets(true), tt.Args(ev, "a/b").Rets(true), tt.Args(ev, "a/b/c/executable").Rets(true), tt.Args(ev, "./bad").Rets(false), tt.Args(ev, "a/bad").Rets(false), // External in PATH tt.Args(ev, "external").Rets(true), tt.Args(ev, "@external").Rets(true), tt.Args(ev, "ex:tern:al").Rets(colonInFilenameOk), // With explicit e: tt.Args(ev, "e:external").Rets(true), tt.Args(ev, "e:bad-external").Rets(false), // Non-existent tt.Args(ev, "bad").Rets(false), tt.Args(ev, "a:").Rets(false), }) } func mustParse(src string) parse.Tree { tree, err := parse.Parse(parse.SourceForTest(src), parse.Config{}) if err != nil { panic(err) } return tree } func mustMkdirAll(path string) { err := os.MkdirAll(path, 0700) if err != nil { panic(err) } } func mustMkExecutable(path string) { if runtime.GOOS == "windows" { path += ".exe" } err := os.WriteFile(path, nil, 0700) if err != nil { panic(err) } } elvish-0.17.0/pkg/edit/hist_store.go000066400000000000000000000025111415471104000173050ustar00rootroot00000000000000package edit import ( "sync" "src.elv.sh/pkg/cli/histutil" "src.elv.sh/pkg/store/storedefs" ) // A wrapper of histutil.Store that is concurrency-safe and supports an // additional FastForward method. type histStore struct { m sync.Mutex db storedefs.Store hs histutil.Store } func newHistStore(db storedefs.Store) (*histStore, error) { hs, err := histutil.NewHybridStore(db) return &histStore{db: db, hs: hs}, err } func (s *histStore) AddCmd(cmd storedefs.Cmd) (int, error) { s.m.Lock() defer s.m.Unlock() return s.hs.AddCmd(cmd) } // AllCmds returns a slice of all interactive commands in oldest to newest order. func (s *histStore) AllCmds() ([]storedefs.Cmd, error) { s.m.Lock() defer s.m.Unlock() return s.hs.AllCmds() } func (s *histStore) Cursor(prefix string) histutil.Cursor { s.m.Lock() defer s.m.Unlock() return cursor{&s.m, histutil.NewDedupCursor(s.hs.Cursor(prefix))} } func (s *histStore) FastForward() error { s.m.Lock() defer s.m.Unlock() hs, err := histutil.NewHybridStore(s.db) s.hs = hs return err } type cursor struct { m *sync.Mutex c histutil.Cursor } func (c cursor) Prev() { c.m.Lock() defer c.m.Unlock() c.c.Prev() } func (c cursor) Next() { c.m.Lock() defer c.m.Unlock() c.c.Next() } func (c cursor) Get() (storedefs.Cmd, error) { c.m.Lock() defer c.m.Unlock() return c.c.Get() } elvish-0.17.0/pkg/edit/histwalk.go000066400000000000000000000042041415471104000167510ustar00rootroot00000000000000package edit import ( "errors" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/histutil" "src.elv.sh/pkg/cli/modes" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/eval" ) //elvdoc:var history:binding // // Binding table for the history mode. //elvdoc:fn history:start // // Starts the history mode. //elvdoc:fn history:up // // Walks to the previous entry in history mode. //elvdoc:fn history:down // // Walks to the next entry in history mode. //elvdoc:fn history:down-or-quit // // Walks to the next entry in history mode, or quit the history mode if already // at the newest entry. //elvdoc:fn history:fast-forward // // Import command history entries that happened after the current session // started. func initHistWalk(ed *Editor, ev *eval.Evaler, hs *histStore, nb eval.NsBuilder) { bindingVar := newBindingVar(emptyBindingsMap) bindings := newMapBindings(ed, ev, bindingVar) app := ed.app nb.AddNs("history", eval.BuildNsNamed("edit:history"). AddVar("binding", bindingVar). AddGoFns(map[string]interface{}{ "start": func() { notifyError(app, histwalkStart(app, hs, bindings)) }, "up": func() { notifyError(app, histwalkDo(app, modes.Histwalk.Prev)) }, "down": func() { notifyError(app, histwalkDo(app, modes.Histwalk.Next)) }, "down-or-quit": func() { err := histwalkDo(app, modes.Histwalk.Next) if err == histutil.ErrEndOfHistory { app.PopAddon() } else { notifyError(app, err) } }, "fast-forward": hs.FastForward, })) } func histwalkStart(app cli.App, hs *histStore, bindings tk.Bindings) error { codeArea, ok := focusedCodeArea(app) if !ok { return nil } buf := codeArea.CopyState().Buffer w, err := modes.NewHistwalk(app, modes.HistwalkSpec{ Bindings: bindings, Store: hs, Prefix: buf.Content[:buf.Dot]}) if w != nil { app.PushAddon(w) } return err } var errNotInHistoryMode = errors.New("not in history mode") func histwalkDo(app cli.App, f func(modes.Histwalk) error) error { w, ok := app.ActiveWidget().(modes.Histwalk) if !ok { return errNotInHistoryMode } return f(w) } func notifyError(app cli.App, err error) { if err != nil { app.Notify(err.Error()) } } elvish-0.17.0/pkg/edit/histwalk_test.go000066400000000000000000000031371415471104000200140ustar00rootroot00000000000000package edit import ( "testing" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/store/storedefs" "src.elv.sh/pkg/ui" ) func TestHistWalk_Up_EndOfHistory(t *testing.T) { f := startHistwalkTest(t) f.TTYCtrl.Inject(term.K(ui.Up)) f.TestTTYNotes(t, "end of history") } func TestHistWalk_Down_EndOfHistory(t *testing.T) { f := startHistwalkTest(t) // Not bound by default, so we need to use evals. evals(f.Evaler, `edit:history:down`) f.TestTTYNotes(t, "end of history") } func TestHistWalk_Accept(t *testing.T) { f := startHistwalkTest(t) f.TTYCtrl.Inject(term.K(ui.Right)) f.TestTTY(t, "~> echo a", Styles, " vvvv ", term.DotHere, ) } func TestHistWalk_Close(t *testing.T) { f := startHistwalkTest(t) f.TTYCtrl.Inject(term.K('[', ui.Ctrl)) f.TestTTY(t, "~> ", term.DotHere) } func TestHistWalk_DownOrQuit(t *testing.T) { f := startHistwalkTest(t) f.TTYCtrl.Inject(term.K(ui.Down)) f.TestTTY(t, "~> ", term.DotHere) } func TestHistory_FastForward(t *testing.T) { f := setup(t, storeOp(func(s storedefs.Store) { s.AddCmd("echo a") })) f.Store.AddCmd("echo b") evals(f.Evaler, `edit:history:fast-forward`) f.TTYCtrl.Inject(term.K(ui.Up)) f.TestTTY(t, "~> echo b", Styles, " VVVV__", term.DotHere, "\n", " HISTORY #2 ", Styles, "************", ) } func startHistwalkTest(t *testing.T) *fixture { // The part of the test shared by all tests. f := setup(t, storeOp(func(s storedefs.Store) { s.AddCmd("echo a") })) f.TTYCtrl.Inject(term.K(ui.Up)) f.TestTTY(t, "~> echo a", Styles, " VVVV__", term.DotHere, "\n", " HISTORY #1 ", Styles, "************", ) return f } elvish-0.17.0/pkg/edit/init.elv000066400000000000000000000073101415471104000162500ustar00rootroot00000000000000after-command = [ # Capture the most recent interactive command duration in $edit:command-duration # as a convenience for prompt functions. Note: The first time this is run is after # shell.sourceRC() finishes so the initial value of command-duration is the time # to execute the user's interactive configuration script. [m]{ command-duration = $m[duration] } ] global-binding = (binding-table [ &Ctrl-'['= $close-mode~ &Alt-x= $minibuf:start~ ]) insert:binding = (binding-table [ &Left= $move-dot-left~ &Right= $move-dot-right~ &Ctrl-Left= $move-dot-left-word~ &Ctrl-Right= $move-dot-right-word~ &Alt-Left= $move-dot-left-word~ &Alt-Right= $move-dot-right-word~ &Alt-b= $move-dot-left-word~ &Alt-f= $move-dot-right-word~ &Home= $move-dot-sol~ &End= $move-dot-eol~ &Backspace= $kill-rune-left~ &Ctrl-H= $kill-rune-left~ &Delete= $kill-rune-right~ &Ctrl-W= $kill-word-left~ &Ctrl-U= $kill-line-left~ &Ctrl-K= $kill-line-right~ &Ctrl-V= $insert-raw~ &Alt-,= $lastcmd:start~ &Alt-.= $insert-last-word~ &Ctrl-R= $histlist:start~ &Ctrl-L= $location:start~ &Ctrl-N= $navigation:start~ &Tab= $completion:smart-start~ &Up= $history:start~ &Enter= $smart-enter~ &Ctrl-D= $return-eof~ ]) command:binding = (binding-table [ &'$'= $move-dot-eol~ &0= $move-dot-sol~ &D= $kill-line-right~ &b= $move-dot-left-word~ &h= $move-dot-left~ &i= $close-mode~ &j= $move-dot-down~ &k= $move-dot-up~ &l= $move-dot-right~ &w= $move-dot-right-word~ &x= $kill-rune-right~ ]) listing:binding = (binding-table [ &Up= $listing:up~ &Down= $listing:down~ &Tab= $listing:down-cycle~ &Shift-Tab= $listing:up-cycle~ ]) histlist:binding = (binding-table [ &Ctrl-D= $histlist:toggle-dedup~ ]) navigation:binding = (binding-table [ &Left= $navigation:left~ &Right= $navigation:right~ &Up= $navigation:up~ &Down= $navigation:down~ &PageUp= $navigation:page-up~ &PageDown= $navigation:page-down~ &Alt-Up= $navigation:file-preview-up~ &Alt-Down= $navigation:file-preview-down~ &Enter= $navigation:insert-selected-and-quit~ &Alt-Enter= $navigation:insert-selected~ &Ctrl-F= $navigation:trigger-filter~ &Ctrl-H= $navigation:trigger-shown-hidden~ ]) completion:binding = (binding-table [ &Down= $completion:down~ &Up= $completion:up~ &Tab= $completion:down-cycle~ &Shift-Tab=$completion:up-cycle~ &Left= $completion:left~ &Right= $completion:right~ ]) history:binding = (binding-table [ &Up= $history:up~ &Down= $history:down-or-quit~ &Ctrl-'['= $close-mode~ ]) lastcmd:binding = (binding-table [ &Alt-,= $listing:accept~ ]) -instant:binding = (binding-table [ & ]) # TODO: Avoid duplicating the bindings here by having a base binding table # shared by insert and minibuf modes (like how the listing modes all share # listing:binding). minibuf:binding = (binding-table [ &Left= $move-dot-left~ &Right= $move-dot-right~ &Ctrl-Left= $move-dot-left-word~ &Ctrl-Right= $move-dot-right-word~ &Alt-Left= $move-dot-left-word~ &Alt-Right= $move-dot-right-word~ &Alt-b= $move-dot-left-word~ &Alt-f= $move-dot-right-word~ &Home= $move-dot-sol~ &End= $move-dot-eol~ &Backspace= $kill-rune-left~ &Ctrl-H= $kill-rune-left~ &Delete= $kill-rune-right~ &Ctrl-W= $kill-word-left~ &Ctrl-U= $kill-line-left~ &Ctrl-K= $kill-line-right~ &Ctrl-V= $insert-raw~ &Alt-,= $lastcmd:start~ &Alt-.= $insert-last-word~ &Ctrl-R= $histlist:start~ &Ctrl-L= $location:start~ &Ctrl-N= $navigation:start~ &Tab= $completion:smart-start~ &Up= $history:start~ ]) elvish-0.17.0/pkg/edit/insert_api.go000066400000000000000000000113631415471104000172640ustar00rootroot00000000000000package edit import ( "src.elv.sh/pkg/cli" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" "src.elv.sh/pkg/persistent/hashmap" ) //elvdoc:var abbr // // A map from (simple) abbreviations to their expansions. // // An abbreviation is replaced by its expansion when it is typed in full // and consecutively, without being interrupted by the use of other editing // functionalities, such as cursor movements. // // If more than one abbreviations would match, the longest one is used. // // Examples: // // ```elvish // edit:abbr['||'] = '| less' // edit:abbr['>dn'] = '2>/dev/null' // ``` // // With the definitions above, typing `||` anywhere expands to `| less`, and // typing `>dn` anywhere expands to `2>/dev/null`. However, typing a `|`, moving // the cursor left, and typing another `|` does **not** expand to `| less`, // since the abbreviation `||` was not typed consecutively. // // @cf edit:small-word-abbr //elvdoc:var small-word-abbr // // A map from small-word abbreviations and their expansions. // // A small-word abbreviation is replaced by its expansion after it is typed in // full and consecutively, and followed by another character (the *trigger* // character). Furthermore, the expansion requires the following conditions to // be satisfied: // // - The end of the abbreviation must be adjacent to a small-word boundary, // i.e. the last character of the abbreviation and the trigger character // must be from two different small-word categories. // // - The start of the abbreviation must also be adjacent to a small-word // boundary, unless it appears at the beginning of the code buffer. // // - The cursor must be at the end of the buffer. // // If more than one abbreviations would match, the longest one is used. // // As an example, with the following configuration: // // ```elvish // edit:small-word-abbr['gcm'] = 'git checkout master' // ``` // // In the following scenarios, the `gcm` abbreviation is expanded: // // - With an empty buffer, typing `gcm` and a space or semicolon; // // - When the buffer ends with a space, typing `gcm` and a space or semicolon. // // The space or semicolon after `gcm` is preserved in both cases. // // In the following scenarios, the `gcm` abbreviation is **not** expanded: // // - With an empty buffer, typing `Xgcm` and a space or semicolon (start of // abbreviation is not adjacent to a small-word boundary); // // - When the buffer ends with `X`, typing `gcm` and a space or semicolon (end // of abbreviation is not adjacent to a small-word boundary); // // - When the buffer is non-empty, move the cursor to the beginning, and typing // `gcm` and a space (cursor not at the end of the buffer). // // This example shows the case where the abbreviation consists of a single small // word of alphanumerical characters, but that doesn't have to be the case. For // example, with the following configuration: // // ```elvish // edit:small-word-abbr['>dn'] = ' 2>/dev/null' // ``` // // The abbreviation `>dn` starts with a punctuation character, and ends with an // alphanumerical character. This means that it is expanded when it borders // a whitespace or alphanumerical character to the left, and a whitespace or // punctuation to the right; for example, typing `ls>dn;` will expand it. // // Some extra examples of small-word abbreviations: // // ```elvish // edit:small-word-abbr['gcp'] = 'git cherry-pick -x' // edit:small-word-abbr['ll'] = 'ls -ltr' // ``` // // If both a [simple abbreviation](#edit:abbr) and a small-word abbreviation can // be expanded, the simple abbreviation has priority. // // @cf edit:abbr func initInsertAPI(appSpec *cli.AppSpec, nt notifier, ev *eval.Evaler, nb eval.NsBuilder) { abbr := vals.EmptyMap abbrVar := vars.FromPtr(&abbr) appSpec.Abbreviations = makeMapIterator(abbrVar) SmallWordAbbr := vals.EmptyMap SmallWordAbbrVar := vars.FromPtr(&SmallWordAbbr) appSpec.SmallWordAbbreviations = makeMapIterator(SmallWordAbbrVar) bindingVar := newBindingVar(emptyBindingsMap) appSpec.CodeAreaBindings = newMapBindings(nt, ev, bindingVar) quotePaste := newBoolVar(false) appSpec.QuotePaste = func() bool { return quotePaste.GetRaw().(bool) } toggleQuotePaste := func() { quotePaste.Set(!quotePaste.Get().(bool)) } nb.AddVar("abbr", abbrVar) nb.AddVar("small-word-abbr", SmallWordAbbrVar) nb.AddGoFn("toggle-quote-paste", toggleQuotePaste) nb.AddNs("insert", eval.BuildNs(). AddVar("binding", bindingVar). AddVar("quote-paste", quotePaste)) } func makeMapIterator(mv vars.PtrVar) func(func(a, b string)) { return func(f func(a, b string)) { for it := mv.GetRaw().(hashmap.Map).Iterator(); it.HasElem(); it.Next() { k, v := it.Elem() ks, kok := k.(string) vs, vok := v.(string) if !kok || !vok { continue } f(ks, vs) } } } elvish-0.17.0/pkg/edit/insert_api_test.go000066400000000000000000000027261415471104000203260ustar00rootroot00000000000000package edit import ( "testing" "src.elv.sh/pkg/cli/term" ) func TestInsert_Abbr(t *testing.T) { f := setup(t) evals(f.Evaler, `edit:abbr = [&x=full]`) f.TTYCtrl.Inject(term.K('x'), term.K('\n')) if code := <-f.codeCh; code != "full" { t.Errorf("abbreviation expanded to %q, want %q", code, "full") } } func TestInsert_Binding(t *testing.T) { f := setup(t) evals(f.Evaler, `called = 0`, `edit:insert:binding[x] = { called = (+ $called 1) }`) f.TTYCtrl.Inject(term.K('x'), term.K('\n')) if code := <-f.codeCh; code != "" { t.Errorf("code = %q, want %q", code, "") } if called, _ := f.Evaler.Global().Index("called"); called != 1 { t.Errorf("called = %v, want 1", called) } } func TestInsert_QuotePaste(t *testing.T) { f := setup(t) evals(f.Evaler, `edit:insert:quote-paste = $true`) f.TTYCtrl.Inject( term.PasteSetting(true), term.K('>'), term.PasteSetting(false), term.K('\n')) wantCode := `'>'` if code := <-f.codeCh; code != wantCode { t.Errorf("Got code %q, want %q", code, wantCode) } } func TestToggleQuotePaste(t *testing.T) { f := setup(t) evals(f.Evaler, `v0 = $edit:insert:quote-paste`, `edit:toggle-quote-paste`, `v1 = $edit:insert:quote-paste`, `edit:toggle-quote-paste`, `v2 = $edit:insert:quote-paste`) v0 := getGlobal(f.Evaler, "v0").(bool) v1 := getGlobal(f.Evaler, "v1").(bool) v2 := getGlobal(f.Evaler, "v2").(bool) if v1 == v0 { t.Errorf("got v1 = v0") } if v2 == v1 { t.Errorf("got v2 = v1") } } elvish-0.17.0/pkg/edit/instant.go000066400000000000000000000030501415471104000166010ustar00rootroot00000000000000package edit import ( "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/modes" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/parse" ) //elvdoc:var -instant:binding // // Binding for the instant mode. //elvdoc:fn -instant:start // // Starts the instant mode. In instant mode, any text entered at the command // line is evaluated immediately, with the output displayed. // // **WARNING**: Beware of unintended consequences when using destructive // commands. For example, if you type `sudo rm -rf /tmp/*` in the instant mode, // Elvish will attempt to evaluate `sudo rm -rf /` when you typed that far. func initInstant(ed *Editor, ev *eval.Evaler, nb eval.NsBuilder) { bindingVar := newBindingVar(emptyBindingsMap) bindings := newMapBindings(ed, ev, bindingVar) nb.AddNs("-instant", eval.BuildNsNamed("edit:-instant"). AddVar("binding", bindingVar). AddGoFns(map[string]interface{}{ "start": func() { instantStart(ed.app, ev, bindings) }, })) } func instantStart(app cli.App, ev *eval.Evaler, bindings tk.Bindings) { execute := func(code string) ([]string, error) { outPort, collect, err := eval.StringCapturePort() if err != nil { return nil, err } err = ev.Eval( parse.Source{Name: "[instant]", Code: code}, eval.EvalCfg{ Ports: []*eval.Port{nil, outPort}, Interrupt: eval.ListenInterrupts}) return collect(), err } w, err := modes.NewInstant(app, modes.InstantSpec{Bindings: bindings, Execute: execute}) if w != nil { app.PushAddon(w) app.Redraw() } if err != nil { app.Notify(err.Error()) } } elvish-0.17.0/pkg/edit/instant_test.go000066400000000000000000000021671415471104000176500ustar00rootroot00000000000000package edit import ( "testing" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/cli/tk" ) func TestInstantAddon_ValueOutput(t *testing.T) { f := setup(t) evals(f.Evaler, "edit:-instant:start") f.TestTTY(t, "~> ", term.DotHere, "\n", " INSTANT \n", Styles, "*********", ) feedInput(f.TTYCtrl, "+") f.TestTTY(t, "~> +", Styles, " v", term.DotHere, "\n", " INSTANT \n", Styles, "*********", "▶ 0", ) feedInput(f.TTYCtrl, " 1 2") f.TestTTY(t, "~> + 1 2", Styles, " v ", term.DotHere, "\n", " INSTANT \n", Styles, "*********", "▶ 3", ) } func TestInstantAddon_ByteOutput(t *testing.T) { f := setup(t) // We don't want to trigger the evaluation of "e", "ec", and "ech", so we // start with a non-empty code buffer. f.SetCodeBuffer(tk.CodeBuffer{Content: "echo ", Dot: 5}) evals(f.Evaler, "edit:-instant:start") f.TestTTY(t, "~> echo ", Styles, " vvvv ", term.DotHere, "\n", " INSTANT \n", Styles, "*********", ) feedInput(f.TTYCtrl, "hello") f.TestTTY(t, "~> echo hello", Styles, " vvvv ", term.DotHere, "\n", " INSTANT \n", Styles, "*********", "hello", ) } elvish-0.17.0/pkg/edit/key_binding.go000066400000000000000000000045541415471104000174150ustar00rootroot00000000000000package edit import ( "bufio" "io" "os" "sync" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/ui" ) type mapBindings struct { nt notifier ev *eval.Evaler mapVars []vars.PtrVar } func newMapBindings(nt notifier, ev *eval.Evaler, mapVars ...vars.PtrVar) tk.Bindings { return mapBindings{nt, ev, mapVars} } func (b mapBindings) Handle(w tk.Widget, e term.Event) bool { k, ok := e.(term.KeyEvent) if !ok { return false } maps := make([]bindingsMap, len(b.mapVars)) for i, v := range b.mapVars { maps[i] = v.GetRaw().(bindingsMap) } f := indexLayeredBindings(ui.Key(k), maps...) if f == nil { return false } callWithNotifyPorts(b.nt, b.ev, f) return true } // Indexes a series of layered bindings. Returns nil if none of the bindings // have the required key or a default. func indexLayeredBindings(k ui.Key, maps ...bindingsMap) eval.Callable { for _, m := range maps { if m.HasKey(k) { return m.GetKey(k) } } for _, m := range maps { if m.HasKey(ui.Default) { return m.GetKey(ui.Default) } } return nil } var bindingSource = parse.Source{Name: "[editor binding]"} func callWithNotifyPorts(nt notifier, ev *eval.Evaler, f eval.Callable, args ...interface{}) { notifyPort, cleanup := makeNotifyPort(nt) defer cleanup() err := ev.Call(f, eval.CallCfg{Args: args, From: "[editor binding]"}, eval.EvalCfg{Ports: []*eval.Port{nil, notifyPort, notifyPort}}) if err != nil { nt.notifyError("binding", err) } } func makeNotifyPort(nt notifier) (*eval.Port, func()) { ch := make(chan interface{}) r, w, err := os.Pipe() if err != nil { panic(err) } var wg sync.WaitGroup wg.Add(2) go func() { // Relay value outputs for v := range ch { nt.notifyf("[value out] %s", vals.Repr(v, vals.NoPretty)) } wg.Done() }() go func() { // Relay byte outputs reader := bufio.NewReader(r) for { line, err := reader.ReadString('\n') if err != nil { if line != "" { nt.notifyf("[bytes out] %s", line) } if err != io.EOF { nt.notifyf("[bytes error] %s", err) } break } nt.notifyf("[bytes out] %s", line[:len(line)-1]) } r.Close() wg.Done() }() port := &eval.Port{Chan: ch, File: w} cleanup := func() { close(ch) w.Close() wg.Wait() } return port, cleanup } elvish-0.17.0/pkg/edit/listing.go000066400000000000000000000161611415471104000166010ustar00rootroot00000000000000package edit import ( "os" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/histutil" "src.elv.sh/pkg/cli/modes" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/edit/filter" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" "src.elv.sh/pkg/persistent/hashmap" "src.elv.sh/pkg/store/storedefs" ) func initListings(ed *Editor, ev *eval.Evaler, st storedefs.Store, histStore histutil.Store, nb eval.NsBuilder) { bindingVar := newBindingVar(emptyBindingsMap) app := ed.app nb.AddNs("listing", eval.BuildNsNamed("edit:listing"). AddVar("binding", bindingVar). AddGoFns(map[string]interface{}{ "accept": func() { listingAccept(app) }, "up": func() { listingUp(app) }, "down": func() { listingDown(app) }, "up-cycle": func() { listingUpCycle(app) }, "down-cycle": func() { listingDownCycle(app) }, "page-up": func() { listingPageUp(app) }, "page-down": func() { listingPageDown(app) }, "start-custom": func(fm *eval.Frame, opts customListingOpts, items interface{}) { listingStartCustom(ed, fm, opts, items) }, })) initHistlist(ed, ev, histStore, bindingVar, nb) initLastcmd(ed, ev, histStore, bindingVar, nb) initLocation(ed, ev, st, bindingVar, nb) } var filterSpec = modes.FilterSpec{ Maker: func(f string) func(string) bool { q, _ := filter.Compile(f) if q == nil { return func(string) bool { return true } } return q.Match }, Highlighter: filter.Highlight, } func initHistlist(ed *Editor, ev *eval.Evaler, histStore histutil.Store, commonBindingVar vars.PtrVar, nb eval.NsBuilder) { bindingVar := newBindingVar(emptyBindingsMap) bindings := newMapBindings(ed, ev, bindingVar, commonBindingVar) dedup := newBoolVar(true) nb.AddNs("histlist", eval.BuildNsNamed("edit:histlist"). AddVar("binding", bindingVar). AddGoFns(map[string]interface{}{ "start": func() { w, err := modes.NewHistlist(ed.app, modes.HistlistSpec{ Bindings: bindings, AllCmds: histStore.AllCmds, Dedup: func() bool { return dedup.Get().(bool) }, Filter: filterSpec, }) startMode(ed.app, w, err) }, "toggle-dedup": func() { dedup.Set(!dedup.Get().(bool)) listingRefilter(ed.app) ed.app.Redraw() }, })) } func initLastcmd(ed *Editor, ev *eval.Evaler, histStore histutil.Store, commonBindingVar vars.PtrVar, nb eval.NsBuilder) { bindingVar := newBindingVar(emptyBindingsMap) bindings := newMapBindings(ed, ev, bindingVar, commonBindingVar) nb.AddNs("lastcmd", eval.BuildNsNamed("edit:lastcmd"). AddVar("binding", bindingVar). AddGoFn("start", func() { // TODO: Specify wordifier w, err := modes.NewLastcmd(ed.app, modes.LastcmdSpec{ Bindings: bindings, Store: histStore}) startMode(ed.app, w, err) })) } func initLocation(ed *Editor, ev *eval.Evaler, st storedefs.Store, commonBindingVar vars.PtrVar, nb eval.NsBuilder) { bindingVar := newBindingVar(emptyBindingsMap) pinnedVar := newListVar(vals.EmptyList) hiddenVar := newListVar(vals.EmptyList) workspacesVar := newMapVar(vals.EmptyMap) bindings := newMapBindings(ed, ev, bindingVar, commonBindingVar) workspaceIterator := modes.LocationWSIterator( adaptToIterateStringPair(workspacesVar)) nb.AddNs("location", eval.BuildNsNamed("edit:location"). AddVars(map[string]vars.Var{ "binding": bindingVar, "hidden": hiddenVar, "pinned": pinnedVar, "workspaces": workspacesVar, }). AddGoFn("start", func() { w, err := modes.NewLocation(ed.app, modes.LocationSpec{ Bindings: bindings, Store: dirStore{ev, st}, IteratePinned: adaptToIterateString(pinnedVar), IterateHidden: adaptToIterateString(hiddenVar), IterateWorkspaces: workspaceIterator, Filter: filterSpec, }) startMode(ed.app, w, err) })) ev.AddAfterChdir(func(string) { wd, err := os.Getwd() if err != nil { // TODO(xiaq): Surface the error. return } st.AddDir(wd, 1) kind, root := workspaceIterator.Parse(wd) if kind != "" { st.AddDir(kind+wd[len(root):], 1) } }) } //elvdoc:fn listing:accept // // Accepts the current selected listing item. func listingAccept(app cli.App) { if w, ok := activeComboBox(app); ok { w.ListBox().Accept() } } //elvdoc:fn listing:up // // Moves the cursor up in listing mode. func listingUp(app cli.App) { listingSelect(app, tk.Prev) } //elvdoc:fn listing:down // // Moves the cursor down in listing mode. func listingDown(app cli.App) { listingSelect(app, tk.Next) } //elvdoc:fn listing:up-cycle // // Moves the cursor up in listing mode, or to the last item if the first item is // currently selected. func listingUpCycle(app cli.App) { listingSelect(app, tk.PrevWrap) } //elvdoc:fn listing:down-cycle // // Moves the cursor down in listing mode, or to the first item if the last item is // currently selected. func listingDownCycle(app cli.App) { listingSelect(app, tk.NextWrap) } //elvdoc:fn listing:page-up // // Moves the cursor up one page. func listingPageUp(app cli.App) { listingSelect(app, tk.PrevPage) } //elvdoc:fn listing:page-down // // Moves the cursor down one page. func listingPageDown(app cli.App) { listingSelect(app, tk.NextPage) } //elvdoc:fn listing:left // // Moves the cursor left in listing mode. func listingLeft(app cli.App) { listingSelect(app, tk.Left) } //elvdoc:fn listing:right // // Moves the cursor right in listing mode. func listingRight(app cli.App) { listingSelect(app, tk.Right) } func listingSelect(app cli.App, f func(tk.ListBoxState) int) { if w, ok := activeComboBox(app); ok { w.ListBox().Select(f) } } func listingRefilter(app cli.App) { if w, ok := activeComboBox(app); ok { w.Refilter() } } //elvdoc:var location:hidden // // A list of directories to hide in the location addon. //elvdoc:var location:pinned // // A list of directories to always show at the top of the list of the location // addon. //elvdoc:var location:workspaces // // A map mapping types of workspaces to their patterns. func adaptToIterateString(variable vars.Var) func(func(string)) { return func(f func(s string)) { vals.Iterate(variable.Get(), func(v interface{}) bool { f(vals.ToString(v)) return true }) } } func adaptToIterateStringPair(variable vars.Var) func(func(string, string) bool) { return func(f func(a, b string) bool) { m := variable.Get().(hashmap.Map) for it := m.Iterator(); it.HasElem(); it.Next() { k, v := it.Elem() ks, kok := k.(string) vs, vok := v.(string) if kok && vok { next := f(ks, vs) if !next { break } } } } } // Wraps an Evaler to implement the cli.DirStore interface. type dirStore struct { ev *eval.Evaler st storedefs.Store } func (d dirStore) Chdir(path string) error { return d.ev.Chdir(path) } func (d dirStore) Dirs(blacklist map[string]struct{}) ([]storedefs.Dir, error) { return d.st.Dirs(blacklist) } func (d dirStore) Getwd() (string, error) { return os.Getwd() } func startMode(app cli.App, w tk.Widget, err error) { if w != nil { app.PushAddon(w) app.Redraw() } if err != nil { app.Notify(err.Error()) } } func activeComboBox(app cli.App) (tk.ComboBox, bool) { w, ok := app.ActiveWidget().(tk.ComboBox) return w, ok } elvish-0.17.0/pkg/edit/listing_custom.go000066400000000000000000000063331415471104000201730ustar00rootroot00000000000000package edit import ( "bufio" "os" "strings" "sync" "src.elv.sh/pkg/cli/modes" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" "src.elv.sh/pkg/strutil" "src.elv.sh/pkg/ui" ) type customListingOpts struct { Binding bindingsMap Caption string KeepBottom bool Accept eval.Callable AutoAccept bool } func (*customListingOpts) SetDefaultOptions() {} //elvdoc:fn listing:start-custom // // Starts a custom listing addon. func listingStartCustom(ed *Editor, fm *eval.Frame, opts customListingOpts, items interface{}) { var bindings tk.Bindings if opts.Binding.Map != nil { bindings = newMapBindings(ed, fm.Evaler, vars.FromPtr(&opts.Binding)) } var getItems func(string) []modes.ListingItem if fn, isFn := items.(eval.Callable); isFn { getItems = func(q string) []modes.ListingItem { var items []modes.ListingItem var itemsMutex sync.Mutex collect := func(item modes.ListingItem) { itemsMutex.Lock() defer itemsMutex.Unlock() items = append(items, item) } valuesCb := func(ch <-chan interface{}) { for v := range ch { if item, itemOk := getListingItem(v); itemOk { collect(item) } } } bytesCb := func(r *os.File) { buffered := bufio.NewReader(r) for { line, err := buffered.ReadString('\n') if line != "" { s := strutil.ChopLineEnding(line) collect(modes.ListingItem{ToAccept: s, ToShow: ui.T(s)}) } if err != nil { break } } } f := func(fm *eval.Frame) error { return fn.Call(fm, []interface{}{q}, eval.NoOpts) } err := fm.PipeOutput(f, valuesCb, bytesCb) // TODO(xiaq): Report the error. _ = err return items } } else { getItems = func(q string) []modes.ListingItem { convertedItems := []modes.ListingItem{} vals.Iterate(items, func(v interface{}) bool { toFilter, toFilterOk := getToFilter(v) item, itemOk := getListingItem(v) if toFilterOk && itemOk && strings.Contains(toFilter, q) { // TODO(xiaq): Report type error when ok is false. convertedItems = append(convertedItems, item) } return true }) return convertedItems } } w, err := modes.NewListing(ed.app, modes.ListingSpec{ Bindings: bindings, Caption: opts.Caption, GetItems: func(q string) ([]modes.ListingItem, int) { items := getItems(q) selected := 0 if opts.KeepBottom { selected = len(items) - 1 } return items, selected }, Accept: func(s string) { if opts.Accept != nil { callWithNotifyPorts(ed, fm.Evaler, opts.Accept, s) } }, AutoAccept: opts.AutoAccept, }) startMode(ed.app, w, err) } func getToFilter(v interface{}) (string, bool) { toFilterValue, _ := vals.Index(v, "to-filter") toFilter, toFilterOk := toFilterValue.(string) return toFilter, toFilterOk } func getListingItem(v interface{}) (item modes.ListingItem, ok bool) { toAcceptValue, _ := vals.Index(v, "to-accept") toAccept, toAcceptOk := toAcceptValue.(string) toShowValue, _ := vals.Index(v, "to-show") toShow, toShowOk := toShowValue.(ui.Text) if toShowString, ok := toShowValue.(string); ok { toShow = ui.T(toShowString) toShowOk = true } return modes.ListingItem{ToAccept: toAccept, ToShow: toShow}, toAcceptOk && toShowOk } elvish-0.17.0/pkg/edit/listing_nonwindows_test.go000066400000000000000000000046241415471104000221260ustar00rootroot00000000000000//go:build !windows // +build !windows package edit import ( "os" "path/filepath" "reflect" "sort" "testing" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/store/storedefs" "src.elv.sh/pkg/testutil" "src.elv.sh/pkg/ui" ) func TestLocationAddon(t *testing.T) { f := setup(t, storeOp(func(s storedefs.Store) { s.AddDir("/usr/bin", 1) s.AddDir("/tmp", 1) s.AddDir("/home/elf", 1) })) evals(f.Evaler, `edit:location:pinned = [/opt]`, `edit:location:hidden = [/tmp]`) f.TTYCtrl.Inject(term.K('L', ui.Ctrl)) f.TestTTY(t, "~> \n", " LOCATION ", Styles, "********** ", term.DotHere, "\n", " * /opt \n", Styles, "++++++++++++++++++++++++++++++++++++++++++++++++++", " 10 /home/elf\n", " 10 /usr/bin", ) } func TestLocationAddon_Workspace(t *testing.T) { f := setup(t, storeOp(func(s storedefs.Store) { s.AddDir("/usr/bin", 1) s.AddDir("ws/bin", 1) s.AddDir("other-ws/bin", 1) })) testutil.ApplyDir( testutil.Dir{ "ws1": testutil.Dir{ "bin": testutil.Dir{}, "tmp": testutil.Dir{}}}) err := os.Chdir("ws1/tmp") if err != nil { t.Skip("chdir:", err) } evals(f.Evaler, `edit:location:workspaces = [&ws=$E:HOME/ws.]`) f.TTYCtrl.Inject(term.K('L', ui.Ctrl)) f.TestTTY(t, "~/ws1/tmp> \n", " LOCATION ", Styles, "********** ", term.DotHere, "\n", " 10 ws/bin \n", Styles, "++++++++++++++++++++++++++++++++++++++++++++++++++", " 10 /usr/bin", ) f.TTYCtrl.Inject(term.K(ui.Enter)) f.TestTTY(t, "~/ws1/bin> ", term.DotHere) } func TestLocation_AddDir(t *testing.T) { f := setup(t) testutil.ApplyDir( testutil.Dir{ "bin": testutil.Dir{}, "ws1": testutil.Dir{ "bin": testutil.Dir{}}}) evals(f.Evaler, `edit:location:workspaces = [&ws=$E:HOME/ws.]`) chdir := func(path string) { err := f.Evaler.Chdir(path) if err != nil { t.Skip("chdir:", err) } } chdir("bin") chdir("../ws1/bin") entries, err := f.Store.Dirs(map[string]struct{}{}) if err != nil { t.Error("unable to list dir history:", err) } dirs := make([]string, len(entries)) for i, entry := range entries { dirs[i] = entry.Path } wantDirs := []string{ filepath.Join(f.Home, "bin"), filepath.Join(f.Home, "ws1", "bin"), filepath.Join("ws", "bin"), } sort.Strings(dirs) sort.Strings(wantDirs) if !reflect.DeepEqual(dirs, wantDirs) { t.Errorf("got dirs %v, want %v", dirs, wantDirs) } } elvish-0.17.0/pkg/edit/listing_test.go000066400000000000000000000140771415471104000176440ustar00rootroot00000000000000package edit import ( "testing" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/store/storedefs" "src.elv.sh/pkg/ui" ) func TestListingBuiltins(t *testing.T) { // Use the custom listing mode since it doesn't require special setup. The // builtins work the same across all listing modes. f := setup(t) evals(f.Evaler, `fn item {|x| put [&to-show=$x &to-accept=$x &to-filter=$x] }`, `edit:listing:start-custom [(item 1) (item 2) (item 3)]`) buf1 := f.MakeBuffer( "~> \n", " LISTING ", Styles, "********* ", term.DotHere, "\n", "1 ", Styles, "++++++++++++++++++++++++++++++++++++++++++++++++++", "2 \n", "3 ", ) f.TTYCtrl.TestBuffer(t, buf1) evals(f.Evaler, "edit:listing:down", "edit:redraw") buf2 := f.MakeBuffer( "~> \n", " LISTING ", Styles, "********* ", term.DotHere, "\n", "1 \n", "2 \n", Styles, "++++++++++++++++++++++++++++++++++++++++++++++++++", "3 ", ) f.TTYCtrl.TestBuffer(t, buf2) evals(f.Evaler, "edit:listing:down", "edit:redraw") buf3 := f.MakeBuffer( "~> \n", " LISTING ", Styles, "********* ", term.DotHere, "\n", "1 \n", "2 \n", "3 ", Styles, "++++++++++++++++++++++++++++++++++++++++++++++++++", ) f.TTYCtrl.TestBuffer(t, buf3) evals(f.Evaler, "edit:listing:down", "edit:redraw") f.TTYCtrl.TestBuffer(t, buf3) evals(f.Evaler, "edit:listing:down-cycle", "edit:redraw") f.TTYCtrl.TestBuffer(t, buf1) evals(f.Evaler, "edit:listing:up", "edit:redraw") f.TTYCtrl.TestBuffer(t, buf1) evals(f.Evaler, "edit:listing:up-cycle", "edit:redraw") f.TTYCtrl.TestBuffer(t, buf3) evals(f.Evaler, "edit:listing:page-up", "edit:redraw") f.TTYCtrl.TestBuffer(t, buf1) evals(f.Evaler, "edit:listing:page-down", "edit:redraw") f.TTYCtrl.TestBuffer(t, buf3) } // Smoke tests for individual addons. func TestHistlistAddon(t *testing.T) { f := setup(t, storeOp(func(s storedefs.Store) { s.AddCmd("ls") s.AddCmd("echo") s.AddCmd("ls") s.AddCmd("LS") })) f.TTYCtrl.Inject(term.K('R', ui.Ctrl)) f.TestTTY(t, "~> \n", " HISTORY (dedup on) ", Styles, "******************** ", term.DotHere, "\n", " 2 echo\n", " 3 ls\n", " 4 LS ", Styles, "++++++++++++++++++++++++++++++++++++++++++++++++++", ) evals(f.Evaler, `edit:histlist:toggle-dedup`) f.TestTTY(t, "~> \n", " HISTORY ", Styles, "********* ", term.DotHere, "\n", " 1 ls\n", " 2 echo\n", " 3 ls\n", " 4 LS ", Styles, "++++++++++++++++++++++++++++++++++++++++++++++++++", ) evals(f.Evaler, `edit:histlist:toggle-dedup`) // Filtering is case-insensitive when filter is all lower case. f.TTYCtrl.Inject(term.K('l')) f.TestTTY(t, "~> \n", " HISTORY (dedup on) l", Styles, "******************** ", term.DotHere, "\n", " 3 ls\n", " 4 LS ", Styles, "++++++++++++++++++++++++++++++++++++++++++++++++++", ) // Filtering is case-sensitive when filter is not all lower case. f.TTYCtrl.Inject(term.K(ui.Backspace), term.K('L')) f.TestTTY(t, "~> \n", " HISTORY (dedup on) L", Styles, "******************** ", term.DotHere, "\n", " 4 LS ", Styles, "++++++++++++++++++++++++++++++++++++++++++++++++++", ) } func TestLastCmdAddon(t *testing.T) { f := setup(t, storeOp(func(s storedefs.Store) { s.AddCmd("echo hello world") })) f.TTYCtrl.Inject(term.K(',', ui.Alt)) f.TestTTY(t, "~> \n", " LASTCMD ", Styles, "********* ", term.DotHere, "\n", " echo hello world \n", Styles, "++++++++++++++++++++++++++++++++++++++++++++++++++", " 0 echo\n", " 1 hello\n", " 2 world", ) } func TestCustomListing_PassingList(t *testing.T) { f := setup(t) evals(f.Evaler, `items = [[&to-filter=1 &to-accept=echo &to-show=echo] [&to-filter=2 &to-accept=put &to-show=(styled put green)]]`, `edit:listing:start-custom $items &accept=$edit:insert-at-dot~ &caption=A`) f.TestTTY(t, "~> \n", "A ", Styles, "* ", term.DotHere, "\n", "echo \n", Styles, "++++++++++++++++++++++++++++++++++++++++++++++++++", "put ", Styles, "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv", ) // Filter - "put" will be selected. f.TTYCtrl.Inject(term.K('2')) // Accept. f.TTYCtrl.Inject(term.K('\n')) f.TestTTY(t, "~> put", Styles, " vvv", term.DotHere, ) } func TestCustomListing_PassingValueCallback(t *testing.T) { f := setup(t) evals(f.Evaler, `f = {|q| put [&to-accept='q '$q &to-show=(styled 'q '$q blue)] }`, `edit:listing:start-custom $f &caption=A`) // Query. f.TTYCtrl.Inject(term.K('x')) f.TestTTY(t, "~> \n", "A x", Styles, "* ", term.DotHere, "\n", "q x ", Styles, "##################################################", ) // No-op accept. f.TTYCtrl.Inject(term.K('\n')) f.TestTTY(t, "~> ", term.DotHere) } func TestCustomListing_PassingBytesCallback(t *testing.T) { f := setup(t) evals(f.Evaler, `f = {|q| echo '# '$q }`, `edit:listing:start-custom $f &accept=$edit:insert-at-dot~ &caption=A `+ `&binding=(edit:binding-table [&Ctrl-X=$edit:listing:accept~])`) // Test that the query function is used to generate candidates. Also test // the caption. f.TTYCtrl.Inject(term.K('x')) f.TestTTY(t, "~> \n", "A x", Styles, "* ", term.DotHere, "\n", "# x ", Styles, "++++++++++++++++++++++++++++++++++++++++++++++++++", ) // Test both the binding and the accept callback. f.TTYCtrl.Inject(term.K('X', ui.Ctrl)) f.TestTTY(t, "~> # x", Styles, " ccc", term.DotHere) } elvish-0.17.0/pkg/edit/listing_windows_test.go000066400000000000000000000047221415471104000214120ustar00rootroot00000000000000package edit import ( "os" "path/filepath" "reflect" "regexp" "sort" "testing" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/store/storedefs" "src.elv.sh/pkg/testutil" "src.elv.sh/pkg/ui" ) func TestLocationAddon(t *testing.T) { f := setup(t, storeOp(func(s storedefs.Store) { s.AddDir(`C:\usr\bin`, 1) s.AddDir(`C:\tmp`, 1) s.AddDir(`C:\home\elf`, 1) })) evals(f.Evaler, `edit:location:pinned = ['C:\opt']`, `edit:location:hidden = ['C:\tmp']`) f.TTYCtrl.Inject(term.K('L', ui.Ctrl)) f.TestTTY(t, "~> \n", " LOCATION ", Styles, "********** ", term.DotHere, "\n", ` * C:\opt `+"\n", Styles, "++++++++++++++++++++++++++++++++++++++++++++++++++", ` 10 C:\home\elf`+"\n", ` 10 C:\usr\bin`, ) } func TestLocationAddon_Workspace(t *testing.T) { f := setup(t, storeOp(func(s storedefs.Store) { s.AddDir(`C:\usr\bin`, 1) s.AddDir(`ws\bin`, 1) s.AddDir(`other-ws\bin`, 1) })) testutil.ApplyDir( testutil.Dir{ "ws1": testutil.Dir{ "bin": testutil.Dir{}, "tmp": testutil.Dir{}}}) err := os.Chdir(`ws1\tmp`) if err != nil { t.Skip("chdir:", err) } evals(f.Evaler, `edit:location:workspaces = [&ws='`+ regexp.QuoteMeta(f.Home)+`\\'ws.]`) f.TTYCtrl.Inject(term.K('L', ui.Ctrl)) f.TestTTY(t, `~\ws1\tmp> `+"\n", " LOCATION ", Styles, "********** ", term.DotHere, "\n", ` 10 ws\bin `+"\n", Styles, "++++++++++++++++++++++++++++++++++++++++++++++++++", ` 10 C:\usr\bin`, ) f.TTYCtrl.Inject(term.K(ui.Enter)) f.TestTTY(t, `~\ws1\bin> `, term.DotHere) } func TestLocation_AddDir(t *testing.T) { f := setup(t) testutil.ApplyDir( testutil.Dir{ "bin": testutil.Dir{}, "ws1": testutil.Dir{ "bin": testutil.Dir{}}}) evals(f.Evaler, `edit:location:workspaces = [&ws='`+ regexp.QuoteMeta(f.Home)+`\\'ws.]`) chdir := func(path string) { err := f.Evaler.Chdir(path) if err != nil { t.Skip("chdir:", err) } } chdir("bin") chdir(`..\ws1\bin`) entries, err := f.Store.Dirs(map[string]struct{}{}) if err != nil { t.Error("unable to list dir history:", err) } dirs := make([]string, len(entries)) for i, entry := range entries { dirs[i] = entry.Path } wantDirs := []string{ filepath.Join(f.Home, "bin"), filepath.Join(f.Home, "ws1", "bin"), filepath.Join("ws", "bin"), } sort.Strings(dirs) sort.Strings(wantDirs) if !reflect.DeepEqual(dirs, wantDirs) { t.Errorf("got dirs %v, want %v", dirs, wantDirs) } } elvish-0.17.0/pkg/edit/minibuf.go000066400000000000000000000024441415471104000165600ustar00rootroot00000000000000package edit import ( "src.elv.sh/pkg/cli/modes" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/parse" ) func initMinibuf(ed *Editor, ev *eval.Evaler, nb eval.NsBuilder) { bindingVar := newBindingVar(emptyBindingsMap) bindings := newMapBindings(ed, ev, bindingVar) nb.AddNs("minibuf", eval.BuildNsNamed("edit:minibuf"). AddVar("binding", bindingVar). AddGoFns(map[string]interface{}{ "start": func() { minibufStart(ed, ev, bindings) }, })) } func minibufStart(ed *Editor, ev *eval.Evaler, bindings tk.Bindings) { w := tk.NewCodeArea(tk.CodeAreaSpec{ Prompt: modes.Prompt(" MINIBUF ", true), Bindings: bindings, OnSubmit: func() { minibufSubmit(ed, ev) }, // TODO: Add Highlighter. Right now the async highlighter is not // directly usable. }) ed.app.PushAddon(w) ed.app.Redraw() } func minibufSubmit(ed *Editor, ev *eval.Evaler) { app := ed.app codeArea, ok := app.ActiveWidget().(tk.CodeArea) if !ok { return } ed.app.PopAddon() code := codeArea.CopyState().Buffer.Content src := parse.Source{Name: "[minibuf]", Code: code} notifyPort, cleanup := makeNotifyPort(ed) defer cleanup() ports := []*eval.Port{eval.DummyInputPort, notifyPort, notifyPort} err := ev.Eval(src, eval.EvalCfg{Ports: ports}) if err != nil { app.Notify(err.Error()) } } elvish-0.17.0/pkg/edit/minibuf_test.go000066400000000000000000000005361415471104000176170ustar00rootroot00000000000000package edit import ( "testing" "src.elv.sh/pkg/cli/term" ) func TestMinibuf(t *testing.T) { f := setup(t) evals(f.Evaler, `edit:minibuf:start`) f.TestTTY(t, "~> \n", " MINIBUF ", Styles, "********* ", term.DotHere, ) feedInput(f.TTYCtrl, "edit:insert-at-dot put\n") f.TestTTY(t, "~> put", Styles, " vvv", term.DotHere, ) } elvish-0.17.0/pkg/edit/navigation.go000066400000000000000000000110041415471104000172560ustar00rootroot00000000000000package edit import ( "strings" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/modes" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" "src.elv.sh/pkg/parse" ) //elvdoc:var selected-file // // Name of the currently selected file in navigation mode. $nil if not in // navigation mode. //elvdoc:var navigation:binding // // Keybinding for the navigation mode. //elvdoc:fn navigation:start // // Start the navigation mode. //elvdoc:fn navigation:insert-selected // // Inserts the selected filename. func navInsertSelected(app cli.App) { w, ok := activeNavigation(app) if !ok { return } codeArea, ok := focusedCodeArea(app) if !ok { return } fname := w.SelectedName() if fname == "" { // User pressed Alt-Enter or Enter in an empty directory with nothing // selected; don't do anything. return } codeArea.MutateState(func(s *tk.CodeAreaState) { dot := s.Buffer.Dot if dot != 0 && !strings.ContainsRune(" \n", rune(s.Buffer.Content[dot-1])) { // The dot is not at the beginning of a buffer, and the previous // character is not a space or newline. Insert a space. s.Buffer.InsertAtDot(" ") } // Insert the selected filename. s.Buffer.InsertAtDot(parse.Quote(fname)) }) } //elvdoc:fn navigation:insert-selected-and-quit // // Inserts the selected filename and closes the navigation addon. func navInsertSelectedAndQuit(app cli.App) { navInsertSelected(app) closeMode(app) } //elvdoc:fn navigation:trigger-filter // // Toggles the filtering status of the navigation addon. //elvdoc:fn navigation:trigger-shown-hidden // // Toggles whether the navigation addon should be showing hidden files. //elvdoc:var navigation:width-ratio // // A list of 3 integers, used for specifying the width ratio of the 3 columns in // navigation mode. func convertNavWidthRatio(v interface{}) [3]int { var ( numbers []int hasErr bool ) vals.Iterate(v, func(elem interface{}) bool { var i int err := vals.ScanToGo(elem, &i) if err != nil { hasErr = true return false } numbers = append(numbers, i) return true }) if hasErr || len(numbers) != 3 { // TODO: Handle the error. return [3]int{1, 3, 4} } var ret [3]int copy(ret[:], numbers) return ret } func initNavigation(ed *Editor, ev *eval.Evaler, nb eval.NsBuilder) { bindingVar := newBindingVar(emptyBindingsMap) bindings := newMapBindings(ed, ev, bindingVar) widthRatioVar := newListVar(vals.MakeList(1.0, 3.0, 4.0)) selectedFileVar := vars.FromGet(func() interface{} { if w, ok := activeNavigation(ed.app); ok { return w.SelectedName() } return nil }) app := ed.app nb.AddVar("selected-file", selectedFileVar) nb.AddNs("navigation", eval.BuildNsNamed("edit:navigation"). AddVars(map[string]vars.Var{ "binding": bindingVar, "width-ratio": widthRatioVar, }). AddGoFns(map[string]interface{}{ "start": func() { w, err := modes.NewNavigation(app, modes.NavigationSpec{ Bindings: bindings, WidthRatio: func() [3]int { return convertNavWidthRatio(widthRatioVar.Get()) }, Filter: filterSpec, }) if err != nil { app.Notify(err.Error()) } else { startMode(app, w, nil) } }, "left": actOnNavigation(app, modes.Navigation.Ascend), "right": actOnNavigation(app, modes.Navigation.Descend), "up": actOnNavigation(app, func(w modes.Navigation) { w.Select(tk.Prev) }), "down": actOnNavigation(app, func(w modes.Navigation) { w.Select(tk.Next) }), "page-up": actOnNavigation(app, func(w modes.Navigation) { w.Select(tk.PrevPage) }), "page-down": actOnNavigation(app, func(w modes.Navigation) { w.Select(tk.NextPage) }), "file-preview-up": actOnNavigation(app, func(w modes.Navigation) { w.ScrollPreview(-1) }), "file-preview-down": actOnNavigation(app, func(w modes.Navigation) { w.ScrollPreview(1) }), "insert-selected": func() { navInsertSelected(app) }, "insert-selected-and-quit": func() { navInsertSelectedAndQuit(app) }, "trigger-filter": actOnNavigation(app, func(w modes.Navigation) { w.MutateFiltering(neg) }), "trigger-shown-hidden": actOnNavigation(app, func(w modes.Navigation) { w.MutateShowHidden(neg) }), })) } func neg(b bool) bool { return !b } func activeNavigation(app cli.App) (modes.Navigation, bool) { w, ok := app.ActiveWidget().(modes.Navigation) return w, ok } func actOnNavigation(app cli.App, f func(modes.Navigation)) func() { return func() { if w, ok := activeNavigation(app); ok { f(w) } } } elvish-0.17.0/pkg/edit/navigation_test.go000066400000000000000000000105541415471104000203260ustar00rootroot00000000000000package edit import ( "path/filepath" "testing" "src.elv.sh/pkg/cli/lscolors" "src.elv.sh/pkg/testutil" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/ui" ) func TestNavigation(t *testing.T) { f := setupNav(t) feedInput(f.TTYCtrl, "put") f.TTYCtrl.Inject(term.K('N', ui.Ctrl)) f.TestTTY(t, filepath.Join("~", "d"), "> ", "put", Styles, "vvv", term.DotHere, "\n", " NAVIGATING \n", Styles, "************ ", " d a \n", Styles, "###### ++++++++++++++++++ ", " e ", Styles, " //////////////////", ) // Test $edit:selected-file. evals(f.Evaler, `file = $edit:selected-file`) wantFile := "a" if file, _ := f.Evaler.Global().Index("file"); file != wantFile { t.Errorf("Got $edit:selected-file %q, want %q", file, wantFile) } // Test Alt-Enter: inserts filename without quitting. f.TTYCtrl.Inject(term.K(ui.Enter, ui.Alt)) f.TestTTY(t, filepath.Join("~", "d"), "> ", "put a", Styles, "vvv ", term.DotHere, "\n", " NAVIGATING \n", Styles, "************ ", " d a \n", Styles, "###### ++++++++++++++++++ ", " e ", Styles, " //////////////////", ) // Test Enter: inserts filename and quits. f.TTYCtrl.Inject(term.K(ui.Enter)) f.TestTTY(t, filepath.Join("~", "d"), "> ", "put a a", Styles, "vvv ", term.DotHere, ) } func TestNavigation_WidthRatio(t *testing.T) { f := setupNav(t) evals(f.Evaler, `@edit:navigation:width-ratio = 1 1 1`) f.TTYCtrl.Inject(term.K('N', ui.Ctrl)) f.TestTTY(t, filepath.Join("~", "d"), "> ", term.DotHere, "\n", " NAVIGATING \n", Styles, "************ ", " d a \n", Styles, "################ ++++++++++++++++ ", " e ", Styles, " ////////////////", ) } // Test corner case: Inserting a selection when the CLI cursor is not at the // start of the edit buffer, but the preceding char is a space, does not // insert another space. func TestNavigation_EnterDoesNotAddSpaceAfterSpace(t *testing.T) { f := setupNav(t) feedInput(f.TTYCtrl, "put ") f.TTYCtrl.Inject(term.K('N', ui.Ctrl)) // begin navigation mode f.TTYCtrl.Inject(term.K(ui.Down)) // select "e" f.TTYCtrl.Inject(term.K(ui.Enter)) // insert the "e" file name f.TestTTY(t, filepath.Join("~", "d"), "> ", "put e", Styles, "vvv", term.DotHere, ) } // Test corner case: Inserting a selection when the CLI cursor is at the start // of the edit buffer omits the space char prefix. func TestNavigation_EnterDoesNotAddSpaceAtStartOfBuffer(t *testing.T) { f := setupNav(t) f.TTYCtrl.Inject(term.K('N', ui.Ctrl)) // begin navigation mode f.TTYCtrl.Inject(term.K(ui.Enter)) // insert the "a" file name f.TestTTY(t, filepath.Join("~", "d"), "> ", "a", Styles, "!", term.DotHere, ) } // Test corner case: Inserting a selection when the CLI cursor is at the start // of a line buffer omits the space char prefix. func TestNavigation_EnterDoesNotAddSpaceAtStartOfLine(t *testing.T) { f := setupNav(t) feedInput(f.TTYCtrl, "put [\n") f.TTYCtrl.Inject(term.K('N', ui.Ctrl)) // begin navigation mode f.TTYCtrl.Inject(term.K(ui.Enter)) // insert the "a" file name f.TestTTY(t, filepath.Join("~", "d"), "> ", "put [", Styles, "vvv b", "\n", " a", term.DotHere, ) } // Test corner case: Inserting the "selection" in an empty directory inserts // nothing. Regression test for https://b.elv.sh/1169. func TestNavigation_EnterDoesNothingInEmptyDir(t *testing.T) { f := setupNav(t) feedInput(f.TTYCtrl, "pu") f.TTYCtrl.Inject(term.K('N', ui.Ctrl)) // begin navigation mode f.TTYCtrl.Inject(term.K(ui.Down)) // select empty directory "e" f.TTYCtrl.Inject(term.K(ui.Right)) // move into "e" directory f.TTYCtrl.Inject(term.K(ui.Enter, ui.Alt)) // insert nothing since the dir is empty f.TTYCtrl.Inject(term.K('t')) // user presses 'a' f.TestTTY(t, filepath.Join("~", "d", "e"), "> ", "put", Styles, "vvv", term.DotHere, "\n", " NAVIGATING \n", Styles, "************ ", " a \n", Styles, " ", " e ", Styles, "######", ) } var testDir = testutil.Dir{ "d": testutil.Dir{ "a": "", "e": testutil.Dir{}, }, } func setupNav(c testutil.Cleanuper) *fixture { f := setup(c) lscolors.SetTestLsColors(c) testutil.ApplyDir(testDir) testutil.MustChdir("d") return f } elvish-0.17.0/pkg/edit/ns_helper.go000066400000000000000000000024261415471104000171060ustar00rootroot00000000000000package edit import ( "os" "strings" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/fsutil" ) // Calls the passed function for each variable name in namespace ns that can be // found from the top context. func eachVariableInTop(builtin, global *eval.Ns, ns string, f func(s string)) { switch ns { case "", ":": global.IterateKeysString(f) builtin.IterateKeysString(f) case "e:": fsutil.EachExternal(func(cmd string) { f(cmd + eval.FnSuffix) }) case "E:": for _, s := range os.Environ() { if i := strings.IndexByte(s, '='); i > 0 { f(s[:i]) } } default: segs := eval.SplitQNameSegs(ns) mod := global.IndexString(segs[0]) if mod == nil { mod = builtin.IndexString(segs[0]) } for _, seg := range segs[1:] { if mod == nil { return } mod = mod.Get().(*eval.Ns).IndexString(seg) } if mod != nil { mod.Get().(*eval.Ns).IterateKeysString(f) } } } // Calls the passed function for each namespace that can be used from the top // context. func eachNsInTop(builtin, global *eval.Ns, f func(s string)) { f("e:") f("E:") global.IterateKeysString(func(name string) { if strings.HasSuffix(name, eval.NsSuffix) { f(name) } }) builtin.IterateKeysString(func(name string) { if strings.HasSuffix(name, eval.NsSuffix) { f(name) } }) } elvish-0.17.0/pkg/edit/ns_helper_test.go000066400000000000000000000043671415471104000201530ustar00rootroot00000000000000package edit import ( "reflect" "sort" "testing" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vars" ) var testVar = vars.NewReadOnly("") var eachVariableInTopTests = []struct { builtin eval.Nser global eval.Nser ns string wantNames []string }{ { builtin: eval.BuildNs().AddVar("foo", testVar).AddVar("bar", testVar), global: eval.BuildNs().AddVar("lorem", testVar).AddVar("ipsum", testVar), ns: "", wantNames: []string{"bar", "foo", "ipsum", "lorem"}, }, { builtin: eval.BuildNs().AddNs("mod", eval.BuildNs().AddVar("a", testVar).AddVar("b", testVar)), ns: "mod:", wantNames: []string{"a", "b"}, }, { global: eval.BuildNs().AddNs("mod", eval.BuildNs().AddVar("a", testVar).AddVar("b", testVar)), ns: "mod:", wantNames: []string{"a", "b"}, }, { ns: "mod:", wantNames: nil, }, } func TestEachVariableInTop(t *testing.T) { for _, test := range eachVariableInTopTests { builtin := getNs(test.builtin) global := getNs(test.global) var names []string eachVariableInTop(builtin, global, test.ns, func(s string) { names = append(names, s) }) sort.Strings(names) if !reflect.DeepEqual(names, test.wantNames) { t.Errorf("got names %v, want %v", names, test.wantNames) } } } var eachNsInTopTests = []struct { builtin eval.Nser global eval.Nser wantNames []string }{ { wantNames: []string{"E:", "e:"}, }, { builtin: eval.BuildNs().AddNs("foo", eval.BuildNs()), wantNames: []string{"E:", "e:", "foo:"}, }, { global: eval.BuildNs().AddNs("foo", eval.BuildNs()), wantNames: []string{"E:", "e:", "foo:"}, }, { builtin: eval.BuildNs().AddNs("foo", eval.BuildNs()), global: eval.BuildNs().AddNs("bar", eval.BuildNs()), wantNames: []string{"E:", "bar:", "e:", "foo:"}, }, } func TestEachNsInTop(t *testing.T) { for _, test := range eachNsInTopTests { builtin := getNs(test.builtin) global := getNs(test.global) var names []string eachNsInTop(builtin, global, func(s string) { names = append(names, s) }) sort.Strings(names) if !reflect.DeepEqual(names, test.wantNames) { t.Errorf("got names %v, want %v", names, test.wantNames) } } } func getNs(ns eval.Nser) *eval.Ns { if ns == nil { return new(eval.Ns) } return ns.Ns() } elvish-0.17.0/pkg/edit/prompt.go000066400000000000000000000110621415471104000164440ustar00rootroot00000000000000package edit import ( "io" "os" "os/user" "sync" "time" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/prompt" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" "src.elv.sh/pkg/fsutil" "src.elv.sh/pkg/ui" ) //elvdoc:var prompt // // See [Prompts](#prompts). //elvdoc:var -prompt-eagerness // // See [Prompt Eagerness](#prompt-eagerness). //elvdoc:var prompt-stale-threshold // // See [Stale Prompt](#stale-prompt). //elvdoc:var prompt-stale-transformer. // // See [Stale Prompt](#stale-prompt). //elvdoc:var rprompt // // See [Prompts](#prompts). //elvdoc:var -rprompt-eagerness // // See [Prompt Eagerness](#prompt-eagerness). //elvdoc:var rprompt-stale-threshold // // See [Stale Prompt](#stale-prompt). //elvdoc:var rprompt-stale-transformer. // // See [Stale Prompt](#stale-prompt). //elvdoc:var rprompt-persistent // // See [RPrompt Persistency](#rprompt-persistency). func initPrompts(appSpec *cli.AppSpec, nt notifier, ev *eval.Evaler, nb eval.NsBuilder) { promptVal, rpromptVal := getDefaultPromptVals() initPrompt(&appSpec.Prompt, "prompt", promptVal, nt, ev, nb) initPrompt(&appSpec.RPrompt, "rprompt", rpromptVal, nt, ev, nb) rpromptPersistentVar := newBoolVar(false) appSpec.RPromptPersistent = func() bool { return rpromptPersistentVar.Get().(bool) } nb.AddVar("rprompt-persistent", rpromptPersistentVar) } func initPrompt(p *cli.Prompt, name string, val eval.Callable, nt notifier, ev *eval.Evaler, nb eval.NsBuilder) { computeVar := vars.FromPtr(&val) nb.AddVar(name, computeVar) eagernessVar := newIntVar(5) nb.AddVar("-"+name+"-eagerness", eagernessVar) staleThresholdVar := newFloatVar(0.2) nb.AddVar(name+"-stale-threshold", staleThresholdVar) staleTransformVar := newFnVar( eval.NewGoFn("", defaultStaleTransform)) nb.AddVar(name+"-stale-transform", staleTransformVar) *p = prompt.New(prompt.Config{ Compute: func() ui.Text { return callForStyledText(nt, ev, name, computeVar.Get().(eval.Callable)) }, Eagerness: func() int { return eagernessVar.GetRaw().(int) }, StaleThreshold: func() time.Duration { seconds := staleThresholdVar.GetRaw().(float64) return time.Duration(seconds * float64(time.Second)) }, StaleTransform: func(original ui.Text) ui.Text { return callForStyledText(nt, ev, name+" stale transform", staleTransformVar.Get().(eval.Callable), original) }, }) } func getDefaultPromptVals() (prompt, rprompt eval.Callable) { user, userErr := user.Current() isRoot := userErr == nil && user.Uid == "0" username := "???" if userErr == nil { username = user.Username } hostname, err := os.Hostname() if err != nil { hostname = "???" } return getDefaultPrompt(isRoot), getDefaultRPrompt(username, hostname) } func getDefaultPrompt(isRoot bool) eval.Callable { p := ui.T("> ") if isRoot { p = ui.T("# ", ui.FgRed) } return eval.NewGoFn("default prompt", func() ui.Text { return ui.Concat(ui.T(fsutil.Getwd()), p) }) } func getDefaultRPrompt(username, hostname string) eval.Callable { rp := ui.T(username+"@"+hostname, ui.Inverse) return eval.NewGoFn("default rprompt", func() ui.Text { return rp }) } func defaultStaleTransform(original ui.Text) ui.Text { return ui.StyleText(original, ui.Inverse) } // Calls a function with the given arguments and closed input, and concatenates // its outputs to a styled text. Used to call prompts and stale transformers. func callForStyledText(nt notifier, ev *eval.Evaler, ctx string, fn eval.Callable, args ...interface{}) ui.Text { var ( result ui.Text resultMutex sync.Mutex ) add := func(v interface{}) { resultMutex.Lock() defer resultMutex.Unlock() newResult, err := result.Concat(v) if err != nil { nt.notifyf("invalid output type from prompt: %s", vals.Kind(v)) } else { result = newResult.(ui.Text) } } // Value outputs are concatenated. valuesCb := func(ch <-chan interface{}) { for v := range ch { add(v) } } // Byte output is added to the prompt as a single unstyled text. bytesCb := func(r *os.File) { allBytes, err := io.ReadAll(r) if err != nil { nt.notifyf("error reading prompt byte output: %v", err) } if len(allBytes) > 0 { add(ui.ParseSGREscapedText(string(allBytes))) } } port1, done1, err := eval.PipePort(valuesCb, bytesCb) if err != nil { nt.notifyf("cannot create pipe for prompt: %v", err) return nil } port2, done2 := makeNotifyPort(nt) err = ev.Call(fn, eval.CallCfg{Args: args, From: "[" + ctx + "]"}, eval.EvalCfg{Ports: []*eval.Port{nil, port1, port2}}) done1() done2() if err != nil { nt.notifyError(ctx, err) } return result } elvish-0.17.0/pkg/edit/prompt_test.go000066400000000000000000000103301415471104000175000ustar00rootroot00000000000000package edit import ( "fmt" "strings" "testing" "time" "src.elv.sh/pkg/cli/clitest" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/testutil" "src.elv.sh/pkg/ui" ) func TestPrompt_ValueOutput(t *testing.T) { f := setup(t, rc(`edit:prompt = { put '#'; num 13; styled '> ' red }`)) f.TestTTY(t, "#13> ", Styles, " !!", term.DotHere) } func TestPrompt_ByteOutput(t *testing.T) { f := setup(t, rc(`edit:prompt = { print 'bytes> ' }`)) f.TestTTY(t, "bytes> ", term.DotHere) } func TestPrompt_ParsesSGRInByteOutput(t *testing.T) { f := setup(t, rc(`edit:prompt = { print "\033[31mred\033[m> " }`)) f.TestTTY(t, "red> ", Styles, "!!! ", term.DotHere) } func TestPrompt_NotifiesInvalidValueOutput(t *testing.T) { f := setup(t, rc(`edit:prompt = { put good [bad] good2 }`)) f.TestTTY(t, "goodgood2", term.DotHere) f.TestTTYNotes(t, "invalid output type from prompt: list") } func TestPrompt_NotifiesException(t *testing.T) { f := setup(t, rc(`edit:prompt = { fail ERROR }`)) f.TestTTYNotes(t, "[prompt error] ERROR\n", `see stack trace with "show $edit:exceptions[0]"`) evals(f.Evaler, `excs = (count $edit:exceptions)`) testGlobal(t, f.Evaler, "excs", 1) } func TestRPrompt(t *testing.T) { f := setup(t, rc(`edit:rprompt = { put 'RRR' }`)) f.TestTTY(t, "~> ", term.DotHere, strings.Repeat(" ", clitest.FakeTTYWidth-6)+"RRR") } func TestPromptEagerness(t *testing.T) { f := setup(t, rc( `i = 0`, `edit:prompt = { i = (+ $i 1); put $i'> ' }`, `edit:-prompt-eagerness = 10`)) f.TestTTY(t, "1> ", term.DotHere) // With eagerness = 10, any key press will cause the prompt to be // recomputed. f.TTYCtrl.Inject(term.K(ui.Backspace)) f.TestTTY(t, "2> ", term.DotHere) } func TestPromptStaleThreshold(t *testing.T) { f := setup(t, rc( `pipe = (file:pipe)`, `edit:prompt = { nop (slurp < $pipe); put '> ' }`, `edit:prompt-stale-threshold = `+scaledMsAsSec(50))) f.TestTTY(t, "???> ", Styles, "+++++", term.DotHere) evals(f.Evaler, `file:close $pipe[w]`) f.TestTTY(t, "> ", term.DotHere) evals(f.Evaler, `file:close $pipe[r]`) } func TestPromptStaleTransform(t *testing.T) { f := setup(t, rc( `pipe = (file:pipe)`, `edit:prompt = { nop (slurp < $pipe); put '> ' }`, `edit:prompt-stale-threshold = `+scaledMsAsSec(50), `edit:prompt-stale-transform = {|a| put S; put $a; put S }`)) f.TestTTY(t, "S???> S", term.DotHere) evals(f.Evaler, `file:close $pipe[w]`) evals(f.Evaler, `file:close $pipe[r]`) } func TestPromptStaleTransform_Exception(t *testing.T) { f := setup(t, rc( `pipe = (file:pipe)`, `edit:prompt = { nop (slurp < $pipe); put '> ' }`, `edit:prompt-stale-threshold = `+scaledMsAsSec(50), `edit:prompt-stale-transform = {|_| fail ERROR }`)) f.TestTTYNotes(t, "[prompt stale transform error] ERROR\n", `see stack trace with "show $edit:exceptions[0]"`) evals(f.Evaler, `excs = (count $edit:exceptions)`) testGlobal(t, f.Evaler, "excs", 1) } func TestRPromptPersistent_True(t *testing.T) { testRPromptPersistent(t, `edit:rprompt-persistent = $true`, "~> "+strings.Repeat(" ", clitest.FakeTTYWidth-6)+"RRR", "\n", term.DotHere, ) } func TestRPromptPersistent_False(t *testing.T) { testRPromptPersistent(t, `edit:rprompt-persistent = $false`, "~> ", // no rprompt "\n", term.DotHere, ) } func testRPromptPersistent(t *testing.T, code string, finalBuf ...interface{}) { f := setup(t, rc(`edit:rprompt = { put RRR }`, code)) // Make sure that the UI has stabilized before hitting Enter. f.TestTTY(t, "~> ", term.DotHere, strings.Repeat(" ", clitest.FakeTTYWidth-6), "RRR", ) f.TTYCtrl.Inject(term.K('\n')) f.TestTTY(t, finalBuf...) } func TestDefaultPromptForNonRoot(t *testing.T) { f := setup(t, assign("edit:prompt", getDefaultPrompt(false))) f.TestTTY(t, "~> ", term.DotHere) } func TestDefaultPromptForRoot(t *testing.T) { f := setup(t, assign("edit:prompt", getDefaultPrompt(true))) f.TestTTY(t, "~# ", Styles, " !!", term.DotHere) } func TestDefaultRPrompt(t *testing.T) { f := setup(t, assign("edit:rprompt", getDefaultRPrompt("elf", "host"))) f.TestTTY(t, "~> ", term.DotHere, strings.Repeat(" ", 39), "elf@host", Styles, "++++++++") } func scaledMsAsSec(ms int) string { return fmt.Sprint(testutil.Scaled(time.Duration(ms) * time.Millisecond).Seconds()) } elvish-0.17.0/pkg/edit/repl.go000066400000000000000000000040371415471104000160710ustar00rootroot00000000000000package edit // This file encapsulates functionality related to a complete REPL cycle. Such as capturing // information about the most recently executed interactive command. import ( "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" "src.elv.sh/pkg/parse" ) //elvdoc:var after-command // // A list of functions to call after each interactive command completes. There is one pre-defined // function used to populate the [`$edit:command-duration`](./edit.html#edit:command-duration) // variable. Each function is called with a single [map](https://elv.sh/ref/language.html#map) // argument containing the following keys: // // * `src`: Information about the source that was executed, same as what // [`src`](builtin.html#src) would output inside the code. // // * `duration`: A [floating-point number](https://elv.sh/ref/language.html#number) representing the // command execution duration in seconds. // // * `error`: An [exception](../ref/language.html#exception) object if the command terminated with // an exception, else [`$nil`](../ref/language.html#nil). // // @cf edit:command-duration //elvdoc:var command-duration // // Duration, in seconds, of the most recent interactive command. This can be useful in your prompt // to provide feedback on how long a command took to run. The initial value of this variable is the // time to evaluate your `~/.elvish/rc.elv` script before printing the first prompt. // // @cf edit:after-command func initRepl(ed *Editor, ev *eval.Evaler, nb eval.NsBuilder) { var commandDuration float64 // TODO: Ensure that this variable can only be written from the Elvish code // in elv_init.go. nb.AddVar("command-duration", vars.FromPtr(&commandDuration)) afterCommandHook := newListVar(vals.EmptyList) nb.AddVar("after-command", afterCommandHook) ed.AfterCommand = append(ed.AfterCommand, func(src parse.Source, duration float64, err error) { m := vals.MakeMap("src", src, "duration", duration, "error", err) callHooks(ev, "$:after-command", afterCommandHook.Get().(vals.List), m) }) } elvish-0.17.0/pkg/edit/state_api.go000066400000000000000000000044071415471104000171010ustar00rootroot00000000000000package edit import ( "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" ) //elvdoc:fn insert-at-dot // // ```elvish // edit:insert-at-dot $text // ``` // // Inserts the given text at the dot, moving the dot after the newly // inserted text. func insertAtDot(app cli.App, text string) { codeArea, ok := focusedCodeArea(app) if !ok { return } codeArea.MutateState(func(s *tk.CodeAreaState) { s.Buffer.InsertAtDot(text) }) } //elvdoc:fn replace-input // // ```elvish // edit:replace-input $text // ``` // // Equivalent to assigning `$text` to `$edit:current-command`. func replaceInput(app cli.App, text string) { codeArea, ok := focusedCodeArea(app) if !ok { return } codeArea.MutateState(func(s *tk.CodeAreaState) { s.Buffer = tk.CodeBuffer{Content: text, Dot: len(text)} }) } //elvdoc:var -dot // // Contains the current position of the cursor, as a byte position within // `$edit:current-command`. //elvdoc:var current-command // // Contains the content of the current input. Setting the variable will // cause the cursor to move to the very end, as if `edit-dot = (count // $edit:current-command)` has been invoked. // // This API is subject to change. func initStateAPI(app cli.App, nb eval.NsBuilder) { // State API always operates on the root CodeArea widget codeArea := app.ActiveWidget().(tk.CodeArea) nb.AddGoFns(map[string]interface{}{ "insert-at-dot": func(s string) { insertAtDot(app, s) }, "replace-input": func(s string) { replaceInput(app, s) }, }) setDot := func(v interface{}) error { var dot int err := vals.ScanToGo(v, &dot) if err != nil { return err } codeArea.MutateState(func(s *tk.CodeAreaState) { s.Buffer.Dot = dot }) return nil } getDot := func() interface{} { return vals.FromGo(codeArea.CopyState().Buffer.Dot) } nb.AddVar("-dot", vars.FromSetGet(setDot, getDot)) setCurrentCommand := func(v interface{}) error { var content string err := vals.ScanToGo(v, &content) if err != nil { return err } replaceInput(app, content) return nil } getCurrentCommand := func() interface{} { return vals.FromGo(codeArea.CopyState().Buffer.Content) } nb.AddVar("current-command", vars.FromSetGet(setCurrentCommand, getCurrentCommand)) } elvish-0.17.0/pkg/edit/state_api_test.go000066400000000000000000000021151415471104000201320ustar00rootroot00000000000000package edit import ( "testing" "src.elv.sh/pkg/cli/tk" ) func TestInsertAtDot(t *testing.T) { f := setup(t) f.SetCodeBuffer(tk.CodeBuffer{Content: "ab", Dot: 1}) evals(f.Evaler, `edit:insert-at-dot XYZ`) testCodeBuffer(t, f.Editor, tk.CodeBuffer{Content: "aXYZb", Dot: 4}) } func TestReplaceInput(t *testing.T) { f := setup(t) f.SetCodeBuffer(tk.CodeBuffer{Content: "ab", Dot: 1}) evals(f.Evaler, `edit:replace-input XYZ`) testCodeBuffer(t, f.Editor, tk.CodeBuffer{Content: "XYZ", Dot: 3}) } func TestDot(t *testing.T) { f := setup(t) f.SetCodeBuffer(tk.CodeBuffer{Content: "code", Dot: 4}) evals(f.Evaler, `edit:-dot = 0`) testCodeBuffer(t, f.Editor, tk.CodeBuffer{Content: "code", Dot: 0}) } func TestCurrentCommand(t *testing.T) { f := setup(t) evals(f.Evaler, `edit:current-command = code`) testCodeBuffer(t, f.Editor, tk.CodeBuffer{Content: "code", Dot: 4}) } func testCodeBuffer(t *testing.T, ed *Editor, wantBuf tk.CodeBuffer) { t.Helper() if buf := codeArea(ed.app).CopyState().Buffer; buf != wantBuf { t.Errorf("content = %v, want %v", buf, wantBuf) } } elvish-0.17.0/pkg/edit/store_api.go000066400000000000000000000066621415471104000171220ustar00rootroot00000000000000package edit import ( "errors" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/histutil" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/parse/parseutil" "src.elv.sh/pkg/store/storedefs" ) var errStoreOffline = errors.New("store offline") //elvdoc:fn command-history // // ```elvish // edit:command-history &cmd-only=$false &dedup=$false &newest-first // ``` // // Outputs the command history. // // By default, each entry is represented as a map, with an `id` key key for the // sequence number of the command, and a `cmd` key for the text of the command. // If `&cmd-only` is `$true`, only the text of each command is output. // // All entries are output by default. If `&dedup` is `$true`, only the most // recent instance of each command (when comparing just the `cmd` key) is // output. // // Commands are are output in oldest to newest order by default. If // `&newest-first` is `$true` the output is in newest to oldest order instead. // // As an example, either of the following extracts the text of the most recent // command: // // ```elvish // edit:command-history | put [(all)][-1][cmd] // edit:command-history &cmd-only &newest-first | take 1 // ``` // // @cf builtin:dir-history type cmdhistOpt struct{ CmdOnly, Dedup, NewestFirst bool } func (o *cmdhistOpt) SetDefaultOptions() {} func commandHistory(opts cmdhistOpt, fuser histutil.Store, out eval.ValueOutput) error { if fuser == nil { return errStoreOffline } cmds, err := fuser.AllCmds() if err != nil { return err } if opts.Dedup { cmds = dedupCmds(cmds, opts.NewestFirst) } else if opts.NewestFirst { reverseCmds(cmds) } if opts.CmdOnly { for _, cmd := range cmds { err := out.Put(cmd.Text) if err != nil { return err } } } else { for _, cmd := range cmds { err := out.Put(vals.MakeMap("id", cmd.Seq, "cmd", cmd.Text)) if err != nil { return err } } } return nil } func dedupCmds(allCmds []storedefs.Cmd, newestFirst bool) []storedefs.Cmd { // Capacity allocation below is based on some personal empirical observation. uniqCmds := make([]storedefs.Cmd, 0, len(allCmds)/4) seenCmds := make(map[string]bool, len(allCmds)/4) for i := len(allCmds) - 1; i >= 0; i-- { if !seenCmds[allCmds[i].Text] { seenCmds[allCmds[i].Text] = true uniqCmds = append(uniqCmds, allCmds[i]) } } if !newestFirst { reverseCmds(uniqCmds) } return uniqCmds } // Reverse the order of commands, in place, in the slice. This reorders the // command history between oldest or newest command being first in the slice. func reverseCmds(cmds []storedefs.Cmd) { for i, j := 0, len(cmds)-1; i < j; i, j = i+1, j-1 { cmds[i], cmds[j] = cmds[j], cmds[i] } } //elvdoc:fn insert-last-word // // Inserts the last word of the last command. func insertLastWord(app cli.App, histStore histutil.Store) error { codeArea, ok := focusedCodeArea(app) if !ok { return nil } c := histStore.Cursor("") c.Prev() cmd, err := c.Get() if err != nil { return err } words := parseutil.Wordify(cmd.Text) if len(words) > 0 { codeArea.MutateState(func(s *tk.CodeAreaState) { s.Buffer.InsertAtDot(words[len(words)-1]) }) } return nil } func initStoreAPI(app cli.App, nb eval.NsBuilder, fuser histutil.Store) { nb.AddGoFns(map[string]interface{}{ "command-history": func(fm *eval.Frame, opts cmdhistOpt) error { return commandHistory(opts, fuser, fm.ValueOutput()) }, "insert-last-word": func() { insertLastWord(app, fuser) }, }) } elvish-0.17.0/pkg/edit/store_api_test.go000066400000000000000000000042321415471104000201500ustar00rootroot00000000000000package edit import ( "testing" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/store/storedefs" ) func TestCommandHistory(t *testing.T) { f := setup(t, storeOp(func(s storedefs.Store) { s.AddCmd("echo 0") s.AddCmd("echo 1") s.AddCmd("echo 2") s.AddCmd("echo 2") s.AddCmd("echo 1") s.AddCmd("echo 3") s.AddCmd("echo 1") })) // TODO(xiaq): Test session history too. See NewHybridStore and NewMemStore. evals(f.Evaler, `@cmds = (edit:command-history)`) testGlobal(t, f.Evaler, "cmds", vals.MakeList( cmdMap(1, "echo 0"), cmdMap(2, "echo 1"), cmdMap(3, "echo 2"), cmdMap(4, "echo 2"), cmdMap(5, "echo 1"), cmdMap(6, "echo 3"), cmdMap(7, "echo 1"), )) evals(f.Evaler, `@cmds = (edit:command-history &newest-first)`) testGlobal(t, f.Evaler, "cmds", vals.MakeList( cmdMap(7, "echo 1"), cmdMap(6, "echo 3"), cmdMap(5, "echo 1"), cmdMap(4, "echo 2"), cmdMap(3, "echo 2"), cmdMap(2, "echo 1"), cmdMap(1, "echo 0"), )) evals(f.Evaler, `@cmds = (edit:command-history &dedup)`) testGlobal(t, f.Evaler, "cmds", vals.MakeList( cmdMap(1, "echo 0"), cmdMap(4, "echo 2"), cmdMap(6, "echo 3"), cmdMap(7, "echo 1"), )) evals(f.Evaler, `@cmds = (edit:command-history &dedup &newest-first)`) testGlobal(t, f.Evaler, "cmds", vals.MakeList( cmdMap(7, "echo 1"), cmdMap(6, "echo 3"), cmdMap(4, "echo 2"), cmdMap(1, "echo 0"), )) evals(f.Evaler, `@cmds = (edit:command-history &dedup &newest-first &cmd-only)`) testGlobal(t, f.Evaler, "cmds", vals.MakeList( "echo 1", "echo 3", "echo 2", "echo 0", )) testThatOutputErrorIsBubbled(t, f, "edit:command-history") testThatOutputErrorIsBubbled(t, f, "edit:command-history &cmd-only") } func cmdMap(id int, cmd string) vals.Map { return vals.MakeMap("id", id, "cmd", cmd) } func TestInsertLastWord(t *testing.T) { f := setup(t, storeOp(func(s storedefs.Store) { s.AddCmd("echo foo bar") })) evals(f.Evaler, "edit:insert-last-word") wantBuf := tk.CodeBuffer{Content: "bar", Dot: 3} if buf := codeArea(f.Editor.app).CopyState().Buffer; buf != wantBuf { t.Errorf("buf = %v, want %v", buf, wantBuf) } } elvish-0.17.0/pkg/edit/testutils_test.go000066400000000000000000000073271415471104000202330ustar00rootroot00000000000000package edit import ( "fmt" "testing" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/clitest" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" "src.elv.sh/pkg/mods/file" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/store" "src.elv.sh/pkg/store/storedefs" "src.elv.sh/pkg/testutil" ) var Styles = clitest.Styles type fixture struct { Editor *Editor TTYCtrl clitest.TTYCtrl Evaler *eval.Evaler Store storedefs.Store Home string width int codeCh <-chan string errCh <-chan error } func rc(codes ...string) func(*fixture) { return func(f *fixture) { evals(f.Evaler, codes...) } } func assign(name string, val interface{}) func(*fixture) { return func(f *fixture) { f.Evaler.ExtendGlobal(eval.BuildNs().AddVar("temp", vars.NewReadOnly(val))) evals(f.Evaler, name+` = $temp`) } } func storeOp(storeFn func(storedefs.Store)) func(*fixture) { return func(f *fixture) { storeFn(f.Store) // TODO(xiaq): Don't depend on this Elvish API. evals(f.Evaler, "edit:history:fast-forward") } } func setup(c testutil.Cleanuper, fns ...func(*fixture)) *fixture { st := store.MustTempStore(c) home := testutil.InTempHome(c) testutil.Setenv(c, "PATH", "") tty, ttyCtrl := clitest.NewFakeTTY() ev := eval.NewEvaler() ev.ExtendGlobal(eval.BuildNs().AddNs("file", file.Ns)) ed := NewEditor(tty, ev, st) ev.ExtendBuiltin(eval.BuildNs().AddNs("edit", ed)) evals(ev, // This is the same as the default prompt for non-root users. This makes // sure that the tests will work when run as root. "edit:prompt = { tilde-abbr $pwd; put '> ' }", // This will simplify most tests against the terminal. "edit:rprompt = { }") f := &fixture{Editor: ed, TTYCtrl: ttyCtrl, Evaler: ev, Store: st, Home: home} for _, fn := range fns { fn(f) } _, f.width = tty.Size() f.codeCh, f.errCh = clitest.StartReadCode(f.Editor.ReadCode) c.Cleanup(func() { f.Editor.app.CommitEOF() f.Wait() }) return f } func (f *fixture) Wait() (string, error) { return <-f.codeCh, <-f.errCh } func (f *fixture) MakeBuffer(args ...interface{}) *term.Buffer { return term.NewBufferBuilder(f.width).MarkLines(args...).Buffer() } func (f *fixture) TestTTY(t *testing.T, args ...interface{}) { t.Helper() f.TTYCtrl.TestBuffer(t, f.MakeBuffer(args...)) } func (f *fixture) TestTTYNotes(t *testing.T, args ...interface{}) { t.Helper() f.TTYCtrl.TestNotesBuffer(t, f.MakeBuffer(args...)) } func (f *fixture) SetCodeBuffer(b tk.CodeBuffer) { codeArea(f.Editor.app).MutateState(func(s *tk.CodeAreaState) { s.Buffer = b }) } func feedInput(ttyCtrl clitest.TTYCtrl, s string) { for _, r := range s { ttyCtrl.Inject(term.K(r)) } } func evals(ev *eval.Evaler, codes ...string) { for _, code := range codes { err := ev.Eval(parse.Source{Name: "[test]", Code: code}, eval.EvalCfg{}) if err != nil { panic(fmt.Errorf("eval %q: %s", code, err)) } } } func getGlobal(ev *eval.Evaler, name string) interface{} { v, _ := ev.Global().Index(name) return v } func testGlobals(t *testing.T, ev *eval.Evaler, wantVals map[string]interface{}) { t.Helper() for name, wantVal := range wantVals { testGlobal(t, ev, name, wantVal) } } func testGlobal(t *testing.T, ev *eval.Evaler, name string, wantVal interface{}) { t.Helper() if val := getGlobal(ev, name); !vals.Equal(val, wantVal) { t.Errorf("$%s = %s, want %s", name, vals.Repr(val, vals.NoPretty), vals.Repr(wantVal, vals.NoPretty)) } } func testThatOutputErrorIsBubbled(t *testing.T, f *fixture, code string) { t.Helper() evals(f.Evaler, "var ret = (bool ?("+code+" >&-))") // Exceptions are booleanly false testGlobal(t, f.Evaler, "ret", false) } func codeArea(app cli.App) tk.CodeArea { return app.ActiveWidget().(tk.CodeArea) } elvish-0.17.0/pkg/edit/vars.go000066400000000000000000000035431415471104000161030ustar00rootroot00000000000000package edit import ( "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" ) func initVarsAPI(ed *Editor, nb eval.NsBuilder) { nb.AddGoFns(map[string]interface{}{ "add-var": addVar, "add-vars": addVars, }) } //elvdoc:fn add-var // // ```elvish // edit:add-var $name $value // ``` // // Declares a new variable in the REPL. The new variable becomes available // during the next REPL cycle. // // Equivalent to running `var $name = $value` at the REPL, but `$name` can be // dynamic. // // Example: // // ```elvish-transcript // ~> edit:add-var foo bar // ~> put $foo // ▶ bar // ``` func addVar(fm *eval.Frame, name string, val interface{}) error { if !eval.IsUnqualified(name) { return errs.BadValue{ What: "name argument to edit:add-var", Valid: "unqualified variable name", Actual: name} } variable := eval.MakeVarFromName(name) err := variable.Set(val) if err != nil { return err } fm.Evaler.ExtendGlobal(eval.BuildNs().AddVar(name, vars.FromInit(val))) return nil } //elvdoc:fn add-vars // // ```elvish // edit:add-vars $map // ``` // // Takes a map from strings to arbitrary values. Equivalent to calling // `edit:add-var` for each key-value pair in the map. func addVars(fm *eval.Frame, m vals.Map) error { nb := eval.BuildNs() for it := m.Iterator(); it.HasElem(); it.Next() { k, val := it.Elem() name, ok := k.(string) if !ok { return errs.BadValue{ What: "key of argument to edit:add-vars", Valid: "string", Actual: vals.Kind(k)} } if !eval.IsUnqualified(name) { return errs.BadValue{ What: "key of argument to edit:add-vars", Valid: "unqualified variable name", Actual: name} } variable := eval.MakeVarFromName(name) err := variable.Set(val) if err != nil { return err } nb.AddVar(name, variable) } fm.Evaler.ExtendGlobal(nb) return nil } elvish-0.17.0/pkg/edit/vars_test.go000066400000000000000000000023121415471104000171330ustar00rootroot00000000000000package edit import ( "testing" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/errs" . "src.elv.sh/pkg/eval/evaltest" ) func TestAddVar(t *testing.T) { TestWithSetup(t, func(ev *eval.Evaler) { ev.ExtendGlobal(eval.BuildNs().AddGoFn("add-var", addVar)) }, That("add-var foo bar").Then("put $foo").Puts("bar"), // Qualified name That("add-var a:b ''").Throws( errs.BadValue{ What: "name argument to edit:add-var", Valid: "unqualified variable name", Actual: "a:b"}), // Bad type That("add-var a~ ''").Throws(AnyError), ) } func TestAddVars(t *testing.T) { TestWithSetup(t, func(ev *eval.Evaler) { ev.ExtendGlobal(eval.BuildNs().AddGoFn("add-vars", addVars)) }, That("add-vars [&foo=bar]").Then("put $foo").Puts("bar"), That("add-vars [&a=A &b=B]").Then("put $a $b").Puts("A", "B"), // Non-string key That("add-vars [&[]='']").Throws( errs.BadValue{ What: "key of argument to edit:add-vars", Valid: "string", Actual: "list"}), // Qualified name That("add-vars [&a:b='']").Throws( errs.BadValue{ What: "key of argument to edit:add-vars", Valid: "unqualified variable name", Actual: "a:b"}), // Bad type That("add-vars [&a~='']").Throws(AnyError), ) } elvish-0.17.0/pkg/env/000077500000000000000000000000001415471104000144375ustar00rootroot00000000000000elvish-0.17.0/pkg/env/env.go000066400000000000000000000011671415471104000155630ustar00rootroot00000000000000// Package env keeps names of environment variables with special significance to // Elvish. package env // Environment variables with special significance to Elvish. const ( HOME = "HOME" LS_COLORS = "LS_COLORS" PATH = "PATH" PWD = "PWD" SHLVL = "SHLVL" USERNAME = "USERNAME" // Only used on Unix XDG_CONFIG_HOME = "XDG_CONFIG_HOME" XDG_DATA_DIRS = "XDG_DATA_DIRS" XDG_DATA_HOME = "XDG_DATA_HOME" XDG_RUNTIME_DIR = "XDG_RUNTIME_DIR" XDG_STATE_HOME = "XDG_STATE_HOME" // Only used on Windows PATHEXT = "PATHEXT" // Only used in tests ELVISH_TEST_TIME_SCALE = "ELVISH_TEST_TIME_SCALE" ) elvish-0.17.0/pkg/eval/000077500000000000000000000000001415471104000145765ustar00rootroot00000000000000elvish-0.17.0/pkg/eval/benchmarks_test.go000066400000000000000000000016511415471104000203040ustar00rootroot00000000000000package eval import ( "testing" "src.elv.sh/pkg/parse" ) var benchmarks = []struct { name string code string }{ {"empty", ""}, {"nop", "nop"}, {"nop-nop", "nop | nop"}, {"put-x", "put x"}, {"for-100", "for x [(range 100)] { }"}, {"range-100", "range 100 | each {|_| }"}, {"read-local", "var x = val; nop $x"}, {"read-upval", "var x = val; { nop $x }"}, } func BenchmarkEval(b *testing.B) { for _, bench := range benchmarks { b.Run(bench.name, func(b *testing.B) { ev := NewEvaler() src := parse.Source{Name: "[benchmark]", Code: bench.code} tree, err := parse.Parse(src, parse.Config{}) if err != nil { panic(err) } op, err := ev.compile(tree, ev.Global(), nil) if err != nil { panic(err) } b.ResetTimer() for i := 0; i < b.N; i++ { fm, cleanup := ev.prepareFrame(src, EvalCfg{Global: ev.Global()}) _, exec := op.prepare(fm) _ = exec() cleanup() } }) } } elvish-0.17.0/pkg/eval/builtin_fn_cmd.go000066400000000000000000000040031415471104000200760ustar00rootroot00000000000000package eval import ( "os" "os/exec" "src.elv.sh/pkg/eval/errs" ) // Command and process control. // TODO(xiaq): Document "fg". func init() { addBuiltinFns(map[string]interface{}{ // Command resolution "external": external, "has-external": hasExternal, "search-external": searchExternal, // Process control "fg": fg, "exec": execFn, "exit": exit, }) } //elvdoc:fn external // // ```elvish // external $program // ``` // // Construct a callable value for the external program `$program`. Example: // // ```elvish-transcript // ~> x = (external man) // ~> $x ls # opens the manpage for ls // ``` // // @cf has-external search-external func external(cmd string) Callable { return NewExternalCmd(cmd) } //elvdoc:fn has-external // // ```elvish // has-external $command // ``` // // Test whether `$command` names a valid external command. Examples (your output // might differ): // // ```elvish-transcript // ~> has-external cat // ▶ $true // ~> has-external lalala // ▶ $false // ``` // // @cf external search-external func hasExternal(cmd string) bool { _, err := exec.LookPath(cmd) return err == nil } //elvdoc:fn search-external // // ```elvish // search-external $command // ``` // // Output the full path of the external `$command`. Throws an exception when not // found. Example (your output might vary): // // ```elvish-transcript // ~> search-external cat // ▶ /bin/cat // ``` // // @cf external has-external func searchExternal(cmd string) (string, error) { return exec.LookPath(cmd) } //elvdoc:fn exit // // ```elvish // exit $status? // ``` // // Exit the Elvish process with `$status` (defaulting to 0). func exit(fm *Frame, codes ...int) error { code := 0 switch len(codes) { case 0: case 1: code = codes[0] default: return errs.ArityMismatch{What: "arguments", ValidLow: 0, ValidHigh: 1, Actual: len(codes)} } preExit(fm) os.Exit(code) // Does not return panic("os.Exit returned") } func preExit(fm *Frame) { for _, hook := range fm.Evaler.BeforeExit { hook() } } elvish-0.17.0/pkg/eval/builtin_fn_cmd_test.go000066400000000000000000000002311415471104000211340ustar00rootroot00000000000000package eval_test import ( "testing" . "src.elv.sh/pkg/eval/evaltest" ) func TestBuiltinFnCmd(t *testing.T) { Test(t /* TODO: Add test cases */) } elvish-0.17.0/pkg/eval/builtin_fn_cmd_unix.go000066400000000000000000000050001415471104000211370ustar00rootroot00000000000000//go:build !windows && !plan9 // +build !windows,!plan9 package eval import ( "errors" "os" "os/exec" "strconv" "syscall" "src.elv.sh/pkg/env" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/sys/eunix" ) // ErrNotInSameProcessGroup is thrown when the process IDs passed to fg are not // in the same process group. var ErrNotInSameProcessGroup = errors.New("not in the same process group") //elvdoc:fn exec // // ```elvish // exec $command? $args... // ``` // // Replace the Elvish process with an external `$command`, defaulting to // `elvish`, passing the given arguments. This decrements `$E:SHLVL` before // starting the new process. // // This command always raises an exception on Windows with the message "not // supported on Windows". // Reference to syscall.Exec. Can be overridden in tests. var syscallExec = syscall.Exec func execFn(fm *Frame, args ...interface{}) error { var argstrings []string if len(args) == 0 { argstrings = []string{"elvish"} } else { argstrings = make([]string, len(args)) for i, a := range args { argstrings[i] = vals.ToString(a) } } var err error argstrings[0], err = exec.LookPath(argstrings[0]) if err != nil { return err } preExit(fm) decSHLVL() return syscallExec(argstrings[0], argstrings, os.Environ()) } // Decrements $E:SHLVL. Called from execFn to ensure that $E:SHLVL remains the // same in the new command. func decSHLVL() { i, err := strconv.Atoi(os.Getenv(env.SHLVL)) if err != nil { return } os.Setenv(env.SHLVL, strconv.Itoa(i-1)) } func fg(pids ...int) error { if len(pids) == 0 { return errs.ArityMismatch{What: "arguments", ValidLow: 1, ValidHigh: -1, Actual: len(pids)} } var thepgid int for i, pid := range pids { pgid, err := syscall.Getpgid(pid) if err != nil { return err } if i == 0 { thepgid = pgid } else if pgid != thepgid { return ErrNotInSameProcessGroup } } err := eunix.Tcsetpgrp(0, thepgid) if err != nil { return err } errors := make([]Exception, len(pids)) for i, pid := range pids { err := syscall.Kill(pid, syscall.SIGCONT) if err != nil { errors[i] = &exception{err, nil} } } for i, pid := range pids { if errors[i] != nil { continue } var ws syscall.WaitStatus _, err = syscall.Wait4(pid, &ws, syscall.WUNTRACED, nil) if err != nil { errors[i] = &exception{err, nil} } else { // TODO find command name errors[i] = &exception{NewExternalCmdExit( "[pid "+strconv.Itoa(pid)+"]", ws, pid), nil} } } return MakePipelineError(errors) } elvish-0.17.0/pkg/eval/builtin_fn_cmd_unix_test.go000066400000000000000000000025241415471104000222060ustar00rootroot00000000000000//go:build !windows && !plan9 && !js // +build !windows,!plan9,!js package eval_test import ( "testing" . "src.elv.sh/pkg/eval/evaltest" ) func TestHasExternal(t *testing.T) { Test(t, That("has-external sh").Puts(true), That("has-external random-invalid-command").Puts(false), ) } func TestSearchExternal(t *testing.T) { Test(t, // Even on UNIX systems we can't assume that commands like `sh` or // `test` are in a specific directory. Those commands might be in /bin // or /usr/bin. However, on all systems we currently support it will // be in /bin and, possibly, /usr/bin. So ensure we limit the search // to the one universal UNIX directory for basic commands. That("E:PATH=/bin search-external sh").Puts("/bin/sh"), // We should check for a specific error if the external command cannot // be found. However, the current implementation of `search-external` // returns the raw error returned by a Go runtime function over which // we have no control. // // TODO: Replace the raw Go runtime `exec.LookPath` error with an // Elvish error; possibly wrapping the Go runtime error. Then tighten // this test to that specific error. That("search-external random-invalid-command").Throws(AnyError), ) } func TestExternal(t *testing.T) { Test(t, That(`(external sh) -c 'echo external-sh'`).Prints("external-sh\n"), ) } elvish-0.17.0/pkg/eval/builtin_fn_cmd_windows.go000066400000000000000000000003511415471104000216520ustar00rootroot00000000000000package eval import "errors" var errNotSupportedOnWindows = errors.New("not supported on Windows") func execFn(...interface{}) error { return errNotSupportedOnWindows } func fg(...int) error { return errNotSupportedOnWindows } elvish-0.17.0/pkg/eval/builtin_fn_container.go000066400000000000000000000573311415471104000213310ustar00rootroot00000000000000package eval import ( "errors" "fmt" "math" "math/big" "sort" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" "src.elv.sh/pkg/persistent/hashmap" ) // Sequence, list and maps. func init() { addBuiltinFns(map[string]interface{}{ "ns": nsFn, "make-map": makeMap, "range": rangeFn, "repeat": repeat, "assoc": assoc, "dissoc": dissoc, "all": all, "one": one, "has-key": hasKey, "has-value": hasValue, "take": take, "drop": drop, "count": count, "keys": keys, "compare": compare, "order": order, }) } //elvdoc:fn ns // // ```elvish // ns $map // ``` // // Constructs a namespace from `$map`, using the keys as variable names and the // values as their values. Examples: // // ```elvish-transcript // ~> n = (ns [&name=value]) // ~> put $n[name] // ▶ value // ~> n: = (ns [&name=value]) // ~> put $n:name // ▶ value // ``` func nsFn(m hashmap.Map) (*Ns, error) { nb := BuildNs() for it := m.Iterator(); it.HasElem(); it.Next() { k, v := it.Elem() kstring, ok := k.(string) if !ok { return nil, errs.BadValue{ What: `key of argument of "ns"`, Valid: "string", Actual: vals.Kind(k)} } nb.AddVar(kstring, vars.FromInit(v)) } return nb.Ns(), nil } //elvdoc:fn make-map // // ```elvish // make-map $input? // ``` // // Outputs a map from an input consisting of containers with two elements. The // first element of each container is used as the key, and the second element is // used as the value. // // If the same key appears multiple times, the last value is used. // // Examples: // // ```elvish-transcript // ~> make-map [[k v]] // ▶ [&k=v] // ~> make-map [[k v1] [k v2]] // ▶ [&k=v2] // ~> put [k1 v1] [k2 v2] | make-map // ▶ [&k1=v1 &k2=v2] // ~> put aA bB | make-map // ▶ [&a=A &b=B] // ``` func makeMap(input Inputs) (vals.Map, error) { m := vals.EmptyMap var errMakeMap error input(func(v interface{}) { if errMakeMap != nil { return } if !vals.CanIterate(v) { errMakeMap = errs.BadValue{ What: "input to make-map", Valid: "iterable", Actual: vals.Kind(v)} return } if l := vals.Len(v); l != 2 { errMakeMap = errs.BadValue{ What: "input to make-map", Valid: "iterable with 2 elements", Actual: fmt.Sprintf("%v with %v elements", vals.Kind(v), l)} return } elems, err := vals.Collect(v) if err != nil { errMakeMap = err return } if len(elems) != 2 { errMakeMap = fmt.Errorf("internal bug: collected %v values", len(elems)) return } m = m.Assoc(elems[0], elems[1]) }) return m, errMakeMap } //elvdoc:fn range // // ```elvish // range &step $start=0 $end // ``` // // Outputs numbers, starting from `$start` and ending before `$end`, using // `&step` as the increment. // // - If `$start` <= `$end`, `&step` defaults to 1, and `range` outputs values as // long as they are smaller than `$end`. An exception is thrown if `&step` is // given a negative value. // // - If `$start` > `$end`, `&step` defaults to -1, and `range` outputs values as // long as they are greater than `$end`. An exception is thrown if `&step` is // given a positive value. // // As a special case, if the outputs are floating point numbers, `range` also // terminates if the values stop changing. // // This command is [exactness-preserving](#exactness-preserving). // // Examples: // // ```elvish-transcript // ~> range 4 // ▶ (num 0) // ▶ (num 1) // ▶ (num 2) // ▶ (num 3) // ~> range 4 0 // ▶ (num 4) // ▶ (num 3) // ▶ (num 2) // ▶ (num 1) // ~> range -3 3 &step=2 // ▶ (num -3) // ▶ (num -1) // ▶ (num 1) // ~> range 3 -3 &step=-2 // ▶ (num 3) // ▶ (num 1) // ▶ (num -1) // ~> range (- (math:pow 2 53) 1) +inf // ▶ (num 9007199254740991.0) // ▶ (num 9007199254740992.0) // ``` // // When using floating-point numbers, beware that numerical errors can result in // an incorrect number of outputs: // // ```elvish-transcript // ~> range 0.9 &step=0.3 // ▶ (num 0.0) // ▶ (num 0.3) // ▶ (num 0.6) // ▶ (num 0.8999999999999999) // ``` // // Avoid this problem by using exact rationals: // // ```elvish-transcript // ~> range 9/10 &step=3/10 // ▶ (num 0) // ▶ (num 3/10) // ▶ (num 3/5) // ``` // // Etymology: // [Python](https://docs.python.org/3/library/functions.html#func-range). type rangeOpts struct{ Step vals.Num } // TODO: The default value can only be used implicitly; passing "range // &step=nil" results in an error. func (o *rangeOpts) SetDefaultOptions() { o.Step = nil } func rangeFn(fm *Frame, opts rangeOpts, args ...vals.Num) error { var rawNums []vals.Num switch len(args) { case 1: rawNums = []vals.Num{0, args[0]} case 2: rawNums = []vals.Num{args[0], args[1]} default: return errs.ArityMismatch{What: "arguments", ValidLow: 1, ValidHigh: 2, Actual: len(args)} } if opts.Step != nil { rawNums = append(rawNums, opts.Step) } nums := vals.UnifyNums(rawNums, vals.Int) out := fm.ValueOutput() switch nums := nums.(type) { case []int: return rangeInt(nums, out) case []*big.Int: return rangeBigInt(nums, out) case []*big.Rat: return rangeBitRat(nums, out) case []float64: return rangeFloat64(nums, out) default: panic("unreachable") } } func rangeInt(nums []int, out ValueOutput) error { start, end := nums[0], nums[1] var step int if start <= end { if len(nums) == 3 { step = nums[2] if step <= 0 { return errs.BadValue{ What: "step", Valid: "positive", Actual: vals.ToString(step)} } } else { step = 1 } for cur := start; cur < end; cur += step { err := out.Put(vals.FromGo(cur)) if err != nil { return err } if cur+step <= cur { break } } } else { if len(nums) == 3 { step = nums[2] if step >= 0 { return errs.BadValue{ What: "step", Valid: "negative", Actual: vals.ToString(step)} } } else { step = -1 } for cur := start; cur > end; cur += step { err := out.Put(vals.FromGo(cur)) if err != nil { return err } if cur+step >= cur { break } } } return nil } // TODO: Use type parameters to deduplicate this with rangeInt when Elvish // requires Go 1.18. func rangeFloat64(nums []float64, out ValueOutput) error { start, end := nums[0], nums[1] var step float64 if start <= end { if len(nums) == 3 { step = nums[2] if step <= 0 { return errs.BadValue{ What: "step", Valid: "positive", Actual: vals.ToString(step)} } } else { step = 1 } for cur := start; cur < end; cur += step { err := out.Put(vals.FromGo(cur)) if err != nil { return err } if cur+step <= cur { break } } } else { if len(nums) == 3 { step = nums[2] if step >= 0 { return errs.BadValue{ What: "step", Valid: "negative", Actual: vals.ToString(step)} } } else { step = -1 } for cur := start; cur > end; cur += step { err := out.Put(vals.FromGo(cur)) if err != nil { return err } if cur+step >= cur { break } } } return nil } var ( bigInt1 = big.NewInt(1) bigIntNeg1 = big.NewInt(-1) ) func rangeBigInt(nums []*big.Int, out ValueOutput) error { start, end := nums[0], nums[1] var step *big.Int if start.Cmp(end) <= 0 { if len(nums) == 3 { step = nums[2] if step.Sign() <= 0 { return errs.BadValue{ What: "step", Valid: "positive", Actual: vals.ToString(step)} } } else { step = bigInt1 } var cur, next *big.Int for cur = start; cur.Cmp(end) < 0; cur = next { err := out.Put(vals.FromGo(cur)) if err != nil { return err } next = &big.Int{} next.Add(cur, step) cur = next } } else { if len(nums) == 3 { step = nums[2] if step.Sign() >= 0 { return errs.BadValue{ What: "step", Valid: "negative", Actual: vals.ToString(step)} } } else { step = bigIntNeg1 } var cur, next *big.Int for cur = start; cur.Cmp(end) > 0; cur = next { err := out.Put(vals.FromGo(cur)) if err != nil { return err } next = &big.Int{} next.Add(cur, step) cur = next } } return nil } var ( bigRat1 = big.NewRat(1, 1) bigRatNeg1 = big.NewRat(-1, 1) ) // TODO: Use type parameters to deduplicate this with rangeBitInt when Elvish // requires Go 1.18. func rangeBitRat(nums []*big.Rat, out ValueOutput) error { start, end := nums[0], nums[1] var step *big.Rat if start.Cmp(end) <= 0 { if len(nums) == 3 { step = nums[2] if step.Sign() <= 0 { return errs.BadValue{ What: "step", Valid: "positive", Actual: vals.ToString(step)} } } else { step = bigRat1 } var cur, next *big.Rat for cur = start; cur.Cmp(end) < 0; cur = next { err := out.Put(vals.FromGo(cur)) if err != nil { return err } next = &big.Rat{} next.Add(cur, step) cur = next } } else { if len(nums) == 3 { step = nums[2] if step.Sign() >= 0 { return errs.BadValue{ What: "step", Valid: "negative", Actual: vals.ToString(step)} } } else { step = bigRatNeg1 } var cur, next *big.Rat for cur = start; cur.Cmp(end) > 0; cur = next { err := out.Put(vals.FromGo(cur)) if err != nil { return err } next = &big.Rat{} next.Add(cur, step) cur = next } } return nil } //elvdoc:fn repeat // // ```elvish // repeat $n $value // ``` // // Output `$value` for `$n` times. Example: // // ```elvish-transcript // ~> repeat 0 lorem // ~> repeat 4 NAN // ▶ NAN // ▶ NAN // ▶ NAN // ▶ NAN // ``` // // Etymology: [Clojure](https://clojuredocs.org/clojure.core/repeat). func repeat(fm *Frame, n int, v interface{}) error { out := fm.ValueOutput() for i := 0; i < n; i++ { err := out.Put(v) if err != nil { return err } } return nil } //elvdoc:fn assoc // // ```elvish // assoc $container $k $v // ``` // // Output a slightly modified version of `$container`, such that its value at `$k` // is `$v`. Applies to both lists and to maps. // // When `$container` is a list, `$k` may be a negative index. However, slice is not // yet supported. // // ```elvish-transcript // ~> assoc [foo bar quux] 0 lorem // ▶ [lorem bar quux] // ~> assoc [foo bar quux] -1 ipsum // ▶ [foo bar ipsum] // ~> assoc [&k=v] k v2 // ▶ [&k=v2] // ~> assoc [&k=v] k2 v2 // ▶ [&k2=v2 &k=v] // ``` // // Etymology: [Clojure](https://clojuredocs.org/clojure.core/assoc). // // @cf dissoc func assoc(a, k, v interface{}) (interface{}, error) { return vals.Assoc(a, k, v) } var errCannotDissoc = errors.New("cannot dissoc") //elvdoc:fn dissoc // // ```elvish // dissoc $map $k // ``` // // Output a slightly modified version of `$map`, with the key `$k` removed. If // `$map` does not contain `$k` as a key, the same map is returned. // // ```elvish-transcript // ~> dissoc [&foo=bar &lorem=ipsum] foo // ▶ [&lorem=ipsum] // ~> dissoc [&foo=bar &lorem=ipsum] k // ▶ [&lorem=ipsum &foo=bar] // ``` // // @cf assoc func dissoc(a, k interface{}) (interface{}, error) { a2 := vals.Dissoc(a, k) if a2 == nil { return nil, errCannotDissoc } return a2, nil } //elvdoc:fn all // // ```elvish // all $input-list? // ``` // // Passes inputs to the output as is. Byte inputs into values, one per line. // // This is an identity function for commands with value outputs: `a | all` is // equivalent to `a` if it only outputs values. // // This function is useful for turning inputs into arguments, like: // // ```elvish-transcript // ~> use str // ~> put 'lorem,ipsum' | str:split , (all) // ▶ lorem // ▶ ipsum // ``` // // Or capturing all inputs in a variable: // // ```elvish-transcript // ~> x = [(all)] // foo // bar // (Press ^D) // ~> put $x // ▶ [foo bar] // ``` // // When given a list, it outputs all elements of the list: // // ```elvish-transcript // ~> all [foo bar] // ▶ foo // ▶ bar // ``` // // @cf one func all(fm *Frame, inputs Inputs) error { out := fm.ValueOutput() var errOut error inputs(func(v interface{}) { if errOut != nil { return } errOut = out.Put(v) }) return errOut } //elvdoc:fn one // // ```elvish // one $input-list? // ``` // // Passes inputs to outputs, if there is only a single one. Otherwise raises an // exception. // // This function can be used in a similar way to [`all`](#all), but is a better // choice when you expect that there is exactly one output: // // @cf all func one(fm *Frame, inputs Inputs) error { var val interface{} n := 0 inputs(func(v interface{}) { if n == 0 { val = v } n++ }) if n == 1 { return fm.ValueOutput().Put(val) } return fmt.Errorf("expect a single value, got %d", n) } //elvdoc:fn take // // ```elvish // take $n $input-list? // ``` // // Retain the first `$n` input elements. If `$n` is larger than the number of input // elements, the entire input is retained. Examples: // // ```elvish-transcript // ~> take 3 [a b c d e] // ▶ a // ▶ b // ▶ c // ~> use str // ~> str:split ' ' 'how are you?' | take 1 // ▶ how // ~> range 2 | take 10 // ▶ 0 // ▶ 1 // ``` // // Etymology: Haskell. func take(fm *Frame, n int, inputs Inputs) error { out := fm.ValueOutput() var errOut error i := 0 inputs(func(v interface{}) { if errOut != nil { return } if i < n { errOut = out.Put(v) } i++ }) return errOut } //elvdoc:fn drop // // ```elvish // drop $n $input-list? // ``` // // Drop the first `$n` elements of the input. If `$n` is larger than the number of // input elements, the entire input is dropped. // // Example: // // ```elvish-transcript // ~> drop 2 [a b c d e] // ▶ c // ▶ d // ▶ e // ~> use str // ~> str:split ' ' 'how are you?' | drop 1 // ▶ are // ▶ 'you?' // ~> range 2 | drop 10 // ``` // // Etymology: Haskell. // // @cf take func drop(fm *Frame, n int, inputs Inputs) error { out := fm.ValueOutput() var errOut error i := 0 inputs(func(v interface{}) { if errOut != nil { return } if i >= n { errOut = out.Put(v) } i++ }) return errOut } //elvdoc:fn has-value // // ```elvish // has-value $container $value // ``` // // Determine whether `$value` is a value in `$container`. // // Examples, maps: // // ```elvish-transcript // ~> has-value [&k1=v1 &k2=v2] v1 // ▶ $true // ~> has-value [&k1=v1 &k2=v2] k1 // ▶ $false // ``` // // Examples, lists: // // ```elvish-transcript // ~> has-value [v1 v2] v1 // ▶ $true // ~> has-value [v1 v2] k1 // ▶ $false // ``` // // Examples, strings: // // ```elvish-transcript // ~> has-value ab b // ▶ $true // ~> has-value ab c // ▶ $false // ``` func hasValue(container, value interface{}) (bool, error) { switch container := container.(type) { case hashmap.Map: for it := container.Iterator(); it.HasElem(); it.Next() { _, v := it.Elem() if vals.Equal(v, value) { return true, nil } } return false, nil default: var found bool err := vals.Iterate(container, func(v interface{}) bool { found = (v == value) return !found }) return found, err } } //elvdoc:fn has-key // // ```elvish // has-key $container $key // ``` // // Determine whether `$key` is a key in `$container`. A key could be a map key or // an index on a list or string. This includes a range of indexes. // // Examples, maps: // // ```elvish-transcript // ~> has-key [&k1=v1 &k2=v2] k1 // ▶ $true // ~> has-key [&k1=v1 &k2=v2] v1 // ▶ $false // ``` // // Examples, lists: // // ```elvish-transcript // ~> has-key [v1 v2] 0 // ▶ $true // ~> has-key [v1 v2] 1 // ▶ $true // ~> has-key [v1 v2] 2 // ▶ $false // ~> has-key [v1 v2] 0:2 // ▶ $true // ~> has-key [v1 v2] 0:3 // ▶ $false // ``` // // Examples, strings: // // ```elvish-transcript // ~> has-key ab 0 // ▶ $true // ~> has-key ab 1 // ▶ $true // ~> has-key ab 2 // ▶ $false // ~> has-key ab 0:2 // ▶ $true // ~> has-key ab 0:3 // ▶ $false // ``` func hasKey(container, key interface{}) bool { return vals.HasKey(container, key) } //elvdoc:fn count // // ```elvish // count $input-list? // ``` // // Count the number of inputs. // // Examples: // // ```elvish-transcript // ~> count lorem # count bytes in a string // ▶ 5 // ~> count [lorem ipsum] // ▶ 2 // ~> range 100 | count // ▶ 100 // ~> seq 100 | count // ▶ 100 // ``` // The count implementation uses a custom varargs based implementation rather // than the more common `Inputs` API (see pkg/eval/go_fn.go) because this // allows the implementation to be O(1) for the common cases rather than O(n). func count(fm *Frame, args ...interface{}) (int, error) { var n int switch nargs := len(args); nargs { case 0: // Count inputs. fm.IterateInputs(func(interface{}) { n++ }) case 1: // Get length of argument. v := args[0] if len := vals.Len(v); len >= 0 { n = len } else { err := vals.Iterate(v, func(interface{}) bool { n++ return true }) if err != nil { return 0, fmt.Errorf("cannot get length of a %s", vals.Kind(v)) } } default: // The error matches what would be returned if the `Inputs` API was // used. See GoFn.Call(). return 0, errs.ArityMismatch{What: "arguments", ValidLow: 0, ValidHigh: 1, Actual: nargs} } return n, nil } //elvdoc:fn keys // // ```elvish // keys $map // ``` // // Put all keys of `$map` on the structured stdout. // // Example: // // ```elvish-transcript // ~> keys [&a=foo &b=bar &c=baz] // ▶ a // ▶ c // ▶ b // ``` // // Note that there is no guaranteed order for the keys of a map. func keys(fm *Frame, v interface{}) error { out := fm.ValueOutput() var errPut error errIterate := vals.IterateKeys(v, func(k interface{}) bool { errPut = out.Put(k) return errPut == nil }) if errIterate != nil { return errIterate } return errPut } //elvdoc:fn order // // ```elvish // order &reverse=$false $less-than=$nil $inputs? // ``` // // Outputs the input values sorted in ascending order. The sort is guaranteed to // be [stable](https://en.wikipedia.org/wiki/Sorting_algorithm#Stability). // // The `&reverse` option, if true, reverses the order of output. // // The `&less-than` option, if given, establishes the ordering of the elements. // Its value should be a function that takes two arguments and outputs a single // boolean indicating whether the first argument is less than the second // argument. If the function throws an exception, `order` rethrows the exception // without outputting any value. // // If `&less-than` has value `$nil` (the default if not set), it is equivalent // to `{|a b| eq -1 (compare $a $b) }`. // // Examples: // // ```elvish-transcript // ~> put foo bar ipsum | order // ▶ bar // ▶ foo // ▶ ipsum // ~> order [(float64 10) (float64 1) (float64 5)] // ▶ (float64 1) // ▶ (float64 5) // ▶ (float64 10) // ~> order [[a b] [a] [b b] [a c]] // ▶ [a] // ▶ [a b] // ▶ [a c] // ▶ [b b] // ~> order &reverse [a c b] // ▶ c // ▶ b // ▶ a // ~> order &less-than={|a b| eq $a x } [l x o r x e x m] // ▶ x // ▶ x // ▶ x // ▶ l // ▶ o // ▶ r // ▶ e // ▶ m // ``` // // Beware that strings that look like numbers are treated as strings, not // numbers. To sort strings as numbers, use an explicit `&less-than` option: // // ```elvish-transcript // ~> order [5 1 10] // ▶ 1 // ▶ 10 // ▶ 5 // ~> order &less-than={|a b| < $a $b } [5 1 10] // ▶ 1 // ▶ 5 // ▶ 10 // ``` // // @cf compare type orderOptions struct { Reverse bool LessThan Callable } func (opt *orderOptions) SetDefaultOptions() {} // ErrUncomparable is raised by the compare and order commands when inputs contain // uncomparable values. var ErrUncomparable = errs.BadValue{ What: `inputs to "compare" or "order"`, Valid: "comparable values", Actual: "uncomparable values"} func order(fm *Frame, opts orderOptions, inputs Inputs) error { var values []interface{} inputs(func(v interface{}) { values = append(values, v) }) var errSort error var lessFn func(i, j int) bool if opts.LessThan != nil { lessFn = func(i, j int) bool { if errSort != nil { return true } var args []interface{} if opts.Reverse { args = []interface{}{values[j], values[i]} } else { args = []interface{}{values[i], values[j]} } outputs, err := fm.CaptureOutput(func(fm *Frame) error { return opts.LessThan.Call(fm, args, NoOpts) }) if err != nil { errSort = err return true } if len(outputs) != 1 { errSort = errs.BadValue{ What: "output of the &less-than callback", Valid: "a single boolean", Actual: fmt.Sprintf("%d values", len(outputs))} return true } if b, ok := outputs[0].(bool); ok { return b } errSort = errs.BadValue{ What: "output of the &less-than callback", Valid: "boolean", Actual: vals.Kind(outputs[0])} return true } } else { // Use default comparison implemented by cmp. lessFn = func(i, j int) bool { if errSort != nil { return true } o := cmp(values[i], values[j]) if o == uncomparable { errSort = ErrUncomparable return true } if opts.Reverse { return o == more } return o == less } } sort.SliceStable(values, lessFn) if errSort != nil { return errSort } out := fm.ValueOutput() for _, v := range values { err := out.Put(v) if err != nil { return err } } return nil } type ordering uint8 const ( less ordering = iota equal more uncomparable ) //elvdoc:fn compare // // ```elvish // compare $a $b // ``` // // Outputs -1 if `$a` < `$b`, 0 if `$a` = `$b`, and 1 if `$a` > `$b`. // // The following comparison algorithm is used: // // - Typed numbers are compared numerically. The comparison is consistent with // the [number comparison commands](#num-cmp), except that `NaN` values are // considered equal to each other and smaller than all other numbers. // // - Strings are compared lexicographically by bytes, consistent with the // [string comparison commands](#str-cmp). For UTF-8 encoded strings, this is // equivalent to comparing by codepoints. // // - Lists are compared lexicographically by elements, if the elements at the // same positions are comparable. // // If the ordering between two elements is not defined by the conditions above, // i.e. if the value of `$a` or `$b` is not covered by any of the cases above or // if they belong to different cases, a "bad value" exception is thrown. // // Examples: // // ```elvish-transcript // ~> compare a b // ▶ (num 1) // ~> compare b a // ▶ (num -1) // ~> compare x x // ▶ (num 0) // ~> compare (float64 10) (float64 1) // ▶ (num 1) // ``` // // Beware that strings that look like numbers are treated as strings, not // numbers. // // @cf order func compare(fm *Frame, a, b interface{}) (int, error) { switch cmp(a, b) { case less: return -1, nil case equal: return 0, nil case more: return 1, nil default: return 0, ErrUncomparable } } func cmp(a, b interface{}) ordering { switch a := a.(type) { case int, *big.Int, *big.Rat, float64: switch b.(type) { case int, *big.Int, *big.Rat, float64: a, b := vals.UnifyNums2(a, b, 0) switch a := a.(type) { case int: return compareInt(a, b.(int)) case *big.Int: return compareInt(a.Cmp(b.(*big.Int)), 0) case *big.Rat: return compareInt(a.Cmp(b.(*big.Rat)), 0) case float64: return compareFloat(a, b.(float64)) default: panic("unreachable") } } case string: if b, ok := b.(string); ok { switch { case a == b: return equal case a < b: return less default: // a > b return more } } case vals.List: if b, ok := b.(vals.List); ok { aIt := a.Iterator() bIt := b.Iterator() for aIt.HasElem() && bIt.HasElem() { o := cmp(aIt.Elem(), bIt.Elem()) if o != equal { return o } aIt.Next() bIt.Next() } switch { case a.Len() == b.Len(): return equal case a.Len() < b.Len(): return less default: // a.Len() > b.Len() return more } } } return uncomparable } func compareInt(a, b int) ordering { if a < b { return less } else if a > b { return more } return equal } func compareFloat(a, b float64) ordering { // For the sake of ordering, NaN's are considered equal to each // other and smaller than all numbers switch { case math.IsNaN(a): if math.IsNaN(b) { return equal } return less case math.IsNaN(b): return more case a < b: return less case a > b: return more default: // a == b return equal } } elvish-0.17.0/pkg/eval/builtin_fn_container_test.go000066400000000000000000000265461415471104000223740ustar00rootroot00000000000000package eval_test import ( "math" "math/big" "testing" "unsafe" . "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/errs" . "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/eval/vals" ) func TestNsCmd(t *testing.T) { Test(t, That("put (ns [&name=value])[name]").Puts("value"), That("n: = (ns [&name=value]); put $n:name").Puts("value"), That("ns [&[]=[]]").Throws(errs.BadValue{ What: `key of argument of "ns"`, Valid: "string", Actual: "list"}), ) } func TestMakeMap(t *testing.T) { Test(t, That("make-map []").Puts(vals.EmptyMap), That("make-map [[k v]]").Puts(vals.MakeMap("k", "v")), That("make-map [[k v] [k v2]]").Puts(vals.MakeMap("k", "v2")), That("make-map [[k1 v1] [k2 v2]]"). Puts(vals.MakeMap("k1", "v1", "k2", "v2")), That("make-map [kv]").Puts(vals.MakeMap("k", "v")), That("make-map [{ } [k v]]"). Throws( errs.BadValue{ What: "input to make-map", Valid: "iterable", Actual: "fn"}, "make-map [{ } [k v]]"), That("make-map [[k v] [k]]"). Throws( errs.BadValue{ What: "input to make-map", Valid: "iterable with 2 elements", Actual: "list with 1 elements"}, "make-map [[k v] [k]]"), ) } var ( maxInt = 1<<((unsafe.Sizeof(0)*8)-1) - 1 minInt = -maxInt - 1 maxDenseIntInFloat = float64(1 << 53) ) func TestRange(t *testing.T) { Test(t, // Basic argument sanity checks. That("range").Throws(ErrorWithType(errs.ArityMismatch{})), That("range 0 1 2").Throws(ErrorWithType(errs.ArityMismatch{})), // Int count up. That("range 3").Puts(0, 1, 2), That("range 1 3").Puts(1, 2), // Int count down. That("range -1 10 &step=3").Puts(-1, 2, 5, 8), That("range 3 -3").Puts(3, 2, 1, 0, -1, -2), // Near maxInt or minInt. That("range "+args(maxInt-2, maxInt)).Puts(maxInt-2, maxInt-1), That("range "+args(maxInt, maxInt-2)).Puts(maxInt, maxInt-1), That("range "+args(minInt, minInt+2)).Puts(minInt, minInt+1), That("range "+args(minInt+2, minInt)).Puts(minInt+2, minInt+1), // Invalid step given the "start" and "end" values of the range. That("range &step=-1 1"). Throws(errs.BadValue{What: "step", Valid: "positive", Actual: "-1"}), That("range &step=1 1 0"). Throws(errs.BadValue{What: "step", Valid: "negative", Actual: "1"}), thatOutputErrorIsBubbled("range 2"), // Big int count up. That("range "+z+" "+z3).Puts(bigInt(z), bigInt(z1), bigInt(z2)), That("range "+z+" "+z3+" &step=2").Puts(bigInt(z), bigInt(z2)), // Big int count down. That("range "+z3+" "+z).Puts(bigInt(z3), bigInt(z2), bigInt(z1)), That("range "+z3+" "+z+" &step=-2").Puts(bigInt(z3), bigInt(z1)), // Invalid big int step. That("range &step=-"+z+" 10"). Throws(errs.BadValue{What: "step", Valid: "positive", Actual: "-" + z}), That("range &step="+z+" 10 0"). Throws(errs.BadValue{What: "step", Valid: "negative", Actual: z}), thatOutputErrorIsBubbled("range "+z+" "+z1), // Rational count up. That("range 23/10").Puts(0, 1, 2), That("range 1/10 23/10").Puts( big.NewRat(1, 10), big.NewRat(11, 10), big.NewRat(21, 10)), That("range 23/10 1/10").Puts( big.NewRat(23, 10), big.NewRat(13, 10), big.NewRat(3, 10)), That("range 1/10 9/10 &step=3/10").Puts( big.NewRat(1, 10), big.NewRat(4, 10), big.NewRat(7, 10)), // Rational count down. That("range 9/10 0/10 &step=-3/10").Puts( big.NewRat(9, 10), big.NewRat(6, 10), big.NewRat(3, 10)), // Invalid rational step. That("range &step=-1/2 10"). Throws(errs.BadValue{What: "step", Valid: "positive", Actual: "-1/2"}), That("range &step=1/2 10 0"). Throws(errs.BadValue{What: "step", Valid: "negative", Actual: "1/2"}), thatOutputErrorIsBubbled("range 1/2 3/2"), // Float64 count up. That("range 1.2").Puts(0.0, 1.0), That("range &step=0.5 1 3").Puts(1.0, 1.5, 2.0, 2.5), // Float64 count down. That("range 1.2 -1.2").Puts(1.2, Approximately{F: 0.2}, Approximately{F: -0.8}), That("range &step=-0.5 3 1").Puts(3.0, 2.5, 2.0, 1.5), // Near maxDenseIntInFloat. That("range "+args(maxDenseIntInFloat-2, "+inf")). Puts(maxDenseIntInFloat-2, maxDenseIntInFloat-1, maxDenseIntInFloat), That("range "+args(maxDenseIntInFloat, maxDenseIntInFloat-2)). Puts(maxDenseIntInFloat, maxDenseIntInFloat-1), // Invalid float64 step. That("range &step=-0.5 10"). Throws(errs.BadValue{What: "step", Valid: "positive", Actual: "-0.5"}), That("range &step=0.5 10 0"). Throws(errs.BadValue{What: "step", Valid: "negative", Actual: "0.5"}), thatOutputErrorIsBubbled("range 1.2"), ) } func TestRepeat(t *testing.T) { Test(t, That(`repeat 4 foo`).Puts("foo", "foo", "foo", "foo"), thatOutputErrorIsBubbled("repeat 1 foo"), ) } func TestAssoc(t *testing.T) { Test(t, That(`put (assoc [0] 0 zero)[0]`).Puts("zero"), That(`put (assoc [&] k v)[k]`).Puts("v"), That(`put (assoc [&k=v] k v2)[k]`).Puts("v2"), ) } func TestDissoc(t *testing.T) { Test(t, That(`has-key (dissoc [&k=v] k) k`).Puts(false), That("dissoc foo 0").Throws(ErrorWithMessage("cannot dissoc")), ) } func TestAll(t *testing.T) { Test(t, That(`put foo bar | all`).Puts("foo", "bar"), That(`echo foobar | all`).Puts("foobar"), That(`all [foo bar]`).Puts("foo", "bar"), thatOutputErrorIsBubbled("all [foo bar]"), ) } func TestOne(t *testing.T) { Test(t, That(`put foo | one`).Puts("foo"), That(`put | one`).Throws(AnyError), That(`put foo bar | one`).Throws(AnyError), That(`one [foo]`).Puts("foo"), That(`one []`).Throws(AnyError), That(`one [foo bar]`).Throws(AnyError), thatOutputErrorIsBubbled("one [foo]"), ) } func TestTake(t *testing.T) { Test(t, That(`range 100 | take 2`).Puts(0, 1), thatOutputErrorIsBubbled("take 1 [foo bar]"), ) } func TestDrop(t *testing.T) { Test(t, That(`range 100 | drop 98`).Puts(98, 99), thatOutputErrorIsBubbled("drop 1 [foo bar lorem]"), ) } func TestHasKey(t *testing.T) { Test(t, That(`has-key [foo bar] 0`).Puts(true), That(`has-key [foo bar] 0..1`).Puts(true), That(`has-key [foo bar] 0..20`).Puts(false), That(`has-key [&lorem=ipsum &foo=bar] lorem`).Puts(true), That(`has-key [&lorem=ipsum &foo=bar] loremwsq`).Puts(false), ) } func TestHasValue(t *testing.T) { Test(t, That(`has-value [&lorem=ipsum &foo=bar] lorem`).Puts(false), That(`has-value [&lorem=ipsum &foo=bar] bar`).Puts(true), That(`has-value [foo bar] bar`).Puts(true), That(`has-value [foo bar] badehose`).Puts(false), That(`has-value "foo" o`).Puts(true), That(`has-value "foo" d`).Puts(false), ) } func TestCount(t *testing.T) { Test(t, That(`range 100 | count`).Puts(100), That(`count [(range 100)]`).Puts(100), That(`count 123`).Puts(3), That(`count 1 2 3`).Throws( errs.ArityMismatch{What: "arguments", ValidLow: 0, ValidHigh: 1, Actual: 3}, "count 1 2 3"), That(`count $true`).Throws(ErrorWithMessage("cannot get length of a bool")), ) } func TestKeys(t *testing.T) { Test(t, That(`keys [&]`).DoesNothing(), That(`keys [&a=foo]`).Puts("a"), // Windows does not have an external sort command. Disabled until we have a // builtin sort command. That(`keys [&a=foo &b=bar] | order`).Puts("a", "b"), That("keys (num 1)").Throws(ErrorWithMessage("cannot iterate keys of number")), thatOutputErrorIsBubbled("keys [&a=foo]"), ) } func TestCompare(t *testing.T) { Test(t, // Comparing strings. That("compare a b").Puts(-1), That("compare b a").Puts(1), That("compare x x").Puts(0), // Comparing numbers. That("compare (num 1) (num 2)").Puts(-1), That("compare (num 2) (num 1)").Puts(1), That("compare (num 3) (num 3)").Puts(0), That("compare (num 1/4) (num 1/2)").Puts(-1), That("compare (num 1/3) (num 0.2)").Puts(1), That("compare (num 3.0) (num 3)").Puts(0), That("compare (num nan) (num 3)").Puts(-1), That("compare (num 3) (num nan)").Puts(1), That("compare (num nan) (num nan)").Puts(0), // Comparing lists. That("compare [a, b] [a, a]").Puts(1), That("compare [a, a] [a, b]").Puts(-1), That("compare [x, y] [x, y]").Puts(0), // Uncomparable values. That("compare 1 (num 1)").Throws(ErrUncomparable), That("compare x [x]").Throws(ErrUncomparable), That("compare a [&a=x]").Throws(ErrUncomparable), ) } func TestOrder(t *testing.T) { Test(t, // Ordering strings That("put foo bar ipsum | order").Puts("bar", "foo", "ipsum"), That("put foo bar bar | order").Puts("bar", "bar", "foo"), That("put 10 1 5 2 | order").Puts("1", "10", "2", "5"), // Ordering typed numbers // Only small integers That("put 10 1 1 | each $num~ | order").Puts(1, 1, 10), That("put 10 1 5 2 -1 | each $num~ | order").Puts(-1, 1, 2, 5, 10), // Small and large integers That("put 1 "+z+" 2 "+z+" | each $num~ | order").Puts(1, 2, bigInt(z), bigInt(z)), // Integers and rationals That("put 1 2 3/2 3/2 | each $num~ | order"). Puts(1, big.NewRat(3, 2), big.NewRat(3, 2), 2), // Integers and floats That("put 1 1.5 2 1.5 | each $num~ | order"). Puts(1, 1.5, 1.5, 2), // Mixed integers and floats. That("put (num 1) (float64 1.5) (float64 2) (num 1.5) | order"). Puts(1, 1.5, 1.5, 2.0), // For the sake of ordering, NaN's are considered smaller than other numbers That("put NaN -1 NaN | each $num~ | order").Puts(math.NaN(), math.NaN(), -1), // Ordering lists That("put [b] [a] | order").Puts(vals.MakeList("a"), vals.MakeList("b")), That("put [a] [b] [a] | order"). Puts(vals.MakeList("a"), vals.MakeList("a"), vals.MakeList("b")), That("put [(float64 10)] [(float64 2)] | order"). Puts(vals.MakeList(2.0), vals.MakeList(10.0)), That("put [a b] [b b] [a c] | order"). Puts( vals.MakeList("a", "b"), vals.MakeList("a", "c"), vals.MakeList("b", "b")), That("put [a] [] [a (float64 2)] [a (float64 1)] | order"). Puts(vals.EmptyList, vals.MakeList("a"), vals.MakeList("a", 1.0), vals.MakeList("a", 2.0)), // Attempting to order uncomparable values That("put (num 1) 1 | order"). Throws(ErrUncomparable, "order"), That("put 1 (float64 1) | order"). Throws(ErrUncomparable, "order"), That("put 1 (float64 1) b | order"). Throws(ErrUncomparable, "order"), That("put [a] a | order"). Throws(ErrUncomparable, "order"), That("put [a] [(float64 1)] | order"). Throws(ErrUncomparable, "order"), // &reverse That("put foo bar ipsum | order &reverse").Puts("ipsum", "foo", "bar"), // &less-than That("put 1 10 2 5 | order &less-than={|a b| < $a $b }"). Puts("1", "2", "5", "10"), // &less-than writing more than one value That("put 1 10 2 5 | order &less-than={|a b| put $true $false }"). Throws( errs.BadValue{ What: "output of the &less-than callback", Valid: "a single boolean", Actual: "2 values"}, "order &less-than={|a b| put $true $false }"), // &less-than writing non-boolean value That("put 1 10 2 5 | order &less-than={|a b| put x }"). Throws( errs.BadValue{ What: "output of the &less-than callback", Valid: "boolean", Actual: "string"}, "order &less-than={|a b| put x }"), // &less-than throwing an exception That("put 1 10 2 5 | order &less-than={|a b| fail bad }"). Throws( FailError{"bad"}, "fail bad ", "order &less-than={|a b| fail bad }"), // &less-than and &reverse That("put 1 10 2 5 | order &reverse &less-than={|a b| < $a $b }"). Puts("10", "5", "2", "1"), // Sort should be stable - test by pretending that all values but one // are equal, an check that the order among them has not changed. That("put l x o x r x e x m | order &less-than={|a b| eq $a x }"). Puts("x", "x", "x", "x", "l", "o", "r", "e", "m"), thatOutputErrorIsBubbled("order [foo]"), ) } elvish-0.17.0/pkg/eval/builtin_fn_debug.go000066400000000000000000000040431415471104000204250ustar00rootroot00000000000000package eval import ( "runtime" "src.elv.sh/pkg/logutil" "src.elv.sh/pkg/parse" ) func init() { addBuiltinFns(map[string]interface{}{ "src": src, "-gc": _gc, "-stack": _stack, "-log": _log, }) } //elvdoc:fn src // // ```elvish // src // ``` // // Output a map-like value describing the current source being evaluated. The value // contains the following fields: // // - `name`, a unique name of the current source. If the source originates from a // file, it is the full path of the file. // // - `code`, the full body of the current source. // // - `is-file`, whether the source originates from a file. // // Examples: // // ```elvish-transcript // ~> put (src)[name code is-file] // ▶ '[tty]' // ▶ 'put (src)[name code is-file]' // ▶ $false // ~> echo 'put (src)[name code is-file]' > show-src.elv // ~> elvish show-src.elv // ▶ /home/elf/show-src.elv // ▶ "put (src)[name code is-file]\n" // ▶ $true // ``` // // Note: this builtin always returns information of the source of the function // calling `src`. Consider the following example: // // ```elvish-transcript // ~> echo 'fn show { put (src)[name] }' > ~/.elvish/lib/src-fsutil.elv // ~> use src-util // ~> src-util:show // ▶ /home/elf/.elvish/lib/src-fsutil.elv // ``` func src(fm *Frame) parse.Source { return fm.srcMeta } //elvdoc:fn -gc // // ```elvish // -gc // ``` // // Force the Go garbage collector to run. // // This is only useful for debug purposes. func _gc() { runtime.GC() } //elvdoc:fn -stack // // ```elvish // -stack // ``` // // Print a stack trace. // // This is only useful for debug purposes. func _stack(fm *Frame) error { // TODO(xiaq): Dup with main.go. buf := make([]byte, 1024) for runtime.Stack(buf, true) == cap(buf) { buf = make([]byte, cap(buf)*2) } _, err := fm.ByteOutput().Write(buf) return err } //elvdoc:fn -log // // ```elvish // -log $filename // ``` // // Direct internal debug logs to the named file. // // This is only useful for debug purposes. func _log(fname string) error { return logutil.SetOutputFile(fname) } elvish-0.17.0/pkg/eval/builtin_fn_env.go000066400000000000000000000034001415471104000201230ustar00rootroot00000000000000package eval import ( "errors" "os" ) // ErrNonExistentEnvVar is raised by the get-env command when the environment // variable does not exist. var ErrNonExistentEnvVar = errors.New("non-existent environment variable") //elvdoc:fn set-env // // ```elvish // set-env $name $value // ``` // // Sets an environment variable to the given value. Example: // // ```elvish-transcript // ~> set-env X foobar // ~> put $E:X // ▶ foobar // ``` // // @cf get-env has-env unset-env //elvdoc:fn unset-env // // ```elvish // unset-env $name // ``` // // Unset an environment variable. Example: // // ```elvish-transcript // ~> E:X = foo // ~> unset-env X // ~> has-env X // ▶ $false // ~> put $E:X // ▶ '' // ``` // // @cf has-env get-env set-env func init() { addBuiltinFns(map[string]interface{}{ "has-env": hasEnv, "get-env": getEnv, "set-env": os.Setenv, "unset-env": os.Unsetenv, }) } //elvdoc:fn has-env // // ```elvish // has-env $name // ``` // // Test whether an environment variable exists. Examples: // // ```elvish-transcript // ~> has-env PATH // ▶ $true // ~> has-env NO_SUCH_ENV // ▶ $false // ``` // // @cf get-env set-env unset-env func hasEnv(key string) bool { _, ok := os.LookupEnv(key) return ok } //elvdoc:fn get-env // // ```elvish // get-env $name // ``` // // Gets the value of an environment variable. Throws an exception if the // environment variable does not exist. Examples: // // ```elvish-transcript // ~> get-env LANG // ▶ zh_CN.UTF-8 // ~> get-env NO_SUCH_ENV // Exception: non-existent environment variable // [tty], line 1: get-env NO_SUCH_ENV // ``` // // @cf has-env set-env unset-env func getEnv(key string) (string, error) { value, ok := os.LookupEnv(key) if !ok { return "", ErrNonExistentEnvVar } return value, nil } elvish-0.17.0/pkg/eval/builtin_fn_env_test.go000066400000000000000000000033021415471104000211630ustar00rootroot00000000000000package eval_test import ( "os" "testing" "src.elv.sh/pkg/eval" . "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" ) func TestGetEnv(t *testing.T) { restore := saveEnv("var") defer restore() os.Unsetenv("var") Test(t, That(`get-env var`).Throws(eval.ErrNonExistentEnvVar)) os.Setenv("var", "test1") Test(t, That(`get-env var`).Puts("test1"), That(`put $E:var`).Puts("test1"), ) os.Setenv("var", "test2") Test(t, That(`get-env var`).Puts("test2"), That(`put $E:var`).Puts("test2"), ) } func TestHasEnv(t *testing.T) { restore := saveEnv("var") defer restore() os.Setenv("var", "test1") Test(t, That(`has-env var`).Puts(true)) os.Unsetenv("var") Test(t, That(`has-env var`).Puts(false)) } func TestSetEnv(t *testing.T) { restore := saveEnv("var") defer restore() Test(t, That("set-env var test1").DoesNothing()) if envVal := os.Getenv("var"); envVal != "test1" { t.Errorf("got $E:var = %q, want 'test1'", envVal) } } func TestSetEnv_PATH(t *testing.T) { restore := saveEnv("PATH") defer restore() listSep := string(os.PathListSeparator) Test(t, That(`set-env PATH /test-path`), That(`put $paths`).Puts(vals.MakeList("/test-path")), That(`paths = [/test-path2 $@paths]`), That(`paths = [$true]`).Throws(vars.ErrPathMustBeString), That(`paths = ["/invalid`+string(os.PathListSeparator)+`:path"]`). Throws(vars.ErrPathContainsForbiddenChar), That(`paths = ["/invalid\000path"]`).Throws(vars.ErrPathContainsForbiddenChar), That(`get-env PATH`).Puts("/test-path2"+listSep+"/test-path"), ) } func saveEnv(name string) func() { oldValue, ok := os.LookupEnv(name) return func() { if ok { os.Setenv(name, oldValue) } } } elvish-0.17.0/pkg/eval/builtin_fn_flow.go000066400000000000000000000230241415471104000203060ustar00rootroot00000000000000package eval import ( "sync" "sync/atomic" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/eval/vals" ) // Flow control. // TODO(xiaq): Document "multi-error". func init() { addBuiltinFns(map[string]interface{}{ "run-parallel": runParallel, // Exception and control "fail": fail, "multi-error": multiErrorFn, "return": returnFn, "break": breakFn, "continue": continueFn, // Iterations. "each": each, "peach": peach, }) } //elvdoc:fn run-parallel // // ```elvish // run-parallel $callable ... // ``` // // Run several callables in parallel, and wait for all of them to finish. // // If one or more callables throw exceptions, the other callables continue running, // and a composite exception is thrown when all callables finish execution. // // The behavior of `run-parallel` is consistent with the behavior of pipelines, // except that it does not perform any redirections. // // Here is an example that lets you pipe the stdout and stderr of a command to two // different commands in order to independently capture the output of each byte stream: // // ```elvish-transcript // ~> fn capture {|f| // var pout = (file:pipe) // var perr = (file:pipe) // var out err // run-parallel { // $f > $pout[w] 2> $perr[w] // file:close $pout[w] // file:close $perr[w] // } { // set out = (slurp < $pout[r]) // file:close $pout[r] // } { // set err = (slurp < $perr[r]) // file:close $perr[r] // } // put $out $err // } // ~> capture { echo stdout-test; echo stderr-test >&2 } // ▶ "stdout-test\n" // ▶ "stderr-test\n" // ``` // // This command is intended for doing a fixed number of heterogeneous things in // parallel. If you need homogeneous parallel processing of possibly unbound data, // use `peach` instead. // // @cf peach func runParallel(fm *Frame, functions ...Callable) error { var wg sync.WaitGroup wg.Add(len(functions)) exceptions := make([]Exception, len(functions)) for i, function := range functions { go func(fm2 *Frame, function Callable, pexc *Exception) { err := function.Call(fm2, NoArgs, NoOpts) if err != nil { *pexc = err.(Exception) } wg.Done() }(fm.fork("[run-parallel function]"), function, &exceptions[i]) } wg.Wait() return MakePipelineError(exceptions) } //elvdoc:fn each // // ```elvish // each $f $input-list? // ``` // // Call `$f` on all inputs. // // An exception raised from [`break`](#break) is caught by `each`, and will // cause it to terminate early. // // An exception raised from [`continue`](#continue) is swallowed and can be used // to terminate a single iteration early. // // Examples: // // ```elvish-transcript // ~> range 5 8 | each {|x| * $x $x } // ▶ 25 // ▶ 36 // ▶ 49 // ~> each {|x| put $x[:3] } [lorem ipsum] // ▶ lor // ▶ ips // ``` // // @cf peach // // Etymology: Various languages, as `for each`. Happens to have the same name as // the iteration construct of // [Factor](http://docs.factorcode.org/content/word-each,sequences.html). func each(fm *Frame, f Callable, inputs Inputs) error { broken := false var err error inputs(func(v interface{}) { if broken { return } newFm := fm.fork("closure of each") ex := f.Call(newFm, []interface{}{v}, NoOpts) newFm.Close() if ex != nil { switch Reason(ex) { case nil, Continue: // nop case Break: broken = true default: broken = true err = ex } } }) return err } //elvdoc:fn peach // // ```elvish // peach $f $input-list? // ``` // // Calls `$f` on all inputs, possibly in parallel. // // Like `each`, an exception raised from [`break`](#break) will cause `peach` // to terminate early. However due to the parallel nature of `peach`, the exact // time of termination is non-deterministic and not even guaranteed. // // An exception raised from [`continue`](#continue) is swallowed and can be used // to terminate a single iteration early. // // Example (your output will differ): // // ```elvish-transcript // ~> range 1 10 | peach {|x| + $x 10 } // ▶ (num 12) // ▶ (num 13) // ▶ (num 11) // ▶ (num 16) // ▶ (num 18) // ▶ (num 14) // ▶ (num 17) // ▶ (num 15) // ▶ (num 19) // ~> range 1 101 | // peach {|x| if (== 50 $x) { break } else { put $x } } | // + (all) # 1+...+49 = 1225; 1+...+100 = 5050 // ▶ (num 1328) // ``` // // This command is intended for homogeneous processing of possibly unbound data. If // you need to do a fixed number of heterogeneous things in parallel, use // `run-parallel`. // // @cf each run-parallel func peach(fm *Frame, f Callable, inputs Inputs) error { var wg sync.WaitGroup var broken int32 var errMu sync.Mutex var err error inputs(func(v interface{}) { if atomic.LoadInt32(&broken) != 0 { return } wg.Add(1) go func() { newFm := fm.fork("closure of peach") newFm.ports[0] = DummyInputPort ex := f.Call(newFm, []interface{}{v}, NoOpts) newFm.Close() if ex != nil { switch Reason(ex) { case nil, Continue: // nop case Break: atomic.StoreInt32(&broken, 1) default: errMu.Lock() err = diag.Errors(err, ex) defer errMu.Unlock() atomic.StoreInt32(&broken, 1) } } wg.Done() }() }) wg.Wait() return err } // FailError is an error returned by the "fail" command. type FailError struct{ Content interface{} } // Error returns the string representation of the cause. func (e FailError) Error() string { return vals.ToString(e.Content) } // Fields returns a structmap for accessing fields from Elvish. func (e FailError) Fields() vals.StructMap { return failFields{e} } type failFields struct{ e FailError } func (failFields) IsStructMap() {} func (f failFields) Type() string { return "fail" } func (f failFields) Content() interface{} { return f.e.Content } //elvdoc:fn fail // // ```elvish // fail $v // ``` // // Throws an exception; `$v` may be any type. If `$v` is already an exception, // `fail` rethrows it. // // ```elvish-transcript // ~> fail bad // Exception: bad // [tty 9], line 1: fail bad // ~> put ?(fail bad) // ▶ ?(fail bad) // ~> fn f { fail bad } // ~> fail ?(f) // Exception: bad // Traceback: // [tty 7], line 1: // fn f { fail bad } // [tty 8], line 1: // fail ?(f) // ``` func fail(v interface{}) error { if e, ok := v.(error); ok { // MAYBE TODO: if v is an exception, attach a "rethrown" stack trace, // like Java return e } return FailError{v} } func multiErrorFn(excs ...Exception) error { return PipelineError{excs} } //elvdoc:fn return // // Raises the special "return" exception. When raised inside a named function // (defined by the [`fn` keyword](language.html#fn)) it is captured by the // function and causes the function to terminate. It is not captured by an // ordinary anonymous function. // // Because `return` raises an exception it can be caught by a // [`try`](language.html#try) block. If not caught, either implicitly by a // named function or explicitly, it causes a failure like any other uncaught // exception. // // See the discussion about [flow commands and // exceptions](language.html#exception-and-flow-commands) // // **Note**: If you want to shadow the builtin `return` function with a local // wrapper, do not define it with `fn` as `fn` swallows the special exception // raised by return. Consider this example: // // ```elvish-transcript // ~> use builtin // ~> fn return { put return; builtin:return } // ~> fn test-return { put before; return; put after } // ~> test-return // ▶ before // ▶ return // ▶ after // ``` // // Instead, shadow the function by directly assigning to `return~`: // // ```elvish-transcript // ~> use builtin // ~> var return~ = { put return; builtin:return } // ~> fn test-return { put before; return; put after } // ~> test-return // ▶ before // ▶ return // ``` func returnFn() error { return Return } //elvdoc:fn break // // Raises the special "break" exception. When raised inside a loop it is // captured and causes the loop to terminate. // // Because `break` raises an exception it can be caught by a // [`try`](language.html#try) block. If not caught, either implicitly by a loop // or explicitly, it causes a failure like any other uncaught exception. // // See the discussion about [flow commands and exceptions](language.html#exception-and-flow-commands) // // **Note**: You can create a `break` function and it will shadow the builtin // command. If you do so you should explicitly invoke the builtin. For example: // // ```elvish-transcript // ~> use builtin // ~> fn break { put 'break'; builtin:break; put 'should not appear' } // ~> for x [a b c] { put $x; break; put 'unexpected' } // ▶ a // ▶ break // ``` func breakFn() error { return Break } //elvdoc:fn continue // // Raises the special "continue" exception. When raised inside a loop it is // captured and causes the loop to begin its next iteration. // // Because `continue` raises an exception it can be caught by a // [`try`](language.html#try) block. If not caught, either implicitly by a loop // or explicitly, it causes a failure like any other uncaught exception. // // See the discussion about [flow commands and exceptions](language.html#exception-and-flow-commands) // // **Note**: You can create a `continue` function and it will shadow the builtin // command. If you do so you should explicitly invoke the builtin. For example: // // ```elvish-transcript // ~> use builtin // ~> fn continue { put 'continue'; builtin:continue; put 'should not appear' } // ~> for x [a b c] { put $x; continue; put 'unexpected' } // ▶ a // ▶ continue // ▶ b // ▶ continue // ▶ c // ▶ continue // ``` func continueFn() error { return Continue } elvish-0.17.0/pkg/eval/builtin_fn_flow_test.go000066400000000000000000000055661415471104000213600ustar00rootroot00000000000000package eval_test import ( "testing" . "src.elv.sh/pkg/eval" . "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/eval/vals" ) func TestRunParallel(t *testing.T) { Test(t, That(`run-parallel { put lorem } { echo ipsum }`). Puts("lorem").Prints("ipsum\n"), That(`run-parallel { } { fail foo }`).Throws(FailError{"foo"}), ) } func TestEach(t *testing.T) { Test(t, That(`put 1 233 | each $put~`).Puts("1", "233"), That(`echo "1\n233" | each $put~`).Puts("1", "233"), That(`echo "1\r\n233" | each $put~`).Puts("1", "233"), That(`each $put~ [1 233]`).Puts("1", "233"), That(`range 10 | each {|x| if (== $x 4) { break }; put $x }`). Puts(0, 1, 2, 3), That(`range 10 | each {|x| if (== $x 4) { continue }; put $x }`). Puts(0, 1, 2, 3, 5, 6, 7, 8, 9), That(`range 10 | each {|x| if (== $x 4) { fail haha }; put $x }`). Puts(0, 1, 2, 3).Throws(AnyError), // TODO(xiaq): Test that "each" does not close the stdin. ) } func TestPeach(t *testing.T) { // Testing the `peach` builtin is a challenge since, by definition, the order of execution is // undefined. Test(t, // Verify the output has the expected values when sorted. That(`range 5 | peach {|x| * 2 $x } | order`).Puts(0, 2, 4, 6, 8), // Handling of "continue". That(`range 5 | peach {|x| if (== $x 2) { continue }; * 2 $x } | order`). Puts(0, 2, 6, 8), // Test that the order of output does not necessarily match the order of // input. // // Most of the time this effect can be observed without the need of any // jitter, but if the system only has one CPU core to execute goroutines // (which can happen even when GOMAXPROCS > 1), the scheduling of // goroutines can become deterministic. The random jitter fixes that by // forcing goroutines to yield the thread and allow other goroutines to // execute. That(` var @in = (range 100) while $true { var @out = (all $in | peach {|x| sleep (* (rand) 0.01); put $x }) if (not-eq $in $out) { put $true break } } `).Puts(true), // Verify that exceptions are propagated. That(`peach {|x| fail $x } [a]`). Throws(FailError{"a"}, "fail $x ", "peach {|x| fail $x } [a]"), // Verify that `break` works by terminating the `peach` before the entire sequence is // consumed. That(` var tot = 0 range 1 101 | peach {|x| if (== 50 $x) { break } else { put $x } } | < (+ (all)) (+ (range 1 101)) `).Puts(true), ) } func TestFail(t *testing.T) { Test(t, That("fail haha").Throws(FailError{"haha"}, "fail haha"), That("fn f { fail haha }", "fail ?(f)").Throws( FailError{"haha"}, "fail haha ", "f"), That("fail []").Throws( FailError{vals.EmptyList}, "fail []"), That("put ?(fail 1)[reason][type]").Puts("fail"), That("put ?(fail 1)[reason][content]").Puts("1"), ) } func TestReturn(t *testing.T) { Test(t, That("return").Throws(Return), // Use of return inside fn is tested in TestFn ) } elvish-0.17.0/pkg/eval/builtin_fn_fs.go000066400000000000000000000045461415471104000177570ustar00rootroot00000000000000package eval import ( "errors" "src.elv.sh/pkg/fsutil" "src.elv.sh/pkg/store/storedefs" "src.elv.sh/pkg/eval/errs" ) // Filesystem commands. // ErrStoreNotConnected is thrown by dir-history when the store is not connected. var ErrStoreNotConnected = errors.New("store not connected") func init() { addBuiltinFns(map[string]interface{}{ // Directory "cd": cd, "dir-history": dirs, // Path "tilde-abbr": tildeAbbr, }) } //elvdoc:fn cd // // ```elvish // cd $dirname // ``` // // Change directory. This affects the entire process; i.e., all threads // whether running indirectly (e.g., prompt functions) or started explicitly // by commands such as [`peach`](#peach). // // Note that Elvish's `cd` does not support `cd -`. // // @cf pwd func cd(fm *Frame, args ...string) error { var dir string switch len(args) { case 0: var err error dir, err = fsutil.GetHome("") if err != nil { return err } case 1: dir = args[0] default: return errs.ArityMismatch{What: "arguments", ValidLow: 0, ValidHigh: 1, Actual: len(args)} } return fm.Evaler.Chdir(dir) } //elvdoc:fn dir-history // // ```elvish // dir-history // ``` // // Return a list containing the interactive directory history. Each element is a map with two keys: // `path` and `score`. The list is sorted by descending score. // // Example: // // ```elvish-transcript // ~> dir-history | take 1 // ▶ [&path=/Users/foo/.elvish &score=96.79928] // ``` // // @cf edit:command-history type dirHistoryEntry struct { Path string Score float64 } func (dirHistoryEntry) IsStructMap() {} func dirs(fm *Frame) error { daemon := fm.Evaler.DaemonClient if daemon == nil { return ErrStoreNotConnected } dirs, err := daemon.Dirs(storedefs.NoBlacklist) if err != nil { return err } out := fm.ValueOutput() for _, dir := range dirs { err := out.Put(dirHistoryEntry{dir.Path, dir.Score}) if err != nil { return err } } return nil } //elvdoc:fn tilde-abbr // // ```elvish // tilde-abbr $path // ``` // // If `$path` represents a path under the home directory, replace the home // directory with `~`. Examples: // // ```elvish-transcript // ~> echo $E:HOME // /Users/foo // ~> tilde-abbr /Users/foo // ▶ '~' // ~> tilde-abbr /Users/foobar // ▶ /Users/foobar // ~> tilde-abbr /Users/foo/a/b // ▶ '~/a/b' // ``` func tildeAbbr(path string) string { return fsutil.TildeAbbr(path) } elvish-0.17.0/pkg/eval/builtin_fn_fs_test.go000066400000000000000000000035401415471104000210070ustar00rootroot00000000000000package eval_test import ( "errors" "fmt" "os/user" "path/filepath" "testing" . "src.elv.sh/pkg/eval" "src.elv.sh/pkg/testutil" "src.elv.sh/pkg/eval/errs" . "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/fsutil" "src.elv.sh/pkg/parse" ) // For error injection into the fsutil.GetHome function. func currentUser() (*user.User, error) { return nil, fmt.Errorf("user unknown") } func TestTildeAbbr(t *testing.T) { tmpHome := testutil.InTempHome(t) testutil.MustMkdirAll("dir") testutil.MustCreateEmpty("file") Test(t, That("tilde-abbr "+parse.Quote(filepath.Join(tmpHome, "foobar"))). Puts(filepath.Join("~", "foobar")), ) } func TestCd(t *testing.T) { tmpHome := testutil.InTempHome(t) testutil.MustMkdirAll("d1") d1Path := filepath.Join(tmpHome, "d1") // We install this mock for all tests, not just the one that needs it, // because it should not be invoked by any of the other tests. fsutil.CurrentUser = currentUser defer func() { fsutil.CurrentUser = user.Current }() Test(t, That(`cd dir1 dir2`).Throws(ErrorWithType(errs.ArityMismatch{}), "cd dir1 dir2"), // Basic `cd` test and verification that `$pwd` is correct. That(`old = $pwd; cd `+d1Path+`; put $pwd; cd $old; eq $old $pwd`).Puts(d1Path, true), // Verify that `cd` with no arg defaults to the home directory. That(`cd `+d1Path+`; cd; eq $pwd $E:HOME`).Puts(true), // Verify that `cd` with no arg and no $E:HOME var fails since our // currentUser mock should result in being unable to dynamically // determine the user's home directory. That(`unset-env HOME; cd; set-env HOME `+tmpHome).Throws( errors.New("can't resolve ~: user unknown"), "cd"), ) } func TestDirHistory(t *testing.T) { // TODO: Add a Store mock so we can test the behavior when a history Store // is available. Test(t, That(`dir-history`).Throws(ErrStoreNotConnected, "dir-history"), ) } elvish-0.17.0/pkg/eval/builtin_fn_io.go000066400000000000000000000472311415471104000177540ustar00rootroot00000000000000package eval import ( "bufio" "encoding/json" "fmt" "io" "strconv" "strings" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/strutil" ) // Input and output. func init() { addBuiltinFns(map[string]interface{}{ // Value output "put": put, // Bytes input "read-upto": readUpto, "read-line": readLine, // Bytes output "print": print, "echo": echo, "pprint": pprint, "repr": repr, "show": show, "printf": printf, // Only bytes or values // // These are now implemented as commands forwarding one part of input to // output and discarding the other. A future optimization the evaler can // do is to connect the relevant parts directly together without any // kind of forwarding. "only-bytes": onlyBytes, "only-values": onlyValues, // Bytes to value "slurp": slurp, "from-lines": fromLines, "from-json": fromJSON, "from-terminated": fromTerminated, // Value to bytes "to-lines": toLines, "to-json": toJSON, "to-terminated": toTerminated, }) } //elvdoc:fn put // // ```elvish // put $value... // ``` // // Takes arbitrary arguments and write them to the structured stdout. // // Examples: // // ```elvish-transcript // ~> put a // ▶ a // ~> put lorem ipsum [a b] { ls } // ▶ lorem // ▶ ipsum // ▶ [a b] // ▶ // ``` // // **Note**: It is almost never necessary to use `put (...)` - just write the // `...` part. For example, `put (eq a b)` is the equivalent to just `eq a b`. // // Etymology: Various languages, in particular // [C](https://manpages.debian.org/stretch/manpages-dev/puts.3.en.html) and // [Ruby](https://ruby-doc.org/core-2.2.2/IO.html#method-i-puts) as `puts`. func put(fm *Frame, args ...interface{}) error { out := fm.ValueOutput() for _, a := range args { err := out.Put(a) if err != nil { return err } } return nil } //elvdoc:fn read-upto // // ```elvish // read-upto $terminator // ``` // // Reads byte input until `$terminator` or end-of-file is encountered. It outputs the part of the // input read as a string value. The output contains the trailing `$terminator`, unless `read-upto` // terminated at end-of-file. // // The `$terminator` must be a single ASCII character such as `"\x00"` (NUL). // // Examples: // // ```elvish-transcript // ~> echo "a,b,c" | read-upto "," // ▶ 'a,' // ~> echo "foo\nbar" | read-upto "\n" // ▶ "foo\n" // ~> echo "a.elv\x00b.elv" | read-upto "\x00" // ▶ "a.elv\x00" // ~> print "foobar" | read-upto "\n" // ▶ foobar // ``` func readUpto(fm *Frame, terminator string) (string, error) { if err := checkTerminator(terminator); err != nil { return "", err } in := fm.InputFile() var buf []byte for { var b [1]byte _, err := in.Read(b[:]) if err != nil { if err == io.EOF { break } return "", err } buf = append(buf, b[0]) if b[0] == terminator[0] { break } } return string(buf), nil } func checkTerminator(s string) error { if len(s) != 1 || s[0] > 127 { return errs.BadValue{What: "terminator", Valid: "a single ASCII character", Actual: parse.Quote(s)} } return nil } //elvdoc:fn read-line // // ```elvish // read-line // ``` // // Reads a single line from byte input, and writes the line to the value output, // stripping the line ending. A line can end with `"\r\n"`, `"\n"`, or end of // file. Examples: // // ```elvish-transcript // ~> print line | read-line // ▶ line // ~> print "line\n" | read-line // ▶ line // ~> print "line\r\n" | read-line // ▶ line // ~> print "line-with-extra-cr\r\r\n" | read-line // ▶ "line-with-extra-cr\r" // ``` func readLine(fm *Frame) (string, error) { s, err := readUpto(fm, "\n") if err != nil { return "", err } return strutil.ChopLineEnding(s), nil } //elvdoc:fn print // // ```elvish // print &sep=' ' $value... // ``` // // Like `echo`, just without the newline. // // @cf echo // // Etymology: Various languages, in particular // [Perl](https://perldoc.perl.org/functions/print.html) and // [zsh](http://zsh.sourceforge.net/Doc/Release/Shell-Builtin-Commands.html), whose // `print`s do not print a trailing newline. type printOpts struct{ Sep string } func (o *printOpts) SetDefaultOptions() { o.Sep = " " } func print(fm *Frame, opts printOpts, args ...interface{}) error { out := fm.ByteOutput() for i, arg := range args { if i > 0 { _, err := out.WriteString(opts.Sep) if err != nil { return err } } _, err := out.WriteString(vals.ToString(arg)) if err != nil { return err } } return nil } //elvdoc:fn printf // // ```elvish // printf $template $value... // ``` // // Prints values to the byte stream according to a template. If you need to inject the output into // the value stream use this pattern: `printf .... | slurp`. That ensures that any newlines in the // output of `printf` do not cause its output to be broken into multiple values, thus eliminating // the newlines, which will occur if you do `put (printf ....)`. // // Like [`print`](#print), this command does not add an implicit newline; include an explicit `"\n"` // in the formatting template instead. For example, `printf "%.1f\n" (/ 10.0 3)`. // // See Go's [`fmt`](https://golang.org/pkg/fmt/#hdr-Printing) package for // details about the formatting verbs and the various flags that modify the // default behavior, such as padding and justification. // // Unlike Go, each formatting verb has a single associated internal type, and // accepts any argument that can reasonably be converted to that type: // // - The verbs `%s`, `%q` and `%v` convert the corresponding argument to a // string in different ways: // // - `%s` uses [to-string](#to-string) to convert a value to string. // // - `%q` uses [repr](#repr) to convert a value to string. // // - `%v` is equivalent to `%s`, and `%#v` is equivalent to `%q`. // // - The verb `%t` first convert the corresponding argument to a boolean using // [bool](#bool), and then uses its Go counterpart to format the boolean. // // - The verbs `%b`, `%c`, `%d`, `%o`, `%O`, `%x`, `%X` and `%U` first convert // the corresponding argument to an integer using an internal algorithm, and // use their Go counterparts to format the integer. // // - The verbs `%e`, `%E`, `%f`, `%F`, `%g` and `%G` first convert the // corresponding argument to a floating-point number using // [float64](#float64), and then use their Go counterparts to format the // number. // // The special verb `%%` prints a literal `%` and consumes no argument. // // Verbs not documented above are not supported. // // Examples: // // ```elvish-transcript // ~> printf "%10s %.2f\n" Pi $math:pi // Pi 3.14 // ~> printf "%-10s %.2f %s\n" Pi $math:pi $math:pi // Pi 3.14 3.141592653589793 // ~> printf "%d\n" 0b11100111 // 231 // ~> printf "%08b\n" 231 // 11100111 // ~> printf "list is: %q\n" [foo bar 'foo bar'] // list is: [foo bar 'foo bar'] // ``` // // **Note**: Compared to the [POSIX `printf` // command](https://pubs.opengroup.org/onlinepubs/007908799/xcu/printf.html) // found in other shells, there are 3 key differences: // // - The behavior of the formatting verbs are based on Go's // [`fmt`](https://golang.org/pkg/fmt/) package instead of the POSIX // specification. // // - The number of arguments after the formatting template must match the number // of formatting verbs. The POSIX command will repeat the template string to // consume excess values; this command does not have that behavior. // // - This command does not interpret escape sequences such as `\n`; just use // [double-quoted strings](language.html#double-quoted-string). // // @cf print echo pprint repr func printf(fm *Frame, template string, args ...interface{}) error { wrappedArgs := make([]interface{}, len(args)) for i, arg := range args { wrappedArgs[i] = formatter{arg} } _, err := fmt.Fprintf(fm.ByteOutput(), template, wrappedArgs...) return err } type formatter struct { wrapped interface{} } func (f formatter) Format(state fmt.State, r rune) { wrapped := f.wrapped switch r { case 's': writeFmt(state, 's', vals.ToString(wrapped)) case 'q': // TODO: Support using the precision flag to specify indentation. writeFmt(state, 's', vals.Repr(wrapped, vals.NoPretty)) case 'v': var s string if state.Flag('#') { s = vals.Repr(wrapped, vals.NoPretty) } else { s = vals.ToString(wrapped) } writeFmt(state, 's', s) case 't': writeFmt(state, 't', vals.Bool(wrapped)) case 'b', 'c', 'd', 'o', 'O', 'x', 'X', 'U': var i int if err := vals.ScanToGo(wrapped, &i); err != nil { fmt.Fprintf(state, "%%!%c(%s)", r, err.Error()) return } writeFmt(state, r, i) case 'e', 'E', 'f', 'F', 'g', 'G': var f float64 if err := vals.ScanToGo(wrapped, &f); err != nil { fmt.Fprintf(state, "%%!%c(%s)", r, err.Error()) return } writeFmt(state, r, f) default: fmt.Fprintf(state, "%%!%c(unsupported formatting verb)", r) } } // Writes to State using the flag it stores, but with a potentially different // verb and value. func writeFmt(state fmt.State, v rune, val interface{}) { // Reconstruct the verb string. var sb strings.Builder sb.WriteRune('%') for _, f := range "+-# 0" { if state.Flag(int(f)) { sb.WriteRune(f) } } if w, ok := state.Width(); ok { sb.WriteString(strconv.Itoa(w)) } if p, ok := state.Precision(); ok { sb.WriteRune('.') sb.WriteString(strconv.Itoa(p)) } sb.WriteRune(v) fmt.Fprintf(state, sb.String(), val) } //elvdoc:fn echo // // ```elvish // echo &sep=' ' $value... // ``` // // Print all arguments, joined by the `sep` option, and followed by a newline. // // Examples: // // ```elvish-transcript // ~> echo Hello elvish // Hello elvish // ~> echo "Hello elvish" // Hello elvish // ~> echo &sep=, lorem ipsum // lorem,ipsum // ``` // // Notes: The `echo` builtin does not treat `-e` or `-n` specially. For instance, // `echo -n` just prints `-n`. Use double-quoted strings to print special // characters, and `print` to suppress the trailing newline. // // @cf print // // Etymology: Bourne sh. func echo(fm *Frame, opts printOpts, args ...interface{}) error { err := print(fm, opts, args...) if err != nil { return err } _, err = fm.ByteOutput().WriteString("\n") return err } //elvdoc:fn pprint // // ```elvish // pprint $value... // ``` // // Pretty-print representations of Elvish values. Examples: // // ```elvish-transcript // ~> pprint [foo bar] // [ // foo // bar // ] // ~> pprint [&k1=v1 &k2=v2] // [ // &k2= // v2 // &k1= // v1 // ] // ``` // // The output format is subject to change. // // @cf repr func pprint(fm *Frame, args ...interface{}) error { out := fm.ByteOutput() for _, arg := range args { _, err := out.WriteString(vals.Repr(arg, 0)) if err != nil { return err } _, err = out.WriteString("\n") if err != nil { return err } } return nil } //elvdoc:fn repr // // ```elvish // repr $value... // ``` // // Writes representation of `$value`s, separated by space and followed by a // newline. Example: // // ```elvish-transcript // ~> repr [foo 'lorem ipsum'] "aha\n" // [foo 'lorem ipsum'] "aha\n" // ``` // // @cf pprint // // Etymology: [Python](https://docs.python.org/3/library/functions.html#repr). func repr(fm *Frame, args ...interface{}) error { out := fm.ByteOutput() for i, arg := range args { if i > 0 { _, err := out.WriteString(" ") if err != nil { return err } } _, err := out.WriteString(vals.Repr(arg, vals.NoPretty)) if err != nil { return err } } _, err := out.WriteString("\n") return err } //elvdoc:fn show // // ```elvish // show $e // ``` // // Shows the value to the output, which is assumed to be a VT-100-compatible // terminal. // // Currently, the only type of value that can be showed is exceptions, but this // will likely expand in future. // // Example: // // ```elvish-transcript // ~> e = ?(fail lorem-ipsum) // ~> show $e // Exception: lorem-ipsum // [tty 3], line 1: e = ?(fail lorem-ipsum) // ``` func show(fm *Frame, v diag.Shower) error { out := fm.ByteOutput() _, err := out.WriteString(v.Show("")) if err != nil { return err } _, err = out.WriteString("\n") return err } //elvdoc:fn only-bytes // // ```elvish // only-bytes // ``` // // Passes byte input to output, and discards value inputs. // // Example: // // ```elvish-transcript // ~> { put value; echo bytes } | only-bytes // bytes // ``` func onlyBytes(fm *Frame) error { // Discard values in a goroutine. valuesDone := make(chan struct{}) go func() { for range fm.InputChan() { } close(valuesDone) }() // Make sure the goroutine has finished before returning. defer func() { <-valuesDone }() _, err := io.Copy(fm.ByteOutput(), fm.InputFile()) return err } //elvdoc:fn only-values // // ```elvish // only-values // ``` // // Passes value input to output, and discards byte inputs. // // Example: // // ```elvish-transcript // ~> { put value; echo bytes } | only-values // ▶ value // ``` func onlyValues(fm *Frame) error { // Discard bytes in a goroutine. bytesDone := make(chan struct{}) go func() { // Ignore the error _, _ = io.Copy(blackholeWriter{}, fm.InputFile()) close(bytesDone) }() // Wait for the goroutine to finish before returning. defer func() { <-bytesDone }() // Forward values. out := fm.ValueOutput() for v := range fm.InputChan() { err := out.Put(v) if err != nil { return err } } return nil } type blackholeWriter struct{} func (blackholeWriter) Write(p []byte) (int, error) { return len(p), nil } //elvdoc:fn slurp // // ```elvish // slurp // ``` // // Reads bytes input into a single string, and put this string on structured // stdout. // // Example: // // ```elvish-transcript // ~> echo "a\nb" | slurp // ▶ "a\nb\n" // ``` // // Etymology: Perl, as // [`File::Slurp`](http://search.cpan.org/~uri/File-Slurp-9999.19/lib/File/Slurp.pm). func slurp(fm *Frame) (string, error) { b, err := io.ReadAll(fm.InputFile()) return string(b), err } //elvdoc:fn from-lines // // ```elvish // from-lines // ``` // // Splits byte input into lines, and writes them to the value output. Value // input is ignored. // // ```elvish-transcript // ~> { echo a; echo b } | from-lines // ▶ a // ▶ b // ~> { echo a; put b } | from-lines // ▶ a // ``` // // @cf from-terminated read-upto to-lines func fromLines(fm *Frame) error { filein := bufio.NewReader(fm.InputFile()) out := fm.ValueOutput() for { line, err := filein.ReadString('\n') if line != "" { err := out.Put(strutil.ChopLineEnding(line)) if err != nil { return err } } if err != nil { if err != io.EOF { return err } return nil } } } //elvdoc:fn from-json // // ```elvish // from-json // ``` // // Takes bytes stdin, parses it as JSON and puts the result on structured stdout. // The input can contain multiple JSONs, and whitespace between them are ignored. // // Note that JSON's only number type corresponds to Elvish's floating-point // number type, and is always considered [inexact](language.html#exactness). // It may be necessary to coerce JSON numbers to exact numbers using // [exact-num](#exact-num). // // Examples: // // ```elvish-transcript // ~> echo '"a"' | from-json // ▶ a // ~> echo '["lorem", "ipsum"]' | from-json // ▶ [lorem ipsum] // ~> echo '{"lorem": "ipsum"}' | from-json // ▶ [&lorem=ipsum] // ~> # multiple JSONs running together // echo '"a""b"["x"]' | from-json // ▶ a // ▶ b // ▶ [x] // ~> # multiple JSONs separated by newlines // echo '"a" // {"k": "v"}' | from-json // ▶ a // ▶ [&k=v] // ``` // // @cf to-json func fromJSON(fm *Frame) error { in := fm.InputFile() out := fm.ValueOutput() dec := json.NewDecoder(in) for { var v interface{} err := dec.Decode(&v) if err != nil { if err == io.EOF { return nil } return err } converted, err := fromJSONInterface(v) if err != nil { return err } err = out.Put(converted) if err != nil { return err } } } // Converts a interface{} that results from json.Unmarshal to an Elvish value. func fromJSONInterface(v interface{}) (interface{}, error) { switch v := v.(type) { case nil, bool, string: return v, nil case float64: return v, nil case []interface{}: vec := vals.EmptyList for _, elem := range v { converted, err := fromJSONInterface(elem) if err != nil { return nil, err } vec = vec.Cons(converted) } return vec, nil case map[string]interface{}: m := vals.EmptyMap for key, val := range v { convertedVal, err := fromJSONInterface(val) if err != nil { return nil, err } m = m.Assoc(key, convertedVal) } return m, nil default: return nil, fmt.Errorf("unexpected json type: %T", v) } } //elvdoc:fn from-terminated // // ```elvish // from-terminated $terminator // ``` // // Splits byte input into lines at each `$terminator` character, and writes // them to the value output. If the byte input ends with `$terminator`, it is // dropped. Value input is ignored. // // The `$terminator` must be a single ASCII character such as `"\x00"` (NUL). // // ```elvish-transcript // ~> { echo a; echo b } | from-terminated "\x00" // ▶ "a\nb\n" // ~> print "a\x00b" | from-terminated "\x00" // ▶ a // ▶ b // ~> print "a\x00b\x00" | from-terminated "\x00" // ▶ a // ▶ b // ``` // // @cf from-lines read-upto to-terminated func fromTerminated(fm *Frame, terminator string) error { if err := checkTerminator(terminator); err != nil { return err } filein := bufio.NewReader(fm.InputFile()) out := fm.ValueOutput() for { line, err := filein.ReadString(terminator[0]) if line != "" { err := out.Put(strutil.ChopTerminator(line, terminator[0])) if err != nil { return err } } if err != nil { if err != io.EOF { logger.Println("error on reading:", err) return err } return nil } } } //elvdoc:fn to-lines // // ```elvish // to-lines $input? // ``` // // Writes each value input to a separate line in the byte output. Byte input is // ignored. // // ```elvish-transcript // ~> put a b | to-lines // a // b // ~> to-lines [a b] // a // b // ~> { put a; echo b } | to-lines // b // a // ``` // // @cf from-lines to-terminated func toLines(fm *Frame, inputs Inputs) error { out := fm.ByteOutput() var errOut error inputs(func(v interface{}) { if errOut != nil { return } // TODO: Don't ignore the error. _, errOut = fmt.Fprintln(out, vals.ToString(v)) }) return errOut } //elvdoc:fn to-terminated // // ```elvish // to-terminated $terminator $input? // ``` // // Writes each value input to the byte output with the specified terminator character. Byte input is // ignored. This behavior is useful, for example, when feeding output into a program that accepts // NUL terminated lines to avoid ambiguities if the values contains newline characters. // // The `$terminator` must be a single ASCII character such as `"\x00"` (NUL). // // ```elvish-transcript // ~> put a b | to-terminated "\x00" | slurp // ▶ "a\x00b\x00" // ~> to-terminated "\x00" [a b] | slurp // ▶ "a\x00b\x00" // ``` // // @cf from-terminated to-lines func toTerminated(fm *Frame, terminator string, inputs Inputs) error { if err := checkTerminator(terminator); err != nil { return err } out := fm.ByteOutput() var errOut error inputs(func(v interface{}) { if errOut != nil { return } _, errOut = fmt.Fprint(out, vals.ToString(v), terminator) }) return errOut } //elvdoc:fn to-json // // ```elvish // to-json // ``` // // Takes structured stdin, convert it to JSON and puts the result on bytes stdout. // // ```elvish-transcript // ~> put a | to-json // "a" // ~> put [lorem ipsum] | to-json // ["lorem","ipsum"] // ~> put [&lorem=ipsum] | to-json // {"lorem":"ipsum"} // ``` // // @cf from-json func toJSON(fm *Frame, inputs Inputs) error { encoder := json.NewEncoder(fm.ByteOutput()) var errEncode error inputs(func(v interface{}) { if errEncode != nil { return } errEncode = encoder.Encode(v) }) return errEncode } elvish-0.17.0/pkg/eval/builtin_fn_io_test.go000066400000000000000000000136601415471104000210120ustar00rootroot00000000000000package eval_test import ( "os" "testing" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/errs" . "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/eval/vals" ) func TestPut(t *testing.T) { Test(t, That(`put foo bar`).Puts("foo", "bar"), That(`put $nil`).Puts(nil), thatOutputErrorIsBubbled("put foo"), ) } func TestReadUpto(t *testing.T) { Test(t, That("print abcd | read-upto c").Puts("abc"), // read-upto does not consume more than needed That("print abcd | { read-upto c; slurp }").Puts("abc", "d"), // read-upto reads up to EOF That("print abcd | read-upto z").Puts("abcd"), That("print abcd | read-upto cd").Throws( errs.BadValue{What: "terminator", Valid: "a single ASCII character", Actual: "cd"}), thatOutputErrorIsBubbled("print abcd | read-upto c"), ) } func TestReadLine(t *testing.T) { Test(t, That(`print eof-ending | read-line`).Puts("eof-ending"), That(`print "lf-ending\n" | read-line`).Puts("lf-ending"), That(`print "crlf-ending\r\n" | read-line`).Puts("crlf-ending"), That(`print "extra-cr\r\r\n" | read-line`).Puts("extra-cr\r"), thatOutputErrorIsBubbled(`print eof-ending | read-line`), ) } func TestPrint(t *testing.T) { Test(t, That(`print [foo bar]`).Prints("[foo bar]"), That(`print foo bar &sep=,`).Prints("foo,bar"), thatOutputErrorIsBubbled("print foo"), ) } func TestEcho(t *testing.T) { Test(t, That(`echo [foo bar]`).Prints("[foo bar]\n"), thatOutputErrorIsBubbled("echo foo"), ) } func TestPprint(t *testing.T) { Test(t, That(`pprint [foo bar]`).Prints("[\n foo\n bar\n]\n"), thatOutputErrorIsBubbled("pprint foo"), ) } func TestReprCmd(t *testing.T) { Test(t, That(`repr foo bar ['foo bar']`).Prints("foo bar ['foo bar']\n"), thatOutputErrorIsBubbled("repr foo"), ) } func TestShow(t *testing.T) { Test(t, // A sanity test that show writes something. That(`show ?(fail foo) | !=s (slurp) ''`).Puts(true), thatOutputErrorIsBubbled("repr ?(fail foo)"), ) } func TestOnlyBytesAndOnlyValues(t *testing.T) { Test(t, // Baseline That(`{ print bytes; put values }`).Prints("bytes").Puts("values"), That(`{ print bytes; put values } | only-bytes`).Prints("bytes").Puts(), thatOutputErrorIsBubbled("{ print bytes; put values } | only-bytes"), ) } func TestOnlyValues(t *testing.T) { Test(t, // Baseline That(`{ print bytes; put values }`).Prints("bytes").Puts("values"), That(`{ print bytes; put values } | only-values`).Prints("").Puts("values"), thatOutputErrorIsBubbled("{ print bytes; put values } | only-values"), ) } func TestSlurp(t *testing.T) { Test(t, That(`print "a\nb" | slurp`).Puts("a\nb"), thatOutputErrorIsBubbled(`print "a\nb" | slurp`), ) } func TestFromLines(t *testing.T) { Test(t, That(`print "a\nb" | from-lines`).Puts("a", "b"), That(`print "a\nb\n" | from-lines`).Puts("a", "b"), thatOutputErrorIsBubbled(`print "a\nb\n" | from-lines`), ) } func TestFromTerminated(t *testing.T) { Test(t, That(`print "a\nb\x00\x00c\x00d" | from-terminated "\x00"`).Puts("a\nb", "", "c", "d"), That(`print "a\x00b\x00" | from-terminated "\x00"`).Puts("a", "b"), That(`print aXbXcXXd | from-terminated "X"`).Puts("a", "b", "c", "", "d"), That(`from-terminated "xyz"`).Throws( errs.BadValue{What: "terminator", Valid: "a single ASCII character", Actual: "xyz"}), thatOutputErrorIsBubbled("print aXbX | from-terminated X"), ) } func TestFromJson(t *testing.T) { Test(t, That(`echo '{"k": "v", "a": [1, 2]}' '"foo"' | from-json`). Puts(vals.MakeMap("k", "v", "a", vals.MakeList(1.0, 2.0)), "foo"), That(`echo '[null, "foo"]' | from-json`).Puts( vals.MakeList(nil, "foo")), That(`echo 'invalid' | from-json`).Throws(AnyError), thatOutputErrorIsBubbled(`echo '[]' | from-json`), ) } func TestToLines(t *testing.T) { Test(t, That(`put "l\norem" ipsum | to-lines`).Prints("l\norem\nipsum\n"), thatOutputErrorIsBubbled("to-lines [foo]"), ) } func TestToTerminated(t *testing.T) { Test(t, That(`put "l\norem" ipsum | to-terminated "\x00"`).Prints("l\norem\x00ipsum\x00"), That(`to-terminated "X" [a b c]`).Prints("aXbXcX"), That(`to-terminated "XYZ" [a b c]`).Throws( errs.BadValue{What: "terminator", Valid: "a single ASCII character", Actual: "XYZ"}), thatOutputErrorIsBubbled(`to-terminated "X" [a b c]`), ) } func TestToJson(t *testing.T) { Test(t, That(`put [&k=v &a=[1 2]] foo | to-json`). Prints(`{"a":["1","2"],"k":"v"} "foo" `), That(`put [$nil foo] | to-json`).Prints("[null,\"foo\"]\n"), thatOutputErrorIsBubbled("to-json [foo]"), ) } func TestPrintf(t *testing.T) { Test(t, That(`printf abcd`).Prints("abcd"), That(`printf "%s\n%s\n" abc xyz`).Prints("abc\nxyz\n"), That(`printf "%q" "abc xyz"`).Prints(`'abc xyz'`), That(`printf "%q" ['a b']`).Prints(`['a b']`), That(`printf "%v" abc`).Prints("abc"), That(`printf "%#v" "abc xyz"`).Prints(`'abc xyz'`), That(`printf '%5.3s' 3.1415`).Prints(" 3.1"), That(`printf '%5.3s' (float64 3.1415)`).Prints(" 3.1"), That(`printf '%t' $true`).Prints("true"), That(`printf '%t' $nil`).Prints("false"), That(`printf '%3d' (num 5)`).Prints(" 5"), That(`printf '%3d' 5`).Prints(" 5"), That(`printf '%08b' (num 5)`).Prints("00000101"), That(`printf '%08b' 5`).Prints("00000101"), That(`printf '%.1f' 3.1415`).Prints("3.1"), That(`printf '%.1f' (float64 3.1415)`).Prints("3.1"), // Does not interpret escape sequences That(`printf '%s\n%s\n' abc xyz`).Prints("abc\\nxyz\\n"), // Error cases // Float verb with argument that can't be converted to float That(`printf '%f' 1.3x`).Prints("%!f(cannot parse as number: 1.3x)"), // Integer verb with argument that can't be converted to integer That(`printf '%d' 3.5`).Prints("%!d(cannot parse as integer: 3.5)"), // Unsupported verb That(`printf '%A' foo`).Prints("%!A(unsupported formatting verb)"), thatOutputErrorIsBubbled("printf foo"), ) } func thatOutputErrorIsBubbled(code string) Case { return That(code + " >&-").Throws(OneOfErrors(os.ErrInvalid, eval.ErrNoValueOutput)) } elvish-0.17.0/pkg/eval/builtin_fn_misc.go000066400000000000000000000267101415471104000202770ustar00rootroot00000000000000package eval // Misc builtin functions. import ( "errors" "fmt" "math/rand" "net" "os" "sync" "time" "unicode/utf8" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/parse" ) var ( ErrNegativeSleepDuration = errors.New("sleep duration must be >= zero") ErrInvalidSleepDuration = errors.New("invalid sleep duration") ) // Builtins that have not been put into their own groups go here. func init() { addBuiltinFns(map[string]interface{}{ "nop": nop, "kind-of": kindOf, "constantly": constantly, "resolve": resolve, "eval": eval, "use-mod": useMod, "deprecate": deprecate, // Time "sleep": sleep, "time": timeCmd, "-ifaddrs": _ifaddrs, }) // For rand and randint. rand.Seed(time.Now().UTC().UnixNano()) } //elvdoc:fn nop // // ```elvish // nop &any-opt= $value... // ``` // // Accepts arbitrary arguments and options and does exactly nothing. // // Examples: // // ```elvish-transcript // ~> nop // ~> nop a b c // ~> nop &k=v // ``` // // Etymology: Various languages, in particular NOP in // [assembly languages](https://en.wikipedia.org/wiki/NOP). func nop(opts RawOptions, args ...interface{}) { // Do nothing } //elvdoc:fn kind-of // // ```elvish // kind-of $value... // ``` // // Output the kinds of `$value`s. Example: // // ```elvish-transcript // ~> kind-of lorem [] [&] // ▶ string // ▶ list // ▶ map // ``` // // The terminology and definition of "kind" is subject to change. func kindOf(fm *Frame, args ...interface{}) error { out := fm.ValueOutput() for _, a := range args { err := out.Put(vals.Kind(a)) if err != nil { return err } } return nil } //elvdoc:fn constantly // // ```elvish // constantly $value... // ``` // // Output a function that takes no arguments and outputs `$value`s when called. // Examples: // // ```elvish-transcript // ~> f=(constantly lorem ipsum) // ~> $f // ▶ lorem // ▶ ipsum // ``` // // The above example is actually equivalent to simply `f = { put lorem ipsum }`; // it is most useful when the argument is **not** a literal value, e.g. // // ```elvish-transcript // ~> f = (constantly (uname)) // ~> $f // ▶ Darwin // ~> $f // ▶ Darwin // ``` // // The above code only calls `uname` once, while if you do `f = { put (uname) }`, // every time you invoke `$f`, `uname` will be called. // // Etymology: [Clojure](https://clojuredocs.org/clojure.core/constantly). func constantly(args ...interface{}) Callable { // TODO(xiaq): Repr of this function is not right. return NewGoFn( "created by constantly", func(fm *Frame) error { out := fm.ValueOutput() for _, v := range args { err := out.Put(v) if err != nil { return err } } return nil }, ) } //elvdoc:fn resolve // // ```elvish // resolve $command // ``` // // Output what `$command` resolves to in symbolic form. Command resolution is // described in the [language reference](language.html#ordinary-command). // // Example: // // ```elvish-transcript // ~> resolve echo // ▶ // ~> fn f { } // ~> resolve f // ▶ // ~> resolve cat // ▶ // ``` func resolve(fm *Frame, head string) string { special, fnRef := resolveCmdHeadInternally(fm, head, nil) switch { case special != nil: return "special" case fnRef != nil: return "$" + head + FnSuffix default: return "(external " + parse.Quote(head) + ")" } } //elvdoc:fn eval // // ```elvish // eval $code &ns=$nil &on-end=$nil // ``` // // Evaluates `$code`, which should be a string. The evaluation happens in a // new, restricted namespace, whose initial set of variables can be specified by // the `&ns` option. After evaluation completes, the new namespace is passed to // the callback specified by `&on-end` if it is not nil. // // The namespace specified by `&ns` is never modified; it will not be affected // by the creation or deletion of variables by `$code`. However, the values of // the variables may be mutated by `$code`. // // If the `&ns` option is `$nil` (the default), a temporary namespace built by // amalgamating the local and upvalue scopes of the caller is used. // // If `$code` fails to parse or compile, the parse error or compilation error is // raised as an exception. // // Basic examples that do not modify the namespace or any variable: // // ```elvish-transcript // ~> eval 'put x' // ▶ x // ~> x = foo // ~> eval 'put $x' // ▶ foo // ~> ns = (ns [&x=bar]) // ~> eval &ns=$ns 'put $x' // ▶ bar // ``` // // Examples that modify existing variables: // // ```elvish-transcript // ~> y = foo // ~> eval 'y = bar' // ~> put $y // ▶ bar // ``` // // Examples that creates new variables and uses the callback to access it: // // ```elvish-transcript // ~> eval 'z = lorem' // ~> put $z // compilation error: variable $z not found // [ttz 2], line 1: put $z // ~> saved-ns = $nil // ~> eval &on-end={|ns| saved-ns = $ns } 'z = lorem' // ~> put $saved-ns[z] // ▶ lorem // ``` type evalOpts struct { Ns *Ns OnEnd Callable } func (*evalOpts) SetDefaultOptions() {} func eval(fm *Frame, opts evalOpts, code string) error { src := parse.Source{Name: fmt.Sprintf("[eval %d]", nextEvalCount()), Code: code} ns := opts.Ns if ns == nil { ns = CombineNs(fm.up, fm.local) } // The stacktrace already contains the line that calls "eval", so we pass // nil as the second argument. newNs, exc := fm.Eval(src, nil, ns) if opts.OnEnd != nil { newFm := fm.fork("on-end callback of eval") errCb := opts.OnEnd.Call(newFm, []interface{}{newNs}, NoOpts) if exc == nil { return errCb } } return exc } // Used to generate unique names for each source passed to eval. var ( evalCount int evalCountMutex sync.Mutex ) func nextEvalCount() int { evalCountMutex.Lock() defer evalCountMutex.Unlock() evalCount++ return evalCount } //elvdoc:fn use-mod // // ```elvish // use-mod $use-spec // ``` // // Imports a module, and outputs the namespace for the module. // // Most code should use the [use](language.html#importing-modules-with-use) // special command instead. // // Examples: // // ```elvish-transcript // ~> echo 'x = value' > a.elv // ~> put (use-mod ./a)[x] // ▶ value // ``` func useMod(fm *Frame, spec string) (*Ns, error) { return use(fm, spec, nil) } func readFileUTF8(fname string) (string, error) { bytes, err := os.ReadFile(fname) if err != nil { return "", err } if !utf8.Valid(bytes) { return "", fmt.Errorf("%s: source is not valid UTF-8", fname) } return string(bytes), nil } //elvdoc:fn deprecate // // ```elvish // deprecate $msg // ``` // // Shows the given deprecation message to stderr. If called from a function // or module, also shows the call site of the function or import site of the // module. Does nothing if the combination of the call site and the message has // been shown before. // // ```elvish-transcript // ~> deprecate msg // deprecation: msg // ~> fn f { deprecate msg } // ~> f // deprecation: msg // [tty 19], line 1: f // ~> exec // ~> deprecate msg // deprecation: msg // ~> fn f { deprecate msg } // ~> f // deprecation: msg // [tty 3], line 1: f // ~> f # a different call site; shows deprecate message // deprecation: msg // [tty 4], line 1: f // ~> fn g { f } // ~> g // deprecation: msg // [tty 5], line 1: fn g { f } // ~> g # same call site, no more deprecation message // ``` func deprecate(fm *Frame, msg string) { var ctx *diag.Context if fm.traceback.Next != nil { ctx = fm.traceback.Next.Head } fm.Deprecate(msg, ctx, 0) } // TimeAfter is used by the sleep command to obtain a channel that is delivered // a value after the specified time. // // It is a variable to allow for unit tests to efficiently test the behavior of // the `sleep` command, both by eliminating an actual sleep and verifying the // duration was properly parsed. var TimeAfter = func(fm *Frame, d time.Duration) <-chan time.Time { return time.After(d) } //elvdoc:fn sleep // // ```elvish // sleep $duration // ``` // // Pauses for at least the specified duration. The actual pause duration depends // on the system. // // This only affects the current Elvish context. It does not affect any other // contexts that might be executing in parallel as a consequence of a command // such as [`peach`](#peach). // // A duration can be a simple [number](language.html#number) (with optional // fractional value) without an explicit unit suffix, with an implicit unit of // seconds. // // A duration can also be a string written as a sequence of decimal numbers, // each with optional fraction, plus a unit suffix. For example, "300ms", // "1.5h" or "1h45m7s". Valid time units are "ns", "us" (or "µs"), "ms", "s", // "m", "h". // // Passing a negative duration causes an exception; this is different from the // typical BSD or GNU `sleep` command that silently exits with a success status // without pausing when given a negative duration. // // See the [Go documentation](https://golang.org/pkg/time/#ParseDuration) for // more information about how durations are parsed. // // Examples: // // ```elvish-transcript // ~> sleep 0.1 # sleeps 0.1 seconds // ~> sleep 100ms # sleeps 0.1 seconds // ~> sleep 1.5m # sleeps 1.5 minutes // ~> sleep 1m30s # sleeps 1.5 minutes // ~> sleep -1 // Exception: sleep duration must be >= zero // [tty 8], line 1: sleep -1 // ``` func sleep(fm *Frame, duration interface{}) error { var f float64 var d time.Duration if err := vals.ScanToGo(duration, &f); err == nil { d = time.Duration(f * float64(time.Second)) } else { // See if it is a duration string rather than a simple number. switch duration := duration.(type) { case string: d, err = time.ParseDuration(duration) if err != nil { return ErrInvalidSleepDuration } default: return ErrInvalidSleepDuration } } if d < 0 { return ErrNegativeSleepDuration } select { case <-fm.Interrupts(): return ErrInterrupted case <-TimeAfter(fm, d): return nil } } //elvdoc:fn time // // ```elvish // time &on-end=$nil $callable // ``` // // Runs the callable, and call `$on-end` with the duration it took, as a // number in seconds. If `$on-end` is `$nil` (the default), prints the // duration in human-readable form. // // If `$callable` throws an exception, the exception is propagated after the // on-end or default printing is done. // // If `$on-end` throws an exception, it is propagated, unless `$callable` has // already thrown an exception. // // Example: // // ```elvish-transcript // ~> time { sleep 1 } // 1.006060647s // ~> time { sleep 0.01 } // 1.288977ms // ~> t = '' // ~> time &on-end={|x| t = $x } { sleep 1 } // ~> put $t // ▶ (float64 1.000925004) // ~> time &on-end={|x| t = $x } { sleep 0.01 } // ~> put $t // ▶ (float64 0.011030208) // ``` type timeOpt struct{ OnEnd Callable } func (o *timeOpt) SetDefaultOptions() {} func timeCmd(fm *Frame, opts timeOpt, f Callable) error { t0 := time.Now() err := f.Call(fm, NoArgs, NoOpts) t1 := time.Now() dt := t1.Sub(t0) if opts.OnEnd != nil { newFm := fm.fork("on-end callback of time") errCb := opts.OnEnd.Call(newFm, []interface{}{dt.Seconds()}, NoOpts) if err == nil { err = errCb } } else { _, errWrite := fmt.Fprintln(fm.ByteOutput(), dt) if err == nil { err = errWrite } } return err } //elvdoc:fn -ifaddrs // // ```elvish // -ifaddrs // ``` // // Output all IP addresses of the current host. // // This should be part of a networking module instead of the builtin module. func _ifaddrs(fm *Frame) error { addrs, err := net.InterfaceAddrs() if err != nil { return err } out := fm.ValueOutput() for _, addr := range addrs { err := out.Put(addr.String()) if err != nil { return err } } return nil } elvish-0.17.0/pkg/eval/builtin_fn_misc_test.go000066400000000000000000000107771415471104000213440ustar00rootroot00000000000000package eval_test import ( "os" "testing" "time" . "src.elv.sh/pkg/eval" . "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/testutil" ) func TestKindOf(t *testing.T) { Test(t, That("kind-of a []").Puts("string", "list"), thatOutputErrorIsBubbled("kind-of a"), ) } func TestConstantly(t *testing.T) { Test(t, That(`f = (constantly foo); $f; $f`).Puts("foo", "foo"), thatOutputErrorIsBubbled("(constantly foo)"), ) } func TestEval(t *testing.T) { Test(t, That("eval 'put x'").Puts("x"), // Using variable from the local scope. That("x = foo; eval 'put $x'").Puts("foo"), // Setting a variable in the local scope. That("x = foo; eval 'x = bar'; put $x").Puts("bar"), // Using variable from the upvalue scope. That("x = foo; { nop $x; eval 'put $x' }").Puts("foo"), // Specifying a namespace. That("n = (ns [&x=foo]); eval 'put $x' &ns=$n").Puts("foo"), // Altering variables in the specified namespace. That("n = (ns [&x=foo]); eval 'x = bar' &ns=$n; put $n[x]").Puts("bar"), // Newly created variables do not appear in the local namespace. That("eval 'x = foo'; put $x").DoesNotCompile(), // Newly created variables do not alter the specified namespace, either. That("n = (ns [&]); eval &ns=$n 'x = foo'; put $n[x]"). Throws(vals.NoSuchKey("x"), "$n[x]"), // However, newly created variable can be accessed in the final // namespace using &on-end. That("eval &on-end={|n| put $n[x] } 'x = foo'").Puts("foo"), // Parse error. That("eval '['").Throws(AnyError), // Compilation error. That("eval 'put $x'").Throws(AnyError), // Exception. That("eval 'fail x'").Throws(FailError{"x"}), ) } func TestDeprecate(t *testing.T) { Test(t, That("deprecate msg").PrintsStderrWith("msg"), // Different call sites trigger multiple deprecation messages That("fn f { deprecate msg }", "f 2>"+os.DevNull, "f"). PrintsStderrWith("msg"), // The same call site only triggers the message once That("fn f { deprecate msg}", "fn g { f }", "g 2>"+os.DevNull, "g 2>&1"). DoesNothing(), ) } func TestTime(t *testing.T) { Test(t, // Since runtime duration is non-deterministic, we only have some sanity // checks here. That("time { echo foo } | a _ = (all)", "put $a").Puts("foo"), That("duration = ''", "time &on-end={|x| duration = $x } { echo foo } | out = (all)", "put $out", "kind-of $duration").Puts("foo", "number"), That("time { fail body } | nop (all)").Throws(FailError{"body"}), That("time &on-end={|_| fail on-end } { }").Throws( FailError{"on-end"}), That("time &on-end={|_| fail on-end } { fail body }").Throws( FailError{"body"}), thatOutputErrorIsBubbled("time { }"), ) } func TestUseMod(t *testing.T) { testutil.InTempDir(t) testutil.MustWriteFile("mod.elv", "x = value") Test(t, That("put (use-mod ./mod)[x]").Puts("value"), ) } func timeAfterMock(fm *Frame, d time.Duration) <-chan time.Time { fm.ValueOutput().Put(d) // report to the test framework the duration we received return time.After(0) } func TestSleep(t *testing.T) { TimeAfter = timeAfterMock Test(t, That(`sleep 0`).Puts(0*time.Second), That(`sleep 1`).Puts(1*time.Second), That(`sleep 1.3s`).Puts(1300*time.Millisecond), That(`sleep 0.1`).Puts(100*time.Millisecond), That(`sleep 0.1ms`).Puts(100*time.Microsecond), That(`sleep 3h5m7s`).Puts((3*3600+5*60+7)*time.Second), That(`sleep 1x`).Throws(ErrInvalidSleepDuration, "sleep 1x"), That(`sleep -7`).Throws(ErrNegativeSleepDuration, "sleep -7"), That(`sleep -3h`).Throws(ErrNegativeSleepDuration, "sleep -3h"), That(`sleep 1/2`).Puts(time.Second/2), // rational number string // Verify the correct behavior if a numeric type, rather than a string, is passed to the // command. That(`sleep (num 42)`).Puts(42*time.Second), That(`sleep (float64 0)`).Puts(0*time.Second), That(`sleep (float64 1.7)`).Puts(1700*time.Millisecond), That(`sleep (float64 -7)`).Throws(ErrNegativeSleepDuration, "sleep (float64 -7)"), // An invalid argument type should raise an exception. That(`sleep [1]`).Throws(ErrInvalidSleepDuration, "sleep [1]"), ) } func TestResolve(t *testing.T) { libdir := testutil.InTempDir(t) testutil.MustWriteFile("mod.elv", "fn func { }") TestWithSetup(t, func(ev *Evaler) { ev.LibDirs = []string{libdir} }, That("resolve for").Puts("special"), That("resolve put").Puts("$put~"), That("fn f { }; resolve f").Puts("$f~"), That("use mod; resolve mod:func").Puts("$mod:func~"), That("resolve cat").Puts("(external cat)"), That(`resolve external`).Puts("$external~"), ) } elvish-0.17.0/pkg/eval/builtin_fn_misc_unix_test.go000066400000000000000000000017061415471104000223770ustar00rootroot00000000000000//go:build !windows && !plan9 && !js // +build !windows,!plan9,!js package eval_test import ( "os" "testing" "time" . "src.elv.sh/pkg/eval" "src.elv.sh/pkg/testutil" . "src.elv.sh/pkg/eval/evaltest" ) func interruptedTimeAfterMock(fm *Frame, d time.Duration) <-chan time.Time { if d == time.Second { // Special-case intended to verity that a sleep can be interrupted. go func() { // Wait a little bit to ensure that the control flow in the "sleep" // function is in the select block when the interrupt is sent. time.Sleep(testutil.Scaled(time.Millisecond)) p, _ := os.FindProcess(os.Getpid()) p.Signal(os.Interrupt) }() return time.After(1 * time.Second) } panic("unreachable") } func TestInterruptedSleep(t *testing.T) { TimeAfter = interruptedTimeAfterMock Test(t, // Special-case that should result in the sleep being interrupted. See // timeAfterMock above. That(`sleep 1s`).Throws(ErrInterrupted, "sleep 1s"), ) } elvish-0.17.0/pkg/eval/builtin_fn_num.go000066400000000000000000000311341415471104000201370ustar00rootroot00000000000000package eval import ( "fmt" "math" "math/big" "math/rand" "strconv" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" ) // Numerical operations. //elvdoc:fn rand // // ```elvish // rand // ``` // // Output a pseudo-random number in the interval [0, 1). Example: // // ```elvish-transcript // ~> rand // ▶ 0.17843564133528436 // ``` func init() { addBuiltinFns(map[string]interface{}{ // Constructor "float64": toFloat64, "num": num, "exact-num": exactNum, // Comparison "<": lt, "<=": le, "==": eqNum, "!=": ne, ">": gt, ">=": ge, // Arithmetic "+": add, "-": sub, "*": mul, // Also handles cd / "/": slash, "%": rem, // Random "rand": rand.Float64, "randint": randint, }) } //elvdoc:fn num // // ```elvish // num $string-or-number // ``` // // Constructs a [typed number](./language.html#number). // // If the argument is a string, this command outputs the typed number the // argument represents, or raises an exception if the argument is not a valid // representation of a number. If the argument is already a typed number, this // command outputs it as is. // // This command is usually not needed for working with numbers; see the // discussion of [numeric commands](#numeric-commands). // // Examples: // // ```elvish-transcript // ~> num 10 // ▶ (num 10) // ~> num 0x10 // ▶ (num 16) // ~> num 1/12 // ▶ (num 1/12) // ~> num 3.14 // ▶ (num 3.14) // ~> num (num 10) // ▶ (num 10) // ``` func num(n vals.Num) vals.Num { // Conversion is actually handled in vals/conversion.go. return n } //elvdoc:fn exact-num // // ```elvish // exact-num $string-or-number // ``` // // Coerces the argument to an exact number. If the argument is infinity or NaN, // an exception is thrown. // // If the argument is a string, it is converted to a typed number first. If the // argument is already an exact number, it is returned as is. // // Examples: // // ```elvish-transcript // ~> exact-num (num 0.125) // ▶ (num 1/8) // ~> exact-num 0.125 // ▶ (num 1/8) // ~> exact-num (num 1) // ▶ (num 1) // ``` // // Beware that seemingly simple fractions that can't be represented precisely in // binary can result in the denominator being a very large power of 2: // // ```elvish-transcript // ~> exact-num 0.1 // ▶ (num 3602879701896397/36028797018963968) // ``` func exactNum(n vals.Num) (vals.Num, error) { if f, ok := n.(float64); ok { r := new(big.Rat).SetFloat64(f) if r == nil { return nil, errs.BadValue{What: "argument here", Valid: "finite float", Actual: vals.ToString(f)} } return r, nil } return n, nil } //elvdoc:fn float64 // // ```elvish // float64 $string-or-number // ``` // // Constructs a floating-point number. // // This command is deprecated; use [`num`](#num) instead. func toFloat64(f float64) float64 { return f } //elvdoc:fn < <= == != > >= {#num-cmp} // // ```elvish // < $number... # less // <= $number... # less or equal // == $number... # equal // != $number... # not equal // > $number... # greater // >= $number... # greater or equal // ``` // // Number comparisons. All of them accept an arbitrary number of arguments: // // 1. When given fewer than two arguments, all output `$true`. // // 2. When given two arguments, output whether the two arguments satisfy the named // relationship. // // 3. When given more than two arguments, output whether every adjacent pair of // numbers satisfy the named relationship. // // Examples: // // ```elvish-transcript // ~> == 3 3.0 // ▶ $true // ~> < 3 4 // ▶ $true // ~> < 3 4 10 // ▶ $true // ~> < 6 9 1 // ▶ $false // ``` // // As a consequence of rule 3, the `!=` command outputs `$true` as long as any // _adjacent_ pair of numbers are not equal, even if some numbers that are not // adjacent are equal: // // ```elvish-transcript // ~> != 5 5 4 // ▶ $false // ~> != 5 6 5 // ▶ $true // ``` func lt(nums ...vals.Num) bool { return chainCompare(nums, func(a, b int) bool { return a < b }, func(a, b *big.Int) bool { return a.Cmp(b) < 0 }, func(a, b *big.Rat) bool { return a.Cmp(b) < 0 }, func(a, b float64) bool { return a < b }) } func le(nums ...vals.Num) bool { return chainCompare(nums, func(a, b int) bool { return a <= b }, func(a, b *big.Int) bool { return a.Cmp(b) <= 0 }, func(a, b *big.Rat) bool { return a.Cmp(b) <= 0 }, func(a, b float64) bool { return a <= b }) } func eqNum(nums ...vals.Num) bool { return chainCompare(nums, func(a, b int) bool { return a == b }, func(a, b *big.Int) bool { return a.Cmp(b) == 0 }, func(a, b *big.Rat) bool { return a.Cmp(b) == 0 }, func(a, b float64) bool { return a == b }) } func ne(nums ...vals.Num) bool { return chainCompare(nums, func(a, b int) bool { return a != b }, func(a, b *big.Int) bool { return a.Cmp(b) != 0 }, func(a, b *big.Rat) bool { return a.Cmp(b) != 0 }, func(a, b float64) bool { return a != b }) } func gt(nums ...vals.Num) bool { return chainCompare(nums, func(a, b int) bool { return a > b }, func(a, b *big.Int) bool { return a.Cmp(b) > 0 }, func(a, b *big.Rat) bool { return a.Cmp(b) > 0 }, func(a, b float64) bool { return a > b }) } func ge(nums ...vals.Num) bool { return chainCompare(nums, func(a, b int) bool { return a >= b }, func(a, b *big.Int) bool { return a.Cmp(b) >= 0 }, func(a, b *big.Rat) bool { return a.Cmp(b) >= 0 }, func(a, b float64) bool { return a >= b }) } func chainCompare(nums []vals.Num, p1 func(a, b int) bool, p2 func(a, b *big.Int) bool, p3 func(a, b *big.Rat) bool, p4 func(a, b float64) bool) bool { for i := 0; i < len(nums)-1; i++ { var r bool a, b := vals.UnifyNums2(nums[i], nums[i+1], 0) switch a := a.(type) { case int: r = p1(a, b.(int)) case *big.Int: r = p2(a, b.(*big.Int)) case *big.Rat: r = p3(a, b.(*big.Rat)) case float64: r = p4(a, b.(float64)) } if !r { return false } } return true } //elvdoc:fn + {#add} // // ```elvish // + $num... // ``` // // Outputs the sum of all arguments, or 0 when there are no arguments. // // This command is [exactness-preserving](#exactness-preserving). // // Examples: // // ```elvish-transcript // ~> + 5 2 7 // ▶ (num 14) // ~> + 1/2 1/3 1/4 // ▶ (num 13/12) // ~> + 1/2 0.5 // ▶ (num 1.0) // ``` func add(rawNums ...vals.Num) vals.Num { nums := vals.UnifyNums(rawNums, vals.BigInt) switch nums := nums.(type) { case []*big.Int: acc := big.NewInt(0) for _, num := range nums { acc.Add(acc, num) } return vals.NormalizeBigInt(acc) case []*big.Rat: acc := big.NewRat(0, 1) for _, num := range nums { acc.Add(acc, num) } return vals.NormalizeBigRat(acc) case []float64: acc := float64(0) for _, num := range nums { acc += num } return acc default: panic("unreachable") } } //elvdoc:fn - {#sub} // // ```elvish // - $x-num $y-num... // ``` // // Outputs the result of subtracting from `$x-num` all the `$y-num`s, working // from left to right. When no `$y-num` is given, outputs the negation of // `$x-num` instead (in other words, `- $x-num` is equivalent to `- 0 $x-num`). // // This command is [exactness-preserving](#exactness-preserving). // // Examples: // // ```elvish-transcript // ~> - 5 // ▶ (num -5) // ~> - 5 2 // ▶ (num 3) // ~> - 5 2 7 // ▶ (num -4) // ~> - 1/2 1/3 // ▶ (num 1/6) // ~> - 1/2 0.3 // ▶ (num 0.2) // ~> - 10 // ▶ (num -10) // ``` func sub(rawNums ...vals.Num) (vals.Num, error) { if len(rawNums) == 0 { return nil, errs.ArityMismatch{What: "arguments", ValidLow: 1, ValidHigh: -1, Actual: 0} } nums := vals.UnifyNums(rawNums, vals.BigInt) switch nums := nums.(type) { case []*big.Int: acc := &big.Int{} if len(nums) == 1 { acc.Neg(nums[0]) return acc, nil } acc.Set(nums[0]) for _, num := range nums[1:] { acc.Sub(acc, num) } return acc, nil case []*big.Rat: acc := &big.Rat{} if len(nums) == 1 { acc.Neg(nums[0]) return acc, nil } acc.Set(nums[0]) for _, num := range nums[1:] { acc.Sub(acc, num) } return acc, nil case []float64: if len(nums) == 1 { return -nums[0], nil } acc := nums[0] for _, num := range nums[1:] { acc -= num } return acc, nil default: panic("unreachable") } } //elvdoc:fn * {#mul} // // ```elvish // * $num... // ``` // // Outputs the product of all arguments, or 1 when there are no arguments. // // This command is [exactness-preserving](#exactness-preserving). Additionally, // when any argument is exact 0 and no other argument is a floating-point // infinity, the result is exact 0. // // Examples: // // ```elvish-transcript // ~> * 2 5 7 // ▶ (num 70) // ~> * 1/2 0.5 // ▶ (num 0.25) // ~> * 0 0.5 // ▶ (num 0) // ``` func mul(rawNums ...vals.Num) vals.Num { hasExact0 := false hasInf := false for _, num := range rawNums { if num == 0 { hasExact0 = true } if f, ok := num.(float64); ok && math.IsInf(f, 0) { hasInf = true break } } if hasExact0 && !hasInf { return 0 } nums := vals.UnifyNums(rawNums, vals.BigInt) switch nums := nums.(type) { case []*big.Int: acc := big.NewInt(1) for _, num := range nums { acc.Mul(acc, num) } return vals.NormalizeBigInt(acc) case []*big.Rat: acc := big.NewRat(1, 1) for _, num := range nums { acc.Mul(acc, num) } return vals.NormalizeBigRat(acc) case []float64: acc := float64(1) for _, num := range nums { acc *= num } return acc default: panic("unreachable") } } //elvdoc:fn / {#div} // // ```elvish // / $x-num $y-num... // ``` // // Outputs the result of dividing `$x-num` with all the `$y-num`s, working from // left to right. When no `$y-num` is given, outputs the reciprocal of `$x-num` // instead (in other words, `/ $y-num` is equivalent to `/ 1 $y-num`). // // Dividing by exact 0 raises an exception. Dividing by inexact 0 results with // either infinity or NaN according to floating-point semantics. // // This command is [exactness-preserving](#exactness-preserving). Additionally, // when `$x-num` is exact 0 and no `$y-num` is exact 0, the result is exact 0. // // Examples: // // ```elvish-transcript // ~> / 2 // ▶ (num 1/2) // ~> / 2.0 // ▶ (num 0.5) // ~> / 10 5 // ▶ (num 2) // ~> / 2 5 // ▶ (num 2/5) // ~> / 2 5 7 // ▶ (num 2/35) // ~> / 0 1.0 // ▶ (num 0) // ~> / 2 0 // Exception: bad value: divisor must be number other than exact 0, but is exact 0 // [tty 6], line 1: / 2 0 // ~> / 2 0.0 // ▶ (num +Inf) // ``` // // When given no argument, this command is equivalent to `cd /`, due to the // implicit cd feature. (The implicit cd feature will probably change to avoid // this oddity). func slash(fm *Frame, args ...vals.Num) error { if len(args) == 0 { // cd / return fm.Evaler.Chdir("/") } // Division result, err := div(args...) if err != nil { return err } return fm.ValueOutput().Put(vals.FromGo(result)) } // ErrDivideByZero is thrown when attempting to divide by zero. var ErrDivideByZero = errs.BadValue{ What: "divisor", Valid: "number other than exact 0", Actual: "exact 0"} func div(rawNums ...vals.Num) (vals.Num, error) { for _, num := range rawNums[1:] { if num == 0 { return nil, ErrDivideByZero } } if rawNums[0] == 0 { return 0, nil } nums := vals.UnifyNums(rawNums, vals.BigRat) switch nums := nums.(type) { case []*big.Rat: acc := &big.Rat{} acc.Set(nums[0]) if len(nums) == 1 { acc.Inv(acc) return acc, nil } for _, num := range nums[1:] { acc.Quo(acc, num) } return acc, nil case []float64: acc := nums[0] if len(nums) == 1 { return 1 / acc, nil } for _, num := range nums[1:] { acc /= num } return acc, nil default: panic("unreachable") } } //elvdoc:fn % {#rem} // // ```elvish // % $x $y // ``` // // Output the remainder after dividing `$x` by `$y`. The result has the same // sign as `$x`. Both must be integers that can represented in a machine word // (this limit may be lifted in future). // // Examples: // // ```elvish-transcript // ~> % 10 3 // ▶ 1 // ~> % -10 3 // ▶ -1 // ~> % 10 -3 // ▶ 1 // ``` func rem(a, b int) (int, error) { // TODO: Support other number types if b == 0 { return 0, ErrDivideByZero } return a % b, nil } //elvdoc:fn randint // // ```elvish // randint $low? $high // ``` // // Output a pseudo-random integer N such that `$low <= N < $high`. If not given, // `$low` defaults to 0. Examples: // // ```elvish-transcript // ~> # Emulate dice // randint 1 7 // ▶ 6 // ``` func randint(args ...int) (int, error) { var low, high int switch len(args) { case 1: low, high = 0, args[0] case 2: low, high = args[0], args[1] default: return -1, errs.ArityMismatch{What: "arguments", ValidLow: 1, ValidHigh: 2, Actual: len(args)} } if high <= low { return 0, errs.BadValue{What: "high value", Valid: fmt.Sprint("larger than ", low), Actual: strconv.Itoa(high)} } return low + rand.Intn(high-low), nil } elvish-0.17.0/pkg/eval/builtin_fn_num_test.go000066400000000000000000000147271415471104000212070ustar00rootroot00000000000000package eval_test import ( "math" "math/big" "strings" "testing" . "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" . "src.elv.sh/pkg/eval/evaltest" ) const ( zeros = "0000000000000000000" // Values that exceed the range of int64, used for testing bigint. z = "1" + zeros + "0" z1 = "1" + zeros + "1" // z+1 z2 = "1" + zeros + "2" // z+2 z3 = "1" + zeros + "3" // z+3 zz = "2" + zeros + "0" // 2z zz1 = "2" + zeros + "1" // 2z+1 zz2 = "2" + zeros + "2" // 2z+2 zz3 = "2" + zeros + "3" // 2z+3 ) func TestNum(t *testing.T) { Test(t, That("num 1").Puts(1), That("num "+z).Puts(bigInt(z)), That("num 1/2").Puts(big.NewRat(1, 2)), That("num 0.1").Puts(0.1), That("num (num 1)").Puts(1), ) } func TestExactNum(t *testing.T) { Test(t, That("exact-num 1").Puts(1), That("exact-num 0.125").Puts(big.NewRat(1, 8)), That("exact-num inf").Throws(errs.BadValue{ What: "argument here", Valid: "finite float", Actual: "+Inf"}), ) } func TestFloat64(t *testing.T) { Test(t, That("float64 1").Puts(1.0), That("float64 (float64 1)").Puts(1.0), ) } func TestNumCmp(t *testing.T) { Test(t, // int That("< 1 2 3").Puts(true), That("< 1 3 2").Puts(false), // bigint That("< "+args(z1, z2, z3)).Puts(true), That("< "+args(z1, z3, z2)).Puts(false), // bigint and int That("< "+args("1", z1)).Puts(true), // bigrat That("< 1/4 1/3 1/2").Puts(true), That("< 1/4 1/2 1/3").Puts(false), // bigrat, bigint and int That("< "+args("1/2", "1", z1)).Puts(true), That("< "+args("1/2", z1, "1")).Puts(false), // float64 That("< 1.0 2.0 3.0").Puts(true), That("< 1.0 3.0 2.0").Puts(false), // float64, bigrat and int That("< 1.0 3/2 2").Puts(true), That("< 1.0 2 3/2").Puts(false), // Mixing of types not tested for commands below; they share the same // code path as <. // int That("<= 1 1 2").Puts(true), That("<= 1 2 1").Puts(false), // bigint That("<= "+args(z1, z1, z2)).Puts(true), That("<= "+args(z1, z2, z1)).Puts(false), // bigrat That("<= 1/3 1/3 1/2").Puts(true), That("<= 1/3 1/2 1/1").Puts(true), // float64 That("<= 1.0 1.0 2.0").Puts(true), That("<= 1.0 2.0 1.0").Puts(false), // int That("== 1 1 1").Puts(true), That("== 1 2 1").Puts(false), // bigint That("== "+args(z1, z1, z1)).Puts(true), That("== "+args(z1, z2, z1)).Puts(false), // bigrat That("== 1/2 1/2 1/2").Puts(true), That("== 1/2 1/3 1/2").Puts(false), // float64 That("== 1.0 1.0 1.0").Puts(true), That("== 1.0 2.0 1.0").Puts(false), // int That("!= 1 2 1").Puts(true), That("!= 1 1 2").Puts(false), // bigint That("!= "+args(z1, z2, z1)).Puts(true), That("!= "+args(z1, z1, z2)).Puts(false), // bigrat That("!= 1/2 1/3 1/2").Puts(true), That("!= 1/2 1/2 1/3").Puts(false), // float64 That("!= 1.0 2.0 1.0").Puts(true), That("!= 1.0 1.0 2.0").Puts(false), // int That("> 3 2 1").Puts(true), That("> 3 1 2").Puts(false), // bigint That("> "+args(z3, z2, z1)).Puts(true), That("> "+args(z3, z1, z2)).Puts(false), // bigrat That("> 1/2 1/3 1/4").Puts(true), That("> 1/2 1/4 1/3").Puts(false), // float64 That("> 3.0 2.0 1.0").Puts(true), That("> 3.0 1.0 2.0").Puts(false), // int That(">= 3 3 2").Puts(true), That(">= 3 2 3").Puts(false), // bigint That(">= "+args(z3, z3, z2)).Puts(true), That(">= "+args(z3, z2, z3)).Puts(false), // bigrat That(">= 1/2 1/2 1/3").Puts(true), That(">= 1/2 1/3 1/2").Puts(false), // float64 That(">= 3.0 3.0 2.0").Puts(true), That(">= 3.0 2.0 3.0").Puts(false), ) } func TestArithmeticCommands(t *testing.T) { Test(t, // No argument That("+").Puts(0), // int That("+ 233100 233").Puts(233333), // bigint That("+ "+args(z, z1)).Puts(bigInt(zz1)), // bigint and int That("+ 1 2 "+z).Puts(bigInt(z3)), // bigrat That("+ 1/2 1/3 1/4").Puts(big.NewRat(13, 12)), // bigrat, bigint and int That("+ 1/2 1/2 1 "+z).Puts(bigInt(z2)), // float64 That("+ 0.5 0.25 1.0").Puts(1.75), // float64 and other types That("+ 0.5 1/4 1").Puts(1.75), // Mixing of types not tested for commands below; they share the same // code path as +. That("-").Throws(ErrorWithType(errs.ArityMismatch{})), // One argument - negation That("- 233").Puts(-233), That("- "+z).Puts(bigInt("-"+z)), That("- 1/2").Puts(big.NewRat(-1, 2)), That("- 1.0").Puts(-1.0), // int That("- 20 10 2").Puts(8), // bigint That("- "+args(zz3, z1)).Puts(bigInt(z2)), // bigrat That("- 1/2 1/3").Puts(big.NewRat(1, 6)), // float64 That("- 2.0 1.0 0.5").Puts(0.5), // No argument That("*").Puts(1), // int That("* 2 7 4").Puts(56), // bigint That("* 2 "+z1).Puts(bigInt(zz2)), // bigrat That("* 1/2 1/3").Puts(big.NewRat(1, 6)), // float64 That("* 2.0 0.5 1.75").Puts(1.75), // 0 * non-infinity That("* 0 1/2 1.0").Puts(0), // 0 * infinity That("* 0 +Inf").Puts(math.NaN()), // One argument - inversion That("/ 2").Puts(big.NewRat(1, 2)), That("/ "+z).Puts(bigRat("1/"+z)), That("/ 2.0").Puts(0.5), // int That("/ 233333 353").Puts(661), That("/ 3 4 2").Puts(big.NewRat(3, 8)), // bigint That("/ "+args(zz, z)).Puts(2), That("/ "+args(zz, "2")).Puts(bigInt(z)), That("/ "+args(z1, z)).Puts(bigRat(z1+"/"+z)), // float64 That("/ 1.0 2.0 4.0").Puts(0.125), // 0 / non-zero That("/ 0 1/2 0.1").Puts(0), // anything / 0 That("/ 0 0").Throws(ErrDivideByZero, "/ 0 0"), That("/ 1 0").Throws(ErrDivideByZero, "/ 1 0"), That("/ 1.0 0").Throws(ErrDivideByZero, "/ 1.0 0"), That("% 23 7").Puts(2), That("% 1 0").Throws(ErrDivideByZero, "% 1 0"), ) } func TestRandint(t *testing.T) { Test(t, That("randint 1 2").Puts(1), That("randint 1").Puts(0), That("i = (randint 10 100); and (<= 10 $i) (< $i 100)").Puts(true), That("i = (randint 10); and (<= 0 $i) (< $i 10)").Puts(true), That("randint 2 1").Throws( errs.BadValue{What: "high value", Valid: "larger than 2", Actual: "1"}, "randint 2 1"), That("randint").Throws(ErrorWithType(errs.ArityMismatch{}), "randint"), That("randint 1 2 3").Throws(ErrorWithType(errs.ArityMismatch{}), "randint 1 2 3"), ) } func bigInt(s string) *big.Int { z, ok := new(big.Int).SetString(s, 0) if !ok { panic("cannot parse as big int: " + s) } return z } func bigRat(s string) *big.Rat { z, ok := new(big.Rat).SetString(s) if !ok { panic("cannot parse as big rat: " + s) } return z } func args(vs ...interface{}) string { s := make([]string, len(vs)) for i, v := range vs { s[i] = vals.ToString(v) } return strings.Join(s, " ") } elvish-0.17.0/pkg/eval/builtin_fn_pred.go000066400000000000000000000063621415471104000202770ustar00rootroot00000000000000package eval import "src.elv.sh/pkg/eval/vals" // Basic predicate commands. //elvdoc:fn bool // // ```elvish // bool $value // ``` // // Convert a value to boolean. In Elvish, only `$false` and errors are booleanly // false. Everything else, including 0, empty strings and empty lists, is booleanly // true: // // ```elvish-transcript // ~> bool $true // ▶ $true // ~> bool $false // ▶ $false // ~> bool $ok // ▶ $true // ~> bool ?(fail haha) // ▶ $false // ~> bool '' // ▶ $true // ~> bool [] // ▶ $true // ~> bool abc // ▶ $true // ``` // // @cf not func init() { addBuiltinFns(map[string]interface{}{ "bool": vals.Bool, "not": not, "is": is, "eq": eq, "not-eq": notEq, }) } //elvdoc:fn not // // ```elvish // not $value // ``` // // Boolean negation. Examples: // // ```elvish-transcript // ~> not $true // ▶ $false // ~> not $false // ▶ $true // ~> not $ok // ▶ $false // ~> not ?(fail error) // ▶ $true // ``` // // **Note**: The related logical commands `and` and `or` are implemented as // [special commands](language.html#special-commands) instead, since they do not // always evaluate all their arguments. The `not` command always evaluates its // only argument, and is thus a normal command. // // @cf bool func not(v interface{}) bool { return !vals.Bool(v) } //elvdoc:fn is // // ```elvish // is $values... // ``` // // Determine whether all `$value`s have the same identity. Writes `$true` when // given no or one argument. // // The definition of identity is subject to change. Do not rely on its behavior. // // ```elvish-transcript // ~> is a a // ▶ $true // ~> is a b // ▶ $false // ~> is [] [] // ▶ $true // ~> is [a] [a] // ▶ $false // ``` // // @cf eq // // Etymology: [Python](https://docs.python.org/3/reference/expressions.html#is). func is(args ...interface{}) bool { for i := 0; i+1 < len(args); i++ { if args[i] != args[i+1] { return false } } return true } //elvdoc:fn eq // // ```elvish // eq $values... // ``` // // Determines whether all `$value`s are equal. Writes `$true` when // given no or one argument. // // Two values are equal when they have the same type and value. // // For complex data structures like lists and maps, comparison is done // recursively. A pseudo-map is equal to another pseudo-map with the same // internal type (which is not exposed to Elvish code now) and value. // // ```elvish-transcript // ~> eq a a // ▶ $true // ~> eq [a] [a] // ▶ $true // ~> eq [&k=v] [&k=v] // ▶ $true // ~> eq a [b] // ▶ $false // ``` // // @cf is not-eq // // Etymology: [Perl](https://perldoc.perl.org/perlop.html#Equality-Operators). func eq(args ...interface{}) bool { for i := 0; i+1 < len(args); i++ { if !vals.Equal(args[i], args[i+1]) { return false } } return true } //elvdoc:fn not-eq // // ```elvish // not-eq $values... // ``` // // Determines whether every adjacent pair of `$value`s are not equal. Note that // this does not imply that `$value`s are all distinct. Examples: // // ```elvish-transcript // ~> not-eq 1 2 3 // ▶ $true // ~> not-eq 1 2 1 // ▶ $true // ~> not-eq 1 1 2 // ▶ $false // ``` // // @cf eq func notEq(args ...interface{}) bool { for i := 0; i+1 < len(args); i++ { if vals.Equal(args[i], args[i+1]) { return false } } return true } elvish-0.17.0/pkg/eval/builtin_fn_pred_test.go000066400000000000000000000023311415471104000213260ustar00rootroot00000000000000package eval_test import ( "testing" . "src.elv.sh/pkg/eval/evaltest" ) func TestBool(t *testing.T) { Test(t, That(`bool $true`).Puts(true), That(`bool a`).Puts(true), That(`bool [a]`).Puts(true), // "Empty" values are also true in Elvish That(`bool []`).Puts(true), That(`bool [&]`).Puts(true), That(`bool 0`).Puts(true), That(`bool ""`).Puts(true), // Only errors and $false are false That(`bool ?(fail x)`).Puts(false), That(`bool $false`).Puts(false), ) } func TestNot(t *testing.T) { Test(t, That(`not $false`).Puts(true), That(`not ?(fail x)`).Puts(true), That(`not $true`).Puts(false), That(`not 0`).Puts(false), ) } func TestIs(t *testing.T) { Test(t, That(`is 1 1`).Puts(true), That(`is a b`).Puts(false), That(`is [] []`).Puts(true), That(`is [1] [1]`).Puts(false), ) } func TestEq(t *testing.T) { Test(t, That(`eq 1 1`).Puts(true), That(`eq a b`).Puts(false), That(`eq [] []`).Puts(true), That(`eq [1] [1]`).Puts(true), That(`eq 1 1 2`).Puts(false), ) } func TestNotEq(t *testing.T) { Test(t, That(`not-eq a b`).Puts(true), That(`not-eq a a`).Puts(false), // not-eq is true as long as each adjacent pair is not equal. That(`not-eq 1 2 1`).Puts(true), ) } elvish-0.17.0/pkg/eval/builtin_fn_str.go000066400000000000000000000104771415471104000201570ustar00rootroot00000000000000package eval import ( "errors" "regexp" "strconv" "strings" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/wcwidth" ) // String operations. // ErrInputOfEawkMustBeString is thrown when eawk gets a non-string input. var ErrInputOfEawkMustBeString = errors.New("input of eawk must be string") //elvdoc:fn <s <=s ==s !=s >s >=s {#str-cmp} // // ```elvish // s $string... # greater // >=s $string... # greater or equal // ``` // // String comparisons. They behave similarly to their number counterparts when // given multiple arguments. Examples: // // ```elvish-transcript // ~> >s lorem ipsum // ▶ $true // ~> ==s 1 1.0 // ▶ $false // ~> >s 8 12 // ▶ $true // ``` //elvdoc:fn wcswidth // // ```elvish // wcswidth $string // ``` // // Output the width of `$string` when displayed on the terminal. Examples: // // ```elvish-transcript // ~> wcswidth a // ▶ 1 // ~> wcswidth lorem // ▶ 5 // ~> wcswidth 你好,世界 // ▶ 10 // ``` // TODO(xiaq): Document -override-wcswidth. func init() { addBuiltinFns(map[string]interface{}{ "s": func(a, b string) bool { return a > b }, ">=s": func(a, b string) bool { return a >= b }, "to-string": toString, "base": base, "wcswidth": wcwidth.Of, "-override-wcwidth": wcwidth.Override, "eawk": eawk, }) } //elvdoc:fn to-string // // ```elvish // to-string $value... // ``` // // Convert arguments to string values. // // ```elvish-transcript // ~> to-string foo [a] [&k=v] // ▶ foo // ▶ '[a]' // ▶ '[&k=v]' // ``` func toString(fm *Frame, args ...interface{}) error { out := fm.ValueOutput() for _, a := range args { err := out.Put(vals.ToString(a)) if err != nil { return err } } return nil } //elvdoc:fn base // // ```elvish // base $base $number... // ``` // // Outputs a string for each `$number` written in `$base`. The `$base` must be // between 2 and 36, inclusive. Examples: // // ```elvish-transcript // ~> base 2 1 3 4 16 255 // ▶ 1 // ▶ 11 // ▶ 100 // ▶ 10000 // ▶ 11111111 // ~> base 16 1 3 4 16 255 // ▶ 1 // ▶ 3 // ▶ 4 // ▶ 10 // ▶ ff // ``` // ErrBadBase is thrown by the "base" builtin if the base is smaller than 2 or // greater than 36. var ErrBadBase = errors.New("bad base") func base(fm *Frame, b int, nums ...int) error { if b < 2 || b > 36 { return ErrBadBase } out := fm.ValueOutput() for _, num := range nums { err := out.Put(strconv.FormatInt(int64(num), b)) if err != nil { return err } } return nil } var eawkWordSep = regexp.MustCompile("[ \t]+") //elvdoc:fn eawk // // ```elvish // eawk $f $input-list? // ``` // // For each input, call `$f` with the input followed by all its fields. A // [`break`](./builtin.html#break) command will cause `eawk` to stop processing inputs. A // [`continue`](./builtin.html#continue) command will exit $f, but is ignored by `eawk`. // // It should behave the same as the following functions: // // ```elvish // fn eawk {|f @rest| // each {|line| // @fields = (re:split '[ \t]+' // (re:replace '^[ \t]+|[ \t]+$' '' $line)) // $f $line $@fields // } $@rest // } // ``` // // This command allows you to write code very similar to `awk` scripts using // anonymous functions. Example: // // ```elvish-transcript // ~> echo ' lorem ipsum // 1 2' | awk '{ print $1 }' // lorem // 1 // ~> echo ' lorem ipsum // 1 2' | eawk {|line a b| put $a } // ▶ lorem // ▶ 1 // ``` func eawk(fm *Frame, f Callable, inputs Inputs) error { broken := false var err error inputs(func(v interface{}) { if broken { return } line, ok := v.(string) if !ok { broken = true err = ErrInputOfEawkMustBeString return } args := []interface{}{line} for _, field := range eawkWordSep.Split(strings.Trim(line, " \t"), -1) { args = append(args, field) } newFm := fm.fork("fn of eawk") // TODO: Close port 0 of newFm. ex := f.Call(newFm, args, NoOpts) newFm.Close() if ex != nil { switch Reason(ex) { case nil, Continue: // nop case Break: broken = true default: broken = true err = ex } } }) return err } elvish-0.17.0/pkg/eval/builtin_fn_str_test.go000066400000000000000000000037741415471104000212200ustar00rootroot00000000000000package eval_test import ( "testing" . "src.elv.sh/pkg/eval" . "src.elv.sh/pkg/eval/evaltest" ) func TestStringComparisonCommands(t *testing.T) { Test(t, That(`s a b`).Puts(false), That(`>s 2 10`).Puts(true), That(`>=s a a`).Puts(true), That(`>=s a b`).Puts(false), That(`>=s b a`).Puts(true), ) } func TestToString(t *testing.T) { Test(t, That(`to-string str (num 1) $true`).Puts("str", "1", "$true"), thatOutputErrorIsBubbled("to-string str"), ) } func TestBase(t *testing.T) { Test(t, That(`base 2 1 3 4 16 255`).Puts("1", "11", "100", "10000", "11111111"), That(`base 16 42 233`).Puts("2a", "e9"), That(`base 1 1`).Throws(AnyError), // no base-1 That(`base 37 10`).Throws(AnyError), // no letter for base-37 thatOutputErrorIsBubbled("base 2 1"), ) } func TestWcswidth(t *testing.T) { Test(t, That(`wcswidth 你好`).Puts(4), That(`-override-wcwidth x 10; wcswidth 1x2x; -override-wcwidth x 1`). Puts(22), ) } func TestEawk(t *testing.T) { Test(t, That(`echo " ax by cz \n11\t22 33" | eawk {|@a| put $a[-1] }`). Puts("cz", "33"), // Bad input type That(`num 42 | eawk {|@a| fail "this should not run" }`). Throws(ErrInputOfEawkMustBeString), // Propagation of exception That(` to-lines [1 2 3 4] | eawk {|@a| if (==s 3 $a[1]) { fail "stop eawk" } put $a[1] } `).Puts("1", "2").Throws(FailError{"stop eawk"}), // break That(` to-lines [" a" "b\tc " "d" "e"] | eawk {|@a| if (==s d $a[1]) { break } else { put $a[-1] } } `).Puts("a", "c"), // continue That(` to-lines [" a" "b\tc " "d" "e"] | eawk {|@a| if (==s d $a[1]) { continue } else { put $a[-1] } } `).Puts("a", "c", "e"), ) } elvish-0.17.0/pkg/eval/builtin_fn_styled.go000066400000000000000000000130561415471104000206470ustar00rootroot00000000000000package eval import ( "errors" "fmt" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/ui" ) var errStyledSegmentArgType = errors.New("argument to styled-segment must be a string or a styled segment") func init() { addBuiltinFns(map[string]interface{}{ "styled-segment": styledSegment, "styled": styled, }) } //elvdoc:fn styled-segment // // ```elvish // styled-segment $object &fg-color=default &bg-color=default &bold=$false &dim=$false &italic=$false &underlined=$false &blink=$false &inverse=$false // ``` // // Constructs a styled segment and is a helper function for styled transformers. // `$object` can be a plain string, a styled segment or a concatenation thereof. // Probably the only reason to use it is to build custom style transformers: // // ```elvish // fn my-awesome-style-transformer {|seg| styled-segment $seg &bold=(not $seg[dim]) &dim=(not $seg[italic]) &italic=$seg[bold] } // styled abc $my-awesome-style-transformer~ // ``` // // As just seen the properties of styled segments can be inspected by indexing into // it. Valid indices are the same as the options to `styled-segment` plus `text`. // // ```elvish // s = (styled-segment abc &bold) // put $s[text] // put $s[fg-color] // put $s[bold] // ``` // Turns a string or ui.Segment into a new ui.Segment with the attributes // from the supplied options applied to it. If the input is already a Segment its // attributes are copied and modified. func styledSegment(options RawOptions, input interface{}) (*ui.Segment, error) { var text string var style ui.Style switch input := input.(type) { case string: text = input case *ui.Segment: text = input.Text style = input.Style default: return nil, errStyledSegmentArgType } if err := style.MergeFromOptions(options); err != nil { return nil, err } return &ui.Segment{ Text: text, Style: style, }, nil } //elvdoc:fn styled // // ```elvish // styled $object $style-transformer... // ``` // // Construct a styled text by applying the supplied transformers to the supplied // object. `$object` can be either a string, a styled segment (see below), a styled // text or an arbitrary concatenation of them. A `$style-transformer` is either: // // - The name of a builtin style transformer, which may be one of the following: // // - One of the attribute names `bold`, `dim`, `italic`, `underlined`, // `blink` or `inverse` for setting the corresponding attribute. // // - An attribute name prefixed by `no-` for unsetting the attribute. // // - An attribute name prefixed by `toggle-` for toggling the attribute // between set and unset. // // - A color name for setting the text color, which may be one of the // following: // // - One of the 8 basic ANSI colors: `black`, `red`, `green`, `yellow`, // `blue`, `magenta`, `cyan` and `white`. // // - The bright variant of the 8 basic ANSI colors, with a `bright-` // prefix. // // - Any color from the xterm 256-color palette, as `colorX` (such as // `color12`). // // - A 24-bit RGB color written as `#RRGGBB` such as `'#778899'`. // // **Note**: You need to quote such values since an unquoted `#` char // introduces a comment. So use `'bg-#778899'` not `bg-#778899`. If // you omit the quotes the text after the `#` char is ignored which // will result in an error or unexpected behavior. // // - A color name prefixed by `bg-` to set the background color. // // - A color name prefixed by `fg-` to set the foreground color. This has // the same effect as specifying the color name without the `fg-` prefix. // // - A lambda that receives a styled segment as the only argument and returns a // single styled segment. // // - A function with the same properties as the lambda (provided via the // `$transformer~` syntax). // // When a styled text is converted to a string the corresponding // [ANSI SGR code](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_.28Select_Graphic_Rendition.29_parameters) // is built to render the style. // // A styled text is nothing more than a wrapper around a list of styled segments. // They can be accessed by indexing into it. // // ```elvish // s = (styled abc red)(styled def green) // put $s[0] $s[1] // ``` func styled(fm *Frame, input interface{}, stylings ...interface{}) (ui.Text, error) { var text ui.Text switch input := input.(type) { case string: text = ui.Text{&ui.Segment{ Text: input, Style: ui.Style{}, }} case *ui.Segment: text = ui.Text{input.Clone()} case ui.Text: text = input.Clone() default: return nil, fmt.Errorf("expected string, styled segment or styled text; got %s", vals.Kind(input)) } for _, styling := range stylings { switch styling := styling.(type) { case string: parsedStyling := ui.ParseStyling(styling) if parsedStyling == nil { return nil, fmt.Errorf("%s is not a valid style transformer", parse.Quote(styling)) } text = ui.StyleText(text, parsedStyling) case Callable: for i, seg := range text { vs, err := fm.CaptureOutput(func(fm *Frame) error { return styling.Call(fm, []interface{}{seg}, NoOpts) }) if err != nil { return nil, err } if n := len(vs); n != 1 { return nil, fmt.Errorf("styling function must return a single segment; got %d values", n) } else if styledSegment, ok := vs[0].(*ui.Segment); !ok { return nil, fmt.Errorf("styling function must return a segment; got %s", vals.Kind(vs[0])) } else { text[i] = styledSegment } } default: return nil, fmt.Errorf("need string or callable; got %s", vals.Kind(styling)) } } return text, nil } elvish-0.17.0/pkg/eval/builtin_fn_styled_test.go000066400000000000000000000131671415471104000217110ustar00rootroot00000000000000package eval_test import ( "testing" "src.elv.sh/pkg/eval" . "src.elv.sh/pkg/eval/evaltest" ) func TestStyledSegment(t *testing.T) { Test(t, That("print (styled (styled-segment abc &fg-color=cyan) bold)"). Prints("\033[1;36mabc\033[m"), That("print (styled (styled-segment (styled-segment abc &fg-color=magenta) &dim=$true) cyan)"). Prints("\033[2;36mabc\033[m"), That("print (styled (styled-segment abc &inverse=$true) inverse)"). Prints("\033[7mabc\033[m"), That("print (styled (styled-segment abc) toggle-inverse)"). Prints("\033[7mabc\033[m"), That("print (styled (styled-segment abc &inverse=$true) no-inverse)"). Prints("abc"), That("print (styled (styled-segment abc &inverse=$true) toggle-inverse)"). Prints("abc"), That("styled-segment []").Throws(ErrorWithMessage( "argument to styled-segment must be a string or a styled segment")), That("styled-segment text &foo=bar"). Throws(ErrorWithMessage("unrecognized option 'foo'")), ) } func TestStyled(t *testing.T) { Test(t, // Transform string That("print (styled abc bold)").Prints("\033[1mabc\033[m"), That("print (styled abc red cyan)").Prints("\033[36mabc\033[m"), That("print (styled abc bg-green)").Prints("\033[42mabc\033[m"), That("print (styled abc no-dim)").Prints("abc"), // Transform already styled text That("print (styled (styled abc red) blue)"). Prints("\033[34mabc\033[m"), That("print (styled (styled abc italic) red)"). Prints("\033[3;31mabc\033[m"), That("print (styled (styled abc inverse) inverse)"). Prints("\033[7mabc\033[m"), That("print (styled (styled abc inverse) no-inverse)").Prints("abc"), That("print (styled (styled abc inverse) toggle-inverse)").Prints("abc"), That("print (styled (styled abc inverse) toggle-inverse toggle-inverse)").Prints("\033[7mabc\033[m"), // Function as transformer That("print (styled abc {|s| put $s })").Prints("abc"), That("print (styled abc {|s| styled-segment $s &bold=$true &italic=$false })").Prints("\033[1mabc\033[m"), That("print (styled abc italic {|s| styled-segment $s &bold=$true &italic=$false })").Prints("\033[1mabc\033[m"), That("styled abc {|_| fail bad }").Throws(eval.FailError{"bad"}), That("styled abc {|_| put a b }").Throws(ErrorWithMessage( "styling function must return a single segment; got 2 values")), That("styled abc {|_| put [] }").Throws(ErrorWithMessage( "styling function must return a segment; got list")), // Bad usage That("styled abc hopefully-never-exists").Throws(ErrorWithMessage( "hopefully-never-exists is not a valid style transformer")), That("styled []").Throws(ErrorWithMessage( "expected string, styled segment or styled text; got list")), That("styled abc []").Throws(ErrorWithMessage( "need string or callable; got list")), ) } func TestStyled_DoesNotModifyArgument(t *testing.T) { Test(t, That("x = (styled text); _ = (styled $x red); put $x[0][fg-color]"). Puts("default"), That("x = (styled-segment text); _ = (styled $x red); put $x[fg-color]"). Puts("default"), ) } func TestStyledConcat(t *testing.T) { Test(t, // string+segment That("print abc(styled-segment abc &fg-color=red)").Prints("abc\033[31mabc\033[m"), // segment+string That("print (styled-segment abc &fg-color=red)abc").Prints("\033[31mabc\033[mabc"), // segment+segment That("print (styled-segment abc &bg-color=red)(styled-segment abc &fg-color=red)").Prints("\033[41mabc\033[m\033[31mabc\033[m"), // segment+text That("print (styled-segment abc &underlined=$true)(styled abc bright-cyan)").Prints("\033[4mabc\033[m\033[96mabc\033[m"), // segment+num That("print (num 99.0)(styled-segment abc &blink)").Prints("99.0\033[5mabc\033[m"), That("print (num 66)(styled-segment abc &blink)").Prints("66\033[5mabc\033[m"), That("print (num 3/2)(styled-segment abc &blink)").Prints("3/2\033[5mabc\033[m"), // num+segment That("print (styled-segment abc &blink)(float64 88)").Prints("\033[5mabc\033[m88.0"), That("print (styled-segment abc &blink)(num 44/3)").Prints("\033[5mabc\033[m44/3"), That("print (styled-segment abc &blink)(num 42)").Prints("\033[5mabc\033[m42"), // string+text That("print abc(styled abc blink)").Prints("abc\033[5mabc\033[m"), // text+string That("print (styled abc blink)abc").Prints("\033[5mabc\033[mabc"), // number+text That("print (float64 13)(styled abc blink)").Prints("13.0\033[5mabc\033[m"), That("print (num 13)(styled abc blink)").Prints("13\033[5mabc\033[m"), That("print (num 4/3)(styled abc blink)").Prints("4/3\033[5mabc\033[m"), // text+number That("print (styled abc blink)(float64 127)").Prints("\033[5mabc\033[m127.0"), That("print (styled abc blink)(num 13)").Prints("\033[5mabc\033[m13"), That("print (styled abc blink)(num 3/4)").Prints("\033[5mabc\033[m3/4"), // text+segment That("print (styled abc inverse)(styled-segment abc &bg-color=white)").Prints("\033[7mabc\033[m\033[47mabc\033[m"), // text+text That("print (styled abc bold)(styled abc dim)").Prints("\033[1mabc\033[m\033[2mabc\033[m"), ) } func TestStyledIndexing(t *testing.T) { Test(t, That("put (styled-segment abc &italic=$true &fg-color=red)[bold]").Puts(false), That("put (styled-segment abc &italic=$true &fg-color=red)[italic]").Puts(true), That("put (styled-segment abc &italic=$true &fg-color=red)[fg-color]").Puts("red"), ) Test(t, That("put (styled abc red)[0][bold]").Puts(false), That("put (styled abc red)[0][bg-color]").Puts("default"), That("t = (styled-segment abc &underlined=$true)(styled abc bright-cyan); put $t[1][fg-color]").Puts("bright-cyan"), That("t = (styled-segment abc &underlined=$true)(styled abc bright-cyan); put $t[1][underlined]").Puts(false), ) } elvish-0.17.0/pkg/eval/builtin_ns.go000066400000000000000000000055111415471104000172750ustar00rootroot00000000000000package eval import ( "strconv" "syscall" "src.elv.sh/pkg/buildinfo" "src.elv.sh/pkg/eval/vars" ) //elvdoc:var _ // // A blackhole variable. // // Values assigned to it will be discarded. Referencing it always results in $nil. //elvdoc:var args // // A list containing command-line arguments. Analogous to `argv` in some other // languages. Examples: // // ```elvish-transcript // ~> echo 'put $args' > args.elv // ~> elvish args.elv foo -bar // ▶ [foo -bar] // ~> elvish -c 'put $args' foo -bar // ▶ [foo -bar] // ``` // // As demonstrated above, this variable does not contain the name of the script // used to invoke it. For that information, use the `src` command. // // @cf src //elvdoc:var false // // The boolean false value. //elvdoc:var ok // // The special value used by `?()` to signal absence of exceptions. //elvdoc:var nil // // A special value useful for representing the lack of values. //elvdoc:var paths // // A list of search paths, kept in sync with `$E:PATH`. It is easier to use than // `$E:PATH`. //elvdoc:var pid // // The process ID of the current Elvish process. //elvdoc:var pwd // // The present working directory. Setting this variable has the same effect as // `cd`. This variable is most useful in a temporary assignment. // // Example: // // ```elvish // ## Updates all git repositories // for x [*/] { // pwd=$x { // if ?(test -d .git) { // git pull // } // } // } // ``` // // Etymology: the `pwd` command. // // @cf cd //elvdoc:var true // // The boolean true value. //elvdoc:var buildinfo // // A [psuedo-map](./language.html#pseudo-map) that exposes information about the Elvish binary. // Running `put $buildinfo | to-json` will produce the same output as `elvish -buildinfo -json`. // // @cf version //elvdoc:var version // // The full version of the Elvish binary as a string. This is the same information reported by // `elvish -version` and the value of `$buildinfo[version]`. // // **Note:** In general it is better to perform functionality tests rather than testing `$version`. // For example, do something like // // ``` // has-key $builtin: new-var // ```` // // to test if variable `new-var` is available rather than comparing against `$version` to see if the // elvish version is equal to or newer than the version that introduced `new-var`. // // @cf buildinfo var builtinNs = BuildNsNamed("").AddVars(map[string]vars.Var{ "_": vars.NewBlackhole(), "pid": vars.NewReadOnly(strconv.Itoa(syscall.Getpid())), "ok": vars.NewReadOnly(OK), "nil": vars.NewReadOnly(nil), "true": vars.NewReadOnly(true), "false": vars.NewReadOnly(false), "buildinfo": vars.NewReadOnly(buildinfo.Value), "version": vars.NewReadOnly(buildinfo.Value.Version), "paths": vars.NewEnvListVar("PATH"), }) func addBuiltinFns(fns map[string]interface{}) { builtinNs.AddGoFns(fns) } elvish-0.17.0/pkg/eval/builtin_ns_test.go000066400000000000000000000005741415471104000203400ustar00rootroot00000000000000package eval_test import ( "testing" . "src.elv.sh/pkg/eval" . "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/eval/vals" ) func TestExplicitBuiltinModule(t *testing.T) { TestWithSetup(t, func(ev *Evaler) { ev.Args = vals.MakeList("a", "b") }, That("all $args").Puts("a", "b"), // Regression test for #1414 That("use builtin; all $builtin:args").Puts("a", "b"), ) } elvish-0.17.0/pkg/eval/builtin_special.go000066400000000000000000000523061415471104000203010ustar00rootroot00000000000000package eval // Builtin special forms. Special forms behave mostly like ordinary commands - // they are valid commands syntactically, and can take part in pipelines - but // they have special rules for the evaluation of their arguments and can affect // the compilation phase (whereas ordinary commands can only affect the // evaluation phase). // // For example, the "and" special form evaluates its arguments from left to // right, and stops as soon as one booleanly false value is obtained: the // command "and $false (fail haha)" does not produce an exception. // // As another example, the "del" special form removes a variable, affecting the // compiler. // // Flow control structures are also implemented as special forms in elvish, with // closures functioning as code blocks. import ( "os" "path/filepath" "strings" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/parse/cmpd" ) type compileBuiltin func(*compiler, *parse.Form) effectOp var builtinSpecials map[string]compileBuiltin // IsBuiltinSpecial is the set of all names of builtin special forms. It is // intended for external consumption, e.g. the syntax highlighter. var IsBuiltinSpecial = map[string]bool{} // NoSuchModule encodes an error where a module spec cannot be resolved. type NoSuchModule struct{ spec string } // Error implements the error interface. func (err NoSuchModule) Error() string { return "no such module: " + err.spec } func init() { // Needed to avoid initialization loop builtinSpecials = map[string]compileBuiltin{ "var": compileVar, "set": compileSet, "del": compileDel, "fn": compileFn, "use": compileUse, "and": compileAnd, "or": compileOr, "coalesce": compileCoalesce, "if": compileIf, "while": compileWhile, "for": compileFor, "try": compileTry, "pragma": compilePragma, } for name := range builtinSpecials { IsBuiltinSpecial[name] = true } } // VarForm = 'var' { VariablePrimary } [ '=' { Compound } ] func compileVar(cp *compiler, fn *parse.Form) effectOp { eqIndex := -1 for i, cn := range fn.Args { if parse.SourceText(cn) == "=" { eqIndex = i break } } if eqIndex == -1 { cp.parseCompoundLValues(fn.Args, newLValue) // Just create new variables, nothing extra to do at runtime. return nopOp{} } // Compile rhs before lhs before many potential shadowing var rhs valuesOp if eqIndex == len(fn.Args)-1 { rhs = nopValuesOp{diag.PointRanging(fn.Range().To)} } else { rhs = seqValuesOp{ diag.MixedRanging(fn.Args[eqIndex+1], fn.Args[len(fn.Args)-1]), cp.compoundOps(fn.Args[eqIndex+1:])} } lhs := cp.parseCompoundLValues(fn.Args[:eqIndex], newLValue) return &assignOp{fn.Range(), lhs, rhs} } // IsUnqualified returns whether name is an unqualified variable name. func IsUnqualified(name string) bool { i := strings.IndexByte(name, ':') return i == -1 || i == len(name)-1 } // SetForm = 'set' { LHS } '=' { Compound } func compileSet(cp *compiler, fn *parse.Form) effectOp { eq := -1 for i, cn := range fn.Args { if parse.SourceText(cn) == "=" { eq = i break } } if eq == -1 { cp.errorpf(diag.PointRanging(fn.Range().To), "need = and right-hand-side") } lhs := cp.parseCompoundLValues(fn.Args[:eq], setLValue) var rhs valuesOp if eq == len(fn.Args)-1 { rhs = nopValuesOp{diag.PointRanging(fn.Range().To)} } else { rhs = seqValuesOp{ diag.MixedRanging(fn.Args[eq+1], fn.Args[len(fn.Args)-1]), cp.compoundOps(fn.Args[eq+1:])} } return &assignOp{fn.Range(), lhs, rhs} } const delArgMsg = "arguments to del must be variable or variable elements" // DelForm = 'del' { LHS } func compileDel(cp *compiler, fn *parse.Form) effectOp { var ops []effectOp for _, cn := range fn.Args { if len(cn.Indexings) != 1 { cp.errorpf(cn, delArgMsg) continue } head, indices := cn.Indexings[0].Head, cn.Indexings[0].Indices if head.Type == parse.Variable { cp.errorpf(cn, "arguments to del must drop $") } else if !parse.ValidLHSVariable(head, false) { cp.errorpf(cn, delArgMsg) } qname := head.Value var f effectOp ref := resolveVarRef(cp, qname, nil) if ref == nil { cp.errorpf(cn, "no variable $%s", head.Value) continue } if len(indices) == 0 { if ref.scope == envScope { f = delEnvVarOp{fn.Range(), ref.subNames[0]} } else if ref.scope == localScope && len(ref.subNames) == 0 { f = delLocalVarOp{ref.index} cp.thisScope().infos[ref.index].deleted = true } else { cp.errorpf(cn, "only variables in local: or E: can be deleted") continue } } else { f = newDelElementOp(ref, head.Range().From, head.Range().To, cp.arrayOps(indices)) } ops = append(ops, f) } return seqOp{ops} } type delLocalVarOp struct{ index int } func (op delLocalVarOp) exec(fm *Frame) Exception { fm.local.slots[op.index] = nil return nil } type delEnvVarOp struct { diag.Ranging name string } func (op delEnvVarOp) exec(fm *Frame) Exception { return fm.errorp(op, os.Unsetenv(op.name)) } func newDelElementOp(ref *varRef, begin, headEnd int, indexOps []valuesOp) effectOp { ends := make([]int, len(indexOps)+1) ends[0] = headEnd for i, op := range indexOps { ends[i+1] = op.Range().To } return &delElemOp{ref, indexOps, begin, ends} } type delElemOp struct { ref *varRef indexOps []valuesOp begin int ends []int } func (op *delElemOp) Range() diag.Ranging { return diag.Ranging{From: op.begin, To: op.ends[0]} } func (op *delElemOp) exec(fm *Frame) Exception { var indices []interface{} for _, indexOp := range op.indexOps { indexValues, exc := indexOp.exec(fm) if exc != nil { return exc } if len(indexValues) != 1 { return fm.errorpf(indexOp, "index must evaluate to a single value in argument to del") } indices = append(indices, indexValues[0]) } err := vars.DelElement(deref(fm, op.ref), indices) if err != nil { if level := vars.ElementErrorLevel(err); level >= 0 { return fm.errorp(diag.Ranging{From: op.begin, To: op.ends[level]}, err) } return fm.errorp(op, err) } return nil } // FnForm = 'fn' StringPrimary LambdaPrimary // // fn f { foobar } is a shorthand for set '&'f = { foobar }. func compileFn(cp *compiler, fn *parse.Form) effectOp { args := cp.walkArgs(fn) nameNode := args.next() name := stringLiteralOrError(cp, nameNode, "function name") bodyNode := args.nextMustLambda("function body") args.mustEnd() // Define the variable before compiling the body, so that the body may refer // to the function itself. index := cp.thisScope().add(name + FnSuffix) op := cp.lambda(bodyNode) return fnOp{nameNode.Range(), index, op} } type fnOp struct { keywordRange diag.Ranging varIndex int lambdaOp valuesOp } func (op fnOp) exec(fm *Frame) Exception { // Initialize the function variable with the builtin nop function. This step // allows the definition of recursive functions; the actual function will // never be called. fm.local.slots[op.varIndex].Set(NewGoFn("", nop)) values, exc := op.lambdaOp.exec(fm) if exc != nil { return exc } c := values[0].(*closure) c.Op = fnWrap{c.Op} return fm.errorp(op.keywordRange, fm.local.slots[op.varIndex].Set(c)) } type fnWrap struct{ effectOp } func (op fnWrap) Range() diag.Ranging { return op.effectOp.(diag.Ranger).Range() } func (op fnWrap) exec(fm *Frame) Exception { exc := op.effectOp.exec(fm) if exc != nil && exc.Reason() != Return { // rethrow return exc } return nil } // UseForm = 'use' StringPrimary func compileUse(cp *compiler, fn *parse.Form) effectOp { var name, spec string switch len(fn.Args) { case 0: end := fn.Head.Range().To cp.errorpf(diag.PointRanging(end), "lack module name") case 1: spec = stringLiteralOrError(cp, fn.Args[0], "module spec") // Use the last path component as the name; for instance, if path = // "a/b/c/d", name is "d". If path doesn't have slashes, name = path. name = spec[strings.LastIndexByte(spec, '/')+1:] case 2: // TODO(xiaq): Allow using variable as module path spec = stringLiteralOrError(cp, fn.Args[0], "module spec") name = stringLiteralOrError(cp, fn.Args[1], "module name") default: // > 2 cp.errorpf(diag.MixedRanging(fn.Args[2], fn.Args[len(fn.Args)-1]), "superfluous argument(s)") } return useOp{fn.Range(), cp.thisScope().add(name + NsSuffix), spec} } type useOp struct { diag.Ranging varIndex int spec string } func (op useOp) exec(fm *Frame) Exception { ns, err := use(fm, op.spec, op) if err != nil { return fm.errorp(op, err) } fm.local.slots[op.varIndex].Set(ns) return nil } // TODO: Add support for module specs relative to a package/workspace. // See https://github.com/elves/elvish/issues/1421. func use(fm *Frame, spec string, r diag.Ranger) (*Ns, error) { // Handle relative imports. Note that this deliberately does not support Windows backslash as a // path separator because module specs are meant to be platform independent. If necessary, we // translate a module spec to an appropriate path for the platform. if strings.HasPrefix(spec, "./") || strings.HasPrefix(spec, "../") { var dir string if fm.srcMeta.IsFile { dir = filepath.Dir(fm.srcMeta.Name) } else { var err error dir, err = os.Getwd() if err != nil { return nil, err } } path := filepath.Clean(dir + "/" + spec) return useFromFile(fm, spec, path, r) } // Handle imports of pre-defined modules like `builtin` and `str`. if ns, ok := fm.Evaler.modules[spec]; ok { return ns, nil } if code, ok := fm.Evaler.BundledModules[spec]; ok { return evalModule(fm, spec, parse.Source{Name: "[bundled " + spec + "]", Code: code}, r) } // Handle imports relative to the Elvish module search directories. // // TODO: For non-relative imports, use the spec (instead of the full path) // as the module key instead to avoid searching every time. for _, dir := range fm.Evaler.LibDirs { ns, err := useFromFile(fm, spec, filepath.Join(dir, spec), r) if _, isNoSuchModule := err.(NoSuchModule); isNoSuchModule { continue } return ns, err } // Sadly, we couldn't resolve the module spec. return nil, NoSuchModule{spec} } // TODO: Make access to fm.Evaler.modules concurrency-safe. func useFromFile(fm *Frame, spec, path string, r diag.Ranger) (*Ns, error) { if ns, ok := fm.Evaler.modules[path]; ok { return ns, nil } _, err := os.Stat(path + ".so") if err != nil { code, err := readFileUTF8(path + ".elv") if err != nil { if os.IsNotExist(err) { return nil, NoSuchModule{spec} } return nil, err } src := parse.Source{Name: path + ".elv", Code: code, IsFile: true} return evalModule(fm, path, src, r) } plug, err := pluginOpen(path + ".so") if err != nil { return nil, NoSuchModule{spec} } sym, err := plug.Lookup("Ns") if err != nil { return nil, err } ns, ok := sym.(**Ns) if !ok { return nil, NoSuchModule{spec} } fm.Evaler.modules[path] = *ns return *ns, nil } // TODO: Make access to fm.Evaler.modules concurrency-safe. func evalModule(fm *Frame, key string, src parse.Source, r diag.Ranger) (*Ns, error) { ns, exec, err := fm.PrepareEval(src, r, new(Ns)) if err != nil { return nil, err } // Installs the namespace before executing. This prevent circular use'es // from resulting in an infinite recursion. fm.Evaler.modules[key] = ns err = exec() if err != nil { // Unload the namespace. delete(fm.Evaler.modules, key) return nil, err } return ns, nil } // compileAnd compiles the "and" special form. // // The and special form evaluates arguments until a false-ish values is found // and outputs it; the remaining arguments are not evaluated. If there are no // false-ish values, the last value is output. If there are no arguments, it // outputs $true, as if there is a hidden $true before actual arguments. func compileAnd(cp *compiler, fn *parse.Form) effectOp { return &andOrOp{fn.Range(), cp.compoundOps(fn.Args), true, false} } // compileOr compiles the "or" special form. // // The or special form evaluates arguments until a true-ish values is found and // outputs it; the remaining arguments are not evaluated. If there are no // true-ish values, the last value is output. If there are no arguments, it // outputs $false, as if there is a hidden $false before actual arguments. func compileOr(cp *compiler, fn *parse.Form) effectOp { return &andOrOp{fn.Range(), cp.compoundOps(fn.Args), false, true} } type andOrOp struct { diag.Ranging argOps []valuesOp init bool stopAt bool } func (op *andOrOp) exec(fm *Frame) Exception { var lastValue interface{} = vals.Bool(op.init) out := fm.ValueOutput() for _, argOp := range op.argOps { values, exc := argOp.exec(fm) if exc != nil { return exc } for _, value := range values { if vals.Bool(value) == op.stopAt { return fm.errorp(op, out.Put(value)) } lastValue = value } } return fm.errorp(op, out.Put(lastValue)) } // Compiles the "coalesce" special form, which is like "or", but evaluates until // a non-nil value is found. func compileCoalesce(cp *compiler, fn *parse.Form) effectOp { return &coalesceOp{fn.Range(), cp.compoundOps(fn.Args)} } type coalesceOp struct { diag.Ranging argOps []valuesOp } func (op *coalesceOp) exec(fm *Frame) Exception { out := fm.ValueOutput() for _, argOp := range op.argOps { values, exc := argOp.exec(fm) if exc != nil { return exc } for _, value := range values { if value != nil { return fm.errorp(op, out.Put(value)) } } } return fm.errorp(op, out.Put(nil)) } func compileIf(cp *compiler, fn *parse.Form) effectOp { args := cp.walkArgs(fn) var condNodes []*parse.Compound var bodyNodes []*parse.Primary condLeader := "if" for { condNodes = append(condNodes, args.next()) bodyNodes = append(bodyNodes, args.nextMustLambda(condLeader)) if !args.nextIs("elif") { break } condLeader = "elif" } elseNode := args.nextMustLambdaIfAfter("else") args.mustEnd() condOps := cp.compoundOps(condNodes) bodyOps := cp.primaryOps(bodyNodes) var elseOp valuesOp if elseNode != nil { elseOp = cp.primaryOp(elseNode) } return &ifOp{fn.Range(), condOps, bodyOps, elseOp} } type ifOp struct { diag.Ranging condOps []valuesOp bodyOps []valuesOp elseOp valuesOp } func (op *ifOp) exec(fm *Frame) Exception { bodies := make([]Callable, len(op.bodyOps)) for i, bodyOp := range op.bodyOps { bodies[i] = execLambdaOp(fm, bodyOp) } elseFn := execLambdaOp(fm, op.elseOp) for i, condOp := range op.condOps { condValues, exc := condOp.exec(fm.fork("if cond")) if exc != nil { return exc } if allTrue(condValues) { return fm.errorp(op, bodies[i].Call(fm.fork("if body"), NoArgs, NoOpts)) } } if op.elseOp != nil { return fm.errorp(op, elseFn.Call(fm.fork("if else"), NoArgs, NoOpts)) } return nil } func compileWhile(cp *compiler, fn *parse.Form) effectOp { args := cp.walkArgs(fn) condNode := args.next() bodyNode := args.nextMustLambda("while body") elseNode := args.nextMustLambdaIfAfter("else") args.mustEnd() condOp := cp.compoundOp(condNode) bodyOp := cp.primaryOp(bodyNode) var elseOp valuesOp if elseNode != nil { elseOp = cp.primaryOp(elseNode) } return &whileOp{fn.Range(), condOp, bodyOp, elseOp} } type whileOp struct { diag.Ranging condOp, bodyOp, elseOp valuesOp } func (op *whileOp) exec(fm *Frame) Exception { body := execLambdaOp(fm, op.bodyOp) elseBody := execLambdaOp(fm, op.elseOp) iterated := false for { condValues, exc := op.condOp.exec(fm.fork("while cond")) if exc != nil { return exc } if !allTrue(condValues) { break } iterated = true err := body.Call(fm.fork("while"), NoArgs, NoOpts) if err != nil { exc := err.(Exception) if exc.Reason() == Continue { // Do nothing } else if exc.Reason() == Break { break } else { return exc } } } if op.elseOp != nil && !iterated { return fm.errorp(op, elseBody.Call(fm.fork("while else"), NoArgs, NoOpts)) } return nil } func compileFor(cp *compiler, fn *parse.Form) effectOp { args := cp.walkArgs(fn) varNode := args.next() iterNode := args.next() bodyNode := args.nextMustLambda("for body") elseNode := args.nextMustLambdaIfAfter("else") args.mustEnd() lvalue := cp.compileOneLValue(varNode, setLValue|newLValue) iterOp := cp.compoundOp(iterNode) bodyOp := cp.primaryOp(bodyNode) var elseOp valuesOp if elseNode != nil { elseOp = cp.primaryOp(elseNode) } return &forOp{fn.Range(), lvalue, iterOp, bodyOp, elseOp} } type forOp struct { diag.Ranging lvalue lvalue iterOp valuesOp bodyOp valuesOp elseOp valuesOp } func (op *forOp) exec(fm *Frame) Exception { variable, err := derefLValue(fm, op.lvalue) if err != nil { return fm.errorp(op.lvalue, err) } iterable, err := evalForValue(fm, op.iterOp, "value being iterated") if err != nil { return fm.errorp(op, err) } body := execLambdaOp(fm, op.bodyOp) elseBody := execLambdaOp(fm, op.elseOp) iterated := false var errElement error errIterate := vals.Iterate(iterable, func(v interface{}) bool { iterated = true err := variable.Set(v) if err != nil { errElement = err return false } err = body.Call(fm.fork("for"), NoArgs, NoOpts) if err != nil { exc := err.(Exception) if exc.Reason() == Continue { // do nothing } else if exc.Reason() == Break { return false } else { errElement = err return false } } return true }) if errIterate != nil { return fm.errorp(op, errIterate) } if errElement != nil { return fm.errorp(op, errElement) } if !iterated && elseBody != nil { return fm.errorp(op, elseBody.Call(fm.fork("for else"), NoArgs, NoOpts)) } return nil } func compileTry(cp *compiler, fn *parse.Form) effectOp { logger.Println("compiling try") args := cp.walkArgs(fn) bodyNode := args.nextMustLambda("try body") logger.Printf("body is %q", parse.SourceText(bodyNode)) var exceptVarNode *parse.Compound var exceptNode *parse.Primary if args.nextIs("except") { logger.Println("except-ing") // Parse an optional lvalue into exceptVarNode. n := args.peek() if _, ok := cmpd.StringLiteral(n); ok { exceptVarNode = n args.next() } exceptNode = args.nextMustLambda("except body") } elseNode := args.nextMustLambdaIfAfter("else") finallyNode := args.nextMustLambdaIfAfter("finally") args.mustEnd() var exceptVar lvalue var bodyOp, exceptOp, elseOp, finallyOp valuesOp bodyOp = cp.primaryOp(bodyNode) if exceptVarNode != nil { exceptVar = cp.compileOneLValue(exceptVarNode, setLValue|newLValue) } if exceptNode != nil { exceptOp = cp.primaryOp(exceptNode) } if elseNode != nil { elseOp = cp.primaryOp(elseNode) } if finallyNode != nil { finallyOp = cp.primaryOp(finallyNode) } return &tryOp{fn.Range(), bodyOp, exceptVar, exceptOp, elseOp, finallyOp} } type tryOp struct { diag.Ranging bodyOp valuesOp exceptVar lvalue exceptOp valuesOp elseOp valuesOp finallyOp valuesOp } func (op *tryOp) exec(fm *Frame) Exception { body := execLambdaOp(fm, op.bodyOp) var exceptVar vars.Var if op.exceptVar.ref != nil { var err error exceptVar, err = derefLValue(fm, op.exceptVar) if err != nil { return fm.errorp(op, err) } } except := execLambdaOp(fm, op.exceptOp) elseFn := execLambdaOp(fm, op.elseOp) finally := execLambdaOp(fm, op.finallyOp) err := body.Call(fm.fork("try body"), NoArgs, NoOpts) if err != nil { if except != nil { if exceptVar != nil { err := exceptVar.Set(err.(Exception)) if err != nil { return fm.errorp(op.exceptVar, err) } } err = except.Call(fm.fork("try except"), NoArgs, NoOpts) } } else { if elseFn != nil { err = elseFn.Call(fm.fork("try else"), NoArgs, NoOpts) } } if finally != nil { errFinally := finally.Call(fm.fork("try finally"), NoArgs, NoOpts) if errFinally != nil { // TODO: If err is not nil, this discards err. Use something similar // to pipeline exception to expose both. return fm.errorp(op, errFinally) } } return fm.errorp(op, err) } // PragmaForm = 'pragma' 'fallback-resolver' '=' { Compound } func compilePragma(cp *compiler, fn *parse.Form) effectOp { args := cp.walkArgs(fn) nameNode := args.next() name := stringLiteralOrError(cp, nameNode, "pragma name") eqNode := args.next() eq := stringLiteralOrError(cp, eqNode, "literal =") if eq != "=" { cp.errorpf(eqNode, "must be literal =") } valueNode := args.next() args.mustEnd() switch name { case "unknown-command": value := stringLiteralOrError(cp, valueNode, "value for unknown-command") switch value { case "disallow": cp.currentPragma().unknownCommandIsExternal = false case "external": cp.currentPragma().unknownCommandIsExternal = true default: cp.errorpf(valueNode, "invalid value for unknown-command: %s", parse.Quote(value)) } default: cp.errorpf(nameNode, "unknown pragma %s", parse.Quote(name)) } return nopOp{} } func (cp *compiler) compileOneLValue(n *parse.Compound, f lvalueFlag) lvalue { if len(n.Indexings) != 1 { cp.errorpf(n, "must be valid lvalue") } lvalues := cp.parseIndexingLValue(n.Indexings[0], f) if lvalues.rest != -1 { cp.errorpf(lvalues.lvalues[lvalues.rest], "rest variable not allowed") } if len(lvalues.lvalues) != 1 { cp.errorpf(n, "must be exactly one lvalue") } return lvalues.lvalues[0] } // Executes a valuesOp that is known to yield a lambda and returns the lambda. // Returns nil if op is nil. func execLambdaOp(fm *Frame, op valuesOp) Callable { if op == nil { return nil } values, exc := op.exec(fm) if exc != nil { panic("must not be erroneous") } return values[0].(Callable) } elvish-0.17.0/pkg/eval/builtin_special_test.go000066400000000000000000000334171415471104000213420ustar00rootroot00000000000000package eval_test import ( "testing" . "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/prog/progtest" . "src.elv.sh/pkg/eval/evaltest" . "src.elv.sh/pkg/testutil" ) func TestPragma(t *testing.T) { Test(t, That("pragma unknown-command").DoesNotCompile(), That("pragma unknown-command =").DoesNotCompile(), That("pragma unknown-command x").DoesNotCompile(), That("pragma bad-name = some-value").DoesNotCompile(), That("pragma unknown-command = bad").DoesNotCompile(), ) // Actual effect of the unknown-command pragma is tested in TestCommand_External } func TestVar(t *testing.T) { Test(t, // Declaring one variable That("var x", "put $x").Puts(nil), // Declaring one variable whose name needs to be quoted That("var 'a/b'", "put $'a/b'").Puts(nil), // Declaring one variable whose name ends in ":". That("var a:").DoesNothing(), // Declaring a variable whose name ends in "~" initializes it to the // builtin nop function. That("var cmd~; cmd &ignored-opt ignored-arg").DoesNothing(), // Declaring multiple variables That("var x y", "put $x $y").Puts(nil, nil), // Declaring one variable with initial value That("var x = foo", "put $x").Puts("foo"), // Declaring multiple variables with initial values That("var x y = foo bar", "put $x $y").Puts("foo", "bar"), // Declaring multiple variables with initial values, including a rest // variable in the assignment LHS That("var x @y z = a b c d", "put $x $y $z"). Puts("a", vals.MakeList("b", "c"), "d"), // An empty RHS is technically legal although rarely useful. That("var @x =", "put $x").Puts(vals.EmptyList), // Shadowing. That("var x = old; fn f { put $x }", "var x = new; put $x; f"). Puts("new", "old"), // Explicit local: is allowed That("var local:x = foo", "put $x").Puts("foo"), // Variable name that must be quoted after $ must be quoted That("var a/b").DoesNotCompile(), // Multiple @ not allowed That("var x @y @z = a b c d").DoesNotCompile(), // Non-local not allowed That("var ns:a").DoesNotCompile(), // Index not allowed That("var a[0]").DoesNotCompile(), // Composite expression not allowed That("var a'b'").DoesNotCompile(), ) } func TestSet(t *testing.T) { Test(t, // Setting one variable That("var x; set x = foo", "put $x").Puts("foo"), // An empty RHS is technically legal although rarely useful. That("var x; set @x =", "put $x").Puts(vals.EmptyList), // Variable must already exist That("set x = foo").DoesNotCompile(), // Not duplicating tests with TestCommand_Assignment. // // TODO: After legacy assignment form is removed, transfer tests here. // = is required. That("var x; set x").DoesNotCompile(), ) } func TestDel(t *testing.T) { Setenv(t, "TEST_ENV", "test value") Test(t, // Deleting variable That("x = 1; del x").DoesNothing(), That("x = 1; del x; echo $x").DoesNotCompile(), That("x = 1; del :x; echo $x").DoesNotCompile(), That("x = 1; del local:x; echo $x").DoesNotCompile(), // Deleting environment variable That("has-env TEST_ENV", "del E:TEST_ENV", "has-env TEST_ENV").Puts(true, false), // Deleting variable whose name contains special characters That("'a/b' = foo; del 'a/b'").DoesNothing(), // Deleting element That("x = [&k=v &k2=v2]; del x[k2]; keys $x").Puts("k"), That("x = [[&k=v &k2=v2]]; del x[0][k2]; keys $x[0]").Puts("k"), // Error cases // Deleting nonexistent variable That("del x").DoesNotCompile(), // Deleting element of nonexistent variable That("del x[0]").DoesNotCompile(), // Deleting variable in non-local namespace That("var a: = (ns [&b=$nil])", "del a:b").DoesNotCompile(), // Variable name given with $ That("x = 1; del $x").DoesNotCompile(), // Variable name not given as a single primary expression That("ab = 1; del a'b'").DoesNotCompile(), // Variable name not a string That("del [a]").DoesNotCompile(), // Variable name has sigil That("x = []; del @x").DoesNotCompile(), // Variable name not quoted when it should be That("'a/b' = foo; del a/b").DoesNotCompile(), // Index is multiple values That("x = [&k1=v1 &k2=v2]", "del x[k1 k2]").Throws( ErrorWithMessage("index must evaluate to a single value in argument to del"), "k1 k2"), // Index expression throws exception That("x = [&k]", "del x[(fail x)]").Throws(FailError{"x"}, "fail x"), // Value does not support element removal That("x = (num 1)", "del x[k]").Throws( ErrorWithMessage("value does not support element removal"), // TODO: Fix the stack trace so that it is "x[k]" "x[k"), // Intermediate element does not exist That("x = [&]", "del x[k][0]").Throws( ErrorWithMessage("no such key: k"), // TODO: Fix the stack trace so that it is "x[k]" "x"), ) } func TestAnd(t *testing.T) { Test(t, That("and $true $false").Puts(false), That("and a b").Puts("b"), That("and $false b").Puts(false), That("and $true b").Puts("b"), // short circuit That("x = a; and $false (x = b); put $x").Puts(false, "a"), // Exception That("and a (fail x)").Throws(FailError{"x"}, "fail x"), thatOutputErrorIsBubbled("and a"), ) } func TestOr(t *testing.T) { Test(t, That("or $true $false").Puts(true), That("or a b").Puts("a"), That("or $false b").Puts("b"), That("or $true b").Puts(true), // short circuit That("x = a; or $true (x = b); put $x").Puts(true, "a"), // Exception That("or $false (fail x)").Throws(FailError{"x"}, "fail x"), thatOutputErrorIsBubbled("or a"), ) } func TestCoalesce(t *testing.T) { Test(t, That("coalesce a b").Puts("a"), That("coalesce $nil b").Puts("b"), That("coalesce $nil $nil").Puts(nil), That("coalesce").Puts(nil), // exception propagation That("coalesce $nil (fail foo)").Throws(FailError{"foo"}), // short circuit That("coalesce a (fail foo)").Puts("a"), thatOutputErrorIsBubbled("coalesce a"), ) } func TestIf(t *testing.T) { Test(t, That("if true { put then }").Puts("then"), That("if $false { put then } else { put else }").Puts("else"), That("if $false { put 1 } elif $false { put 2 } else { put 3 }"). Puts("3"), That("if $false { put 2 } elif true { put 2 } else { put 3 }").Puts("2"), // Exception in condition expression That("if (fail x) { }").Throws(FailError{"x"}, "fail x"), ) } func TestTry(t *testing.T) { Test(t, That("try { nop } except { put bad } else { put good }").Puts("good"), That("try { e:false } except - { put bad } else { put good }"). Puts("bad"), That("try { fail tr }").Throws(ErrorWithMessage("tr")), That("try { fail tr } finally { put final }"). Puts("final"). Throws(ErrorWithMessage("tr")), That("try { fail tr } except { fail ex } finally { put final }"). Puts("final"). Throws(ErrorWithMessage("ex")), That("try { fail tr } except { put ex } finally { fail final }"). Puts("ex"). Throws(ErrorWithMessage("final")), That("try { fail tr } except { fail ex } finally { fail final }"). Throws(ErrorWithMessage("final")), // wrong syntax That("try { nop } except @a { }").DoesNotCompile(), // A quoted var name, that would be invalid as a bareword, should be allowed as the referent // in a `try...except...` block. That("try { fail hard } except 'x=' { put 'x= ='(to-string $'x=') }"). Puts("x= =[&reason=[&content=hard &type=fail]]"), ) } func TestWhile(t *testing.T) { Test(t, That("var x = (num 0)", "while (< $x 4) { put $x; set x = (+ $x 1) }"). Puts(0, 1, 2, 3), // break That("var x = (num 0)", "while (< $x 4) { put $x; break }").Puts(0), // continue That("var x = (num 0)", "while (< $x 4) { put $x; set x = (+ $x 1); continue; put bad }"). Puts(0, 1, 2, 3), // Exception in body That("var x = 0; while (< $x 4) { fail haha }").Throws(AnyError), // Exception in condition That("while (fail x) { }").Throws(FailError{"x"}, "fail x"), // else branch - not taken That("var x = 0; while (< $x 4) { put $x; set x = (+ $x 1) } else { put bad }"). Puts("0", 1, 2, 3), // else branch - taken That("while $false { put bad } else { put good }").Puts("good"), ) } func TestFor(t *testing.T) { Test(t, // for That("for x [tempora mores] { put 'O '$x }"). Puts("O tempora", "O mores"), // break That("for x [a] { break } else { put $x }").DoesNothing(), // else That("for x [a] { put $x } else { put $x }").Puts("a"), // continue That("for x [a b] { put $x; continue; put $x; }").Puts("a", "b"), // else That("for x [] { } else { put else }").Puts("else"), That("for x [a] { } else { put else }").DoesNothing(), // Propagating exception. That("for x [a] { fail foo }").Throws(FailError{"foo"}), // More than one iterator. That("for {x,y} [] { }").DoesNotCompile(), // Invalid for loop lvalue. You can't use a var in a namespace other // than the local namespace as the lvalue in a for loop. That("for no-such-namespace:x [a b] { }").DoesNotCompile(), // Exception with the variable That("var a: = (ns [&])", "for a:b [] { }").Throws( ErrorWithMessage("no variable $a:b"), "a:b"), // Exception when evaluating iterable. That("for x [][0] { }").Throws(ErrorWithType(errs.OutOfRange{}), "[][0]"), // More than one iterable. That("for x (put a b) { }").Throws( errs.ArityMismatch{What: "value being iterated", ValidLow: 1, ValidHigh: 1, Actual: 2}, "(put a b)"), // Non-iterable value That("for x (num 0) { }").Throws(ErrorWithMessage("cannot iterate number")), ) } func TestFn(t *testing.T) { Test(t, That("fn f {|x| put x=$x'.' }; f lorem; f ipsum"). Puts("x=lorem.", "x=ipsum."), // Recursive functions with fn. Regression test for #1206. That("fn f {|n| if (== $n 0) { num 1 } else { * $n (f (- $n 1)) } }; f 3"). Puts(6), // Exception thrown by return is swallowed by a fn-defined function. That("fn f { put a; return; put b }; f").Puts("a"), // Error when evaluating the lambda That("fn f {|&opt=(fail x)| }").Throws(FailError{"x"}, "fail x"), ) } // Regression test for #1225 func TestUse_SetsVariableCorrectlyIfModuleCallsExtendGlobal(t *testing.T) { libdir := InTempDir(t) ApplyDir(Dir{"a.elv": "add-var"}) ev := NewEvaler() ev.LibDirs = []string{libdir} addVar := func() { ev.ExtendGlobal(BuildNs().AddVar("b", vars.NewReadOnly("foo"))) } ev.ExtendBuiltin(BuildNs().AddGoFn("add-var", addVar)) err := ev.Eval(parse.Source{Code: "use a"}, EvalCfg{}) if err != nil { t.Fatal(err) } g := ev.Global() if g.IndexString("a:").Get().(*Ns) == nil { t.Errorf("$a: is nil") } if g.IndexString("b").Get().(string) != "foo" { t.Errorf(`$b is not "foo"`) } } func TestUse_SupportsCircularDependency(t *testing.T) { libdir := InTempDir(t) ApplyDir(Dir{ "a.elv": "var pre = apre; use b; put $b:pre $b:post; var post = apost", "b.elv": "var pre = bpre; use a; put $a:pre $a:post; var post = bpost", }) TestWithSetup(t, func(ev *Evaler) { ev.LibDirs = []string{libdir} }, That(`use a`).Puts( // When b.elv is imported from a.elv, $a:pre is set but $a:post is // not "apre", nil, // After a.elv imports b.elv, both $b:pre and $b:post are set "bpre", "bpost"), ) } func TestUse(t *testing.T) { libdir1 := InTempDir(t) ApplyDir(Dir{ "shadow.elv": "put lib1", }) libdir2 := InTempDir(t) ApplyDir(Dir{ "has-init.elv": "put has-init", "put-x.elv": "put $x", "lorem.elv": "name = lorem; fn put-name { put $name }", "d.elv": "name = d", "shadow.elv": "put lib2", "a": Dir{ "b": Dir{ "c": Dir{ "d.elv": "name = a/b/c/d", "x.elv": "use ./d; d = $d:name; use ../../../lorem; lorem = $lorem:name", }, }, }, }) TestWithSetup(t, func(ev *Evaler) { ev.LibDirs = []string{libdir1, libdir2} }, That(`use lorem; put $lorem:name`).Puts("lorem"), // imports are lexically scoped // TODO: Support testing for compilation error That(`{ use lorem }; put $lorem:name`).DoesNotCompile(), // prefers lib dir that appear earlier That("use shadow").Puts("lib1"), // use of imported variable is captured in upvalue That(`use lorem; { put $lorem:name }`).Puts("lorem"), That(`{ use lorem; { put $lorem:name } }`).Puts("lorem"), That(`({ use lorem; put { { put $lorem:name } } })`).Puts("lorem"), // use of imported function is also captured in upvalue That(`{ use lorem; { lorem:put-name } }`).Puts("lorem"), // use of a nested module That(`use a/b/c/d; put $d:name`).Puts("a/b/c/d"), // module is cached after first use That(`use has-init; use has-init`).Puts("has-init"), // repeated uses result in the same namespace being imported That("use lorem; use lorem lorem2; put $lorem:name $lorem2:name"). Puts("lorem", "lorem"), // overriding module That(`use d; put $d:name; use a/b/c/d; put $d:name`). Puts("d", "a/b/c/d"), // relative uses That(`use a/b/c/x; put $x:d $x:lorem`).Puts("a/b/c/d", "lorem"), // relative uses from top-level That(`use ./d; put $d:name`).Puts("d"), // Renaming module That(`use a/b/c/d mod; put $mod:name`).Puts("a/b/c/d"), // Variables defined in the default global scope is invisible from // modules That("x = foo; use put-x").Throws(AnyError), // Using an unknown module spec fails. That("use unknown").Throws(ErrorWithType(NoSuchModule{})), That("use ./unknown").Throws(ErrorWithType(NoSuchModule{})), That("use ../unknown").Throws(ErrorWithType(NoSuchModule{})), // Nonexistent module That("use non-existent").Throws(ErrorWithMessage("no such module: non-existent")), // Wrong uses of "use". That("use").DoesNotCompile(), That("use a b c").DoesNotCompile(), ) } // Regression test for #1072 func TestUse_WarnsAboutDeprecatedFeatures(t *testing.T) { progtest.SetDeprecationLevel(t, 17) libdir := InTempDir(t) MustWriteFile("dep.elv", "fn x { dir-history }") TestWithSetup(t, func(ev *Evaler) { ev.LibDirs = []string{libdir} }, // Importing module triggers check for deprecated features That("use dep").PrintsStderrWith("is deprecated"), ) } elvish-0.17.0/pkg/eval/callable.go000066400000000000000000000006651415471104000166730ustar00rootroot00000000000000package eval // Callable wraps the Call method. type Callable interface { // Call calls the receiver in a Frame with arguments and options. Call(fm *Frame, args []interface{}, opts map[string]interface{}) error } var ( // NoArgs is an empty argument list. It can be used as an argument to Call. NoArgs = []interface{}{} // NoOpts is an empty option map. It can be used as an argument to Call. NoOpts = map[string]interface{}{} ) elvish-0.17.0/pkg/eval/chdir_test.go000066400000000000000000000031341415471104000172560ustar00rootroot00000000000000package eval_test import ( "os" "testing" "src.elv.sh/pkg/env" . "src.elv.sh/pkg/eval" . "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/testutil" ) func TestChdir(t *testing.T) { dst := testutil.TempDir(t) ev := NewEvaler() argDirInBefore, argDirInAfter := "", "" ev.AddBeforeChdir(func(dir string) { argDirInBefore = dir }) ev.AddAfterChdir(func(dir string) { argDirInAfter = dir }) back := saveWd() defer back() err := ev.Chdir(dst) if err != nil { t.Errorf("Chdir => error %v", err) } if envPwd := os.Getenv(env.PWD); envPwd != dst { t.Errorf("$PWD is %q after Chdir, want %q", envPwd, dst) } if argDirInBefore != dst { t.Errorf("Chdir called before-hook with %q, want %q", argDirInBefore, dst) } if argDirInAfter != dst { t.Errorf("Chdir called before-hook with %q, want %q", argDirInAfter, dst) } } func TestChdirElvishHooks(t *testing.T) { dst := testutil.TempDir(t) back := saveWd() defer back() Test(t, That(` dir-in-before dir-in-after = '' '' @before-chdir = {|dst| dir-in-before = $dst } @after-chdir = {|dst| dir-in-after = $dst } cd `+parse.Quote(dst)+` put $dir-in-before $dir-in-after `).Puts(dst, dst), ) } func TestChdirError(t *testing.T) { testutil.InTempDir(t) ev := NewEvaler() err := ev.Chdir("i/dont/exist") if err == nil { t.Errorf("Chdir => no error when dir does not exist") } } // Saves the current working directory, and returns a function for returning to // it. func saveWd() func() { wd, err := os.Getwd() if err != nil { panic(err) } return func() { testutil.MustChdir(wd) } } elvish-0.17.0/pkg/eval/closure.go000066400000000000000000000126521415471104000166070ustar00rootroot00000000000000package eval import ( "fmt" "sort" "strconv" "strings" "unsafe" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/persistent/hash" ) // A user-defined function in Elvish code. Each closure has its unique identity. type closure struct { ArgNames []string // The index of the rest argument. -1 if there is no rest argument. RestArg int OptNames []string OptDefaults []interface{} Op effectOp NewLocal []staticVarInfo Captured *Ns SrcMeta parse.Source DefRange diag.Ranging } var _ Callable = &closure{} // Kind returns "fn". func (*closure) Kind() string { return "fn" } // Equal compares by address. func (c *closure) Equal(rhs interface{}) bool { return c == rhs } // Hash returns the hash of the address of the closure. func (c *closure) Hash() uint32 { return hash.Pointer(unsafe.Pointer(c)) } // Repr returns an opaque representation "". func (c *closure) Repr(int) string { return fmt.Sprintf("", c) } // Call calls a closure. func (c *closure) Call(fm *Frame, args []interface{}, opts map[string]interface{}) error { // Check number of arguments. if c.RestArg != -1 { if len(args) < len(c.ArgNames)-1 { return errs.ArityMismatch{What: "arguments", ValidLow: len(c.ArgNames) - 1, ValidHigh: -1, Actual: len(args)} } } else { if len(args) != len(c.ArgNames) { return errs.ArityMismatch{What: "arguments", ValidLow: len(c.ArgNames), ValidHigh: len(c.ArgNames), Actual: len(args)} } } // Check whether all supplied options are supported. This map contains the // subset of keys from opts that can be found in c.OptNames. optSupported := make(map[string]struct{}) for _, name := range c.OptNames { _, ok := opts[name] if ok { optSupported[name] = struct{}{} } } if len(optSupported) < len(opts) { // Report all the options that are not supported. unsupported := make([]string, 0, len(opts)-len(optSupported)) for name := range opts { _, supported := optSupported[name] if !supported { unsupported = append(unsupported, parse.Quote(name)) } } sort.Strings(unsupported) return UnsupportedOptionsError{unsupported} } // This Frame is dedicated to the current form, so we can modify it in place. // BUG(xiaq): When evaluating closures, async access to global variables // and ports can be problematic. // Make upvalue namespace and capture variables. fm.up = c.Captured // Populate local scope with arguments, options, and newly created locals. localSize := len(c.ArgNames) + len(c.OptNames) + len(c.NewLocal) local := &Ns{make([]vars.Var, localSize), make([]staticVarInfo, localSize)} for i, name := range c.ArgNames { local.infos[i] = staticVarInfo{name, false, false} } if c.RestArg == -1 { for i := range c.ArgNames { local.slots[i] = vars.FromInit(args[i]) } } else { for i := 0; i < c.RestArg; i++ { local.slots[i] = vars.FromInit(args[i]) } restOff := len(args) - len(c.ArgNames) local.slots[c.RestArg] = vars.FromInit( vals.MakeList(args[c.RestArg : c.RestArg+restOff+1]...)) for i := c.RestArg + 1; i < len(c.ArgNames); i++ { local.slots[i] = vars.FromInit(args[i+restOff]) } } offset := len(c.ArgNames) for i, name := range c.OptNames { v, ok := opts[name] if !ok { v = c.OptDefaults[i] } local.infos[offset+i] = staticVarInfo{name, false, false} local.slots[offset+i] = vars.FromInit(v) } offset += len(c.OptNames) for i, info := range c.NewLocal { local.infos[offset+i] = info // TODO: Take info.readOnly into account too when creating variable local.slots[offset+i] = MakeVarFromName(info.name) } fm.local = local fm.srcMeta = c.SrcMeta return c.Op.exec(fm) } var ( fnDefault = NewGoFn("nop~", nop) nsDefault = &Ns{} ) // MakeVarFromName creates a Var with a suitable type constraint inferred from // the name. func MakeVarFromName(name string) vars.Var { switch { case strings.HasSuffix(name, FnSuffix): val := fnDefault return vars.FromPtr(&val) case strings.HasSuffix(name, NsSuffix): val := nsDefault return vars.FromPtr(&val) default: return vars.FromInit(nil) } } // UnsupportedOptionsError is an error returned by a closure call when there are // unsupported options. type UnsupportedOptionsError struct { Options []string } func (er UnsupportedOptionsError) Error() string { if len(er.Options) == 1 { return fmt.Sprintf("unsupported option: %s", er.Options[0]) } return fmt.Sprintf("unsupported options: %s", strings.Join(er.Options, ", ")) } func (c *closure) Fields() vals.StructMap { return closureFields{c} } type closureFields struct{ c *closure } func (closureFields) IsStructMap() {} func (cf closureFields) ArgNames() vals.List { return listOfStrings(cf.c.ArgNames) } func (cf closureFields) RestArg() string { return strconv.Itoa(cf.c.RestArg) } func (cf closureFields) OptNames() vals.List { return listOfStrings(cf.c.OptNames) } func (cf closureFields) Src() parse.Source { return cf.c.SrcMeta } func (cf closureFields) OptDefaults() vals.List { return vals.MakeList(cf.c.OptDefaults...) } func (cf closureFields) Body() string { r := cf.c.Op.(diag.Ranger).Range() return cf.c.SrcMeta.Code[r.From:r.To] } func (cf closureFields) Def() string { return cf.c.SrcMeta.Code[cf.c.DefRange.From:cf.c.DefRange.To] } func listOfStrings(ss []string) vals.List { list := vals.EmptyList for _, s := range ss { list = list.Cons(s) } return list } elvish-0.17.0/pkg/eval/closure_test.go000066400000000000000000000033661415471104000176500ustar00rootroot00000000000000package eval_test import ( "testing" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/tt" . "src.elv.sh/pkg/eval/evaltest" ) func TestClosureAsValue(t *testing.T) { Test(t, // Basic operations as a value. That("kind-of { }").Puts("fn"), That("eq { } { }").Puts(false), That("x = { }; put [&$x= foo][$x]").Puts("foo"), // Argument arity mismatch. That("f = {|x| }", "$f a b").Throws( errs.ArityMismatch{What: "arguments", ValidLow: 1, ValidHigh: 1, Actual: 2}, "$f a b"), That("f = {|x y| }", "$f a").Throws( errs.ArityMismatch{What: "arguments", ValidLow: 2, ValidHigh: 2, Actual: 1}, "$f a"), That("f = {|x y @rest| }", "$f a").Throws( errs.ArityMismatch{What: "arguments", ValidLow: 2, ValidHigh: -1, Actual: 1}, "$f a"), // Unsupported option. That("f = {|&valid1=1 &valid2=2| }; $f &bad1=1 &bad2=2").Throws( eval.UnsupportedOptionsError{[]string{"bad1", "bad2"}}, "$f &bad1=1 &bad2=2"), That("all {|a b| }[arg-names]").Puts("a", "b"), That("put {|@r| }[rest-arg]").Puts("0"), That("all {|&opt=def| }[opt-names]").Puts("opt"), That("all {|&opt=def| }[opt-defaults]").Puts("def"), That("put { body }[body]").Puts("body "), That("put {|x @y| body }[def]").Puts("{|x @y| body }"), That("put { body }[src][code]"). Puts("put { body }[src][code]"), // Regression test for https://b.elv.sh/1126 That("fn f { body }; put $f~[body]").Puts("body "), ) } func TestUnsupportedOptionsError(t *testing.T) { tt.Test(t, tt.Fn("Error", error.Error), tt.Table{ tt.Args(eval.UnsupportedOptionsError{[]string{"sole-opt"}}).Rets( "unsupported option: sole-opt"), tt.Args(eval.UnsupportedOptionsError{[]string{"opt-foo", "opt-bar"}}).Rets( "unsupported options: opt-foo, opt-bar"), }) } elvish-0.17.0/pkg/eval/compile_effect.go000066400000000000000000000364741415471104000201070ustar00rootroot00000000000000package eval import ( "fmt" "os" "sync" "sync/atomic" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" "src.elv.sh/pkg/fsutil" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/parse/cmpd" ) // An operation with some side effects. type effectOp interface{ exec(*Frame) Exception } func (cp *compiler) chunkOp(n *parse.Chunk) effectOp { return chunkOp{n.Range(), cp.pipelineOps(n.Pipelines)} } type chunkOp struct { diag.Ranging subops []effectOp } func (op chunkOp) exec(fm *Frame) Exception { for _, subop := range op.subops { exc := subop.exec(fm) if exc != nil { return exc } } // Check for interrupts after the chunk. // We also check for interrupts before each pipeline, so there is no // need to check it before the chunk or after each pipeline. if fm.IsInterrupted() { return fm.errorp(op, ErrInterrupted) } return nil } func (cp *compiler) pipelineOp(n *parse.Pipeline) effectOp { formOps := cp.formOps(n.Forms) return &pipelineOp{n.Range(), n.Background, parse.SourceText(n), formOps} } func (cp *compiler) pipelineOps(ns []*parse.Pipeline) []effectOp { ops := make([]effectOp, len(ns)) for i, n := range ns { ops[i] = cp.pipelineOp(n) } return ops } type pipelineOp struct { diag.Ranging bg bool source string subops []effectOp } const pipelineChanBufferSize = 32 func (op *pipelineOp) exec(fm *Frame) Exception { if fm.IsInterrupted() { return fm.errorp(op, ErrInterrupted) } if op.bg { fm = fm.fork("background job" + op.source) fm.intCh = nil fm.background = true fm.Evaler.addNumBgJobs(1) } nforms := len(op.subops) var wg sync.WaitGroup wg.Add(nforms) excs := make([]Exception, nforms) var nextIn *Port // For each form, create a dedicated evalCtx and run asynchronously for i, formOp := range op.subops { newFm := fm.fork("[form op]") inputIsPipe := i > 0 outputIsPipe := i < nforms-1 if inputIsPipe { newFm.ports[0] = nextIn } if outputIsPipe { // Each internal port pair consists of a (byte) pipe pair and a // channel. // os.Pipe sets O_CLOEXEC, which is what we want. reader, writer, e := os.Pipe() if e != nil { return fm.errorpf(op, "failed to create pipe: %s", e) } ch := make(chan interface{}, pipelineChanBufferSize) sendStop := make(chan struct{}) sendError := new(error) readerGone := new(int32) newFm.ports[1] = &Port{ File: writer, Chan: ch, closeFile: true, closeChan: true, sendStop: sendStop, sendError: sendError, readerGone: readerGone} nextIn = &Port{ File: reader, Chan: ch, closeFile: true, closeChan: false, // Store in input port for ease of retrieval later sendStop: sendStop, sendError: sendError, readerGone: readerGone} } f := func(formOp effectOp, pexc *Exception) { exc := formOp.exec(newFm) if exc != nil && !(outputIsPipe && isReaderGone(exc)) { *pexc = exc } if inputIsPipe { input := newFm.ports[0] *input.sendError = errs.ReaderGone{} close(input.sendStop) atomic.StoreInt32(input.readerGone, 1) } newFm.Close() wg.Done() } if i == nforms-1 && !op.bg { f(formOp, &excs[i]) } else { go f(formOp, &excs[i]) } } if op.bg { // Background job, wait for form termination asynchronously. go func() { wg.Wait() fm.Evaler.addNumBgJobs(-1) if notify := fm.Evaler.BgJobNotify; notify != nil { msg := "job " + op.source + " finished" err := MakePipelineError(excs) if err != nil { msg += ", errors = " + err.Error() } if fm.Evaler.getNotifyBgJobSuccess() || err != nil { notify(msg) } } }() return nil } wg.Wait() return fm.errorp(op, MakePipelineError(excs)) } func isReaderGone(exc Exception) bool { _, ok := exc.Reason().(errs.ReaderGone) return ok } func (cp *compiler) formOp(n *parse.Form) effectOp { var tempLValues []lvalue var assignmentOps []effectOp if len(n.Assignments) > 0 { assignmentOps = cp.assignmentOps(n.Assignments) if n.Head == nil { cp.errorpf(n, `using the syntax of temporary assignment for non-temporary assignment is no longer supported; use "var" or "set" instead`) return nopOp{} } for _, a := range n.Assignments { lvalues := cp.parseIndexingLValue(a.Left, setLValue|newLValue) tempLValues = append(tempLValues, lvalues.lvalues...) } logger.Println("temporary assignment of", len(n.Assignments), "pairs") } redirOps := cp.redirOps(n.Redirs) body := cp.formBody(n) return &formOp{n.Range(), tempLValues, assignmentOps, redirOps, body} } func (cp *compiler) formBody(n *parse.Form) formBody { if n.Head == nil { // Compiling an incomplete form node, return an empty body. return formBody{} } // Determine if this form is a special command. if head, ok := cmpd.StringLiteral(n.Head); ok { special, _ := resolveCmdHeadInternally(cp, head, n.Head) if special != nil { specialOp := special(cp, n) return formBody{specialOp: specialOp} } } // Determine if the form is a legacy assignment form, by looking for an // argument whose source is a literal "=". for i, arg := range n.Args { if parse.SourceText(arg) == "=" { cp.deprecate(n, "legacy assignment form is deprecated, use var or set instead; migrate scripts with https://go.elv.sh/u0.17", 17) lhsNodes := make([]*parse.Compound, i+1) lhsNodes[0] = n.Head copy(lhsNodes[1:], n.Args[:i]) lhs := cp.parseCompoundLValues(lhsNodes, setLValue|newLValue) rhsOps := cp.compoundOps(n.Args[i+1:]) var rhsRange diag.Ranging if len(rhsOps) > 0 { rhsRange = diag.MixedRanging(rhsOps[0], rhsOps[len(rhsOps)-1]) } else { rhsRange = diag.PointRanging(n.Range().To) } rhs := seqValuesOp{rhsRange, rhsOps} return formBody{assignOp: &assignOp{n.Range(), lhs, rhs}} } } var headOp valuesOp if head, ok := cmpd.StringLiteral(n.Head); ok { // Head is a literal string: resolve to function or external (special // commands are already handled above). if _, fnRef := resolveCmdHeadInternally(cp, head, n.Head); fnRef != nil { headOp = variableOp{n.Head.Range(), false, head + FnSuffix, fnRef} } else if cp.currentPragma().unknownCommandIsExternal || fsutil.DontSearch(head) { headOp = literalValues(n.Head, NewExternalCmd(head)) } else { cp.errorpf(n.Head, "unknown command disallowed by current pragma") } } else { // Head is not a literal string: evaluate as a normal expression. headOp = cp.compoundOp(n.Head) } argOps := cp.compoundOps(n.Args) optsOp := cp.mapPairs(n.Opts) return formBody{ordinaryCmd: ordinaryCmd{headOp, argOps, optsOp}} } func (cp *compiler) formOps(ns []*parse.Form) []effectOp { ops := make([]effectOp, len(ns)) for i, n := range ns { ops[i] = cp.formOp(n) } return ops } type formOp struct { diag.Ranging tempLValues []lvalue tempAssignOps []effectOp redirOps []effectOp body formBody } type formBody struct { // Exactly one field will be populated. specialOp effectOp assignOp effectOp ordinaryCmd ordinaryCmd } type ordinaryCmd struct { headOp valuesOp argOps []valuesOp optsOp *mapPairsOp } func (op *formOp) exec(fm *Frame) (errRet Exception) { // fm here is always a sub-frame created in compiler.pipeline, so it can // be safely modified. // Temporary assignment. if len(op.tempLValues) > 0 { // There is a temporary assignment. // Save variables. var saveVars []vars.Var var saveVals []interface{} for _, lv := range op.tempLValues { variable, err := derefLValue(fm, lv) if err != nil { return fm.errorp(op, err) } saveVars = append(saveVars, variable) } for i, v := range saveVars { // TODO(xiaq): If the variable to save is a elemVariable, save // the outermost variable instead. if u := vars.HeadOfElement(v); u != nil { v = u saveVars[i] = v } val := v.Get() saveVals = append(saveVals, val) logger.Printf("saved %s = %s", v, val) } // Do assignment. for _, subop := range op.tempAssignOps { exc := subop.exec(fm) if exc != nil { return exc } } // Defer variable restoration. Will be executed even if an error // occurs when evaling other part of the form. defer func() { for i, v := range saveVars { val := saveVals[i] if val == nil { // TODO(xiaq): Old value is nonexistent. We should delete // the variable. However, since the compiler now doesn't // delete it, we don't delete it in the evaler either. val = "" } err := v.Set(val) if err != nil { errRet = fm.errorp(op, err) } logger.Printf("restored %s = %s", v, val) } }() } // Redirections. for _, redirOp := range op.redirOps { exc := redirOp.exec(fm) if exc != nil { return exc } } if op.body.specialOp != nil { return op.body.specialOp.exec(fm) } if op.body.assignOp != nil { return op.body.assignOp.exec(fm) } // Ordinary command: evaluate head, arguments and options. cmd := op.body.ordinaryCmd // Special case: evaluating an incomplete form node. Return directly. if cmd.headOp == nil { return nil } headFn, err := evalForCommand(fm, cmd.headOp, "command") if err != nil { return fm.errorp(cmd.headOp, err) } var args []interface{} for _, argOp := range cmd.argOps { moreArgs, exc := argOp.exec(fm) if exc != nil { return exc } args = append(args, moreArgs...) } // TODO(xiaq): This conversion should be avoided. convertedOpts := make(map[string]interface{}) exc := cmd.optsOp.exec(fm, func(k, v interface{}) Exception { if ks, ok := k.(string); ok { convertedOpts[ks] = v return nil } // TODO(xiaq): Point to the particular key. return fm.errorp(op, errs.BadValue{ What: "option key", Valid: "string", Actual: vals.Kind(k)}) }) if exc != nil { return exc } fm.traceback = fm.addTraceback(op) err = headFn.Call(fm, args, convertedOpts) if exc, ok := err.(Exception); ok { return exc } return &exception{err, fm.traceback} } func evalForCommand(fm *Frame, op valuesOp, what string) (Callable, error) { value, err := evalForValue(fm, op, what) if err != nil { return nil, err } switch value := value.(type) { case Callable: return value, nil case string: if fsutil.DontSearch(value) { return NewExternalCmd(value), nil } } return nil, fm.errorp(op, errs.BadValue{ What: what, Valid: "callable or string containing slash", Actual: vals.Repr(value, vals.NoPretty)}) } func allTrue(vs []interface{}) bool { for _, v := range vs { if !vals.Bool(v) { return false } } return true } func (cp *compiler) assignmentOp(n *parse.Assignment) effectOp { lhs := cp.parseIndexingLValue(n.Left, setLValue|newLValue) rhs := cp.compoundOp(n.Right) return &assignOp{n.Range(), lhs, rhs} } func (cp *compiler) assignmentOps(ns []*parse.Assignment) []effectOp { ops := make([]effectOp, len(ns)) for i, n := range ns { ops[i] = cp.assignmentOp(n) } return ops } const defaultFileRedirPerm = 0644 // redir compiles a Redir into a op. func (cp *compiler) redirOp(n *parse.Redir) effectOp { var dstOp valuesOp if n.Left != nil { dstOp = cp.compoundOp(n.Left) } flag := makeFlag(n.Mode) if flag == -1 { // TODO: Record and get redirection sign position cp.errorpf(n, "bad redirection sign") } return &redirOp{n.Range(), dstOp, cp.compoundOp(n.Right), n.RightIsFd, n.Mode, flag} } func (cp *compiler) redirOps(ns []*parse.Redir) []effectOp { ops := make([]effectOp, len(ns)) for i, n := range ns { ops[i] = cp.redirOp(n) } return ops } func makeFlag(m parse.RedirMode) int { switch m { case parse.Read: return os.O_RDONLY case parse.Write: return os.O_WRONLY | os.O_CREATE | os.O_TRUNC case parse.ReadWrite: return os.O_RDWR | os.O_CREATE case parse.Append: return os.O_WRONLY | os.O_CREATE | os.O_APPEND default: return -1 } } type redirOp struct { diag.Ranging dstOp valuesOp srcOp valuesOp srcIsFd bool mode parse.RedirMode flag int } type invalidFD struct{ fd int } func (err invalidFD) Error() string { return fmt.Sprintf("invalid fd: %d", err.fd) } func (op *redirOp) exec(fm *Frame) Exception { var dst int if op.dstOp == nil { // No explicit FD destination specified; use default destinations switch op.mode { case parse.Read: dst = 0 case parse.Write, parse.ReadWrite, parse.Append: dst = 1 default: return fm.errorpf(op, "bad RedirMode; parser bug") } } else { // An explicit FD destination specified, evaluate it. var err error dst, err = evalForFd(fm, op.dstOp, false, "redirection destination") if err != nil { return fm.errorp(op, err) } } growPorts(&fm.ports, dst+1) fm.ports[dst].close() if op.srcIsFd { src, err := evalForFd(fm, op.srcOp, true, "redirection source") if err != nil { return fm.errorp(op, err) } switch { case src == -1: // close fm.ports[dst] = &Port{ // Ensure that writing to value output throws an exception sendStop: closedSendStop, sendError: &ErrNoValueOutput} case src >= len(fm.ports) || fm.ports[src] == nil: return fm.errorp(op, invalidFD{src}) default: fm.ports[dst] = fm.ports[src].fork() } return nil } src, err := evalForValue(fm, op.srcOp, "redirection source") if err != nil { return fm.errorp(op, err) } switch src := src.(type) { case string: f, err := os.OpenFile(src, op.flag, defaultFileRedirPerm) if err != nil { return fm.errorpf(op, "failed to open file %s: %s", vals.Repr(src, vals.NoPretty), err) } fm.ports[dst] = fileRedirPort(op.mode, f, true) case vals.File: fm.ports[dst] = fileRedirPort(op.mode, src, false) case vals.Pipe: var f *os.File switch op.mode { case parse.Read: f = src.ReadEnd case parse.Write: f = src.WriteEnd default: return fm.errorpf(op, "can only use < or > with pipes") } fm.ports[dst] = fileRedirPort(op.mode, f, false) default: return fm.errorp(op.srcOp, errs.BadValue{ What: "redirection source", Valid: "string, file or pipe", Actual: vals.Kind(src)}) } return nil } // Creates a port that only have a file component, populating the // channel-related fields with suitable values depending on the redirection // mode. func fileRedirPort(mode parse.RedirMode, f *os.File, closeFile bool) *Port { if mode == parse.Read { return &Port{ File: f, closeFile: closeFile, // ClosedChan produces no values when reading. Chan: ClosedChan, } } return &Port{ File: f, closeFile: closeFile, // Throws errValueOutputIsClosed when writing. Chan: nil, sendStop: closedSendStop, sendError: &ErrNoValueOutput, } } // Makes the size of *ports at least n, adding nil's if necessary. func growPorts(ports *[]*Port, n int) { if len(*ports) >= n { return } oldPorts := *ports *ports = make([]*Port, n) copy(*ports, oldPorts) } func evalForFd(fm *Frame, op valuesOp, closeOK bool, what string) (int, error) { value, err := evalForValue(fm, op, what) if err != nil { return -1, err } switch value { case "stdin": return 0, nil case "stdout": return 1, nil case "stderr": return 2, nil } var fd int if vals.ScanToGo(value, &fd) == nil { return fd, nil } else if value == "-" && closeOK { return -1, nil } valid := "fd name or number" if closeOK { valid = "fd name or number or '-'" } return -1, fm.errorp(op, errs.BadValue{ What: what, Valid: valid, Actual: vals.Repr(value, vals.NoPretty)}) } type seqOp struct{ subops []effectOp } func (op seqOp) exec(fm *Frame) Exception { for _, subop := range op.subops { exc := subop.exec(fm) if exc != nil { return exc } } return nil } type nopOp struct{} func (nopOp) exec(fm *Frame) Exception { return nil } elvish-0.17.0/pkg/eval/compile_effect_test.go000066400000000000000000000275331415471104000211420ustar00rootroot00000000000000package eval_test import ( "testing" "time" . "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/errs" . "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/mods/file" "src.elv.sh/pkg/testutil" ) func TestChunk(t *testing.T) { Test(t, // Empty chunk That("").DoesNothing(), // Outputs of pipelines in a chunk are concatenated That("put x; put y; put z").Puts("x", "y", "z"), // A failed pipeline cause the whole chunk to fail That("put a; e:false; put b").Puts("a").Throws(AnyError), ) } func TestPipeline(t *testing.T) { Test(t, // Pure byte pipeline That(`echo "Albert\nAllan\nAlbraham\nBerlin" | sed s/l/1/g | grep e`). Prints("A1bert\nBer1in\n"), // Pure value pipeline That(`put 233 42 19 | each {|x|+ $x 10}`).Puts(243, 52, 29), // Pipeline draining. That(`range 100 | put x`).Puts("x"), // TODO: Add a useful hybrid pipeline sample ) } func TestPipeline_BgJob(t *testing.T) { setup := func(ev *Evaler) { ev.ExtendGlobal(BuildNs().AddNs("file", file.Ns)) } notes1 := make(chan string) notes2 := make(chan string) putNote := func(ch chan<- string) func(*Evaler) { return func(ev *Evaler) { ev.BgJobNotify = func(note string) { ch <- note } } } verifyNote := func(notes <-chan string, wantNote string) func(t *testing.T) { return func(t *testing.T) { select { case note := <-notes: if note != wantNote { t.Errorf("got note %q, want %q", note, wantNote) } case <-time.After(testutil.Scaled(100 * time.Millisecond)): t.Errorf("timeout waiting for notification") } } } TestWithSetup(t, setup, That( "notify-bg-job-success = $false", "p = (file:pipe)", "{ print foo > $p; file:close $p[w] }&", "slurp < $p; file:close $p[r]"). Puts("foo"), // Notification That( "notify-bg-job-success = $true", "p = (file:pipe)", "fn f { file:close $p[w] }", "f &", "slurp < $p; file:close $p[r]"). Puts(""). WithSetup(putNote(notes1)). Passes(verifyNote(notes1, "job f & finished")), // Notification, with exception That( "notify-bg-job-success = $true", "p = (file:pipe)", "fn f { file:close $p[w]; fail foo }", "f &", "slurp < $p; file:close $p[r]"). Puts(""). WithSetup(putNote(notes2)). Passes(verifyNote(notes2, "job f & finished, errors = foo")), ) } func TestPipeline_ReaderGone(t *testing.T) { // See UNIX-only tests in compile_effect_unix_test.go. Test(t, // Internal commands writing to byte output raises ReaderGone when the // reader is exited, which is then suppressed. That("while $true { echo y } | nop").DoesNothing(), That( "var reached = $false", "{ while $true { echo y }; reached = $true } | nop", "put $reached", ).Puts(false), // Similar for value output. That("while $true { put y } | nop").DoesNothing(), That( "var reached = $false", "{ while $true { put y }; reached = $true } | nop", "put $reached", ).Puts(false), ) } func TestCommand(t *testing.T) { Test(t, That("put foo").Puts("foo"), // Command errors when the head is not a single value. That("{put put} foo").Throws( errs.ArityMismatch{What: "command", ValidLow: 1, ValidHigh: 1, Actual: 2}, "{put put}"), // Command errors when the head is not callable or string containing slash. That("[] foo").Throws( errs.BadValue{ What: "command", Valid: "callable or string containing slash", Actual: "[]"}, "[]"), // Command errors when when argument errors. That("put [][1]").Throws(ErrorWithType(errs.OutOfRange{}), "[][1]"), // Command errors when an option key is not string. That("put &[]=[]").Throws( errs.BadValue{What: "option key", Valid: "string", Actual: "list"}, "put &[]=[]"), // Command errors when any optional evaluation errors. That("put &x=[][1]").Throws(ErrorWithType(errs.OutOfRange{}), "[][1]"), ) } func TestCommand_Special(t *testing.T) { Test(t, // Regression test for #1204; ensures that the arguments of special // forms are not accidentally compiled twice. That("nop (and (use builtin)); nop $builtin:echo~").DoesNothing(), // Behavior of individual special commands are tested in // builtin_special_test.go. ) } func TestCommand_Assignment(t *testing.T) { // NOTE: TestClosure has more tests for the interaction between assignment // and variable scoping. Test(t, // Spacey assignment. That("a = foo; put $a").Puts("foo"), That("a b = foo bar; put $a $b").Puts("foo", "bar"), That("a @b = 2 3 foo; put $a $b").Puts("2", vals.MakeList("3", "foo")), That("a @b c = 1 2 3 4; put $a $b $c"). Puts("1", vals.MakeList("2", "3"), "4"), That("a @b c = 1 2; put $a $b $c").Puts("1", vals.EmptyList, "2"), That("@a = ; put $a").Puts(vals.EmptyList), // Unsupported LHS expressions That("a'b' = foo").DoesNotCompile(), That("@a @b = foo").DoesNotCompile(), That("{a b}[idx] = foo").DoesNotCompile(), That("[] = foo").DoesNotCompile(), // List element assignment That("var li = [foo bar]; set li[0] = 233; put $@li").Puts("233", "bar"), // Variable in list assignment must already be defined. Regression test // for b.elv.sh/889. That("set foobarlorem[0] = a").DoesNotCompile(), // Map element assignment That("var di = [&k=v]; set di[k] = lorem; set di[k2] = ipsum", "put $di[k] $di[k2]").Puts("lorem", "ipsum"), That("var d = [&a=[&b=v]]; put $d[a][b]; set d[a][b] = u; put $d[a][b]"). Puts("v", "u"), That("var li = [foo]; set li[(fail foo)] = bar").Throws(FailError{"foo"}), That("var li = [foo]; set li[0 1] = foo bar"). Throws(ErrorWithMessage("multi indexing not implemented")), That("var li = [[]]; set li[1][2] = bar"). Throws(errs.OutOfRange{What: "index", ValidLow: "0", ValidHigh: "0", Actual: "1"}, "li[1][2]"), // Temporary assignment. That("var a b = alice bob; {a,@b}=(put amy ben) put $a $@b; put $a $b"). Puts("amy", "ben", "alice", "bob"), // Temporary assignment of list element. That("l = [a]; l[0]=x put $l[0]; put $l[0]").Puts("x", "a"), // Temporary assignment of map element. That("m = [&k=v]; m[k]=v2 put $m[k]; put $m[k]").Puts("v2", "v"), // Temporary assignment before special form. That("li=[foo bar] for x $li { put $x }").Puts("foo", "bar"), // Multiple LHSs in temporary assignments. That("{a b}={foo bar} put $a $b").Puts("foo", "bar"), That("@a=(put a b) put $@a").Puts("a", "b"), That("{a,@b}=(put a b c) put $@b").Puts("b", "c"), // Spacey assignment with temporary assignment That("x = 1; x=2 y = (+ 1 $x); put $x $y").Puts("1", 3), // Using syntax of temporary assignment for non-temporary assignment no // longer compiles That("x=y").DoesNotCompile(), // Concurrently creating a new variable and accessing existing variable. // Run with "go test -race". That("x = 1", "put $x | y = (all)").DoesNothing(), That("nop (x = 1) | nop").DoesNothing(), // Assignment errors when the RHS errors. That("x = [][1]").Throws(ErrorWithType(errs.OutOfRange{}), "[][1]"), // Assignment to read-only var is a compile-time error. That("nil = 1").DoesNotCompile(), That("a true b = 1 2 3").DoesNotCompile(), That("@true = 1").DoesNotCompile(), That("true @r = 1").DoesNotCompile(), That("@r true = 1").DoesNotCompile(), // A readonly var as a target for the "except" clause is also a // compile-time error. That("try { fail reason } except nil { }").DoesNotCompile(), That("try { fail reason } except x { }").DoesNothing(), // Arity mismatch. That("x = 1 2").Throws( errs.ArityMismatch{What: "assignment right-hand-side", ValidLow: 1, ValidHigh: 1, Actual: 2}, "x = 1 2"), That("x y = 1").Throws( errs.ArityMismatch{What: "assignment right-hand-side", ValidLow: 2, ValidHigh: 2, Actual: 1}, "x y = 1"), That("x y @z = 1").Throws( errs.ArityMismatch{What: "assignment right-hand-side", ValidLow: 2, ValidHigh: -1, Actual: 1}, "x y @z = 1"), // Trying to add a new name in a namespace throws an exception. // Regression test for #1214. That("ns: = (ns [&]); ns:a = b").Throws(NoSuchVariable("ns:a"), "ns:a = b"), ) } func TestCommand_LegacyAssignmentIsDeprecated(t *testing.T) { testCompileTimeDeprecation(t, "a = foo", "legacy assignment form is deprecated", 17) } func TestCommand_DeprecatedSpecialNamespacesInAssignment(t *testing.T) { testCompileTimeDeprecation(t, "var local:a = foo", "the local: special namespace is deprecated", 17) testCompileTimeDeprecation(t, "var a; set local:a = foo", "the local: special namespace is deprecated", 17) testCompileTimeDeprecation(t, "var a; { set up:a = foo }", "the up: special namespace is deprecated", 17) testCompileTimeDeprecation(t, "var :a = foo", "the empty namespace is deprecated", 17) testCompileTimeDeprecation(t, "var a; { set :a = foo }", "the empty namespace is deprecated", 17) } func TestCommand_Redir(t *testing.T) { setup := func(ev *Evaler) { ev.ExtendGlobal(BuildNs().AddNs("file", file.Ns)) } testutil.InTempDir(t) TestWithSetup(t, setup, // Output and input redirection. That("echo 233 > out1", " slurp < out1").Puts("233\n"), // Append. That("echo 1 > out; echo 2 >> out; slurp < out").Puts("1\n2\n"), // Read and write. // TODO: Add a meaningful use case that uses both read and write. That("echo 233 <> out1", " slurp < out1").Puts("233\n"), // Redirections from special form. That(`for x [lorem ipsum] { echo $x } > out2`, `slurp < out2`). Puts("lorem\nipsum\n"), // Using numeric FDs as source and destination. That(`{ echo foobar >&2 } 2> out3`, `slurp < out3`). Puts("foobar\n"), // Using named FDs as source and destination. That("echo 233 stdout> out1", " slurp stdin< out1").Puts("233\n"), That(`{ echo foobar >&stderr } stderr> out4`, `slurp < out4`). Puts("foobar\n"), // Using a new FD as source throws an exception. That(`echo foo >&4`).Throws(AnyError), // Using a new FD as destination is OK, and makes it available. That(`{ echo foo >&4 } 4>out5`, `slurp < out5`).Puts("foo\n"), // Redirections from File object. That(`echo haha > out3`, `f = (file:open out3)`, `slurp <$f`, ` file:close $f`). Puts("haha\n"), // Redirections from Pipe object. That(`p = (file:pipe); echo haha > $p; file:close $p[w]; slurp < $p; file:close $p[r]`). Puts("haha\n"), // We can't read values from a file and shouldn't hang when iterating // over input from a file. // Regression test for https://src.elv.sh/issues/1010 That("echo abc > bytes", "each $echo~ < bytes").Prints("abc\n"), That("echo def > bytes", "only-values < bytes | count").Puts(0), // Writing value output to file throws an exception. That("put foo >a").Throws(ErrNoValueOutput, "put foo >a"), // Writing value output to closed port throws an exception too. That("put foo >&-").Throws(ErrNoValueOutput, "put foo >&-"), // Invalid redirection destination. That("echo []> test").Throws( errs.BadValue{ What: "redirection destination", Valid: "fd name or number", Actual: "[]"}, "[]"), // Invalid fd redirection source. That("echo >&test").Throws( errs.BadValue{ What: "redirection source", Valid: "fd name or number or '-'", Actual: "test"}, "test"), // Invalid redirection source. That("echo > []").Throws( errs.BadValue{ What: "redirection source", Valid: "string, file or pipe", Actual: "list"}, "[]"), // Exception when evaluating source or destination. That("echo > (fail foo)").Throws(FailError{"foo"}, "fail foo"), That("echo (fail foo)> file").Throws(FailError{"foo"}, "fail foo"), ) } func TestCommand_Stacktrace(t *testing.T) { oops := ErrorWithMessage("oops") Test(t, // Stack traces. That("fail oops").Throws(oops, "fail oops"), That("fn f { fail oops }", "f").Throws(oops, "fail oops ", "f"), That("fn f { fail oops }", "fn g { f }", "g").Throws( oops, "fail oops ", "f ", "g"), // Error thrown before execution. That("fn f { }", "f a").Throws(ErrorWithType(errs.ArityMismatch{}), "f a"), // Error from builtin. That("count 1 2 3").Throws( ErrorWithType(errs.ArityMismatch{}), "count 1 2 3"), ) } elvish-0.17.0/pkg/eval/compile_effect_unix_test.go000066400000000000000000000044771415471104000222070ustar00rootroot00000000000000//go:build !windows && !plan9 // +build !windows,!plan9 package eval_test import ( "os" "strings" "testing" "src.elv.sh/pkg/eval/errs" . "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/testutil" ) func TestPipeline_ReaderGone_Unix(t *testing.T) { Test(t, // External commands terminated by SIGPIPE due to reader exiting early // raise ReaderGone, which is then suppressed. That("yes | true").DoesNothing(), That( "var reached = $false", "{ yes; reached = $true } | true", "put $reached", ).Puts(false), ) } func TestCommand_External(t *testing.T) { d := testutil.InTempDir(t) mustWriteScript("foo", "#!/bin/sh", "echo foo") mustWriteScript("lorem/ipsum", "#!/bin/sh", "echo lorem ipsum") testutil.Setenv(t, "PATH", d+"/bin") mustWriteScript("bin/hello", "#!/bin/sh", "echo hello") Test(t, // External commands, searched and relative That("hello").Prints("hello\n"), That("./foo").Prints("foo\n"), That("lorem/ipsum").Prints("lorem ipsum\n"), // Using the explicit e: namespace. That("e:hello").Prints("hello\n"), That("e:./foo").Prints("foo\n"), // Relative external commands may be a dynamic string. That("var x = ipsum", "lorem/$x").Prints("lorem ipsum\n"), // Searched external commands may not be a dynamic string. That("var x = hello; $x").Throws( errs.BadValue{What: "command", Valid: "callable or string containing slash", Actual: "hello"}, "$x"), // Using new FD as destination in external commands. // Regression test against b.elv.sh/788. That("./foo 5 0 { cp.errorpf(n, "braced list may not have indices when used as lvalue") } return cp.parseCompoundLValues(n.Head.Braced, f) } // A basic lvalue. if !parse.ValidLHSVariable(n.Head, true) { cp.errorpf(n.Head, "lvalue must be valid literal variable names") } varUse := n.Head.Value sigil, qname := SplitSigil(varUse) var ref *varRef if f&setLValue != 0 { ref = resolveVarRef(cp, qname, n) if ref != nil && len(ref.subNames) == 0 && ref.info.readOnly { cp.errorpf(n, "variable $%s is read-only", qname) } } if ref == nil { if f&newLValue == 0 { cp.errorpf(n, "cannot find variable $%s", qname) } if len(n.Indices) > 0 { cp.errorpf(n, "name for new variable must not have indices") } segs := SplitQNameSegs(qname) if len(segs) == 1 { // Unqualified name - implicit local name := segs[0] ref = &varRef{localScope, staticVarInfo{name, false, false}, cp.thisScope().add(name), nil} } else if len(segs) == 2 && (segs[0] == "local:" || segs[0] == ":") { if segs[0] == "local:" { cp.deprecate(n, "the local: special namespace is deprecated; use the variable directly", 17) } else { cp.deprecate(n, "the empty namespace is deprecated; use the variable directly", 17) } name := segs[1] // Qualified local name ref = &varRef{localScope, staticVarInfo{name, false, false}, cp.thisScope().add(name), nil} } else { cp.errorpf(n, "cannot create variable $%s; new variables can only be created in the local scope", qname) } } ends := make([]int, len(n.Indices)+1) ends[0] = n.Head.Range().To for i, idx := range n.Indices { ends[i+1] = idx.Range().To } lv := lvalue{n.Range(), ref, cp.arrayOps(n.Indices), ends} restIndex := -1 if sigil == "@" { restIndex = 0 } // TODO: Support % (and other sigils?) if https://b.elv.sh/584 is implemented for map explosion. return lvaluesGroup{[]lvalue{lv}, restIndex} } type assignOp struct { diag.Ranging lhs lvaluesGroup rhs valuesOp } func (op *assignOp) exec(fm *Frame) Exception { variables := make([]vars.Var, len(op.lhs.lvalues)) for i, lvalue := range op.lhs.lvalues { variable, err := derefLValue(fm, lvalue) if err != nil { return fm.errorp(op, err) } variables[i] = variable } values, exc := op.rhs.exec(fm) if exc != nil { return exc } if op.lhs.rest == -1 { if len(variables) != len(values) { return fm.errorp(op, errs.ArityMismatch{What: "assignment right-hand-side", ValidLow: len(variables), ValidHigh: len(variables), Actual: len(values)}) } for i, variable := range variables { err := variable.Set(values[i]) if err != nil { return fm.errorp(op.lhs.lvalues[i], err) } } } else { if len(values) < len(variables)-1 { return fm.errorp(op, errs.ArityMismatch{What: "assignment right-hand-side", ValidLow: len(variables) - 1, ValidHigh: -1, Actual: len(values)}) } rest := op.lhs.rest for i := 0; i < rest; i++ { err := variables[i].Set(values[i]) if err != nil { return fm.errorp(op.lhs.lvalues[i], err) } } restOff := len(values) - len(variables) err := variables[rest].Set(vals.MakeList(values[rest : rest+restOff+1]...)) if err != nil { return fm.errorp(op.lhs.lvalues[rest], err) } for i := rest + 1; i < len(variables); i++ { err := variables[i].Set(values[i+restOff]) if err != nil { return fm.errorp(op.lhs.lvalues[i], err) } } } return nil } // NoSuchVariable returns an error representing that a variable can't be found. func NoSuchVariable(name string) error { return noSuchVariableError{name} } type noSuchVariableError struct{ name string } func (er noSuchVariableError) Error() string { return "no variable $" + er.name } func derefLValue(fm *Frame, lv lvalue) (vars.Var, error) { variable := deref(fm, lv.ref) if variable == nil { return nil, NoSuchVariable(fm.srcMeta.Code[lv.From:lv.To]) } if len(lv.indexOps) == 0 { return variable, nil } indices := make([]interface{}, len(lv.indexOps)) for i, op := range lv.indexOps { values, exc := op.exec(fm) if exc != nil { return nil, exc } // TODO: Implement multi-indexing. if len(values) != 1 { return nil, errors.New("multi indexing not implemented") } indices[i] = values[0] } elemVar, err := vars.MakeElement(variable, indices) if err != nil { level := vars.ElementErrorLevel(err) if level < 0 { return nil, fm.errorp(lv, err) } return nil, fm.errorp(diag.Ranging{From: lv.From, To: lv.ends[level]}, err) } return elemVar, nil } elvish-0.17.0/pkg/eval/compile_value.go000066400000000000000000000327361415471104000177640ustar00rootroot00000000000000package eval import ( "errors" "fmt" "strings" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" "src.elv.sh/pkg/fsutil" "src.elv.sh/pkg/glob" "src.elv.sh/pkg/parse" ) // An operation that produces values. type valuesOp interface { diag.Ranger exec(*Frame) ([]interface{}, Exception) } var outputCaptureBufferSize = 16 func (cp *compiler) compoundOp(n *parse.Compound) valuesOp { if len(n.Indexings) == 0 { return literalValues(n, "") } tilde := false indexings := n.Indexings if n.Indexings[0].Head.Type == parse.Tilde { // A lone ~. if len(n.Indexings) == 1 { return loneTildeOp{n.Range()} } tilde = true indexings = indexings[1:] } return compoundOp{n.Range(), tilde, cp.indexingOps(indexings)} } type loneTildeOp struct{ diag.Ranging } func (op loneTildeOp) exec(fm *Frame) ([]interface{}, Exception) { home, err := fsutil.GetHome("") if err != nil { return nil, fm.errorp(op, err) } return []interface{}{home}, nil } func (cp *compiler) compoundOps(ns []*parse.Compound) []valuesOp { ops := make([]valuesOp, len(ns)) for i, n := range ns { ops[i] = cp.compoundOp(n) } return ops } type compoundOp struct { diag.Ranging tilde bool subops []valuesOp } func (op compoundOp) exec(fm *Frame) ([]interface{}, Exception) { // Accumulator. vs, exc := op.subops[0].exec(fm) if exc != nil { return nil, exc } for _, subop := range op.subops[1:] { us, exc := subop.exec(fm) if exc != nil { return nil, exc } var err error vs, err = outerProduct(vs, us, vals.Concat) if err != nil { return nil, fm.errorp(op, err) } } if op.tilde { newvs := make([]interface{}, len(vs)) for i, v := range vs { tilded, err := doTilde(v) if err != nil { return nil, fm.errorp(op, err) } newvs[i] = tilded } vs = newvs } hasGlob := false for _, v := range vs { if _, ok := v.(globPattern); ok { hasGlob = true break } } if hasGlob { newvs := make([]interface{}, 0, len(vs)) for _, v := range vs { if gp, ok := v.(globPattern); ok { results, err := doGlob(gp, fm.Interrupts()) if err != nil { return nil, fm.errorp(op, err) } newvs = append(newvs, results...) } else { newvs = append(newvs, v) } } vs = newvs } return vs, nil } func outerProduct(vs []interface{}, us []interface{}, f func(interface{}, interface{}) (interface{}, error)) ([]interface{}, error) { ws := make([]interface{}, len(vs)*len(us)) nu := len(us) for i, v := range vs { for j, u := range us { var err error ws[i*nu+j], err = f(v, u) if err != nil { return nil, err } } } return ws, nil } // Errors thrown when globbing. var ( ErrBadglobPattern = errors.New("bad globPattern; elvish bug") ErrCannotDetermineUsername = errors.New("cannot determine user name from glob pattern") ) func doTilde(v interface{}) (interface{}, error) { switch v := v.(type) { case string: s := v // TODO: Make this correct on Windows. i := strings.Index(s, "/") var uname, rest string if i == -1 { uname = s } else { uname = s[:i] rest = s[i:] } dir, err := fsutil.GetHome(uname) if err != nil { return nil, err } return dir + rest, nil case globPattern: if len(v.Segments) == 0 { return nil, ErrBadglobPattern } switch seg := v.Segments[0].(type) { case glob.Literal: if len(v.Segments) == 1 { return nil, ErrBadglobPattern } _, isSlash := v.Segments[1].(glob.Slash) if isSlash { // ~username or ~username/xxx. Replace the first segment with // the home directory of the specified user. dir, err := fsutil.GetHome(seg.Data) if err != nil { return nil, err } v.Segments[0] = glob.Literal{Data: dir} return v, nil } case glob.Slash: dir, err := fsutil.GetHome("") if err != nil { return nil, err } v.DirOverride = dir return v, nil } return nil, ErrCannotDetermineUsername default: return nil, fmt.Errorf("tilde doesn't work on value of type %s", vals.Kind(v)) } } func (cp *compiler) arrayOp(n *parse.Array) valuesOp { return seqValuesOp{n.Range(), cp.compoundOps(n.Compounds)} } func (cp *compiler) arrayOps(ns []*parse.Array) []valuesOp { ops := make([]valuesOp, len(ns)) for i, n := range ns { ops[i] = cp.arrayOp(n) } return ops } func (cp *compiler) indexingOp(n *parse.Indexing) valuesOp { if len(n.Indices) == 0 { return cp.primaryOp(n.Head) } return &indexingOp{n.Range(), cp.primaryOp(n.Head), cp.arrayOps(n.Indices)} } func (cp *compiler) indexingOps(ns []*parse.Indexing) []valuesOp { ops := make([]valuesOp, len(ns)) for i, n := range ns { ops[i] = cp.indexingOp(n) } return ops } type indexingOp struct { diag.Ranging headOp valuesOp indexOps []valuesOp } func (op *indexingOp) exec(fm *Frame) ([]interface{}, Exception) { vs, exc := op.headOp.exec(fm) if exc != nil { return nil, exc } for _, indexOp := range op.indexOps { indices, exc := indexOp.exec(fm) if exc != nil { return nil, exc } newvs := make([]interface{}, 0, len(vs)*len(indices)) for _, v := range vs { for _, index := range indices { result, err := vals.Index(v, index) if err != nil { return nil, fm.errorp(op, err) } newvs = append(newvs, result) } } vs = newvs } return vs, nil } func (cp *compiler) primaryOp(n *parse.Primary) valuesOp { switch n.Type { case parse.Bareword, parse.SingleQuoted, parse.DoubleQuoted: return literalValues(n, n.Value) case parse.Variable: sigil, qname := SplitSigil(n.Value) ref := resolveVarRef(cp, qname, n) if ref == nil { cp.errorpf(n, "variable $%s not found", qname) } return &variableOp{n.Range(), sigil != "", qname, ref} case parse.Wildcard: seg, err := wildcardToSegment(parse.SourceText(n)) if err != nil { cp.errorpf(n, "%s", err) } vs := []interface{}{ globPattern{Pattern: glob.Pattern{Segments: []glob.Segment{seg}, DirOverride: ""}, Flags: 0, Buts: nil, TypeCb: nil}} return literalValues(n, vs...) case parse.Tilde: cp.errorpf(n, "compiler bug: Tilde not handled in .compound") return literalValues(n, "~") case parse.ExceptionCapture: return exceptionCaptureOp{n.Range(), cp.chunkOp(n.Chunk)} case parse.OutputCapture: return outputCaptureOp{n.Range(), cp.chunkOp(n.Chunk)} case parse.List: return listOp{n.Range(), cp.compoundOps(n.Elements)} case parse.Lambda: return cp.lambda(n) case parse.Map: return mapOp{n.Range(), cp.mapPairs(n.MapPairs)} case parse.Braced: return seqValuesOp{n.Range(), cp.compoundOps(n.Braced)} default: cp.errorpf(n, "bad PrimaryType; parser bug") return literalValues(n, parse.SourceText(n)) } } func (cp *compiler) primaryOps(ns []*parse.Primary) []valuesOp { ops := make([]valuesOp, len(ns)) for i, n := range ns { ops[i] = cp.primaryOp(n) } return ops } type variableOp struct { diag.Ranging explode bool qname string ref *varRef } func (op variableOp) exec(fm *Frame) ([]interface{}, Exception) { variable := deref(fm, op.ref) if variable == nil { return nil, fm.errorpf(op, "variable $%s not found", op.qname) } value := variable.Get() if op.explode { vs, err := vals.Collect(value) return vs, fm.errorp(op, err) } return []interface{}{value}, nil } type listOp struct { diag.Ranging subops []valuesOp } func (op listOp) exec(fm *Frame) ([]interface{}, Exception) { list := vals.EmptyList for _, subop := range op.subops { moreValues, exc := subop.exec(fm) if exc != nil { return nil, exc } for _, moreValue := range moreValues { list = list.Cons(moreValue) } } return []interface{}{list}, nil } type exceptionCaptureOp struct { diag.Ranging subop effectOp } func (op exceptionCaptureOp) exec(fm *Frame) ([]interface{}, Exception) { exc := op.subop.exec(fm) if exc == nil { return []interface{}{OK}, nil } return []interface{}{exc}, nil } type outputCaptureOp struct { diag.Ranging subop effectOp } func (op outputCaptureOp) exec(fm *Frame) ([]interface{}, Exception) { outPort, collect, err := CapturePort() if err != nil { return nil, fm.errorp(op, err) } exc := op.subop.exec(fm.forkWithOutput("[output capture]", outPort)) return collect(), exc } func (cp *compiler) lambda(n *parse.Primary) valuesOp { if n.LegacyLambda { cp.deprecate(n, "legacy lambda syntax is deprecated; migrate scripts with https://go.elv.sh/u0.17", 17) } // Parse signature. var ( argNames []string restArg int = -1 optNames []string optDefaultOps []valuesOp ) if len(n.Elements) > 0 { // Argument list. argNames = make([]string, len(n.Elements)) for i, arg := range n.Elements { ref := stringLiteralOrError(cp, arg, "argument name") sigil, qname := SplitSigil(ref) name, rest := SplitQName(qname) if rest != "" { cp.errorpf(arg, "argument name must be unqualified") } if name == "" { cp.errorpf(arg, "argument name must not be empty") } if sigil == "@" { if restArg != -1 { cp.errorpf(arg, "only one argument may have @") } restArg = i } argNames[i] = name } } if len(n.MapPairs) > 0 { optNames = make([]string, len(n.MapPairs)) optDefaultOps = make([]valuesOp, len(n.MapPairs)) for i, opt := range n.MapPairs { qname := stringLiteralOrError(cp, opt.Key, "option name") name, rest := SplitQName(qname) if rest != "" { cp.errorpf(opt.Key, "option name must be unqualified") } if name == "" { cp.errorpf(opt.Key, "option name must not be empty") } optNames[i] = name if opt.Value == nil { cp.errorpf(opt.Key, "option must have default value") } else { optDefaultOps[i] = cp.compoundOp(opt.Value) } } } local, capture := cp.pushScope() for _, argName := range argNames { local.add(argName) } for _, optName := range optNames { local.add(optName) } scopeSizeInit := len(local.infos) chunkOp := cp.chunkOp(n.Chunk) newLocal := local.infos[scopeSizeInit:] cp.popScope() return &lambdaOp{n.Range(), argNames, restArg, optNames, optDefaultOps, newLocal, capture, chunkOp, cp.srcMeta} } type lambdaOp struct { diag.Ranging argNames []string restArg int optNames []string optDefaultOps []valuesOp newLocal []staticVarInfo capture *staticUpNs subop effectOp srcMeta parse.Source } func (op *lambdaOp) exec(fm *Frame) ([]interface{}, Exception) { capture := &Ns{ make([]vars.Var, len(op.capture.infos)), make([]staticVarInfo, len(op.capture.infos))} for i, info := range op.capture.infos { if info.local { capture.slots[i] = fm.local.slots[info.index] capture.infos[i] = fm.local.infos[info.index] } else { capture.slots[i] = fm.up.slots[info.index] capture.infos[i] = fm.up.infos[info.index] } } optDefaults := make([]interface{}, len(op.optDefaultOps)) for i, op := range op.optDefaultOps { defaultValue, err := evalForValue(fm, op, "option default value") if err != nil { return nil, err } optDefaults[i] = defaultValue } return []interface{}{&closure{op.argNames, op.restArg, op.optNames, optDefaults, op.subop, op.newLocal, capture, op.srcMeta, op.Range()}}, nil } type mapOp struct { diag.Ranging pairsOp *mapPairsOp } func (op mapOp) exec(fm *Frame) ([]interface{}, Exception) { m := vals.EmptyMap exc := op.pairsOp.exec(fm, func(k, v interface{}) Exception { m = m.Assoc(k, v) return nil }) if exc != nil { return nil, exc } return []interface{}{m}, nil } func (cp *compiler) mapPairs(pairs []*parse.MapPair) *mapPairsOp { npairs := len(pairs) keysOps := make([]valuesOp, npairs) valuesOps := make([]valuesOp, npairs) begins, ends := make([]int, npairs), make([]int, npairs) for i, pair := range pairs { keysOps[i] = cp.compoundOp(pair.Key) if pair.Value == nil { p := pair.Range().To valuesOps[i] = literalValues(diag.PointRanging(p), true) } else { valuesOps[i] = cp.compoundOp(pairs[i].Value) } begins[i], ends[i] = pair.Range().From, pair.Range().To } return &mapPairsOp{keysOps, valuesOps, begins, ends} } type mapPairsOp struct { keysOps []valuesOp valuesOps []valuesOp begins []int ends []int } func (op *mapPairsOp) exec(fm *Frame, f func(k, v interface{}) Exception) Exception { for i := range op.keysOps { keys, exc := op.keysOps[i].exec(fm) if exc != nil { return exc } values, exc := op.valuesOps[i].exec(fm) if exc != nil { return exc } if len(keys) != len(values) { return fm.errorpf(diag.Ranging{From: op.begins[i], To: op.ends[i]}, "%d keys but %d values", len(keys), len(values)) } for j, key := range keys { err := f(key, values[j]) if err != nil { return err } } } return nil } type literalValuesOp struct { diag.Ranging values []interface{} } func (op literalValuesOp) exec(*Frame) ([]interface{}, Exception) { return op.values, nil } func literalValues(r diag.Ranger, vs ...interface{}) valuesOp { return literalValuesOp{r.Range(), vs} } type seqValuesOp struct { diag.Ranging subops []valuesOp } func (op seqValuesOp) exec(fm *Frame) ([]interface{}, Exception) { var values []interface{} for _, subop := range op.subops { moreValues, exc := subop.exec(fm) if exc != nil { return nil, exc } values = append(values, moreValues...) } return values, nil } type nopValuesOp struct{ diag.Ranging } func (nopValuesOp) exec(fm *Frame) ([]interface{}, Exception) { return nil, nil } func evalForValue(fm *Frame, op valuesOp, what string) (interface{}, Exception) { values, exc := op.exec(fm) if exc != nil { return nil, exc } if len(values) != 1 { return nil, fm.errorp(op, errs.ArityMismatch{What: what, ValidLow: 1, ValidHigh: 1, Actual: len(values)}) } return values[0], nil } elvish-0.17.0/pkg/eval/compile_value_test.go000066400000000000000000000207001415471104000210070ustar00rootroot00000000000000package eval_test import ( "testing" . "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/errs" . "src.elv.sh/pkg/testutil" . "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/eval/vals" ) func TestCompound(t *testing.T) { Test(t, That("put {fi,elvi}sh{1.0,1.1}").Puts( "fish1.0", "fish1.1", "elvish1.0", "elvish1.1"), // As a special case, an empty compound expression evaluates to an empty // string. That("put {}").Puts(""), That("put [&k=][k]").Puts(""), // TODO: Test the case where fsutil.GetHome returns an error. // Error in any of the components throws an exception. That("put a{[][1]}").Throws(ErrorWithType(errs.OutOfRange{}), "[][1]"), // Error in concatenating the values throws an exception. That("put []a").Throws(ErrorWithMessage("cannot concatenate list and string")), // Error when applying tilde throws an exception. That("put ~[]").Throws(ErrorWithMessage("tilde doesn't work on value of type list")), ) } func TestIndexing(t *testing.T) { Test(t, That("put [a b c][2]").Puts("c"), That("put [][0]").Throws(ErrorWithType(errs.OutOfRange{}), "[][0]"), That("put [&key=value][key]").Puts("value"), That("put [&key=value][bad]").Throws( vals.NoSuchKey("bad"), "[&key=value][bad]"), That("put (fail x)[a]").Throws(FailError{"x"}, "fail x"), That("put [foo][(fail x)]").Throws(FailError{"x"}, "fail x"), ) } func TestListLiteral(t *testing.T) { Test(t, That("put [a b c]").Puts(vals.MakeList("a", "b", "c")), That("put []").Puts(vals.EmptyList), // List expression errors if an element expression errors. That("put [ [][0] ]").Throws(ErrorWithType(errs.OutOfRange{}), "[][0]"), ) } func TestMapLiteral(t *testing.T) { Test(t, That("put [&key=value]").Puts(vals.MakeMap("key", "value")), That("put [&]").Puts(vals.EmptyMap), // Map keys and values may evaluate to multiple values as long as their // numbers match. That("put [&{a b}={foo bar}]").Puts(vals.MakeMap("a", "foo", "b", "bar")), // Map expression errors if a key or value expression errors. That("put [ &[][0]=a ]").Throws(ErrorWithType(errs.OutOfRange{}), "[][0]"), That("put [ &a=[][0] ]").Throws(ErrorWithType(errs.OutOfRange{}), "[][0]"), // Map expression errors if number of keys and values in a single pair // does not match. That("put [&{a b}={foo bar lorem}]").Throws(ErrorWithMessage("2 keys but 3 values")), ) } func TestStringLiteral(t *testing.T) { Test(t, That(`put 'such \"''literal'`).Puts(`such \"'literal`), That(`put "much \n\033[31;1m$cool\033[m"`). Puts("much \n\033[31;1m$cool\033[m"), ) } func TestTilde(t *testing.T) { home := InTempHome(t) ApplyDir(Dir{"file1": "", "file2": ""}) Test(t, // Tilde // ----- That("put ~").Puts(home), That("put ~/src").Puts(home+"/src"), // Make sure that tilde processing retains trailing slashes. That("put ~/src/").Puts(home+"/src/"), // Tilde and wildcard. That("put ~/*").Puts(home+"/file1", home+"/file2"), // TODO: Add regression test for #793. // TODO: Add regression test for #1246. ) } func TestWildcard(t *testing.T) { Test(t, That("put ***").DoesNotCompile(), ) // More tests in glob_test.go } func TestOutputCapture(t *testing.T) { Test(t, // Output capture That("put (put lorem ipsum)").Puts("lorem", "ipsum"), That("put (print \"lorem\nipsum\")").Puts("lorem", "ipsum"), // \r\n is also supported as a line separator That(`print "lorem\r\nipsum\r\n" | all`).Puts("lorem", "ipsum"), ) } func TestExceptionCapture(t *testing.T) { Test(t, // Exception capture That("bool ?(nop); bool ?(e:false)").Puts(true, false), ) } func TestVariableUse(t *testing.T) { Test(t, That("x = foo", "put $x").Puts("foo"), // Must exist before use That("put $x").DoesNotCompile(), That("put $x[0]").DoesNotCompile(), // Compounding That("x = SHELL", "put 'WOW, SUCH '$x', MUCH COOL'\n"). Puts("WOW, SUCH SHELL, MUCH COOL"), // Splicing That("x = [elvish rules]", "put $@x").Puts("elvish", "rules"), // Variable namespace // ------------------ // Pseudo-namespace local: accesses the local scope. That("x = outer; { local:x = inner; put $local:x }").Puts("inner"), // Pseudo-namespace up: accesses upvalues. That("x = outer; { local:x = inner; put $up:x }").Puts("outer"), // Unqualified name prefers local: to up:. That("x = outer; { local:x = inner; put $x }").Puts("inner"), // Unqualified name resolves to upvalue if no local name exists. That("x = outer; { put $x }").Puts("outer"), // Unqualified name resolves to builtin if no local name or upvalue // exists. That("put $true").Puts(true), // A name can be explicitly unqualified by having a leading colon. That("x = val; put $:x").Puts("val"), That("put $:true").Puts(true), // Pseudo-namespace E: provides read-write access to environment // variables. Colons inside the name are supported. That("set-env a:b VAL; put $E:a:b").Puts("VAL"), That("E:a:b = VAL2; get-env a:b").Puts("VAL2"), // Pseudo-namespace e: provides readonly access to external commands. // Only names ending in ~ are resolved, and resolution always succeeds // regardless of whether the command actually exists. Colons inside the // name are supported. That("put $e:a:b~").Puts(NewExternalCmd("a:b")), // A "normal" namespace access indexes the namespace as a variable. That("ns: = (ns [&a= val]); put $ns:a").Puts("val"), // Multi-level namespace access is supported. That("ns: = (ns [&a:= (ns [&b= val])]); put $ns:a:b").Puts("val"), // Multi-level namespace access can have a leading colon to signal that // the first component is unqualified. That("ns: = (ns [&a:= (ns [&b= val])]); put $:ns:a:b").Puts("val"), // Multi-level namespace access can be combined with the local: // pseudo-namespaces. That("ns: = (ns [&a:= (ns [&b= val])]); put $local:ns:a:b").Puts("val"), // Multi-level namespace access can be combined with the up: // pseudo-namespaces. That("ns: = (ns [&a:= (ns [&b= val])]); { put $up:ns:a:b }").Puts("val"), ) } func TestVariableUse_DeprecatedSpecialNamespaces(t *testing.T) { testCompileTimeDeprecation(t, "var a; put $local:a", "the local: special namespace is deprecated", 17) testCompileTimeDeprecation(t, "var a; { put $up:a }", "the up: special namespace is deprecated", 17) testCompileTimeDeprecation(t, "var a; { put $:a }", "the empty namespace is deprecated", 17) } func TestClosure(t *testing.T) { Test(t, That("{|| }").DoesNothing(), That("{|x| put $x} foo").Puts("foo"), // Assigning to captured variable That("var x = lorem; {|| set x = ipsum}; put $x").Puts("ipsum"), That("var x = lorem; {|| put $x; set x = ipsum }; put $x"). Puts("lorem", "ipsum"), // Assigning to element of captured variable That("x = a; { x = b }; put $x").Puts("b"), That("x = [a]; { x[0] = b }; put $x[0]").Puts("b"), // Shadowing That("var x = ipsum; { var x = lorem; put $x }; put $x"). Puts("lorem", "ipsum"), // Shadowing by argument That("var x = ipsum; {|x| put $x; set x = BAD } lorem; put $x"). Puts("lorem", "ipsum"), // Closure captures new local variables every time That("fn f { var x = (num 0); put { set x = (+ $x 1) } { put $x } }", "var inc1 put1 = (f); $put1; $inc1; $put1", "var inc2 put2 = (f); $put2; $inc2; $put2").Puts(0, 1, 0, 1), // Rest argument. That("{|x @xs| put $x $xs } a b c").Puts("a", vals.MakeList("b", "c")), That("{|a @b c| put $a $b $c } a b c d"). Puts("a", vals.MakeList("b", "c"), "d"), // Options. That("{|a &k=v| put $a $k } foo &k=bar").Puts("foo", "bar"), // Option default value. That("{|a &k=v| put $a $k } foo").Puts("foo", "v"), // Option must have default value That("{|&k| }").DoesNotCompile(), // Exception when evaluating option default value. That("{|&a=[][0]| }").Throws(ErrorWithType(errs.OutOfRange{}), "[][0]"), // Option default value must be one value. That("{|&a=(put foo bar)| }").Throws( errs.ArityMismatch{What: "option default value", ValidLow: 1, ValidHigh: 1, Actual: 2}, "(put foo bar)"), // Argument name must be unqualified. That("{|a:b| }").DoesNotCompile(), // Argument name must not be empty. That("{|''| }").DoesNotCompile(), That("{|@| }").DoesNotCompile(), // Option name must be unqualified. That("{|&a:b=1| }").DoesNotCompile(), // Option name must not be empty. That("{|&''=b| }").DoesNotCompile(), // Should not have multiple rest arguments. That("{|@a @b| }").DoesNotCompile(), ) } func TestClosure_LegacySyntaxIsDeprecated(t *testing.T) { testCompileTimeDeprecation(t, "a = []{ }", "legacy lambda syntax is deprecated", 17) } elvish-0.17.0/pkg/eval/compiler.go000066400000000000000000000102411415471104000167350ustar00rootroot00000000000000package eval import ( "fmt" "io" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/eval/vars" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/prog" ) // compiler maintains the set of states needed when compiling a single source // file. type compiler struct { // Builtin namespace. builtin *staticNs // Lexical namespaces. scopes []*staticNs // Sources of captured variables. captures []*staticUpNs // Pragmas tied to scopes. pragmas []*scopePragma // Destination of warning messages. This is currently only used for // deprecation messages. warn io.Writer // Deprecation registry. deprecations deprecationRegistry // Information about the source. srcMeta parse.Source } type scopePragma struct { unknownCommandIsExternal bool } func compile(b, g *staticNs, tree parse.Tree, w io.Writer) (op nsOp, err error) { g = g.clone() cp := &compiler{ b, []*staticNs{g}, []*staticUpNs{new(staticUpNs)}, []*scopePragma{{unknownCommandIsExternal: true}}, w, newDeprecationRegistry(), tree.Source} defer func() { r := recover() if r == nil { return } else if e := GetCompilationError(r); e != nil { // Save the compilation error and stop the panic. err = e } else { // Resume the panic; it is not supposed to be handled here. panic(r) } }() chunkOp := cp.chunkOp(tree.Root) return nsOp{chunkOp, g}, nil } type nsOp struct { inner effectOp template *staticNs } // Prepares the local namespace, and returns the namespace and a function for // executing the inner effectOp. Mutates fm.local. func (op nsOp) prepare(fm *Frame) (*Ns, func() Exception) { if len(op.template.infos) > len(fm.local.infos) { n := len(op.template.infos) newLocal := &Ns{make([]vars.Var, n), op.template.infos} copy(newLocal.slots, fm.local.slots) for i := len(fm.local.infos); i < n; i++ { // TODO: Take readOnly into account too newLocal.slots[i] = MakeVarFromName(newLocal.infos[i].name) } fm.local = newLocal } else { // If no new variable has been created, there might still be some // existing variables deleted. fm.local = &Ns{fm.local.slots, op.template.infos} } return fm.local, func() Exception { return op.inner.exec(fm) } } const compilationErrorType = "compilation error" func (cp *compiler) errorpf(r diag.Ranger, format string, args ...interface{}) { // The panic is caught by the recover in compile above. panic(&diag.Error{ Type: compilationErrorType, Message: fmt.Sprintf(format, args...), Context: *diag.NewContext(cp.srcMeta.Name, cp.srcMeta.Code, r)}) } // GetCompilationError returns a *diag.Error if the given value is a compilation // error. Otherwise it returns nil. func GetCompilationError(e interface{}) *diag.Error { if e, ok := e.(*diag.Error); ok && e.Type == compilationErrorType { return e } return nil } func (cp *compiler) thisScope() *staticNs { return cp.scopes[len(cp.scopes)-1] } func (cp *compiler) currentPragma() *scopePragma { return cp.pragmas[len(cp.pragmas)-1] } func (cp *compiler) pushScope() (*staticNs, *staticUpNs) { sc := new(staticNs) up := new(staticUpNs) cp.scopes = append(cp.scopes, sc) cp.captures = append(cp.captures, up) currentPragmaCopy := *cp.currentPragma() cp.pragmas = append(cp.pragmas, ¤tPragmaCopy) return sc, up } func (cp *compiler) popScope() { cp.scopes[len(cp.scopes)-1] = nil cp.scopes = cp.scopes[:len(cp.scopes)-1] cp.captures[len(cp.captures)-1] = nil cp.captures = cp.captures[:len(cp.captures)-1] cp.pragmas[len(cp.pragmas)-1] = nil cp.pragmas = cp.pragmas[:len(cp.pragmas)-1] } func (cp *compiler) checkDeprecatedBuiltin(name string, r diag.Ranger) { msg := "" minLevel := 17 switch name { case "dir-history~": msg = `the "dir-history" command is deprecated; use "store:dirs" instead` default: return } cp.deprecate(r, msg, minLevel) } func (cp *compiler) deprecate(r diag.Ranger, msg string, minLevel int) { if cp.warn == nil || r == nil { return } dep := deprecation{cp.srcMeta.Name, r.Range(), msg} if prog.DeprecationLevel >= minLevel && cp.deprecations.register(dep) { err := diag.Error{ Type: "deprecation", Message: msg, Context: diag.Context{ Name: cp.srcMeta.Name, Source: cp.srcMeta.Code, Ranging: r.Range()}} fmt.Fprintln(cp.warn, err.Show("")) } } elvish-0.17.0/pkg/eval/compiler_test.go000066400000000000000000000016651415471104000200060ustar00rootroot00000000000000package eval_test import ( "bytes" "strings" "testing" . "src.elv.sh/pkg/eval" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/prog/progtest" ) func TestDeprecatedBuiltin(t *testing.T) { testCompileTimeDeprecation(t, "dir-history", `the "dir-history" command is deprecated`, 17) // Deprecations of other builtins are implemented in the same way, so we // don't test them repeatedly } func testCompileTimeDeprecation(t *testing.T, code, wantWarning string, level int) { t.Helper() progtest.SetDeprecationLevel(t, level) ev := NewEvaler() errOutput := new(bytes.Buffer) parseErr, compileErr := ev.Check(parse.Source{Code: code}, errOutput) if parseErr != nil { t.Errorf("got parse err %v", parseErr) } if compileErr != nil { t.Errorf("got compile err %v", compileErr) } warning := errOutput.String() if !strings.Contains(warning, wantWarning) { t.Errorf("got warning %q, want warning containing %q", warning, wantWarning) } } elvish-0.17.0/pkg/eval/deprecation.go000066400000000000000000000011161415471104000174210ustar00rootroot00000000000000package eval import ( "src.elv.sh/pkg/diag" ) type deprecationRegistry struct { registered map[deprecation]struct{} } func newDeprecationRegistry() deprecationRegistry { return deprecationRegistry{registered: make(map[deprecation]struct{})} } type deprecation struct { srcName string location diag.Ranging message string } // Registers a deprecation, and returns whether it was registered for the first // time. func (r *deprecationRegistry) register(dep deprecation) bool { if _, ok := r.registered[dep]; ok { return false } r.registered[dep] = struct{}{} return true } elvish-0.17.0/pkg/eval/errs/000077500000000000000000000000001415471104000155515ustar00rootroot00000000000000elvish-0.17.0/pkg/eval/errs/errs.go000066400000000000000000000050631415471104000170570ustar00rootroot00000000000000// Package errs declares error types used as exception causes. package errs import ( "fmt" "strconv" "src.elv.sh/pkg/parse" ) // OutOfRange encodes an error where a value is out of its valid range. type OutOfRange struct { What string ValidLow string ValidHigh string Actual string } // Error implements the error interface. func (e OutOfRange) Error() string { if e.ValidHigh < e.ValidLow { return fmt.Sprintf( "out of range: %v has no valid value, but is %v", e.What, e.Actual) } return fmt.Sprintf( "out of range: %s must be from %s to %s, but is %s", e.What, e.ValidLow, e.ValidHigh, e.Actual) } // BadValue encodes an error where the value does not meet a requirement. For // out-of-range errors, use OutOfRange. type BadValue struct { What string Valid string Actual string } // Error implements the error interface. func (e BadValue) Error() string { return fmt.Sprintf( "bad value: %v must be %v, but is %v", e.What, e.Valid, e.Actual) } // ArityMismatch encodes an error where the expected number of values is out of // the valid range. type ArityMismatch struct { What string ValidLow int ValidHigh int Actual int } func (e ArityMismatch) Error() string { switch { case e.ValidHigh == e.ValidLow: return fmt.Sprintf("arity mismatch: %v must be %v, but is %v", e.What, nValues(e.ValidLow), nValues(e.Actual)) case e.ValidHigh == -1: return fmt.Sprintf("arity mismatch: %v must be %v or more values, but is %v", e.What, e.ValidLow, nValues(e.Actual)) default: return fmt.Sprintf("arity mismatch: %v must be %v to %v values, but is %v", e.What, e.ValidLow, e.ValidHigh, nValues(e.Actual)) } } func nValues(n int) string { if n == 1 { return "1 value" } return strconv.Itoa(n) + " values" } // SetReadOnlyVar is returned by the Set method of a read-only variable. type SetReadOnlyVar struct { // Name of the read-only variable. This field is initially empty, and // populated later when context information is available. VarName string } // Error implements the error interface. func (e SetReadOnlyVar) Error() string { return fmt.Sprintf( "cannot set read-only variable $%s", parse.QuoteVariableName(e.VarName)) } // ReaderGone is raised by the writer in a pipeline when the reader end has // terminated. It could be raised directly by builtin commands, or when an // external command gets terminated by SIGPIPE after Elvish detects the read end // of the pipe has exited earlier. type ReaderGone struct { } // Error implements the error interface. func (e ReaderGone) Error() string { return "reader gone" } elvish-0.17.0/pkg/eval/errs/errs_test.go000066400000000000000000000024531415471104000201160ustar00rootroot00000000000000package errs import ( "testing" ) var errorMessageTests = []struct { err error wantMsg string }{ { OutOfRange{What: "list index here", ValidLow: "0", ValidHigh: "2", Actual: "3"}, "out of range: list index here must be from 0 to 2, but is 3", }, { OutOfRange{What: "list index here", ValidLow: "1", ValidHigh: "0", Actual: "0"}, "out of range: list index here has no valid value, but is 0", }, { BadValue{What: "command", Valid: "callable", Actual: "number"}, "bad value: command must be callable, but is number", }, { ArityMismatch{What: "arguments", ValidLow: 2, ValidHigh: 2, Actual: 3}, "arity mismatch: arguments must be 2 values, but is 3 values", }, { ArityMismatch{What: "arguments", ValidLow: 2, ValidHigh: -1, Actual: 1}, "arity mismatch: arguments must be 2 or more values, but is 1 value", }, { ArityMismatch{What: "arguments", ValidLow: 2, ValidHigh: 3, Actual: 1}, "arity mismatch: arguments must be 2 to 3 values, but is 1 value", }, { SetReadOnlyVar{VarName: "x"}, "cannot set read-only variable $x", }, { ReaderGone{}, "reader gone", }, } func TestErrorMessages(t *testing.T) { for _, test := range errorMessageTests { if gotMsg := test.err.Error(); gotMsg != test.wantMsg { t.Errorf("got message %v, want %v", gotMsg, test.wantMsg) } } } elvish-0.17.0/pkg/eval/eval.go000066400000000000000000000331031415471104000160540ustar00rootroot00000000000000// Package eval handles evaluation of parsed Elvish code and provides runtime // facilities. package eval import ( "fmt" "io" "os" "strconv" "sync" "src.elv.sh/pkg/daemon/daemondefs" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/env" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" "src.elv.sh/pkg/logutil" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/persistent/vector" ) var logger = logutil.GetLogger("[eval] ") const ( // FnSuffix is the suffix for the variable names of functions. Defining a // function "foo" is equivalent to setting a variable named "foo~", and vice // versa. FnSuffix = "~" // NsSuffix is the suffix for the variable names of namespaces. Defining a // namespace foo is equivalent to setting a variable named "foo:", and vice // versa. NsSuffix = ":" ) const ( defaultValuePrefix = "▶ " defaultNotifyBgJobSuccess = true initIndent = vals.NoPretty ) // Evaler provides methods for evaluating code, and maintains state that is // persisted between evaluation of different pieces of code. An Evaler is safe // to use concurrently. type Evaler struct { // The following fields must only be set before the Evaler is used to // evaluate any code; mutating them afterwards may cause race conditions. // Command-line arguments, exposed as $args. Args vals.List // Hooks to run before exit or exec. BeforeExit []func() // Chdir hooks, exposed indirectly as $before-chdir and $after-chdir. BeforeChdir, AfterChdir []func(string) // TODO: Remove after the dir-history command is removed. DaemonClient daemondefs.Client // Directories to search libraries. LibDirs []string // Source code of internal bundled modules indexed by use specs. BundledModules map[string]string // Callback to notify the success or failure of background jobs. Must not be // mutated once the Evaler is used to evaluate any code. BgJobNotify func(string) mu sync.RWMutex // Mutations to fields below must be guarded by mutex. // // Note that this is *not* a GIL; most state mutations when executing Elvish // code is localized and do not need to hold this mutex. // // TODO: Actually guard all mutations by this mutex. global, builtin *Ns deprecations deprecationRegistry // Internal modules are indexed by use specs. External modules are indexed by // absolute paths. modules map[string]*Ns // Various states and configs exposed to Elvish code. // // The prefix to prepend to value outputs when writing them to terminal, // exposed as $value-out-prefix. valuePrefix string // Whether to notify the success of background jobs, exposed as // $notify-bg-job-sucess. notifyBgJobSuccess bool // The current number of background jobs, exposed as $num-bg-jobs. numBgJobs int } //elvdoc:var after-chdir // // A list of functions to run after changing directory. These functions are always // called with directory to change it, which might be a relative path. The // following example also shows `$before-chdir`: // // ```elvish-transcript // ~> before-chdir = [{|dir| echo "Going to change to "$dir", pwd is "$pwd }] // ~> after-chdir = [{|dir| echo "Changed to "$dir", pwd is "$pwd }] // ~> cd /usr // Going to change to /usr, pwd is /Users/xiaq // Changed to /usr, pwd is /usr // /usr> cd local // Going to change to local, pwd is /usr // Changed to local, pwd is /usr/local // /usr/local> // ``` // // @cf before-chdir //elvdoc:var before-chdir // // A list of functions to run before changing directory. These functions are always // called with the new working directory. // // @cf after-chdir //elvdoc:var num-bg-jobs // // Number of background jobs. //elvdoc:var notify-bg-job-success // // Whether to notify success of background jobs, defaulting to `$true`. // // Failures of background jobs are always notified. //elvdoc:var value-out-indicator // // A string put before value outputs (such as those of of `put`). Defaults to // `'▶ '`. Example: // // ```elvish-transcript // ~> put lorem ipsum // ▶ lorem // ▶ ipsum // ~> value-out-indicator = 'val> ' // ~> put lorem ipsum // val> lorem // val> ipsum // ``` // // Note that you almost always want some trailing whitespace for readability. // NewEvaler creates a new Evaler. func NewEvaler() *Evaler { builtin := builtinNs.Ns() beforeChdirElvish, afterChdirElvish := vector.Empty, vector.Empty ev := &Evaler{ global: new(Ns), builtin: builtin, deprecations: newDeprecationRegistry(), modules: make(map[string]*Ns), BundledModules: make(map[string]string), valuePrefix: defaultValuePrefix, notifyBgJobSuccess: defaultNotifyBgJobSuccess, numBgJobs: 0, Args: vals.EmptyList, } ev.BeforeChdir = []func(string){ adaptChdirHook("before-chdir", ev, &beforeChdirElvish)} ev.AfterChdir = []func(string){ adaptChdirHook("after-chdir", ev, &afterChdirElvish)} ev.ExtendBuiltin(BuildNs(). AddVar("pwd", NewPwdVar(ev)). AddVar("before-chdir", vars.FromPtr(&beforeChdirElvish)). AddVar("after-chdir", vars.FromPtr(&afterChdirElvish)). AddVar("value-out-indicator", vars.FromPtrWithMutex(&ev.valuePrefix, &ev.mu)). AddVar("notify-bg-job-success", vars.FromPtrWithMutex(&ev.notifyBgJobSuccess, &ev.mu)). AddVar("num-bg-jobs", vars.FromGet(func() interface{} { return strconv.Itoa(ev.getNumBgJobs()) })). AddVar("args", vars.FromGet(func() interface{} { return ev.Args }))) // Install the "builtin" module after extension is complete. ev.modules["builtin"] = ev.builtin return ev } func adaptChdirHook(name string, ev *Evaler, pfns *vector.Vector) func(string) { return func(path string) { ports, cleanup := PortsFromStdFiles(ev.ValuePrefix()) defer cleanup() callCfg := CallCfg{Args: []interface{}{path}, From: "[hook " + name + "]"} evalCfg := EvalCfg{Ports: ports[:]} for it := (*pfns).Iterator(); it.HasElem(); it.Next() { fn, ok := it.Elem().(Callable) if !ok { fmt.Fprintln(os.Stderr, name, "hook must be callable") continue } err := ev.Call(fn, callCfg, evalCfg) if err != nil { // TODO: Stack trace fmt.Fprintln(os.Stderr, err) } } } } // Access methods. // Global returns the global Ns. func (ev *Evaler) Global() *Ns { ev.mu.RLock() defer ev.mu.RUnlock() return ev.global } // ExtendGlobal extends the global namespace with the given namespace. func (ev *Evaler) ExtendGlobal(ns Nser) { ev.mu.Lock() defer ev.mu.Unlock() ev.global = CombineNs(ev.global, ns.Ns()) } // Builtin returns the builtin Ns. func (ev *Evaler) Builtin() *Ns { ev.mu.RLock() defer ev.mu.RUnlock() return ev.builtin } // ExtendBuiltin extends the builtin namespace with the given namespace. func (ev *Evaler) ExtendBuiltin(ns Nser) { ev.mu.Lock() defer ev.mu.Unlock() ev.builtin = CombineNs(ev.builtin, ns.Ns()) } func (ev *Evaler) registerDeprecation(d deprecation) bool { ev.mu.Lock() defer ev.mu.Unlock() return ev.deprecations.register(d) } // AddModule add an internal module so that it can be used with "use $name" from // script. func (ev *Evaler) AddModule(name string, mod *Ns) { ev.mu.Lock() defer ev.mu.Unlock() ev.modules[name] = mod } // ValuePrefix returns the prefix to prepend to value outputs when writing them // to terminal. func (ev *Evaler) ValuePrefix() string { ev.mu.RLock() defer ev.mu.RUnlock() return ev.valuePrefix } func (ev *Evaler) getNotifyBgJobSuccess() bool { ev.mu.RLock() defer ev.mu.RUnlock() return ev.notifyBgJobSuccess } func (ev *Evaler) getNumBgJobs() int { ev.mu.RLock() defer ev.mu.RUnlock() return ev.numBgJobs } func (ev *Evaler) addNumBgJobs(delta int) { ev.mu.Lock() defer ev.mu.Unlock() ev.numBgJobs += delta } // SetArgs sets the value of the $args variable to a list of strings, built from // the given slice. This method must be called before the Evaler is used to // evaluate any code. func (ev *Evaler) SetArgs(args []string) { ev.Args = listOfStrings(args) } // AddBeforeChdir adds a function to run before changing directory. This method // must be called before the Evaler is used to evaluate any code. func (ev *Evaler) AddBeforeChdir(f func(string)) { ev.BeforeChdir = append(ev.BeforeChdir, f) } // AddAfterChdir adds a function to run after changing directory. This method // must be called before the Evaler is used to evaluate any code. func (ev *Evaler) AddAfterChdir(f func(string)) { ev.AfterChdir = append(ev.AfterChdir, f) } // AddBeforeExit adds a function to run before the Elvish process exits or gets // replaced (via "exec" on UNIX). This method must be called before the Evaler // is used to evaluate any code. func (ev *Evaler) AddBeforeExit(f func()) { ev.BeforeExit = append(ev.BeforeExit, f) } // Chdir changes the current directory, and updates $E:PWD on success // // It runs the functions in beforeChdir immediately before changing the // directory, and the functions in afterChdir immediately after (if chdir was // successful). It returns nil as long as the directory changing part succeeds. func (ev *Evaler) Chdir(path string) error { for _, hook := range ev.BeforeChdir { hook(path) } err := os.Chdir(path) if err != nil { return err } for _, hook := range ev.AfterChdir { hook(path) } pwd, err := os.Getwd() if err != nil { logger.Println("getwd after cd:", err) return nil } os.Setenv(env.PWD, pwd) return nil } // EvalCfg keeps configuration for the (*Evaler).Eval method. type EvalCfg struct { // Ports to use in evaluation. The first 3 elements, if not specified // (either being nil or Ports containing fewer than 3 elements), // will be filled with DummyInputPort, DummyOutputPort and // DummyOutputPort respectively. Ports []*Port // Callback to get a channel of interrupt signals and a function to call // when the channel is no longer needed. Interrupt func() (<-chan struct{}, func()) // Whether the Eval method should try to put the Elvish in the foreground // after the code is executed. PutInFg bool // If not nil, used the given global namespace, instead of Evaler's own. Global *Ns } func (cfg *EvalCfg) fillDefaults() { if len(cfg.Ports) < 3 { cfg.Ports = append(cfg.Ports, make([]*Port, 3-len(cfg.Ports))...) } if cfg.Ports[0] == nil { cfg.Ports[0] = DummyInputPort } if cfg.Ports[1] == nil { cfg.Ports[1] = DummyOutputPort } if cfg.Ports[2] == nil { cfg.Ports[2] = DummyOutputPort } } // Eval evaluates a piece of source code with the given configuration. The // returned error may be a parse error, compilation error or exception. func (ev *Evaler) Eval(src parse.Source, cfg EvalCfg) error { cfg.fillDefaults() errFile := cfg.Ports[2].File tree, err := parse.Parse(src, parse.Config{WarningWriter: errFile}) if err != nil { return err } ev.mu.Lock() b := ev.builtin defaultGlobal := cfg.Global == nil if defaultGlobal { // If cfg.Global is nil, use the Evaler's default global, and also // mutate the default global. cfg.Global = ev.global // Continue to hold the mutex; it will be released when ev.global gets // mutated. } else { ev.mu.Unlock() } op, err := compile(b.static(), cfg.Global.static(), tree, errFile) if err != nil { if defaultGlobal { ev.mu.Unlock() } return err } fm, cleanup := ev.prepareFrame(src, cfg) defer cleanup() newLocal, exec := op.prepare(fm) if defaultGlobal { ev.global = newLocal ev.mu.Unlock() } return exec() } // CallCfg keeps configuration for the (*Evaler).Call method. type CallCfg struct { // Arguments to pass to the the function. Args []interface{} // Options to pass to the function. Opts map[string]interface{} // The name of the internal source that is calling the function. From string } func (cfg *CallCfg) fillDefaults() { if cfg.Opts == nil { cfg.Opts = NoOpts } if cfg.From == "" { cfg.From = "[internal]" } } // Call calls a given function. func (ev *Evaler) Call(f Callable, callCfg CallCfg, evalCfg EvalCfg) error { callCfg.fillDefaults() evalCfg.fillDefaults() if evalCfg.Global == nil { evalCfg.Global = ev.Global() } fm, cleanup := ev.prepareFrame(parse.Source{Name: callCfg.From}, evalCfg) defer cleanup() return f.Call(fm, callCfg.Args, callCfg.Opts) } func (ev *Evaler) prepareFrame(src parse.Source, cfg EvalCfg) (*Frame, func()) { var intCh <-chan struct{} var intChCleanup func() if cfg.Interrupt != nil { intCh, intChCleanup = cfg.Interrupt() } ports := fillDefaultDummyPorts(cfg.Ports) fm := &Frame{ev, src, cfg.Global, new(Ns), intCh, ports, nil, false} return fm, func() { if intChCleanup != nil { intChCleanup() } if cfg.PutInFg { err := putSelfInFg() if err != nil { fmt.Fprintln(ports[2].File, "failed to put myself in foreground:", err) } } } } func fillDefaultDummyPorts(ports []*Port) []*Port { growPorts(&ports, 3) if ports[0] == nil { ports[0] = DummyInputPort } if ports[1] == nil { ports[1] = DummyOutputPort } if ports[2] == nil { ports[2] = DummyOutputPort } return ports } // Check checks the given source code for any parse error and compilation error. // It always tries to compile the code even if there is a parse error; both // return values may be non-nil. If w is not nil, deprecation messages are // written to it. func (ev *Evaler) Check(src parse.Source, w io.Writer) (*parse.Error, *diag.Error) { tree, parseErr := parse.Parse(src, parse.Config{WarningWriter: w}) return parse.GetError(parseErr), ev.CheckTree(tree, w) } // CheckTree checks the given parsed source tree for compilation errors. If w is // not nil, deprecation messages are written to it. func (ev *Evaler) CheckTree(tree parse.Tree, w io.Writer) *diag.Error { _, compileErr := ev.compile(tree, ev.Global(), w) return GetCompilationError(compileErr) } // Compiles a parsed tree. func (ev *Evaler) compile(tree parse.Tree, g *Ns, w io.Writer) (nsOp, error) { return compile(ev.Builtin().static(), g.static(), tree, w) } elvish-0.17.0/pkg/eval/eval_test.go000066400000000000000000000075721415471104000171260ustar00rootroot00000000000000package eval_test import ( "strconv" "sync" "syscall" "testing" . "src.elv.sh/pkg/eval" "src.elv.sh/pkg/prog/progtest" . "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/testutil" ) func TestPid(t *testing.T) { pid := strconv.Itoa(syscall.Getpid()) Test(t, That("put $pid").Puts(pid)) } func TestNumBgJobs(t *testing.T) { Test(t, That("put $num-bg-jobs").Puts("0"), // TODO(xiaq): Test cases where $num-bg-jobs > 0. This cannot be done // with { put $num-bg-jobs }& because the output channel may have // already been closed when the closure is run. ) } func TestArgs(t *testing.T) { Test(t, That("put $args").Puts(vals.EmptyList)) TestWithSetup(t, func(ev *Evaler) { ev.SetArgs([]string{"foo", "bar"}) }, That("put $args").Puts(vals.MakeList("foo", "bar"))) } func TestEvalTimeDeprecate(t *testing.T) { progtest.SetDeprecationLevel(t, 42) testutil.InTempDir(t) TestWithSetup(t, func(ev *Evaler) { ev.ExtendGlobal(BuildNs().AddGoFn("dep", func(fm *Frame) { fm.Deprecate("deprecated", nil, 42) })) }, That("dep").PrintsStderrWith("deprecated"), // Deprecation message is only shown once. That("dep 2> tmp.txt; dep").DoesNothing(), ) } func TestMultipleEval(t *testing.T) { Test(t, That("x = hello").Then("put $x").Puts("hello"), // Shadowing with fn. Regression test for #1213. That("fn f { put old }").Then("fn f { put new }").Then("f"). Puts("new"), // Variable deletion. Regression test for #1213. That("x = foo").Then("del x").Then("put $x").DoesNotCompile(), ) } func TestEval_AlternativeGlobal(t *testing.T) { ev := NewEvaler() g := BuildNs().AddVar("a", vars.NewReadOnly("")).Ns() err := ev.Eval(parse.Source{Code: "nop $a"}, EvalCfg{Global: g}) if err != nil { t.Errorf("got error %v, want nil", err) } // Regression test for #1223 if ev.Global().HasKeyString("a") { t.Errorf("$a from alternative global leaked into Evaler global") } } func TestEval_Concurrent(t *testing.T) { ev := NewEvaler() var wg sync.WaitGroup wg.Add(2) go func() { ev.Eval(parse.Source{Code: "var a"}, EvalCfg{}) wg.Done() }() go func() { ev.Eval(parse.Source{Code: "var b"}, EvalCfg{}) wg.Done() }() wg.Wait() g := ev.Global() if !g.HasKeyString("a") { t.Errorf("variable $a not created") } if !g.HasKeyString("b") { t.Errorf("variable $b not created") } } type fooOpts struct{ Opt string } func (*fooOpts) SetDefaultOptions() {} func TestCall(t *testing.T) { ev := NewEvaler() var gotOpt, gotArg string fn := NewGoFn("foo", func(fm *Frame, opts fooOpts, arg string) { gotOpt = opts.Opt gotArg = arg }) passedArg := "arg value" passedOpt := "opt value" ev.Call(fn, CallCfg{ Args: []interface{}{passedArg}, Opts: map[string]interface{}{"opt": passedOpt}, From: "[TestCall]"}, EvalCfg{}) if gotArg != passedArg { t.Errorf("got arg %q, want %q", gotArg, passedArg) } if gotOpt != passedOpt { t.Errorf("got opt %q, want %q", gotOpt, passedOpt) } } var checkTests = []struct { name string code string wantParseErr bool wantCompileErr bool }{ {name: "no error", code: "put $nil"}, {name: "parse error only", code: "put [", wantParseErr: true}, {name: "compile error only", code: "put $x", wantCompileErr: true}, {name: "both parse and compile error", code: "put [$x", wantParseErr: true, wantCompileErr: true}, } func TestCheck(t *testing.T) { ev := NewEvaler() for _, test := range checkTests { t.Run(test.name, func(t *testing.T) { parseErr, compileErr := ev.Check(parse.Source{Code: test.code}, nil) if (parseErr != nil) != test.wantParseErr { t.Errorf("got parse error %v, when wantParseErr = %v", parseErr, test.wantParseErr) } if (compileErr != nil) != test.wantCompileErr { t.Errorf("got compile error %v, when wantCompileErr = %v", compileErr, test.wantCompileErr) } }) } } elvish-0.17.0/pkg/eval/evaltest/000077500000000000000000000000001415471104000164255ustar00rootroot00000000000000elvish-0.17.0/pkg/eval/evaltest/evaltest.go000066400000000000000000000171501415471104000206070ustar00rootroot00000000000000// Package evaltest provides a framework for testing Elvish script. // // The entry point for the framework is the Test function, which accepts a // *testing.T and any number of test cases. // // Test cases are constructed using the That function, followed by method calls // that add additional information to it. // // Example: // // Test(t, // That("put x").Puts("x"), // That("echo x").Prints("x\n")) // // If some setup is needed, use the TestWithSetup function instead. package evaltest import ( "bytes" "os" "reflect" "strings" "testing" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/testutil" ) // Case is a test case that can be used in Test. type Case struct { codes []string setup func(ev *eval.Evaler) verify func(t *testing.T) want result } type result struct { ValueOut []interface{} BytesOut []byte StderrOut []byte CompilationError error Exception error } // That returns a new Case with the specified source code. Multiple arguments // are joined with newlines. To specify multiple pieces of code that are // executed separately, use the Then method to append code pieces. // // When combined with subsequent method calls, a test case reads like English. // For example, a test for the fact that "put x" puts "x" reads: // // That("put x").Puts("x") func That(lines ...string) Case { return Case{codes: []string{strings.Join(lines, "\n")}} } // Then returns a new Case that executes the given code in addition. Multiple // arguments are joined with newlines. func (c Case) Then(lines ...string) Case { c.codes = append(c.codes, strings.Join(lines, "\n")) return c } // Then returns a new Case with the given setup function executed on the Evaler // before the code is executed. func (c Case) WithSetup(f func(*eval.Evaler)) Case { c.setup = f return c } // DoesNothing returns t unchanged. It is useful to mark tests that don't have // any side effects, for example: // // That("nop").DoesNothing() func (c Case) DoesNothing() Case { return c } // Puts returns an altered Case that runs an additional verification function. func (c Case) Passes(f func(t *testing.T)) Case { c.verify = f return c } // Puts returns an altered Case that requires the source code to produce the // specified values in the value channel when evaluated. func (c Case) Puts(vs ...interface{}) Case { c.want.ValueOut = vs return c } // Prints returns an altered Case that requires the source code to produce the // specified output in the byte pipe when evaluated. func (c Case) Prints(s string) Case { c.want.BytesOut = []byte(s) return c } // PrintsStderrWith returns an altered Case that requires the stderr output to // contain the given text. func (c Case) PrintsStderrWith(s string) Case { c.want.StderrOut = []byte(s) return c } // Throws returns an altered Case that requires the source code to throw an // exception with the given reason. The reason supports special matcher values // constructed by functions like ErrorWithMessage. // // If at least one stacktrace string is given, the exception must also have a // stacktrace matching the given source fragments, frame by frame (innermost // frame first). If no stacktrace string is given, the stack trace of the // exception is not checked. func (c Case) Throws(reason error, stacks ...string) Case { c.want.Exception = exc{reason, stacks} return c } // DoesNotCompile returns an altered Case that requires the source code to fail // compilation. func (c Case) DoesNotCompile() Case { c.want.CompilationError = anyError{} return c } // Test runs test cases. For each test case, a new Evaler is created with // NewEvaler. func Test(t *testing.T, tests ...Case) { t.Helper() TestWithSetup(t, func(*eval.Evaler) {}, tests...) } // TestWithSetup runs test cases. For each test case, a new Evaler is created // with NewEvaler and passed to the setup function. func TestWithSetup(t *testing.T, setup func(*eval.Evaler), tests ...Case) { t.Helper() for _, tt := range tests { t.Run(strings.Join(tt.codes, "\n"), func(t *testing.T) { t.Helper() ev := eval.NewEvaler() setup(ev) if tt.setup != nil { tt.setup(ev) } r := evalAndCollect(t, ev, tt.codes) if tt.verify != nil { tt.verify(t) } if !matchOut(tt.want.ValueOut, r.ValueOut) { t.Errorf("got value out %v, want %v", reprs(r.ValueOut), reprs(tt.want.ValueOut)) } if !bytes.Equal(tt.want.BytesOut, r.BytesOut) { t.Errorf("got bytes out %q, want %q", r.BytesOut, tt.want.BytesOut) } if !bytes.Contains(r.StderrOut, tt.want.StderrOut) { t.Errorf("got stderr out %q, want %q", r.StderrOut, tt.want.StderrOut) } if !matchErr(tt.want.CompilationError, r.CompilationError) { t.Errorf("got compilation error %v, want %v", r.CompilationError, tt.want.CompilationError) } if !matchErr(tt.want.Exception, r.Exception) { t.Errorf("unexpected exception") if exc, ok := r.Exception.(eval.Exception); ok { // For an eval.Exception report the type of the underlying error. t.Logf("got: %T: %v", exc.Reason(), exc) t.Logf("stack trace: %#v", getStackTexts(exc.StackTrace())) } else { t.Logf("got: %T: %v", r.Exception, r.Exception) } t.Errorf("want: %v", tt.want.Exception) } }) } } func evalAndCollect(t *testing.T, ev *eval.Evaler, texts []string) result { var r result port1, collect1 := capturePort() port2, collect2 := capturePort() ports := []*eval.Port{eval.DummyInputPort, port1, port2} for _, text := range texts { err := ev.Eval(parse.Source{Name: "[test]", Code: text}, eval.EvalCfg{Ports: ports, Interrupt: eval.ListenInterrupts}) if parse.GetError(err) != nil { t.Fatalf("Parse(%q) error: %s", text, err) } else if eval.GetCompilationError(err) != nil { // NOTE: If multiple code pieces have compilation errors, only the // last one compilation error is saved. r.CompilationError = err } else if err != nil { // NOTE: If multiple code pieces throw exceptions, only the last one // is saved. r.Exception = err } } r.ValueOut, r.BytesOut = collect1() _, r.StderrOut = collect2() return r } // Like eval.CapturePort, but captures values and bytes separately. Also panics // if it cannot create a pipe. func capturePort() (*eval.Port, func() ([]interface{}, []byte)) { var values []interface{} var bytes []byte port, done, err := eval.PipePort( func(ch <-chan interface{}) { for v := range ch { values = append(values, v) } }, func(r *os.File) { bytes = testutil.MustReadAllAndClose(r) }) if err != nil { panic(err) } return port, func() ([]interface{}, []byte) { done() return values, bytes } } func matchOut(want, got []interface{}) bool { if len(got) != len(want) { return false } for i := range got { if !match(got[i], want[i]) { return false } } return true } func match(got, want interface{}) bool { switch got := got.(type) { case float64: // Special-case float64 to correctly handle NaN and support // approximate comparison. switch want := want.(type) { case float64: return matchFloat64(got, want, 0) case Approximately: return matchFloat64(got, want.F, ApproximatelyThreshold) } case string: switch want := want.(type) { case MatchingRegexp: return matchRegexp(want.Pattern, got) } } return vals.Equal(got, want) } func reprs(values []interface{}) []string { s := make([]string, len(values)) for i, v := range values { s[i] = vals.Repr(v, vals.NoPretty) } return s } func matchErr(want, got error) bool { if want == nil { return got == nil } if matcher, ok := want.(errorMatcher); ok { return matcher.matchError(got) } return reflect.DeepEqual(want, got) } elvish-0.17.0/pkg/eval/evaltest/matchers.go000066400000000000000000000076401415471104000205710ustar00rootroot00000000000000package evaltest import ( "fmt" "math" "reflect" "regexp" "src.elv.sh/pkg/eval" ) // ApproximatelyThreshold defines the threshold for matching float64 values when // using Approximately. const ApproximatelyThreshold = 1e-15 // Approximately can be passed to Case.Puts to match a float64 within the // threshold defined by ApproximatelyThreshold. type Approximately struct{ F float64 } func matchFloat64(a, b, threshold float64) bool { if math.IsNaN(a) && math.IsNaN(b) { return true } if math.IsInf(a, 0) && math.IsInf(b, 0) && math.Signbit(a) == math.Signbit(b) { return true } return math.Abs(a-b) <= threshold } // MatchingRegexp can be passed to Case.Puts to match a any string that matches // a regexp pattern. If the pattern is not a valid regexp, the test will panic. type MatchingRegexp struct{ Pattern string } func matchRegexp(p, s string) bool { matched, err := regexp.MatchString(p, s) if err != nil { panic(err) } return matched } type errorMatcher interface{ matchError(error) bool } // AnyError is an error that can be passed to Case.Throws to match any non-nil // error. var AnyError = anyError{} // An errorMatcher for any error. type anyError struct{} func (anyError) Error() string { return "any error" } func (anyError) matchError(e error) bool { return e != nil } // An errorMatcher for exceptions. type exc struct { reason error stacks []string } func (e exc) Error() string { if len(e.stacks) == 0 { return fmt.Sprintf("exception with reason %v", e.reason) } return fmt.Sprintf("exception with reason %v and stacks %v", e.reason, e.stacks) } func (e exc) matchError(e2 error) bool { if e2, ok := e2.(eval.Exception); ok { return matchErr(e.reason, e2.Reason()) && (len(e.stacks) == 0 || reflect.DeepEqual(e.stacks, getStackTexts(e2.StackTrace()))) } return false } func getStackTexts(tb *eval.StackTrace) []string { texts := []string{} for tb != nil { ctx := tb.Head texts = append(texts, ctx.Source[ctx.From:ctx.To]) tb = tb.Next } return texts } // ErrorWithType returns an error that can be passed to the Case.Throws to match // any error with the same type as the argument. func ErrorWithType(v error) error { return errWithType{v} } // An errorMatcher for any error with the given type. type errWithType struct{ v error } func (e errWithType) Error() string { return fmt.Sprintf("error with type %T", e.v) } func (e errWithType) matchError(e2 error) bool { return reflect.TypeOf(e.v) == reflect.TypeOf(e2) } // ErrorWithMessage returns an error that can be passed to Case.Throws to match // any error with the given message. func ErrorWithMessage(msg string) error { return errWithMessage{msg} } // An errorMatcher for any error with the given message. type errWithMessage struct{ msg string } func (e errWithMessage) Error() string { return "error with message " + e.msg } func (e errWithMessage) matchError(e2 error) bool { return e2 != nil && e.msg == e2.Error() } // CmdExit returns an error that can be passed to Case.Throws to match an // eval.ExternalCmdExit ignoring the Pid field. func CmdExit(v eval.ExternalCmdExit) error { return errCmdExit{v} } // An errorMatcher for an ExternalCmdExit error that ignores the `Pid` member. // We only match the command name and exit status because at run time we // cannot know the correct value for `Pid`. type errCmdExit struct{ v eval.ExternalCmdExit } func (e errCmdExit) Error() string { return e.v.Error() } func (e errCmdExit) matchError(gotErr error) bool { if gotErr == nil { return false } ge := gotErr.(eval.ExternalCmdExit) return e.v.CmdName == ge.CmdName && e.v.WaitStatus == ge.WaitStatus } type errOneOf struct{ errs []error } func OneOfErrors(errs ...error) error { return errOneOf{errs} } func (e errOneOf) Error() string { return fmt.Sprint("one of", e.errs) } func (e errOneOf) matchError(gotError error) bool { for _, want := range e.errs { if matchErr(want, gotError) { return true } } return false } elvish-0.17.0/pkg/eval/exception.go000066400000000000000000000213131415471104000171230ustar00rootroot00000000000000package eval import ( "bytes" "fmt" "strconv" "syscall" "unsafe" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/persistent/hash" ) // Exception represents exceptions. It is both a Value accessible to Elvish // code, and can be returned by methods like like (*Evaler).Eval. type Exception interface { error diag.Shower Reason() error StackTrace() *StackTrace // This is not strictly necessary, but it makes sure that there is only one // implementation of Exception, so that the compiler may de-virtualize this // interface. isException() } // NewException creates a new Exception. func NewException(reason error, stackTrace *StackTrace) Exception { return &exception{reason, stackTrace} } // Implementation of the Exception interface. type exception struct { reason error stackTrace *StackTrace } // StackTrace represents a stack trace as a linked list of diag.Context. The // head is the innermost stack. // // Since pipelines can call multiple functions in parallel, all the StackTrace // nodes form a DAG. type StackTrace struct { Head *diag.Context Next *StackTrace } // Reason returns the Reason field if err is an Exception. Otherwise it returns // err itself. func Reason(err error) error { if exc, ok := err.(*exception); ok { return exc.reason } return err } // OK is a pointer to a special value of Exception that represents the absence // of exception. var OK = &exception{} func (exc *exception) isException() {} func (exc *exception) Reason() error { return exc.reason } func (exc *exception) StackTrace() *StackTrace { return exc.stackTrace } // Error returns the message of the cause of the exception. func (exc *exception) Error() string { return exc.reason.Error() } // Show shows the exception. func (exc *exception) Show(indent string) string { buf := new(bytes.Buffer) var causeDescription string if shower, ok := exc.reason.(diag.Shower); ok { causeDescription = shower.Show(indent) } else if exc.reason == nil { causeDescription = "ok" } else { causeDescription = "\033[31;1m" + exc.reason.Error() + "\033[m" } fmt.Fprintf(buf, "Exception: %s", causeDescription) if exc.stackTrace != nil { buf.WriteString("\n") if exc.stackTrace.Next == nil { buf.WriteString(exc.stackTrace.Head.ShowCompact(indent)) } else { buf.WriteString(indent + "Traceback:") for tb := exc.stackTrace; tb != nil; tb = tb.Next { buf.WriteString("\n" + indent + " ") buf.WriteString(tb.Head.Show(indent + " ")) } } } if pipeExcs, ok := exc.reason.(PipelineError); ok { buf.WriteString("\n" + indent + "Caused by:") for _, e := range pipeExcs.Errors { if e == OK { continue } buf.WriteString("\n" + indent + " " + e.Show(indent+" ")) } } return buf.String() } // Kind returns "exception". func (exc *exception) Kind() string { return "exception" } // Repr returns a representation of the exception. It is lossy in that it does // not preserve the stacktrace. func (exc *exception) Repr(indent int) string { if exc.reason == nil { return "$ok" } return "[&reason=" + vals.Repr(exc.reason, indent+1) + "]" } // Equal compares by address. func (exc *exception) Equal(rhs interface{}) bool { return exc == rhs } // Hash returns the hash of the address. func (exc *exception) Hash() uint32 { return hash.Pointer(unsafe.Pointer(exc)) } // Bool returns whether this exception has a nil cause; that is, it is $ok. func (exc *exception) Bool() bool { return exc.reason == nil } func (exc *exception) Fields() vals.StructMap { return excFields{exc} } type excFields struct{ e *exception } func (excFields) IsStructMap() {} func (f excFields) Reason() error { return f.e.reason } // PipelineError represents the errors of pipelines, in which multiple commands // may error. type PipelineError struct { Errors []Exception } // Error returns a plain text representation of the pipeline error. func (pe PipelineError) Error() string { b := new(bytes.Buffer) b.WriteString("(") for i, e := range pe.Errors { if i > 0 { b.WriteString(" | ") } if e == nil || e.Reason() == nil { b.WriteString("") } else { b.WriteString(e.Error()) } } b.WriteString(")") return b.String() } // MakePipelineError builds an error from the execution results of multiple // commands in a pipeline. // // If all elements are either nil or OK, it returns nil. If there is exactly // non-nil non-OK Exception, it returns it. Otherwise, it return a PipelineError // built from the slice, with nil items turned into OK's for easier access from // Elvish code. func MakePipelineError(excs []Exception) error { newexcs := make([]Exception, len(excs)) notOK, lastNotOK := 0, 0 for i, e := range excs { if e == nil { newexcs[i] = OK } else { newexcs[i] = e if e.Reason() != nil { notOK++ lastNotOK = i } } } switch notOK { case 0: return nil case 1: return newexcs[lastNotOK] default: return PipelineError{newexcs} } } func (pe PipelineError) Fields() vals.StructMap { return peFields{pe} } type peFields struct{ pe PipelineError } func (peFields) IsStructMap() {} func (f peFields) Type() string { return "pipeline" } func (f peFields) Exceptions() vals.List { li := vals.EmptyList for _, exc := range f.pe.Errors { li = li.Cons(exc) } return li } // Flow is a special type of error used for control flows. type Flow uint // Control flows. const ( Return Flow = iota Break Continue ) var flowNames = [...]string{ "return", "break", "continue", } func (f Flow) Error() string { if f >= Flow(len(flowNames)) { return fmt.Sprintf("!(BAD FLOW: %d)", f) } return flowNames[f] } // Show shows the flow "error". func (f Flow) Show(string) string { return "\033[33;1m" + f.Error() + "\033[m" } func (f Flow) Fields() vals.StructMap { return flowFields{f} } type flowFields struct{ f Flow } func (flowFields) IsStructMap() {} func (f flowFields) Type() string { return "flow" } func (f flowFields) Name() string { return f.f.Error() } // ExternalCmdExit contains the exit status of external commands. type ExternalCmdExit struct { syscall.WaitStatus CmdName string Pid int } // NewExternalCmdExit constructs an error for representing a non-zero exit from // an external command. func NewExternalCmdExit(name string, ws syscall.WaitStatus, pid int) error { if ws.Exited() && ws.ExitStatus() == 0 { return nil } return ExternalCmdExit{ws, name, pid} } func (exit ExternalCmdExit) Error() string { ws := exit.WaitStatus quotedName := parse.Quote(exit.CmdName) switch { case ws.Exited(): return quotedName + " exited with " + strconv.Itoa(ws.ExitStatus()) case ws.Signaled(): causeDescription := quotedName + " killed by signal " + ws.Signal().String() if ws.CoreDump() { causeDescription += " (core dumped)" } return causeDescription case ws.Stopped(): causeDescription := quotedName + " stopped by signal " + fmt.Sprintf("%s (pid=%d)", ws.StopSignal(), exit.Pid) trap := ws.TrapCause() if trap != -1 { causeDescription += fmt.Sprintf(" (trapped %v)", trap) } return causeDescription default: return fmt.Sprint(quotedName, " has unknown WaitStatus ", ws) } } func (exit ExternalCmdExit) Fields() vals.StructMap { ws := exit.WaitStatus f := exitFieldsCommon{exit} switch { case ws.Exited(): return exitFieldsExited{f} case ws.Signaled(): return exitFieldsSignaled{f} case ws.Stopped(): return exitFieldsStopped{f} default: return exitFieldsUnknown{f} } } type exitFieldsCommon struct{ e ExternalCmdExit } func (exitFieldsCommon) IsStructMap() {} func (f exitFieldsCommon) CmdName() string { return f.e.CmdName } func (f exitFieldsCommon) Pid() string { return strconv.Itoa(f.e.Pid) } type exitFieldsExited struct{ exitFieldsCommon } func (exitFieldsExited) Type() string { return "external-cmd/exited" } func (f exitFieldsExited) ExitStatus() string { return strconv.Itoa(f.e.ExitStatus()) } type exitFieldsSignaled struct{ exitFieldsCommon } func (f exitFieldsSignaled) Type() string { return "external-cmd/signaled" } func (f exitFieldsSignaled) SignalName() string { return f.e.Signal().String() } func (f exitFieldsSignaled) SignalNumber() string { return strconv.Itoa(int(f.e.Signal())) } func (f exitFieldsSignaled) CoreDumped() bool { return f.e.CoreDump() } type exitFieldsStopped struct{ exitFieldsCommon } func (f exitFieldsStopped) Type() string { return "external-cmd/stopped" } func (f exitFieldsStopped) SignalName() string { return f.e.StopSignal().String() } func (f exitFieldsStopped) SignalNumber() string { return strconv.Itoa(int(f.e.StopSignal())) } func (f exitFieldsStopped) TrapCause() int { return f.e.TrapCause() } type exitFieldsUnknown struct{ exitFieldsCommon } func (exitFieldsUnknown) Type() string { return "external-cmd/unknown" } elvish-0.17.0/pkg/eval/exception_test.go000066400000000000000000000046121415471104000201650ustar00rootroot00000000000000package eval_test import ( "errors" "reflect" "runtime" "testing" "unsafe" "src.elv.sh/pkg/diag" . "src.elv.sh/pkg/eval" . "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/persistent/hash" "src.elv.sh/pkg/tt" ) func TestReason(t *testing.T) { err := errors.New("ordinary error") tt.Test(t, tt.Fn("Reason", Reason), tt.Table{ tt.Args(err).Rets(err), tt.Args(makeException(err)).Rets(err), }) } func TestException(t *testing.T) { err := FailError{"error"} exc := makeException(err) vals.TestValue(t, exc). Kind("exception"). Bool(false). Hash(hash.Pointer(unsafe.Pointer(reflect.ValueOf(exc).Pointer()))). Equal(exc). NotEqual(makeException(errors.New("error"))). AllKeys("reason"). Index("reason", err). IndexError("stack", vals.NoSuchKey("stack")). Repr("[&reason=[&content=error &type=fail]]") vals.TestValue(t, OK). Kind("exception"). Bool(true). Repr("$ok") } func makeException(cause error, entries ...*diag.Context) Exception { return NewException(cause, makeStackTrace(entries...)) } // Creates a new StackTrace, using the first entry as the head. func makeStackTrace(entries ...*diag.Context) *StackTrace { var s *StackTrace for i := len(entries) - 1; i >= 0; i-- { s = &StackTrace{Head: entries[i], Next: s} } return s } func TestFlow_Fields(t *testing.T) { Test(t, That("put ?(return)[reason][type name]").Puts("flow", "return"), ) } func TestExternalCmdExit_Fields(t *testing.T) { badCmd := "false" if runtime.GOOS == "windows" { badCmd = "cmd /c exit 1" } Test(t, That("put ?("+badCmd+")[reason][type exit-status]"). Puts("external-cmd/exited", "1"), // TODO: Test killed and stopped commands ) } func TestPipelineError_Fields(t *testing.T) { Test(t, That("put ?(fail 1 | fail 2)[reason][type]").Puts("pipeline"), That("count ?(fail 1 | fail 2)[reason][exceptions]").Puts(2), That("put ?(fail 1 | fail 2)[reason][exceptions][0][reason][type]"). Puts("fail"), ) } func TestErrorMethods(t *testing.T) { tt.Test(t, tt.Fn("Error", error.Error), tt.Table{ tt.Args(makeException(errors.New("err"))).Rets("err"), tt.Args(MakePipelineError([]Exception{ makeException(errors.New("err1")), makeException(errors.New("err2"))})).Rets("(err1 | err2)"), tt.Args(Return).Rets("return"), tt.Args(Break).Rets("break"), tt.Args(Continue).Rets("continue"), tt.Args(Flow(1000)).Rets("!(BAD FLOW: 1000)"), }) } elvish-0.17.0/pkg/eval/exception_unix_test.go000066400000000000000000000026711415471104000212330ustar00rootroot00000000000000//go:build !windows && !plan9 && !js // +build !windows,!plan9,!js package eval_test import ( "fmt" "runtime" "syscall" "testing" . "src.elv.sh/pkg/eval" "src.elv.sh/pkg/tt" ) func TestExternalCmdExit_Error(t *testing.T) { tt.Test(t, tt.Fn("Error", error.Error), tt.Table{ tt.Args(ExternalCmdExit{0x0, "ls", 1}).Rets("ls exited with 0"), tt.Args(ExternalCmdExit{0x100, "ls", 1}).Rets("ls exited with 1"), // Note: all Unix'es have SIGINT = 2, but syscall package has different // string in gccgo("Interrupt") and gc("interrupt"). tt.Args(ExternalCmdExit{0x2, "ls", 1}).Rets("ls killed by signal " + syscall.SIGINT.String()), // 0x80 + signal for core dumped tt.Args(ExternalCmdExit{0x82, "ls", 1}).Rets("ls killed by signal " + syscall.SIGINT.String() + " (core dumped)"), // 0x7f + signal<<8 for stopped tt.Args(ExternalCmdExit{0x27f, "ls", 1}).Rets("ls stopped by signal " + syscall.SIGINT.String() + " (pid=1)"), }) if runtime.GOOS == "linux" { tt.Test(t, tt.Fn("Error", error.Error), tt.Table{ // 0x057f + cause<<16 for trapped. SIGTRAP is 5 on all Unix'es but have // different string representations on different OSes. tt.Args(ExternalCmdExit{0x1057f, "ls", 1}).Rets(fmt.Sprintf( "ls stopped by signal %s (pid=1) (trapped 1)", syscall.SIGTRAP)), // 0xff is the only exit code that is not exited, signaled or stopped. tt.Args(ExternalCmdExit{0xff, "ls", 1}).Rets("ls has unknown WaitStatus 255"), }) } } elvish-0.17.0/pkg/eval/external_cmd.go000066400000000000000000000054041415471104000175750ustar00rootroot00000000000000package eval import ( "errors" "os" "os/exec" "sync/atomic" "syscall" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/fsutil" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/persistent/hash" ) var ( // ErrExternalCmdOpts is thrown when an external command is passed Elvish // options. // // TODO: Catch this kind of errors at compilation time. ErrExternalCmdOpts = errors.New("external commands don't accept elvish options") // ErrImplicitCdNoArg is thrown when an implicit cd form is passed arguments. ErrImplicitCdNoArg = errors.New("implicit cd accepts no arguments") ) // externalCmd is an external command. type externalCmd struct { Name string } // NewExternalCmd returns a callable that executes the named external command. // // An external command converts all arguments to strings, and does not accept // any option. func NewExternalCmd(name string) Callable { return externalCmd{name} } func (e externalCmd) Kind() string { return "fn" } func (e externalCmd) Equal(a interface{}) bool { return e == a } func (e externalCmd) Hash() uint32 { return hash.String(e.Name) } func (e externalCmd) Repr(int) string { return "" } // Call calls an external command. func (e externalCmd) Call(fm *Frame, argVals []interface{}, opts map[string]interface{}) error { if len(opts) > 0 { return ErrExternalCmdOpts } if fsutil.DontSearch(e.Name) { stat, err := os.Stat(e.Name) if err == nil && stat.IsDir() { // implicit cd if len(argVals) > 0 { return ErrImplicitCdNoArg } return fm.Evaler.Chdir(e.Name) } } files := make([]*os.File, len(fm.ports)) for i, port := range fm.ports { if port != nil { files[i] = port.File } } args := make([]string, len(argVals)+1) for i, a := range argVals { // TODO: Maybe we should enforce string arguments instead of coercing // all args to strings. args[i+1] = vals.ToString(a) } path, err := exec.LookPath(e.Name) if err != nil { return err } args[0] = path sys := makeSysProcAttr(fm.background) proc, err := os.StartProcess(path, args, &os.ProcAttr{Files: files, Sys: sys}) if err != nil { return err } state, err := proc.Wait() if err != nil { // This should be a can't happen situation. Nonetheless, treat it as a // soft error rather than panicking since the Go documentation is not // explicit that this can only happen if we make a mistake. Such as // calling `Wait` twice on a particular process object. return err } ws := state.Sys().(syscall.WaitStatus) if ws.Signaled() && isSIGPIPE(ws.Signal()) { readerGone := fm.ports[1].readerGone if readerGone != nil && atomic.LoadInt32(readerGone) == 1 { return errs.ReaderGone{} } } return NewExternalCmdExit(e.Name, state.Sys().(syscall.WaitStatus), proc.Pid) } elvish-0.17.0/pkg/eval/external_cmd_test.go000066400000000000000000000022401415471104000206270ustar00rootroot00000000000000package eval_test import ( "os" "testing" . "src.elv.sh/pkg/eval" . "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/testutil" ) func TestBuiltinFnExternal(t *testing.T) { tmpHome := testutil.InTempHome(t) testutil.Setenv(t, "PATH", tmpHome+":"+os.Getenv("PATH")) Test(t, That(`e = (external true); kind-of $e`).Puts("fn"), That(`e = (external true); put (repr $e)`).Puts(""), That(`e = (external false); m = [&$e=true]; put (repr $m)`).Puts("[&=true]"), // Test calling of external commands. That(`e = (external true); $e`).DoesNothing(), That(`e = (external true); $e &option`).Throws(ErrExternalCmdOpts, "$e &option"), That(`e = (external false); $e`).Throws(CmdExit( ExternalCmdExit{CmdName: "false", WaitStatus: exitWaitStatus(1)})), // TODO: Modify the ExternalCmd.Call method to wrap the Go error in a // predictable Elvish error so we don't have to resort to using // ThrowsAny in the following tests. // // The command shouldn't be found when run so we should get an // exception along the lines of "executable file not found in $PATH". That(`e = (external true); E:PATH=/ $e`).Throws(AnyError), ) } elvish-0.17.0/pkg/eval/external_cmd_unix.go000066400000000000000000000002531415471104000206350ustar00rootroot00000000000000//go:build !windows && !plan9 && !js // +build !windows,!plan9,!js package eval import "syscall" func isSIGPIPE(s syscall.Signal) bool { return s == syscall.SIGPIPE } elvish-0.17.0/pkg/eval/external_cmd_unix_internal_test.go000066400000000000000000000046661415471104000236040ustar00rootroot00000000000000//go:build !windows && !plan9 && !js // +build !windows,!plan9,!js package eval import ( "os" "reflect" "syscall" "testing" "src.elv.sh/pkg/env" "src.elv.sh/pkg/parse" . "src.elv.sh/pkg/testutil" ) func TestExec_Argv0Argv(t *testing.T) { dir := InTempDir(t) ApplyDir(Dir{ "bin": Dir{ "elvish": File{Perm: 0755}, "cat": File{Perm: 0755}, }, }) Setenv(t, "PATH", dir+"/bin") Setenv(t, env.SHLVL, "1") var tests = []struct { name string code string wantArgv0 string wantArgv []string wantError bool }{ { name: "absolute path command", code: "exec /bin/sh foo bar", wantArgv0: "/bin/sh", wantArgv: []string{"/bin/sh", "foo", "bar"}, }, { name: "relative path command", code: "exec cat foo bar", wantArgv0: dir + "/bin/cat", wantArgv: []string{dir + "/bin/cat", "foo", "bar"}, }, { name: "no command", code: "exec", wantArgv0: dir + "/bin/elvish", wantArgv: []string{dir + "/bin/elvish"}, }, { name: "bad command", code: "exec bad", wantError: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Helper() var ( gotArgv0 string gotArgv []string ) syscallExec = func(argv0 string, argv []string, envv []string) error { gotArgv0 = argv0 gotArgv = argv return nil } defer func() { syscallExec = syscall.Exec }() ev := NewEvaler() err := ev.Eval(parse.Source{Name: "[test]", Code: test.code}, EvalCfg{}) if gotArgv0 != test.wantArgv0 { t.Errorf("got argv0 %q, want %q", gotArgv0, test.wantArgv0) } if !reflect.DeepEqual(gotArgv, test.wantArgv) { t.Errorf("got argv %q, want %q", gotArgv, test.wantArgv) } hasError := err != nil if hasError != test.wantError { t.Errorf("has error %v, want %v", hasError, test.wantError) } }) } } func TestDecSHLVL(t *testing.T) { // Valid integers are decremented, regardless of sign testDecSHLVL(t, "-2", "-3") testDecSHLVL(t, "-1", "-2") testDecSHLVL(t, "0", "-1") testDecSHLVL(t, "1", "0") testDecSHLVL(t, "2", "1") testDecSHLVL(t, "3", "2") // Non-integers are kept unchanged testDecSHLVL(t, "", "") testDecSHLVL(t, "x", "x") } func testDecSHLVL(t *testing.T, oldValue, newValue string) { t.Helper() Setenv(t, env.SHLVL, oldValue) decSHLVL() if gotValue := os.Getenv(env.SHLVL); gotValue != newValue { t.Errorf("got new value %q, want %q", gotValue, newValue) } } elvish-0.17.0/pkg/eval/external_cmd_unix_test.go000066400000000000000000000006351415471104000217000ustar00rootroot00000000000000//go:build !windows && !plan9 && !js // +build !windows,!plan9,!js package eval_test import ( "syscall" ) func exitWaitStatus(exit uint32) syscall.WaitStatus { // The exit<<8 is gross but I can't find any exported symbols that would // allow us to construct WaitStatus. So assume legacy UNIX encoding // for a process that exits normally; i.e., not due to a signal. return syscall.WaitStatus(exit << 8) } elvish-0.17.0/pkg/eval/external_cmd_windows.go000066400000000000000000000001721415471104000213440ustar00rootroot00000000000000package eval import "syscall" func isSIGPIPE(s syscall.Signal) bool { // Windows doesn't have SIGPIPE. return false } elvish-0.17.0/pkg/eval/external_cmd_windows_test.go000066400000000000000000000002511415471104000224010ustar00rootroot00000000000000//go:build windows // +build windows package eval_test import ( "syscall" ) func exitWaitStatus(exit uint32) syscall.WaitStatus { return syscall.WaitStatus{exit} } elvish-0.17.0/pkg/eval/frame.go000066400000000000000000000145721415471104000162300ustar00rootroot00000000000000package eval import ( "bufio" "fmt" "io" "os" "sync" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/prog" "src.elv.sh/pkg/strutil" ) // Frame contains information of the current running function, akin to a call // frame in native CPU execution. A Frame is only modified during and very // shortly after creation; new Frame's are "forked" when needed. type Frame struct { Evaler *Evaler srcMeta parse.Source local, up *Ns intCh <-chan struct{} ports []*Port traceback *StackTrace background bool } // PrepareEval prepares a piece of code for evaluation in a copy of the current // Frame. If r is not nil, it is added to the traceback of the evaluation // context. If ns is not nil, it is used in place of the current local namespace // as the namespace to evaluate the code in. // // If there is any parse error or compilation error, it returns a nil *Ns, nil // function and the error. If there is no parse error or compilation error, it // returns the altered local namespace, function that can be called to actuate // the evaluation, and a nil error. func (fm *Frame) PrepareEval(src parse.Source, r diag.Ranger, ns *Ns) (*Ns, func() Exception, error) { tree, err := parse.Parse(src, parse.Config{WarningWriter: fm.ErrorFile()}) if err != nil { return nil, nil, err } local := fm.local if ns != nil { local = ns } traceback := fm.traceback if r != nil { traceback = fm.addTraceback(r) } newFm := &Frame{ fm.Evaler, src, local, new(Ns), fm.intCh, fm.ports, traceback, fm.background} op, err := compile(newFm.Evaler.Builtin().static(), local.static(), tree, fm.ErrorFile()) if err != nil { return nil, nil, err } newLocal, exec := op.prepare(newFm) return newLocal, exec, nil } // Eval evaluates a piece of code in a copy of the current Frame. It returns the // altered local namespace, and any parse error, compilation error or exception. // // See PrepareEval for a description of the arguments. func (fm *Frame) Eval(src parse.Source, r diag.Ranger, ns *Ns) (*Ns, error) { newLocal, exec, err := fm.PrepareEval(src, r, ns) if err != nil { return nil, err } return newLocal, exec() } // Close releases resources allocated for this frame. It always returns a nil // error. It may be called only once. func (fm *Frame) Close() error { for _, port := range fm.ports { port.close() } return nil } // InputChan returns a channel from which input can be read. func (fm *Frame) InputChan() chan interface{} { return fm.ports[0].Chan } // InputFile returns a file from which input can be read. func (fm *Frame) InputFile() *os.File { return fm.ports[0].File } // ValueOutput returns a handle for writing value outputs. func (fm *Frame) ValueOutput() ValueOutput { p := fm.ports[1] return valueOutput{p.Chan, p.sendStop, p.sendError} } // ByteOutput returns a handle for writing byte outputs. func (fm *Frame) ByteOutput() ByteOutput { return byteOutput{fm.ports[1].File} } // ErrorFile returns a file onto which error messages can be written. func (fm *Frame) ErrorFile() *os.File { return fm.ports[2].File } // IterateInputs calls the passed function for each input element. func (fm *Frame) IterateInputs(f func(interface{})) { var wg sync.WaitGroup inputs := make(chan interface{}) wg.Add(2) go func() { linesToChan(fm.InputFile(), inputs) wg.Done() }() go func() { for v := range fm.ports[0].Chan { inputs <- v } wg.Done() }() go func() { wg.Wait() close(inputs) }() for v := range inputs { f(v) } } func linesToChan(r io.Reader, ch chan<- interface{}) { filein := bufio.NewReader(r) for { line, err := filein.ReadString('\n') if line != "" { ch <- strutil.ChopLineEnding(line) } if err != nil { if err != io.EOF { logger.Println("error on reading:", err) } break } } } // fork returns a modified copy of ec. The ports are forked, and the name is // changed to the given value. Other fields are copied shallowly. func (fm *Frame) fork(name string) *Frame { newPorts := make([]*Port, len(fm.ports)) for i, p := range fm.ports { if p != nil { newPorts[i] = p.fork() } } return &Frame{ fm.Evaler, fm.srcMeta, fm.local, fm.up, fm.intCh, newPorts, fm.traceback, fm.background, } } // A shorthand for forking a frame and setting the output port. func (fm *Frame) forkWithOutput(name string, p *Port) *Frame { newFm := fm.fork(name) newFm.ports[1] = p return newFm } // CaptureOutput captures the output of a given callback that operates on a Frame. func (fm *Frame) CaptureOutput(f func(*Frame) error) ([]interface{}, error) { outPort, collect, err := CapturePort() if err != nil { return nil, err } err = f(fm.forkWithOutput("[output capture]", outPort)) return collect(), err } // PipeOutput calls a callback with output piped to the given output handlers. func (fm *Frame) PipeOutput(f func(*Frame) error, vCb func(<-chan interface{}), bCb func(*os.File)) error { outPort, done, err := PipePort(vCb, bCb) if err != nil { return err } err = f(fm.forkWithOutput("[output pipe]", outPort)) done() return err } func (fm *Frame) addTraceback(r diag.Ranger) *StackTrace { return &StackTrace{ Head: diag.NewContext(fm.srcMeta.Name, fm.srcMeta.Code, r.Range()), Next: fm.traceback, } } // Returns an Exception with specified range and cause. func (fm *Frame) errorp(r diag.Ranger, e error) Exception { switch e := e.(type) { case nil: return nil case Exception: return e default: ctx := diag.NewContext(fm.srcMeta.Name, fm.srcMeta.Code, r) if _, ok := e.(errs.SetReadOnlyVar); ok { e = errs.SetReadOnlyVar{VarName: ctx.RelevantString()} } return &exception{e, &StackTrace{Head: ctx, Next: fm.traceback}} } } // Returns an Exception with specified range and error text. func (fm *Frame) errorpf(r diag.Ranger, format string, args ...interface{}) Exception { return fm.errorp(r, fmt.Errorf(format, args...)) } // Deprecate shows a deprecation message. The message is not shown if the same // deprecation message has been shown for the same location before. func (fm *Frame) Deprecate(msg string, ctx *diag.Context, minLevel int) { if prog.DeprecationLevel < minLevel { return } if ctx == nil { fmt.Fprintf(fm.ErrorFile(), "deprecation: \033[31;1m%s\033[m\n", msg) return } if fm.Evaler.registerDeprecation(deprecation{ctx.Name, ctx.Ranging, msg}) { err := diag.Error{Type: "deprecation", Message: msg, Context: *ctx} fm.ErrorFile().WriteString(err.Show("") + "\n") } } elvish-0.17.0/pkg/eval/glob.go000066400000000000000000000144531415471104000160570ustar00rootroot00000000000000package eval import ( "errors" "fmt" "os" "strings" "unicode" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/glob" "src.elv.sh/pkg/parse" ) // An ephemeral value generated when evaluating tilde and wildcards. type globPattern struct { glob.Pattern Flags globFlag Buts []string TypeCb func(os.FileMode) bool } type globFlag uint var typeCbMap = map[string]func(os.FileMode) bool{ "dir": os.FileMode.IsDir, "regular": os.FileMode.IsRegular, } const ( // noMatchOK indicates that the "nomatch-ok" glob index modifier was // present. noMatchOK globFlag = 1 << iota ) func (f globFlag) Has(g globFlag) bool { return (f & g) == g } var _ vals.ErrIndexer = globPattern{} var ( ErrMustFollowWildcard = errors.New("must follow wildcard") ErrModifierMustBeString = errors.New("modifier must be string") ErrWildcardNoMatch = errors.New("wildcard has no match") ErrMultipleTypeModifiers = errors.New("only one type modifier allowed") ErrUnknownTypeModifier = errors.New("unknown type modifier") ) var runeMatchers = map[string]func(rune) bool{ "control": unicode.IsControl, "digit": unicode.IsDigit, "graphic": unicode.IsGraphic, "letter": unicode.IsLetter, "lower": unicode.IsDigit, "mark": unicode.IsMark, "number": unicode.IsNumber, "print": unicode.IsPrint, "punct": unicode.IsPunct, "space": unicode.IsSpace, "symbol": unicode.IsSymbol, "title": unicode.IsTitle, "upper": unicode.IsUpper, } func (gp globPattern) Kind() string { return "glob-pattern" } func (gp globPattern) Index(k interface{}) (interface{}, error) { modifierv, ok := k.(string) if !ok { return nil, ErrModifierMustBeString } modifier := modifierv switch { case modifier == "nomatch-ok": gp.Flags |= noMatchOK case strings.HasPrefix(modifier, "but:"): gp.Buts = append(gp.Buts, modifier[len("but:"):]) case modifier == "match-hidden": lastSeg, err := gp.lastWildSeg() if err != nil { return nil, err } gp.Segments[len(gp.Segments)-1] = glob.Wild{ Type: lastSeg.Type, MatchHidden: true, Matchers: lastSeg.Matchers, } case strings.HasPrefix(modifier, "type:"): if gp.TypeCb != nil { return nil, ErrMultipleTypeModifiers } typeName := modifier[len("type:"):] cb, ok := typeCbMap[typeName] if !ok { return nil, ErrUnknownTypeModifier } gp.TypeCb = cb default: var matcher func(rune) bool if m, ok := runeMatchers[modifier]; ok { matcher = m } else if strings.HasPrefix(modifier, "set:") { set := modifier[len("set:"):] matcher = func(r rune) bool { return strings.ContainsRune(set, r) } } else if strings.HasPrefix(modifier, "range:") { rangeExpr := modifier[len("range:"):] badRangeExpr := fmt.Errorf("bad range modifier: %s", parse.Quote(rangeExpr)) runes := []rune(rangeExpr) if len(runes) != 3 { return nil, badRangeExpr } from, sep, to := runes[0], runes[1], runes[2] switch sep { case '-': matcher = func(r rune) bool { return from <= r && r <= to } case '~': matcher = func(r rune) bool { return from <= r && r < to } default: return nil, badRangeExpr } } else { return nil, fmt.Errorf("unknown modifier %s", vals.Repr(modifierv, vals.NoPretty)) } err := gp.addMatcher(matcher) return gp, err } return gp, nil } func (gp globPattern) Concat(v interface{}) (interface{}, error) { switch rhs := v.(type) { case string: gp.append(stringToSegments(rhs)...) return gp, nil case globPattern: // We know rhs contains exactly one segment. gp.append(rhs.Segments[0]) gp.Flags |= rhs.Flags gp.Buts = append(gp.Buts, rhs.Buts...) // This handles illegal cases such as `**[type:regular]x*[type:directory]`. if gp.TypeCb != nil && rhs.TypeCb != nil { return nil, ErrMultipleTypeModifiers } if rhs.TypeCb != nil { gp.TypeCb = rhs.TypeCb } return gp, nil } return nil, vals.ErrConcatNotImplemented } func (gp globPattern) RConcat(v interface{}) (interface{}, error) { switch lhs := v.(type) { case string: segs := stringToSegments(lhs) // We know gp contains exactly one segment. segs = append(segs, gp.Segments[0]) return globPattern{Pattern: glob.Pattern{Segments: segs}, Flags: gp.Flags, Buts: gp.Buts, TypeCb: gp.TypeCb}, nil } return nil, vals.ErrConcatNotImplemented } func (gp *globPattern) lastWildSeg() (glob.Wild, error) { if len(gp.Segments) == 0 { return glob.Wild{}, ErrBadglobPattern } if !glob.IsWild(gp.Segments[len(gp.Segments)-1]) { return glob.Wild{}, ErrMustFollowWildcard } return gp.Segments[len(gp.Segments)-1].(glob.Wild), nil } func (gp *globPattern) addMatcher(matcher func(rune) bool) error { lastSeg, err := gp.lastWildSeg() if err != nil { return err } gp.Segments[len(gp.Segments)-1] = glob.Wild{ Type: lastSeg.Type, MatchHidden: lastSeg.MatchHidden, Matchers: append(lastSeg.Matchers, matcher), } return nil } func (gp *globPattern) append(segs ...glob.Segment) { gp.Segments = append(gp.Segments, segs...) } func wildcardToSegment(s string) (glob.Segment, error) { switch s { case "*": return glob.Wild{Type: glob.Star, MatchHidden: false, Matchers: nil}, nil case "**": return glob.Wild{Type: glob.StarStar, MatchHidden: false, Matchers: nil}, nil case "?": return glob.Wild{Type: glob.Question, MatchHidden: false, Matchers: nil}, nil default: return nil, fmt.Errorf("bad wildcard: %q", s) } } func stringToSegments(s string) []glob.Segment { segs := []glob.Segment{} for i := 0; i < len(s); { j := i for ; j < len(s) && s[j] != '/'; j++ { } if j > i { segs = append(segs, glob.Literal{Data: s[i:j]}) } if j < len(s) { for ; j < len(s) && s[j] == '/'; j++ { } segs = append(segs, glob.Slash{}) i = j } else { break } } return segs } func doGlob(gp globPattern, abort <-chan struct{}) ([]interface{}, error) { but := make(map[string]struct{}) for _, s := range gp.Buts { but[s] = struct{}{} } vs := make([]interface{}, 0) if !gp.Glob(func(pathInfo glob.PathInfo) bool { select { case <-abort: logger.Println("glob aborted") return false default: } if _, ignore := but[pathInfo.Path]; ignore { return true } if gp.TypeCb == nil || gp.TypeCb(pathInfo.Info.Mode()) { vs = append(vs, pathInfo.Path) } return true }) { return nil, ErrInterrupted } if len(vs) == 0 && !gp.Flags.Has(noMatchOK) { return nil, ErrWildcardNoMatch } return vs, nil } elvish-0.17.0/pkg/eval/glob_test.go000066400000000000000000000064341415471104000171160ustar00rootroot00000000000000package eval_test import ( "testing" . "src.elv.sh/pkg/eval" . "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/testutil" ) func TestGlob_Simple(t *testing.T) { testutil.InTempDir(t) testutil.MustMkdirAll("z", "z2") testutil.MustCreateEmpty("bar", "foo", "ipsum", "lorem") Test(t, That("put *").Puts("bar", "foo", "ipsum", "lorem", "z", "z2"), That("put z*").Puts("z", "z2"), That("put ?").Puts("z"), That("put ????m").Puts("ipsum", "lorem"), ) } func TestGlob_Recursive(t *testing.T) { testutil.InTempDir(t) testutil.MustMkdirAll("1/2/3") testutil.MustCreateEmpty("a.go", "1/a.go", "1/2/3/a.go") Test(t, That("put **").Puts("1/2/3/a.go", "1/2/3", "1/2", "1/a.go", "1", "a.go"), That("put **.go").Puts("1/2/3/a.go", "1/a.go", "a.go"), That("put 1**.go").Puts("1/2/3/a.go", "1/a.go"), ) } func TestGlob_NoMatch(t *testing.T) { testutil.InTempDir(t) Test(t, That("put a/b/nonexistent*").Throws(ErrWildcardNoMatch), That("put a/b/nonexistent*[nomatch-ok]").DoesNothing(), ) } func TestGlob_MatchHidden(t *testing.T) { testutil.InTempDir(t) testutil.MustMkdirAll("d", ".d") testutil.MustCreateEmpty("a", ".a", "d/a", "d/.a", ".d/a", ".d/.a") Test(t, That("put *").Puts("a", "d"), That("put *[match-hidden]").Puts(".a", ".d", "a", "d"), That("put *[match-hidden]/*").Puts(".d/a", "d/a"), That("put */*[match-hidden]").Puts("d/.a", "d/a"), That("put *[match-hidden]/*[match-hidden]").Puts( ".d/.a", ".d/a", "d/.a", "d/a"), ) } func TestGlob_RuneMatchers(t *testing.T) { testutil.InTempDir(t) testutil.MustCreateEmpty("a1", "a2", "b1", "c1", "ipsum", "lorem") Test(t, That("put *[letter]").Puts("ipsum", "lorem"), That("put ?[set:ab]*").Puts("a1", "a2", "b1"), That("put ?[range:a-c]*").Puts("a1", "a2", "b1", "c1"), That("put ?[range:a~c]*").Puts("a1", "a2", "b1"), That("put *[range:a-z]").Puts("ipsum", "lorem"), That("put *[range:a-zz]").Throws(ErrorWithMessage("bad range modifier: a-zz")), That("put *[range:foo]").Throws(ErrorWithMessage("bad range modifier: foo")), ) } func TestGlob_But(t *testing.T) { testutil.InTempDir(t) testutil.MustCreateEmpty("bar", "foo", "ipsum", "lorem") Test(t, // Nonexistent files can also be excluded That("put *[but:foobar][but:ipsum]").Puts("bar", "foo", "lorem"), ) } func TestGlob_Type(t *testing.T) { testutil.InTempDir(t) testutil.MustMkdirAll("d1", "d2", ".d", "b/c") testutil.MustCreateEmpty("bar", "foo", "ipsum", "lorem", "d1/f1", "d2/fm") Test(t, That("put **[type:dir]").Puts("b/c", "b", "d1", "d2"), That("put **[type:regular]m").Puts("d2/fm", "ipsum", "lorem"), That("put **[type:regular]f*").Puts("d1/f1", "d2/fm", "foo"), That("put **f*[type:regular]").Puts("d1/f1", "d2/fm", "foo"), That("put *[type:dir][type:regular]").Throws(ErrMultipleTypeModifiers), That("put **[type:dir]f*[type:regular]").Throws(ErrMultipleTypeModifiers), That("put **[type:unknown]").Throws(ErrUnknownTypeModifier), ) } func TestGlob_BadOperation(t *testing.T) { testutil.InTempDir(t) Test(t, That("put *[[]]").Throws(ErrModifierMustBeString), That("put *[bad-mod]").Throws(ErrorWithMessage("unknown modifier bad-mod")), That("put *{ }"). Throws(ErrorWithMessage("cannot concatenate glob-pattern and fn")), That("put { }*"). Throws(ErrorWithMessage("cannot concatenate fn and glob-pattern")), ) } elvish-0.17.0/pkg/eval/go_fn.go000066400000000000000000000164051415471104000162230ustar00rootroot00000000000000package eval import ( "errors" "fmt" "reflect" "unsafe" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/persistent/hash" ) var ( // ErrNoOptAccepted is thrown when a Go function that does not accept any // options gets passed options. ErrNoOptAccepted = errors.New("function does not accept any options") ) type goFn struct { name string impl interface{} // Type information of impl. // If true, pass the frame as a *Frame argument. frame bool // If true, pass options as a RawOptions argument. rawOptions bool // If not nil, type of the parameter that gets options via RawOptions.Scan. options reflect.Type // If not nil, pass the inputs as an Input-typed last argument. inputs bool // Type of "normal" (non-frame, non-options, non-variadic) arguments. normalArgs []reflect.Type // If not nil, type of variadic arguments. variadicArg reflect.Type } // An interface to be implemented by pointers to structs that should hold // scanned options. type optionsPtr interface { SetDefaultOptions() } // Inputs is the type that the last parameter of a Go-native function can take. // When that is the case, it is a callback to get inputs. See the doc of GoFn // for details. type Inputs func(func(interface{})) var ( frameType = reflect.TypeOf((*Frame)(nil)) rawOptionsType = reflect.TypeOf(RawOptions(nil)) optionsPtrType = reflect.TypeOf((*optionsPtr)(nil)).Elem() inputsType = reflect.TypeOf(Inputs(nil)) ) // NewGoFn wraps a Go function into an Elvish function using reflection. // // Parameters are passed following these rules: // // 1. If the first parameter of function has type *Frame, it gets the current // call frame. // // 2. After the potential *Frame argument, the first parameter has type // RawOptions, it gets a map of option names to their values. // // Alternatively, this parameter may be a (non-pointer) struct whose pointer // type implements a SetDefaultOptions method that takes no arguments and has no // return value. In this case, a new instance of the struct is constructed, the // SetDefaultOptions method is called, and any option passed to the Elvish // function is used to populate the fields of the struct. Field names are mapped // to option names using strutil.CamelToDashed, unless they have a field tag // "name", in which case the tag is preferred. // // If the function does not declare that it accepts options via either method // described above, it accepts no options. // // 3. If the last parameter is non-variadic and has type Inputs, it represents // an optional parameter that contains the input to this function. If the // argument is not supplied, the input channel of the Frame will be used to // supply the inputs. // // 4. Other parameters are converted using vals.ScanToGo. // // Return values are written to the stdout channel, after being converted using // vals.FromGo. Return values whose types are arrays or slices, and not defined // types, have their individual elements written to the output. // // If the last return value has nominal type error and is not nil, it is turned // into an exception and no return value is written. If the last return value is // a nil error, it is ignored. func NewGoFn(name string, impl interface{}) Callable { implType := reflect.TypeOf(impl) b := &goFn{name: name, impl: impl} i := 0 if i < implType.NumIn() && implType.In(i) == frameType { b.frame = true i++ } if i < implType.NumIn() && implType.In(i) == rawOptionsType { b.rawOptions = true i++ } if i < implType.NumIn() && reflect.PtrTo(implType.In(i)).Implements(optionsPtrType) { if b.rawOptions { panic("Function declares both RawOptions and Options parameters") } b.options = implType.In(i) i++ } for ; i < implType.NumIn(); i++ { paramType := implType.In(i) if i == implType.NumIn()-1 { if implType.IsVariadic() { b.variadicArg = paramType.Elem() break } else if paramType == inputsType { b.inputs = true break } } b.normalArgs = append(b.normalArgs, paramType) } return b } // Kind returns "fn". func (*goFn) Kind() string { return "fn" } // Equal compares identity. func (b *goFn) Equal(rhs interface{}) bool { return b == rhs } // Hash hashes the address. func (b *goFn) Hash() uint32 { return hash.Pointer(unsafe.Pointer(b)) } // Repr returns an opaque representation "". func (b *goFn) Repr(int) string { return "" } // error(nil) is treated as nil by reflect.TypeOf, so we first get the type of // *error and use Elem to obtain type of error. var errorType = reflect.TypeOf((*error)(nil)).Elem() // Call calls the implementation using reflection. func (b *goFn) Call(f *Frame, args []interface{}, opts map[string]interface{}) error { if b.variadicArg != nil { if len(args) < len(b.normalArgs) { return errs.ArityMismatch{What: "arguments", ValidLow: len(b.normalArgs), ValidHigh: -1, Actual: len(args)} } } else if b.inputs { if len(args) != len(b.normalArgs) && len(args) != len(b.normalArgs)+1 { return errs.ArityMismatch{What: "arguments", ValidLow: len(b.normalArgs), ValidHigh: len(b.normalArgs) + 1, Actual: len(args)} } } else if len(args) != len(b.normalArgs) { return errs.ArityMismatch{What: "arguments", ValidLow: len(b.normalArgs), ValidHigh: len(b.normalArgs), Actual: len(args)} } if !b.rawOptions && b.options == nil && len(opts) > 0 { return ErrNoOptAccepted } var in []reflect.Value if b.frame { in = append(in, reflect.ValueOf(f)) } if b.rawOptions { in = append(in, reflect.ValueOf(opts)) } if b.options != nil { ptrValue := reflect.New(b.options) ptr := ptrValue.Interface() ptr.(optionsPtr).SetDefaultOptions() err := scanOptions(opts, ptr) if err != nil { return err } in = append(in, ptrValue.Elem()) } for i, arg := range args { var typ reflect.Type if i < len(b.normalArgs) { typ = b.normalArgs[i] } else if b.variadicArg != nil { typ = b.variadicArg } else if b.inputs { break // Handled after the loop } else { panic("impossible") } ptr := reflect.New(typ) err := vals.ScanToGo(arg, ptr.Interface()) if err != nil { return fmt.Errorf("wrong type of argument %d: %v", i, err) } in = append(in, ptr.Elem()) } if b.inputs { var inputs Inputs if len(args) == len(b.normalArgs) { inputs = f.IterateInputs } else { // Wrap an iterable argument in Inputs. iterable := args[len(args)-1] if !vals.CanIterate(iterable) { return fmt.Errorf("%s cannot be iterated", vals.Kind(iterable)) } inputs = func(f func(interface{})) { // CanIterate(iterable) is true _ = vals.Iterate(iterable, func(v interface{}) bool { f(v) return true }) } } in = append(in, reflect.ValueOf(inputs)) } rets := reflect.ValueOf(b.impl).Call(in) if len(rets) > 0 && rets[len(rets)-1].Type() == errorType { err := rets[len(rets)-1].Interface() if err != nil { return err.(error) } rets = rets[:len(rets)-1] } out := f.ValueOutput() for _, ret := range rets { t := ret.Type() k := t.Kind() if (k == reflect.Slice || k == reflect.Array) && t.Name() == "" { for i := 0; i < ret.Len(); i++ { err := out.Put(vals.FromGo(ret.Index(i).Interface())) if err != nil { return err } } } else { err := out.Put(vals.FromGo(ret.Interface())) if err != nil { return err } } } return nil } elvish-0.17.0/pkg/eval/go_fn_internal_test.go000066400000000000000000000005521415471104000211520ustar00rootroot00000000000000package eval import ( "testing" "unsafe" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/persistent/hash" ) func TestGoFnAsValue(t *testing.T) { fn1 := NewGoFn("fn1", func() {}) fn2 := NewGoFn("fn2", func() {}) vals.TestValue(t, fn1). Kind("fn"). Hash(hash.Pointer(unsafe.Pointer(fn1.(*goFn)))). Equal(fn1). NotEqual(fn2). Repr("") } elvish-0.17.0/pkg/eval/go_fn_test.go000066400000000000000000000112441415471104000172560ustar00rootroot00000000000000package eval_test import ( "errors" "math/big" "reflect" "testing" . "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/errs" . "src.elv.sh/pkg/eval/evaltest" ) type someOptions struct { Foo string Bar string } func (o *someOptions) SetDefaultOptions() { o.Bar = "default" } //lint:ignore ST1012 test code var anError = errors.New("an error") type namedSlice []string func TestGoFn_RawOptions(t *testing.T) { Test(t, That("f").DoesNothing(). WithSetup(f(func() {})), // RawOptions That("f &foo=bar").DoesNothing(). WithSetup(f(func(opts RawOptions) { if opts["foo"] != "bar" { t.Errorf("RawOptions parameter doesn't get options") } })), // Options when the function does not accept options. That("f &foo=bar").Throws(ErrNoOptAccepted). WithSetup(f(func() { t.Errorf("Function called when there are extra options") })), // Parsed options That("f &foo=bar").DoesNothing(). WithSetup(f(func(opts someOptions) { if opts.Foo != "bar" { t.Errorf("ScanOptions parameter doesn't get options") } if opts.Bar != "default" { t.Errorf("ScanOptions parameter doesn't use default value") } })), // Invalid option; regression test for #958. That("f &bad=bar").Throws(AnyError). WithSetup(f(func(opts someOptions) { t.Errorf("function called when there are invalid options") })), // Invalid option type; regression test for #958. That("f &foo=[]").Throws(AnyError). WithSetup(f(func(opts someOptions) { t.Errorf("function called when there are invalid options") })), // Argument That("f lorem ipsum").DoesNothing(). WithSetup(f(func(x, y string) { if x != "lorem" { t.Errorf("Argument x not passed") } if y != "ipsum" { t.Errorf("Argument y not passed") } })), // Variadic arguments That("f lorem ipsum").DoesNothing(). WithSetup(f(func(args ...string) { wantArgs := []string{"lorem", "ipsum"} if !reflect.DeepEqual(args, wantArgs) { t.Errorf("got args %v, want %v", args, wantArgs) } })), // Argument conversion That("f 314 1.25").DoesNothing(). WithSetup(f(func(i int, f float64) { if i != 314 { t.Errorf("Integer argument i not passed") } if f != 1.25 { t.Errorf("Float argument f not passed") } })), // Inputs That("f [foo bar]").DoesNothing(). WithSetup(f(testInputs(t, "foo", "bar"))), That("f [foo bar]").DoesNothing(). WithSetup(f(testInputs(t, "foo", "bar"))), // Too many arguments That("f x"). Throws(errs.ArityMismatch{What: "arguments", ValidLow: 0, ValidHigh: 0, Actual: 1}). WithSetup(f(func() { t.Errorf("Function called when there are too many arguments") })), // Too few arguments That("f"). Throws(errs.ArityMismatch{What: "arguments", ValidLow: 1, ValidHigh: 1, Actual: 0}). WithSetup(f(func(x string) { t.Errorf("Function called when there are too few arguments") })), That("f"). Throws(errs.ArityMismatch{What: "arguments", ValidLow: 1, ValidHigh: -1, Actual: 0}). WithSetup(f(func(x string, y ...string) { t.Errorf("Function called when there are too few arguments") })), // Wrong argument type That("f (num 1)").Throws(AnyError). WithSetup(f(func(x string) { t.Errorf("Function called when arguments have wrong type") })), That("f str").Throws(AnyError). WithSetup(f(func(x int) { t.Errorf("Function called when arguments have wrong type") })), // Return value That("f").Puts("foo"). WithSetup(f(func() string { return "foo" })), // Return value conversion That("f").Puts(314). WithSetup(f(func() *big.Int { return big.NewInt(314) })), // Slice and array return value That("f").Puts("foo", "bar"). WithSetup(f(func() []string { return []string{"foo", "bar"} })), That("f").Puts("foo", "bar"). WithSetup(f(func() [2]string { return [2]string{"foo", "bar"} })), // Named types with underlying slice type treated as a single value That("f").Puts(namedSlice{"foo", "bar"}). WithSetup(f(func() namedSlice { return namedSlice{"foo", "bar"} })), // Error return value That("f").Throws(anError). WithSetup(f(func() (string, error) { return "x", anError })), That("f").DoesNothing(). WithSetup(f(func() error { return nil })), ) } func f(body interface{}) func(*Evaler) { return func(ev *Evaler) { ev.ExtendGlobal(BuildNs().AddGoFn("f", body)) } } func testInputs(t *testing.T, wantValues ...interface{}) func(Inputs) { return func(i Inputs) { t.Helper() var values []interface{} i(func(x interface{}) { values = append(values, x) }) wantValues := []interface{}{"foo", "bar"} if !reflect.DeepEqual(values, wantValues) { t.Errorf("Inputs parameter didn't get supplied inputs") } } } elvish-0.17.0/pkg/eval/interrupts.go000066400000000000000000000026521415471104000173510ustar00rootroot00000000000000package eval import ( "errors" "os" "os/signal" "syscall" ) // Interrupts returns a channel that is closed when an interrupt signal comes. func (fm *Frame) Interrupts() <-chan struct{} { return fm.intCh } // ErrInterrupted is thrown when the execution is interrupted by a signal. var ErrInterrupted = errors.New("interrupted") // IsInterrupted reports whether there has been an interrupt. func (fm *Frame) IsInterrupted() bool { select { case <-fm.Interrupts(): return true default: return false } } // ListenInterrupts returns a channel that is closed when SIGINT or SIGQUIT // has been received by the process. It also returns a function that should be // called when the channel is no longer needed. func ListenInterrupts() (<-chan struct{}, func()) { sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGQUIT) // Channel to return, closed after receiving the first SIGINT or SIGQUIT. intCh := make(chan struct{}) // Closed in the cleanup function to request the relaying goroutine to stop. stop := make(chan struct{}) // Closed in the relaying goroutine to signal that it has stopped. stopped := make(chan struct{}) go func() { closed := false loop: for { select { case <-sigCh: if !closed { close(intCh) closed = true } case <-stop: break loop } } signal.Stop(sigCh) close(stopped) }() return intCh, func() { close(stop) <-stopped } } elvish-0.17.0/pkg/eval/node_utils.go000066400000000000000000000035201415471104000172720ustar00rootroot00000000000000package eval import ( "src.elv.sh/pkg/diag" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/parse/cmpd" ) // Utilities for working with nodes. func stringLiteralOrError(cp *compiler, n *parse.Compound, what string) string { s, err := cmpd.StringLiteralOrError(n, what) if err != nil { cp.errorpf(n, "%v", err) } return s } type errorpfer interface { errorpf(r diag.Ranger, fmt string, args ...interface{}) } // argsWalker is used by builtin special forms to implement argument parsing. type argsWalker struct { cp errorpfer form *parse.Form idx int } func (cp *compiler) walkArgs(f *parse.Form) *argsWalker { return &argsWalker{cp, f, 0} } func (aw *argsWalker) more() bool { return aw.idx < len(aw.form.Args) } func (aw *argsWalker) peek() *parse.Compound { if !aw.more() { aw.cp.errorpf(aw.form, "need more arguments") } return aw.form.Args[aw.idx] } func (aw *argsWalker) next() *parse.Compound { n := aw.peek() aw.idx++ return n } // nextIs returns whether the next argument's source matches the given text. It // also consumes the argument if it is. func (aw *argsWalker) nextIs(text string) bool { if aw.more() && parse.SourceText(aw.form.Args[aw.idx]) == text { aw.idx++ return true } return false } // nextMustLambda fetches the next argument, raising an error if it is not a // lambda. func (aw *argsWalker) nextMustLambda(what string) *parse.Primary { n := aw.next() pn, ok := cmpd.Lambda(n) if !ok { aw.cp.errorpf(n, "%s must be lambda, found %s", what, cmpd.Shape(n)) } return pn } func (aw *argsWalker) nextMustLambdaIfAfter(leader string) *parse.Primary { if aw.nextIs(leader) { return aw.nextMustLambda(leader + " body") } return nil } func (aw *argsWalker) mustEnd() { if aw.more() { aw.cp.errorpf(diag.Ranging{From: aw.form.Args[aw.idx].Range().From, To: aw.form.Range().To}, "too many arguments") } } elvish-0.17.0/pkg/eval/ns.go000066400000000000000000000172071415471104000155540ustar00rootroot00000000000000package eval import ( "fmt" "unsafe" "src.elv.sh/pkg/eval/vars" "src.elv.sh/pkg/persistent/hash" ) // Ns is the runtime representation of a namespace. The zero value of Ns is an // empty namespace. To create a non-empty Ns, use either NsBuilder or CombineNs. // // An Ns is immutable after its associated code chunk has finished execution. type Ns struct { // All variables in the namespace. Static variable accesses are compiled // into indexed accesses into this slice. slots []vars.Var // Static information for each variable, reflecting the state when the // associated code chunk has finished execution. // // This is only used for introspection and seeding the compilation of a new // code chunk. Normal static variable accesses are compiled into indexed // accesses into the slots slice. // // This is a slice instead of a map with the names of variables as keys, // because most namespaces are small enough for linear lookup to be faster // than map access. infos []staticVarInfo } // Nser is anything that can be converted to an *Ns. type Nser interface { Ns() *Ns } // Static information known about a variable. type staticVarInfo struct { name string readOnly bool // Deleted variables can still be kept in the Ns since there might be a // reference to them in a closure. Shadowed variables are also considered // deleted. deleted bool } // CombineNs returns an *Ns that contains all the bindings from both ns1 and // ns2. Names in ns2 takes precedence over those in ns1. func CombineNs(ns1, ns2 *Ns) *Ns { ns := &Ns{ append([]vars.Var(nil), ns2.slots...), append([]staticVarInfo(nil), ns2.infos...)} hasName := map[string]bool{} for _, info := range ns.infos { if !info.deleted { hasName[info.name] = true } } for i, info := range ns1.infos { if !info.deleted && !hasName[info.name] { ns.slots = append(ns.slots, ns1.slots[i]) ns.infos = append(ns.infos, info) } } return ns } // Ns returns ns itself. func (ns *Ns) Ns() *Ns { return ns } // Kind returns "ns". func (ns *Ns) Kind() string { return "ns" } // Hash returns a hash of the address of ns. func (ns *Ns) Hash() uint32 { return hash.Pointer(unsafe.Pointer(ns)) } // Equal returns whether rhs has the same identity as ns. func (ns *Ns) Equal(rhs interface{}) bool { if ns2, ok := rhs.(*Ns); ok { return ns == ns2 } return false } // Repr returns an opaque representation of the Ns showing its address. func (ns *Ns) Repr(int) string { return fmt.Sprintf("", ns) } // Index looks up a variable with the given name, and returns its value if it // exists. This is only used for introspection from Elvish code; for // introspection from Go code, use IndexName. func (ns *Ns) Index(k interface{}) (interface{}, bool) { if ks, ok := k.(string); ok { variable := ns.IndexString(ks) if variable == nil { return nil, false } return variable.Get(), true } return nil, false } // IndexName looks up a variable with the given name, and returns its value if // it exists, or nil if it does not. This is the type-safe version of Index and // is useful for introspection from Go code. func (ns *Ns) IndexString(k string) vars.Var { _, i := ns.lookup(k) if i != -1 { return ns.slots[i] } return nil } func (ns *Ns) lookup(k string) (staticVarInfo, int) { for i, info := range ns.infos { if info.name == k && !info.deleted { return info, i } } return staticVarInfo{}, -1 } // IterateKeys produces the names of all the variables in this Ns. func (ns *Ns) IterateKeys(f func(interface{}) bool) { for _, info := range ns.infos { if info.deleted { continue } if !f(info.name) { break } } } // IterateKeysString produces the names of all variables in the Ns. It is the // type-safe version of IterateKeys and is useful for introspection from Go // code. It doesn't support breaking early. func (ns *Ns) IterateKeysString(f func(string)) { for _, info := range ns.infos { if !info.deleted { f(info.name) } } } // HasKeyString reports whether the Ns has a variable with the given name. func (ns *Ns) HasKeyString(k string) bool { for _, info := range ns.infos { if info.name == k && !info.deleted { return true } } return false } func (ns *Ns) static() *staticNs { return &staticNs{ns.infos} } // NsBuilder is a helper type used for building an Ns. type NsBuilder struct { prefix string m map[string]vars.Var } // BuildNs returns a helper for building an Ns. func BuildNs() NsBuilder { return BuildNsNamed("") } // BuildNs returns a helper for building an Ns with the given name. The name is // only used for the names of Go functions. func BuildNsNamed(name string) NsBuilder { prefix := "" if name != "" { prefix = "<" + name + ">:" } return NsBuilder{prefix, make(map[string]vars.Var)} } // Add adds a variable. func (nb NsBuilder) AddVar(name string, v vars.Var) NsBuilder { nb.m[name] = v return nb } // AddVars adds all the variables given in the map. func (nb NsBuilder) AddVars(m map[string]vars.Var) NsBuilder { for name, v := range m { nb.AddVar(name, v) } return nb } // AddFn adds a function. The resulting variable will be read-only. func (nb NsBuilder) AddFn(name string, v Callable) NsBuilder { return nb.AddVar(name+FnSuffix, vars.NewReadOnly(v)) } // AddNs adds a sub-namespace. The resulting variable will be read-only. func (nb NsBuilder) AddNs(name string, v Nser) NsBuilder { return nb.AddVar(name+NsSuffix, vars.NewReadOnly(v.Ns())) } // AddGoFn adds a Go function. The resulting variable will be read-only. func (nb NsBuilder) AddGoFn(name string, impl interface{}) NsBuilder { return nb.AddFn(name, NewGoFn(nb.prefix+name, impl)) } // AddGoFns adds Go functions. The resulting variables will be read-only. func (nb NsBuilder) AddGoFns(fns map[string]interface{}) NsBuilder { for name, impl := range fns { nb.AddGoFn(name, impl) } return nb } // Ns builds a namespace. func (nb NsBuilder) Ns() *Ns { n := len(nb.m) ns := &Ns{make([]vars.Var, n), make([]staticVarInfo, n)} i := 0 for name, variable := range nb.m { ns.slots[i] = variable ns.infos[i] = staticVarInfo{name, vars.IsReadOnly(variable), false} i++ } return ns } // The compile-time representation of a namespace. Called "static" namespace // since it contains information that are known without executing the code. // The data structure itself, however, is not static, and gets mutated as the // compiler gains more information about the namespace. The zero value of // staticNs is an empty namespace. type staticNs struct { infos []staticVarInfo } func (ns *staticNs) clone() *staticNs { return &staticNs{append([]staticVarInfo(nil), ns.infos...)} } func (ns *staticNs) del(k string) { if _, i := ns.lookup(k); i != -1 { ns.infos[i].deleted = true } } // Adds a name, shadowing any existing one, and returns the index for the new // name. func (ns *staticNs) add(k string) int { ns.del(k) ns.infos = append(ns.infos, staticVarInfo{k, false, false}) return len(ns.infos) - 1 } func (ns *staticNs) lookup(k string) (staticVarInfo, int) { for i, info := range ns.infos { if info.name == k && !info.deleted { return info, i } } return staticVarInfo{}, -1 } type staticUpNs struct { infos []upvalInfo } type upvalInfo struct { name string // Whether the upvalue comes from the immediate outer scope, i.e. the local // scope a lambda is evaluated in. local bool // Index of the upvalue variable. If local is true, this is an index into // the local scope. If local is false, this is an index into the up scope. index int } func (up *staticUpNs) add(k string, local bool, index int) int { for i, info := range up.infos { if info.name == k { return i } } up.infos = append(up.infos, upvalInfo{k, local, index}) return len(up.infos) - 1 } elvish-0.17.0/pkg/eval/ns_test.go000066400000000000000000000013301415471104000166010ustar00rootroot00000000000000package eval_test import ( "testing" . "src.elv.sh/pkg/eval/evaltest" ) func TestNs(t *testing.T) { Test(t, That("kind-of (ns [&])").Puts("ns"), // A Ns is only equal to itself That("ns = (ns [&]); eq $ns $ns").Puts(true), That("eq (ns [&]) (ns [&])").Puts(false), That("eq (ns [&]) [&]").Puts(false), That(`ns: = (ns [&a=b &x=y]); put $ns:a`).Puts("b"), That(`ns: = (ns [&a=b &x=y]); put $ns:[a]`).Puts("b"), // Test multi-key ns when sorting is possible That(`keys (ns [&a=b])`).Puts("a"), That(`has-key (ns [&a=b &x=y]) a`).Puts(true), That(`has-key (ns [&a=b &x=y]) b`).Puts(false), ) } func TestBuiltinFunctionsReadOnly(t *testing.T) { Test(t, That("return~ = { }").DoesNotCompile(), ) } elvish-0.17.0/pkg/eval/options.go000066400000000000000000000027671415471104000166340ustar00rootroot00000000000000package eval import ( "fmt" "reflect" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/strutil" ) // RawOptions is the type of an argument a Go-native function can take to // declare that it wants to parse options itself. See the doc of NewGoFn for // details. type RawOptions map[string]interface{} // Takes a raw option map and a pointer to a struct, and populate the struct // with options. A field named FieldName corresponds to the option named // field-name, unless the field has a explicit "name" tag. Fields typed // ParsedOptions are ignored. func scanOptions(rawOpts RawOptions, ptr interface{}) error { ptrValue := reflect.ValueOf(ptr) if ptrValue.Kind() != reflect.Ptr || ptrValue.Elem().Kind() != reflect.Struct { return fmt.Errorf( "internal bug: need struct ptr to scan options, got %T", ptr) } // fieldIdxForOpt maps option name to the index of field in `struc`. fieldIdxForOpt := make(map[string]int) struc := ptrValue.Elem() for i := 0; i < struc.Type().NumField(); i++ { if !struc.Field(i).CanSet() { continue // ignore unexported fields } f := struc.Type().Field(i) optName := f.Tag.Get("name") if optName == "" { optName = strutil.CamelToDashed(f.Name) } fieldIdxForOpt[optName] = i } for k, v := range rawOpts { fieldIdx, ok := fieldIdxForOpt[k] if !ok { return fmt.Errorf("unknown option %s", parse.Quote(k)) } err := vals.ScanToGo(v, struc.Field(fieldIdx).Addr().Interface()) if err != nil { return err } } return nil } elvish-0.17.0/pkg/eval/options_test.go000066400000000000000000000023411415471104000176570ustar00rootroot00000000000000package eval import ( "errors" "testing" ) type opts struct { FooBar string POSIX bool `name:"posix"` Min int ignore bool // this should be ignored since it isn't exported } var scanOptionsTests = []struct { rawOpts RawOptions preScan opts postScan opts err error }{ {RawOptions{"foo-bar": "lorem ipsum"}, opts{}, opts{FooBar: "lorem ipsum"}, nil}, {RawOptions{"posix": true}, opts{}, opts{POSIX: true}, nil}, // Since "ignore" is not exported it will result in an error when used. {RawOptions{"ignore": true}, opts{}, opts{ignore: false}, errors.New("unknown option ignore")}, } func TestScanOptions(t *testing.T) { // scanOptions requires a pointer to struct. err := scanOptions(RawOptions{}, opts{}) if err == nil { t.Errorf("Scan should have reported invalid options arg error") } for _, test := range scanOptionsTests { opts := test.preScan err := scanOptions(test.rawOpts, &opts) if ((err == nil) != (test.err == nil)) || (err != nil && test.err != nil && err.Error() != test.err.Error()) { t.Errorf("Scan error mismatch %v: want %q, got %q", test.rawOpts, test.err, err) } if opts != test.postScan { t.Errorf("Scan %v => %v, want %v", test.rawOpts, opts, test.postScan) } } } elvish-0.17.0/pkg/eval/plugin.go000066400000000000000000000001401415471104000164160ustar00rootroot00000000000000//go:build !gccgo // +build !gccgo package eval import "plugin" var pluginOpen = plugin.Open elvish-0.17.0/pkg/eval/plugin_gccgo.go000066400000000000000000000005521415471104000175670ustar00rootroot00000000000000//go:build gccgo // +build gccgo package eval import "errors" var errPluginNotImplemented = errors.New("plugin not implemented") type pluginStub struct{} func pluginOpen(name string) (pluginStub, error) { return pluginStub{}, errPluginNotImplemented } func (pluginStub) Lookup(symName string) (interface{}, error) { return nil, errPluginNotImplemented } elvish-0.17.0/pkg/eval/port.go000066400000000000000000000202331415471104000161110ustar00rootroot00000000000000package eval import ( "bufio" "errors" "fmt" "io" "os" "sync" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/strutil" ) // Port conveys data stream. It always consists of a byte band and a channel band. type Port struct { File *os.File Chan chan interface{} closeFile bool closeChan bool // The following two fields are populated as an additional control // mechanism for output ports. When no more value should be send on Chan, // chanSendError is populated and chanSendStop is closed. This is used for // both detection of reader termination (see readerGone below) and closed // ports. sendStop chan struct{} sendError *error // Only populated in output ports writing to another command in a pipeline. // When the reading end of the pipe exits, it stores 1 in readerGone. This // is used to check if an external command killed by SIGPIPE is caused by // the termination of the reader of the pipe. readerGone *int32 } // ErrNoValueOutput is thrown when writing to a pipe without a value output // component. var ErrNoValueOutput = errors.New("port has no value output") // A closed channel, suitable as a value for Port.sendStop when there is no // reader to start with. var closedSendStop = make(chan struct{}) func init() { close(closedSendStop) } // Returns a copy of the Port with the Close* flags unset. func (p *Port) fork() *Port { return &Port{p.File, p.Chan, false, false, p.sendStop, p.sendError, p.readerGone} } // Closes a Port. func (p *Port) close() { if p == nil { return } if p.closeFile { p.File.Close() } if p.closeChan { close(p.Chan) } } var ( // ClosedChan is a closed channel, suitable as a placeholder input channel. ClosedChan = getClosedChan() // BlackholeChan is a channel that absorbs all values written to it, // suitable as a placeholder output channel. BlackholeChan = getBlackholeChan() // DevNull is /dev/null, suitable as a placeholder file for either input or // output. DevNull = getDevNull() // DummyInputPort is a port made up from DevNull and ClosedChan, suitable as // a placeholder input port. DummyInputPort = &Port{File: DevNull, Chan: ClosedChan} // DummyOutputPort is a port made up from DevNull and BlackholeChan, // suitable as a placeholder output port. DummyOutputPort = &Port{File: DevNull, Chan: BlackholeChan} // DummyPorts contains 3 dummy ports, suitable as stdin, stdout and stderr. DummyPorts = []*Port{DummyInputPort, DummyOutputPort, DummyOutputPort} ) func getClosedChan() chan interface{} { ch := make(chan interface{}) close(ch) return ch } func getBlackholeChan() chan interface{} { ch := make(chan interface{}) go func() { for range ch { } }() return ch } func getDevNull() *os.File { f, err := os.Open(os.DevNull) if err != nil { fmt.Fprintf(os.Stderr, "cannot open %s, shell might not function normally\n", os.DevNull) } return f } // PipePort returns an output *Port whose value and byte components are both // piped. The supplied functions are called on a separate goroutine with the // read ends of the value and byte components of the port. It also returns a // function to clean up the port and wait for the callbacks to finish. func PipePort(vCb func(<-chan interface{}), bCb func(*os.File)) (*Port, func(), error) { r, w, err := os.Pipe() if err != nil { return nil, nil, err } ch := make(chan interface{}, outputCaptureBufferSize) var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() vCb(ch) }() go func() { defer wg.Done() defer r.Close() bCb(r) }() port := &Port{Chan: ch, closeChan: true, File: w, closeFile: true} done := func() { port.close() wg.Wait() } return port, done, nil } // CapturePort returns an output *Port whose value and byte components are // both connected to an internal pipe that saves the output. It also returns a // function to call to obtain the captured output. func CapturePort() (*Port, func() []interface{}, error) { vs := []interface{}{} var m sync.Mutex port, done, err := PipePort( func(ch <-chan interface{}) { for v := range ch { m.Lock() vs = append(vs, v) m.Unlock() } }, func(r *os.File) { buffered := bufio.NewReader(r) for { line, err := buffered.ReadString('\n') if line != "" { v := strutil.ChopLineEnding(line) m.Lock() vs = append(vs, v) m.Unlock() } if err != nil { if err != io.EOF { logger.Println("error on reading:", err) } break } } }) if err != nil { return nil, nil, err } return port, func() []interface{} { done() return vs }, nil } // StringCapturePort is like CapturePort, but processes value outputs by // stringifying them and prepending an output marker. func StringCapturePort() (*Port, func() []string, error) { var lines []string var mu sync.Mutex addLine := func(line string) { mu.Lock() defer mu.Unlock() lines = append(lines, line) } port, done, err := PipePort( func(ch <-chan interface{}) { for v := range ch { addLine("▶ " + vals.ToString(v)) } }, func(r *os.File) { bufr := bufio.NewReader(r) for { line, err := bufr.ReadString('\n') if err != nil { if err != io.EOF { addLine("i/o error: " + err.Error()) } break } addLine(strutil.ChopLineEnding(line)) } }) if err != nil { return nil, nil, err } return port, func() []string { done() return lines }, nil } // Buffer size for the channel to use in FilePort. The value has been chosen // arbitrarily. const filePortChanSize = 32 // FilePort returns an output *Port where the byte component is the file itself, // and the value component is converted to an internal channel that writes // each value to the file, prepending with a prefix. It also returns a cleanup // function, which should be called when the *Port is no longer needed. func FilePort(f *os.File, valuePrefix string) (*Port, func()) { ch := make(chan interface{}, filePortChanSize) relayDone := make(chan struct{}) go func() { for v := range ch { f.WriteString(valuePrefix) f.WriteString(vals.Repr(v, vals.NoPretty)) f.WriteString("\n") } close(relayDone) }() return &Port{File: f, Chan: ch}, func() { close(ch) <-relayDone } } // PortsFromStdFiles is a shorthand for calling PortsFromFiles with os.Stdin, // os.Stdout and os.Stderr. func PortsFromStdFiles(prefix string) ([]*Port, func()) { return PortsFromFiles([3]*os.File{os.Stdin, os.Stdout, os.Stderr}, prefix) } // PortsFromFiles builds 3 ports from 3 files. It also returns a function that // should be called when the ports are no longer needed. func PortsFromFiles(files [3]*os.File, prefix string) ([]*Port, func()) { port1, cleanup1 := FilePort(files[1], prefix) port2, cleanup2 := FilePort(files[2], prefix) return []*Port{{File: files[0], Chan: ClosedChan}, port1, port2}, func() { cleanup1() cleanup2() } } // ValueOutput defines the interface through which builtin commands access the // value output. // // The value output is backed by two channels, one for writing output, another // for the back-chanel signal that the reader of the channel has gone. type ValueOutput interface { // Outputs a value. Returns errs.ReaderGone if the reader is gone. Put(v interface{}) error } type valueOutput struct { data chan<- interface{} sendStop <-chan struct{} sendError *error } func (vo valueOutput) Put(v interface{}) error { select { case vo.data <- v: return nil case <-vo.sendStop: return *vo.sendError } } // ByteOutput defines the interface through which builtin commands access the // byte output. // // It is a thin wrapper around the underlying *os.File value, only exposing // the necessary methods for writing bytes and strings, and converting any // syscall.EPIPE errors to errs.ReaderGone. type ByteOutput interface { io.Writer io.StringWriter } type byteOutput struct { f *os.File } func (bo byteOutput) Write(p []byte) (int, error) { n, err := bo.f.Write(p) return n, convertReaderGone(err) } func (bo byteOutput) WriteString(s string) (int, error) { n, err := bo.f.WriteString(s) return n, convertReaderGone(err) } func convertReaderGone(err error) error { if pathErr, ok := err.(*os.PathError); ok { if pathErr.Err == epipe { return errs.ReaderGone{} } } return err } elvish-0.17.0/pkg/eval/port_helper_test.go000066400000000000000000000020201415471104000205010ustar00rootroot00000000000000package eval import ( "io" "os" "testing" ) func TestEvalerPorts(t *testing.T) { stdoutReader, stdout := mustPipe() defer stdoutReader.Close() stderrReader, stderr := mustPipe() defer stderrReader.Close() prefix := "> " ports, cleanup := PortsFromFiles([3]*os.File{DevNull, stdout, stderr}, prefix) ports[1].Chan <- "x" ports[1].Chan <- "y" ports[2].Chan <- "bad" ports[2].Chan <- "err" cleanup() stdout.Close() stderr.Close() stdoutAll := mustReadAllString(stdoutReader) wantStdoutAll := "> x\n> y\n" if stdoutAll != wantStdoutAll { t.Errorf("stdout is %q, want %q", stdoutAll, wantStdoutAll) } stderrAll := mustReadAllString(stderrReader) wantStderrAll := "> bad\n> err\n" if stderrAll != wantStderrAll { t.Errorf("stderr is %q, want %q", stderrAll, wantStderrAll) } } func mustReadAllString(r io.Reader) string { b, err := io.ReadAll(r) if err != nil { panic(err) } return string(b) } func mustPipe() (*os.File, *os.File) { r, w, err := os.Pipe() if err != nil { panic(err) } return r, w } elvish-0.17.0/pkg/eval/port_unix.go000066400000000000000000000001761415471104000171600ustar00rootroot00000000000000//go:build !windows && !plan9 && !js // +build !windows,!plan9,!js package eval import "syscall" var epipe = syscall.EPIPE elvish-0.17.0/pkg/eval/port_windows.go000066400000000000000000000005361415471104000176670ustar00rootroot00000000000000package eval import "syscall" // Error number 232 is what Windows returns when trying to write on a pipe who // reader has gone. The syscall package defines an EPIPE on Windows, but that's // not what Windows API actually returns. // // https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499- var epipe = syscall.Errno(232) elvish-0.17.0/pkg/eval/process_unix.go000066400000000000000000000012371415471104000176510ustar00rootroot00000000000000//go:build !windows && !plan9 // +build !windows,!plan9 package eval import ( "os" "os/signal" "syscall" "src.elv.sh/pkg/sys" "src.elv.sh/pkg/sys/eunix" ) // Process control functions in Unix. func putSelfInFg() error { if !sys.IsATTY(os.Stdin) { return nil } // If Elvish is in the background, the tcsetpgrp call below will either fail // (if the process is in an orphaned process group) or stop the process. // Ignoring TTOU fixes that. signal.Ignore(syscall.SIGTTOU) defer signal.Reset(syscall.SIGTTOU) return eunix.Tcsetpgrp(0, syscall.Getpgrp()) } func makeSysProcAttr(bg bool) *syscall.SysProcAttr { return &syscall.SysProcAttr{Setpgid: bg} } elvish-0.17.0/pkg/eval/process_windows.go000066400000000000000000000005671415471104000203650ustar00rootroot00000000000000package eval import "syscall" // Nop on Windows. func putSelfInFg() error { return nil } // The bitmask for CreationFlags in SysProcAttr to start a process in background. const detachedProcess = 0x00000008 func makeSysProcAttr(bg bool) *syscall.SysProcAttr { flags := uint32(0) if bg { flags |= detachedProcess } return &syscall.SysProcAttr{CreationFlags: flags} } elvish-0.17.0/pkg/eval/purely_eval.go000066400000000000000000000033511415471104000174560ustar00rootroot00000000000000package eval import ( "strings" "src.elv.sh/pkg/fsutil" "src.elv.sh/pkg/parse" ) func (ev *Evaler) PurelyEvalCompound(cn *parse.Compound) (string, bool) { return ev.PurelyEvalPartialCompound(cn, -1) } func (ev *Evaler) PurelyEvalPartialCompound(cn *parse.Compound, upto int) (string, bool) { tilde := false head := "" for _, in := range cn.Indexings { if len(in.Indices) > 0 { return "", false } if upto >= 0 && in.To > upto { break } switch in.Head.Type { case parse.Tilde: tilde = true case parse.Bareword, parse.SingleQuoted, parse.DoubleQuoted: head += in.Head.Value case parse.Variable: if ev == nil { return "", false } v := ev.PurelyEvalPrimary(in.Head) if s, ok := v.(string); ok { head += s } else { return "", false } default: return "", false } } if tilde { i := strings.Index(head, "/") if i == -1 { i = len(head) } uname := head[:i] home, err := fsutil.GetHome(uname) if err != nil { return "", false } head = home + head[i:] } return head, true } // PurelyEvalPrimary evaluates a primary node without causing any side effects. // If this cannot be done, it returns nil. // // Currently, only string literals and variables with no @ can be evaluated. func (ev *Evaler) PurelyEvalPrimary(pn *parse.Primary) interface{} { switch pn.Type { case parse.Bareword, parse.SingleQuoted, parse.DoubleQuoted: return pn.Value case parse.Variable: sigil, qname := SplitSigil(pn.Value) if sigil != "" { return nil } fm := &Frame{Evaler: ev, local: ev.Global(), up: new(Ns)} ref := resolveVarRef(fm, qname, nil) if ref != nil { variable := deref(fm, ref) if variable == nil { return nil } return variable.Get() } } return nil } elvish-0.17.0/pkg/eval/purely_eval_test.go000066400000000000000000000031721415471104000205160ustar00rootroot00000000000000package eval_test import ( "testing" . "src.elv.sh/pkg/eval" "src.elv.sh/pkg/testutil" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" "src.elv.sh/pkg/parse" ) func TestPurelyEvalCompound(t *testing.T) { home := testutil.InTempHome(t) var tests = []struct { code string upto int wantValue string wantBad bool }{ {code: "foobar", wantValue: "foobar"}, {code: "'foobar'", wantValue: "foobar"}, {code: "foo'bar'", wantValue: "foobar"}, {code: "$x", wantValue: "bar"}, {code: "foo$x", wantValue: "foobar"}, {code: "foo$x", upto: 3, wantValue: "foo"}, {code: "~", wantValue: home}, {code: "~/foo", wantValue: home + "/foo"}, {code: "$ns:x", wantValue: "foo"}, {code: "$bad", wantBad: true}, {code: "$ns:bad", wantBad: true}, {code: "[abc]", wantBad: true}, {code: "$y", wantBad: true}, {code: "a[0]", wantBad: true}, {code: "$@x", wantBad: true}, } ev := NewEvaler() ev.ExtendGlobal(BuildNs(). AddVar("x", vars.NewReadOnly("bar")). AddVar("y", vars.NewReadOnly(vals.MakeList())). AddNs("ns", BuildNs().AddVar("x", vars.NewReadOnly("foo")))) for _, test := range tests { t.Run(test.code, func(t *testing.T) { n := &parse.Compound{} err := parse.ParseAs( parse.Source{Name: "[test]", Code: test.code}, n, parse.Config{}) if err != nil { panic(err) } upto := test.upto if upto == 0 { upto = -1 } value, ok := ev.PurelyEvalPartialCompound(n, upto) if value != test.wantValue { t.Errorf("got value %q, want %q", value, test.wantValue) } if ok != !test.wantBad { t.Errorf("got ok %v, want %v", ok, !test.wantBad) } }) } } elvish-0.17.0/pkg/eval/pwd.go000066400000000000000000000022011415471104000157120ustar00rootroot00000000000000package eval import ( "os" "src.elv.sh/pkg/eval/vars" ) // NewPwdVar returns a variable who value is synchronized with the path of the // current working directory. func NewPwdVar(ev *Evaler) vars.Var { return pwdVar{ev} } // pwdVar is a variable whose value always reflects the current working // directory. Setting it changes the current working directory. type pwdVar struct { ev *Evaler } var _ vars.Var = pwdVar{} // Getwd allows for unit test error injection. var Getwd func() (string, error) = os.Getwd // Get returns the current working directory. It returns "/unknown/pwd" when // it cannot be determined. func (pwdVar) Get() interface{} { pwd, err := Getwd() if err != nil { // This should really use the path separator appropriate for the // platform but in practice this hardcoded string works fine. Both // because MS Windows supports forward slashes and this will very // rarely occur. return "/unknown/pwd" } return pwd } // Set changes the current working directory. func (pwd pwdVar) Set(v interface{}) error { path, ok := v.(string) if !ok { return vars.ErrPathMustBeString } return pwd.ev.Chdir(path) } elvish-0.17.0/pkg/eval/pwd_test.go000066400000000000000000000031011415471104000167510ustar00rootroot00000000000000package eval_test import ( "errors" "path/filepath" "runtime" "testing" . "src.elv.sh/pkg/eval" "src.elv.sh/pkg/testutil" . "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/eval/vars" ) func TestBuiltinPwd(t *testing.T) { tmpHome := testutil.InTempHome(t) testutil.MustMkdirAll("dir1") testutil.MustMkdirAll("dir2") dir1 := filepath.Join(tmpHome, "dir1") dir2 := filepath.Join(tmpHome, "dir2") Test(t, That(`pwd=dir1 put $pwd; put $pwd`).Puts(dir1, tmpHome), That(`pwd=(num 1) put $pwd`).Throws(vars.ErrPathMustBeString, "pwd"), ) // We could separate these two test variants into separate unit test // modules but that's overkill for this situation and makes the // equivalence between the two environments harder to see. if runtime.GOOS == "windows" { Test(t, That(`cd $E:HOME\dir2; pwd=$E:HOME put $pwd; put $pwd`).Puts(tmpHome, dir2), That(`cd $E:HOME\dir2; pwd=..\dir1 put $pwd; put $pwd`).Puts(dir1, dir2), That(`cd $E:HOME\dir1; pwd=..\dir2 put $pwd; put $pwd`).Puts(dir2, dir1), ) } else { Test(t, That(`cd ~/dir2; pwd=~ put $pwd; put $pwd`).Puts(tmpHome, dir2), That(`cd ~/dir2; pwd=~/dir1 put $pwd; put $pwd`).Puts(dir1, dir2), That(`cd ~/dir1; pwd=../dir2 put $pwd; put $pwd`).Puts(dir2, dir1), ) } } // Verify the behavior when the CWD cannot be determined. func TestBuiltinPwd_GetwdError(t *testing.T) { origGetwd := Getwd Getwd = mockGetwdWithError defer func() { Getwd = origGetwd }() Test(t, That(`put $pwd`).Puts("/unknown/pwd"), ) } func mockGetwdWithError() (string, error) { return "", errors.New("cwd unknown") } elvish-0.17.0/pkg/eval/vals/000077500000000000000000000000001415471104000155435ustar00rootroot00000000000000elvish-0.17.0/pkg/eval/vals/aliased_types.go000066400000000000000000000017361415471104000207270ustar00rootroot00000000000000package vals import ( "os" "src.elv.sh/pkg/persistent/hashmap" "src.elv.sh/pkg/persistent/vector" ) // File is an alias for *os.File. type File = *os.File // List is an alias for the underlying type used for lists in Elvish. type List = vector.Vector // EmptyList is an empty list. var EmptyList = vector.Empty // MakeList creates a new List from values. func MakeList(vs ...interface{}) vector.Vector { vec := vector.Empty for _, v := range vs { vec = vec.Cons(v) } return vec } // Map is an alias for the underlying type used for maps in Elvish. type Map = hashmap.Map // EmptyMap is an empty map. var EmptyMap = hashmap.New(Equal, Hash) // MakeMap creates a map from arguments that are alternately keys and values. It // panics if the number of arguments is odd. func MakeMap(a ...interface{}) hashmap.Map { if len(a)%2 == 1 { panic("odd number of arguments to MakeMap") } m := EmptyMap for i := 0; i < len(a); i += 2 { m = m.Assoc(a[i], a[i+1]) } return m } elvish-0.17.0/pkg/eval/vals/aliased_types_test.go000066400000000000000000000005021415471104000217540ustar00rootroot00000000000000package vals import ( "testing" "src.elv.sh/pkg/testutil" . "src.elv.sh/pkg/tt" ) func TestMakeMap_PanicsWithOddNumberOfArguments(t *testing.T) { Test(t, Fn("Recover", testutil.Recover), Table{ //lint:ignore SA5012 testing panic Args(func() { MakeMap("foo") }).Rets("odd number of arguments to MakeMap"), }) } elvish-0.17.0/pkg/eval/vals/assoc.go000066400000000000000000000030331415471104000172010ustar00rootroot00000000000000package vals import ( "errors" ) // Assocer wraps the Assoc method. type Assocer interface { // Assoc returns a slightly modified version of the receiver with key k // associated with value v. Assoc(k, v interface{}) (interface{}, error) } var ( errAssocUnsupported = errors.New("assoc is not supported") errReplacementMustBeString = errors.New("replacement must be string") errAssocWithSlice = errors.New("assoc with slice not yet supported") ) // Assoc takes a container, a key and value, and returns a modified version of // the container, in which the key associated with the value. It is implemented // for the builtin type string, List and Map types, StructMap types, and types // satisfying the Assocer interface. For other types, it returns an error. func Assoc(a, k, v interface{}) (interface{}, error) { switch a := a.(type) { case string: return assocString(a, k, v) case List: return assocList(a, k, v) case Map: return a.Assoc(k, v), nil case Assocer: return a.Assoc(k, v) } return nil, errAssocUnsupported } func assocString(s string, k, v interface{}) (interface{}, error) { i, j, err := convertStringIndex(k, s) if err != nil { return nil, err } repl, ok := v.(string) if !ok { return nil, errReplacementMustBeString } return s[:i] + repl + s[j:], nil } func assocList(l List, k, v interface{}) (interface{}, error) { index, err := ConvertListIndex(k, l.Len()) if err != nil { return nil, err } if index.Slice { return nil, errAssocWithSlice } return l.Assoc(index.Lower, v), nil } elvish-0.17.0/pkg/eval/vals/assoc_test.go000066400000000000000000000026601415471104000202450ustar00rootroot00000000000000package vals import ( "errors" "testing" "src.elv.sh/pkg/eval/errs" . "src.elv.sh/pkg/tt" ) type customAssocer struct{} var errCustomAssoc = errors.New("custom assoc error") func (a customAssocer) Assoc(k, v interface{}) (interface{}, error) { return "custom result", errCustomAssoc } func TestAssoc(t *testing.T) { Test(t, Fn("Assoc", Assoc), Table{ Args("0123", "0", "foo").Rets("foo123", nil), Args("0123", "1..3", "bar").Rets("0bar3", nil), Args("0123", "1..3", 12).Rets(nil, errReplacementMustBeString), Args("0123", "x", "y").Rets(nil, errIndexMustBeInteger), Args(MakeList("0", "1", "2", "3"), "0", "foo").Rets( Eq(MakeList("foo", "1", "2", "3")), nil), Args(MakeList("0", "1", "2", "3"), 0, "foo").Rets( Eq(MakeList("foo", "1", "2", "3")), nil), Args(MakeList("0"), MakeList("0"), "1").Rets(nil, errIndexMustBeInteger), Args(MakeList("0"), "1", "x").Rets(nil, errs.OutOfRange{ What: "index", ValidLow: "0", ValidHigh: "0", Actual: "1"}), // TODO: Support list assoc with slice Args(MakeList("0", "1", "2", "3"), "1..3", MakeList("foo")).Rets( nil, errAssocWithSlice), Args(MakeMap("k", "v", "k2", "v2"), "k", "newv").Rets( Eq(MakeMap("k", "newv", "k2", "v2")), nil), Args(MakeMap("k", "v"), "k2", "v2").Rets( Eq(MakeMap("k", "v", "k2", "v2")), nil), Args(customAssocer{}, "x", "y").Rets("custom result", errCustomAssoc), Args(struct{}{}, "x", "y").Rets(nil, errAssocUnsupported), }) } elvish-0.17.0/pkg/eval/vals/bool.go000066400000000000000000000007231415471104000170270ustar00rootroot00000000000000package vals // Booler wraps the Bool method. type Booler interface { // Bool computes the truth value of the receiver. Bool() bool } // Bool converts a value to bool. It is implemented for nil, the builtin bool // type, and types implementing the Booler interface. For all other values, it // returns true. func Bool(v interface{}) bool { switch v := v.(type) { case nil: return false case bool: return v case Booler: return v.Bool() } return true } elvish-0.17.0/pkg/eval/vals/bool_test.go000066400000000000000000000006761415471104000200750ustar00rootroot00000000000000package vals import ( "testing" . "src.elv.sh/pkg/tt" ) type customBooler struct{ b bool } func (b customBooler) Bool() bool { return b.b } type customNonBooler struct{} func TestBool(t *testing.T) { Test(t, Fn("Bool", Bool), Table{ Args(nil).Rets(false), Args(true).Rets(true), Args(false).Rets(false), Args(customBooler{true}).Rets(true), Args(customBooler{false}).Rets(false), Args(customNonBooler{}).Rets(true), }) } elvish-0.17.0/pkg/eval/vals/concat.go000066400000000000000000000037451415471104000173520ustar00rootroot00000000000000package vals import ( "errors" "fmt" "math/big" ) // Concatter wraps the Concat method. See Concat for how it is used. type Concatter interface { // Concat concatenates the receiver with another value, the receiver being // the left operand. If concatenation is not supported for the given value, // the method can return the special error type ErrCatNotImplemented. Concat(v interface{}) (interface{}, error) } // RConcatter wraps the RConcat method. See Concat for how it is used. type RConcatter interface { RConcat(v interface{}) (interface{}, error) } // ErrConcatNotImplemented is a special error value used to signal that // concatenation is not implemented. See Concat for how it is used. var ErrConcatNotImplemented = errors.New("concat not implemented") type cannotConcat struct { lhsKind string rhsKind string } func (err cannotConcat) Error() string { return fmt.Sprintf("cannot concatenate %s and %s", err.lhsKind, err.rhsKind) } // Concat concatenates two values. If both operands are strings, it returns lhs // + rhs, nil. If the left operand implements Concatter, it calls // lhs.Concat(rhs). If lhs doesn't implement the interface or returned // ErrConcatNotImplemented, it then calls rhs.RConcat(lhs). If all attempts // fail, it returns nil and an error. func Concat(lhs, rhs interface{}) (interface{}, error) { if v, ok := tryConcatBuiltins(lhs, rhs); ok { return v, nil } if lhs, ok := lhs.(Concatter); ok { v, err := lhs.Concat(rhs) if err != ErrConcatNotImplemented { return v, err } } if rhs, ok := rhs.(RConcatter); ok { v, err := rhs.RConcat(lhs) if err != ErrConcatNotImplemented { return v, err } } return nil, cannotConcat{Kind(lhs), Kind(rhs)} } func tryConcatBuiltins(lhs, rhs interface{}) (interface{}, bool) { switch lhs := lhs.(type) { case string, int, *big.Int, *big.Rat, float64: switch rhs := rhs.(type) { case string, int, *big.Int, *big.Rat, float64: return ToString(lhs) + ToString(rhs), true } } return nil, false } elvish-0.17.0/pkg/eval/vals/concat_test.go000066400000000000000000000033431415471104000204030ustar00rootroot00000000000000package vals import ( "errors" "math/big" "testing" . "src.elv.sh/pkg/tt" ) // An implementation for Concatter that accepts strings, returns a special // error when rhs is a float64, and returns ErrConcatNotImplemented when rhs is // of other types. type concatter struct{} var errBadFloat64 = errors.New("float64 is bad") func (concatter) Concat(rhs interface{}) (interface{}, error) { switch rhs := rhs.(type) { case string: return "concatter " + rhs, nil case float64: return nil, errBadFloat64 default: return nil, ErrConcatNotImplemented } } // An implementation of RConcatter that accepts all types. type rconcatter struct{} func (rconcatter) RConcat(lhs interface{}) (interface{}, error) { return "rconcatter", nil } func TestConcat(t *testing.T) { Test(t, Fn("Concat", Concat), Table{ Args("foo", "bar").Rets("foobar", nil), // string+number Args("foo", 2).Rets("foo2", nil), Args("foo", bigInt(z)).Rets("foo"+z, nil), Args("foo", big.NewRat(1, 2)).Rets("foo1/2", nil), Args("foo", 2.0).Rets("foo2.0", nil), // number+string Args(2, "foo").Rets("2foo", nil), Args(bigInt(z), "foo").Rets(z+"foo", nil), Args(big.NewRat(1, 2), "foo").Rets("1/2foo", nil), Args(2.0, "foo").Rets("2.0foo", nil), // LHS implements Concatter and succeeds Args(concatter{}, "bar").Rets("concatter bar", nil), // LHS implements Concatter but returns ErrConcatNotImplemented; RHS // does not implement RConcatter Args(concatter{}, 12).Rets(nil, cannotConcat{"!!vals.concatter", "number"}), // LHS implements Concatter but returns another error Args(concatter{}, 12.0).Rets(nil, errBadFloat64), // LHS does not implement Concatter but RHS implements RConcatter Args(12, rconcatter{}).Rets("rconcatter", nil), }) } elvish-0.17.0/pkg/eval/vals/conversion.go000066400000000000000000000106701415471104000202630ustar00rootroot00000000000000package vals import ( "errors" "fmt" "math/big" "reflect" "strconv" "unicode/utf8" ) // Conversion between native and Elvish values. // // Elvish uses native Go types most of the time - string, bool, hashmap.Map, // vector.Vector, etc., and there is no need for any conversions. There are some // exceptions, for instance int and rune, since Elvish currently lacks integer // types. // // There is a many-to-one relationship between Go types and Elvish types. A // Go value can always be converted to an Elvish value unambiguously, but to // convert an Elvish value into a Go value one must know the destination type // first. For example, all of the Go values int(1), rune('1') and string("1") // convert to Elvish "1"; conversely, Elvish "1" may be converted to any of the // aforementioned three possible values, depending on the destination type. // // In future, Elvish may gain distinct types for integers and characters, making // the examples above unnecessary; however, the conversion logic may not // entirely go away, as there might always be some mismatch between Elvish's // type system and Go's. type wrongType struct { wantKind string gotKind string } func (err wrongType) Error() string { return fmt.Sprintf("wrong type: need %s, got %s", err.wantKind, err.gotKind) } type cannotParseAs struct { want string repr string } func (err cannotParseAs) Error() string { return fmt.Sprintf("cannot parse as %s: %s", err.want, err.repr) } var ( errMustBeString = errors.New("must be string") errMustBeValidUTF8 = errors.New("must be valid UTF-8") errMustHaveSingleRune = errors.New("must have a single rune") errMustBeNumber = errors.New("must be number") errMustBeInteger = errors.New("must be integer") ) // ScanToGo converts an Elvish value to a Go value that the pointer refers to. It // uses the type of the pointer to determine the destination type, and puts the // converted value in the location the pointer points to. Conversion only // happens when the destination type is int, float64 or rune; in other cases, // this function just checks that the source value is already assignable to the // destination. func ScanToGo(src interface{}, ptr interface{}) error { switch ptr := ptr.(type) { case *int: i, err := elvToInt(src) if err == nil { *ptr = i } return err case *float64: n, err := elvToNum(src) if err == nil { *ptr = ConvertToFloat64(n) } return err case *Num: n, err := elvToNum(src) if err == nil { *ptr = n } return err case *rune: r, err := elvToRune(src) if err == nil { *ptr = r } return err default: // Do a generic `*ptr = src` via reflection ptrType := TypeOf(ptr) if ptrType.Kind() != reflect.Ptr { return fmt.Errorf("internal bug: need pointer to scan to, got %T", ptr) } dstType := ptrType.Elem() if !TypeOf(src).AssignableTo(dstType) { var dstKind string if dstType.Kind() == reflect.Interface { dstKind = "!!" + dstType.String() } else { dstKind = Kind(reflect.Zero(dstType).Interface()) } return wrongType{dstKind, Kind(src)} } ValueOf(ptr).Elem().Set(ValueOf(src)) return nil } } // FromGo converts a Go value to an Elvish value. Most types are returned as // is, but exact numerical types are normalized to one of int, *big.Int and // *big.Rat, using the small representation that can hold the value, and runes // are converted to strings. func FromGo(a interface{}) interface{} { switch a := a.(type) { case *big.Int: return NormalizeBigInt(a) case *big.Rat: return NormalizeBigRat(a) case rune: return string(a) default: return a } } func elvToInt(arg interface{}) (int, error) { switch arg := arg.(type) { case int: return arg, nil case string: num, err := strconv.ParseInt(arg, 0, 0) if err == nil { return int(num), nil } return 0, cannotParseAs{"integer", Repr(arg, -1)} default: return 0, errMustBeInteger } } func elvToNum(arg interface{}) (Num, error) { switch arg := arg.(type) { case int, *big.Int, *big.Rat, float64: return arg, nil case string: n := ParseNum(arg) if n == nil { return 0, cannotParseAs{"number", Repr(arg, -1)} } return n, nil default: return 0, errMustBeNumber } } func elvToRune(arg interface{}) (rune, error) { ss, ok := arg.(string) if !ok { return -1, errMustBeString } s := ss r, size := utf8.DecodeRuneInString(s) if r == utf8.RuneError { return -1, errMustBeValidUTF8 } if size != len(s) { return -1, errMustHaveSingleRune } return r, nil } elvish-0.17.0/pkg/eval/vals/conversion_test.go000066400000000000000000000062371415471104000213260ustar00rootroot00000000000000package vals import ( "math/big" "reflect" "testing" . "src.elv.sh/pkg/tt" ) type someType struct { foo string } func TestScanToGo_ConcreteTypeDst(t *testing.T) { // A wrapper around ScanToGo, to make it easier to test. Instead of // supplying a pointer to the destination, an initial value to the // destination is supplied and the result is returned. scanToGo := func(src interface{}, dstInit interface{}) (interface{}, error) { ptr := reflect.New(TypeOf(dstInit)) err := ScanToGo(src, ptr.Interface()) return ptr.Elem().Interface(), err } Test(t, Fn("ScanToGo", scanToGo), Table{ // int Args("12", 0).Rets(12), Args("0x12", 0).Rets(0x12), Args(12.0, 0).Rets(0, errMustBeInteger), Args(0.5, 0).Rets(0, errMustBeInteger), Args(someType{}, 0).Rets(Any, errMustBeInteger), Args("x", 0).Rets(Any, cannotParseAs{"integer", "x"}), // float64 Args(23, 0.0).Rets(23.0), Args(big.NewRat(1, 2), 0.0).Rets(0.5), Args(1.2, 0.0).Rets(1.2), Args("23", 0.0).Rets(23.0), Args("0x23", 0.0).Rets(float64(0x23)), Args(someType{}, 0.0).Rets(Any, errMustBeNumber), Args("x", 0.0).Rets(Any, cannotParseAs{"number", "x"}), // rune Args("x", ' ').Rets('x'), Args(someType{}, ' ').Rets(Any, errMustBeString), Args("\xc3\x28", ' ').Rets(Any, errMustBeValidUTF8), // Invalid UTF8 Args("ab", ' ').Rets(Any, errMustHaveSingleRune), // Other types don't undergo any conversion, as long as the types match Args("foo", "").Rets("foo"), Args(someType{"foo"}, someType{}).Rets(someType{"foo"}), Args(nil, nil).Rets(nil), Args("x", someType{}).Rets(Any, wrongType{"!!vals.someType", "string"}), }) } func TestScanToGo_NumDst(t *testing.T) { scanToGo := func(src interface{}) (Num, error) { var n Num err := ScanToGo(src, &n) return n, err } Test(t, Fn("ScanToGo", scanToGo), Table{ // Strings are automatically converted Args("12").Rets(12), Args(z).Rets(bigInt(z)), Args("1/2").Rets(big.NewRat(1, 2)), Args("12.0").Rets(12.0), // Already numbers Args(12).Rets(12), Args(bigInt(z)).Rets(bigInt(z)), Args(big.NewRat(1, 2)).Rets(big.NewRat(1, 2)), Args(12.0).Rets(12.0), Args("bad").Rets(Any, cannotParseAs{"number", "bad"}), Args(EmptyList).Rets(Any, errMustBeNumber), }) } func TestScanToGo_InterfaceDst(t *testing.T) { scanToGo := func(src interface{}) (interface{}, error) { var l List err := ScanToGo(src, &l) return l, err } Test(t, Fn("ScanToGo", scanToGo), Table{ Args(EmptyList).Rets(EmptyList), Args("foo").Rets(Any, wrongType{"!!vector.Vector", "string"}), }) } func TestScanToGo_ErrorsWithNonPointerDst(t *testing.T) { err := ScanToGo("", 1) if err == nil { t.Errorf("did not return error") } } func TestFromGo(t *testing.T) { Test(t, Fn("FromGo", FromGo), Table{ // BigInt -> int, when in range Args(bigInt(z)).Rets(bigInt(z)), Args(big.NewInt(100)).Rets(100), // BigRat -> BigInt or int, when denominator is 1 Args(bigRat(z1 + "/" + z)).Rets(bigRat(z1 + "/" + z)), Args(bigRat(z + "/1")).Rets(bigInt(z)), Args(bigRat("2/1")).Rets(2), // rune -> string Args('x').Rets("x"), // Other types don't undergo any conversion Args(nil).Rets(nil), Args(someType{"foo"}).Rets(someType{"foo"}), }) } elvish-0.17.0/pkg/eval/vals/dissoc.go000066400000000000000000000012031415471104000173520ustar00rootroot00000000000000package vals // Dissocer wraps the Dissoc method. type Dissocer interface { // Dissoc returns a slightly modified version of the receiver with key k // dissociated with any value. Dissoc(k interface{}) interface{} } // Dissoc takes a container and a key, and returns a modified version of the // container, with the given key dissociated with any value. It is implemented // for the Map type and types satisfying the Dissocer interface. For other // types, it returns nil. func Dissoc(a, k interface{}) interface{} { switch a := a.(type) { case Map: return a.Dissoc(k) case Dissocer: return a.Dissoc(k) default: return nil } } elvish-0.17.0/pkg/eval/vals/dissoc_test.go000066400000000000000000000005761415471104000204250ustar00rootroot00000000000000package vals import ( "testing" . "src.elv.sh/pkg/tt" ) type dissocer struct{} func (dissocer) Dissoc(interface{}) interface{} { return "custom ret" } func TestDissoc(t *testing.T) { Test(t, Fn("Dissoc", Dissoc), Table{ Args(MakeMap("k1", "v1", "k2", "v2"), "k1").Rets(Eq(MakeMap("k2", "v2"))), Args(dissocer{}, "x").Rets("custom ret"), Args("", "x").Rets(nil), }) } elvish-0.17.0/pkg/eval/vals/doc.go000066400000000000000000000001561415471104000166410ustar00rootroot00000000000000// Package vals contains basic facilities for manipulating values used in the // Elvish runtime. package vals elvish-0.17.0/pkg/eval/vals/equal.go000066400000000000000000000042121415471104000172000ustar00rootroot00000000000000package vals import ( "math/big" "reflect" ) // Equaler wraps the Equal method. type Equaler interface { // Equal compares the receiver to another value. Two equal values must have // the same hash code. Equal(other interface{}) bool } // Equal returns whether two values are equal. It is implemented for the builtin // types bool and string, the File, List, Map types, StructMap types, and types // satisfying the Equaler interface. For other types, it uses reflect.DeepEqual // to compare the two values. func Equal(x, y interface{}) bool { switch x := x.(type) { case nil: return x == y case bool: return x == y case int: return x == y case float64: return x == y case *big.Int: if y, ok := y.(*big.Int); ok { return x.Cmp(y) == 0 } return false case *big.Rat: if y, ok := y.(*big.Rat); ok { return x.Cmp(y) == 0 } return false case string: return x == y case Equaler: return x.Equal(y) case File: if yy, ok := y.(File); ok { return x.Fd() == yy.Fd() } return false case List: if yy, ok := y.(List); ok { return equalList(x, yy) } return false case Map: if yy, ok := y.(Map); ok { return equalMap(x, yy) } return false case StructMap: if yy, ok := y.(StructMap); ok { return equalStructMap(x, yy) } return false default: return reflect.DeepEqual(x, y) } } func equalList(x, y List) bool { if x.Len() != y.Len() { return false } ix := x.Iterator() iy := y.Iterator() for ix.HasElem() && iy.HasElem() { if !Equal(ix.Elem(), iy.Elem()) { return false } ix.Next() iy.Next() } return true } func equalMap(x, y Map) bool { if x.Len() != y.Len() { return false } for it := x.Iterator(); it.HasElem(); it.Next() { k, vx := it.Elem() vy, ok := y.Index(k) if !ok || !Equal(vx, vy) { return false } } return true } func equalStructMap(x, y StructMap) bool { t := reflect.TypeOf(x) if t != reflect.TypeOf(y) { return false } xValue := reflect.ValueOf(x) yValue := reflect.ValueOf(y) it := iterateStructMap(t) for it.Next() { _, xField := it.Get(xValue) _, yField := it.Get(yValue) if !Equal(xField, yField) { return false } } return true } elvish-0.17.0/pkg/eval/vals/equal_test.go000066400000000000000000000032461415471104000202450ustar00rootroot00000000000000package vals import ( "math/big" "os" "testing" . "src.elv.sh/pkg/tt" ) type customEqualer struct{ ret bool } func (c customEqualer) Equal(interface{}) bool { return c.ret } type customStruct struct{ a, b string } func TestEqual(t *testing.T) { Test(t, Fn("Equal", Equal), Table{ Args(nil, nil).Rets(true), Args(nil, "").Rets(false), Args(true, true).Rets(true), Args(true, false).Rets(false), Args(1.0, 1.0).Rets(true), Args(1.0, 1.1).Rets(false), Args("1.0", 1.0).Rets(false), Args(1, 1.0).Rets(false), Args(1, 1).Rets(true), Args(bigInt(z), bigInt(z)).Rets(true), Args(bigInt(z), 1).Rets(false), Args(bigInt(z), bigInt(z1)).Rets(false), Args(big.NewRat(1, 2), big.NewRat(1, 2)).Rets(true), Args(big.NewRat(1, 2), 0.5).Rets(false), Args("lorem", "lorem").Rets(true), Args("lorem", "ipsum").Rets(false), Args(os.Stdin, os.Stdin).Rets(true), Args(os.Stdin, os.Stderr).Rets(false), Args(os.Stdin, "").Rets(false), Args(os.Stdin, 0).Rets(false), Args(MakeList("a", "b"), MakeList("a", "b")).Rets(true), Args(MakeList("a", "b"), MakeList("a")).Rets(false), Args(MakeList("a", "b"), MakeList("a", "c")).Rets(false), Args(MakeList("a", "b"), "").Rets(false), Args(MakeList("a", "b"), 1.0).Rets(false), Args(MakeMap("k", "v"), MakeMap("k", "v")).Rets(true), Args(MakeMap("k", "v"), MakeMap("k2", "v")).Rets(false), Args(MakeMap("k", "v", "k2", "v2"), MakeMap("k", "v")).Rets(false), Args(MakeMap("k", "v"), "").Rets(false), Args(MakeMap("k", "v"), 1.0).Rets(false), Args(customEqualer{true}, 2).Rets(true), Args(customEqualer{false}, 2).Rets(false), Args(&customStruct{"a", "b"}, &customStruct{"a", "b"}).Rets(true), }) } elvish-0.17.0/pkg/eval/vals/errors_test.go000066400000000000000000000004231415471104000204440ustar00rootroot00000000000000package vals import ( "testing" . "src.elv.sh/pkg/tt" ) func TestErrors(t *testing.T) { Test(t, Fn("error.Error", error.Error), Table{ Args(cannotIterate{"num"}).Rets("cannot iterate num"), Args(cannotIterateKeysOf{"num"}).Rets("cannot iterate keys of num"), }) } elvish-0.17.0/pkg/eval/vals/feed.go000066400000000000000000000003571415471104000170020ustar00rootroot00000000000000package vals // Feed calls the function with given values, breaking earlier if the function // returns false. func Feed(f func(interface{}) bool, values ...interface{}) { for _, value := range values { if !f(value) { break } } } elvish-0.17.0/pkg/eval/vals/feed_test.go000066400000000000000000000005141415471104000200340ustar00rootroot00000000000000package vals import ( "reflect" "testing" ) func TestFeed(t *testing.T) { var fed []interface{} Feed(func(x interface{}) bool { fed = append(fed, x) return x != 10 }, 1, 2, 3, 10, 11, 12, 13) wantFed := []interface{}{1, 2, 3, 10} if !reflect.DeepEqual(fed, wantFed) { t.Errorf("Fed %v, want %v", fed, wantFed) } } elvish-0.17.0/pkg/eval/vals/has_key.go000066400000000000000000000030411415471104000175130ustar00rootroot00000000000000package vals import ( "reflect" "src.elv.sh/pkg/persistent/hashmap" ) // HasKeyer wraps the HasKey method. type HasKeyer interface { // HasKey returns whether the receiver has the given argument as a valid // key. HasKey(interface{}) bool } // HasKey returns whether a container has a key. It is implemented for the Map // type, StructMap types, and types satisfying the HasKeyer interface. It falls // back to iterating keys using IterateKeys, and if that fails, it falls back to // calling Len and checking if key is a valid numeric or slice index. Otherwise // it returns false. func HasKey(container, key interface{}) bool { switch container := container.(type) { case HasKeyer: return container.HasKey(key) case Map: return hashmap.HasKey(container, key) case StructMap: return hasKeyStructMap(container, key) case PseudoStructMap: return hasKeyStructMap(container.Fields(), key) default: var found bool err := IterateKeys(container, func(k interface{}) bool { if key == k { found = true } return !found }) if err == nil { return found } if len := Len(container); len >= 0 { // TODO(xiaq): Not all types that implement Lener have numerical // indices _, err := ConvertListIndex(key, len) return err == nil } return false } } func hasKeyStructMap(m StructMap, k interface{}) bool { kstring, ok := k.(string) if !ok || kstring == "" { return false } for _, fieldName := range getStructMapInfo(reflect.TypeOf(m)).fieldNames { if fieldName == kstring { return true } } return false } elvish-0.17.0/pkg/eval/vals/has_key_test.go000066400000000000000000000014671415471104000205640ustar00rootroot00000000000000package vals import ( "testing" . "src.elv.sh/pkg/tt" ) type hasKeyer struct{ key interface{} } func (h hasKeyer) HasKey(k interface{}) bool { return k == h.key } func TestHasKey(t *testing.T) { Test(t, Fn("HasKey", HasKey), Table{ // Map Args(MakeMap("k", "v"), "k").Rets(true), Args(MakeMap("k", "v"), "bad").Rets(false), // HasKeyer Args(hasKeyer{"valid"}, "valid").Rets(true), Args(hasKeyer{"valid"}, "invalid").Rets(false), // Fallback to IterateKeys Args(keysIterator{vs("lorem")}, "lorem").Rets(true), Args(keysIterator{vs("lorem")}, "ipsum").Rets(false), // Fallback to Len Args(MakeList("lorem", "ipsum"), "0").Rets(true), Args(MakeList("lorem", "ipsum"), "0..").Rets(true), Args(MakeList("lorem", "ipsum"), "2").Rets(false), // Non-container Args(1, "0").Rets(false), }) } elvish-0.17.0/pkg/eval/vals/hash.go000066400000000000000000000031041415471104000170130ustar00rootroot00000000000000package vals import ( "math" "math/big" "reflect" "src.elv.sh/pkg/persistent/hash" ) // Hasher wraps the Hash method. type Hasher interface { // Hash computes the hash code of the receiver. Hash() uint32 } // Hash returns the 32-bit hash of a value. It is implemented for the builtin // types bool and string, the File, List, Map types, StructMap types, and types // satisfying the Hasher interface. For other values, it returns 0 (which is OK // in terms of correctness). func Hash(v interface{}) uint32 { switch v := v.(type) { case bool: if v { return 1 } return 0 case int: return hash.UIntPtr(uintptr(v)) case *big.Int: h := hash.DJBCombine(hash.DJBInit, uint32(v.Sign())) for _, word := range v.Bits() { h = hash.DJBCombine(h, hash.UIntPtr(uintptr(word))) } return h case *big.Rat: return hash.DJB(Hash(v.Num()), Hash(v.Denom())) case float64: return hash.UInt64(math.Float64bits(v)) case string: return hash.String(v) case Hasher: return v.Hash() case File: return hash.UIntPtr(v.Fd()) case List: h := hash.DJBInit for it := v.Iterator(); it.HasElem(); it.Next() { h = hash.DJBCombine(h, Hash(it.Elem())) } return h case Map: h := hash.DJBInit for it := v.Iterator(); it.HasElem(); it.Next() { k, v := it.Elem() h = hash.DJBCombine(h, Hash(k)) h = hash.DJBCombine(h, Hash(v)) } return h case StructMap: h := hash.DJBInit it := iterateStructMap(reflect.TypeOf(v)) vValue := reflect.ValueOf(v) for it.Next() { _, field := it.Get(vValue) h = hash.DJBCombine(h, Hash(field)) } return h } return 0 } elvish-0.17.0/pkg/eval/vals/hash_test.go000066400000000000000000000017211415471104000200550ustar00rootroot00000000000000package vals import ( "math" "math/big" "os" "testing" "unsafe" "src.elv.sh/pkg/persistent/hash" . "src.elv.sh/pkg/tt" ) type hasher struct{} func (hasher) Hash() uint32 { return 42 } type nonHasher struct{} func TestHash(t *testing.T) { z := big.NewInt(5) z.Lsh(z, 8*uint(unsafe.Sizeof(int(0)))) z.Add(z, big.NewInt(9)) // z = 5 << wordSize + 9 Test(t, Fn("Hash", Hash), Table{ Args(false).Rets(uint32(0)), Args(true).Rets(uint32(1)), Args(1).Rets(uint32(1)), Args(z).Rets(hash.DJB(1, 9, 5)), Args(big.NewRat(3, 2)).Rets(hash.DJB(Hash(big.NewInt(3)), Hash(big.NewInt(2)))), Args(1.0).Rets(hash.UInt64(math.Float64bits(1.0))), Args("foo").Rets(hash.String("foo")), Args(os.Stdin).Rets(hash.UIntPtr(os.Stdin.Fd())), Args(MakeList("foo", "bar")).Rets(hash.DJB(Hash("foo"), Hash("bar"))), Args(MakeMap("foo", "bar")). Rets(hash.DJB(Hash("foo"), Hash("bar"))), Args(hasher{}).Rets(uint32(42)), Args(nonHasher{}).Rets(uint32(0)), }) } elvish-0.17.0/pkg/eval/vals/index.go000066400000000000000000000042131415471104000172010ustar00rootroot00000000000000package vals import ( "errors" "os" "reflect" ) // Indexer wraps the Index method. type Indexer interface { // Index retrieves the value corresponding to the specified key in the // container. It returns the value (if any), and whether it actually exists. Index(k interface{}) (v interface{}, ok bool) } // ErrIndexer wraps the Index method. type ErrIndexer interface { // Index retrieves one value from the receiver at the specified index. Index(k interface{}) (interface{}, error) } var errNotIndexable = errors.New("not indexable") type noSuchKeyError struct { key interface{} } // NoSuchKey returns an error indicating that a key is not found in a map-like // value. func NoSuchKey(k interface{}) error { return noSuchKeyError{k} } func (err noSuchKeyError) Error() string { return "no such key: " + Repr(err.key, NoPretty) } // Index indexes a value with the given key. It is implemented for the builtin // type string, *os.File, List, StructMap and PseudoStructMap types, and types // satisfying the ErrIndexer or Indexer interface (the Map type satisfies // Indexer). For other types, it returns a nil value and a non-nil error. func Index(a, k interface{}) (interface{}, error) { switch a := a.(type) { case string: return indexString(a, k) case *os.File: return indexFile(a, k) case ErrIndexer: return a.Index(k) case Indexer: v, ok := a.Index(k) if !ok { return nil, NoSuchKey(k) } return v, nil case List: return indexList(a, k) case StructMap: return indexStructMap(a, k) case PseudoStructMap: return indexStructMap(a.Fields(), k) default: return nil, errNotIndexable } } func indexFile(f *os.File, k interface{}) (interface{}, error) { switch k { case "fd": return int(f.Fd()), nil case "name": return f.Name(), nil } return nil, NoSuchKey(k) } func indexStructMap(a StructMap, k interface{}) (interface{}, error) { fieldName, ok := k.(string) if !ok || fieldName == "" { return nil, NoSuchKey(k) } aValue := reflect.ValueOf(a) it := iterateStructMap(reflect.TypeOf(a)) for it.Next() { k, v := it.Get(aValue) if k == fieldName { return FromGo(v), nil } } return nil, NoSuchKey(fieldName) } elvish-0.17.0/pkg/eval/vals/index_list.go000066400000000000000000000073551415471104000202460ustar00rootroot00000000000000package vals import ( "errors" "strconv" "strings" "src.elv.sh/pkg/eval/errs" ) var ( errIndexMustBeInteger = errors.New("index must must be integer") ) func indexList(l List, rawIndex interface{}) (interface{}, error) { index, err := ConvertListIndex(rawIndex, l.Len()) if err != nil { return nil, err } if index.Slice { return l.SubVector(index.Lower, index.Upper), nil } // Bounds are already checked. value, _ := l.Index(index.Lower) return value, nil } // ListIndex represents a (converted) list index. type ListIndex struct { Slice bool Lower int Upper int } func adjustAndCheckIndex(i, n int, includeN bool) (int, error) { if i < 0 { if i < -n { return 0, negIndexOutOfRange(strconv.Itoa(i), n) } return i + n, nil } if includeN { if i > n { return 0, posIndexOutOfRange(strconv.Itoa(i), n+1) } } else { if i >= n { return 0, posIndexOutOfRange(strconv.Itoa(i), n) } } return i, nil } // ConvertListIndex parses a list index, check whether it is valid, and returns // the converted structure. func ConvertListIndex(rawIndex interface{}, n int) (*ListIndex, error) { switch rawIndex := rawIndex.(type) { case int: index, err := adjustAndCheckIndex(rawIndex, n, false) if err != nil { return nil, err } return &ListIndex{false, index, 0}, nil case string: slice, i, j, err := parseIndexString(rawIndex, n) if err != nil { return nil, err } if !slice { i, err = adjustAndCheckIndex(i, n, false) if err != nil { return nil, err } } else { i, err = adjustAndCheckIndex(i, n, true) if err != nil { return nil, err } j0 := j j, err = adjustAndCheckIndex(j, n, true) if err != nil { return nil, err } if j < i { if j0 < 0 { return nil, errs.OutOfRange{ What: "negative slice upper index", ValidLow: strconv.Itoa(i - n), ValidHigh: "-1", Actual: strconv.Itoa(j0)} } return nil, errs.OutOfRange{ What: "slice upper index", ValidLow: strconv.Itoa(i), ValidHigh: strconv.Itoa(n), Actual: strconv.Itoa(j0)} } } return &ListIndex{slice, i, j}, nil default: return nil, errIndexMustBeInteger } } // Index = Number | // Number ( ':' | '..' | '..=' ) Number func parseIndexString(s string, n int) (slice bool, i int, j int, err error) { low, sep, high := splitIndexString(s) if sep == "" { // A single number i, err := atoi(s, n) if err != nil { return false, 0, 0, err } return false, i, 0, nil } if low == "" { i = 0 } else { i, err = atoi(low, n+1) if err != nil { return false, 0, 0, err } } if high == "" { j = n } else { j, err = atoi(high, n+1) if err != nil { return false, 0, 0, err } if sep == "..=" { // TODO: Handle j == MaxInt-1 j++ } } // Two numbers return true, i, j, nil } func splitIndexString(s string) (low, sep, high string) { if i := strings.Index(s, "..="); i >= 0 { return s[:i], "..=", s[i+3:] } if i := strings.Index(s, ".."); i >= 0 { return s[:i], "..", s[i+2:] } return s, "", "" } // atoi is a wrapper around strconv.Atoi, converting strconv.ErrRange to // errs.OutOfRange. func atoi(a string, n int) (int, error) { i, err := strconv.Atoi(a) if err != nil { if err.(*strconv.NumError).Err == strconv.ErrRange { if i < 0 { return 0, negIndexOutOfRange(a, n) } return 0, posIndexOutOfRange(a, n) } return 0, errIndexMustBeInteger } return i, nil } func posIndexOutOfRange(index string, n int) errs.OutOfRange { return errs.OutOfRange{ What: "index", ValidLow: "0", ValidHigh: strconv.Itoa(n - 1), Actual: index} } func negIndexOutOfRange(index string, n int) errs.OutOfRange { return errs.OutOfRange{ What: "negative index", ValidLow: strconv.Itoa(-n), ValidHigh: "-1", Actual: index} } elvish-0.17.0/pkg/eval/vals/index_string.go000066400000000000000000000021671415471104000205750ustar00rootroot00000000000000package vals import ( "errors" "unicode/utf8" ) var errIndexNotAtRuneBoundary = errors.New("index not at rune boundary") func indexString(s string, index interface{}) (string, error) { i, j, err := convertStringIndex(index, s) if err != nil { return "", err } return s[i:j], nil } func convertStringIndex(rawIndex interface{}, s string) (int, int, error) { index, err := ConvertListIndex(rawIndex, len(s)) if err != nil { return 0, 0, err } if index.Slice { lower, upper := index.Lower, index.Upper if startsWithRuneBoundary(s[lower:]) && endsWithRuneBoundary(s[:upper]) { return lower, upper, nil } return 0, 0, errIndexNotAtRuneBoundary } // Not slice r, size := utf8.DecodeRuneInString(s[index.Lower:]) if r == utf8.RuneError { return 0, 0, errIndexNotAtRuneBoundary } return index.Lower, index.Lower + size, nil } func startsWithRuneBoundary(s string) bool { if s == "" { return true } r, _ := utf8.DecodeRuneInString(s) return r != utf8.RuneError } func endsWithRuneBoundary(s string) bool { if s == "" { return true } r, _ := utf8.DecodeLastRuneInString(s) return r != utf8.RuneError } elvish-0.17.0/pkg/eval/vals/index_test.go000066400000000000000000000114541415471104000202450ustar00rootroot00000000000000package vals import ( "os" "testing" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/testutil" . "src.elv.sh/pkg/tt" ) var ( li0 = EmptyList li4 = MakeList("foo", "bar", "lorem", "ipsum") m = MakeMap("foo", "bar", "lorem", "ipsum") ) func TestIndex(t *testing.T) { Test(t, Fn("Index", Index), Table{ // String indices Args("abc", "0").Rets("a", nil), Args("abc", 0).Rets("a", nil), Args("你好", "0").Rets("你", nil), Args("你好", "3").Rets("好", nil), Args("你好", "2").Rets(Any, errIndexNotAtRuneBoundary), // String slices with half-open range. Args("abc", "1..2").Rets("b", nil), Args("abc", "1..").Rets("bc", nil), Args("abc", "..").Rets("abc", nil), Args("abc", "..0").Rets("", nil), // i == j == 0 is allowed Args("abc", "3..").Rets("", nil), // i == j == n is allowed // String slices with closed range. Args("abc", "0..=1").Rets("ab", nil), Args("abc", "1..=").Rets("bc", nil), Args("abc", "..=1").Rets("ab", nil), Args("abc", "..=").Rets("abc", nil), // String slices not at rune boundary. Args("你好", "2..").Rets(Any, errIndexNotAtRuneBoundary), Args("你好", "..2").Rets(Any, errIndexNotAtRuneBoundary), // List indices // ============ // Simple indices: 0 <= i < n. Args(li4, "0").Rets("foo", nil), Args(li4, "3").Rets("ipsum", nil), Args(li0, "0").Rets(Any, errs.OutOfRange{ What: "index", ValidLow: "0", ValidHigh: "-1", Actual: "0"}), Args(li4, "4").Rets(Any, errs.OutOfRange{ What: "index", ValidLow: "0", ValidHigh: "3", Actual: "4"}), Args(li4, "5").Rets(Any, errs.OutOfRange{ What: "index", ValidLow: "0", ValidHigh: "3", Actual: "5"}), Args(li4, z).Rets(Any, errs.OutOfRange{What: "index", ValidLow: "0", ValidHigh: "3", Actual: z}), // Negative indices: -n <= i < 0. Args(li4, "-1").Rets("ipsum", nil), Args(li4, "-4").Rets("foo", nil), Args(li4, "-5").Rets(Any, errs.OutOfRange{ What: "negative index", ValidLow: "-4", ValidHigh: "-1", Actual: "-5"}), Args(li4, "-"+z).Rets(Any, errs.OutOfRange{What: "negative index", ValidLow: "-4", ValidHigh: "-1", Actual: "-" + z}), // Float indices are not allowed even if the value is an integer. Args(li4, 0.0).Rets(Any, errIndexMustBeInteger), // Integer indices are allowed. Args(li4, 0).Rets("foo", nil), Args(li4, 3).Rets("ipsum", nil), Args(li4, 5).Rets(nil, errs.OutOfRange{ What: "index", ValidLow: "0", ValidHigh: "3", Actual: "5"}), Args(li4, -1).Rets("ipsum", nil), Args(li4, -5).Rets(nil, errs.OutOfRange{ What: "negative index", ValidLow: "-4", ValidHigh: "-1", Actual: "-5"}), // Half-open slices. Args(li4, "1..3").Rets(Eq(MakeList("bar", "lorem")), nil), Args(li4, "3..4").Rets(Eq(MakeList("ipsum")), nil), Args(li4, "0..0").Rets(Eq(EmptyList), nil), // i == j == 0 is allowed Args(li4, "4..4").Rets(Eq(EmptyList), nil), // i == j == n is allowed // i defaults to 0 Args(li4, "..2").Rets(Eq(MakeList("foo", "bar")), nil), Args(li4, "..-1").Rets(Eq(MakeList("foo", "bar", "lorem")), nil), // j defaults to n Args(li4, "3..").Rets(Eq(MakeList("ipsum")), nil), Args(li4, "-2..").Rets(Eq(MakeList("lorem", "ipsum")), nil), // Both indices can be omitted. Args(li0, "..").Rets(Eq(li0), nil), Args(li4, "..").Rets(Eq(li4), nil), // Closed slices. Args(li4, "1..=2").Rets(Eq(MakeList("bar", "lorem")), nil), Args(li4, "..=1").Rets(Eq(MakeList("foo", "bar")), nil), Args(li4, "..=-2").Rets(Eq(MakeList("foo", "bar", "lorem")), nil), Args(li4, "3..=").Rets(Eq(MakeList("ipsum")), nil), Args(li4, "..=").Rets(Eq(li4), nil), // Slice index out of range. Args(li4, "-5..1").Rets(nil, errs.OutOfRange{ What: "negative index", ValidLow: "-4", ValidHigh: "-1", Actual: "-5"}), Args(li4, "0..5").Rets(nil, errs.OutOfRange{ What: "index", ValidLow: "0", ValidHigh: "4", Actual: "5"}), Args(li4, z+"..").Rets(nil, errs.OutOfRange{What: "index", ValidLow: "0", ValidHigh: "4", Actual: z}), // Slice index upper < lower Args(li4, "3..2").Rets(nil, errs.OutOfRange{ What: "slice upper index", ValidLow: "3", ValidHigh: "4", Actual: "2"}), Args(li4, "-1..-2").Rets(nil, errs.OutOfRange{What: "negative slice upper index", ValidLow: "-1", ValidHigh: "-1", Actual: "-2"}), // Malformed list indices. Args(li4, "a").Rets(Any, errIndexMustBeInteger), // TODO(xiaq): Make the error more accurate. Args(li4, "1:3:2").Rets(Any, errIndexMustBeInteger), // Map indices // ============ Args(m, "foo").Rets("bar", nil), Args(m, "bad").Rets(Any, NoSuchKey("bad")), // Not indexable Args(1, "foo").Rets(nil, errNotIndexable), }) } func TestIndex_File(t *testing.T) { testutil.InTempDir(t) f, err := os.Create("f") if err != nil { t.Skip("create file:", err) } Test(t, Fn("Index", Index), Table{ Args(f, "fd").Rets(int(f.Fd()), nil), Args(f, "name").Rets(f.Name(), nil), Args(f, "x").Rets(nil, NoSuchKey("x")), }) } elvish-0.17.0/pkg/eval/vals/iterate.go000066400000000000000000000032441415471104000175320ustar00rootroot00000000000000package vals // Iterator wraps the Iterate method. type Iterator interface { // Iterate calls the passed function with each value within the receiver. // The iteration is aborted if the function returns false. Iterate(func(v interface{}) bool) } type cannotIterate struct{ kind string } func (err cannotIterate) Error() string { return "cannot iterate " + err.kind } // CanIterate returns whether the value can be iterated. If CanIterate(v) is // true, calling Iterate(v, f) will not result in an error. func CanIterate(v interface{}) bool { switch v.(type) { case Iterator, string, List: return true } return false } // Iterate iterates the supplied value, and calls the supplied function in each // of its elements. The function can return false to break the iteration. It is // implemented for the builtin type string, the List type, and types satisfying // the Iterator interface. For these types, it always returns a nil error. For // other types, it doesn't do anything and returns an error. func Iterate(v interface{}, f func(interface{}) bool) error { switch v := v.(type) { case string: for _, r := range v { b := f(string(r)) if !b { break } } case List: for it := v.Iterator(); it.HasElem(); it.Next() { if !f(it.Elem()) { break } } case Iterator: v.Iterate(f) default: return cannotIterate{Kind(v)} } return nil } // Collect collects all elements of an iterable value into a slice. func Collect(it interface{}) ([]interface{}, error) { var vs []interface{} if len := Len(it); len >= 0 { vs = make([]interface{}, 0, len) } err := Iterate(it, func(v interface{}) bool { vs = append(vs, v) return true }) return vs, err } elvish-0.17.0/pkg/eval/vals/iterate_keys.go000066400000000000000000000026271415471104000205710ustar00rootroot00000000000000package vals import ( "reflect" ) // KeysIterator wraps the IterateKeys method. type KeysIterator interface { // IterateKeys calls the passed function with each key within the receiver. // The iteration is aborted if the function returns false. IterateKeys(func(v interface{}) bool) } type cannotIterateKeysOf struct{ kind string } func (err cannotIterateKeysOf) Error() string { return "cannot iterate keys of " + err.kind } // IterateKeys iterates the keys of the supplied value, calling the supplied // function for each key. The function can return false to break the iteration. // It is implemented for the Map type, StructMap types, and types satisfying the // IterateKeyser interface. For these types, it always returns a nil error. For // other types, it doesn't do anything and returns an error. func IterateKeys(v interface{}, f func(interface{}) bool) error { switch v := v.(type) { case KeysIterator: v.IterateKeys(f) case Map: for it := v.Iterator(); it.HasElem(); it.Next() { k, _ := it.Elem() if !f(k) { break } } case StructMap: iterateKeysStructMap(v, f) case PseudoStructMap: iterateKeysStructMap(v.Fields(), f) default: return cannotIterateKeysOf{Kind(v)} } return nil } func iterateKeysStructMap(v StructMap, f func(interface{}) bool) { for _, k := range getStructMapInfo(reflect.TypeOf(v)).fieldNames { if k == "" { continue } if !f(k) { break } } } elvish-0.17.0/pkg/eval/vals/iterate_keys_test.go000066400000000000000000000027351415471104000216300ustar00rootroot00000000000000package vals import ( "testing" . "src.elv.sh/pkg/tt" ) func vs(xs ...interface{}) []interface{} { return xs } type keysIterator struct{ keys []interface{} } func (k keysIterator) IterateKeys(f func(interface{}) bool) { Feed(f, k.keys...) } type nonKeysIterator struct{} func TestIterateKeys(t *testing.T) { Test(t, Fn("collectKeys", collectKeys), Table{ Args(MakeMap("k1", "v1", "k2", "v2")).Rets(vs("k1", "k2"), nil), Args(keysIterator{vs("lorem", "ipsum")}).Rets(vs("lorem", "ipsum")), Args(nonKeysIterator{}).Rets( Any, cannotIterateKeysOf{"!!vals.nonKeysIterator"}), }) } func TestIterateKeys_Map_Break(t *testing.T) { var gotKey interface{} IterateKeys(MakeMap("k", "v", "k2", "v2"), func(k interface{}) bool { if gotKey != nil { t.Errorf("callback called again after returning false") } gotKey = k return false }) if gotKey != "k" && gotKey != "k2" { t.Errorf("got key %v, want k or k2", gotKey) } } func TestIterateKeys_StructMap_Break(t *testing.T) { var gotKey interface{} IterateKeys(testStructMap{}, func(k interface{}) bool { if gotKey != nil { t.Errorf("callback called again after returning false") } gotKey = k return false }) if gotKey != "name" { t.Errorf("got key %v, want name", gotKey) } } func TestIterateKeys_Unsupported(t *testing.T) { err := IterateKeys(1, func(interface{}) bool { return true }) wantErr := cannotIterateKeysOf{"number"} if err != wantErr { t.Errorf("got error %v, want %v", err, wantErr) } } elvish-0.17.0/pkg/eval/vals/iterate_test.go000066400000000000000000000016001415471104000205630ustar00rootroot00000000000000package vals import ( "testing" . "src.elv.sh/pkg/tt" ) // An implementation of Iterator. type iterator struct{ elements []interface{} } func (i iterator) Iterate(f func(interface{}) bool) { Feed(f, i.elements...) } // A non-implementation of Iterator. type nonIterator struct{} func TestCanIterate(t *testing.T) { Test(t, Fn("CanIterate", CanIterate), Table{ Args("foo").Rets(true), Args(MakeList("foo", "bar")).Rets(true), Args(iterator{vs("a", "b")}).Rets(true), Args(nonIterator{}).Rets(false), }) } func TestCollect(t *testing.T) { Test(t, Fn("Collect", Collect), Table{ Args("foo").Rets(vs("f", "o", "o"), nil), Args(MakeList("foo", "bar")).Rets(vs("foo", "bar"), nil), Args(iterator{vs("a", "b")}).Rets(vs("a", "b"), nil), Args(nonIterator{}).Rets(vs(), cannotIterate{"!!vals.nonIterator"}), }) } // Iterate is tested indirectly by the test against Collect. elvish-0.17.0/pkg/eval/vals/kind.go000066400000000000000000000021741415471104000170230ustar00rootroot00000000000000package vals import ( "fmt" "math/big" ) // Kinder wraps the Kind method. type Kinder interface { Kind() string } // Kind returns the "kind" of the value, a concept similar to type but not yet // very well defined. It is implemented for the builtin nil, bool and string, // the File, List, Map types, StructMap types, and types satisfying the Kinder // interface. For other types, it returns the Go type name of the argument // preceded by "!!". // // TODO: Decide what `kind-of` should report for an external command object // and document the rationale for the choice in the doc string for `func // (ExternalCmd) Kind()` as well as user facing documentation. It's not // obvious why this returns "fn" rather than "external" for that case. func Kind(v interface{}) string { switch v := v.(type) { case nil: return "nil" case bool: return "bool" case string: return "string" case int, *big.Int, *big.Rat, float64: return "number" case Kinder: return v.Kind() case File: return "file" case List: return "list" case Map: return "map" case StructMap: return "structmap" default: return fmt.Sprintf("!!%T", v) } } elvish-0.17.0/pkg/eval/vals/kind_test.go000066400000000000000000000010321415471104000200520ustar00rootroot00000000000000package vals import ( "math/big" "os" "testing" . "src.elv.sh/pkg/tt" ) type xtype int func TestKind(t *testing.T) { Test(t, Fn("Kind", Kind), Table{ Args(nil).Rets("nil"), Args(true).Rets("bool"), Args("").Rets("string"), Args(1).Rets("number"), Args(bigInt(z)).Rets("number"), Args(big.NewRat(1, 2)).Rets("number"), Args(1.0).Rets("number"), Args(os.Stdin).Rets("file"), Args(EmptyList).Rets("list"), Args(EmptyMap).Rets("map"), Args(xtype(0)).Rets("!!vals.xtype"), Args(os.Stdin).Rets("file"), }) } elvish-0.17.0/pkg/eval/vals/len.go000066400000000000000000000012351415471104000166510ustar00rootroot00000000000000package vals import ( "reflect" "src.elv.sh/pkg/persistent/vector" ) // Lener wraps the Len method. type Lener interface { // Len computes the length of the receiver. Len() int } var _ Lener = vector.Vector(nil) // Len returns the length of the value, or -1 if the value does not have a // well-defined length. It is implemented for the builtin type string, StructMap // types, and types satisfying the Lener interface. For other types, it returns // -1. func Len(v interface{}) int { switch v := v.(type) { case string: return len(v) case Lener: return v.Len() case StructMap: return getStructMapInfo(reflect.TypeOf(v)).filledFields } return -1 } elvish-0.17.0/pkg/eval/vals/len_test.go000066400000000000000000000002571415471104000177130ustar00rootroot00000000000000package vals import ( "testing" . "src.elv.sh/pkg/tt" ) func TestLen(t *testing.T) { Test(t, Fn("Len", Len), Table{ Args("foobar").Rets(6), Args(10).Rets(-1), }) } elvish-0.17.0/pkg/eval/vals/num.go000066400000000000000000000151061415471104000166740ustar00rootroot00000000000000package vals import ( "fmt" "math" "math/big" "strconv" "strings" ) // Design notes: // // The choice and relationship of number types in Elvish is closely modelled // after R6RS's numerical tower (with the omission of complex types for now). In // fact, there is a 1:1 correspondence between number types in Elvish and a // typical R6RS implementation (the list below uses Chez Scheme's terminology; // see https://www.scheme.com/csug8/numeric.html): // // int : fixnum // *big.Int : bignum // *big.Rat : ratnum // float64 : flonum // // Similar to Chez Scheme, *big.Int is only used for representing integers // outside the range of int, and *big.Rat is only used for representing // non-integer rationals. Furthermore, *big.Rat values are always in simplest // form (this is guaranteed by the math/big library). As a consequence, each // number in Elvish only has a single unique representation. // // Note that the only machine-native integer type included in the system is int. // This is done primarily for the uniqueness of representation for each number, // but also for simplicity - the vast majority of Go functions that take // machine-native integers take int. When there is a genuine need to work with // other machine-native integer types, you may have to manually convert from and // to *big.Int and check for the relevant range of integers. // Num is a stand-in type for int, *big.Int, *big.Rat or float64. This type // doesn't offer type safety, but is useful as a marker; for example, it is // respected when parsing function arguments. type Num interface{} // NumSlice is a stand-in type for []int, []*big.Int, []*big.Rat or []float64. // This type doesn't offer type safety, but is useful as a marker. type NumSlice interface{} // ParseNum parses a string into a suitable number type. If the string does not // represent a valid number, it returns nil. func ParseNum(s string) Num { if strings.ContainsRune(s, '/') { // Parse as big.Rat if z, ok := new(big.Rat).SetString(s); ok { return NormalizeBigRat(z) } return nil } // Try parsing as big.Int if z, ok := new(big.Int).SetString(s, 0); ok { return NormalizeBigInt(z) } // Try parsing as float64 if f, err := strconv.ParseFloat(s, 64); err == nil { return f } return nil } // NumType represents a number type. type NumType uint8 // PromoteToBigInt converts an int or *big.Int to a *big.Int. It panics if n is // any other type. // Possible values for NumType, sorted in the order of implicit conversion // (lower types can be implicitly converted to higher types). const ( Int NumType = iota BigInt BigRat Float64 ) // UnifyNums unifies the given slice of numbers into the same type, converting // those with lower NumType to the highest NumType present in the slice. The typ // argument can be used to force the minimum NumType. func UnifyNums(nums []Num, typ NumType) NumSlice { for _, num := range nums { if t := getNumType(num); t > typ { typ = t } } switch typ { case Int: // PromoteToBigInt converts an int or *big.Int, a *big.I or *big.Ratnt. It // paniRat if n is any other type. unified := make([]int, len(nums)) for i, num := range nums { unified[i] = num.(int) } return unified case BigInt: unified := make([]*big.Int, len(nums)) for i, num := range nums { unified[i] = PromoteToBigInt(num) } return unified case BigRat: unified := make([]*big.Rat, len(nums)) for i, num := range nums { unified[i] = PromoteToBigRat(num) } return unified case Float64: unified := make([]float64, len(nums)) for i, num := range nums { unified[i] = ConvertToFloat64(num) } return unified default: panic("unreachable") } } // UnifyNums2 is like UnifyNums, but is optimized for two numbers. func UnifyNums2(n1, n2 Num, typ NumType) (u1, u2 Num) { t1 := getNumType(n1) if typ < t1 { typ = t1 } t2 := getNumType(n2) if typ < t2 { typ = t2 } switch typ { case Int: return n1, n2 case BigInt: return PromoteToBigInt(n1), PromoteToBigInt(n2) case BigRat: return PromoteToBigRat(n1), PromoteToBigRat(n2) case Float64: return ConvertToFloat64(n1), ConvertToFloat64(n2) default: panic("unreachable") } } // getNumType returns the type of the interface if the value is a number; otherwise, it panics since // that is a "can't happen" case. func getNumType(n Num) NumType { switch n.(type) { case int: return Int case *big.Int: return BigInt case *big.Rat: return BigRat case float64: return Float64 default: panic("invalid num type " + fmt.Sprintf("%T", n)) } } // PromoteToBigInt converts an int or *big.Int to a *big.Int. It panics if n is // any other type. func PromoteToBigInt(n Num) *big.Int { switch n := n.(type) { case int: return big.NewInt(int64(n)) case *big.Int: return n default: panic("invalid num type " + fmt.Sprintf("%T", n)) } } // PromoteToBigRat converts an int, *big.Int or *big.Rat to a *big.Rat. It // panics if n is any other type. func PromoteToBigRat(n Num) *big.Rat { switch n := n.(type) { case int: return big.NewRat(int64(n), 1) case *big.Int: var r big.Rat r.SetInt(n) return &r case *big.Rat: return n default: panic("invalid num type " + fmt.Sprintf("%T", n)) } } // ConvertToFloat64 converts any number to float64. It panics if num is not a // number value. func ConvertToFloat64(num Num) float64 { switch num := num.(type) { case int: return float64(num) case *big.Int: if num.IsInt64() { // Number can be converted losslessly to int64, so do that and then // rely on the builtin conversion. Numbers too large to fit in // float64 will be handled appropriately by the builtin conversion, // overflowing to +Inf or -Inf. return float64(num.Int64()) } // Number doesn't fit in int64, so definitely won't fit in float64; // handle this by overflowing. return math.Inf(num.Sign()) case *big.Rat: f, _ := num.Float64() return f case float64: return num default: panic("invalid num type " + fmt.Sprintf("%T", num)) } } // NormalizeBigInt converts a big.Int to an int if it is within the range of // int. Otherwise it returns n as is. func NormalizeBigInt(z *big.Int) Num { if i, ok := getInt(z); ok { return i } return z } // NormalizeBigRat converts a big.Rat to a big.Int (or an int if within the // range) if its denominator is 1. func NormalizeBigRat(z *big.Rat) Num { if z.IsInt() { n := z.Num() if i, ok := getInt(n); ok { return i } return n } return z } func getInt(z *big.Int) (int, bool) { // TODO: Use a more efficient implementation by examining z.Bits if z.IsInt64() { i64 := z.Int64() i := int(i64) if int64(i) == i64 { return i, true } } return -1, false } elvish-0.17.0/pkg/eval/vals/num_test.go000066400000000000000000000054321415471104000177340ustar00rootroot00000000000000package vals import ( "math" "math/big" "testing" "src.elv.sh/pkg/testutil" . "src.elv.sh/pkg/tt" ) // Test utilities. const ( zeros = "0000000000000000000" // Values that exceed the range of int64, used for testing BigInt. z = "1" + zeros + "0" z1 = "1" + zeros + "1" // z+1 z2 = "1" + zeros + "2" // z+2 z3 = "1" + zeros + "3" // z+3 zz = "2" + zeros + "0" // 2z zz1 = "2" + zeros + "1" // 2z+1 zz2 = "2" + zeros + "2" // 2z+2 zz3 = "2" + zeros + "3" // 2z+3 ) func TestParseNum(t *testing.T) { Test(t, Fn("ParseNum", ParseNum), Table{ Args("1").Rets(1), Args(z).Rets(bigInt(z)), Args("1/2").Rets(big.NewRat(1, 2)), Args("2/1").Rets(2), Args(z + "/1").Rets(bigInt(z)), Args("1.0").Rets(1.0), Args("1e-5").Rets(1e-5), Args("x").Rets(nil), Args("x/y").Rets(nil), }) } func TestUnifyNums(t *testing.T) { Test(t, Fn("UnifyNums", UnifyNums), Table{ Args([]Num{1, 2, 3, 4}, Int). Rets([]int{1, 2, 3, 4}), Args([]Num{1, 2, 3, bigInt(z)}, Int). Rets([]*big.Int{big.NewInt(1), big.NewInt(2), big.NewInt(3), bigInt(z)}), Args([]Num{1, 2, 3, big.NewRat(1, 2)}, Int). Rets([]*big.Rat{ big.NewRat(1, 1), big.NewRat(2, 1), big.NewRat(3, 1), big.NewRat(1, 2)}), Args([]Num{1, 2, bigInt(z), big.NewRat(1, 2)}, Int). Rets([]*big.Rat{ big.NewRat(1, 1), big.NewRat(2, 1), bigRat(z), big.NewRat(1, 2)}), Args([]Num{1, 2, 3, 4.0}, Int). Rets([]float64{1, 2, 3, 4}), Args([]Num{1, 2, big.NewRat(1, 2), 4.0}, Int). Rets([]float64{1, 2, 0.5, 4}), Args([]Num{1, 2, big.NewInt(3), 4.0}, Int). Rets([]float64{1, 2, 3, 4}), Args([]Num{1, 2, bigInt(z), 4.0}, Int). Rets([]float64{1, 2, math.Inf(1), 4}), Args([]Num{1, 2, 3, 4}, BigInt). Rets([]*big.Int{ big.NewInt(1), big.NewInt(2), big.NewInt(3), big.NewInt(4)}), }) } func TestUnifyNums2(t *testing.T) { Test(t, Fn("UnifyNums2", UnifyNums2), Table{ Args(1, 2, Int).Rets(1, 2), Args(1, bigInt(z), Int).Rets(big.NewInt(1), bigInt(z)), Args(1, big.NewRat(1, 2), Int).Rets(big.NewRat(1, 1), big.NewRat(1, 2)), Args(1, 2.0, Int).Rets(1.0, 2.0), Args(1, 2, BigInt).Rets(big.NewInt(1), big.NewInt(2)), }) } func TestInvalidNumType(t *testing.T) { Test(t, Fn("Recover", testutil.Recover), Table{ Args(func() { UnifyNums([]Num{int32(0)}, 0) }).Rets("invalid num type int32"), Args(func() { PromoteToBigInt(int32(0)) }).Rets("invalid num type int32"), Args(func() { PromoteToBigRat(int32(0)) }).Rets("invalid num type int32"), Args(func() { ConvertToFloat64(int32(0)) }).Rets("invalid num type int32"), }) } func bigInt(s string) *big.Int { z, ok := new(big.Int).SetString(s, 0) if !ok { panic("cannot parse as big int: " + s) } return z } func bigRat(s string) *big.Rat { z, ok := new(big.Rat).SetString(s) if !ok { panic("cannot parse as big rat: " + s) } return z } elvish-0.17.0/pkg/eval/vals/pipe.go000066400000000000000000000022451415471104000170320ustar00rootroot00000000000000package vals import ( "fmt" "os" "src.elv.sh/pkg/persistent/hash" ) // Pipe wraps a pair of pointers to os.File that are the two ends of the same // pipe. type Pipe struct { ReadEnd, WriteEnd *os.File } var _ PseudoStructMap = Pipe{} // NewPipe creates a new Pipe value. func NewPipe(r, w *os.File) Pipe { return Pipe{r, w} } // Kind returns "pipe". func (Pipe) Kind() string { return "pipe" } // Equal compares based on the equality of the two consistuent files. func (p Pipe) Equal(rhs interface{}) bool { q, ok := rhs.(Pipe) if !ok { return false } return Equal(p.ReadEnd, q.ReadEnd) && Equal(p.WriteEnd, q.WriteEnd) } // Hash calculates the hash based on the two constituent files. func (p Pipe) Hash() uint32 { return hash.DJB(Hash(p.ReadEnd), Hash(p.WriteEnd)) } // Repr writes an opaque representation containing the FDs of the two // constituent files. func (p Pipe) Repr(int) string { return fmt.Sprintf("", p.ReadEnd.Fd(), p.WriteEnd.Fd()) } // Fields returns fields of the Pipe value. func (p Pipe) Fields() StructMap { return pipeFields{p.ReadEnd, p.WriteEnd} } type pipeFields struct{ R, W *os.File } func (pipeFields) IsStructMap() {} elvish-0.17.0/pkg/eval/vals/pipe_test.go000066400000000000000000000010041415471104000200610ustar00rootroot00000000000000package vals import ( "fmt" "testing" "src.elv.sh/pkg/persistent/hash" "src.elv.sh/pkg/testutil" ) func TestPipe(t *testing.T) { pr, pw := testutil.MustPipe() defer pr.Close() defer pw.Close() TestValue(t, NewPipe(pr, pw)). Kind("pipe"). Bool(true). Hash(hash.DJB(hash.UIntPtr(pr.Fd()), hash.UIntPtr(pw.Fd()))). Repr(fmt.Sprintf("", pr.Fd(), pw.Fd())). Equal(NewPipe(pr, pw)). NotEqual(123, "a string", NewPipe(pw, pr)). AllKeys("r", "w"). Index("r", pr). Index("w", pw) } elvish-0.17.0/pkg/eval/vals/reflect_wrappers.go000066400000000000000000000013211415471104000214360ustar00rootroot00000000000000package vals import "reflect" var ( dummy interface{} nilValue = reflect.ValueOf(&dummy).Elem() emptyInterfaceType = reflect.TypeOf(&dummy).Elem() ) // ValueOf is like reflect.ValueOf, except that when given an argument of nil, // it does not return a zero Value, but the Value for the zero value of the // empty interface. func ValueOf(i interface{}) reflect.Value { if i == nil { return nilValue } return reflect.ValueOf(i) } // TypeOf is like reflect.TypeOf, except that when given an argument of nil, it // does not return nil, but the Type for the empty interface. func TypeOf(i interface{}) reflect.Type { if i == nil { return emptyInterfaceType } return reflect.TypeOf(i) } elvish-0.17.0/pkg/eval/vals/repr.go000066400000000000000000000051501415471104000170430ustar00rootroot00000000000000package vals import ( "fmt" "math" "math/big" "reflect" "strconv" "src.elv.sh/pkg/parse" ) // NoPretty can be passed to Repr to suppress pretty-printing. const NoPretty = math.MinInt32 // Reprer wraps the Repr method. type Reprer interface { // Repr returns a string that represents a Value. The string either be a // literal of that Value that is preferably deep-equal to it (like `[a b c]` // for a list), or a string enclosed in "<>" containing the kind and // identity of the Value(like ``). // // If indent is at least 0, it should be pretty-printed with the current // indentation level of indent; the indent of the first line has already // been written and shall not be written in Repr. The returned string // should never contain a trailing newline. Repr(indent int) string } // Repr returns the representation for a value, a string that is preferably (but // not necessarily) an Elvish expression that evaluates to the argument. If // indent >= 0, the representation is pretty-printed. It is implemented for the // builtin types nil, bool and string, the File, List and Map types, StructMap // types, and types satisfying the Reprer interface. For other types, it uses // fmt.Sprint with the format "". func Repr(v interface{}, indent int) string { switch v := v.(type) { case nil: return "$nil" case bool: if v { return "$true" } return "$false" case string: return parse.Quote(v) case int: return "(num " + strconv.Itoa(v) + ")" case *big.Int: return "(num " + v.String() + ")" case *big.Rat: return "(num " + v.String() + ")" case float64: return "(num " + formatFloat64(v) + ")" case Reprer: return v.Repr(indent) case File: return fmt.Sprintf("", parse.Quote(v.Name()), v.Fd()) case List: b := NewListReprBuilder(indent) for it := v.Iterator(); it.HasElem(); it.Next() { b.WriteElem(Repr(it.Elem(), indent+1)) } return b.String() case Map: builder := NewMapReprBuilder(indent) for it := v.Iterator(); it.HasElem(); it.Next() { k, v := it.Elem() builder.WritePair(Repr(k, indent+1), indent+2, Repr(v, indent+2)) } return builder.String() case StructMap: return reprStructMap(v, indent) case PseudoStructMap: return reprStructMap(v.Fields(), indent) default: return fmt.Sprintf("", v) } } func reprStructMap(v StructMap, indent int) string { vValue := reflect.ValueOf(v) vType := vValue.Type() builder := NewMapReprBuilder(indent) it := iterateStructMap(vType) for it.Next() { k, v := it.Get(vValue) builder.WritePair(Repr(k, indent+1), indent+2, Repr(v, indent+2)) } return builder.String() } elvish-0.17.0/pkg/eval/vals/repr_helpers.go000066400000000000000000000034651415471104000205740ustar00rootroot00000000000000package vals import ( "bytes" "strings" ) // ListReprBuilder helps to build Repr of list-like Values. type ListReprBuilder struct { indent int buf bytes.Buffer } // NewListReprBuilder makes a new ListReprBuilder. func NewListReprBuilder(indent int) *ListReprBuilder { return &ListReprBuilder{indent: indent} } // WriteElem writes a new element. func (b *ListReprBuilder) WriteElem(v string) { if b.buf.Len() == 0 { b.buf.WriteByte('[') } if b.indent >= 0 { // Pretty-printing: Add a newline and indent+1 spaces. b.buf.WriteString("\n" + strings.Repeat(" ", b.indent+1)) } else if b.buf.Len() > 1 { b.buf.WriteByte(' ') } b.buf.WriteString(v) } // String returns the representation that has been built. After it is called, // the ListReprBuilder may no longer be used. func (b *ListReprBuilder) String() string { if b.buf.Len() == 0 { return "[]" } if b.indent >= 0 { b.buf.WriteString("\n" + strings.Repeat(" ", b.indent)) } b.buf.WriteByte(']') return b.buf.String() } // MapReprBuilder helps building the Repr of a Map. It is also useful for // implementing other Map-like values. The zero value of a MapReprBuilder is // ready to use. type MapReprBuilder struct { inner ListReprBuilder } // NewMapReprBuilder makes a new MapReprBuilder. func NewMapReprBuilder(indent int) *MapReprBuilder { return &MapReprBuilder{ListReprBuilder{indent: indent}} } // WritePair writes a new pair. func (b *MapReprBuilder) WritePair(k string, indent int, v string) { if indent > 0 { b.inner.WriteElem("&" + k + "=\t" + v) } else { b.inner.WriteElem("&" + k + "=" + v) } } // String returns the representation that has been built. After it is called, // the MapReprBuilder should no longer be used. func (b *MapReprBuilder) String() string { s := b.inner.String() if s == "[]" { return "[&]" } return s } elvish-0.17.0/pkg/eval/vals/repr_test.go000066400000000000000000000016661415471104000201120ustar00rootroot00000000000000package vals import ( "fmt" "math/big" "os" "testing" . "src.elv.sh/pkg/tt" ) type reprer struct{} func (reprer) Repr(int) string { return "" } type nonReprer struct{} func repr(a interface{}) string { return Repr(a, NoPretty) } func TestRepr(t *testing.T) { Test(t, Fn("repr", repr), Table{ Args(nil).Rets("$nil"), Args(false).Rets("$false"), Args(true).Rets("$true"), Args("foo").Rets("foo"), Args(1).Rets("(num 1)"), Args(bigInt(z)).Rets("(num " + z + ")"), Args(big.NewRat(1, 2)).Rets("(num 1/2)"), Args(1.0).Rets("(num 1.0)"), Args(1e10).Rets("(num 10000000000.0)"), Args(os.Stdin).Rets( fmt.Sprintf("", os.Stdin.Name(), os.Stdin.Fd())), Args(EmptyList).Rets("[]"), Args(MakeList("foo", "bar")).Rets("[foo bar]"), Args(EmptyMap).Rets("[&]"), Args(MakeMap("foo", "bar")).Rets("[&foo=bar]"), Args(reprer{}).Rets(""), Args(nonReprer{}).Rets(""), }) } elvish-0.17.0/pkg/eval/vals/string.go000066400000000000000000000027551415471104000174110ustar00rootroot00000000000000package vals import ( "math" "strconv" "strings" ) // Stringer wraps the String method. type Stringer interface { // Stringer converts the receiver to a string. String() string } // ToString converts a Value to string. It is implemented for the builtin // float64 and string types, and type satisfying the Stringer interface. It // falls back to Repr(v, NoPretty). func ToString(v interface{}) string { switch v := v.(type) { case int: return strconv.Itoa(v) case float64: return formatFloat64(v) // Other number types handled by "case Stringer" case string: return v case Stringer: return v.String() default: return Repr(v, NoPretty) } } func formatFloat64(f float64) string { // Go's 'g' format is not quite ideal for printing floating point numbers; // it uses scientific notation too aggressively, and relatively small // numbers like 1234567 are printed with scientific notations, something we // don't really want. // // So we use a different algorithm for determining when to use scientific // notation. The algorithm is reverse-engineered from Racket's; it may not // be a perfect clone but hopefully good enough. // // See also b.elv.sh/811 for more context. s := strconv.FormatFloat(f, 'f', -1, 64) noPoint := !strings.ContainsRune(s, '.') if (noPoint && len(s) > 14 && s[len(s)-1] == '0') || strings.HasPrefix(s, "0.0000") { return strconv.FormatFloat(f, 'e', -1, 64) } else if noPoint && !math.IsNaN(f) && !math.IsInf(f, 0) { return s + ".0" } return s } elvish-0.17.0/pkg/eval/vals/string_test.go000066400000000000000000000014241415471104000204400ustar00rootroot00000000000000package vals import ( "bytes" "testing" . "src.elv.sh/pkg/tt" ) func TestToString(t *testing.T) { Test(t, Fn("ToString", ToString), Table{ // string Args("a").Rets("a"), Args(1).Rets("1"), // float64 Args(0.1).Rets("0.1"), Args(42.0).Rets("42.0"), // Whole numbers with more than 14 digits and trailing 0 are printed in // scientific notation. Args(1e13).Rets("10000000000000.0"), Args(1e14).Rets("1e+14"), Args(1e14 + 1).Rets("100000000000001.0"), // Numbers smaller than 0.0001 are printed in scientific notation. Args(0.0001).Rets("0.0001"), Args(0.00001).Rets("1e-05"), Args(0.00009).Rets("9e-05"), // Stringer Args(bytes.NewBufferString("buffer")).Rets("buffer"), // None of the above: delegate to Repr Args(true).Rets("$true"), }) } elvish-0.17.0/pkg/eval/vals/struct_map.go000066400000000000000000000061201415471104000202520ustar00rootroot00000000000000package vals import ( "reflect" "sync" "src.elv.sh/pkg/strutil" ) // StructMap may be implemented by a struct to mark the struct as a "struct // map", which causes Elvish to treat it like a read-only map. Each exported, // named field and getter method (a method taking no argument and returning one // value) becomes a field of the map, with the name mapped to dash-case. // // The following operations are derived for structmaps: Kind, Repr, Hash, Len, // Index, HasKey and IterateKeys. // // Example: // // type someStruct struct { // FooBar int // lorem string // } // // func (someStruct) IsStructMap() { } // // func (s SomeStruct) Ipsum() string { return s.lorem } // // func (s SomeStruct) OtherMethod(int) { } // // An instance of someStruct behaves like a read-only map with 3 fields: // foo-bar, lorem and ipsum. type StructMap interface{ IsStructMap() } // PseudoStructMap may be implemented by a type to derive the Repr, Index, // HasKey and IterateKeys operations from the struct map returned by the Fields // method. type PseudoStructMap interface{ Fields() StructMap } // Keeps cached information about a structMap. type structMapInfo struct { filledFields int plainFields int // Dash-case names for all fields. The first plainFields elements // corresponds to all the plain fields, while the rest corresponds to getter // fields. May contain empty strings if the corresponding field is not // reflected onto the structMap (i.e. unexported fields, unexported methods // and non-getter methods). fieldNames []string } var structMapInfos sync.Map // Gets the structMapInfo associated with a type, caching the result. func getStructMapInfo(t reflect.Type) structMapInfo { if info, ok := structMapInfos.Load(t); ok { return info.(structMapInfo) } info := makeStructMapInfo(t) structMapInfos.Store(t, info) return info } func makeStructMapInfo(t reflect.Type) structMapInfo { n := t.NumField() m := t.NumMethod() fieldNames := make([]string, n+m) filledFields := 0 for i := 0; i < n; i++ { field := t.Field(i) if field.PkgPath == "" && !field.Anonymous { fieldNames[i] = strutil.CamelToDashed(field.Name) filledFields++ } } for i := 0; i < m; i++ { method := t.Method(i) if method.PkgPath == "" && method.Type.NumIn() == 1 && method.Type.NumOut() == 1 { fieldNames[i+n] = strutil.CamelToDashed(method.Name) filledFields++ } } return structMapInfo{filledFields, n, fieldNames} } type structMapIterator struct { info structMapInfo i int } func iterateStructMap(t reflect.Type) *structMapIterator { return &structMapIterator{getStructMapInfo(t), -1} } func (it *structMapIterator) Next() bool { fields := it.info.fieldNames if it.i >= len(fields) { return false } it.i++ for it.i < len(fields) && fields[it.i] == "" { it.i++ } return it.i < len(fields) } func (it *structMapIterator) Get(v reflect.Value) (string, interface{}) { name := it.info.fieldNames[it.i] if it.i < it.info.plainFields { return name, v.Field(it.i).Interface() } method := v.Method(it.i - it.info.plainFields) return name, method.Call(nil)[0].Interface() } elvish-0.17.0/pkg/eval/vals/struct_map_test.go000066400000000000000000000045461415471104000213230ustar00rootroot00000000000000package vals import ( "testing" "src.elv.sh/pkg/persistent/hash" ) type testStructMap struct { Name string ScoreNumber float64 } func (testStructMap) IsStructMap() {} // Structurally identical to testStructMap. type testStructMap2 struct { Name string ScoreNumber float64 } func (testStructMap2) IsStructMap() {} type testStructMap3 struct { Name string score float64 } func (testStructMap3) IsStructMap() {} func (m testStructMap3) Score() float64 { return m.score + 10 } func TestStructMap(t *testing.T) { TestValue(t, testStructMap{}). Kind("structmap"). Bool(true). Hash(hash.DJB(Hash(""), Hash(0.0))). Repr(`[&name='' &score-number=(num 0.0)]`). Len(2). Equal(testStructMap{}). NotEqual("a", MakeMap(), testStructMap{"a", 1.0}). // StructMap's are nominally typed. This may change in future. NotEqual(testStructMap2{}). HasKey("name", "score-number"). HasNoKey("bad", 1.0). IndexError("bad", NoSuchKey("bad")). IndexError(1.0, NoSuchKey(1.0)). AllKeys("name", "score-number"). Index("name", ""). Index("score-number", 0.0) TestValue(t, testStructMap{"a", 1.0}). Kind("structmap"). Bool(true). Hash(hash.DJB(Hash("a"), Hash(1.0))). Repr(`[&name=a &score-number=(num 1.0)]`). Len(2). Equal(testStructMap{"a", 1.0}). NotEqual( "a", MakeMap("name", "", "score-number", 1.0), testStructMap{}, testStructMap{"a", 2.0}, testStructMap{"b", 1.0}). // Keys are tested above, thus omitted here. Index("name", "a"). Index("score-number", 1.0) TestValue(t, testStructMap3{"a", 1.0}). Kind("structmap"). Bool(true). Hash(hash.DJB(Hash("a"), Hash(11.0))). Repr(`[&name=a &score=(num 11.0)]`). Len(2). Equal(testStructMap3{"a", 1.0}). NotEqual( "a", MakeMap("name", "", "score-number", 1.0), testStructMap{}, testStructMap{"a", 11.0}). // Keys are tested above, thus omitted here. Index("name", "a"). Index("score", 11.0) } type pseudoStructMap struct{} func (pseudoStructMap) Fields() StructMap { return testStructMap{"pseudo", 100} } func TestPseudoStructMap(t *testing.T) { TestValue(t, pseudoStructMap{}). Repr("[&name=pseudo &score-number=(num 100.0)]"). HasKey("name", "score-number"). HasNoKey("bad", 1.0). IndexError("bad", NoSuchKey("bad")). IndexError(1.0, NoSuchKey(1.0)). AllKeys("name", "score-number"). Index("name", "pseudo"). Index("score-number", 100.0) } elvish-0.17.0/pkg/eval/vals/testutils.go000066400000000000000000000120731415471104000201350ustar00rootroot00000000000000package vals import ( "reflect" "testing" "src.elv.sh/pkg/tt" ) // ValueTester is a helper for testing properties of a value. type ValueTester struct { t *testing.T v interface{} } // TestValue returns a ValueTester. func TestValue(t *testing.T, v interface{}) ValueTester { return ValueTester{t, v} } // Kind tests the Kind of the value. func (vt ValueTester) Kind(wantKind string) ValueTester { vt.t.Helper() kind := Kind(vt.v) if kind != wantKind { vt.t.Errorf("Kind(v) = %s, want %s", kind, wantKind) } return vt } // Bool tests the Boool of the value. func (vt ValueTester) Bool(wantBool bool) ValueTester { vt.t.Helper() b := Bool(vt.v) if b != wantBool { vt.t.Errorf("Bool(v) = %v, want %v", b, wantBool) } return vt } // Hash tests the Hash of the value. func (vt ValueTester) Hash(wantHash uint32) ValueTester { vt.t.Helper() hash := Hash(vt.v) if hash != wantHash { vt.t.Errorf("Hash(v) = %v, want %v", hash, wantHash) } return vt } // Len tests the Len of the value. func (vt ValueTester) Len(wantLen int) ValueTester { vt.t.Helper() kind := Len(vt.v) if kind != wantLen { vt.t.Errorf("Len(v) = %v, want %v", kind, wantLen) } return vt } // Repr tests the Repr of the value. func (vt ValueTester) Repr(wantRepr string) ValueTester { vt.t.Helper() kind := Repr(vt.v, NoPretty) if kind != wantRepr { vt.t.Errorf("Repr(v) = %s, want %s", kind, wantRepr) } return vt } // Equal tests that the value is Equal to every of the given values. func (vt ValueTester) Equal(others ...interface{}) ValueTester { vt.t.Helper() for _, other := range others { eq := Equal(vt.v, other) if !eq { vt.t.Errorf("Equal(v, %v) = false, want true", other) } } return vt } // NotEqual tests that the value is not Equal to any of the given values. func (vt ValueTester) NotEqual(others ...interface{}) ValueTester { vt.t.Helper() for _, other := range others { eq := Equal(vt.v, other) if eq { vt.t.Errorf("Equal(v, %v) = true, want false", other) } } return vt } // HasKey tests that the value has each of the given keys. func (vt ValueTester) HasKey(keys ...interface{}) ValueTester { vt.t.Helper() for _, key := range keys { has := HasKey(vt.v, key) if !has { vt.t.Errorf("HasKey(v, %v) = false, want true", key) } } return vt } // HasNoKey tests that the value does not have any of the given keys. func (vt ValueTester) HasNoKey(keys ...interface{}) ValueTester { vt.t.Helper() for _, key := range keys { has := HasKey(vt.v, key) if has { vt.t.Errorf("HasKey(v, %v) = true, want false", key) } } return vt } // AllKeys tests that the given keys match what the result of IterateKeys on the // value. // // NOTE: This now checks equality using reflect.DeepEqual, since all the builtin // types have string keys. This can be changed in future to use Equal is the // need arises. func (vt ValueTester) AllKeys(wantKeys ...interface{}) ValueTester { vt.t.Helper() keys, err := collectKeys(vt.v) if err != nil { vt.t.Errorf("IterateKeys(v, f) -> err %v, want nil", err) } if !reflect.DeepEqual(keys, wantKeys) { vt.t.Errorf("IterateKeys(v, f) calls f with %v, want %v", keys, wantKeys) } return vt } func collectKeys(v interface{}) ([]interface{}, error) { var keys []interface{} err := IterateKeys(v, func(k interface{}) bool { keys = append(keys, k) return true }) return keys, err } // Index tests that Index'ing the value with the given key returns the wanted value // and no error. func (vt ValueTester) Index(key, wantVal interface{}) ValueTester { vt.t.Helper() got, err := Index(vt.v, key) if err != nil { vt.t.Errorf("Index(v, %v) -> err %v, want nil", key, err) } if !Equal(got, wantVal) { vt.t.Errorf("Index(v, %v) -> %v, want %v", key, got, wantVal) } return vt } // IndexError tests that Index'ing the value with the given key returns the given // error. func (vt ValueTester) IndexError(key interface{}, wantErr error) ValueTester { vt.t.Helper() _, err := Index(vt.v, key) if !reflect.DeepEqual(err, wantErr) { vt.t.Errorf("Index(v, %v) -> err %v, want %v", key, err, wantErr) } return vt } // Assoc tests that Assoc'ing the value with the given key-value pair returns // the wanted new value and no error. func (vt ValueTester) Assoc(key, val, wantNew interface{}) ValueTester { vt.t.Helper() got, err := Assoc(vt.v, key, val) if err != nil { vt.t.Errorf("Assoc(v, %v) -> err %v, want nil", key, err) } if !Equal(got, wantNew) { vt.t.Errorf("Assoc(v, %v) -> %v, want %v", key, got, wantNew) } return vt } // AssocError tests that Assoc'ing the value with the given key-value pair // returns the given error. func (vt ValueTester) AssocError(key, val interface{}, wantErr error) ValueTester { vt.t.Helper() _, err := Assoc(vt.v, key, val) if !reflect.DeepEqual(err, wantErr) { vt.t.Errorf("Assoc(v, %v) -> err %v, want %v", key, err, wantErr) } return vt } // Eq returns a tt.Matcher that matches using the Equal function. func Eq(r interface{}) tt.Matcher { return equalMatcher{r} } type equalMatcher struct{ want interface{} } func (em equalMatcher) Match(got tt.RetValue) bool { return Equal(got, em.want) } elvish-0.17.0/pkg/eval/value_test.go000066400000000000000000000025441415471104000173050ustar00rootroot00000000000000package eval import ( "reflect" "testing" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/glob" ) var reprTests = []struct { v interface{} want string }{ {"233", "233"}, {"a\nb", `"a\nb"`}, {"foo bar", "'foo bar'"}, {"a\x00b", `"a\x00b"`}, {true, "$true"}, {false, "$false"}, {vals.EmptyList, "[]"}, {vals.MakeList("bash", false), "[bash $false]"}, {vals.EmptyMap, "[&]"}, {vals.MakeMap(&exception{nil, nil}, "elvish"), "[&$ok=elvish]"}, // TODO: test maps of more elements } func TestRepr(t *testing.T) { for _, test := range reprTests { repr := vals.Repr(test.v, vals.NoPretty) if repr != test.want { t.Errorf("Repr = %s, want %s", repr, test.want) } } } var stringToSegmentsTests = []struct { s string want []glob.Segment }{ {"", []glob.Segment{}}, {"a", []glob.Segment{glob.Literal{Data: "a"}}}, {"/a", []glob.Segment{glob.Slash{}, glob.Literal{Data: "a"}}}, {"a/", []glob.Segment{glob.Literal{Data: "a"}, glob.Slash{}}}, {"/a/", []glob.Segment{glob.Slash{}, glob.Literal{Data: "a"}, glob.Slash{}}}, {"a//b", []glob.Segment{glob.Literal{Data: "a"}, glob.Slash{}, glob.Literal{Data: "b"}}}, } func TestStringToSegments(t *testing.T) { for _, tc := range stringToSegmentsTests { segs := stringToSegments(tc.s) if !reflect.DeepEqual(segs, tc.want) { t.Errorf("stringToSegments(%q) => %v, want %v", tc.s, segs, tc.want) } } } elvish-0.17.0/pkg/eval/var_parse.go000066400000000000000000000023051415471104000171070ustar00rootroot00000000000000package eval import "strings" // SplitSigil splits any leading sigil from a qualified variable name. func SplitSigil(ref string) (sigil string, qname string) { if ref == "" { return "", "" } // TODO: Support % (and other sigils?) if https://b.elv.sh/584 is implemented for map explosion. switch ref[0] { case '@': return ref[:1], ref[1:] default: return "", ref } } // SplitQName splits a qualified name into the first namespace segment and the // rest. func SplitQName(qname string) (first, rest string) { colon := strings.IndexByte(qname, ':') if colon == -1 { return qname, "" } return qname[:colon+1], qname[colon+1:] } // SplitQNameSegs splits a qualified name into namespace segments. func SplitQNameSegs(qname string) []string { segs := strings.SplitAfter(qname, ":") if len(segs) > 0 && segs[len(segs)-1] == "" { segs = segs[:len(segs)-1] } return segs } // SplitIncompleteQNameNs splits an incomplete qualified variable name into the // namespace part and the name part. func SplitIncompleteQNameNs(qname string) (ns, name string) { colon := strings.LastIndexByte(qname, ':') // If colon is -1, colon+1 will be 0, rendering an empty ns. return qname[:colon+1], qname[colon+1:] } elvish-0.17.0/pkg/eval/var_parse_test.go000066400000000000000000000026011415471104000201450ustar00rootroot00000000000000package eval_test import ( "testing" . "src.elv.sh/pkg/eval" "src.elv.sh/pkg/tt" ) var Args = tt.Args func TestSplitSigil(t *testing.T) { tt.Test(t, tt.Fn("SplitSigil", SplitSigil), tt.Table{ Args("").Rets("", ""), Args("x").Rets("", "x"), Args("@x").Rets("@", "x"), Args("a:b").Rets("", "a:b"), Args("@a:b").Rets("@", "a:b"), }) } func TestSplitQName(t *testing.T) { tt.Test(t, tt.Fn("SplitQName", SplitQName), tt.Table{ Args("").Rets("", ""), Args("a").Rets("a", ""), Args("a:").Rets("a:", ""), Args("a:b").Rets("a:", "b"), Args("a:b:").Rets("a:", "b:"), Args("a:b:c").Rets("a:", "b:c"), Args("a:b:c:").Rets("a:", "b:c:"), }) } func TestSplitQNameSegs(t *testing.T) { tt.Test(t, tt.Fn("SplitQNameSegs", SplitQNameSegs), tt.Table{ Args("").Rets([]string{}), Args("a").Rets([]string{"a"}), Args("a:").Rets([]string{"a:"}), Args("a:b").Rets([]string{"a:", "b"}), Args("a:b:").Rets([]string{"a:", "b:"}), Args("a:b:c").Rets([]string{"a:", "b:", "c"}), Args("a:b:c:").Rets([]string{"a:", "b:", "c:"}), }) } func TestSplitIncompleteQNameNs(t *testing.T) { tt.Test(t, tt.Fn("SplitIncompleteQNameNs", SplitIncompleteQNameNs), tt.Table{ Args("").Rets("", ""), Args("a").Rets("", "a"), Args("a:").Rets("a:", ""), Args("a:b").Rets("a:", "b"), Args("a:b:").Rets("a:b:", ""), Args("a:b:c").Rets("a:b:", "c"), Args("a:b:c:").Rets("a:b:c:", ""), }) } elvish-0.17.0/pkg/eval/var_ref.go000066400000000000000000000132311415471104000165510ustar00rootroot00000000000000package eval import ( "strings" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/eval/vars" ) // This file implements variable resolution. Elvish has fully static lexical // scopes, so variable resolution involves some work in the compilation phase as // well. // // During compilation, a qualified variable name (whether in lvalue, like "x // = foo", or in variable use, like "$x") is searched in compiler's staticNs // tables to determine which scope they belong to, as well as their indices in // that scope. This step is just called "resolve" in the code, and it stores // information in a varRef struct. // // During evaluation, the varRef is then used to look up the Var for the // variable. This step is called "deref" in the code. // // The resolve phase can take place during evaluation as well for introspection. // Keeps all the information statically about a variable referenced by a // qualified name. type varRef struct { scope varScope info staticVarInfo index int subNames []string } type varScope int const ( localScope varScope = 1 + iota captureScope builtinScope envScope externalScope ) // An interface satisfied by both *compiler and *Frame. Used to implement // resolveVarRef as a function that works for both types. type scopeSearcher interface { searchLocal(k string) (staticVarInfo, int) searchCapture(k string) (staticVarInfo, int) searchBuiltin(k string, r diag.Ranger) (staticVarInfo, int) } // Resolves a qname into a varRef. func resolveVarRef(s scopeSearcher, qname string, r diag.Ranger) *varRef { if strings.HasPrefix(qname, ":") { qname = qname[1:] if cp, ok := s.(*compiler); ok { cp.deprecate(r, "the empty namespace is deprecated; use the variable directly instead", 17) } } if ref := resolveVarRefLocal(s, qname); ref != nil { return ref } if ref := resolveVarRefCapture(s, qname); ref != nil { return ref } if ref := resolveVarRefBuiltin(s, qname, r); ref != nil { return ref } return nil } func resolveVarRefLocal(s scopeSearcher, qname string) *varRef { first, rest := SplitQName(qname) if info, index := s.searchLocal(first); index != -1 { return &varRef{localScope, info, index, SplitQNameSegs(rest)} } return nil } func resolveVarRefCapture(s scopeSearcher, qname string) *varRef { first, rest := SplitQName(qname) if info, index := s.searchCapture(first); index != -1 { return &varRef{captureScope, info, index, SplitQNameSegs(rest)} } return nil } func resolveVarRefBuiltin(s scopeSearcher, qname string, r diag.Ranger) *varRef { first, rest := SplitQName(qname) if rest != "" { // Try special namespace first. switch first { case "local:": if cp, ok := s.(*compiler); ok { cp.deprecate(r, "the local: special namespace is deprecated; use the variable directly instead", 17) } return resolveVarRefLocal(s, rest) case "up:": if cp, ok := s.(*compiler); ok { cp.deprecate(r, "the up: special namespace is deprecated; use the variable directly if it is not shadowed", 17) } return resolveVarRefCapture(s, rest) case "e:": if strings.HasSuffix(rest, FnSuffix) { return &varRef{scope: externalScope, subNames: []string{rest[:len(rest)-1]}} } case "E:": return &varRef{scope: envScope, subNames: []string{rest}} } } if info, index := s.searchBuiltin(first, r); index != -1 { return &varRef{builtinScope, info, index, SplitQNameSegs(rest)} } return nil } // Tries to resolve the command head as an internal command, i.e. a builtin // special command or a function. func resolveCmdHeadInternally(s scopeSearcher, head string, r diag.Ranger) (compileBuiltin, *varRef) { special, ok := builtinSpecials[head] if ok { return special, nil } sigil, qname := SplitSigil(head) if sigil == "" { varName := qname + FnSuffix ref := resolveVarRef(s, varName, r) if ref != nil { return nil, ref } } return nil, nil } // Dereferences a varRef into a Var. func deref(fm *Frame, ref *varRef) vars.Var { variable, subNames := derefBase(fm, ref) for _, subName := range subNames { ns, ok := variable.Get().(*Ns) if !ok { return nil } variable = ns.IndexString(subName) if variable == nil { return nil } } return variable } func derefBase(fm *Frame, ref *varRef) (vars.Var, []string) { switch ref.scope { case localScope: return fm.local.slots[ref.index], ref.subNames case captureScope: return fm.up.slots[ref.index], ref.subNames case builtinScope: return fm.Evaler.Builtin().slots[ref.index], ref.subNames case envScope: return vars.FromEnv(ref.subNames[0]), nil case externalScope: return vars.NewReadOnly(NewExternalCmd(ref.subNames[0])), nil default: return nil, nil } } func (cp *compiler) searchLocal(k string) (staticVarInfo, int) { return cp.thisScope().lookup(k) } func (cp *compiler) searchCapture(k string) (staticVarInfo, int) { for i := len(cp.scopes) - 2; i >= 0; i-- { info, index := cp.scopes[i].lookup(k) if index != -1 { // Record the capture from i+1 to len(cp.scopes)-1, and reuse the // index to keep the index into the previous scope. index = cp.captures[i+1].add(k, true, index) for j := i + 2; j < len(cp.scopes); j++ { index = cp.captures[j].add(k, false, index) } return info, index } } return staticVarInfo{}, -1 } func (cp *compiler) searchBuiltin(k string, r diag.Ranger) (staticVarInfo, int) { info, index := cp.builtin.lookup(k) if index != -1 { cp.checkDeprecatedBuiltin(k, r) } return info, index } func (fm *Frame) searchLocal(k string) (staticVarInfo, int) { return fm.local.lookup(k) } func (fm *Frame) searchCapture(k string) (staticVarInfo, int) { return fm.up.lookup(k) } func (fm *Frame) searchBuiltin(k string, r diag.Ranger) (staticVarInfo, int) { return fm.Evaler.Builtin().lookup(k) } elvish-0.17.0/pkg/eval/vars/000077500000000000000000000000001415471104000155515ustar00rootroot00000000000000elvish-0.17.0/pkg/eval/vars/blackhole.go000066400000000000000000000007561415471104000200340ustar00rootroot00000000000000package vars type blackhole struct{} func (blackhole) Set(interface{}) error { return nil } func (blackhole) Get() interface{} { return nil } // NewBlackhole returns a blackhole variable. Assignments to a blackhole // variable will be discarded, and getting a blackhole variable always returns // nil. func NewBlackhole() Var { return blackhole{} } // IsBlackhole returns whether the variable is a blackhole variable. func IsBlackhole(v Var) bool { _, ok := v.(blackhole) return ok } elvish-0.17.0/pkg/eval/vars/blackhole_test.go000066400000000000000000000007211415471104000210630ustar00rootroot00000000000000package vars import ( "testing" "src.elv.sh/pkg/tt" ) func TestBlackhole(t *testing.T) { v := NewBlackhole() err := v.Set("foo") if err != nil { t.Errorf("v.Set(%q) -> %v, want nil", "foo", err) } val := v.Get() if val != nil { t.Errorf("v.Get() -> %v, want nil", val) } } func TestIsBlackhole(t *testing.T) { tt.Test(t, tt.Fn("IsBlackhole", IsBlackhole), tt.Table{ tt.Args(NewBlackhole()).Rets(true), tt.Args(FromInit("")).Rets(false), }) } elvish-0.17.0/pkg/eval/vars/callback.go000066400000000000000000000013671415471104000176430ustar00rootroot00000000000000package vars import ( "src.elv.sh/pkg/eval/errs" ) type callback struct { set func(interface{}) error get func() interface{} } // FromSetGet makes a variable from a set callback and a get callback. func FromSetGet(set func(interface{}) error, get func() interface{}) Var { return &callback{set, get} } func (cv *callback) Set(val interface{}) error { return cv.set(val) } func (cv *callback) Get() interface{} { return cv.get() } type roCallback func() interface{} // FromGet makes a variable from a get callback. The variable is read-only. func FromGet(get func() interface{}) Var { return roCallback(get) } func (cv roCallback) Set(interface{}) error { return errs.SetReadOnlyVar{} } func (cv roCallback) Get() interface{} { return cv() } elvish-0.17.0/pkg/eval/vars/callback_test.go000066400000000000000000000016331415471104000206760ustar00rootroot00000000000000package vars import "testing" func TestFromSetGet(t *testing.T) { getCalled := false get := func() interface{} { getCalled = true return "cb" } var setCalledWith interface{} set := func(v interface{}) error { setCalledWith = v return nil } v := FromSetGet(set, get) if v.Get() != "cb" { t.Errorf("cbVariable doesn't return value from callback") } if !getCalled { t.Errorf("cbVariable doesn't call callback") } v.Set("setting") if setCalledWith != "setting" { t.Errorf("cbVariable.Set doesn't call setter with value") } } func TestFromGet(t *testing.T) { getCalled := false get := func() interface{} { getCalled = true return "cb" } v := FromGet(get) if v.Get() != "cb" { t.Errorf("roCbVariable doesn't return value from callback") } if !getCalled { t.Errorf("roCbVariable doesn't call callback") } if v.Set("lala") == nil { t.Errorf("roCbVariable.Set doesn't error") } } elvish-0.17.0/pkg/eval/vars/element.go000066400000000000000000000073031415471104000175340ustar00rootroot00000000000000package vars import ( "src.elv.sh/pkg/eval/vals" ) type elem struct { variable Var assocers []interface{} indices []interface{} setValue interface{} } func (ev *elem) Set(v0 interface{}) error { var err error v := v0 // Evaluate the actual new value from inside out. See comments in // MakeElement for how element assignment works. for i := len(ev.assocers) - 1; i >= 0; i-- { v, err = vals.Assoc(ev.assocers[i], ev.indices[i], v) if err != nil { return err } } err = ev.variable.Set(v) // TODO(xiaq): Remember the set value for use in Get. ev.setValue = v0 return err } func (ev *elem) Get() interface{} { // TODO(xiaq): This is only called from fixNilVariables. We don't want to // waste time accessing the variable, so we simply return the value that was // set. return ev.setValue } // MakeElement returns a variable, that when set, simulates the mutation of an // element. func MakeElement(v Var, indices []interface{}) (Var, error) { // Assignment of indexed variables actually assigns the variable, with // the right hand being a nested series of Assocs. As the simplest // example, `a[0] = x` is equivalent to `a = (assoc $a 0 x)`. A more // complex example is that `a[0][1][2] = x` is equivalent to // `a = (assoc $a 0 (assoc $a[0] 1 (assoc $a[0][1] 2 x)))`. // Note that in each assoc form, the first two arguments can be // determined now, while the last argument is only known when the // right-hand-side is known. So here we evaluate the first two arguments // of each assoc form and put them in two slices, assocers and indices. // In the previous example, the two slices will contain: // // assocers: $a $a[0] $a[0][1] // indices: 0 1 2 // // When the right-hand side of the assignment becomes available, the new // value for $a is evaluated by doing Assoc from inside out. assocers := make([]interface{}, len(indices)) varValue := v.Get() assocers[0] = varValue for i, index := range indices[:len(indices)-1] { lastAssocer := assocers[i] v, err := vals.Index(lastAssocer, index) if err != nil { return nil, err } assocers[i+1] = v } return &elem{v, assocers, indices, nil}, nil } // DelElement deletes an element. It uses a similar process to MakeElement, // except that the last level of container needs to be Dissoc-able instead of // Assoc-able. func DelElement(variable Var, indices []interface{}) error { var err error // In "del a[0][1][2]", // // indices: 0 1 2 // assocers: $a $a[0] // dissocer: $a[0][1] assocers := make([]interface{}, len(indices)-1) container := variable.Get() for i, index := range indices[:len(indices)-1] { assocers[i] = container var err error container, err = vals.Index(container, index) if err != nil { return err } } v := vals.Dissoc(container, indices[len(indices)-1]) if v == nil { return elemErr{len(indices), "value does not support element removal"} } for i := len(assocers) - 1; i >= 0; i-- { v, err = vals.Assoc(assocers[i], indices[i], v) if err != nil { return err } } return variable.Set(v) } type elemErr struct { level int msg string } func (err elemErr) Error() string { return err.msg } // HeadOfElement gets the underlying head variable of an element variable, or // nil if the argument is not an element variable. func HeadOfElement(v Var) Var { if ev, ok := v.(*elem); ok { return ev.variable } return nil } // ElementErrorLevel returns the level of an error returned by MakeElement or // DelElement. Level 0 represents that the error is about the variable itself. // Returns -1 if the argument was not returned from MakeElement or DelElement. func ElementErrorLevel(err error) int { if err, ok := err.(elemErr); ok { return err.level } return -1 } elvish-0.17.0/pkg/eval/vars/element_test.go000066400000000000000000000037041415471104000205740ustar00rootroot00000000000000package vars import ( "testing" "src.elv.sh/pkg/eval/vals" ) var elementTests = []struct { name string oldContainer interface{} indices []interface{} elemValue interface{} newContainer interface{} }{ { "single level", vals.MakeMap("k1", "v1", "k2", "v2"), []interface{}{"k1"}, "new v1", vals.MakeMap("k1", "new v1", "k2", "v2"), }, { "multi level", vals.MakeMap( "k1", vals.MakeMap("k1a", "v1a", "k1b", "v1b"), "k2", "v2"), []interface{}{"k1", "k1a"}, "new v1a", vals.MakeMap( "k1", vals.MakeMap("k1a", "new v1a", "k1b", "v1b"), "k2", "v2"), }, } func TestElement(t *testing.T) { for _, test := range elementTests { t.Run(test.name, func(t *testing.T) { m := test.oldContainer elemVar, err := MakeElement(FromPtr(&m), test.indices) if err != nil { t.Errorf("MakeElement -> error %v, want nil", err) } elemVar.Set(test.elemValue) if !vals.Equal(m, test.newContainer) { t.Errorf("Value after Set is %v, want %v", m, test.newContainer) } if elemVar.Get() != test.elemValue { t.Errorf("elemVar.Get() -> %v, want %v", elemVar.Get(), test.elemValue) } }) } } var delElementTests = []struct { name string oldContainer interface{} indices []interface{} newContainer interface{} }{ { "single level", vals.MakeMap("k1", "v1", "k2", "v2"), []interface{}{"k1"}, vals.MakeMap("k2", "v2"), }, { "multi level", vals.MakeMap( "k1", vals.MakeMap("k1a", "v1a", "k1b", "v1b"), "k2", "v2"), []interface{}{"k1", "k1a"}, vals.MakeMap("k1", vals.MakeMap("k1b", "v1b"), "k2", "v2"), }, } func TestDelElement(t *testing.T) { for _, test := range delElementTests { t.Run(test.name, func(t *testing.T) { m := test.oldContainer DelElement(FromPtr(&m), test.indices) if !vals.Equal(m, test.newContainer) { t.Errorf("After deleting, map is %v, want %v", vals.Repr(m, vals.NoPretty), vals.Repr(test.newContainer, vals.NoPretty)) } }) } } elvish-0.17.0/pkg/eval/vars/env.go000066400000000000000000000010261415471104000166670ustar00rootroot00000000000000package vars import ( "errors" "os" ) var errEnvMustBeString = errors.New("environment variable can only be set string values") type envVariable struct { name string } func (ev envVariable) Set(val interface{}) error { if s, ok := val.(string); ok { os.Setenv(ev.name, s) return nil } return errEnvMustBeString } func (ev envVariable) Get() interface{} { return os.Getenv(ev.name) } // FromEnv returns a Var corresponding to the named environment variable. func FromEnv(name string) Var { return envVariable{name} } elvish-0.17.0/pkg/eval/vars/env_list.go000066400000000000000000000042451415471104000177300ustar00rootroot00000000000000package vars // Note: This doesn't have an associated env_list_tests.go because most of its functionality is // tested by TestSetEnv_PATH and related tests. import ( "errors" "os" "strings" "sync" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/persistent/vector" ) var ( pathListSeparator = string(os.PathListSeparator) forbiddenInPath = pathListSeparator + "\x00" ) // Errors var ( ErrPathMustBeString = errors.New("path must be string") ErrPathContainsForbiddenChar = errors.New("path cannot contain NUL byte, colon on UNIX or semicolon on Windows") ) // NewEnvListVar returns a variable whose value is a list synchronized with an // environment variable with the elements joined by os.PathListSeparator. // // Elements in the value of the variable must be strings, and cannot contain // os.PathListSeparator or \0; attempting to put any in its elements will result in // an error. func NewEnvListVar(name string) Var { return &envListVar{envName: name} } type envListVar struct { sync.RWMutex envName string cacheFor string cacheValue interface{} } // Get returns a Value for an EnvPathList. func (envli *envListVar) Get() interface{} { envli.Lock() defer envli.Unlock() value := os.Getenv(envli.envName) if value == envli.cacheFor { return envli.cacheValue } envli.cacheFor = value v := vector.Empty for _, path := range strings.Split(value, pathListSeparator) { v = v.Cons(path) } envli.cacheValue = v return envli.cacheValue } // Set sets an EnvPathList. The underlying environment variable is set. func (envli *envListVar) Set(v interface{}) error { var ( paths []string errElement error ) errIterate := vals.Iterate(v, func(v interface{}) bool { s, ok := v.(string) if !ok { errElement = ErrPathMustBeString return false } path := s if strings.ContainsAny(path, forbiddenInPath) { errElement = ErrPathContainsForbiddenChar return false } paths = append(paths, s) return true }) if errElement != nil || errIterate != nil { return diag.Errors(errElement, errIterate) } envli.Lock() defer envli.Unlock() os.Setenv(envli.envName, strings.Join(paths, pathListSeparator)) return nil } elvish-0.17.0/pkg/eval/vars/env_test.go000066400000000000000000000007401415471104000177300ustar00rootroot00000000000000package vars import ( "os" "testing" ) func TestFromEnv(t *testing.T) { name := "elvish_test" v := FromEnv(name) os.Setenv(name, "foo") if v.Get() != "foo" { t.Errorf("envVariable.Get doesn't return env value") } err := v.Set("bar") if err != nil || os.Getenv(name) != "bar" { t.Errorf("envVariable.Set doesn't alter env value") } err = v.Set(true) if err != errEnvMustBeString { t.Errorf("envVariable.Set to a non-string value didn't return an error") } } elvish-0.17.0/pkg/eval/vars/ptr.go000066400000000000000000000027511415471104000167120ustar00rootroot00000000000000package vars import ( "reflect" "sync" "src.elv.sh/pkg/eval/vals" ) type PtrVar struct { ptr interface{} mutex *sync.RWMutex } // FromPtrWithMutex creates a variable from a pointer. The variable is kept in // sync with the value the pointer points to, converting with vals.ScanToGo and // vals.FromGo when Get and Set. Its access is guarded by the supplied mutex. func FromPtrWithMutex(p interface{}, m *sync.RWMutex) PtrVar { return PtrVar{p, m} } // FromPtr creates a variable from a pointer. The variable is kept in sync with // the value the pointer points to, converting with vals.ScanToGo and // vals.FromGo when Get and Set. Its access is guarded by a new mutex. func FromPtr(p interface{}) PtrVar { return FromPtrWithMutex(p, new(sync.RWMutex)) } // FromInit creates a variable with an initial value. The variable created // can be assigned values of any type. func FromInit(v interface{}) Var { return FromPtr(&v) } // Get returns the value pointed by the pointer, after conversion using FromGo. func (v PtrVar) Get() interface{} { return vals.FromGo(v.GetRaw()) } // GetRaw returns the value pointed by the pointer without any conversion. func (v PtrVar) GetRaw() interface{} { v.mutex.RLock() defer v.mutex.RUnlock() return reflect.Indirect(reflect.ValueOf(v.ptr)).Interface() } // Set sets the value pointed by the pointer, after conversion using ScanToGo. func (v PtrVar) Set(val interface{}) error { v.mutex.Lock() defer v.mutex.Unlock() return vals.ScanToGo(val, v.ptr) } elvish-0.17.0/pkg/eval/vars/ptr_test.go000066400000000000000000000014261415471104000177470ustar00rootroot00000000000000package vars import "testing" func TestFromPtr(t *testing.T) { i := 10 variable := FromPtr(&i) if g := variable.Get(); g != 10 { t.Errorf(`Get -> %v, want 10`, g) } err := variable.Set("20") if err != nil { t.Errorf(`Setting ptrVariable with "20" returns error %v`, err) } if i != 20 { t.Errorf(`Setting ptrVariable didn't change underlying value`) } err = variable.Set("x") if err == nil { t.Errorf("Setting ptrVariable with incompatible value returns no error") } } func TestFromInit(t *testing.T) { v := FromInit(true) if val := v.Get(); val != true { t.Errorf("Get returned %v, want true", val) } if err := v.Set("233"); err != nil { t.Errorf("Set errors: %v", err) } if val := v.Get(); val != "233" { t.Errorf(`Get returns %v, want "233"`, val) } } elvish-0.17.0/pkg/eval/vars/read_only.go000066400000000000000000000011151415471104000200520ustar00rootroot00000000000000package vars import ( "src.elv.sh/pkg/eval/errs" ) type readOnly struct { value interface{} } // NewReadOnly creates a variable that is read-only and always returns an error // on Set. func NewReadOnly(v interface{}) Var { return readOnly{v} } func (rv readOnly) Set(val interface{}) error { return errs.SetReadOnlyVar{} } func (rv readOnly) Get() interface{} { return rv.value } // IsReadOnly returns whether v is a read-only variable. func IsReadOnly(v Var) bool { switch v.(type) { case readOnly: return true case roCallback: return true default: return false } } elvish-0.17.0/pkg/eval/vars/read_only_test.go000066400000000000000000000011621415471104000211130ustar00rootroot00000000000000package vars import ( "testing" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/tt" ) var Args = tt.Args func TestNewReadOnly(t *testing.T) { v := NewReadOnly("haha") if v.Get() != "haha" { t.Errorf("Get doesn't return initial value") } err := v.Set("lala") if _, ok := err.(errs.SetReadOnlyVar); !ok { t.Errorf("Set a readonly var doesn't error as expected: %#v", err) } } func TestIsReadOnly(t *testing.T) { tt.Test(t, tt.Fn("IsReadOnly", IsReadOnly), tt.Table{ Args(NewReadOnly("foo")).Rets(true), Args(FromGet(func() interface{} { return "foo" })).Rets(true), Args(FromInit("foo")).Rets(false), }) } elvish-0.17.0/pkg/eval/vars/vars.go000066400000000000000000000003001415471104000170440ustar00rootroot00000000000000// Package vars contains basic types for manipulating Elvish variables. package vars // Var represents an Elvish variable. type Var interface { Set(v interface{}) error Get() interface{} } elvish-0.17.0/pkg/fsutil/000077500000000000000000000000001415471104000151555ustar00rootroot00000000000000elvish-0.17.0/pkg/fsutil/claim.go000066400000000000000000000042361415471104000165760ustar00rootroot00000000000000package fsutil import ( "errors" "os" "path/filepath" "strconv" "strings" ) // ErrClaimFileBadPattern is thrown when the pattern argument passed to // ClaimFile does not contain exactly one asterisk. var ErrClaimFileBadPattern = errors.New("ClaimFile: pattern must contain exactly one asterisk") // ClaimFile takes a directory and a pattern string containing exactly one // asterisk (e.g. "a*.log"). It opens a file in that directory, with a filename // matching the template, with "*" replaced by a number. That number is one plus // the largest of all existing files matching the template. If no such file // exists, "*" is replaced by 1. The file is opened for read and write, with // permission 0666 (before umask). // // For example, if the directory /tmp/elvish contains a1.log, a2.log and a9.log, // calling ClaimFile("/tmp/elvish", "a*.log") will open a10.log. If the // directory has no files matching the pattern, this same call will open a1.log. // // This function is useful for automatically determining unique names for log // files. Unique filenames can also be derived by embedding the PID, but using // this function preserves the chronical order of the files. // // This function is concurrency-safe: it always opens a new, unclaimed file and // is not subject to race condition. func ClaimFile(dir, pattern string) (*os.File, error) { if strings.Count(pattern, "*") != 1 { return nil, ErrClaimFileBadPattern } asterisk := strings.IndexByte(pattern, '*') prefix, suffix := pattern[:asterisk], pattern[asterisk+1:] files, err := os.ReadDir(dir) if err != nil { return nil, err } max := 0 for _, file := range files { name := file.Name() if len(name) > len(prefix)+len(suffix) && strings.HasPrefix(name, prefix) && strings.HasSuffix(name, suffix) { core := name[len(prefix) : len(name)-len(suffix)] if coreNum, err := strconv.Atoi(core); err == nil { if max < coreNum { max = coreNum } } } } for i := max + 1; ; i++ { name := filepath.Join(dir, prefix+strconv.Itoa(i)+suffix) f, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666) if err == nil { return f, nil } if !os.IsExist(err) { return nil, err } } } elvish-0.17.0/pkg/fsutil/claim_test.go000066400000000000000000000024151415471104000176320ustar00rootroot00000000000000package fsutil import ( "fmt" "path/filepath" "sort" "testing" . "src.elv.sh/pkg/testutil" ) var claimFileTests = []struct { dir string pattern string wantFileName string }{ {".", "a*.log", "a9.log"}, {".", "*.txt", "1.txt"}, {"d", "*.txt", filepath.Join("d", "1.txt")}, } func TestClaimFile(t *testing.T) { InTempDir(t) ApplyDir(Dir{ "a0.log": "", "a1.log": "", "a8.log": "", "d": Dir{}}) for _, test := range claimFileTests { name := claimFileAndGetName(test.dir, test.pattern) if name != test.wantFileName { t.Errorf("ClaimFile claims %s, want %s", name, test.wantFileName) } } } func TestClaimFile_Concurrent(t *testing.T) { InTempDir(t) n := 9 ch := make(chan string, n) for i := 0; i < n; i++ { go func() { ch <- claimFileAndGetName(".", "a*.log") }() } names := make([]string, n) for i := 0; i < n; i++ { names[i] = <-ch } sort.Strings(names) for i, name := range names { wantName := fmt.Sprintf("a%d.log", i+1) if name != wantName { t.Errorf("got names[%d] = %q, want %q", i, name, wantName) } } } func claimFileAndGetName(dir, pattern string) string { f, err := ClaimFile(dir, pattern) if err != nil { panic(fmt.Sprintf("ClaimFile errors: %v", err)) } defer f.Close() return f.Name() } elvish-0.17.0/pkg/fsutil/fsutil.go000066400000000000000000000001001415471104000170010ustar00rootroot00000000000000// Package fsutil provides filesystem utilities. package fsutil elvish-0.17.0/pkg/fsutil/gethome.go000066400000000000000000000015451415471104000171410ustar00rootroot00000000000000package fsutil import ( "fmt" "os" "os/user" "strings" "src.elv.sh/pkg/env" ) // CurrentUser allows for unit test error injection. var CurrentUser func() (*user.User, error) = user.Current // GetHome finds the home directory of a specified user. When given an empty // string, it finds the home directory of the current user. func GetHome(uname string) (string, error) { if uname == "" { // Use $HOME as override if we are looking for the home of the current // variable. home := os.Getenv(env.HOME) if home != "" { return strings.TrimRight(home, pathSep), nil } } // Look up the user. var u *user.User var err error if uname == "" { u, err = CurrentUser() } else { u, err = user.Lookup(uname) } if err != nil { return "", fmt.Errorf("can't resolve ~%s: %s", uname, err.Error()) } return strings.TrimRight(u.HomeDir, "/"), nil } elvish-0.17.0/pkg/fsutil/getwd.go000066400000000000000000000014141415471104000166160ustar00rootroot00000000000000package fsutil import ( "os" "path/filepath" "strings" ) var pathSep = string(filepath.Separator) // Getwd returns path of the working directory in a format suitable as the // prompt. func Getwd() string { pwd, err := os.Getwd() if err != nil { return "?" } return TildeAbbr(pwd) } // TildeAbbr abbreviates the user's home directory to ~. func TildeAbbr(path string) string { home, err := GetHome("") if home == "" || home == "/" { // If home is "" or "/", do not abbreviate because (1) it is likely a // problem with the environment and (2) it will make the path actually // longer. return path } if err == nil { if path == home { return "~" } else if strings.HasPrefix(path, home+pathSep) { return "~" + path[len(home):] } } return path } elvish-0.17.0/pkg/fsutil/getwd_test.go000066400000000000000000000027721415471104000176650ustar00rootroot00000000000000package fsutil import ( "os" "path" "path/filepath" "runtime" "testing" "src.elv.sh/pkg/env" "src.elv.sh/pkg/testutil" ) func TestGetwd(t *testing.T) { tmpdir := testutil.InTempDir(t) testutil.Must(os.Mkdir("a", 0700)) var tests = []struct { name string home string chdir string wantWd string }{ {"wd outside HOME not abbreviated", "/does/not/exist", tmpdir, tmpdir}, {"wd at HOME abbreviated", tmpdir, tmpdir, "~"}, {"wd inside HOME abbreviated", tmpdir, tmpdir + "/a", filepath.Join("~", "a")}, {"wd not abbreviated when HOME is slash", "/", tmpdir, tmpdir}, } testutil.SaveEnv(t, env.HOME) for _, test := range tests { t.Run(test.name, func(t *testing.T) { os.Setenv(env.HOME, test.home) testutil.MustChdir(test.chdir) if gotWd := Getwd(); gotWd != test.wantWd { t.Errorf("Getwd() -> %v, want %v", gotWd, test.wantWd) } }) } // Remove the working directory, and test that Getwd returns "?". // // This test is now only enabled on Linux, where os.Getwd returns an error // when the working directory has been removed. Other operating systems may // return the old path even if it is now invalid. // // TODO(xiaq): Check all the supported operating systems and see which ones // have the same behavior as Linux. So far only macOS has been checked. if runtime.GOOS == "linux" { wd := path.Join(tmpdir, "a") testutil.MustChdir(wd) testutil.Must(os.Remove(wd)) if gotwd := Getwd(); gotwd != "?" { t.Errorf("Getwd() -> %v, want ?", gotwd) } } } elvish-0.17.0/pkg/fsutil/search.go000066400000000000000000000023721415471104000167550ustar00rootroot00000000000000package fsutil import ( "os" "path/filepath" "strings" "src.elv.sh/pkg/env" ) // DontSearch determines whether the path to an external command should be // taken literally and not searched. func DontSearch(exe string) bool { return exe == ".." || strings.ContainsRune(exe, filepath.Separator) || strings.ContainsRune(exe, '/') } // IsExecutable determines whether path refers to an executable file. func IsExecutable(path string) bool { fi, err := os.Stat(path) if err != nil { return false } fm := fi.Mode() return !fm.IsDir() && (fm&0111 != 0) } // EachExternal calls f for each name that can resolve to an external command. // // BUG: EachExternal may generate the same command multiple command it it // appears in multiple directories in PATH. // // BUG: EachExternal doesn't work on Windows since it relies on the execution // permission bit, which doesn't exist on Windows. func EachExternal(f func(string)) { for _, dir := range searchPaths() { files, err := os.ReadDir(dir) if err != nil { continue } for _, file := range files { info, err := file.Info() if err == nil && !info.IsDir() && (info.Mode()&0111 != 0) { f(file.Name()) } } } } func searchPaths() []string { return strings.Split(os.Getenv(env.PATH), ":") } elvish-0.17.0/pkg/fsutil/search_unix_test.go000066400000000000000000000017731415471104000210630ustar00rootroot00000000000000//go:build !windows && !plan9 && !js // +build !windows,!plan9,!js package fsutil import ( "reflect" "sort" "testing" "src.elv.sh/pkg/testutil" ) // TODO: When EachExternal is modified to work on Windows either fold this // test into external_cmd_test.go or create an external_cmd_windows_test.go // that performs an equivalent test on Windows. func TestEachExternal(t *testing.T) { binPath := testutil.InTempDir(t) testutil.Setenv(t, "PATH", "/foo:"+binPath+":/bar") testutil.ApplyDir(testutil.Dir{ "dir": testutil.Dir{}, "file": "", "cmdx": "#!/bin/sh", "cmd1": testutil.File{Perm: 0755, Content: "#!/bin/sh"}, "cmd2": testutil.File{Perm: 0755, Content: "#!/bin/sh"}, "cmd3": testutil.File{Perm: 0755, Content: ""}, }) wantCmds := []string{"cmd1", "cmd2", "cmd3"} gotCmds := []string{} EachExternal(func(cmd string) { gotCmds = append(gotCmds, cmd) }) sort.Strings(gotCmds) if !reflect.DeepEqual(wantCmds, gotCmds) { t.Errorf("EachExternal want %q got %q", wantCmds, gotCmds) } } elvish-0.17.0/pkg/getopt/000077500000000000000000000000001415471104000151515ustar00rootroot00000000000000elvish-0.17.0/pkg/getopt/getopt.go000066400000000000000000000210151415471104000170010ustar00rootroot00000000000000// Package getopt implements a command-line argument parser. // // It tries to cover all common styles of option syntaxes, and provides context // information when given a partial input. It is mainly useful for writing // completion engines and wrapper programs. // // If you are looking for an option parser for your go program, consider using // the flag package in the standard library instead. package getopt //go:generate stringer -type=Config,HasArg,ContextType -output=string.go import "strings" // Getopt specifies the syntax of command-line arguments. type Getopt struct { Options []*Option Config Config } // Config configurates the parsing behavior. type Config uint const ( // DoubleDashTerminatesOptions indicates that all elements after an argument // "--" are treated as arguments. DoubleDashTerminatesOptions Config = 1 << iota // FirstArgTerminatesOptions indicates that all elements after the first // argument are treated as arguments. FirstArgTerminatesOptions // LongOnly indicates that long options may be started by either one or two // dashes, and short options are not allowed. Should replicate the behavior // of getopt_long_only and the // flag package of the Go standard library. LongOnly // GNUGetoptLong is a configuration that should replicate the behavior of // GNU getopt_long. GNUGetoptLong = DoubleDashTerminatesOptions // POSIXGetopt is a configuration that should replicate the behavior of // POSIX getopt. POSIXGetopt = DoubleDashTerminatesOptions | FirstArgTerminatesOptions ) // HasAll tests whether a configuration has all specified flags set. func (conf Config) HasAll(flags Config) bool { return (conf & flags) == flags } // Option is a command-line option. type Option struct { // Short option. Set to 0 for long-only. Short rune // Long option. Set to "" for short-only. Long string // Whether the option takes an argument, and whether it is required. HasArg HasArg } // HasArg indicates whether an option takes an argument, and whether it is // required. type HasArg uint const ( // NoArgument indicates that an option takes no argument. NoArgument HasArg = iota // RequiredArgument indicates that an option must take an argument. The // argument can come either directly after a short option (-oarg), after a // long option followed by an equal sign (--long=arg), or as a subsequent // argument after the option (-o arg, --long arg). RequiredArgument // OptionalArgument indicates that an option takes an optional argument. // The argument can come either directly after a short option (-oarg) or // after a long option followed by an equal sign (--long=arg). OptionalArgument ) // ParsedOption represents a parsed option. type ParsedOption struct { Option *Option Long bool Argument string } // Context indicates what may come after the supplied argument list. type Context struct { // The nature of the context. Type ContextType // Current option, with a likely incomplete Argument. Non-nil when Type is // OptionArgument. Option *ParsedOption // Current partial long option name or argument. Non-empty when Type is // LongOption or Argument. Text string } // ContextType encodes what may be appended to the last element of the argument // list. type ContextType uint const ( // NewOptionOrArgument indicates that the last element may be either a new // option or a new argument. Returned when it is an empty string. NewOptionOrArgument ContextType = iota // NewOption indicates that the last element must be new option, short or // long. Returned when it is "-". NewOption // NewLongOption indicates that the last element must be a new long option. // Returned when it is "--". NewLongOption // LongOption indicates that the last element is a long option, but not its // argument. The partial name of the long option is stored in Context.Text. LongOption // ChainShortOption indicates that a new short option may be chained. // Returned when the last element consists of a chain of options that take // no arguments. ChainShortOption // OptionArgument indicates that the last element list must be an argument // to an option. The option in question is stored in Context.Option. OptionArgument // Argument indicates that the last element is an argument. The partial // argument is stored in Context.Text. Argument ) func (g *Getopt) findShort(r rune) *Option { for _, opt := range g.Options { if r == opt.Short { return opt } } return nil } // parseShort parse short options, without the leading dash. It returns the // parsed options and whether an argument is still to be seen. func (g *Getopt) parseShort(s string) ([]*ParsedOption, bool) { var opts []*ParsedOption var needArg bool for i, r := range s { opt := g.findShort(r) if opt != nil { if opt.HasArg == NoArgument { opts = append(opts, &ParsedOption{opt, false, ""}) continue } else { parsed := &ParsedOption{opt, false, s[i+len(string(r)):]} opts = append(opts, parsed) needArg = parsed.Argument == "" && opt.HasArg == RequiredArgument break } } // Unknown option, treat as taking an optional argument parsed := &ParsedOption{ &Option{r, "", OptionalArgument}, false, s[i+len(string(r)):]} opts = append(opts, parsed) break } return opts, needArg } // parseLong parse a long option, without the leading dashes. It returns the // parsed option and whether an argument is still to be seen. func (g *Getopt) parseLong(s string) (*ParsedOption, bool) { eq := strings.IndexRune(s, '=') for _, opt := range g.Options { if s == opt.Long { return &ParsedOption{opt, true, ""}, opt.HasArg == RequiredArgument } else if eq != -1 && s[:eq] == opt.Long { return &ParsedOption{opt, true, s[eq+1:]}, false } } // Unknown option, treat as taking an optional argument if eq == -1 { return &ParsedOption{&Option{0, s, OptionalArgument}, true, ""}, false } return &ParsedOption{&Option{0, s[:eq], OptionalArgument}, true, s[eq+1:]}, false } // Parse parses an argument list. func (g *Getopt) Parse(elems []string) ([]*ParsedOption, []string, *Context) { var ( opts []*ParsedOption args []string // Non-nil only when the last element was an option with required // argument, but the argument has not been seen. opt *ParsedOption // True if an option terminator has been seen. The criteria of option // terminators is determined by the configuration. noopt bool ) var elem string hasPrefix := func(p string) bool { return strings.HasPrefix(elem, p) } for _, elem = range elems[:len(elems)-1] { if opt != nil { opt.Argument = elem opts = append(opts, opt) opt = nil } else if noopt { args = append(args, elem) } else if g.Config.HasAll(DoubleDashTerminatesOptions) && elem == "--" { noopt = true } else if hasPrefix("--") { newopt, needArg := g.parseLong(elem[2:]) if needArg { opt = newopt } else { opts = append(opts, newopt) } } else if hasPrefix("-") { if g.Config.HasAll(LongOnly) { newopt, needArg := g.parseLong(elem[1:]) if needArg { opt = newopt } else { opts = append(opts, newopt) } } else { newopts, needArg := g.parseShort(elem[1:]) if needArg { opts = append(opts, newopts[:len(newopts)-1]...) opt = newopts[len(newopts)-1] } else { opts = append(opts, newopts...) } } } else { args = append(args, elem) if g.Config.HasAll(FirstArgTerminatesOptions) { noopt = true } } } elem = elems[len(elems)-1] ctx := &Context{} if opt != nil { opt.Argument = elem ctx.Type, ctx.Option = OptionArgument, opt } else if noopt { ctx.Type, ctx.Text = Argument, elem } else if elem == "" { ctx.Type = NewOptionOrArgument } else if elem == "-" { ctx.Type = NewOption } else if elem == "--" { ctx.Type = NewLongOption } else if hasPrefix("--") { if !strings.ContainsRune(elem, '=') { ctx.Type, ctx.Text = LongOption, elem[2:] } else { newopt, _ := g.parseLong(elem[2:]) ctx.Type, ctx.Option = OptionArgument, newopt } } else if hasPrefix("-") { if g.Config.HasAll(LongOnly) { if !strings.ContainsRune(elem, '=') { ctx.Type, ctx.Text = LongOption, elem[1:] } else { newopt, _ := g.parseLong(elem[1:]) ctx.Type, ctx.Option = OptionArgument, newopt } } else { newopts, _ := g.parseShort(elem[1:]) if newopts[len(newopts)-1].Option.HasArg == NoArgument { opts = append(opts, newopts...) ctx.Type = ChainShortOption } else { opts = append(opts, newopts[:len(newopts)-1]...) ctx.Type, ctx.Option = OptionArgument, newopts[len(newopts)-1] } } } else { ctx.Type, ctx.Text = Argument, elem } return opts, args, ctx } elvish-0.17.0/pkg/getopt/getopt_test.go000066400000000000000000000124771415471104000200540ustar00rootroot00000000000000package getopt import ( "reflect" "testing" ) var options = []*Option{ {'a', "all", NoArgument}, {'o', "option", RequiredArgument}, {'n', "number", OptionalArgument}, } var cases = []struct { config Config elems []string wantOpts []*ParsedOption wantArgs []string wantCtx *Context }{ // NoArgument, short option. {0, []string{"-a", ""}, []*ParsedOption{{options[0], false, ""}}, nil, &Context{Type: NewOptionOrArgument}}, // NoArgument, long option. {0, []string{"--all", ""}, []*ParsedOption{{options[0], true, ""}}, nil, &Context{Type: NewOptionOrArgument}}, // RequiredArgument, argument following the option directly {0, []string{"-oname=elvish", ""}, []*ParsedOption{{options[1], false, "name=elvish"}}, nil, &Context{Type: NewOptionOrArgument}}, // RequiredArgument, argument in next element {0, []string{"-o", "name=elvish", ""}, []*ParsedOption{{options[1], false, "name=elvish"}}, nil, &Context{Type: NewOptionOrArgument}}, // RequiredArgument, long option, argument following the option directly {0, []string{"--option=name=elvish", ""}, []*ParsedOption{{options[1], true, "name=elvish"}}, nil, &Context{Type: NewOptionOrArgument}}, // RequiredArgument, long option, argument in next element {0, []string{"--option", "name=elvish", ""}, []*ParsedOption{{options[1], true, "name=elvish"}}, nil, &Context{Type: NewOptionOrArgument}}, // OptionalArgument, with argument {0, []string{"-n1", ""}, []*ParsedOption{{options[2], false, "1"}}, nil, &Context{Type: NewOptionOrArgument}}, // OptionalArgument, without argument {0, []string{"-n", ""}, []*ParsedOption{{options[2], false, ""}}, nil, &Context{Type: NewOptionOrArgument}}, // DoubleDashTerminatesOptions {DoubleDashTerminatesOptions, []string{"-a", "--", "-o", ""}, []*ParsedOption{{options[0], false, ""}}, []string{"-o"}, &Context{Type: Argument}}, // FirstArgTerminatesOptions {FirstArgTerminatesOptions, []string{"-a", "x", "-o", ""}, []*ParsedOption{{options[0], false, ""}}, []string{"x", "-o"}, &Context{Type: Argument}}, // LongOnly {LongOnly, []string{"-all", ""}, []*ParsedOption{{options[0], true, ""}}, nil, &Context{Type: NewOptionOrArgument}}, // NewOption {0, []string{"-"}, nil, nil, &Context{Type: NewOption}}, // NewLongOption {0, []string{"--"}, nil, nil, &Context{Type: NewLongOption}}, // LongOption {0, []string{"--all"}, nil, nil, &Context{Type: LongOption, Text: "all"}}, // LongOption, LongOnly {LongOnly, []string{"-all"}, nil, nil, &Context{Type: LongOption, Text: "all"}}, // ChainShortOption {0, []string{"-a"}, []*ParsedOption{{options[0], false, ""}}, nil, &Context{Type: ChainShortOption}}, // OptionArgument, short option, same element {0, []string{"-o"}, nil, nil, &Context{Type: OptionArgument, Option: &ParsedOption{options[1], false, ""}}}, // OptionArgument, short option, separate element {0, []string{"-o", ""}, nil, nil, &Context{Type: OptionArgument, Option: &ParsedOption{options[1], false, ""}}}, // OptionArgument, long option, same element {0, []string{"--option="}, nil, nil, &Context{Type: OptionArgument, Option: &ParsedOption{options[1], true, ""}}}, // OptionArgument, long option, separate element {0, []string{"--option", ""}, nil, nil, &Context{Type: OptionArgument, Option: &ParsedOption{options[1], true, ""}}}, // OptionArgument, long only, same element {LongOnly, []string{"-option="}, nil, nil, &Context{Type: OptionArgument, Option: &ParsedOption{options[1], true, ""}}}, // OptionArgument, long only, separate element {LongOnly, []string{"-option", ""}, nil, nil, &Context{Type: OptionArgument, Option: &ParsedOption{options[1], true, ""}}}, // Argument {0, []string{"x"}, nil, nil, &Context{Type: Argument, Text: "x"}}, // Unknown short option, same element {0, []string{"-x"}, nil, nil, &Context{ Type: OptionArgument, Option: &ParsedOption{ &Option{'x', "", OptionalArgument}, false, ""}}}, // Unknown short option, separate element {0, []string{"-x", ""}, []*ParsedOption{{ &Option{'x', "", OptionalArgument}, false, ""}}, nil, &Context{Type: NewOptionOrArgument}}, // Unknown long option {0, []string{"--unknown", ""}, []*ParsedOption{{ &Option{0, "unknown", OptionalArgument}, true, ""}}, nil, &Context{Type: NewOptionOrArgument}}, // Unknown long option, with argument {0, []string{"--unknown=value", ""}, []*ParsedOption{{ &Option{0, "unknown", OptionalArgument}, true, "value"}}, nil, &Context{Type: NewOptionOrArgument}}, // Unknown long option, LongOnly {LongOnly, []string{"-unknown", ""}, []*ParsedOption{{ &Option{0, "unknown", OptionalArgument}, true, ""}}, nil, &Context{Type: NewOptionOrArgument}}, // Unknown long option, with argument {LongOnly, []string{"-unknown=value", ""}, []*ParsedOption{{ &Option{0, "unknown", OptionalArgument}, true, "value"}}, nil, &Context{Type: NewOptionOrArgument}}, } func TestGetopt(t *testing.T) { for _, tc := range cases { g := &Getopt{options, tc.config} opts, args, ctx := g.Parse(tc.elems) shouldEqual := func(name string, got, want interface{}) { if !reflect.DeepEqual(got, want) { t.Errorf("Parse(%#v) (config = %v)\ngot %s = %v, want %v", tc.elems, tc.config, name, got, want) } } shouldEqual("opts", opts, tc.wantOpts) shouldEqual("args", args, tc.wantArgs) shouldEqual("ctx", ctx, tc.wantCtx) } } elvish-0.17.0/pkg/getopt/string.go000066400000000000000000000041541415471104000170120ustar00rootroot00000000000000// Code generated by "stringer -type=Config,HasArg,ContextType -output=string.go"; DO NOT EDIT. package getopt import "strconv" func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} _ = x[DoubleDashTerminatesOptions-1] _ = x[FirstArgTerminatesOptions-2] _ = x[LongOnly-4] } const ( _Config_name_0 = "DoubleDashTerminatesOptionsFirstArgTerminatesOptions" _Config_name_1 = "LongOnly" ) var ( _Config_index_0 = [...]uint8{0, 27, 52} ) func (i Config) String() string { switch { case 1 <= i && i <= 2: i -= 1 return _Config_name_0[_Config_index_0[i]:_Config_index_0[i+1]] case i == 4: return _Config_name_1 default: return "Config(" + strconv.FormatInt(int64(i), 10) + ")" } } func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} _ = x[NoArgument-0] _ = x[RequiredArgument-1] _ = x[OptionalArgument-2] } const _HasArg_name = "NoArgumentRequiredArgumentOptionalArgument" var _HasArg_index = [...]uint8{0, 10, 26, 42} func (i HasArg) String() string { if i >= HasArg(len(_HasArg_index)-1) { return "HasArg(" + strconv.FormatInt(int64(i), 10) + ")" } return _HasArg_name[_HasArg_index[i]:_HasArg_index[i+1]] } func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} _ = x[NewOptionOrArgument-0] _ = x[NewOption-1] _ = x[NewLongOption-2] _ = x[LongOption-3] _ = x[ChainShortOption-4] _ = x[OptionArgument-5] _ = x[Argument-6] } const _ContextType_name = "NewOptionOrArgumentNewOptionNewLongOptionLongOptionChainShortOptionOptionArgumentArgument" var _ContextType_index = [...]uint8{0, 19, 28, 41, 51, 67, 81, 89} func (i ContextType) String() string { if i >= ContextType(len(_ContextType_index)-1) { return "ContextType(" + strconv.FormatInt(int64(i), 10) + ")" } return _ContextType_name[_ContextType_index[i]:_ContextType_index[i+1]] } elvish-0.17.0/pkg/glob/000077500000000000000000000000001415471104000145725ustar00rootroot00000000000000elvish-0.17.0/pkg/glob/glob.go000066400000000000000000000174271415471104000160570ustar00rootroot00000000000000// Package glob implements globbing for elvish. package glob import ( "os" "runtime" "unicode/utf8" ) // TODO: On Windows, preserve the original path separator (/ or \) specified in // the glob pattern. // PathInfo keeps a path resulting from glob expansion and its FileInfo. The // FileInfo is useful for efficiently determining if a given pathname satisfies // a particular constraint without doing an extra stat. type PathInfo struct { // The generated path, consistent with the original glob pattern. It cannot // be replaced by Info.Name(), which is just the final path component. Path string Info os.FileInfo } // Glob returns a list of file names satisfying the given pattern. func Glob(p string, cb func(PathInfo) bool) bool { return Parse(p).Glob(cb) } // Glob returns a list of file names satisfying the Pattern. func (p Pattern) Glob(cb func(PathInfo) bool) bool { segs := p.Segments dir := "" // TODO(xiaq): This is a hack solely for supporting globs that start with // ~ (tilde) in the eval package. if p.DirOverride != "" { dir = p.DirOverride } if len(segs) > 0 && IsSlash(segs[0]) { segs = segs[1:] dir += "/" } else if runtime.GOOS == "windows" && len(segs) > 1 && IsLiteral(segs[0]) && IsSlash(segs[1]) { // TODO: Handle Windows UNC paths. elem := segs[0].(Literal).Data if isDrive(elem) { segs = segs[2:] dir = elem + "/" } } return glob(segs, dir, cb) } // isLetter returns true if the byte is an ASCII letter. func isLetter(chr byte) bool { return ('a' <= chr && chr <= 'z') || ('A' <= chr && chr <= 'Z') } // isDrive returns true if the string looks like a Windows drive letter path prefix. func isDrive(s string) bool { return len(s) == 2 && s[1] == ':' && isLetter(s[0]) } // glob finds all filenames matching the given Segments in the given dir, and // calls the callback on all of them. If the callback returns false, globbing is // interrupted, and glob returns false. Otherwise it returns true. func glob(segs []Segment, dir string, cb func(PathInfo) bool) bool { // Consume non-wildcard path elements simply by following the path. This may // seem like an optimization, but is actually required for "." and ".." to // be used as path elements, as they do not appear in the result of ReadDir. for len(segs) > 1 && IsLiteral(segs[0]) && IsSlash(segs[1]) { elem := segs[0].(Literal).Data segs = segs[2:] dir += elem + "/" if info, err := os.Stat(dir); err != nil || !info.IsDir() { return true } } if len(segs) == 0 { if info, err := os.Stat(dir); err == nil { return cb(PathInfo{dir, info}) } return true } else if len(segs) == 1 && IsLiteral(segs[0]) { path := dir + segs[0].(Literal).Data if info, err := os.Stat(path); err == nil { return cb(PathInfo{path, info}) } return true } infos, err := readDir(dir) if err != nil { // TODO(xiaq): Silently drop the error. return true } i := -1 // nexti moves i to the next index in segs that is either / or ** (in other // words, something that matches /). nexti := func() { for i++; i < len(segs); i++ { if IsSlash(segs[i]) || IsWild1(segs[i], StarStar) { break } } } nexti() // Enumerate the position of the first slash. In the presence of multiple // **'s in the pattern, the first slash may be in any of those. // // For instance, in x**y**z, the first slash may be in the first ** or the // second: // 1) If it is in the first, then pattern is equivalent to x*/**y**z. We // match directories with x* and recurse in each subdirectory with the // pattern **y**z. // 2) If it is the in the second, we know that since the first ** can no // longer contain any slashes, we treat it as * (this is done in // matchElement). The pattern is now equivalent to x*y*/**z. We match // directories with x*y* and recurse in each subdirectory with the // pattern **z. // // The rules are: // 1) For each **, we treat it as */** and all previous ones as *. We match // subdirectories with the part before /, and recurse in subdirectories // with the pattern after /. // 2) If a literal / is encountered, we return after recursing in the // subdirectories. for i < len(segs) { slash := IsSlash(segs[i]) var first, rest []Segment if slash { // segs = x/y. Match dir with x, recurse on y. first, rest = segs[:i], segs[i+1:] } else { // segs = x**y. Match dir with x*, recurse on **y. first, rest = segs[:i+1], segs[i:] } for _, info := range infos { name := info.Name() if matchElement(first, name) && info.IsDir() { if !glob(rest, dir+name+"/", cb) { return false } } } if slash { // First slash cannot appear later than a slash in the pattern. return true } nexti() } // If we reach here, it is possible to have no slashes at all. Simply match // the entire pattern with all files. for _, info := range infos { name := info.Name() if matchElement(segs, name) { dirname := dir + name info, err := os.Stat(dirname) if err != nil { return true } if !cb(PathInfo{dirname, info}) { return false } } } return true } // readDir is just like os.ReadDir except that it treats an argument of "" as ".". func readDir(dir string) ([]os.DirEntry, error) { if dir == "" { dir = "." } return os.ReadDir(dir) } // matchElement matches a path element against segments, which may not contain // any Slash segments. It treats StarStar segments as they are Star segments. func matchElement(segs []Segment, name string) bool { if len(segs) == 0 { return name == "" } // If the name start with "." and the first segment is a Wild, only match // when MatchHidden is true. if len(name) > 0 && name[0] == '.' && IsWild(segs[0]) && !segs[0].(Wild).MatchHidden { return false } segs: for len(segs) > 0 { // Find a chunk. A chunk is an optional Star followed by a run of // fixed-length segments (Literal and Question). var i int for i = 1; i < len(segs); i++ { if IsWild2(segs[i], Star, StarStar) { break } } chunk := segs[:i] startsWithStar := IsWild2(chunk[0], Star, StarStar) var startingStar Wild if startsWithStar { startingStar = chunk[0].(Wild) chunk = chunk[1:] } segs = segs[i:] // TODO: Implement a quick path when len(segs) == 0 by matching // backwards. // Match at the current position. If this is the last chunk, we need to // make sure name is exhausted by the matching. ok, rest := matchFixedLength(chunk, name) if ok && (rest == "" || len(segs) > 0) { name = rest continue } if startsWithStar { // TODO: Optimize by stopping at len(name) - LB(# bytes segs can // match) rather than len(names) for i := 0; i < len(name); { r, rsize := utf8.DecodeRuneInString(name[i:]) j := i + rsize // Match name[:j] with the starting *, and the rest with chunk. if !startingStar.Match(r) { break } ok, rest := matchFixedLength(chunk, name[j:]) if ok && (rest == "" || len(segs) > 0) { name = rest continue segs } i = j } } return false } return name == "" } // matchFixedLength returns whether a run of fixed-length segments (Literal and // Question) matches a prefix of name. It returns whether the match is // successful and if if it is, the remaining part of name. func matchFixedLength(segs []Segment, name string) (bool, string) { for _, seg := range segs { if name == "" { return false, "" } switch seg := seg.(type) { case Literal: n := len(seg.Data) if len(name) < n || name[:n] != seg.Data { return false, "" } name = name[n:] case Wild: if seg.Type == Question { r, n := utf8.DecodeRuneInString(name) if !seg.Match(r) { return false, "" } name = name[n:] } else { panic("matchFixedLength given non-question wild segment") } default: panic("matchFixedLength given non-literal non-wild segment") } } return true, name } elvish-0.17.0/pkg/glob/glob_test.go000066400000000000000000000066551415471104000171170ustar00rootroot00000000000000package glob import ( "os" "reflect" "runtime" "sort" "strings" "testing" "src.elv.sh/pkg/testutil" ) var ( mkdirs = []string{"a", "b", "c", "d1", "d1/e", "d1/e/f", "d1/e/f/g", "d2", "d2/e", "d2/e/f", "d2/e/f/g"} mkdirDots = []string{".el"} creates = []string{"a/X", "a/Y", "b/X", "c/Y", "dX", "dXY", "lorem", "ipsum", "d1/e/f/g/X", "d2/e/f/g/X"} createDots = []string{".x", ".el/x"} ) type globCase struct { pattern string want []string } var globCases = []globCase{ {"*", []string{"a", "b", "c", "d1", "d2", "dX", "dXY", "lorem", "ipsum"}}, {".", []string{"."}}, {"./*", []string{"./a", "./b", "./c", "./d1", "./d2", "./dX", "./dXY", "./lorem", "./ipsum"}}, {"..", []string{".."}}, {"a/..", []string{"a/.."}}, {"a/../*", []string{"a/../a", "a/../b", "a/../c", "a/../d1", "a/../d2", "a/../dX", "a/../dXY", "a/../lorem", "a/../ipsum"}}, {"*/", []string{"a/", "b/", "c/", "d1/", "d2/"}}, {"**", append(mkdirs, creates...)}, {"*/X", []string{"a/X", "b/X"}}, {"**X", []string{"a/X", "b/X", "dX", "d1/e/f/g/X", "d2/e/f/g/X"}}, {"*/*/*", []string{"d1/e/f", "d2/e/f"}}, {"l*m", []string{"lorem"}}, {"d*", []string{"d1", "d2", "dX", "dXY"}}, {"d*/", []string{"d1/", "d2/"}}, {"d**", []string{"d1", "d1/e", "d1/e/f", "d1/e/f/g", "d1/e/f/g/X", "d2", "d2/e", "d2/e/f", "d2/e/f/g", "d2/e/f/g/X", "dX", "dXY"}}, {"?", []string{"a", "b", "c"}}, {"??", []string{"d1", "d2", "dX"}}, // Nonexistent paths. {"xxxx", []string{}}, {"xxxx/*", []string{}}, {"a/*/", []string{}}, // TODO Test cases against dotfiles. } func TestGlob_Relative(t *testing.T) { testGlob(t, false) } func TestGlob_Absolute(t *testing.T) { testGlob(t, true) } func testGlob(t *testing.T, abs bool) { dir := testutil.InTempDir(t) dir = strings.ReplaceAll(dir, string(os.PathSeparator), "/") for _, dir := range append(mkdirs, mkdirDots...) { err := os.Mkdir(dir, 0755) if err != nil { panic(err) } } for _, file := range append(creates, createDots...) { f, err := os.Create(file) if err != nil { panic(err) } f.Close() } for _, tc := range globCases { pattern := tc.pattern if abs { pattern = dir + "/" + pattern } wantResults := make([]string, len(tc.want)) for i, result := range tc.want { if abs { wantResults[i] = dir + "/" + result } else { wantResults[i] = result } } sort.Strings(wantResults) results := globPaths(pattern) if !reflect.DeepEqual(results, wantResults) { t.Errorf(`Glob(%q) => %v, want %v`, pattern, results, wantResults) } } } // Regression test for b.elv.sh/1220 func TestGlob_InvalidUTF8InFilename(t *testing.T) { if runtime.GOOS == "windows" { // On Windows, filenames are converted to UTF-16 before being passed // to API calls, meaning that all the invalid byte sequences will be // normalized to U+FFFD, making this impossible to test. t.Skip() } testutil.InTempDir(t) name := string([]byte{255}) + "x" f, err := os.Create(name) if err != nil { // The system may refuse to create a file whose name is not UTF-8. This // happens on macOS 11 with an APFS filesystem. t.Skip("create: ", err) } f.Close() paths := globPaths("*x") wantPaths := []string{name} if !reflect.DeepEqual(paths, wantPaths) { t.Errorf("got %v, want %v", paths, wantPaths) } } func globPaths(pattern string) []string { paths := []string{} Glob(pattern, func(pathInfo PathInfo) bool { paths = append(paths, pathInfo.Path) return true }) sort.Strings(paths) return paths } elvish-0.17.0/pkg/glob/parse.go000066400000000000000000000026251415471104000162400ustar00rootroot00000000000000package glob import ( "bytes" "unicode/utf8" ) // Parse parses a pattern. func Parse(s string) Pattern { segments := []Segment{} add := func(seg Segment) { segments = append(segments, seg) } p := &parser{s, 0, 0} rune: for { r := p.next() switch r { case eof: break rune case '?': add(Wild{Question, false, nil}) case '*': n := 1 for p.next() == '*' { n++ } p.backup() if n == 1 { add(Wild{Star, false, nil}) } else { add(Wild{StarStar, false, nil}) } case '/': for p.next() == '/' { } p.backup() add(Slash{}) default: var literal bytes.Buffer literal: for { switch r { case '?', '*', '/', eof: break literal case '\\': r = p.next() if r == eof { break literal } literal.WriteRune(r) default: literal.WriteRune(r) } r = p.next() } p.backup() add(Literal{literal.String()}) } } return Pattern{segments, ""} } // TODO(xiaq): Contains duplicate code with parse/parser.go. type parser struct { src string pos int overEOF int } const eof rune = -1 func (ps *parser) next() rune { if ps.pos == len(ps.src) { ps.overEOF++ return eof } r, s := utf8.DecodeRuneInString(ps.src[ps.pos:]) ps.pos += s return r } func (ps *parser) backup() { if ps.overEOF > 0 { ps.overEOF-- return } _, s := utf8.DecodeLastRuneInString(ps.src[:ps.pos]) ps.pos -= s } elvish-0.17.0/pkg/glob/parse_test.go000066400000000000000000000020101415471104000172630ustar00rootroot00000000000000package glob import ( "reflect" "testing" ) var parseCases = []struct { src string want []Segment }{ {``, []Segment{}}, {`foo`, []Segment{Literal{"foo"}}}, {`*foo*bar`, []Segment{ Wild{Star, false, nil}, Literal{"foo"}, Wild{Star, false, nil}, Literal{"bar"}}}, {`foo**bar`, []Segment{ Literal{"foo"}, Wild{StarStar, false, nil}, Literal{"bar"}}}, {`/usr/a**b/c`, []Segment{ Slash{}, Literal{"usr"}, Slash{}, Literal{"a"}, Wild{StarStar, false, nil}, Literal{"b"}, Slash{}, Literal{"c"}}}, {`??b`, []Segment{ Wild{Question, false, nil}, Wild{Question, false, nil}, Literal{"b"}}}, // Multiple slashes should be parsed as one. {`//a//b`, []Segment{ Slash{}, Literal{"a"}, Slash{}, Literal{"b"}}}, // Escaping. {`\*\?b`, []Segment{ Literal{"*?b"}, }}, {`abc\`, []Segment{ Literal{"abc"}, }}, } func TestParse(t *testing.T) { for _, tc := range parseCases { p := Parse(tc.src) if !reflect.DeepEqual(p.Segments, tc.want) { t.Errorf("Parse(%q) => %v, want %v", tc.src, p, tc.want) } } } elvish-0.17.0/pkg/glob/pattern.go000066400000000000000000000032131415471104000165750ustar00rootroot00000000000000package glob // Pattern is a glob pattern. type Pattern struct { Segments []Segment DirOverride string } // Segment is the building block of Pattern. type Segment interface { isSegment() } // Slash represents a slash "/". type Slash struct{} // Literal is a series of non-slash, non-wildcard characters, that is to be // matched literally. type Literal struct { Data string } // Wild is a wildcard. type Wild struct { Type WildType MatchHidden bool Matchers []func(rune) bool } // WildType is the type of a Wild. type WildType int // Values for WildType. const ( Question = iota Star StarStar ) // Match returns whether a rune is within the match set. func (w Wild) Match(r rune) bool { if len(w.Matchers) == 0 { return true } for _, m := range w.Matchers { if m(r) { return true } } return false } func (Literal) isSegment() {} func (Slash) isSegment() {} func (Wild) isSegment() {} // IsSlash returns whether a Segment is a Slash. func IsSlash(seg Segment) bool { _, ok := seg.(Slash) return ok } // IsLiteral returns whether a Segment is a Literal. func IsLiteral(seg Segment) bool { _, ok := seg.(Literal) return ok } // IsWild returns whether a Segment is a Wild. func IsWild(seg Segment) bool { _, ok := seg.(Wild) return ok } // IsWild1 returns whether a Segment is a Wild and has the specified type. func IsWild1(seg Segment, t WildType) bool { return IsWild(seg) && seg.(Wild).Type == t } // IsWild2 returns whether a Segment is a Wild and has one of the two specified // types. func IsWild2(seg Segment, t1, t2 WildType) bool { return IsWild(seg) && (seg.(Wild).Type == t1 || seg.(Wild).Type == t2) } elvish-0.17.0/pkg/logutil/000077500000000000000000000000001415471104000153265ustar00rootroot00000000000000elvish-0.17.0/pkg/logutil/logutil.go000066400000000000000000000024611415471104000173370ustar00rootroot00000000000000// Package logutil provides logging utilities. package logutil import ( "io" "log" "os" ) var ( out = io.Discard // If out is set by SetOutputFile, outFile is set and keeps the same value // as out. Otherwise, outFile is nil. outFile *os.File loggers []*log.Logger ) // GetLogger gets a logger with a prefix. func GetLogger(prefix string) *log.Logger { logger := log.New(out, prefix, log.LstdFlags) loggers = append(loggers, logger) return logger } // SetOutput redirects the output of all loggers obtained with GetLogger to the // new io.Writer. If the old output was a file opened by SetOutputFile, it is // closed. func SetOutput(newout io.Writer) { if outFile != nil { outFile.Close() outFile = nil } out = newout outFile = nil for _, logger := range loggers { logger.SetOutput(out) } } // SetOutputFile redirects the output of all loggers obtained with GetLogger to // the named file. If the old output was a file opened by SetOutputFile, it is // closed. The new file is truncated. SetOutFile("") is equivalent to // SetOutput(io.Discard). func SetOutputFile(fname string) error { if fname == "" { SetOutput(io.Discard) return nil } file, err := os.OpenFile(fname, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) if err != nil { return err } SetOutput(file) outFile = file return nil } elvish-0.17.0/pkg/logutil/logutil_test.go000066400000000000000000000001161415471104000203710ustar00rootroot00000000000000package logutil import "testing" func TestLogger(t *testing.T) { // TODO } elvish-0.17.0/pkg/mods/000077500000000000000000000000001415471104000146115ustar00rootroot00000000000000elvish-0.17.0/pkg/mods/daemon/000077500000000000000000000000001415471104000160545ustar00rootroot00000000000000elvish-0.17.0/pkg/mods/daemon/daemon.go000066400000000000000000000013661415471104000176540ustar00rootroot00000000000000// Package daemon implements the builtin daemon: module. package daemon import ( "strconv" "src.elv.sh/pkg/daemon/daemondefs" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vars" ) // Ns makes the daemon: namespace. func Ns(d daemondefs.Client) *eval.Ns { getPid := func() (string, error) { pid, err := d.Pid() return string(strconv.Itoa(pid)), err } // TODO: Deprecate the variable in favor of the function. getPidVar := func() interface{} { pid, err := getPid() if err != nil { return "-1" } return pid } return eval.BuildNsNamed("daemon"). AddVars(map[string]vars.Var{ "pid": vars.FromGet(getPidVar), "sock": vars.NewReadOnly(string(d.SockPath())), }). AddGoFns(map[string]interface{}{ "pid": getPid, }).Ns() } elvish-0.17.0/pkg/mods/daemon/daemon_test.go000066400000000000000000000001151415471104000207020ustar00rootroot00000000000000package daemon import "testing" func TestDaemon(t *testing.T) { // TODO } elvish-0.17.0/pkg/mods/epm/000077500000000000000000000000001415471104000153725ustar00rootroot00000000000000elvish-0.17.0/pkg/mods/epm/epm.elv000066400000000000000000000262701415471104000166720ustar00rootroot00000000000000use re use str use platform # Verbosity configuration var debug-mode = $false # Configuration for common domains var -default-domain-config = [ &"github.com"= [ &method= git &protocol= https &levels= 2 ] &"bitbucket.org"= [ &method= git &protocol= https &levels= 2 ] &"gitlab.com"= [ &method= git &protocol= https &levels= 2 ] ] #elvdoc:var managed-dir # # The path of the `epm`-managed directory. var managed-dir = ( if $platform:is-windows { put $E:LocalAppData/elvish/lib } elif (not-eq $E:XDG_DATA_HOME '') { put $E:XDG_DATA_HOME/elvish/lib } else { put ~/.local/share/elvish/lib } ) # General utility functions fn -debug {|text| if $debug-mode { print (styled '=> ' blue) echo $text } } fn -info {|text| print (styled '=> ' green) echo $text } fn -warn {|text| print (styled '=> ' yellow) echo $text } fn -error {|text| print (styled '=> ' red) echo $text } fn dest {|pkg| put $managed-dir/$pkg } #elvdoc:fn is-installed # # ```elvish # epm:is-installed $pkg # ``` # # Returns a boolean value indicating whether the given package is installed. fn is-installed {|pkg| bool ?(test -e (dest $pkg)) } fn -package-domain {|pkg| str:split &max=2 / $pkg | take 1 } fn -package-without-domain {|pkg| str:split &max=2 / $pkg | drop 1 | str:join '' } # Merge two maps fn -merge {|a b| keys $b | each {|k| set a[$k] = $b[$k] } put $a } # Uppercase first letter of a string fn -first-upper {|s| put (echo $s[0] | tr '[:lower:]' '[:upper:]')$s[(count $s[0]):] } # Expand tilde at the beginning of a string to the home dir fn -tilde-expand {|p| re:replace "^~" $E:HOME $p } # Known method handlers. Each entry is indexed by method name (the # value of the "method" key in the domain configs), and must contain # two keys: install and upgrade, each one must be a closure that # receives two arguments: package name and the domain config entry # # - Method 'git' requires the key 'protocol' in the domain config, # which has to be 'http' or 'https' # - Method 'rsync' requires the key 'location' in the domain config, # which has to contain the directory where the domain files are # stored. It can be any source location understood by the rsync # command. var -method-handler set -method-handler = [ &git= [ &src= {|pkg dom-cfg| put $dom-cfg[protocol]"://"$pkg } &install= {|pkg dom-cfg| var dest = (dest $pkg) -info "Installing "$pkg mkdir -p $dest git clone ($-method-handler[git][src] $pkg $dom-cfg) $dest } &upgrade= {|pkg dom-cfg| var dest = (dest $pkg) -info "Updating "$pkg try { git -C $dest pull } except _ { -error "Something failed, please check error above and retry." } } ] &rsync= [ &src= {|pkg dom-cfg| put (-tilde-expand $dom-cfg[location])/(-package-without-domain $pkg)/ } &install= {|pkg dom-cfg| var dest = (dest $pkg) var pkgd = (-package-without-domain $pkg) -info "Installing "$pkg rsync -av ($-method-handler[rsync][src] $pkg $dom-cfg) $dest } &upgrade= {|pkg dom-cfg| var dest = (dest $pkg) var pkgd = (-package-without-domain $pkg) if (not (is-installed $pkg)) { -error "Package "$pkg" is not installed." return } -info "Updating "$pkg rsync -av ($-method-handler[rsync][src] $pkg $dom-cfg) $dest } ] ] # Return the filename of the domain config file for the given domain # (regardless of whether it exists) fn -domain-config-file {|dom| put $managed-dir/$dom/epm-domain.cfg } # Return the filename of the metadata file for the given package # (regardless of whether it exists) fn -package-metadata-file {|pkg| put (dest $pkg)/metadata.json } fn -write-domain-config {|dom| var cfgfile = (-domain-config-file $dom) mkdir -p (dirname $cfgfile) if (has-key $-default-domain-config $dom) { put $-default-domain-config[$dom] | to-json > $cfgfile } else { -error "No default config exists for domain "$dom"." } } # Returns the domain config for a given domain parsed from JSON. # If the file does not exist but we have a built-in # definition, then we return the default. Otherwise we return $false, # so the result can always be checked with 'if'. fn -domain-config {|dom| var cfgfile = (-domain-config-file $dom) var cfg = $false if ?(test -f $cfgfile) { # If the config file exists, read it... set cfg = (cat $cfgfile | from-json) -debug "Read domain config for "$dom": "(to-string $cfg) } else { # ...otherwise check if we have a default config for the domain, and save it if (has-key $-default-domain-config $dom) { set cfg = $-default-domain-config[$dom] -debug "No existing config for "$dom", using the default: "(to-string $cfg) } else { -debug "No existing config for "$dom" and no default available." } } put $cfg } # Return the method by which a package is installed fn -package-method {|pkg| var dom = (-package-domain $pkg) var cfg = (-domain-config $dom) if $cfg { put $cfg[method] } else { put $false } } # Invoke package operations defined in $-method-handler above fn -package-op {|pkg what| var dom = (-package-domain $pkg) var cfg = (-domain-config $dom) if $cfg { var method = $cfg[method] if (has-key $-method-handler $method) { if (has-key $-method-handler[$method] $what) { $-method-handler[$method][$what] $pkg $cfg } else { fail "Unknown operation '"$what"' for package "$pkg } } else { fail "Unknown method '"$method"', specified in in config file "(-domain-config-file $dom) } } else { -error "No config for domain '"$dom"'." } } # Uninstall a single package by removing its directory fn -uninstall-package {|pkg| if (not (is-installed $pkg)) { -error "Package "$pkg" is not installed." return } var dest = (dest $pkg) -info "Removing package "$pkg rm -rf $dest } ###################################################################### # Main user-facing functions #elvdoc:fn metadata # # ```elvish # epm:metadata $pkg # ``` # # Returns a hash containing the metadata for the given package. Metadata for a # package includes the following base attributes: # # - `name`: name of the package # - `installed`: a boolean indicating whether the package is currently installed # - `method`: method by which it was installed (`git` or `rsync`) # - `src`: source URL of the package # - `dst`: where the package is (or would be) installed. Note that this # attribute is returned even if `installed` is `$false`. # # Additionally, packages can define arbitrary metadata attributes in a file called # `metadata.json` in their top directory. The following attributes are # recommended: # # - `description`: a human-readable description of the package # - `maintainers`: an array containing the package maintainers, in # `Name ` format. # - `homepage`: URL of the homepage for the package, if it has one. # - `dependencies`: an array listing dependencies of the current package. Any # packages listed will be installed automatically by `epm:install` if they are # not yet installed. # Read and parse the package metadata, if it exists fn metadata {|pkg| # Base metadata attributes var res = [ &name= $pkg &method= (-package-method $pkg) &src= (-package-op $pkg src) &dst= (dest $pkg) &installed= (is-installed $pkg) ] # Merge with package-specified attributes, if any var file = (-package-metadata-file $pkg) if (and (is-installed $pkg) ?(test -f $file)) { set res = (-merge (cat $file | from-json) $res) } put $res } #elvdoc:fn query # # ```elvish # epm:query $pkg # ``` # # Pretty print the available metadata of the given package. # Print out information about a package fn query {|pkg| var data = (metadata $pkg) var special-keys = [name method installed src dst] echo (styled "Package "$data[name] cyan) if $data[installed] { echo (styled "Installed at "$data[dst] green) } else { echo (styled "Not installed" red) } echo (styled "Source:" blue) $data[method] $data[src] keys $data | each {|key| if (not (has-value $special-keys $key)) { var val = $data[$key] if (eq (kind-of $val) list) { set val = (str:join ", " $val) } echo (styled (-first-upper $key)":" blue) $val } } } #elvdoc:fn installed # # ```elvish # epm:installed # ``` # # Return an array with all installed packages. `epm:list` can be used as an alias # for `epm:installed`. # List installed packages fn installed { put $managed-dir/*[nomatch-ok] | each {|dir| var dom = (str:replace $managed-dir/ '' $dir) var cfg = (-domain-config $dom) # Only list domains for which we know the config, so that the user # can have his own non-package directories under ~/.elvish/lib # without conflicts. if $cfg { var lvl = $cfg[levels] var pat = '^\Q'$managed-dir'/\E('(repeat (+ $lvl 1) '[^/]+' | str:join '/')')/$' put (each {|d| re:find $pat $d } [ $managed-dir/$dom/**[nomatch-ok]/ ] )[groups][1][text] } } } # epm:list is an alias for epm:installed fn list { installed } #elvdoc:fn install # # ```elvish # epm:install &silent-if-installed=$false $pkg... # ``` # # Install the named packages. By default, if a package is already installed, a # message will be shown. This can be disabled by passing # `&silent-if-installed=$true`, so that already-installed packages are silently # ignored. # Install and upgrade are method-specific, so we call the # corresponding functions using -package-op fn install {|&silent-if-installed=$false @pkgs| if (eq $pkgs []) { -error "You must specify at least one package." return } for pkg $pkgs { if (is-installed $pkg) { if (not $silent-if-installed) { -info "Package "$pkg" is already installed." } } else { -package-op $pkg install # Check if there are any dependencies to install var metadata = (metadata $pkg) if (has-key $metadata dependencies) { var deps = $metadata[dependencies] -info "Installing dependencies: "(str:join " " $deps) # If the installation of dependencies fails, uninstall the # target package (leave any already-installed dependencies in # place) try { install $@deps } except e { -error "Dependency installation failed. Uninstalling "$pkg", please check the errors above and try again." -uninstall-package $pkg } } } } } #elvdoc:fn upgrade # # ```elvish # epm:upgrade $pkg... # ``` # # Upgrade named packages. If no package name is given, upgrade all installed # packages. fn upgrade {|@pkgs| if (eq $pkgs []) { set pkgs = [(installed)] -info 'Upgrading all installed packages' } for pkg $pkgs { if (not (is-installed $pkg)) { -error "Package "$pkg" is not installed." } else { -package-op $pkg upgrade } } } #elvdoc:fn uninstall # # ```elvish # epm:uninstall $pkg... # ``` # # Uninstall named packages. # Uninstall is the same for everyone, just remove the directory fn uninstall {|@pkgs| if (eq $pkgs []) { -error 'You must specify at least one package.' return } for pkg $pkgs { -uninstall-package $pkg } } elvish-0.17.0/pkg/mods/epm/epm.go000066400000000000000000000001661415471104000165050ustar00rootroot00000000000000package epm import _ "embed" // Code contains the source code of the epm module. //go:embed epm.elv var Code string elvish-0.17.0/pkg/mods/epm/epm_test.go000066400000000000000000000004031415471104000175360ustar00rootroot00000000000000package epm_test import ( "testing" . "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/mods" ) func TestEPM(t *testing.T) { // A smoke test to ensure that the epm module has no errors. TestWithSetup(t, mods.AddTo, That("use epm").DoesNothing(), ) } elvish-0.17.0/pkg/mods/file/000077500000000000000000000000001415471104000155305ustar00rootroot00000000000000elvish-0.17.0/pkg/mods/file/file.go000066400000000000000000000061201415471104000167750ustar00rootroot00000000000000package file import ( "math/big" "os" "strconv" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" ) var Ns = eval.BuildNsNamed("file"). AddGoFns(map[string]interface{}{ "close": close, "open": open, "pipe": pipe, "truncate": truncate, }).Ns() //elvdoc:fn open // // ```elvish // file:open $filename // ``` // // Opens a file. Currently, `open` only supports opening a file for reading. // File must be closed with `close` explicitly. Example: // // ```elvish-transcript // ~> cat a.txt // This is // a file. // ~> use file // ~> f = (file:open a.txt) // ~> cat < $f // This is // a file. // ~> close $f // ``` // // @cf file:close func open(name string) (vals.File, error) { return os.Open(name) } //elvdoc:fn close // // ```elvish // file:close $file // ``` // // Closes a file opened with `open`. // // @cf file:open func close(f vals.File) error { return f.Close() } //elvdoc:fn pipe // // ```elvish // file:pipe // ``` // // Create a new pipe that can be used in redirections. A pipe contains a read-end and write-end. // Each pipe object is a [pseudo-map](language.html#pseudo-map) with fields `r` (the read-end [file // object](./language.html#file)) and `w` (the write-end). // // When redirecting command input from a pipe with `<`, the read-end is used. When redirecting // command output to a pipe with `>`, the write-end is used. Redirecting both input and output with // `<>` to a pipe is not supported. // // Pipes have an OS-dependent buffer, so writing to a pipe without an active reader // does not necessarily block. Pipes **must** be explicitly closed with `file:close`. // // Putting values into pipes will cause those values to be discarded. // // Examples (assuming the pipe has a large enough buffer): // // ```elvish-transcript // ~> p = (file:pipe) // ~> echo 'lorem ipsum' > $p // ~> head -n1 < $p // lorem ipsum // ~> put 'lorem ipsum' > $p // ~> file:close $p[w] # close the write-end // ~> head -n1 < $p # blocks unless the write-end is closed // ~> file:close $p[r] # close the read-end // ``` // // @cf file:close func pipe() (vals.Pipe, error) { r, w, err := os.Pipe() return vals.NewPipe(r, w), err } //elvdoc:fn truncate // // ```elvish // file:truncate $filename $size // ``` // // changes the size of the named file. If the file is a symbolic link, it // changes the size of the link's target. The size must be an integer between 0 // and 2^64-1. func truncate(name string, rawSize vals.Num) error { var size int64 switch rawSize := rawSize.(type) { case int: size = int64(rawSize) case *big.Int: if rawSize.IsInt64() { size = rawSize.Int64() } else { return truncateSizeOutOfRange(rawSize.String()) } default: return errs.BadValue{ What: "size argument to file:truncate", Valid: "integer", Actual: "non-integer", } } if size < 0 { return truncateSizeOutOfRange(strconv.FormatInt(size, 10)) } return os.Truncate(name, size) } func truncateSizeOutOfRange(size string) error { return errs.OutOfRange{ What: "size argument to file:truncate", ValidLow: "0", ValidHigh: "2^64-1", Actual: size, } } elvish-0.17.0/pkg/mods/file/file_test.go000066400000000000000000000036401415471104000200400ustar00rootroot00000000000000package file import ( "os" "testing" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/errs" . "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/testutil" ) // A number that exceeds the range of int64 const z = "100000000000000000000" func TestFile(t *testing.T) { setup := func(ev *eval.Evaler) { ev.ExtendGlobal(eval.BuildNs().AddNs("file", Ns)) } testutil.InTempDir(t) TestWithSetup(t, setup, That(` echo haha > out3 f = (file:open out3) slurp < $f file:close $f `).Puts("haha\n"), That(` p = (file:pipe) echo haha > $p file:close $p[w] slurp < $p file:close $p[r] `).Puts("haha\n"), That(` p = (file:pipe) echo Legolas > $p file:close $p[r] slurp < $p `).Throws(AnyError), // Verify that input redirection from a closed pipe throws an exception. That exception is a // Go stdlib error whose stringified form looks something like "read |0: file already // closed". That(`p = (file:pipe)`, `echo Legolas > $p`, `file:close $p[r]`, `slurp < $p`).Throws(ErrorWithType(&os.PathError{})), // Side effect checked below That("echo > file100", "file:truncate file100 100").DoesNothing(), // Should also test the case where the argument doesn't fit in an int // but does in a *big.Int, but this could consume too much disk That("file:truncate bad -1").Throws(errs.OutOfRange{ What: "size argument to file:truncate", ValidLow: "0", ValidHigh: "2^64-1", Actual: "-1", }), That("file:truncate bad "+z).Throws(errs.OutOfRange{ What: "size argument to file:truncate", ValidLow: "0", ValidHigh: "2^64-1", Actual: z, }), That("file:truncate bad 1.5").Throws(errs.BadValue{ What: "size argument to file:truncate", Valid: "integer", Actual: "non-integer", }), ) fi, err := os.Stat("file100") if err != nil { t.Errorf("stat file100: %v", err) } if size := fi.Size(); size != 100 { t.Errorf("got file100 size %v, want 100", size) } } elvish-0.17.0/pkg/mods/math/000077500000000000000000000000001415471104000155425ustar00rootroot00000000000000elvish-0.17.0/pkg/mods/math/math.go000066400000000000000000000401511415471104000170230ustar00rootroot00000000000000// Package math exposes functionality from Go's math package as an elvish // module. package math import ( "math" "math/big" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" ) // Ns is the namespace for the math: module. var Ns = eval.BuildNsNamed("math"). AddVars(map[string]vars.Var{ "e": vars.NewReadOnly(math.E), "pi": vars.NewReadOnly(math.Pi), }). AddGoFns(map[string]interface{}{ "abs": abs, "acos": math.Acos, "acosh": math.Acosh, "asin": math.Asin, "asinh": math.Asinh, "atan": math.Atan, "atanh": math.Atanh, "ceil": ceil, "cos": math.Cos, "cosh": math.Cosh, "floor": floor, "is-inf": isInf, "is-nan": isNaN, "log": math.Log, "log10": math.Log10, "log2": math.Log2, "max": max, "min": min, "pow": pow, "round": round, "round-to-even": roundToEven, "sin": math.Sin, "sinh": math.Sinh, "sqrt": math.Sqrt, "tan": math.Tan, "tanh": math.Tanh, "trunc": trunc, }).Ns() //elvdoc:var e // // ```elvish // $math:e // ``` // // Approximate value of // [`e`](https://en.wikipedia.org/wiki/E_(mathematical_constant)): // 2.718281.... This variable is read-only. //elvdoc:var pi // // ```elvish // $math:pi // ``` // // Approximate value of [`π`](https://en.wikipedia.org/wiki/Pi): 3.141592.... This // variable is read-only. //elvdoc:fn abs // // ```elvish // math:abs $number // ``` // // Computes the absolute value `$number`. This function is exactness-preserving. // Examples: // // ```elvish-transcript // ~> math:abs 2 // ▶ (num 2) // ~> math:abs -2 // ▶ (num 2) // ~> math:abs 10000000000000000000 // ▶ (num 10000000000000000000) // ~> math:abs -10000000000000000000 // ▶ (num 10000000000000000000) // ~> math:abs 1/2 // ▶ (num 1/2) // ~> math:abs -1/2 // ▶ (num 1/2) // ~> math:abs 1.23 // ▶ (num 1.23) // ~> math:abs -1.23 // ▶ (num 1.23) // ``` const ( maxInt = int(^uint(0) >> 1) minInt = -maxInt - 1 ) var absMinInt = new(big.Int).Abs(big.NewInt(int64(minInt))) func abs(n vals.Num) vals.Num { switch n := n.(type) { case int: if n < 0 { if n == minInt { return absMinInt } return -n } return n case *big.Int: if n.Sign() < 0 { return new(big.Int).Abs(n) } return n case *big.Rat: if n.Sign() < 0 { return new(big.Rat).Abs(n) } return n case float64: return math.Abs(n) default: panic("unreachable") } } //elvdoc:fn acos // // ```elvish // math:acos $number // ``` // // Outputs the arccosine of `$number`, in radians (not degrees). Examples: // // ```elvish-transcript // ~> math:acos 1 // ▶ (float64 1) // ~> math:acos 1.00001 // ▶ (float64 NaN) // ``` //elvdoc:fn acosh // // ```elvish // math:acosh $number // ``` // // Outputs the inverse hyperbolic cosine of `$number`. Examples: // // ```elvish-transcript // ~> math:acosh 1 // ▶ (float64 0) // ~> math:acosh 0 // ▶ (float64 NaN) // ``` //elvdoc:fn asin // // ```elvish // math:asin $number // ``` // // Outputs the arcsine of `$number`, in radians (not degrees). Examples: // // ```elvish-transcript // ~> math:asin 0 // ▶ (float64 0) // ~> math:asin 1 // ▶ (float64 1.5707963267948966) // ~> math:asin 1.00001 // ▶ (float64 NaN) // ``` //elvdoc:fn asinh // // ```elvish // math:asinh $number // ``` // // Outputs the inverse hyperbolic sine of `$number`. Examples: // // ```elvish-transcript // ~> math:asinh 0 // ▶ (float64 0) // ~> math:asinh inf // ▶ (float64 +Inf) // ``` //elvdoc:fn atan // // ```elvish // math:atan $number // ``` // // Outputs the arctangent of `$number`, in radians (not degrees). Examples: // // ```elvish-transcript // ~> math:atan 0 // ▶ (float64 0) // ~> math:atan $math:inf // ▶ (float64 1.5707963267948966) // ``` //elvdoc:fn atanh // // ```elvish // math:atanh $number // ``` // // Outputs the inverse hyperbolic tangent of `$number`. Examples: // // ```elvish-transcript // ~> math:atanh 0 // ▶ (float64 0) // ~> math:atanh 1 // ▶ (float64 +Inf) // ``` //elvdoc:fn ceil // // ```elvish // math:ceil $number // ``` // // Computes the least integer greater than or equal to `$number`. This function // is exactness-preserving. // // The results for the special floating-point values -0.0, +0.0, -Inf, +Inf and // NaN are themselves. // // Examples: // // ```elvish-transcript // ~> math:floor 1 // ▶ (num 1) // ~> math:floor 3/2 // ▶ (num 1) // ~> math:floor -3/2 // ▶ (num -2) // ~> math:floor 1.1 // ▶ (num 1.0) // ~> math:floor -1.1 // ▶ (num -2.0) // ``` var ( big1 = big.NewInt(1) big2 = big.NewInt(2) ) func ceil(n vals.Num) vals.Num { return integerize(n, math.Ceil, func(n *big.Rat) *big.Int { q := new(big.Int).Div(n.Num(), n.Denom()) return q.Add(q, big1) }) } //elvdoc:fn cos // // ```elvish // math:cos $number // ``` // // Computes the cosine of `$number` in units of radians (not degrees). // Examples: // // ```elvish-transcript // ~> math:cos 0 // ▶ (float64 1) // ~> math:cos 3.14159265 // ▶ (float64 -1) // ``` //elvdoc:fn cosh // // ```elvish // math:cosh $number // ``` // // Computes the hyperbolic cosine of `$number`. Example: // // ```elvish-transcript // ~> math:cosh 0 // ▶ (float64 1) // ``` //elvdoc:fn floor // // ```elvish // math:floor $number // ``` // // Computes the greatest integer less than or equal to `$number`. This function // is exactness-preserving. // // The results for the special floating-point values -0.0, +0.0, -Inf, +Inf and // NaN are themselves. // // Examples: // // ```elvish-transcript // ~> math:floor 1 // ▶ (num 1) // ~> math:floor 3/2 // ▶ (num 1) // ~> math:floor -3/2 // ▶ (num -2) // ~> math:floor 1.1 // ▶ (num 1.0) // ~> math:floor -1.1 // ▶ (num -2.0) // ``` func floor(n vals.Num) vals.Num { return integerize(n, math.Floor, func(n *big.Rat) *big.Int { return new(big.Int).Div(n.Num(), n.Denom()) }) } //elvdoc:fn is-inf // // ```elvish // math:is-inf &sign=0 $number // ``` // // Tests whether the number is infinity. If sign > 0, tests whether `$number` // is positive infinity. If sign < 0, tests whether `$number` is negative // infinity. If sign == 0, tests whether `$number` is either infinity. // // ```elvish-transcript // ~> math:is-inf 123 // ▶ $false // ~> math:is-inf inf // ▶ $true // ~> math:is-inf -inf // ▶ $true // ~> math:is-inf &sign=1 inf // ▶ $true // ~> math:is-inf &sign=-1 inf // ▶ $false // ~> math:is-inf &sign=-1 -inf // ▶ $true // ``` type isInfOpts struct{ Sign int } func (opts *isInfOpts) SetDefaultOptions() { opts.Sign = 0 } func isInf(opts isInfOpts, n vals.Num) bool { if f, ok := n.(float64); ok { return math.IsInf(f, opts.Sign) } return false } //elvdoc:fn is-nan // // ```elvish // math:is-nan $number // ``` // // Tests whether the number is a NaN (not-a-number). // // ```elvish-transcript // ~> math:is-nan 123 // ▶ $false // ~> math:is-nan (float64 inf) // ▶ $false // ~> math:is-nan (float64 nan) // ▶ $true // ``` func isNaN(n vals.Num) bool { if f, ok := n.(float64); ok { return math.IsNaN(f) } return false } //elvdoc:fn log // // ```elvish // math:log $number // ``` // // Computes the natural (base *e*) logarithm of `$number`. Examples: // // ```elvish-transcript // ~> math:log 1.0 // ▶ (float64 1) // ~> math:log -2.3 // ▶ (float64 NaN) // ``` //elvdoc:fn log10 // // ```elvish // math:log10 $number // ``` // // Computes the base 10 logarithm of `$number`. Examples: // // ```elvish-transcript // ~> math:log10 100.0 // ▶ (float64 2) // ~> math:log10 -1.7 // ▶ (float64 NaN) // ``` //elvdoc:fn log2 // // ```elvish // math:log2 $number // ``` // // Computes the base 2 logarithm of `$number`. Examples: // // ```elvish-transcript // ~> math:log2 8 // ▶ (float64 3) // ~> math:log2 -5.3 // ▶ (float64 NaN) // ``` //elvdoc:fn max // // ```elvish // math:max $number... // ``` // // Outputs the maximum number in the arguments. If there are no arguments, // an exception is thrown. If any number is NaN then NaN is output. This // function is exactness-preserving. // // Examples: // // ```elvish-transcript // ~> math:max 3 5 2 // ▶ (num 5) // ~> math:max (range 100) // ▶ (num 99) // ~> math:max 1/2 1/3 2/3 // ▶ (num 2/3) // ``` func max(rawNums ...vals.Num) (vals.Num, error) { if len(rawNums) == 0 { return nil, errs.ArityMismatch{What: "arguments", ValidLow: 1, ValidHigh: -1, Actual: 0} } nums := vals.UnifyNums(rawNums, 0) switch nums := nums.(type) { case []int: n := nums[0] for i := 1; i < len(nums); i++ { if n < nums[i] { n = nums[i] } } return n, nil case []*big.Int: n := nums[0] for i := 1; i < len(nums); i++ { if n.Cmp(nums[i]) < 0 { n = nums[i] } } return n, nil case []*big.Rat: n := nums[0] for i := 1; i < len(nums); i++ { if n.Cmp(nums[i]) < 0 { n = nums[i] } } return n, nil case []float64: n := nums[0] for i := 1; i < len(nums); i++ { n = math.Max(n, nums[i]) } return n, nil default: panic("unreachable") } } //elvdoc:fn min // // ```elvish // math:min $number... // ``` // // Outputs the minimum number in the arguments. If there are no arguments // an exception is thrown. If any number is NaN then NaN is output. This // function is exactness-preserving. // // Examples: // // ```elvish-transcript // ~> math:min // Exception: arity mismatch: arguments must be 1 or more values, but is 0 values // [tty 17], line 1: math:min // ~> math:min 3 5 2 // ▶ (num 2) // ~> math:min 1/2 1/3 2/3 // ▶ (num 1/3) // ``` func min(rawNums ...vals.Num) (vals.Num, error) { if len(rawNums) == 0 { return nil, errs.ArityMismatch{What: "arguments", ValidLow: 1, ValidHigh: -1, Actual: 0} } nums := vals.UnifyNums(rawNums, 0) switch nums := nums.(type) { case []int: n := nums[0] for i := 1; i < len(nums); i++ { if n > nums[i] { n = nums[i] } } return n, nil case []*big.Int: n := nums[0] for i := 1; i < len(nums); i++ { if n.Cmp(nums[i]) > 0 { n = nums[i] } } return n, nil case []*big.Rat: n := nums[0] for i := 1; i < len(nums); i++ { if n.Cmp(nums[i]) > 0 { n = nums[i] } } return n, nil case []float64: n := nums[0] for i := 1; i < len(nums); i++ { n = math.Min(n, nums[i]) } return n, nil default: panic("unreachable") } } //elvdoc:fn pow // // ```elvish // math:pow $base $exponent // ``` // // Outputs the result of raising `$base` to the power of `$exponent`. // // This function produces an exact result when `$base` is exact and `$exponent` // is an exact integer. Otherwise it produces an inexact result. // // Examples: // // ```elvish-transcript // ~> math:pow 3 2 // ▶ (num 9) // ~> math:pow -2 2 // ▶ (num 4) // ~> math:pow 1/2 3 // ▶ (num 1/8) // ~> math:pow 1/2 -3 // ▶ (num 8) // ~> math:pow 9 1/2 // ▶ (num 3.0) // ~> math:pow 12 1.1 // ▶ (num 15.38506624784179) // ``` func pow(base, exp vals.Num) vals.Num { if isExact(base) && isExactInt(exp) { // Produce exact result switch exp { case 0: return 1 case 1: return base case -1: return new(big.Rat).Inv(vals.PromoteToBigRat(base)) } exp := vals.PromoteToBigInt(exp) if isExactInt(base) && exp.Sign() > 0 { base := vals.PromoteToBigInt(base) return new(big.Int).Exp(base, exp, nil) } base := vals.PromoteToBigRat(base) if exp.Sign() < 0 { base = new(big.Rat).Inv(base) exp = new(big.Int).Neg(exp) } return new(big.Rat).SetFrac( new(big.Int).Exp(base.Num(), exp, nil), new(big.Int).Exp(base.Denom(), exp, nil)) } // Produce inexact result basef := vals.ConvertToFloat64(base) expf := vals.ConvertToFloat64(exp) return math.Pow(basef, expf) } func isExact(n vals.Num) bool { switch n.(type) { case int, *big.Int, *big.Rat: return true default: return false } } func isExactInt(n vals.Num) bool { switch n.(type) { case int, *big.Int: return true default: return false } } //elvdoc:fn round // // ```elvish // math:round $number // ``` // // Outputs the nearest integer, rounding half away from zero. This function is // exactness-preserving. // // The results for the special floating-point values -0.0, +0.0, -Inf, +Inf and // NaN are themselves. // // Examples: // // ```elvish-transcript // ~> math:round 2 // ▶ (num 2) // ~> math:round 1/3 // ▶ (num 0) // ~> math:round 1/2 // ▶ (num 1) // ~> math:round 2/3 // ▶ (num 1) // ~> math:round -1/3 // ▶ (num 0) // ~> math:round -1/2 // ▶ (num -1) // ~> math:round -2/3 // ▶ (num -1) // ~> math:round 2.5 // ▶ (num 3.0) // ``` func round(n vals.Num) vals.Num { return integerize(n, math.Round, func(n *big.Rat) *big.Int { q, m := new(big.Int).QuoRem(n.Num(), n.Denom(), new(big.Int)) m = m.Mul(m, big2) if m.CmpAbs(n.Denom()) < 0 { return q } if n.Sign() < 0 { return q.Sub(q, big1) } return q.Add(q, big1) }) } //elvdoc:fn round-to-even // // ```elvish // math:round-to-even $number // ``` // // Outputs the nearest integer, rounding ties to even. This function is // exactness-preserving. // // The results for the special floating-point values -0.0, +0.0, -Inf, +Inf and // NaN are themselves. // // Examples: // // ```elvish-transcript // ~> math:round-to-even 2 // ▶ (num 2) // ~> math:round-to-even 1/2 // ▶ (num 0) // ~> math:round-to-even 3/2 // ▶ (num 2) // ~> math:round-to-even 5/2 // ▶ (num 2) // ~> math:round-to-even -5/2 // ▶ (num -2) // ~> math:round-to-even 2.5 // ▶ (num 2.0) // ~> math:round-to-even 1.5 // ▶ (num 2.0) // ``` func roundToEven(n vals.Num) vals.Num { return integerize(n, math.RoundToEven, func(n *big.Rat) *big.Int { q, m := new(big.Int).QuoRem(n.Num(), n.Denom(), new(big.Int)) m = m.Mul(m, big2) if diff := m.CmpAbs(n.Denom()); diff < 0 || diff == 0 && q.Bit(0) == 0 { return q } if n.Sign() < 0 { return q.Sub(q, big1) } return q.Add(q, big1) }) } //elvdoc:fn sin // // ```elvish // math:sin $number // ``` // // Computes the sine of `$number` in units of radians (not degrees). Examples: // // ```elvish-transcript // ~> math:sin 0 // ▶ (float64 0) // ~> math:sin 3.14159265 // ▶ (float64 3.5897930298416118e-09) // ``` //elvdoc:fn sinh // // ```elvish // math:sinh $number // ``` // // Computes the hyperbolic sine of `$number`. Example: // // ```elvish-transcript // ~> math:sinh 0 // ▶ (float64 0) // ``` //elvdoc:fn sqrt // // ```elvish // math:sqrt $number // ``` // // Computes the square-root of `$number`. Examples: // // ```elvish-transcript // ~> math:sqrt 0 // ▶ (float64 0) // ~> math:sqrt 4 // ▶ (float64 2) // ~> math:sqrt -4 // ▶ (float64 NaN) // ``` //elvdoc:fn tan // // ```elvish // math:tan $number // ``` // // Computes the tangent of `$number` in units of radians (not degrees). Examples: // // ```elvish-transcript // ~> math:tan 0 // ▶ (float64 0) // ~> math:tan 3.14159265 // ▶ (float64 -0.0000000035897930298416118) // ``` //elvdoc:fn tanh // // ```elvish // math:tanh $number // ``` // // Computes the hyperbolic tangent of `$number`. Example: // // ```elvish-transcript // ~> math:tanh 0 // ▶ (float64 0) // ``` //elvdoc:fn trunc // // ```elvish // math:trunc $number // ``` // // Outputs the integer portion of `$number`. This function is exactness-preserving. // // The results for the special floating-point values -0.0, +0.0, -Inf, +Inf and // NaN are themselves. // // Examples: // // ```elvish-transcript // ~> math:trunc 1 // ▶ (num 1) // ~> math:trunc 3/2 // ▶ (num 1) // ~> math:trunc 5/3 // ▶ (num 1) // ~> math:trunc -3/2 // ▶ (num -1) // ~> math:trunc -5/3 // ▶ (num -1) // ~> math:trunc 1.7 // ▶ (num 1.0) // ~> math:trunc -1.7 // ▶ (num -1.0) // ``` func trunc(n vals.Num) vals.Num { return integerize(n, math.Trunc, func(n *big.Rat) *big.Int { return new(big.Int).Quo(n.Num(), n.Denom()) }) } func integerize(n vals.Num, fnFloat func(float64) float64, fnRat func(*big.Rat) *big.Int) vals.Num { switch n := n.(type) { case int: return n case *big.Int: return n case *big.Rat: if n.Denom().IsInt64() && n.Denom().Int64() == 1 { // Elvish always normalizes *big.Rat with a denominator of 1 to // *big.Int, but we still try to be defensive here. return n.Num() } return fnRat(n) case float64: return fnFloat(n) default: panic("unreachable") } } elvish-0.17.0/pkg/mods/math/math_test.go000066400000000000000000000200521415471104000200600ustar00rootroot00000000000000package math import ( "math" "math/big" "strconv" "testing" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/errs" . "src.elv.sh/pkg/eval/evaltest" ) const ( zeros = "0000000000000000000" // Values that exceed the range of int64, used for testing BigInt. z = "1" + zeros + "0" z1 = "1" + zeros + "1" // z+1 z2 = "1" + zeros + "2" // z+2 ) var minIntString = strconv.Itoa(minInt) func TestMath(t *testing.T) { setup := func(ev *eval.Evaler) { ev.ExtendGlobal(eval.BuildNs().AddNs("math", Ns)) } TestWithSetup(t, setup, That("math:abs 2").Puts(2), That("math:abs -2").Puts(2), That("math:abs "+minIntString).Puts(bigInt(minIntString[1:])), That("math:abs "+z).Puts(bigInt(z)), That("math:abs -"+z).Puts(bigInt(z)), That("math:abs -1/2").Puts(big.NewRat(1, 2)), That("math:abs 1/2").Puts(big.NewRat(1, 2)), That("math:abs 2.1").Puts(2.1), That("math:abs -2.1").Puts(2.1), That("math:ceil 2").Puts(2), That("math:ceil "+z).Puts(bigInt(z)), That("math:ceil 3/2").Puts(2), That("math:ceil -3/2").Puts(-1), That("math:ceil 2.1").Puts(3.0), That("math:ceil -2.1").Puts(-2.0), That("math:floor 2").Puts(2), That("math:floor "+z).Puts(bigInt(z)), That("math:floor 3/2").Puts(1), That("math:floor -3/2").Puts(-2), That("math:floor 2.1").Puts(2.0), That("math:floor -2.1").Puts(-3.0), That("math:round 2").Puts(2), That("math:round "+z).Puts(bigInt(z)), That("math:round 1/3").Puts(0), That("math:round 1/2").Puts(1), That("math:round 2/3").Puts(1), That("math:round -1/3").Puts(0), That("math:round -1/2").Puts(-1), That("math:round -2/3").Puts(-1), That("math:round 2.1").Puts(2.0), That("math:round 2.5").Puts(3.0), That("math:round-to-even 2").Puts(2), That("math:round-to-even "+z).Puts(bigInt(z)), That("math:round-to-even 1/3").Puts(0), That("math:round-to-even 2/3").Puts(1), That("math:round-to-even -1/3").Puts(0), That("math:round-to-even -2/3").Puts(-1), That("math:round-to-even 2.5").Puts(2.0), That("math:round-to-even -2.5").Puts(-2.0), That("math:round-to-even 1/2").Puts(0), That("math:round-to-even 3/2").Puts(2), That("math:round-to-even 5/2").Puts(2), That("math:round-to-even 7/2").Puts(4), That("math:round-to-even -1/2").Puts(0), That("math:round-to-even -3/2").Puts(-2), That("math:round-to-even -5/2").Puts(-2), That("math:round-to-even -7/2").Puts(-4), That("math:trunc 2").Puts(2), That("math:trunc "+z).Puts(bigInt(z)), That("math:trunc 3/2").Puts(1), That("math:trunc -3/2").Puts(-1), That("math:trunc 2.1").Puts(2.0), That("math:trunc -2.1").Puts(-2.0), That("math:is-inf 1.3").Puts(false), That("math:is-inf &sign=0 inf").Puts(true), That("math:is-inf &sign=1 inf").Puts(true), That("math:is-inf &sign=-1 -inf").Puts(true), That("math:is-inf &sign=1 -inf").Puts(false), That("math:is-inf -inf").Puts(true), That("math:is-inf nan").Puts(false), That("math:is-inf 1").Puts(false), That("math:is-inf "+z).Puts(false), That("math:is-inf 1/2").Puts(false), That("math:is-nan 1.3").Puts(false), That("math:is-nan inf").Puts(false), That("math:is-nan nan").Puts(true), That("math:is-nan 1").Puts(false), That("math:is-nan "+z).Puts(false), That("math:is-nan 1/2").Puts(false), That("math:max").Throws( errs.ArityMismatch{What: "arguments", ValidLow: 1, ValidHigh: -1, Actual: 0}, "math:max"), That("math:max 42").Puts(42), That("math:max -3 3 10 -4").Puts(10), That("math:max 2 10 "+z).Puts(bigInt(z)), That("math:max "+z1+" "+z2+" "+z).Puts(bigInt(z2)), That("math:max 1/2 1/3 2/3").Puts(big.NewRat(2, 3)), That("math:max 1.0 2.0").Puts(2.0), That("math:max 3 NaN 5").Puts(math.NaN()), That("math:min").Throws( errs.ArityMismatch{What: "arguments", ValidLow: 1, ValidHigh: -1, Actual: 0}, "math:min"), That("math:min 42").Puts(42), That("math:min -3 3 10 -4").Puts(-4), That("math:min 2 10 "+z).Puts(2), That("math:min "+z1+" "+z2+" "+z).Puts(bigInt(z)), That("math:min 1/2 1/3 2/3").Puts(big.NewRat(1, 3)), That("math:min 1.0 2.0").Puts(1.0), That("math:min 3 NaN 5").Puts(math.NaN()), // base is int, exp is int That("math:pow 2 0").Puts(1), That("math:pow 2 1").Puts(2), That("math:pow 2 -1").Puts(big.NewRat(1, 2)), That("math:pow 2 3").Puts(8), That("math:pow 2 -3").Puts(big.NewRat(1, 8)), // base is *big.Rat, exp is int That("math:pow 2/3 0").Puts(1), That("math:pow 2/3 1").Puts(big.NewRat(2, 3)), That("math:pow 2/3 -1").Puts(big.NewRat(3, 2)), That("math:pow 2/3 3").Puts(big.NewRat(8, 27)), That("math:pow 2/3 -3").Puts(big.NewRat(27, 8)), // exp is *big.Rat That("math:pow 4 1/2").Puts(2.0), // exp is float64 That("math:pow 2 2.0").Puts(4.0), That("math:pow 1/2 2.0").Puts(0.25), // base is float64 That("math:pow 2.0 2").Puts(4.0), // Tests below this line are tests against simple bindings for Go's math package. That("put $math:pi").Puts(math.Pi), That("put $math:e").Puts(math.E), That("math:trunc 2.1").Puts(2.0), That("math:trunc -2.1").Puts(-2.0), That("math:trunc 2.5").Puts(2.0), That("math:trunc -2.5").Puts(-2.0), That("math:trunc (float64 Inf)").Puts(math.Inf(1)), That("math:trunc (float64 NaN)").Puts(math.NaN()), That("math:log $math:e").Puts(1.0), That("math:log 1").Puts(0.0), That("math:log 0").Puts(math.Inf(-1)), That("math:log -1").Puts(math.NaN()), That("math:log10 10.0").Puts(1.0), That("math:log10 100.0").Puts(2.0), That("math:log10 1").Puts(0.0), That("math:log10 0").Puts(math.Inf(-1)), That("math:log10 -1").Puts(math.NaN()), That("math:log2 8").Puts(3.0), That("math:log2 1024.0").Puts(10.0), That("math:log2 1").Puts(0.0), That("math:log2 0").Puts(math.Inf(-1)), That("math:log2 -1").Puts(math.NaN()), That("math:cos 0").Puts(1.0), That("math:cos 1").Puts(math.Cos(1.0)), That("math:cos $math:pi").Puts(-1.0), That("math:cosh 0").Puts(1.0), That("math:cosh inf").Puts(math.Inf(1)), That("math:cosh nan").Puts(math.NaN()), That("math:sin 0").Puts(0.0), That("math:sin 1").Puts(math.Sin(1.0)), That("math:sin $math:pi").Puts(math.Sin(math.Pi)), That("math:sinh 0").Puts(0.0), That("math:sinh inf").Puts(math.Inf(1)), That("math:sinh nan").Puts(math.NaN()), That("math:tan 0").Puts(0.0), That("math:tan 1").Puts(math.Tan(1.0)), That("math:tan $math:pi").Puts(math.Tan(math.Pi)), That("math:tanh 0").Puts(0.0), That("math:tanh inf").Puts(1.0), That("math:tanh nan").Puts(math.NaN()), // This block of tests isn't strictly speaking necessary. But it helps // ensure that we're not just confirming Go statements such as // math.Tan(math.Pi) == math.Tan(math.Pi) // are true. The ops that should return a zero value do not actually // do so. Which illustrates why an approximate match is needed. That("math:cos 1").Puts(Approximately{F: 0.5403023058681397174}), That("math:sin 1").Puts(Approximately{F: 0.8414709848078965066}), That("math:sin $math:pi").Puts(Approximately{F: 0.0}), That("math:tan 1").Puts(Approximately{F: 1.5574077246549023}), That("math:tan $math:pi").Puts(Approximately{F: 0.0}), That("math:sqrt 0").Puts(0.0), That("math:sqrt 4").Puts(2.0), That("math:sqrt -4").Puts(math.NaN()), // Test the inverse trigonometric block of functions. That("math:acos 0").Puts(math.Acos(0)), That("math:acos 1").Puts(math.Acos(1)), That("math:acos 1.00001").Puts(math.NaN()), That("math:asin 0").Puts(math.Asin(0)), That("math:asin 1").Puts(math.Asin(1)), That("math:asin 1.00001").Puts(math.NaN()), That("math:atan 0").Puts(math.Atan(0)), That("math:atan 1").Puts(math.Atan(1)), That("math:atan inf").Puts(math.Pi/2), // Test the inverse hyperbolic trigonometric block of functions. That("math:acosh 0").Puts(math.Acosh(0)), That("math:acosh 1").Puts(math.Acosh(1)), That("math:acosh nan").Puts(math.NaN()), That("math:asinh 0").Puts(math.Asinh(0)), That("math:asinh 1").Puts(math.Asinh(1)), That("math:asinh inf").Puts(math.Inf(1)), That("math:atanh 0").Puts(math.Atanh(0)), That("math:atanh 1").Puts(math.Inf(1)), ) } func bigInt(s string) *big.Int { z, ok := new(big.Int).SetString(s, 0) if !ok { panic("cannot parse as big int: " + s) } return z } elvish-0.17.0/pkg/mods/mods.go000066400000000000000000000013101415471104000160750ustar00rootroot00000000000000// Package mods collects standard library modules. package mods import ( "src.elv.sh/pkg/eval" "src.elv.sh/pkg/mods/epm" "src.elv.sh/pkg/mods/file" "src.elv.sh/pkg/mods/math" "src.elv.sh/pkg/mods/path" "src.elv.sh/pkg/mods/platform" "src.elv.sh/pkg/mods/re" "src.elv.sh/pkg/mods/readlinebinding" "src.elv.sh/pkg/mods/str" ) // AddTo adds all standard library modules to the Evaler. func AddTo(ev *eval.Evaler) { ev.AddModule("math", math.Ns) ev.AddModule("path", path.Ns) ev.AddModule("platform", platform.Ns) ev.AddModule("re", re.Ns) ev.AddModule("str", str.Ns) ev.AddModule("file", file.Ns) ev.BundledModules["epm"] = epm.Code ev.BundledModules["readline-binding"] = readlinebinding.Code } elvish-0.17.0/pkg/mods/path/000077500000000000000000000000001415471104000155455ustar00rootroot00000000000000elvish-0.17.0/pkg/mods/path/path.go000066400000000000000000000176411415471104000170410ustar00rootroot00000000000000// Package path provides functions for manipulating filesystem path names. package path import ( "os" "path/filepath" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/errs" ) // Ns is the namespace for the re: module. var Ns = eval.BuildNsNamed("path"). AddGoFns(map[string]interface{}{ "abs": filepath.Abs, "base": filepath.Base, "clean": filepath.Clean, "dir": filepath.Dir, "ext": filepath.Ext, "eval-symlinks": filepath.EvalSymlinks, "is-abs": filepath.IsAbs, "is-dir": isDir, "is-regular": isRegular, "temp-dir": tempDir, "temp-file": tempFile, }).Ns() //elvdoc:fn abs // // ```elvish // path:abs $path // ``` // // Outputs `$path` converted to an absolute path. // // ```elvish-transcript // ~> cd ~ // ~> path:abs bin // ▶ /home/user/bin // ``` //elvdoc:fn base // // ```elvish // path:base $path // ``` // // Outputs the last element of `$path`. This is analogous to the POSIX `basename` command. See the // [Go documentation](https://pkg.go.dev/path/filepath#Base) for more details. // // ```elvish-transcript // ~> path:base ~/bin // ▶ bin // ``` //elvdoc:fn clean // // ```elvish // path:clean $path // ``` // // Outputs the shortest version of `$path` equivalent to `$path` by purely lexical processing. This // is most useful for eliminating unnecessary relative path elements such as `.` and `..` without // asking the OS to evaluate the path name. See the [Go // documentation](https://pkg.go.dev/path/filepath#Clean) for more details. // // ```elvish-transcript // ~> path:clean ./../bin // ▶ ../bin // ``` //elvdoc:fn dir // // ```elvish // path:dir $path // ``` // // Outputs all but the last element of `$path`, typically the path's enclosing directory. See the // [Go documentation](https://pkg.go.dev/path/filepath#Dir) for more details. This is analogous to // the POSIX `dirname` command. // // ```elvish-transcript // ~> path:dir /a/b/c/something // ▶ /a/b/c // ``` //elvdoc:fn ext // // ```elvish // ext $path // ``` // // Outputs the file name extension used by `$path` (including the separating period). If there is no // extension the empty string is output. See the [Go // documentation](https://pkg.go.dev/path/filepath#Ext) for more details. // // ```elvish-transcript // ~> path:ext hello.elv // ▶ .elv // ``` //elvdoc:fn is-abs // // ```elvish // is-abs $path // ``` // // Outputs `$true` if the path is an absolute path. Note that platforms like Windows have different // rules than UNIX like platforms for what constitutes an absolute path. See the [Go // documentation](https://pkg.go.dev/path/filepath#IsAbs) for more details. // // ```elvish-transcript // ~> path:is-abs hello.elv // ▶ false // ~> path:is-abs /hello.elv // ▶ true // ``` //elvdoc:fn eval-symlinks // // ```elvish-transcript // ~> mkdir bin // ~> ln -s bin sbin // ~> path:eval-symlinks ./sbin/a_command // ▶ bin/a_command // ``` // // Outputs `$path` after resolving any symbolic links. If `$path` is relative the result will be // relative to the current directory, unless one of the components is an absolute symbolic link. // This function calls `path:clean` on the result before outputting it. This is analogous to the // external `realpath` or `readlink` command found on many systems. See the [Go // documentation](https://pkg.go.dev/path/filepath#EvalSymlinks) for more details. //elvdoc:fn is-dir // // ```elvish // is-dir &follow-symlink=$false $path // ``` // // Outputs `$true` if the path resolves to a directory. If the final element of the path is a // symlink, even if it points to a directory, it still outputs `$false` since a symlink is not a // directory. Setting option `&follow-symlink` to true will cause the last element of the path, if // it is a symlink, to be resolved before doing the test. // // ```elvish-transcript // ~> touch not-a-dir // ~> path:is-dir not-a-dir // ▶ false // ~> path:is-dir /tmp // ▶ true // ``` // // @cf path:is-regular type isOpts struct{ FollowSymlink bool } func (opts *isOpts) SetDefaultOptions() {} func isDir(opts isOpts, path string) bool { var fi os.FileInfo var err error if opts.FollowSymlink { fi, err = os.Stat(path) } else { fi, err = os.Lstat(path) } return err == nil && fi.Mode().IsDir() } //elvdoc:fn is-regular // // ```elvish // is-regular &follow-symlink=$false $path // ``` // // Outputs `$true` if the path resolves to a regular file. If the final element of the path is a // symlink, even if it points to a regular file, it still outputs `$false` since a symlink is not a // regular file. Setting option `&follow-symlink` to true will cause the last element of the path, // if it is a symlink, to be resolved before doing the test. // // **Note:** This isn't named `is-file` because a UNIX file may be a "bag of bytes" or may be a // named pipe, device special file (e.g. `/dev/tty`), etc. // // ```elvish-transcript // ~> touch not-a-dir // ~> path:is-regular not-a-dir // ▶ true // ~> path:is-dir /tmp // ▶ false // ``` // // @cf path:is-dir func isRegular(opts isOpts, path string) bool { var fi os.FileInfo var err error if opts.FollowSymlink { fi, err = os.Stat(path) } else { fi, err = os.Lstat(path) } return err == nil && fi.Mode().IsRegular() } //elvdoc:fn temp-dir // // ```elvish // temp-dir &dir='' $pattern? // ``` // // Creates a new directory and outputs its name. // // The &dir option determines where the directory will be created; if it is an // empty string (the default), a system-dependent directory suitable for storing // temporary files will be used. The `$pattern` argument determines the name of // the directory, where the last star will be replaced by a random string; it // defaults to `elvish-*`. // // It is the caller's responsibility to remove the directory if it is intended // to be temporary. // // ```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", ValidLow: 0, ValidHigh: 1, Actual: len(args)} } return os.MkdirTemp(opts.Dir, pattern) } //elvdoc:fn temp-file // // ```elvish // temp-file &dir='' $pattern? // ``` // // Creates a new file and outputs a [file](language.html#file) object opened // for reading and writing. // // The &dir option determines where the file will be created; if it is an // empty string (the default), a system-dependent directory suitable for storing // temporary files will be used. The `$pattern` argument determines the name of // the file, where the last star will be replaced by a random string; it // defaults to `elvish-*`. // // It is the caller's responsibility to close the file with // [`file:close`](file.html#file:close). The caller should also remove the file // if it is intended to be temporary (with `rm $f[name]`). // // ```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", ValidLow: 0, ValidHigh: 1, Actual: len(args)} } return os.CreateTemp(opts.Dir, pattern) } elvish-0.17.0/pkg/mods/path/path_test.go000066400000000000000000000117471415471104000201010ustar00rootroot00000000000000package path import ( "os" "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/mods/file" "src.elv.sh/pkg/testutil" ) var testDir = testutil.Dir{ "d": testutil.Dir{ "f": "", }, } // A regular expression fragment to match the directory part of an absolute // path. QuoteMeta is needed since on Windows filepath.Separator is '\\'. var anyDir = "^.*" + regexp.QuoteMeta(string(filepath.Separator)) func TestPath(t *testing.T) { tmpdir := testutil.InTempDir(t) testutil.ApplyDir(testDir) absPath, err := filepath.Abs("a/b/c.png") if err != nil { panic("unable to convert a/b/c.png to an absolute path") } TestWithSetup(t, importModules, // This block of tests is not meant to be comprehensive. Their primary purpose is to simply // ensure the Elvish command is correctly mapped to the relevant Go function. We assume the // Go function behaves correctly. That("path:abs a/b/c.png").Puts(absPath), That("path:base a/b/d.png").Puts("d.png"), That("path:clean ././x").Puts("x"), That("path:clean a/b/.././c").Puts(filepath.Join("a", "c")), That("path:dir a/b/d.png").Puts(filepath.Join("a", "b")), That("path:ext a/b/e.png").Puts(".png"), That("path:ext a/b/s").Puts(""), That("path:is-abs a/b/s").Puts(false), That("path:is-abs "+absPath).Puts(true), // Elvish "path:" module functions that are not trivial wrappers around a Go stdlib function // should have comprehensive tests below this comment. That("path:is-dir "+tmpdir).Puts(true), That("path:is-dir d").Puts(true), That("path:is-dir d/f").Puts(false), That("path:is-dir bad").Puts(false), That("path:is-regular "+tmpdir).Puts(false), That("path:is-regular d").Puts(false), That("path:is-regular d/f").Puts(true), That("path:is-regular bad").Puts(false), // Verify the commands for creating temporary filesystem objects work correctly. That("x = (path:temp-dir)", "rmdir $x", "put $x").Puts( MatchingRegexp{Pattern: anyDir + `elvish-.*$`}), That("x = (path:temp-dir 'x-*.y')", "rmdir $x", "put $x").Puts( MatchingRegexp{Pattern: anyDir + `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", ValidLow: 0, ValidHigh: 1, Actual: 2}, "path:temp-dir a b"), That("f = (path:temp-file)", "file:close $f", "put $f[fd]", "rm $f[name]"). Puts(-1), That("f = (path:temp-file)", "put $f[name]", "file:close $f", "rm $f[name]"). Puts(MatchingRegexp{Pattern: anyDir + `elvish-.*$`}), That("f = (path:temp-file 'x-*.y')", "put $f[name]", "file:close $f", "rm $f[name]"). Puts(MatchingRegexp{Pattern: anyDir + `x-.*\.y$`}), That("f = (path:temp-file &dir=. 'x-*.y')", "put $f[name]", "file:close $f", "rm $f[name]"). Puts(MatchingRegexp{Pattern: `^(\.[/\\])?x-.*\.y$`}), That("f = (path:temp-file &dir=.)", "put $f[name]", "file:close $f", "rm $f[name]"). Puts(MatchingRegexp{Pattern: `^(\.[/\\])?elvish-.*$`}), That("path:temp-file a b").Throws( errs.ArityMismatch{What: "arguments", ValidLow: 0, ValidHigh: 1, Actual: 2}, "path:temp-file a b"), ) } var symlinks = []struct { path string target string }{ {"d/s-f", "f"}, {"s-d", "d"}, {"s-d-f", "d/f"}, {"s-bad", "bad"}, } func TestPath_Symlink(t *testing.T) { testutil.InTempDir(t) testutil.ApplyDir(testDir) // testutil.ApplyDir(testDirSymlinks) for _, link := range symlinks { err := os.Symlink(link.target, link.path) if err != nil { // Creating symlinks requires a special permission on Windows. If // the user doesn't have that permission, just skip the whole test. t.Skip(err) } } TestWithSetup(t, importModules, That("path:eval-symlinks d/f").Puts(filepath.Join("d", "f")), That("path:eval-symlinks d/s-f").Puts(filepath.Join("d", "f")), That("path:eval-symlinks s-d/f").Puts(filepath.Join("d", "f")), That("path:eval-symlinks s-bad").Throws(AnyError), That("path:is-dir s-d").Puts(false), That("path:is-dir s-d &follow-symlink").Puts(true), That("path:is-dir s-d-f").Puts(false), That("path:is-dir s-d-f &follow-symlink").Puts(false), That("path:is-dir s-bad").Puts(false), That("path:is-dir s-bad &follow-symlink").Puts(false), That("path:is-dir bad").Puts(false), That("path:is-dir bad &follow-symlink").Puts(false), That("path:is-regular s-d").Puts(false), That("path:is-regular s-d &follow-symlink").Puts(false), That("path:is-regular s-d-f").Puts(false), That("path:is-regular s-d-f &follow-symlink").Puts(true), That("path:is-regular s-bad").Puts(false), That("path:is-regular s-bad &follow-symlink").Puts(false), That("path:is-regular bad").Puts(false), That("path:is-regular bad &follow-symlink").Puts(false), ) } func importModules(ev *eval.Evaler) { ev.ExtendGlobal(eval.BuildNs().AddNs("path", Ns).AddNs("file", file.Ns)) } elvish-0.17.0/pkg/mods/platform/000077500000000000000000000000001415471104000164355ustar00rootroot00000000000000elvish-0.17.0/pkg/mods/platform/platform.go000066400000000000000000000043351415471104000206150ustar00rootroot00000000000000// Package platform exposes variables and functions that deal with the // specific platform being run on, such as the OS name and CPU architecture. package platform import ( "os" "runtime" "strings" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vars" ) //elvdoc:var arch // // The architecture of the platform; e.g. amd64, arm, ppc. // This corresponds to Go's // [`GOARCH`](https://pkg.go.dev/runtime?tab=doc#pkg-constants) constant. // This is read-only. //elvdoc:var os // // The name of the operating system; e.g. darwin (macOS), linux, etc. // This corresponds to Go's // [`GOOS`](https://pkg.go.dev/runtime?tab=doc#pkg-constants) constant. // This is read-only. //elvdoc:var is-unix // // Whether or not the platform is UNIX-like. This includes Linux, macOS // (Darwin), FreeBSD, NetBSD, and OpenBSD. This can be used to decide, for // example, if the `unix` module is usable. // This is read-only. //elvdoc:var is-windows // // Whether or not the platform is Microsoft Windows. // This is read-only. //elvdoc:fn hostname // // ```elvish // platform:hostname &strip-domain=$false // ``` // // Outputs the hostname of the system. If the option `&strip-domain` is `$true`, // strips the part after the first dot. // // This function throws an exception if it cannot determine the hostname. It is // implemented using Go's [`os.Hostname`](https://golang.org/pkg/os/#Hostname). // // Examples: // // ```elvish-transcript // ~> platform:hostname // ▶ lothlorien.elv.sh // ~> platform:hostname &strip-domain=$true // ▶ lothlorien // ``` var osHostname = os.Hostname // to allow mocking in unit tests type hostnameOpt struct{ StripDomain bool } func (o *hostnameOpt) SetDefaultOptions() {} func hostname(opts hostnameOpt) (string, error) { hostname, err := osHostname() if err != nil { return "", err } if !opts.StripDomain { return hostname, nil } parts := strings.SplitN(hostname, ".", 2) return parts[0], nil } var Ns = eval.BuildNsNamed("platform"). AddVars(map[string]vars.Var{ "arch": vars.NewReadOnly(runtime.GOARCH), "os": vars.NewReadOnly(runtime.GOOS), "is-unix": vars.NewReadOnly(isUnix), "is-windows": vars.NewReadOnly(isWindows), }). AddGoFns(map[string]interface{}{ "hostname": hostname, }).Ns() elvish-0.17.0/pkg/mods/platform/platform_test.go000066400000000000000000000027201415471104000216500ustar00rootroot00000000000000package platform import ( "errors" "runtime" "testing" "src.elv.sh/pkg/eval" . "src.elv.sh/pkg/eval/evaltest" ) const ( testHostname = "mach1.domain.tld" testMachname = "mach1" ) var ( hostnameFail = true errNoHostname = errors.New("hostname cannot be determined") ) func hostnameMock() (string, error) { if hostnameFail { hostnameFail = false return "", errNoHostname } return testHostname, nil } func TestPlatform(t *testing.T) { savedOsHostname := osHostname osHostname = hostnameMock hostnameFail = true defer func() { osHostname = savedOsHostname }() setup := func(ev *eval.Evaler) { ev.ExtendGlobal(eval.BuildNs().AddNs("platform", Ns)) } TestWithSetup(t, setup, That(`put $platform:arch`).Puts(runtime.GOARCH), That(`put $platform:os`).Puts(runtime.GOOS), That(`put $platform:is-windows`).Puts(runtime.GOOS == "windows"), That(`put $platform:is-unix`).Puts( // Convert to bool type explicitly, to workaround gccgo bug. // https://github.com/golang/go/issues/40152 // TODO(zhsj): remove workaround after gcc 11 is the default in CI. bool(runtime.GOOS != "windows" && runtime.GOOS != "plan9" && runtime.GOOS != "js")), // The first time we invoke the mock it acts as if we can't determine // the hostname. Make sure that is turned into the expected exception. That(`platform:hostname`).Throws(errNoHostname), That(`platform:hostname`).Puts(testHostname), That(`platform:hostname &strip-domain`).Puts(testMachname), ) } elvish-0.17.0/pkg/mods/platform/unix.go000066400000000000000000000002051415471104000177440ustar00rootroot00000000000000//go:build !windows && !plan9 && !js // +build !windows,!plan9,!js package platform const ( isUnix = true isWindows = false ) elvish-0.17.0/pkg/mods/platform/windows.go000066400000000000000000000001471415471104000204600ustar00rootroot00000000000000//go:build windows // +build windows package platform const ( isUnix = false isWindows = true ) elvish-0.17.0/pkg/mods/re/000077500000000000000000000000001415471104000152175ustar00rootroot00000000000000elvish-0.17.0/pkg/mods/re/match.go000066400000000000000000000004321415471104000166410ustar00rootroot00000000000000package re import ( "src.elv.sh/pkg/eval/vals" ) type matchStruct struct { Text string Start int End int Groups vals.List } func (matchStruct) IsStructMap() {} type submatchStruct struct { Text string Start int End int } func (submatchStruct) IsStructMap() {} elvish-0.17.0/pkg/mods/re/re.go000066400000000000000000000163361415471104000161650ustar00rootroot00000000000000// Package re implements a regular expression module. package re import ( "fmt" "regexp" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/persistent/vector" ) // Ns is the namespace for the re: module. var Ns = eval.BuildNsNamed("re"). AddGoFns(map[string]interface{}{ "quote": regexp.QuoteMeta, "match": match, "find": find, "replace": replace, "split": split, }).Ns() //elvdoc:fn quote // // ```elvish // re:quote $string // ``` // // Quote `$string` for use in a pattern. Examples: // // ```elvish-transcript // ~> re:quote a.txt // ▶ a\.txt // ~> re:quote '(*)' // ▶ '\(\*\)' // ``` //elvdoc:fn match // // ```elvish // re:match &posix=$false $pattern $source // ``` // // Determine whether `$pattern` matches `$source`. The pattern is not anchored. // Examples: // // ```elvish-transcript // ~> re:match . xyz // ▶ $true // ~> re:match . '' // ▶ $false // ~> re:match '[a-z]' A // ▶ $false // ``` type matchOpts struct{ Posix bool } func (*matchOpts) SetDefaultOptions() {} func match(opts matchOpts, argPattern, source string) (bool, error) { pattern, err := makePattern(argPattern, opts.Posix, false) if err != nil { return false, err } return pattern.MatchString(source), nil } //elvdoc:fn find // // ```elvish // re:find &posix=$false &longest=$false &max=-1 $pattern $source // ``` // // Find all matches of `$pattern` in `$source`. // // Each match is represented by a map-like value `$m`; `$m[text]`, `$m[start]` and // `$m[end]` are the text, start and end positions (as byte indices into `$source`) // of the match; `$m[groups]` is a list of submatches for capture groups in the // pattern. A submatch has a similar structure to a match, except that it does not // have a `group` key. The entire pattern is an implicit capture group, and it // always appears first. // // Examples: // // ```elvish-transcript // ~> re:find . ab // ▶ [&text=a &start=0 &end=1 &groups=[[&text=a &start=0 &end=1]]] // ▶ [&text=b &start=1 &end=2 &groups=[[&text=b &start=1 &end=2]]] // ~> re:find '[A-Z]([0-9])' 'A1 B2' // ▶ [&text=A1 &start=0 &end=2 &groups=[[&text=A1 &start=0 &end=2] [&text=1 &start=1 &end=2]]] // ▶ [&text=B2 &start=3 &end=5 &groups=[[&text=B2 &start=3 &end=5] [&text=2 &start=4 &end=5]]] // ``` // Struct for holding options to find. Also used by split. type findOpts struct { Posix bool Longest bool Max int } func (o *findOpts) SetDefaultOptions() { o.Max = -1 } func find(fm *eval.Frame, opts findOpts, argPattern, source string) error { out := fm.ValueOutput() pattern, err := makePattern(argPattern, opts.Posix, opts.Longest) if err != nil { return err } matches := pattern.FindAllSubmatchIndex([]byte(source), opts.Max) for _, match := range matches { start, end := match[0], match[1] groups := vector.Empty for i := 0; i < len(match); i += 2 { start, end := match[i], match[i+1] text := "" // FindAllSubmatchIndex may return negative indices to indicate // that the pattern didn't appear in the text. if start >= 0 && end >= 0 { text = source[start:end] } groups = groups.Cons(submatchStruct{text, start, end}) } err := out.Put(matchStruct{source[start:end], start, end, groups}) if err != nil { return err } } return nil } //elvdoc:fn replace // // ```elvish // re:replace &posix=$false &longest=$false &literal=$false $pattern $repl $source // ``` // // Replace all occurrences of `$pattern` in `$source` with `$repl`. // // The replacement `$repl` can be any of the following: // // - A string-typed replacement template. The template can use `$name` or // `${name}` patterns to refer to capture groups, where `name` consists of // letters, digits and underscores. A purely numeric patterns like `$1` // refers to the capture group with the corresponding index; other names // refer to capture groups named with the `(?P...)`) syntax. // // In the `$name` form, the name is taken to be as long as possible; `$1` is // equivalent to `${1x}`, not `${1}x`; `$10` is equivalent to `${10}`, not `${1}0`. // // To insert a literal `$`, use `$$`. // // - A function that takes a string argument and outputs a string. For each // match, the function is called with the content of the match, and its output // is used as the replacement. // // If `$literal` is true, `$repl` must be a string and is treated literally instead // of as a pattern. // // Example: // // ```elvish-transcript // ~> re:replace '(ba|z)sh' '${1}SH' 'bash and zsh' // ▶ 'baSH and zSH' // ~> re:replace '(ba|z)sh' elvish 'bash and zsh rock' // ▶ 'elvish and elvish rock' // ~> re:replace '(ba|z)sh' {|x| put [&bash=BaSh &zsh=ZsH][$x] } 'bash and zsh' // ▶ 'BaSh and ZsH' // ``` type replaceOpts struct { Posix bool Longest bool Literal bool } func (*replaceOpts) SetDefaultOptions() {} func replace(fm *eval.Frame, opts replaceOpts, argPattern string, argRepl interface{}, source string) (string, error) { pattern, err := makePattern(argPattern, opts.Posix, opts.Longest) if err != nil { return "", err } if opts.Literal { repl, ok := argRepl.(string) if !ok { return "", fmt.Errorf( "replacement must be string when literal is set, got %s", vals.Kind(argRepl)) } return pattern.ReplaceAllLiteralString(source, repl), nil } switch repl := argRepl.(type) { case string: return pattern.ReplaceAllString(source, repl), nil case eval.Callable: var errReplace error replFunc := func(s string) string { if errReplace != nil { return "" } values, err := fm.CaptureOutput(func(fm *eval.Frame) error { return repl.Call(fm, []interface{}{s}, eval.NoOpts) }) if err != nil { errReplace = err return "" } if len(values) != 1 { errReplace = fmt.Errorf("replacement function must output exactly one value, got %d", len(values)) return "" } output, ok := values[0].(string) if !ok { errReplace = fmt.Errorf( "replacement function must output one string, got %s", vals.Kind(values[0])) return "" } return output } return pattern.ReplaceAllStringFunc(source, replFunc), errReplace default: return "", fmt.Errorf( "replacement must be string or function, got %s", vals.Kind(argRepl)) } } //elvdoc:fn split // // ```elvish // re:split &posix=$false &longest=$false &max=-1 $pattern $source // ``` // // Split `$source`, using `$pattern` as separators. Examples: // // ```elvish-transcript // ~> re:split : /usr/sbin:/usr/bin:/bin // ▶ /usr/sbin // ▶ /usr/bin // ▶ /bin // ~> re:split &max=2 : /usr/sbin:/usr/bin:/bin // ▶ /usr/sbin // ▶ /usr/bin:/bin // ``` func split(fm *eval.Frame, opts findOpts, argPattern, source string) error { out := fm.ValueOutput() pattern, err := makePattern(argPattern, opts.Posix, opts.Longest) if err != nil { return err } pieces := pattern.Split(source, opts.Max) for _, piece := range pieces { err := out.Put(piece) if err != nil { return err } } return nil } func makePattern(p string, posix, longest bool) (*regexp.Regexp, error) { pattern, err := compile(p, posix) if err != nil { return nil, err } if longest { pattern.Longest() } return pattern, nil } func compile(pattern string, posix bool) (*regexp.Regexp, error) { if posix { return regexp.CompilePOSIX(pattern) } return regexp.Compile(pattern) } elvish-0.17.0/pkg/mods/re/re_test.go000066400000000000000000000053621415471104000172210ustar00rootroot00000000000000package re import ( "testing" "src.elv.sh/pkg/eval" . "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/eval/vals" ) func TestRe(t *testing.T) { setup := func(ev *eval.Evaler) { ev.ExtendGlobal(eval.BuildNs().AddNs("re", Ns)) } TestWithSetup(t, setup, That("re:match . xyz").Puts(true), That("re:match . ''").Puts(false), That("re:match '[a-z]' A").Puts(false), // Invalid pattern in re:match That("re:match '(' x").Throws(AnyError), That("re:find . ab").Puts( matchStruct{"a", 0, 1, vals.MakeList(submatchStruct{"a", 0, 1})}, matchStruct{"b", 1, 2, vals.MakeList(submatchStruct{"b", 1, 2})}, ), That("re:find '[A-Z]([0-9])' 'A1 B2'").Puts( matchStruct{"A1", 0, 2, vals.MakeList( submatchStruct{"A1", 0, 2}, submatchStruct{"1", 1, 2})}, matchStruct{"B2", 3, 5, vals.MakeList( submatchStruct{"B2", 3, 5}, submatchStruct{"2", 4, 5})}, ), // Access to fields in the match StructMap That("put (re:find . a)[text start end groups]"). Puts("a", 0, 1, vals.MakeList(submatchStruct{"a", 0, 1})), // Invalid pattern in re:find That("re:find '(' x").Throws(AnyError), // Without any flag, finds ax That("put (re:find 'a(x|xy)' AaxyZ)[text]").Puts("ax"), // With &longest, finds axy That("put (re:find &longest 'a(x|xy)' AaxyZ)[text]").Puts("axy"), // Basic verification of &posix behavior. That("put (re:find &posix 'a(x|xy)+' AaxyxxxyZ)[text]").Puts("axyxxxy"), // re:find bubbles output error That("re:find . ab >&-").Throws(eval.ErrNoValueOutput), That("re:replace '(ba|z)sh' '${1}SH' 'bash and zsh'").Puts("baSH and zSH"), That("re:replace &literal '(ba|z)sh' '$sh' 'bash and zsh'").Puts("$sh and $sh"), That("re:replace '(ba|z)sh' {|x| put [&bash=BaSh &zsh=ZsH][$x] } 'bash and zsh'").Puts("BaSh and ZsH"), // Invalid pattern in re:replace That("re:replace '(' x bash").Throws(AnyError), That("re:replace &posix '[[:argle:]]' x bash").Throws(AnyError), // Replacement function outputs more than one value That("re:replace x {|x| put a b } xx").Throws(AnyError), // Replacement function outputs non-string value That("re:replace x {|x| put [] } xx").Throws(AnyError), // Replacement is not string or function That("re:replace x [] xx").Throws(AnyError), // Replacement is function when &literal is set That("re:replace &literal x {|_| put y } xx").Throws(AnyError), That("re:split : /usr/sbin:/usr/bin:/bin").Puts("/usr/sbin", "/usr/bin", "/bin"), That("re:split &max=2 : /usr/sbin:/usr/bin:/bin").Puts("/usr/sbin", "/usr/bin:/bin"), // Invalid pattern in re:split That("re:split '(' x").Throws(AnyError), // re:split bubbles output error That("re:split . ab >&-").Throws(eval.ErrNoValueOutput), That("re:quote a.txt").Puts(`a\.txt`), That("re:quote '(*)'").Puts(`\(\*\)`), ) } elvish-0.17.0/pkg/mods/readlinebinding/000077500000000000000000000000001415471104000177275ustar00rootroot00000000000000elvish-0.17.0/pkg/mods/readlinebinding/readline-binding.elv000066400000000000000000000034001415471104000236270ustar00rootroot00000000000000set edit:global-binding[Ctrl-G] = $edit:close-mode~ b={|k f| set edit:insert:binding[$k] = $f } { $b Ctrl-A $edit:move-dot-sol~ $b Ctrl-B $edit:move-dot-left~ $b Ctrl-D { if (> (count $edit:current-command) 0) { edit:kill-rune-right } else { edit:return-eof } } $b Ctrl-E $edit:move-dot-eol~ $b Ctrl-F $edit:move-dot-right~ $b Ctrl-H $edit:kill-rune-left~ $b Ctrl-L { edit:clear } $b Ctrl-N $edit:end-of-history~ # TODO: ^O $b Ctrl-P $edit:history:start~ # TODO: ^S ^T ^X family ^Y ^_ $b Alt-b $edit:move-dot-left-word~ # TODO Alt-c Alt-d $b Alt-f $edit:move-dot-right-word~ # TODO Alt-l Alt-r Alt-u # Ctrl-N and Ctrl-L occupied by readline binding, $b to Alt- instead. $b Alt-n $edit:navigation:start~ $b Alt-l $edit:location:start~ $b Ctrl-t $edit:transpose-rune~ $b Alt-t $edit:transpose-word~ } b={|k f| set edit:completion:binding[$k] = $f } { $b Ctrl-B $edit:completion:left~ $b Ctrl-F $edit:completion:right~ $b Ctrl-N $edit:completion:down~ $b Ctrl-P $edit:completion:up~ } b={|k f| set edit:navigation:binding[$k] = $f } { $b Ctrl-B $edit:navigation:left~ $b Ctrl-F $edit:navigation:right~ $b Ctrl-N $edit:navigation:down~ $b Ctrl-P $edit:navigation:up~ $b Alt-f $edit:navigation:trigger-filter~ } b={|k f| set edit:history:binding[$k] = $f } { $b Ctrl-N $edit:history:down-or-quit~ $b Ctrl-P $edit:history:up~ } b={|k f| set edit:listing:binding[$k] = $f } { $b Ctrl-N $edit:listing:down~ $b Ctrl-P $edit:listing:up~ $b Ctrl-V $edit:listing:page-down~ $b Alt-v $edit:listing:page-up~ } b={|k f| set edit:histlist:binding[$k] = $f } { $b Alt-d $edit:histlist:toggle-dedup~ } elvish-0.17.0/pkg/mods/readlinebinding/readlinebinding.go000066400000000000000000000002341415471104000233730ustar00rootroot00000000000000package readlinebinding import _ "embed" // Code contains the source code of the readline-binding module. //go:embed readline-binding.elv var Code string elvish-0.17.0/pkg/mods/readlinebinding/readlinebinding_test.go000066400000000000000000000010261415471104000244320ustar00rootroot00000000000000package readlinebinding_test import ( "os" "testing" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/edit" "src.elv.sh/pkg/eval" . "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/mods" ) func TestReadlineBinding(t *testing.T) { // A smoke test to ensure that the readline-binding module has no errors. TestWithSetup(t, func(ev *eval.Evaler) { mods.AddTo(ev) ed := edit.NewEditor(cli.NewTTY(os.Stdin, os.Stderr), ev, nil) ev.ExtendBuiltin(eval.BuildNs().AddNs("edit", ed)) }, That("use readline-binding").DoesNothing(), ) } elvish-0.17.0/pkg/mods/store/000077500000000000000000000000001415471104000157455ustar00rootroot00000000000000elvish-0.17.0/pkg/mods/store/store.go000066400000000000000000000057321415471104000174370ustar00rootroot00000000000000package store import ( "src.elv.sh/pkg/eval" "src.elv.sh/pkg/store/storedefs" ) //elvdoc:fn next-cmd-seq // // ```elvish // store:next-cmd-seq // ``` // // Outputs the sequence number that will be used for the next entry of the // command history. //elvdoc:fn add-cmd // // ```elvish // store:add-cmd $text // ``` // // Adds an entry to the command history with the given content. Outputs its // sequence number. //elvdoc:fn del-cmd // // ```elvish // store:del-cmd $seq // ``` // // Deletes the command history entry with the given sequence number. // // **NOTE**: This command only deletes the entry from the persistent store. When // deleting an entry that was added in the current session, the deletion will // not take effect for the current session, since the entry still exists in the // in-memory per-session history. //elvdoc:fn cmd // // ```elvish // store:cmd $seq // ``` // // Outputs the content of the command history entry with the given sequence // number. //elvdoc:fn cmds // // ```elvish // store:cmds $from $upto // ``` // // Outputs all command history entries with sequence numbers between `$from` // (inclusive) and `$upto` (exclusive). Use -1 for `$upto` to not set an upper // bound. // // Each entry is represented by a pseudo-map with fields `text` and `seq`. //elvdoc:fn add-dir // // ```elvish // store:add-dir $path // ``` // // Adds a path to the directory history. This will also cause the scores of all // other directories to decrease. //elvdoc:fn del-dir // // ```elvish // store:del-dir $path // ``` // // Deletes a path from the directory history. This has no impact on the scores // of other directories. //elvdoc:fn dirs // // ```elvish // store:dirs // ``` // // Outputs all directory history entries, in decreasing order of score. // // Each entry is represented by a pseudo-map with fields `path` and `score`. //elvdoc:fn shared-var // // ```elvish // store:shared-var $name // ``` // // Outputs the value of the shared variable with the given name. Throws an error // if the shared variable doesn't exist. //elvdoc:fn set-shared-var // // ```elvish // store:set-shared-var $name $value // ``` // // Sets the value of the shared variable with the given name, creating it if it // doesn't exist. The value must be a string. //elvdoc:fn del-shared-var // // ```elvish // store:del-shared-var $name // ``` // // Deletes the shared variable with the given name. func Ns(s storedefs.Store) *eval.Ns { return eval.BuildNsNamed("store"). AddGoFns(map[string]interface{}{ "next-cmd-seq": s.NextCmdSeq, "add-cmd": s.AddCmd, "del-cmd": s.DelCmd, "cmd": s.Cmd, "cmds": s.CmdsWithSeq, "next-cmd": s.NextCmd, "prev-cmd": s.PrevCmd, "add-dir": func(dir string) error { return s.AddDir(dir, 1) }, "del-dir": s.DelDir, "dirs": func() ([]storedefs.Dir, error) { return s.Dirs(storedefs.NoBlacklist) }, "shared-var": s.SharedVar, "set-shared-var": s.SetSharedVar, "del-shared-var": s.DelSharedVar, }).Ns() } elvish-0.17.0/pkg/mods/store/store_test.go000066400000000000000000000037231415471104000204740ustar00rootroot00000000000000package store import ( "testing" "src.elv.sh/pkg/eval" . "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/store" "src.elv.sh/pkg/store/storedefs" "src.elv.sh/pkg/testutil" ) func TestStore(t *testing.T) { testutil.InTempDir(t) s, err := store.NewStore("db") if err != nil { t.Fatal(err) } ns := Ns(s) setup := func(ev *eval.Evaler) { ev.ExtendGlobal(eval.BuildNs().AddNs("store", ns)) } TestWithSetup(t, setup, // Add commands That("store:next-cmd-seq").Puts(1), That("store:add-cmd foo").Puts(1), That("store:add-cmd bar").Puts(2), That("store:add-cmd baz").Puts(3), That("store:next-cmd-seq").Puts(4), // Query commands That("store:cmd 1").Puts("foo"), That("store:cmds 1 4").Puts(cmd("foo", 1), cmd("bar", 2), cmd("baz", 3)), That("store:cmds 2 3").Puts(cmd("bar", 2)), That("store:next-cmd 1 f").Puts(cmd("foo", 1)), That("store:prev-cmd 3 b").Puts(cmd("bar", 2)), // Delete commands That("store:del-cmd 2").DoesNothing(), That("store:cmds 1 4").Puts(cmd("foo", 1), cmd("baz", 3)), // Add directories That("store:add-dir /foo").DoesNothing(), That("store:add-dir /bar").DoesNothing(), // Query directories That("store:dirs").Puts( dir("/bar", store.DirScoreIncrement), dir("/foo", store.DirScoreIncrement*store.DirScoreDecay)), // Delete directories That("store:del-dir /foo").DoesNothing(), That("store:dirs").Puts( dir("/bar", store.DirScoreIncrement)), // Set shared variables That("store:set-shared-var foo lorem").DoesNothing(), That("store:set-shared-var bar ipsum").DoesNothing(), // Query shared variables That("store:shared-var foo").Puts("lorem"), That("store:shared-var bar").Puts("ipsum"), // Delete shared variables That("store:del-shared-var foo").DoesNothing(), That("store:shared-var foo").Throws(AnyError), ) } func cmd(s string, i int) storedefs.Cmd { return storedefs.Cmd{Text: s, Seq: i} } func dir(s string, f float64) storedefs.Dir { return storedefs.Dir{Path: s, Score: f} } elvish-0.17.0/pkg/mods/str/000077500000000000000000000000001415471104000154215ustar00rootroot00000000000000elvish-0.17.0/pkg/mods/str/str.go000066400000000000000000000314641415471104000165700ustar00rootroot00000000000000// Package str exposes functionality from Go's strings package as an Elvish // module. package str import ( "bytes" "fmt" "strconv" "strings" "unicode" "unicode/utf8" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" ) var Ns = eval.BuildNsNamed("str"). AddGoFns(map[string]interface{}{ "compare": strings.Compare, "contains": strings.Contains, "contains-any": strings.ContainsAny, "count": strings.Count, "equal-fold": strings.EqualFold, // TODO: Fields, FieldsFunc "from-codepoints": fromCodepoints, "from-utf8-bytes": fromUtf8Bytes, "has-prefix": strings.HasPrefix, "has-suffix": strings.HasSuffix, "index": strings.Index, "index-any": strings.IndexAny, // TODO: IndexFunc "join": join, "last-index": strings.LastIndex, // TODO: LastIndexFunc, Map, Repeat "replace": replace, "split": split, // TODO: SplitAfter "title": strings.Title, "to-codepoints": toCodepoints, "to-lower": strings.ToLower, "to-title": strings.ToTitle, "to-upper": strings.ToUpper, "to-utf8-bytes": toUtf8Bytes, // TODO: ToLowerSpecial, ToTitleSpecial, ToUpperSpecial "trim": strings.Trim, "trim-left": strings.TrimLeft, "trim-right": strings.TrimRight, // TODO: TrimLeft,Right}Func "trim-space": strings.TrimSpace, "trim-prefix": strings.TrimPrefix, "trim-suffix": strings.TrimSuffix, }).Ns() //elvdoc:fn compare // // ```elvish // str:compare $a $b // ``` // // Compares two strings and output an integer that will be 0 if a == b, // -1 if a < b, and +1 if a > b. // // ```elvish-transcript // ~> str:compare a a // ▶ 0 // ~> str:compare a b // ▶ -1 // ~> str:compare b a // ▶ 1 // ``` //elvdoc:fn contains // // ```elvish // str:contains $str $substr // ``` // // Outputs whether `$str` contains `$substr` as a substring. // // ```elvish-transcript // ~> str:contains abcd x // ▶ $false // ~> str:contains abcd bc // ▶ $true // ``` //elvdoc:fn contains-any // // ```elvish // str:contains-any $str $chars // ``` // // Outputs whether `$str` contains any Unicode code points in `$chars`. // // ```elvish-transcript // ~> str:contains-any abcd x // ▶ $false // ~> str:contains-any abcd xby // ▶ $true // ``` //elvdoc:fn count // // ```elvish // str:count $str $substr // ``` // // Outputs the number of non-overlapping instances of `$substr` in `$s`. // If `$substr` is an empty string, output 1 + the number of Unicode code // points in `$s`. // // ```elvish-transcript // ~> str:count abcdefabcdef bc // ▶ 2 // ~> str:count abcdef '' // ▶ 7 // ``` //elvdoc:fn equal-fold // // ```elvish // str:equal-fold $str1 $str2 // ``` // // Outputs if `$str1` and `$str2`, interpreted as UTF-8 strings, are equal // under Unicode case-folding. // // ```elvish-transcript // ~> str:equal-fold ABC abc // ▶ $true // ~> str:equal-fold abc ab // ▶ $false // ``` //elvdoc:fn from-codepoints // // ```elvish // str:from-codepoints $number... // ``` // // Outputs a string consisting of the given Unicode codepoints. Example: // // ```elvish-transcript // ~> str:from-codepoints 0x61 // ▶ a // ~> str:from-codepoints 0x4f60 0x597d // ▶ 你好 // ``` // // @cf str:to-codepoints func fromCodepoints(nums ...int) (string, error) { var b bytes.Buffer for _, num := range nums { if num < 0 || num > unicode.MaxRune { return "", errs.OutOfRange{ What: "codepoint", ValidLow: "0", ValidHigh: strconv.Itoa(unicode.MaxRune), Actual: hex(num), } } if !utf8.ValidRune(rune(num)) { return "", errs.BadValue{ What: "argument to str:from-codepoints", Valid: "valid Unicode codepoint", Actual: hex(num), } } b.WriteRune(rune(num)) } return b.String(), nil } func hex(i int) string { if i < 0 { return "-0x" + strconv.FormatInt(-int64(i), 16) } return "0x" + strconv.FormatInt(int64(i), 16) } //elvdoc:fn from-utf8-bytes // // ```elvish // str:from-utf8-bytes $number... // ``` // // Outputs a string consisting of the given Unicode bytes. Example: // // ```elvish-transcript // ~> str:from-utf8-bytes 0x61 // ▶ a // ~> str:from-utf8-bytes 0xe4 0xbd 0xa0 0xe5 0xa5 0xbd // ▶ 你好 // ``` // // @cf str:to-utf8-bytes func fromUtf8Bytes(nums ...int) (string, error) { var b bytes.Buffer for _, num := range nums { if num < 0 || num > 255 { return "", errs.OutOfRange{ What: "byte", ValidLow: "0", ValidHigh: "255", Actual: strconv.Itoa(num)} } b.WriteByte(byte(num)) } if !utf8.Valid(b.Bytes()) { return "", errs.BadValue{ What: "arguments to str:from-utf8-bytes", Valid: "valid UTF-8 sequence", Actual: fmt.Sprint(b.Bytes())} } return b.String(), nil } //elvdoc:fn has-prefix // // ```elvish // str:has-prefix $str $prefix // ``` // // Outputs if `$str` begins with `$prefix`. // // ```elvish-transcript // ~> str:has-prefix abc ab // ▶ $true // ~> str:has-prefix abc bc // ▶ $false // ``` //elvdoc:fn has-suffix // // ```elvish // str:has-suffix $str $suffix // ``` // // Outputs if `$str` ends with `$suffix`. // // ```elvish-transcript // ~> str:has-suffix abc ab // ▶ $false // ~> str:has-suffix abc bc // ▶ $true // ``` //elvdoc:fn index // // ```elvish // str:index $str $substr // ``` // // Outputs the index of the first instance of `$substr` in `$str`, or -1 // if `$substr` is not present in `$str`. // // ```elvish-transcript // ~> str:index abcd cd // ▶ 2 // ~> str:index abcd xyz // ▶ -1 // ``` //elvdoc:fn index-any // // ```elvish // str:index-any $str $chars // ``` // // Outputs the index of the first instance of any Unicode code point // from `$chars` in `$str`, or -1 if no Unicode code point from `$chars` is // present in `$str`. // // ```elvish-transcript // ~> str:index-any "chicken" "aeiouy" // ▶ 2 // ~> str:index-any l33t aeiouy // ▶ -1 // ``` //elvdoc:fn join // // ```elvish // str:join $sep $input-list? // ``` // // Joins inputs with `$sep`. Examples: // // ```elvish-transcript // ~> put lorem ipsum | str:join , // ▶ lorem,ipsum // ~> str:join , [lorem ipsum] // ▶ lorem,ipsum // ~> str:join '' [lorem ipsum] // ▶ loremipsum // ~> str:join '...' [lorem ipsum] // ▶ lorem...ipsum // ``` // // Etymology: Various languages, // [Python](https://docs.python.org/3.6/library/stdtypes.html#str.join). // // @cf str:split func join(sep string, inputs eval.Inputs) (string, error) { var buf bytes.Buffer var errJoin error first := true inputs(func(v interface{}) { if errJoin != nil { return } if s, ok := v.(string); ok { if first { first = false } else { buf.WriteString(sep) } buf.WriteString(s) } else { errJoin = errs.BadValue{ What: "input to str:join", Valid: "string", Actual: vals.Kind(v)} } }) return buf.String(), errJoin } //elvdoc:fn last-index // // ```elvish // str:last-index $str $substr // ``` // // Outputs the index of the last instance of `$substr` in `$str`, // or -1 if `$substr` is not present in `$str`. // // ```elvish-transcript // ~> str:last-index "elven speak elvish" elv // ▶ 12 // ~> str:last-index "elven speak elvish" romulan // ▶ -1 // ``` //elvdoc:fn replace // // ```elvish // str:replace &max=-1 $old $repl $source // ``` // // Replaces all occurrences of `$old` with `$repl` in `$source`. If `$max` is // non-negative, it determines the max number of substitutions. // // **Note**: This command does not support searching by regular expressions, `$old` // is always interpreted as a plain string. Use [re:replace](re.html#re:replace) if // you need to search by regex. type maxOpt struct{ Max int } func (o *maxOpt) SetDefaultOptions() { o.Max = -1 } func replace(opts maxOpt, old, repl, s string) string { return strings.Replace(s, old, repl, opts.Max) } //elvdoc:fn split // // ```elvish // str:split &max=-1 $sep $string // ``` // // Splits `$string` by `$sep`. If `$sep` is an empty string, split it into // codepoints. // // If the `&max` option is non-negative, stops after producing the maximum // number of results. // // ```elvish-transcript // ~> str:split , lorem,ipsum // ▶ lorem // ▶ ipsum // ~> str:split '' 你好 // ▶ 你 // ▶ 好 // ~> str:split &max=2 ' ' 'a b c d' // ▶ a // ▶ 'b c d' // ``` // // **Note**: This command does not support splitting by regular expressions, // `$sep` is always interpreted as a plain string. Use [re:split](re.html#re:split) // if you need to split by regex. // // Etymology: Various languages, in particular // [Python](https://docs.python.org/3.6/library/stdtypes.html#str.split). // // @cf str:join func split(fm *eval.Frame, opts maxOpt, sep, s string) error { out := fm.ValueOutput() parts := strings.SplitN(s, sep, opts.Max) for _, p := range parts { err := out.Put(p) if err != nil { return err } } return nil } //elvdoc:fn title // // ```elvish // str:title $str // ``` // // Outputs `$str` with all Unicode letters that begin words mapped to their // Unicode title case. // // ```elvish-transcript // ~> str:title "her royal highness" // ▶ Her Royal Highness // ``` //elvdoc:fn to-codepoints // // ```elvish // str:to-codepoints $string // ``` // // Outputs value of each codepoint in `$string`, in hexadecimal. Examples: // // ```elvish-transcript // ~> str:to-codepoints a // ▶ 0x61 // ~> str:to-codepoints 你好 // ▶ 0x4f60 // ▶ 0x597d // ``` // // The output format is subject to change. // // @cf str:from-codepoints func toCodepoints(fm *eval.Frame, s string) error { out := fm.ValueOutput() for _, r := range s { err := out.Put("0x" + strconv.FormatInt(int64(r), 16)) if err != nil { return err } } return nil } //elvdoc:fn to-lower // // ```elvish // str:to-lower $str // ``` // // Outputs `$str` with all Unicode letters mapped to their lower-case // equivalent. // // ```elvish-transcript // ~> str:to-lower 'ABC!123' // ▶ abc!123 // ``` //elvdoc:fn to-utf8-bytes // // ```elvish // str:to-utf8-bytes $string // ``` // // Outputs value of each byte in `$string`, in hexadecimal. Examples: // // ```elvish-transcript // ~> str:to-utf8-bytes a // ▶ 0x61 // ~> str:to-utf8-bytes 你好 // ▶ 0xe4 // ▶ 0xbd // ▶ 0xa0 // ▶ 0xe5 // ▶ 0xa5 // ▶ 0xbd // ``` // // The output format is subject to change. // // @cf str:from-utf8-bytes func toUtf8Bytes(fm *eval.Frame, s string) error { out := fm.ValueOutput() for _, r := range []byte(s) { err := out.Put("0x" + strconv.FormatInt(int64(r), 16)) if err != nil { return err } } return nil } //elvdoc:fn to-title // // ```elvish // str:to-title $str // ``` // // Outputs `$str` with all Unicode letters mapped to their Unicode title case. // // ```elvish-transcript // ~> str:to-title "her royal highness" // ▶ HER ROYAL HIGHNESS // ~> str:to-title "хлеб" // ▶ ХЛЕБ // ``` //elvdoc:fn to-upper // // ```elvish // str:to-upper // ``` // // Outputs `$str` with all Unicode letters mapped to their upper-case // equivalent. // // ```elvish-transcript // ~> str:to-upper 'abc!123' // ▶ ABC!123 // ``` //elvdoc:fn trim // // ```elvish // str:trim $str $cutset // ``` // // Outputs `$str` with all leading and trailing Unicode code points contained // in `$cutset` removed. // // ```elvish-transcript // ~> str:trim "¡¡¡Hello, Elven!!!" "!¡" // ▶ 'Hello, Elven' // ``` //elvdoc:fn trim-left // // ```elvish // str:trim-left $str $cutset // ``` // // Outputs `$str` with all leading Unicode code points contained in `$cutset` // removed. To remove a prefix string use [`str:trim-prefix`](#str:trim-prefix). // // ```elvish-transcript // ~> str:trim-left "¡¡¡Hello, Elven!!!" "!¡" // ▶ 'Hello, Elven!!!' // ``` //elvdoc:fn trim-prefix // // ```elvish // str:trim-prefix $str $prefix // ``` // // Outputs `$str` minus the leading `$prefix` string. If `$str` doesn't begin // with `$prefix`, `$str` is output unchanged. // // ```elvish-transcript // ~> str:trim-prefix "¡¡¡Hello, Elven!!!" "¡¡¡Hello, " // ▶ Elven!!! // ~> str:trim-prefix "¡¡¡Hello, Elven!!!" "¡¡¡Hola, " // ▶ '¡¡¡Hello, Elven!!!' // ``` //elvdoc:fn trim-right // // ```elvish // str:trim-right $str $cutset // ``` // // Outputs `$str` with all leading Unicode code points contained in `$cutset` // removed. To remove a suffix string use [`str:trim-suffix`](#str:trim-suffix). // // ```elvish-transcript // ~> str:trim-right "¡¡¡Hello, Elven!!!" "!¡" // ▶ '¡¡¡Hello, Elven' // ``` //elvdoc:fn trim-space // // ```elvish // str:trim-space $str // ``` // // Outputs `$str` with all leading and trailing white space removed as defined // by Unicode. // // ```elvish-transcript // ~> str:trim-space " \t\n Hello, Elven \n\t\r\n" // ▶ 'Hello, Elven' // ``` //elvdoc:fn trim-suffix // // ```elvish // str:trim-suffix $str $suffix // ``` // // Outputs `$str` minus the trailing `$suffix` string. If `$str` doesn't end // with `$suffix`, `$str` is output unchanged. // // ```elvish-transcript // ~> str:trim-suffix "¡¡¡Hello, Elven!!!" ", Elven!!!" // ▶ ¡¡¡Hello // ~> str:trim-suffix "¡¡¡Hello, Elven!!!" ", Klingons!!!" // ▶ '¡¡¡Hello, Elven!!!' // ``` elvish-0.17.0/pkg/mods/str/str_test.go000066400000000000000000000127641415471104000176310ustar00rootroot00000000000000package str import ( "fmt" "strconv" "testing" "unicode" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/errs" . "src.elv.sh/pkg/eval/evaltest" ) func TestStr(t *testing.T) { setup := func(ev *eval.Evaler) { ev.ExtendGlobal(eval.BuildNs().AddNs("str", Ns)) } TestWithSetup(t, setup, That(`str:compare abc`).Throws(AnyError), That(`str:compare abc abc`).Puts(0), That(`str:compare abc def`).Puts(-1), That(`str:compare def abc`).Puts(1), That(`str:contains abc`).Throws(AnyError), That(`str:contains abcd x`).Puts(false), That(`str:contains abcd bc`).Puts(true), That(`str:contains abcd cde`).Puts(false), That(`str:contains-any abc`).Throws(AnyError), That(`str:contains-any abcd x`).Puts(false), That(`str:contains-any abcd xcy`).Puts(true), That(`str:equal-fold abc`).Throws(AnyError), That(`str:equal-fold ABC abc`).Puts(true), That(`str:equal-fold abc ABC`).Puts(true), That(`str:equal-fold abc A`).Puts(false), That(`str:from-codepoints 0x61`).Puts("a"), That(`str:from-codepoints 0x4f60 0x597d`).Puts("你好"), That(`str:from-codepoints -0x1`).Throws(errs.OutOfRange{ What: "codepoint", ValidLow: "0", ValidHigh: strconv.Itoa(unicode.MaxRune), Actual: "-0x1"}), That(fmt.Sprintf(`str:from-codepoints 0x%x`, unicode.MaxRune+1)).Throws(errs.OutOfRange{ What: "codepoint", ValidLow: "0", ValidHigh: strconv.Itoa(unicode.MaxRune), Actual: hex(unicode.MaxRune + 1)}), That(`str:from-codepoints 0xd800`).Throws(errs.BadValue{ What: "argument to str:from-codepoints", Valid: "valid Unicode codepoint", Actual: "0xd800"}), That(`str:from-utf8-bytes 0x61`).Puts("a"), That(`str:from-utf8-bytes 0xe4 0xbd 0xa0 0xe5 0xa5 0xbd`).Puts("你好"), That(`str:from-utf8-bytes -1`).Throws(errs.OutOfRange{ What: "byte", ValidLow: "0", ValidHigh: "255", Actual: strconv.Itoa(-1)}), That(`str:from-utf8-bytes 256`).Throws(errs.OutOfRange{ What: "byte", ValidLow: "0", ValidHigh: "255", Actual: strconv.Itoa(256)}), That(`str:from-utf8-bytes 0xff 0x3 0xaa`).Throws(errs.BadValue{ What: "arguments to str:from-utf8-bytes", Valid: "valid UTF-8 sequence", Actual: "[255 3 170]"}), That(`str:has-prefix abc`).Throws(AnyError), That(`str:has-prefix abcd ab`).Puts(true), That(`str:has-prefix abcd cd`).Puts(false), That(`str:has-suffix abc`).Throws(AnyError), That(`str:has-suffix abcd ab`).Puts(false), That(`str:has-suffix abcd cd`).Puts(true), That(`str:index abc`).Throws(AnyError), That(`str:index abcd cd`).Puts(2), That(`str:index abcd de`).Puts(-1), That(`str:index-any abc`).Throws(AnyError), That(`str:index-any "chicken" "aeiouy"`).Puts(2), That(`str:index-any l33t aeiouy`).Puts(-1), That(`str:join : [/usr /bin /tmp]`).Puts("/usr:/bin:/tmp"), That(`str:join : ['' a '']`).Puts(":a:"), That(`str:join : [(float64 1) 2]`).Throws( errs.BadValue{What: "input to str:join", Valid: "string", Actual: "number"}), That(`str:last-index abc`).Throws(AnyError), That(`str:last-index "elven speak elvish" "elv"`).Puts(12), That(`str:last-index "elven speak elvish" "romulan"`).Puts(-1), That(`str:replace : / ":usr:bin:tmp"`).Puts("/usr/bin/tmp"), That(`str:replace &max=2 : / :usr:bin:tmp`).Puts("/usr/bin:tmp"), That(`str:split : /usr:/bin:/tmp`).Puts("/usr", "/bin", "/tmp"), That(`str:split : /usr:/bin:/tmp &max=2`).Puts("/usr", "/bin:/tmp"), That(`str:split : a:b >&-`).Throws(eval.ErrNoValueOutput), That(`str:to-codepoints a`).Puts("0x61"), That(`str:to-codepoints 你好`).Puts("0x4f60", "0x597d"), That(`str:to-codepoints 你好 | str:from-codepoints (all)`).Puts("你好"), That(`str:to-codepoints a >&-`).Throws(eval.ErrNoValueOutput), That(`str:to-utf8-bytes a`).Puts("0x61"), That(`str:to-utf8-bytes 你好`).Puts("0xe4", "0xbd", "0xa0", "0xe5", "0xa5", "0xbd"), That(`str:to-utf8-bytes 你好 | str:from-utf8-bytes (all)`).Puts("你好"), That(`str:to-utf8-bytes a >&-`).Throws(eval.ErrNoValueOutput), That(`str:title abc`).Puts("Abc"), That(`str:title "abc def"`).Puts("Abc Def"), That(`str:to-lower abc def`).Throws(AnyError), That(`str:to-lower abc`).Puts("abc"), That(`str:to-lower ABC`).Puts("abc"), That(`str:to-lower ABC def`).Throws(AnyError), That(`str:to-title "her royal highness"`).Puts("HER ROYAL HIGHNESS"), That(`str:to-title "хлеб"`).Puts("ХЛЕБ"), That(`str:to-upper abc`).Puts("ABC"), That(`str:to-upper ABC`).Puts("ABC"), That(`str:to-upper ABC def`).Throws(AnyError), That(`str:trim "¡¡¡Hello, Elven!!!" "!¡"`).Puts("Hello, Elven"), That(`str:trim def`).Throws(AnyError), That(`str:trim-left "¡¡¡Hello, Elven!!!" "!¡"`).Puts("Hello, Elven!!!"), That(`str:trim-left def`).Throws(AnyError), That(`str:trim-prefix "¡¡¡Hello, Elven!!!" "¡¡¡Hello, "`).Puts("Elven!!!"), That(`str:trim-prefix "¡¡¡Hello, Elven!!!" "¡¡¡Hola, "`).Puts("¡¡¡Hello, Elven!!!"), That(`str:trim-prefix def`).Throws(AnyError), That(`str:trim-right "¡¡¡Hello, Elven!!!" "!¡"`).Puts("¡¡¡Hello, Elven"), That(`str:trim-right def`).Throws(AnyError), That(`str:trim-space " \t\n Hello, Elven \n\t\r\n"`).Puts("Hello, Elven"), That(`str:trim-space " \t\n Hello Elven \n\t\r\n"`).Puts("Hello Elven"), That(`str:trim-space " \t\n Hello Elven \n\t\r\n" argle`).Throws(AnyError), That(`str:trim-suffix "¡¡¡Hello, Elven!!!" ", Elven!!!"`).Puts("¡¡¡Hello"), That(`str:trim-suffix "¡¡¡Hello, Elven!!!" ", Klingons!!!"`).Puts("¡¡¡Hello, Elven!!!"), That(`str:trim-suffix "¡¡¡Hello, Elven!!!"`).Throws(AnyError), ) } elvish-0.17.0/pkg/mods/unix/000077500000000000000000000000001415471104000155745ustar00rootroot00000000000000elvish-0.17.0/pkg/mods/unix/non_unix.go000066400000000000000000000011331415471104000177560ustar00rootroot00000000000000//go:build windows || plan9 || js // +build windows plan9 js // Package unix exports an Elvish namespace that contains variables and // functions that deal with features unique to UNIX-like operating systems. On // non-UNIX operating systems it exports an empty namespace. package unix import ( "src.elv.sh/pkg/eval" ) // ExposeUnixNs indicate whether this module should be exposed as a usable // elvish namespace. const ExposeUnixNs = false // Ns is an Elvish namespace that contains variables and functions that deal // with features unique to UNIX-like operating systems. On var Ns = &eval.Ns{} elvish-0.17.0/pkg/mods/unix/umask.go000066400000000000000000000065331415471104000172520ustar00rootroot00000000000000//go:build !windows && !plan9 && !js // +build !windows,!plan9,!js package unix import ( "fmt" "math" "strconv" "sync" "golang.org/x/sys/unix" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" ) const ( validUmaskMsg = "integer in the range [0..0o777]" ) //elvdoc:var umask // // The file mode creation mask. Its value is a string in Elvish octal // representation; e.g. 0o027. This makes it possible to use it in any context // that expects a `$number`. // // When assigning a new value a string is implicitly treated as an octal // number. If that fails the usual rules for interpreting // [numbers](./language.html#number) are used. The following are equivalent: // `unix:umask = 027` and `unix:umask = 0o27`. You can also assign to it a // `float64` data type that has no fractional component. // The assigned value must be within the range [0 ... 0o777], otherwise the // assignment will throw an exception. // // You can do a temporary assignment to affect a single command; e.g. // `umask=077 touch a_file`. After the command completes the old umask will be // restored. **Warning**: Since the umask applies to the entire process, not // individual threads, changing it temporarily in this manner is dangerous if // you are doing anything in parallel. Such as via the // [`peach`](builtin.html#peach) command. // UmaskVariable is a variable whose value always reflects the current file // creation permission mask. Setting it changes the current file creation // permission mask for the process (not an individual thread). type UmaskVariable struct{} var _ vars.Var = UmaskVariable{} // Guard against concurrent fetch and assignment of $unix:umask. This assumes // no other part of the elvish code base will call unix.Umask() as it only // protects against races involving the aforementioned Elvish var. var umaskMutex sync.Mutex // Get returns the current file creation umask as a string. func (UmaskVariable) Get() interface{} { // Note: The seemingly redundant syscall is because the unix.Umask() API // doesn't allow querying the current value without changing it. So ensure // we reinstate the current value. umaskMutex.Lock() defer umaskMutex.Unlock() umask := unix.Umask(0) unix.Umask(umask) return fmt.Sprintf("0o%03o", umask) } // Set changes the current file creation umask. It can be called with a string // (the usual case) or a float64. func (UmaskVariable) Set(v interface{}) error { var umask int switch v := v.(type) { case string: i, err := strconv.ParseInt(v, 8, 0) if err != nil { i, err = strconv.ParseInt(v, 0, 0) if err != nil { return errs.BadValue{ What: "umask", Valid: validUmaskMsg, Actual: vals.ToString(v)} } } umask = int(i) case int: // We don't bother supporting big.Int or bit.Rat because no valid umask value would be // represented by those types. umask = v case float64: intPart, fracPart := math.Modf(v) if fracPart != 0 { return errs.BadValue{ What: "umask", Valid: validUmaskMsg, Actual: vals.ToString(v)} } umask = int(intPart) default: return errs.BadValue{ What: "umask", Valid: validUmaskMsg, Actual: vals.Kind(v)} } if umask < 0 || umask > 0o777 { return errs.OutOfRange{ What: "umask", ValidLow: "0", ValidHigh: "0o777", Actual: fmt.Sprintf("%O", umask)} } umaskMutex.Lock() defer umaskMutex.Unlock() unix.Umask(umask) return nil } elvish-0.17.0/pkg/mods/unix/umask_test.go000066400000000000000000000051211415471104000203010ustar00rootroot00000000000000//go:build !windows && !plan9 && !js // +build !windows,!plan9,!js package unix import ( "testing" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/errs" . "src.elv.sh/pkg/eval/evaltest" ) // Note that this unit test assumes a UNIX environment with a POSIX compatible // /bin/sh program. func TestUmask(t *testing.T) { setup := func(ev *eval.Evaler) { ev.ExtendGlobal(eval.BuildNs().AddNs("unix", Ns)) } TestWithSetup(t, setup, // We have to start with a known umask value. That(`set unix:umask = 022`).Puts(), That(`put $unix:umask`).Puts(`0o022`), // Verify that mutating the value and outputting the new value works. That(`set unix:umask = 23`).Puts(), That(`put $unix:umask`).Puts(`0o023`), That(`set unix:umask = 0o75`).Puts(), That(`put $unix:umask`).Puts(`0o075`), // Verify that a temporary umask change is reverted upon completion of // the command. Both for builtin and external commands. That(`unix:umask=012 put $unix:umask`).Puts(`0o012`), That(`unix:umask=0o23 /bin/sh -c 'umask'`).Prints("0023\n"), That(`unix:umask=56 /bin/sh -c 'umask'`).Prints("0056\n"), That(`put $unix:umask`).Puts(`0o075`), // People won't normally use non-octal bases but make sure these cases // behave sensibly given that Elvish supports number literals with an // explicit base. That(`unix:umask=0x43 /bin/sh -c 'umask'`).Prints("0103\n"), That(`unix:umask=0b001010100 sh -c 'umask'`).Prints("0124\n"), // We should be back to our expected umask given the preceding tests // applied a temporary change to that process attribute. That(`put $unix:umask`).Puts(`0o075`), // An explicit num (int) value is handled correctly. That(`unix:umask=(num 0o123) put $unix:umask`).Puts(`0o123`), // An explicit float64 value is handled correctly. That(`unix:umask=(float64 0o17) put $unix:umask`).Puts(`0o017`), That(`set unix:umask = (float64 123.4)`).Throws( errs.BadValue{What: "umask", Valid: validUmaskMsg, Actual: "123.4"}), // An invalid string should raise the expected exception. That(`unix:umask = 022z`).Throws(errs.BadValue{ What: "umask", Valid: validUmaskMsg, Actual: "022z"}), // An invalid data type should raise the expected exception. That(`unix:umask = [1]`).Throws(errs.BadValue{ What: "umask", Valid: validUmaskMsg, Actual: "list"}), // Values outside the legal range should raise the expected exception. That(`unix:umask = 0o1000`).Throws(errs.OutOfRange{ What: "umask", ValidLow: "0", ValidHigh: "0o777", Actual: "0o1000"}), That(`unix:umask = -1`).Throws(errs.OutOfRange{ What: "umask", ValidLow: "0", ValidHigh: "0o777", Actual: "-0o1"}), ) } elvish-0.17.0/pkg/mods/unix/unix.go000066400000000000000000000013041415471104000171040ustar00rootroot00000000000000//go:build !windows && !plan9 && !js // +build !windows,!plan9,!js // Package unix exports an Elvish namespace that contains variables and // functions that deal with features unique to UNIX-like operating systems. On // non-UNIX operating systems it exports an empty namespace. package unix import ( "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vars" ) // ExposeUnixNs indicate whether this module should be exposed as a usable // elvish namespace. const ExposeUnixNs = true // Ns is an Elvish namespace that contains variables and functions that deal // with features unique to UNIX-like operating systems. On var Ns = eval.BuildNs(). AddVars(map[string]vars.Var{ "umask": UmaskVariable{}, }).Ns() elvish-0.17.0/pkg/parse/000077500000000000000000000000001415471104000147615ustar00rootroot00000000000000elvish-0.17.0/pkg/parse/check_ast_test.go000066400000000000000000000105401415471104000202730ustar00rootroot00000000000000package parse import ( "fmt" "reflect" "strings" "unicode" "unicode/utf8" ) // AST checking utilities. Used in test cases. // ast is an AST specification. The name part identifies the type of the Node; // for instance, "Chunk" specifies a Chunk. The fields part is specifies children // to check; see document of fs. // // When a Node contains exactly one child, It can be coalesced with its child // by adding "/ChildName" in the name part. For instance, "Chunk/Pipeline" // specifies a Chunk that contains exactly one Pipeline. In this case, the // fields part specified the children of the Pipeline instead of the Chunk // (which has no additional interesting fields anyway). Multi-level coalescence // like "Chunk/Pipeline/Form" is also allowed. // // The dynamic type of the Node being checked is assumed to be a pointer to a // struct that embeds the "node" struct. type ast struct { name string fields fs } // fs specifies fields of a Node to check. For the value of field $f in the // Node ("found value"), fs[$f] ("wanted value") is used to check against it. // // If the key is "text", the SourceText of the Node is checked. It doesn't // involve a found value. // // If the wanted value is nil, the found value is checked against nil. // // If the found value implements Node, then the wanted value must be either an // ast, where the checking algorithm of ast applies, or a string, where the // source text of the found value is checked. // // If the found value is a slice whose elements implement Node, then the wanted // value must be a slice where checking is then done recursively. // // If the found value satisfied none of the above conditions, it is checked // against the wanted value using reflect.DeepEqual. type fs map[string]interface{} // checkAST checks an AST against a specification. func checkAST(n Node, want ast) error { wantnames := strings.Split(want.name, "/") // Check coalesced levels for i, wantname := range wantnames { name := reflect.TypeOf(n).Elem().Name() if wantname != name { return fmt.Errorf("want %s, got %s (%s)", wantname, name, summary(n)) } if i == len(wantnames)-1 { break } fields := Children(n) if len(fields) != 1 { return fmt.Errorf("want exactly 1 child, got %d (%s)", len(fields), summary(n)) } n = fields[0] } ntype := reflect.TypeOf(n).Elem() nvalue := reflect.ValueOf(n).Elem() for i := 0; i < ntype.NumField(); i++ { fieldname := ntype.Field(i).Name if !exported(fieldname) { // Unexported field continue } got := nvalue.Field(i).Interface() want, ok := want.fields[fieldname] if ok { err := checkField(got, want, "field "+fieldname+" of: "+summary(n)) if err != nil { return err } } else { // Not specified. Check if got is a zero value of its type. zero := reflect.Zero(reflect.TypeOf(got)).Interface() if !reflect.DeepEqual(got, zero) { return fmt.Errorf("want %v, got %v (field %s of: %s)", zero, got, fieldname, summary(n)) } } } return nil } // checkField checks a field against a field specification. func checkField(got interface{}, want interface{}, ctx string) error { // Want nil. if want == nil { if !reflect.ValueOf(got).IsNil() { return fmt.Errorf("want nil, got %v (%s)", got, ctx) } return nil } if got, ok := got.(Node); ok { // Got a Node. return checkNodeInField(got, want) } tgot := reflect.TypeOf(got) if tgot.Kind() == reflect.Slice && tgot.Elem().Implements(nodeType) { // Got a slice of Nodes. vgot := reflect.ValueOf(got) vwant := reflect.ValueOf(want) if vgot.Len() != vwant.Len() { return fmt.Errorf("want %d, got %d (%s)", vwant.Len(), vgot.Len(), ctx) } for i := 0; i < vgot.Len(); i++ { err := checkNodeInField(vgot.Index(i).Interface().(Node), vwant.Index(i).Interface()) if err != nil { return err } } return nil } if !reflect.DeepEqual(want, got) { return fmt.Errorf("want %v, got %v (%s)", want, got, ctx) } return nil } func checkNodeInField(got Node, want interface{}) error { switch want := want.(type) { case string: text := SourceText(got) if want != text { return fmt.Errorf("want %q, got %q (%s)", want, text, summary(got)) } return nil case ast: return checkAST(got, want) default: panic(fmt.Sprintf("bad want type %T (%s)", want, summary(got))) } } func exported(name string) bool { r, _ := utf8.DecodeRuneInString(name) return unicode.IsUpper(r) } elvish-0.17.0/pkg/parse/check_parse_tree_test.go000066400000000000000000000022371415471104000216410ustar00rootroot00000000000000package parse import "fmt" // checkParseTree checks whether the parse tree part of a Node is well-formed. func checkParseTree(n Node) error { children := Children(n) if len(children) == 0 { return nil } // Parent pointers of all children should point to me. for i, ch := range children { if Parent(ch) != n { return fmt.Errorf("parent of child %d (%s) is wrong: %s", i, summary(ch), summary(n)) } } // The Begin of the first child should be equal to mine. if children[0].Range().From != n.Range().From { return fmt.Errorf("gap between node and first child: %s", summary(n)) } // The End of the last child should be equal to mine. nch := len(children) if children[nch-1].Range().To != n.Range().To { return fmt.Errorf("gap between node and last child: %s", summary(n)) } // Consecutive children have consecutive position ranges. for i := 0; i < nch-1; i++ { if children[i].Range().To != children[i+1].Range().From { return fmt.Errorf("gap between child %d and %d of: %s", i, i+1, summary(n)) } } // Check children recursively. for _, ch := range Children(n) { err := checkParseTree(ch) if err != nil { return err } } return nil } elvish-0.17.0/pkg/parse/cmpd/000077500000000000000000000000001415471104000157045ustar00rootroot00000000000000elvish-0.17.0/pkg/parse/cmpd/cmpd.go000066400000000000000000000034411415471104000171600ustar00rootroot00000000000000// Package cmpd contains utilities for working with compound nodes. package cmpd import ( "fmt" "src.elv.sh/pkg/parse" ) // Primary returns a primary node and true if that's the only child of the // compound node. Otherwise it returns nil and false. func Primary(n *parse.Compound) (*parse.Primary, bool) { if len(n.Indexings) == 1 && len(n.Indexings[0].Indices) == 0 { return n.Indexings[0].Head, true } return nil, false } // StringLiteral returns the value of a string literal and true if that's the // only child of the compound node. Otherwise it returns "" and false. func StringLiteral(n *parse.Compound) (string, bool) { if pn, ok := Primary(n); ok { switch pn.Type { case parse.Bareword, parse.SingleQuoted, parse.DoubleQuoted: return pn.Value, true } } return "", false } // Lambda returns a lambda primary node and true if that's the only child of the // compound node. Otherwise it returns nil and false. func Lambda(n *parse.Compound) (*parse.Primary, bool) { if pn, ok := Primary(n); ok { if pn.Type == parse.Lambda { return pn, true } } return nil, false } // StringLiteralOrError is like StringLiteral, but returns an error suitable as // a compiler error when StringLiteral would return false. func StringLiteralOrError(n *parse.Compound, what string) (string, error) { s, ok := StringLiteral(n) if !ok { return "", fmt.Errorf("%s must be string literal, found %s", what, Shape(n)) } return s, nil } // Shape describes the shape of the compound node. func Shape(n *parse.Compound) string { if len(n.Indexings) == 0 { return "empty expression" } if len(n.Indexings) > 1 { return "compound expression" } in := n.Indexings[0] if len(in.Indices) > 0 { return "indexing expression" } pn := in.Head return "primary expression of type " + pn.Type.String() } elvish-0.17.0/pkg/parse/error.go000066400000000000000000000032731415471104000164460ustar00rootroot00000000000000package parse import ( "fmt" "strings" "src.elv.sh/pkg/diag" ) const parseErrorType = "parse error" // Error stores multiple underlying parse errors, and can pretty print them. type Error struct { Entries []*diag.Error } var _ diag.Shower = &Error{} // GetError returns an *Error if the given error has dynamic type *Error, i.e. // is returned by one of the Parse functions. Otherwise it returns nil. func GetError(e error) *Error { if er, ok := e.(*Error); ok { return er } return nil } func (er *Error) add(msg string, ctx *diag.Context) { err := &diag.Error{Type: parseErrorType, Message: msg, Context: *ctx} er.Entries = append(er.Entries, err) } // Error returns a string representation of the error. func (er *Error) Error() string { switch len(er.Entries) { case 0: return "no parse error" case 1: return er.Entries[0].Error() default: sb := new(strings.Builder) // Contexts of parse error entries all have the same name fmt.Fprintf(sb, "multiple parse errors in %s: ", er.Entries[0].Context.Name) for i, e := range er.Entries { if i > 0 { fmt.Fprint(sb, "; ") } fmt.Fprintf(sb, "%d-%d: %s", e.Context.From, e.Context.To, e.Message) } return sb.String() } } // Show shows the error. func (er *Error) Show(indent string) string { switch len(er.Entries) { case 0: return "no parse error" case 1: return er.Entries[0].Show(indent) default: sb := new(strings.Builder) fmt.Fprint(sb, "Multiple parse errors:") for _, e := range er.Entries { sb.WriteString("\n" + indent + " ") fmt.Fprintf(sb, "\033[31;1m%s\033[m\n", e.Message) sb.WriteString(indent + " ") sb.WriteString(e.Context.Show(indent + " ")) } return sb.String() } } elvish-0.17.0/pkg/parse/error_test.go000066400000000000000000000016211415471104000175000ustar00rootroot00000000000000package parse import ( "errors" "testing" "src.elv.sh/pkg/diag" . "src.elv.sh/pkg/tt" ) func TestGetError(t *testing.T) { parseError := makeError() Test(t, Fn("GetError", GetError), Table{ Args(parseError).Rets(parseError), Args(errors.New("random error")).Rets((*Error)(nil)), }) } var errorTests = []struct { err *Error indent string wantError string wantShow string }{ {makeError(), "", "no parse error", "no parse error"}, // TODO: Add more complex test cases. } func TestError(t *testing.T) { for _, test := range errorTests { gotError := test.err.Error() if gotError != test.wantError { t.Errorf("got error %q, want %q", gotError, test.wantError) } gotShow := test.err.Show(test.indent) if gotShow != test.wantShow { t.Errorf("got show %q, want %q", gotShow, test.wantShow) } } } func makeError(entries ...*diag.Error) *Error { return &Error{entries} } elvish-0.17.0/pkg/parse/node.go000066400000000000000000000016141415471104000162370ustar00rootroot00000000000000package parse import "src.elv.sh/pkg/diag" // Node represents a parse tree as well as an AST. type Node interface { diag.Ranger parse(*parser) n() *node } type node struct { diag.Ranging sourceText string parent Node children []Node } func (n *node) n() *node { return n } func (n *node) addChild(ch Node) { n.children = append(n.children, ch) } // Range returns the range within the full source text that parses to the node. func (n *node) Range() diag.Ranging { return n.Ranging } // Parent returns the parent of a node. It returns nil if the node is the root // of the parse tree. func Parent(n Node) Node { return n.n().parent } // SourceText returns the part of the source text that parses to the node. func SourceText(n Node) string { return n.n().sourceText } // Children returns all children of the node in the parse tree. func Children(n Node) []Node { return n.n().children } elvish-0.17.0/pkg/parse/parse.go000066400000000000000000000601761415471104000164340ustar00rootroot00000000000000// Package parse implements the elvish parser. // // The parser builds a hybrid of AST (abstract syntax tree) and parse tree // (a.k.a. concrete syntax tree). The AST part only includes parts that are // semantically significant -- i.e. skipping whitespaces and symbols that do not // alter the semantics, and is embodied in the fields of each *Node type. The // parse tree part corresponds to all the text in the original source text, and // is embodied in the children of each *Node type. package parse //go:generate stringer -type=PrimaryType,RedirMode,ExprCtx -output=string.go import ( "bytes" "fmt" "io" "unicode" "src.elv.sh/pkg/diag" ) // Tree represents a parsed tree. type Tree struct { Root *Chunk Source Source } // Config keeps configuration options when parsing. type Config struct { // Destination of warnings. If nil, warnings are suppressed. WarningWriter io.Writer } // Parse parses the given source. The returned error always has type *Error // if it is not nil. func Parse(src Source, cfg Config) (Tree, error) { tree := Tree{&Chunk{}, src} err := ParseAs(src, tree.Root, cfg) return tree, err } // ParseAs parses the given source as a node, depending on the dynamic type of // n. If the error is not nil, it always has type *Error. func ParseAs(src Source, n Node, cfg Config) error { ps := &parser{srcName: src.Name, src: src.Code, warn: cfg.WarningWriter} ps.parse(n) ps.done() return ps.assembleError() } // Errors. var ( errShouldBeForm = newError("", "form") errBadRedirSign = newError("bad redir sign", "'<'", "'>'", "'>>'", "'<>'") errShouldBeFD = newError("", "a composite term representing fd") errShouldBeFilename = newError("", "a composite term representing filename") errShouldBeArray = newError("", "spaced") errStringUnterminated = newError("string not terminated") errInvalidEscape = newError("invalid escape sequence") errInvalidEscapeOct = newError("invalid escape sequence", "octal digit") errInvalidEscapeHex = newError("invalid escape sequence", "hex digit") errInvalidEscapeControl = newError("invalid control sequence", "a codepoint between 0x3F and 0x5F") errShouldBePrimary = newError("", "single-quoted string", "double-quoted string", "bareword") errShouldBeVariableName = newError("", "variable name") errShouldBeRBracket = newError("", "']'") errShouldBeRBrace = newError("", "'}'") errShouldBeBraceSepOrRBracket = newError("", "','", "'}'") errShouldBeRParen = newError("", "')'") errShouldBeCompound = newError("", "compound") errShouldBeEqual = newError("", "'='") errShouldBePipe = newError("", "'|'") errBothElementsAndPairs = newError("cannot contain both list elements and map pairs") errShouldBeNewline = newError("", "newline") ) // Chunk = { PipelineSep | Space } { Pipeline { PipelineSep | Space } } type Chunk struct { node Pipelines []*Pipeline } func (bn *Chunk) parse(ps *parser) { bn.parseSeps(ps) for startsPipeline(ps.peek()) { ps.parse(&Pipeline{}).addTo(&bn.Pipelines, bn) if bn.parseSeps(ps) == 0 { break } } } func isPipelineSep(r rune) bool { return r == '\r' || r == '\n' || r == ';' } // parseSeps parses pipeline separators along with whitespaces. It returns the // number of pipeline separators parsed. func (bn *Chunk) parseSeps(ps *parser) int { nseps := 0 for { r := ps.peek() if isPipelineSep(r) { // parse as a Sep parseSep(bn, ps, r) nseps++ } else if IsInlineWhitespace(r) || r == '#' { // parse a run of spaces as a Sep parseSpaces(bn, ps) } else { break } } return nseps } // Pipeline = Form { '|' Form } type Pipeline struct { node Forms []*Form Background bool } func (pn *Pipeline) parse(ps *parser) { ps.parse(&Form{}).addTo(&pn.Forms, pn) for parseSep(pn, ps, '|') { parseSpacesAndNewlines(pn, ps) if !startsForm(ps.peek()) { ps.error(errShouldBeForm) return } ps.parse(&Form{}).addTo(&pn.Forms, pn) } parseSpaces(pn, ps) if ps.peek() == '&' { ps.next() addSep(pn, ps) pn.Background = true parseSpaces(pn, ps) } } func startsPipeline(r rune) bool { return startsForm(r) } // Form = { Space } { { Assignment } { Space } } // { Compound } { Space } { ( Compound | MapPair | Redir ) { Space } } type Form struct { node Assignments []*Assignment Head *Compound Args []*Compound Opts []*MapPair Redirs []*Redir } func (fn *Form) parse(ps *parser) { parseSpaces(fn, ps) for fn.tryAssignment(ps) { parseSpaces(fn, ps) } // Parse head. if !startsCompound(ps.peek(), CmdExpr) { if len(fn.Assignments) > 0 { // Assignment-only form. return } // Bad form. ps.error(fmt.Errorf("bad rune at form head: %q", ps.peek())) } ps.parse(&Compound{ExprCtx: CmdExpr}).addAs(&fn.Head, fn) parseSpaces(fn, ps) for { r := ps.peek() switch { case r == '&': ps.next() hasMapPair := startsCompound(ps.peek(), LHSExpr) ps.backup() if !hasMapPair { // background indicator return } ps.parse(&MapPair{}).addTo(&fn.Opts, fn) case startsCompound(r, NormalExpr): cn := &Compound{} ps.parse(cn) if isRedirSign(ps.peek()) { // Redir ps.parse(&Redir{Left: cn}).addTo(&fn.Redirs, fn) } else { parsed{cn}.addTo(&fn.Args, fn) } case isRedirSign(r): ps.parse(&Redir{}).addTo(&fn.Redirs, fn) default: return } parseSpaces(fn, ps) } } // tryAssignment tries to parse an assignment. If succeeded, it adds the parsed // assignment to fn.Assignments and returns true. Otherwise it rewinds the // parser and returns false. func (fn *Form) tryAssignment(ps *parser) bool { if !startsIndexing(ps.peek(), LHSExpr) { return false } pos := ps.pos errorEntries := ps.errors.Entries parsedAssignment := ps.parse(&Assignment{}) // If errors were added, revert if len(ps.errors.Entries) > len(errorEntries) { ps.errors.Entries = errorEntries ps.pos = pos return false } parsedAssignment.addTo(&fn.Assignments, fn) return true } func startsForm(r rune) bool { return IsInlineWhitespace(r) || startsCompound(r, CmdExpr) } // Assignment = Indexing '=' Compound type Assignment struct { node Left *Indexing Right *Compound } func (an *Assignment) parse(ps *parser) { ps.parse(&Indexing{ExprCtx: LHSExpr}).addAs(&an.Left, an) head := an.Left.Head if !ValidLHSVariable(head, true) { ps.errorp(head, errShouldBeVariableName) } if !parseSep(an, ps, '=') { ps.error(errShouldBeEqual) } ps.parse(&Compound{}).addAs(&an.Right, an) } func ValidLHSVariable(p *Primary, allowSigil bool) bool { switch p.Type { case Braced: // TODO(xiaq): check further inside braced expression return true case SingleQuoted, DoubleQuoted: // Quoted variable names may contain anything return true case Bareword: // Bareword variable names may only contain runes that are valid in raw // variable names if p.Value == "" { return false } name := p.Value if allowSigil && name[0] == '@' { name = name[1:] } for _, r := range name { if !allowedInVariableName(r) { return false } } return true default: return false } } // Redir = { Compound } { '<'|'>'|'<>'|'>>' } { Space } ( '&'? Compound ) type Redir struct { node Left *Compound Mode RedirMode RightIsFd bool Right *Compound } func (rn *Redir) parse(ps *parser) { // The parsing of the Left part is done in Form.parse. if rn.Left != nil { addChild(rn, rn.Left) rn.From = rn.Left.From } begin := ps.pos for isRedirSign(ps.peek()) { ps.next() } sign := ps.src[begin:ps.pos] switch sign { case "<": rn.Mode = Read case ">": rn.Mode = Write case ">>": rn.Mode = Append case "<>": rn.Mode = ReadWrite default: ps.error(errBadRedirSign) } addSep(rn, ps) parseSpaces(rn, ps) if parseSep(rn, ps, '&') { rn.RightIsFd = true } ps.parse(&Compound{}).addAs(&rn.Right, rn) if len(rn.Right.Indexings) == 0 { if rn.RightIsFd { ps.error(errShouldBeFD) } else { ps.error(errShouldBeFilename) } return } } func isRedirSign(r rune) bool { return r == '<' || r == '>' } // RedirMode records the mode of an IO redirection. type RedirMode int // Possible values for RedirMode. const ( BadRedirMode RedirMode = iota Read Write ReadWrite Append ) // Filter is the Elvish filter DSL. It uses the same syntax as arguments and // options to a command. type Filter struct { node Args []*Compound Opts []*MapPair } func (qn *Filter) parse(ps *parser) { parseSpaces(qn, ps) for { r := ps.peek() switch { case r == '&': ps.parse(&MapPair{}).addTo(&qn.Opts, qn) case startsCompound(r, NormalExpr): ps.parse(&Compound{}).addTo(&qn.Args, qn) default: return } parseSpaces(qn, ps) } } // Compound = { Indexing } type Compound struct { node ExprCtx ExprCtx Indexings []*Indexing } // ExprCtx represents special contexts of expression parsing. type ExprCtx int const ( // NormalExpr represents a normal expression, namely none of the special // ones below. It is the default value. NormalExpr ExprCtx = iota // CmdExpr represents an expression used as the command in a form. In this // context, unquoted <>*^ are treated as bareword characters. CmdExpr // LHSExpr represents an expression used as the left-hand-side in either // assignments or map pairs. In this context, an unquoted = serves as an // expression terminator and is thus not treated as a bareword character. LHSExpr // BracedElemExpr represents an expression used as an element in a braced // expression. In this context, an unquoted , serves as an expression // terminator and is thus not treated as a bareword character. BracedElemExpr // strictExpr is only meaningful to allowedInBareword. strictExpr ) func (cn *Compound) parse(ps *parser) { cn.tilde(ps) for startsIndexing(ps.peek(), cn.ExprCtx) { ps.parse(&Indexing{ExprCtx: cn.ExprCtx}).addTo(&cn.Indexings, cn) } } // tilde parses a tilde if there is one. It is implemented here instead of // within Primary since a tilde can only appear as the first part of a // Compound. Elsewhere tildes are barewords. func (cn *Compound) tilde(ps *parser) { if ps.peek() == '~' { ps.next() base := node{Ranging: diag.Ranging{From: ps.pos - 1, To: ps.pos}, sourceText: "~", parent: nil, children: nil} pn := &Primary{node: base, Type: Tilde, Value: "~"} in := &Indexing{node: base} parsed{pn}.addAs(&in.Head, in) parsed{in}.addTo(&cn.Indexings, cn) } } func startsCompound(r rune, ctx ExprCtx) bool { return startsIndexing(r, ctx) } // Indexing = Primary { '[' Array ']' } type Indexing struct { node ExprCtx ExprCtx Head *Primary Indices []*Array } func (in *Indexing) parse(ps *parser) { ps.parse(&Primary{ExprCtx: in.ExprCtx}).addAs(&in.Head, in) for parseSep(in, ps, '[') { if !startsArray(ps.peek()) { ps.error(errShouldBeArray) } ps.parse(&Array{}).addTo(&in.Indices, in) if !parseSep(in, ps, ']') { ps.error(errShouldBeRBracket) return } } } func startsIndexing(r rune, ctx ExprCtx) bool { return startsPrimary(r, ctx) } // Array = { Space | '\n' } { Compound { Space | '\n' } } type Array struct { node Compounds []*Compound // When non-empty, records the occurrences of semicolons by the indices of // the compounds they appear before. For instance, [; ; a b; c d;] results // in Semicolons={0 0 2 4}. Semicolons []int } func (sn *Array) parse(ps *parser) { parseSep := func() { parseSpacesAndNewlines(sn, ps) } parseSep() for startsCompound(ps.peek(), NormalExpr) { ps.parse(&Compound{}).addTo(&sn.Compounds, sn) parseSep() } } func startsArray(r rune) bool { return IsWhitespace(r) || startsIndexing(r, NormalExpr) } // Primary is the smallest expression unit. type Primary struct { node ExprCtx ExprCtx Type PrimaryType // Legacy lambda uses [args]{ body } instead of { |args| body } LegacyLambda bool // The unquoted string value. Valid for Bareword, SingleQuoted, // DoubleQuoted, Variable, Wildcard and Tilde. Value string Elements []*Compound // Valid for List and Lambda Chunk *Chunk // Valid for OutputCapture, ExitusCapture and Lambda MapPairs []*MapPair // Valid for Map and Lambda Braced []*Compound // Valid for Braced } // PrimaryType is the type of a Primary. type PrimaryType int // Possible values for PrimaryType. const ( BadPrimary PrimaryType = iota Bareword SingleQuoted DoubleQuoted Variable Wildcard Tilde ExceptionCapture OutputCapture List Lambda Map Braced ) func (pn *Primary) parse(ps *parser) { r := ps.peek() if !startsPrimary(r, pn.ExprCtx) { ps.error(errShouldBePrimary) return } // Try bareword early, since it has precedence over wildcard on * // when ctx = commandExpr. if allowedInBareword(r, pn.ExprCtx) { pn.bareword(ps) return } switch r { case '\'': pn.singleQuoted(ps) case '"': pn.doubleQuoted(ps) case '$': pn.variable(ps) case '*': pn.starWildcard(ps) case '?': if ps.hasPrefix("?(") { pn.exitusCapture(ps) } else { pn.questionWildcard(ps) } case '(': pn.outputCapture(ps) case '[': pn.lbracket(ps) case '{': pn.lbrace(ps) default: // Parse an empty bareword. pn.Type = Bareword } } func (pn *Primary) singleQuoted(ps *parser) { pn.Type = SingleQuoted ps.next() pn.singleQuotedInner(ps) } // Parses a single-quoted string after the opening quote. Sets pn.Value but not // pn.Type. func (pn *Primary) singleQuotedInner(ps *parser) { var buf bytes.Buffer defer func() { pn.Value = buf.String() }() for { switch r := ps.next(); r { case eof: ps.error(errStringUnterminated) return case '\'': if ps.peek() == '\'' { // Two consecutive single quotes ps.next() buf.WriteByte('\'') } else { // End of string return } default: buf.WriteRune(r) } } } func (pn *Primary) doubleQuoted(ps *parser) { pn.Type = DoubleQuoted ps.next() pn.doubleQuotedInner(ps) } // Parses a double-quoted string after the opening quote. Sets pn.Value but not // pn.Type. func (pn *Primary) doubleQuotedInner(ps *parser) { var buf bytes.Buffer defer func() { pn.Value = buf.String() }() for { switch r := ps.next(); r { case eof: ps.error(errStringUnterminated) return case '"': return case '\\': switch r := ps.next(); r { case 'c', '^': // control sequence r := ps.next() if r < 0x3F || r > 0x5F { ps.backup() ps.error(errInvalidEscapeControl) ps.next() } if byte(r) == '?' { // special-case: \c? => del buf.WriteByte(byte(0x7F)) } else { buf.WriteByte(byte(r - 0x40)) } case 'x', 'u', 'U': // two, four, or eight hex digits var n int switch r { case 'x': n = 2 case 'u': n = 4 case 'U': n = 8 } var rr rune for i := 0; i < n; i++ { d, ok := hexToDigit(ps.next()) if !ok { ps.backup() ps.error(errInvalidEscapeHex) break } rr = rr*16 + d } buf.WriteRune(rr) case '0', '1', '2', '3', '4', '5', '6', '7': // three octal digits rr := r - '0' for i := 0; i < 2; i++ { r := ps.next() if r < '0' || r > '7' { ps.backup() ps.error(errInvalidEscapeOct) break } rr = rr*8 + (r - '0') } buf.WriteRune(rr) default: if rr, ok := doubleEscape[r]; ok { buf.WriteRune(rr) } else { ps.backup() ps.error(errInvalidEscape) ps.next() } } default: buf.WriteRune(r) } } } // a table for the simple double-quote escape sequences. var doubleEscape = map[rune]rune{ // same as golang 'a': '\a', 'b': '\b', 'f': '\f', 'n': '\n', 'r': '\r', 't': '\t', 'v': '\v', '\\': '\\', '"': '"', // additional 'e': '\033', } var doubleUnescape = map[rune]rune{} func init() { for k, v := range doubleEscape { doubleUnescape[v] = k } } func hexToDigit(r rune) (rune, bool) { switch { case '0' <= r && r <= '9': return r - '0', true case 'a' <= r && r <= 'f': return r - 'a' + 10, true case 'A' <= r && r <= 'F': return r - 'A' + 10, true default: return -1, false } } func (pn *Primary) variable(ps *parser) { pn.Type = Variable ps.next() switch r := ps.next(); r { case eof: ps.backup() ps.error(errShouldBeVariableName) ps.next() case '\'': pn.singleQuotedInner(ps) case '"': pn.doubleQuotedInner(ps) default: defer func() { pn.Value = ps.src[pn.From+1 : ps.pos] }() if !allowedInVariableName(r) && r != '@' { ps.backup() ps.error(errShouldBeVariableName) } for allowedInVariableName(ps.peek()) { ps.next() } } } // The following are allowed in variable names: // * Anything beyond ASCII that is printable // * Letters and numbers // * The symbols "-_:~" func allowedInVariableName(r rune) bool { return (r >= 0x80 && unicode.IsPrint(r)) || ('0' <= r && r <= '9') || ('a' <= r && r <= 'z') || ('A' <= r && r <= 'Z') || r == '-' || r == '_' || r == ':' || r == '~' } func (pn *Primary) starWildcard(ps *parser) { pn.Type = Wildcard for ps.peek() == '*' { ps.next() } pn.Value = ps.src[pn.From:ps.pos] } func (pn *Primary) questionWildcard(ps *parser) { pn.Type = Wildcard if ps.peek() == '?' { ps.next() } pn.Value = ps.src[pn.From:ps.pos] } func (pn *Primary) exitusCapture(ps *parser) { ps.next() ps.next() addSep(pn, ps) pn.Type = ExceptionCapture ps.parse(&Chunk{}).addAs(&pn.Chunk, pn) if !parseSep(pn, ps, ')') { ps.error(errShouldBeRParen) } } func (pn *Primary) outputCapture(ps *parser) { pn.Type = OutputCapture parseSep(pn, ps, '(') ps.parse(&Chunk{}).addAs(&pn.Chunk, pn) if !parseSep(pn, ps, ')') { ps.error(errShouldBeRParen) } } // List = '[' { Space } { Compound } ']' // = '[' { Space } { MapPair { Space } } ']' // Map = '[' { Space } '&' { Space } ']' // Lambda = '[' { Space } { (Compound | MapPair) { Space } } ']' '{' Chunk '}' func (pn *Primary) lbracket(ps *parser) { parseSep(pn, ps, '[') parseSpacesAndNewlines(pn, ps) loneAmpersand := false items: for { r := ps.peek() switch { case r == '&': ps.next() hasMapPair := startsCompound(ps.peek(), LHSExpr) if !hasMapPair { loneAmpersand = true addSep(pn, ps) parseSpacesAndNewlines(pn, ps) break items } ps.backup() ps.parse(&MapPair{}).addTo(&pn.MapPairs, pn) case startsCompound(r, NormalExpr): ps.parse(&Compound{}).addTo(&pn.Elements, pn) default: break items } parseSpacesAndNewlines(pn, ps) } if !parseSep(pn, ps, ']') { ps.error(errShouldBeRBracket) } if parseSep(pn, ps, '{') { pn.LegacyLambda = true pn.lambda(ps) } else { if loneAmpersand || len(pn.MapPairs) > 0 { if len(pn.Elements) > 0 { // TODO(xiaq): Add correct position information. ps.error(errBothElementsAndPairs) } pn.Type = Map } else { pn.Type = List } } } // lambda parses a lambda expression. The opening brace has been seen. func (pn *Primary) lambda(ps *parser) { pn.Type = Lambda if !pn.LegacyLambda { parseSpacesAndNewlines(pn, ps) if parseSep(pn, ps, '|') { parseSpacesAndNewlines(pn, ps) items: for { r := ps.peek() switch { case r == '&': ps.parse(&MapPair{}).addTo(&pn.MapPairs, pn) case startsCompound(r, NormalExpr): ps.parse(&Compound{}).addTo(&pn.Elements, pn) default: break items } parseSpacesAndNewlines(pn, ps) } if !parseSep(pn, ps, '|') { ps.error(errShouldBePipe) } } } ps.parse(&Chunk{}).addAs(&pn.Chunk, pn) if !parseSep(pn, ps, '}') { ps.error(errShouldBeRBrace) } } // Braced = '{' Compound { BracedSep Compounds } '}' // BracedSep = { Space | '\n' } [ ',' ] { Space | '\n' } func (pn *Primary) lbrace(ps *parser) { parseSep(pn, ps, '{') if r := ps.peek(); r == ';' || r == '\r' || r == '\n' || r == '|' || IsInlineWhitespace(r) { pn.lambda(ps) return } pn.Type = Braced // TODO(xiaq): The compound can be empty, which allows us to parse {,foo}. // Allowing compounds to be empty can be fragile in other cases. ps.parse(&Compound{ExprCtx: BracedElemExpr}).addTo(&pn.Braced, pn) for isBracedSep(ps.peek()) { parseSpacesAndNewlines(pn, ps) // optional, so ignore the return value parseSep(pn, ps, ',') parseSpacesAndNewlines(pn, ps) ps.parse(&Compound{ExprCtx: BracedElemExpr}).addTo(&pn.Braced, pn) } if !parseSep(pn, ps, '}') { ps.error(errShouldBeBraceSepOrRBracket) } } func isBracedSep(r rune) bool { return r == ',' || IsWhitespace(r) } func (pn *Primary) bareword(ps *parser) { pn.Type = Bareword defer func() { pn.Value = ps.src[pn.From:ps.pos] }() for allowedInBareword(ps.peek(), pn.ExprCtx) { ps.next() } } // allowedInBareword returns where a rune is allowed in barewords in the given // expression context. The special strictExpr context queries whether the rune // is allowed in all contexts. // // The following are allowed in barewords: // // * Anything allowed in variable names // * The symbols "./\@%+!" // * The symbol "=", if ctx != lhsExpr && ctx != strictExpr // * The symbol ",", if ctx != bracedExpr && ctx != strictExpr // * The symbols "<>*^", if ctx = commandExpr // // The seemingly weird inclusion of \ is for easier path manipulation in // Windows. func allowedInBareword(r rune, ctx ExprCtx) bool { return allowedInVariableName(r) || r == '.' || r == '/' || r == '\\' || r == '@' || r == '%' || r == '+' || r == '!' || (ctx != LHSExpr && ctx != strictExpr && r == '=') || (ctx != BracedElemExpr && ctx != strictExpr && r == ',') || (ctx == CmdExpr && (r == '<' || r == '>' || r == '*' || r == '^')) } func startsPrimary(r rune, ctx ExprCtx) bool { return r == '\'' || r == '"' || r == '$' || allowedInBareword(r, ctx) || r == '?' || r == '*' || r == '(' || r == '[' || r == '{' } // MapPair = '&' { Space } Compound { Space } Compound type MapPair struct { node Key, Value *Compound } func (mpn *MapPair) parse(ps *parser) { parseSep(mpn, ps, '&') ps.parse(&Compound{ExprCtx: LHSExpr}).addAs(&mpn.Key, mpn) if len(mpn.Key.Indexings) == 0 { ps.error(errShouldBeCompound) } if parseSep(mpn, ps, '=') { parseSpacesAndNewlines(mpn, ps) // Parse value part. It can be empty. ps.parse(&Compound{}).addAs(&mpn.Value, mpn) } } // Sep is the catch-all node type for leaf nodes that lack internal structures // and semantics, and serve solely for syntactic purposes. The parsing of // separators depend on the Parent node; as such it lacks a genuine parse // method. type Sep struct { node } // NewSep makes a new Sep. func NewSep(src string, begin, end int) *Sep { return &Sep{node: node{diag.Ranging{From: begin, To: end}, src[begin:end], nil, nil}} } func (*Sep) parse(*parser) { // A no-op, only to satisfy the Node interface. } func addSep(n Node, ps *parser) { var begin int ch := Children(n) if len(ch) > 0 { begin = ch[len(ch)-1].Range().To } else { begin = n.Range().From } if begin < ps.pos { addChild(n, NewSep(ps.src, begin, ps.pos)) } } func parseSep(n Node, ps *parser, sep rune) bool { if ps.peek() == sep { ps.next() addSep(n, ps) return true } return false } func parseSpaces(n Node, ps *parser) { parseSpacesInner(n, ps, false) } func parseSpacesAndNewlines(n Node, ps *parser) { parseSpacesInner(n, ps, true) } func parseSpacesInner(n Node, ps *parser, newlines bool) { spaces: for { r := ps.peek() switch { case IsInlineWhitespace(r): ps.next() case newlines && IsWhitespace(r): ps.next() case r == '#': // Comment is like inline whitespace as long as we don't include the // trailing newline. ps.next() for { r := ps.peek() if r == eof || r == '\r' || r == '\n' { break } ps.next() } case r == '^': // Line continuation is like inline whitespace. ps.next() switch ps.peek() { case '\r': ps.next() if ps.peek() == '\n' { ps.next() } case '\n': ps.next() case eof: ps.error(errShouldBeNewline) default: ps.backup() break spaces } default: break spaces } } addSep(n, ps) } // IsInlineWhitespace reports whether r is an inline whitespace character. // Currently this includes space (Unicode 0x20) and tab (Unicode 0x9). func IsInlineWhitespace(r rune) bool { return r == ' ' || r == '\t' } // IsWhitespace reports whether r is a whitespace. Currently this includes // inline whitespace characters and newline (Unicode 0xa). func IsWhitespace(r rune) bool { return IsInlineWhitespace(r) || r == '\r' || r == '\n' } func addChild(p Node, ch Node) { p.n().addChild(ch) ch.n().parent = p } elvish-0.17.0/pkg/parse/parse_test.go000066400000000000000000000424421415471104000174670ustar00rootroot00000000000000package parse import ( "fmt" "os" "testing" ) func a(c ...interface{}) ast { // Shorthand used for checking Compound and levels beneath. return ast{"Chunk/Pipeline/Form", fs{"Head": "a", "Args": c}} } var testCases = []struct { name string code string node Node want ast wantErrPart string wantErrAtEnd bool wantErrMsg string }{ // Chunk { name: "empty chunk", code: "", node: &Chunk{}, want: ast{"Chunk", fs{"Pipelines": nil}}, }, { name: "multiple pipelines separated by newlines and semicolons", code: "a;b;c\n;d", node: &Chunk{}, want: ast{"Chunk", fs{"Pipelines": []string{"a", "b", "c", "d"}}}, }, { name: "extra newlines and semicolons do not result in empty pipelines", code: " ;\n\n ls \t ;\n", node: &Chunk{}, want: ast{"Chunk", fs{"Pipelines": []string{"ls \t "}}}, }, // Pipeline { name: "pipeline", code: "a|b|c|d", node: &Pipeline{}, want: ast{"Pipeline", fs{"Forms": []string{"a", "b", "c", "d"}}}, }, { name: "newlines after pipes are allowed", code: "a| \n \n b", node: &Pipeline{}, want: ast{"Pipeline", fs{"Forms": []string{"a", "b"}}}, }, { name: "no form after pipe", code: "a|", node: &Chunk{}, wantErrAtEnd: true, wantErrMsg: "should be form", }, // Form { name: "command form", code: "ls x y", node: &Form{}, want: ast{"Form", fs{ "Head": "ls", "Args": []string{"x", "y"}}}, }, { name: "assignment form", code: "k=v k[a][b]=v {a,b[1]}=(ha)", node: &Form{}, want: ast{"Form", fs{ "Assignments": []string{"k=v", "k[a][b]=v", "{a,b[1]}=(ha)"}}}, }, { name: "temporary assignment", code: "k=v k[a][b]=v a", node: &Form{}, want: ast{"Form", fs{ "Assignments": []string{"k=v", "k[a][b]=v"}, "Head": "a"}}, }, { name: "redirection", code: "a >b", node: &Form{}, want: ast{"Form", fs{ "Head": "a", "Redirs": []ast{ {"Redir", fs{"Mode": Write, "Right": "b"}}}, }}, }, { name: "advanced redirections", code: "a >>b 2>b 3>&- 4>&1 5d", node: &Form{}, want: ast{"Form", fs{ "Head": "a", "Redirs": []ast{ {"Redir", fs{"Mode": Append, "Right": "b"}}, {"Redir", fs{"Left": "2", "Mode": Write, "Right": "b"}}, {"Redir", fs{"Left": "3", "Mode": Write, "RightIsFd": true, "Right": "-"}}, {"Redir", fs{"Left": "4", "Mode": Write, "RightIsFd": true, "Right": "1"}}, {"Redir", fs{"Left": "5", "Mode": Read, "Right": "c"}}, {"Redir", fs{"Left": "6", "Mode": ReadWrite, "Right": "d"}}, }, }}}, { name: "command options", code: "a &a=1 x &b=2", node: &Form{}, want: ast{"Form", fs{ "Head": "a", "Args": []string{"x"}, "Opts": []string{"&a=1", "&b=2"}, }}, // More tests for MapPair below with map syntax }, { name: "bogus ampersand in command form", code: "a & &", node: &Chunk{}, wantErrPart: "&", wantErrMsg: "unexpected rune '&'", }, { name: "no filename redirection source", code: "a >", node: &Chunk{}, wantErrAtEnd: true, wantErrMsg: "should be a composite term representing filename", }, { name: "no FD direction source", code: "a >&", node: &Chunk{}, wantErrAtEnd: true, wantErrMsg: "should be a composite term representing fd", }, // Filter { name: "empty filter", code: "", node: &Filter{}, want: ast{"Filter", fs{}}, }, { name: "filter with arguments", code: "foo bar", node: &Filter{}, want: ast{"Filter", fs{"Args": []string{"foo", "bar"}}}, }, { name: "filter with options", code: "&foo=bar &lorem=ipsum", node: &Filter{}, want: ast{"Filter", fs{"Opts": []string{"&foo=bar", "&lorem=ipsum"}}}, }, { name: "filter mixing arguments and options", code: "foo &a=b bar &x=y", node: &Filter{}, want: ast{"Filter", fs{ "Args": []string{"foo", "bar"}, "Opts": []string{"&a=b", "&x=y"}}}, }, { name: "filter with leading and trailing whitespaces", code: " foo ", node: &Filter{}, want: ast{"Filter", fs{"Args": []string{"foo"}}}, }, // Compound { name: "compound expression", code: `b"foo"?$c*'xyz'`, node: &Compound{}, want: ast{"Compound", fs{ "Indexings": []string{"b", `"foo"`, "?", "$c", "*", "'xyz'"}}}, }, // Indexing { name: "indexing expression", code: "$b[c][d][\ne\n]", node: &Indexing{}, want: ast{"Indexing", fs{ "Head": "$b", "Indices": []string{"c", "d", "\ne\n"}}}, }, // Primary { name: "bareword", code: "foo", node: &Primary{}, want: ast{"Primary", fs{"Type": Bareword, "Value": "foo"}}, }, { name: "bareword with all allowed symbols", code: "./\\@%+!=,", node: &Primary{}, want: ast{"Primary", fs{"Type": Bareword, "Value": "./\\@%+!=,"}}, }, { name: "single-quoted string", code: "'''x''y'''", node: &Primary{}, want: ast{"Primary", fs{"Type": SingleQuoted, "Value": "'x'y'"}}, }, { name: "double-quoted string with control char escape sequences", code: `"[\c?\c@\cI\^I\^[]"`, node: &Primary{}, want: ast{"Primary", fs{ "Type": DoubleQuoted, "Value": "[\x7f\x00\t\t\x1b]", }}, }, { name: "double-quoted string with single-char escape sequences", code: `"[\n\t\a\v\\\"]"`, node: &Primary{}, want: ast{"Primary", fs{ "Type": DoubleQuoted, "Value": "[\n\t\a\v\\\"]", }}, }, { name: "double-quoted string with numerical escape sequences", code: `"b\^[\x1b\u548c\U0002CE23\123\n\t\\"`, node: &Primary{}, want: ast{"Primary", fs{ "Type": DoubleQuoted, "Value": "b\x1b\x1b\u548c\U0002CE23\123\n\t\\", }}, }, { name: "wildcard", code: "a * ? ** ??", node: &Chunk{}, want: a( ast{"Compound/Indexing/Primary", fs{"Type": Wildcard, "Value": "*"}}, ast{"Compound/Indexing/Primary", fs{"Type": Wildcard, "Value": "?"}}, ast{"Compound/Indexing/Primary", fs{"Type": Wildcard, "Value": "**"}}, ast{"Compound", fs{"Indexings": []string{"?", "?"}}}, ), }, { name: "variable", code: `a $x $'!@#' $"\n"`, node: &Chunk{}, want: a( ast{"Compound/Indexing/Primary", fs{"Type": Variable, "Value": "x"}}, ast{"Compound/Indexing/Primary", fs{"Type": Variable, "Value": "!@#"}}, ast{"Compound/Indexing/Primary", fs{"Type": Variable, "Value": "\n"}}, ), }, { name: "list", code: "a [] [ ] [1] [ 2] [3 ] [\n 4 \n5\n 6 7 \n]", node: &Chunk{}, want: a( ast{"Compound/Indexing/Primary", fs{ "Type": List, "Elements": []ast{}}}, ast{"Compound/Indexing/Primary", fs{ "Type": List, "Elements": []ast{}}}, ast{"Compound/Indexing/Primary", fs{ "Type": List, "Elements": []string{"1"}}}, ast{"Compound/Indexing/Primary", fs{ "Type": List, "Elements": []string{"2"}}}, ast{"Compound/Indexing/Primary", fs{ "Type": List, "Elements": []string{"3"}}}, ast{"Compound/Indexing/Primary", fs{ "Type": List, "Elements": []string{"4", "5", "6", "7"}}}, ), }, { name: "map", code: "a [&k=v] [ &k=v] [&k=v ] [ &k=v ] [ &k= v] [&k= \n v] [\n&a=b &c=d \n &e=f\n\n]", node: &Chunk{}, want: a( ast{"Compound/Indexing/Primary", fs{ "Type": Map, "MapPairs": []ast{{"MapPair", fs{"Key": "k", "Value": "v"}}}}}, ast{"Compound/Indexing/Primary", fs{ "Type": Map, "MapPairs": []ast{{"MapPair", fs{"Key": "k", "Value": "v"}}}}}, ast{"Compound/Indexing/Primary", fs{ "Type": Map, "MapPairs": []ast{{"MapPair", fs{"Key": "k", "Value": "v"}}}}}, ast{"Compound/Indexing/Primary", fs{ "Type": Map, "MapPairs": []ast{{"MapPair", fs{"Key": "k", "Value": "v"}}}}}, ast{"Compound/Indexing/Primary", fs{ "Type": Map, "MapPairs": []ast{{"MapPair", fs{"Key": "k", "Value": "v"}}}}}, ast{"Compound/Indexing/Primary", fs{ "Type": Map, "MapPairs": []ast{{"MapPair", fs{"Key": "k", "Value": "v"}}}}}, ast{"Compound/Indexing/Primary", fs{ "Type": Map, "MapPairs": []ast{ {"MapPair", fs{"Key": "a", "Value": "b"}}, {"MapPair", fs{"Key": "c", "Value": "d"}}, {"MapPair", fs{"Key": "e", "Value": "f"}}, }}}, ), }, { name: "empty map", code: "a [&] [ &] [& ] [ & ]", node: &Chunk{}, want: a( ast{"Compound/Indexing/Primary", fs{"Type": Map, "MapPairs": nil}}, ast{"Compound/Indexing/Primary", fs{"Type": Map, "MapPairs": nil}}, ast{"Compound/Indexing/Primary", fs{"Type": Map, "MapPairs": nil}}, ast{"Compound/Indexing/Primary", fs{"Type": Map, "MapPairs": nil}}, ), }, { name: "lambda", code: "a []{} [ ]{ } []{ echo 233 } [ x y ]{puts $x $y} { put haha}", node: &Chunk{}, want: a( ast{"Compound/Indexing/Primary", fs{ "Type": Lambda, "LegacyLambda": true, "Elements": []ast{}, "Chunk": "", }}, ast{"Compound/Indexing/Primary", fs{ "Type": Lambda, "LegacyLambda": true, "Elements": []ast{}, "Chunk": " ", }}, ast{"Compound/Indexing/Primary", fs{ "Type": Lambda, "LegacyLambda": true, "Elements": []ast{}, "Chunk": " echo 233 ", }}, ast{"Compound/Indexing/Primary", fs{ "Type": Lambda, "LegacyLambda": true, "Elements": []string{"x", "y"}, "Chunk": "puts $x $y", }}, ast{"Compound/Indexing/Primary", fs{ "Type": Lambda, "Elements": []ast{}, "Chunk": "put haha", }}, ), }, { name: "new-style lambda with arguments and options", code: "{|a b &k=v|}", node: &Primary{}, want: ast{"Primary", fs{ "Type": Lambda, "LegacyLambda": false, "Elements": []string{"a", "b"}, "MapPairs": []string{"&k=v"}, "Chunk": "", }}, }, { name: "legacy lambda with arguments and options", code: "[a b &k=v]{}", node: &Primary{}, want: ast{"Primary", fs{ "Type": Lambda, "LegacyLambda": true, "Elements": []string{"a", "b"}, "MapPairs": []string{"&k=v"}, "Chunk": "", }}, }, { name: "output capture", code: "a () (b;c) (c\nd)", node: &Chunk{}, want: a( ast{"Compound/Indexing/Primary", fs{ "Type": OutputCapture, "Chunk": ""}}, ast{"Compound/Indexing/Primary", fs{ "Type": OutputCapture, "Chunk": ast{ "Chunk", fs{"Pipelines": []string{"b", "c"}}, }}}, ast{"Compound/Indexing/Primary", fs{ "Type": OutputCapture, "Chunk": ast{ "Chunk", fs{"Pipelines": []string{"c", "d"}}, }}}, ), }, { name: "exception capture", code: "a ?() ?(b;c)", node: &Chunk{}, want: a( ast{"Compound/Indexing/Primary", fs{ "Type": ExceptionCapture, "Chunk": ""}}, ast{"Compound/Indexing/Primary", fs{ "Type": ExceptionCapture, "Chunk": "b;c", }}), }, { name: "braced list", code: "{,a,c\ng\n}", node: &Primary{}, want: ast{"Primary", fs{ "Type": Braced, "Braced": []string{"", "a", "c", "g", ""}}}, }, { name: "tilde", code: "~xiaq/go", node: &Compound{}, want: ast{"Compound", fs{ "Indexings": []ast{ {"Indexing/Primary", fs{"Type": Tilde, "Value": "~"}}, {"Indexing/Primary", fs{"Type": Bareword, "Value": "xiaq/go"}}, }, }}, }, { name: "tilde and wildcard", code: "~xiaq/*.go", node: &Compound{}, want: ast{"Compound", fs{ "Indexings": []ast{ {"Indexing/Primary", fs{"Type": Tilde, "Value": "~"}}, {"Indexing/Primary", fs{"Type": Bareword, "Value": "xiaq/"}}, {"Indexing/Primary", fs{"Type": Wildcard, "Value": "*"}}, {"Indexing/Primary", fs{"Type": Bareword, "Value": ".go"}}, }, }}, }, { name: "unterminated single-quoted string", code: "'a", node: &Chunk{}, wantErrAtEnd: true, wantErrMsg: "string not terminated", }, { name: "unterminated double-quoted string", code: `"a`, node: &Chunk{}, wantErrAtEnd: true, wantErrMsg: "string not terminated", }, { name: "invalid control sequence", code: `a "\^` + "\t", node: &Chunk{}, wantErrPart: "\t", wantErrMsg: "invalid control sequence, should be a codepoint between 0x3F and 0x5F", }, { name: "invalid hex escape sequence", code: `a "\xQQ"`, node: &Chunk{}, wantErrPart: "Q", wantErrMsg: "invalid escape sequence, should be hex digit", }, { name: "invalid octal escape sequence", code: `a "\1ab"`, node: &Chunk{}, wantErrPart: "a", wantErrMsg: "invalid escape sequence, should be octal digit", }, { name: "invalid single-char escape sequence", code: `a "\i"`, node: &Chunk{}, wantErrPart: "i", wantErrMsg: "invalid escape sequence", }, { name: "unterminated variable name", code: "$", node: &Chunk{}, wantErrAtEnd: true, wantErrMsg: "should be variable name", }, { name: "list-map hybrid not supported", code: "a [a &k=v]", node: &Chunk{}, // TODO(xiaq): Add correct position information. wantErrAtEnd: true, wantErrMsg: "cannot contain both list elements and map pairs", }, // Line continuation { name: "line continuation", code: "a b^\nc", node: &Chunk{}, want: ast{ "Chunk/Pipeline/Form", fs{"Head": "a", "Args": []string{"b", "c"}}}, }, { name: "unterminated line continuation", code: `a ^`, node: &Chunk{}, wantErrAtEnd: true, wantErrMsg: "should be newline", }, // Carriage return { name: "carriage return separating pipelines", code: "a\rb", node: &Chunk{}, want: ast{"Chunk", fs{"Pipelines": []string{"a", "b"}}}, }, { name: "carriage return + newline separating pipelines", code: "a\r\nb", node: &Chunk{}, want: ast{"Chunk", fs{"Pipelines": []string{"a", "b"}}}, }, { name: "carriage return as whitespace padding in lambdas", code: "a { \rfoo\r\nbar }", node: &Chunk{}, want: a( ast{"Compound/Indexing/Primary", fs{"Type": Lambda, "Chunk": "foo\r\nbar "}}, ), }, { name: "carriage return separating elements in a lists", code: "a [a\rb]", node: &Chunk{}, want: a( ast{"Compound/Indexing/Primary", fs{ "Type": List, "Elements": []string{"a", "b"}}}), }, { name: "carriage return in line continuation", code: "a b^\rc", node: &Chunk{}, want: ast{ "Chunk/Pipeline/Form", fs{"Head": "a", "Args": []string{"b", "c"}}}, }, { name: "carriage return + newline as a single newline in line continuation", code: "a b^\r\nc", node: &Chunk{}, want: ast{ "Chunk/Pipeline/Form", fs{"Head": "a", "Args": []string{"b", "c"}}}, }, // Comment { name: "comments in chunks", code: "a#haha\nb#lala", node: &Chunk{}, want: ast{ "Chunk", fs{"Pipelines": []ast{ {"Pipeline/Form", fs{"Head": "a"}}, {"Pipeline/Form", fs{"Head": "b"}}, }}}, }, { name: "comments in lists", code: "a [a#haha\nb]", node: &Chunk{}, want: a( ast{"Compound/Indexing/Primary", fs{ "Type": List, "Elements": []string{"a", "b"}, }}, ), }, // Other errors { name: "unmatched )", code: ")", node: &Chunk{}, wantErrPart: ")", wantErrMsg: "unexpected rune ')'", }, { name: "unmatched ]", code: "]", node: &Chunk{}, wantErrPart: "]", wantErrMsg: "unexpected rune ']'", }, { name: "unmatched }", code: "}", node: &Chunk{}, wantErrPart: "}", wantErrMsg: "unexpected rune '}'", }, { name: "unmatched (", code: "a (", node: &Chunk{}, wantErrAtEnd: true, wantErrMsg: "should be ')'", }, { name: "unmatched [", code: "a [", node: &Chunk{}, wantErrAtEnd: true, wantErrMsg: "should be ']'", }, { name: "unmatched {", code: "a {", node: &Chunk{}, wantErrAtEnd: true, wantErrMsg: "should be ',' or '}'", }, { name: "unmatched { in lambda", code: "a { ", node: &Chunk{}, wantErrAtEnd: true, wantErrMsg: "should be '}'", }, { name: "unmatched [ in indexing expression", code: "a $a[0}", node: &Chunk{}, wantErrPart: "}", wantErrMsg: "should be ']'", }, } func TestParse(t *testing.T) { for _, test := range testCases { t.Run(test.name, func(t *testing.T) { n := test.node src := SourceForTest(test.code) err := ParseAs(src, n, Config{}) if test.wantErrMsg == "" { if err != nil { t.Errorf("Parse(%q) returns error: %v", test.code, err) } err = checkParseTree(n) if err != nil { t.Errorf("Parse(%q) returns bad parse tree: %v", test.code, err) fmt.Fprintf(os.Stderr, "Parse tree of %q:\n", test.code) pprintParseTree(n, os.Stderr) } err = checkAST(n, test.want) if err != nil { t.Errorf("Parse(%q) returns bad AST: %v", test.code, err) fmt.Fprintf(os.Stderr, "AST of %q:\n", test.code) pprintAST(n, os.Stderr) } } else { if err == nil { t.Errorf("Parse(%q) returns no error, want error with %q", test.code, test.wantErrMsg) } parseError := err.(*Error).Entries[0] r := parseError.Context if errPart := test.code[r.From:r.To]; errPart != test.wantErrPart { t.Errorf("Parse(%q) returns error with part %q, want %q", test.code, errPart, test.wantErrPart) } if atEnd := r.From == len(test.code); atEnd != test.wantErrAtEnd { t.Errorf("Parse(%q) returns error at end = %v, want %v", test.code, atEnd, test.wantErrAtEnd) } if errMsg := parseError.Message; errMsg != test.wantErrMsg { t.Errorf("Parse(%q) returns error with message %q, want %q", test.code, errMsg, test.wantErrMsg) } } }) } } func TestParse_ReturnsTreeContainingSourceFromArgument(t *testing.T) { src := SourceForTest("a") tree, _ := Parse(src, Config{}) if tree.Source != src { t.Errorf("tree.Source = %v, want %v", tree.Source, src) } } elvish-0.17.0/pkg/parse/parser.go000066400000000000000000000050621415471104000166070ustar00rootroot00000000000000package parse import ( "bytes" "errors" "fmt" "io" "reflect" "strings" "unicode/utf8" "src.elv.sh/pkg/diag" ) // parser maintains some mutable states of parsing. // // NOTE: The str member is assumed to be valid UF-8. type parser struct { srcName string src string pos int overEOF int errors Error warn io.Writer } func (ps *parser) parse(n Node) parsed { begin := ps.pos n.n().From = begin n.parse(ps) n.n().To = ps.pos n.n().sourceText = ps.src[begin:ps.pos] return parsed{n} } var nodeType = reflect.TypeOf((*Node)(nil)).Elem() type parsed struct { n Node } func (p parsed) addAs(ptr interface{}, parent Node) { dst := reflect.ValueOf(ptr).Elem() dst.Set(reflect.ValueOf(p.n)) // *ptr = p.n addChild(parent, p.n) } func (p parsed) addTo(ptr interface{}, parent Node) { dst := reflect.ValueOf(ptr).Elem() dst.Set(reflect.Append(dst, reflect.ValueOf(p.n))) // *ptr = append(*ptr, n) addChild(parent, p.n) } // Tells the parser that parsing is done. func (ps *parser) done() { if ps.pos != len(ps.src) { r, _ := utf8.DecodeRuneInString(ps.src[ps.pos:]) ps.error(fmt.Errorf("unexpected rune %q", r)) } } // Assembles all parsing errors as one, or returns nil if there were no errors. func (ps *parser) assembleError() error { if len(ps.errors.Entries) > 0 { return &ps.errors } return nil } const eof rune = -1 func (ps *parser) peek() rune { if ps.pos == len(ps.src) { return eof } r, _ := utf8.DecodeRuneInString(ps.src[ps.pos:]) return r } func (ps *parser) hasPrefix(prefix string) bool { return strings.HasPrefix(ps.src[ps.pos:], prefix) } func (ps *parser) next() rune { if ps.pos == len(ps.src) { ps.overEOF++ return eof } r, s := utf8.DecodeRuneInString(ps.src[ps.pos:]) ps.pos += s return r } func (ps *parser) backup() { if ps.overEOF > 0 { ps.overEOF-- return } _, s := utf8.DecodeLastRuneInString(ps.src[:ps.pos]) ps.pos -= s } func (ps *parser) errorp(r diag.Ranger, e error) { ps.errors.add(e.Error(), diag.NewContext(ps.srcName, ps.src, r)) } func (ps *parser) error(e error) { end := ps.pos if end < len(ps.src) { end++ } ps.errorp(diag.Ranging{From: ps.pos, To: end}, e) } func newError(text string, shouldbe ...string) error { if len(shouldbe) == 0 { return errors.New(text) } var buf bytes.Buffer if len(text) > 0 { buf.WriteString(text + ", ") } buf.WriteString("should be " + shouldbe[0]) for i, opt := range shouldbe[1:] { if i == len(shouldbe)-2 { buf.WriteString(" or ") } else { buf.WriteString(", ") } buf.WriteString(opt) } return errors.New(buf.String()) } elvish-0.17.0/pkg/parse/parseutil/000077500000000000000000000000001415471104000167715ustar00rootroot00000000000000elvish-0.17.0/pkg/parse/parseutil/parseutil.go000066400000000000000000000021411415471104000213260ustar00rootroot00000000000000// Package parseutil contains utilities built on top of the parse package. package parseutil import ( "strings" "src.elv.sh/pkg/parse" ) // FindLeafNode finds the leaf node at a specific position. It returns nil if // position is out of bound. func FindLeafNode(n parse.Node, p int) parse.Node { descend: for len(parse.Children(n)) > 0 { for _, ch := range parse.Children(n) { if ch.Range().From <= p && p <= ch.Range().To { n = ch continue descend } } return nil } return n } // Wordify turns a piece of source code into words. func Wordify(src string) []string { tree, _ := parse.Parse(parse.Source{Code: src}, parse.Config{}) return wordifyInner(tree.Root, nil) } func wordifyInner(n parse.Node, words []string) []string { if len(parse.Children(n)) == 0 || isCompound(n) { text := parse.SourceText(n) if strings.TrimFunc(text, parse.IsWhitespace) != "" { return append(words, text) } return words } for _, ch := range parse.Children(n) { words = wordifyInner(ch, words) } return words } func isCompound(n parse.Node) bool { _, ok := n.(*parse.Compound) return ok } elvish-0.17.0/pkg/parse/parseutil/parseutil_test.go000066400000000000000000000001641415471104000223700ustar00rootroot00000000000000package parseutil import "testing" func Test(t *testing.T) { // Required to get accurate test coverage report. } elvish-0.17.0/pkg/parse/pprint.go000066400000000000000000000064101415471104000166250ustar00rootroot00000000000000package parse import ( "fmt" "io" "reflect" "strconv" ) const ( maxL = 10 maxR = 10 indentInc = 2 ) // Pretty-prints the AST part of a Node to a Writer. func pprintAST(n Node, w io.Writer) { pprintASTRec(n, w, 0, "") } type field struct { name string tag reflect.StructTag value interface{} } var zeroValue reflect.Value func pprintASTRec(n Node, wr io.Writer, indent int, leading string) { nodeType := reflect.TypeOf((*Node)(nil)).Elem() var childFields, childrenFields, propertyFields []field nt := reflect.TypeOf(n).Elem() nv := reflect.ValueOf(n).Elem() for i := 0; i < nt.NumField(); i++ { f := nt.Field(i) if f.Anonymous { // embedded node struct, skip continue } ft := f.Type fv := nv.Field(i) if ft.Kind() == reflect.Slice { // list of children if ft.Elem().Implements(nodeType) { childrenFields = append(childrenFields, field{f.Name, f.Tag, fv.Interface()}) continue } } else if child, ok := fv.Interface().(Node); ok { // a child node if reflect.Indirect(fv) != zeroValue { childFields = append(childFields, field{f.Name, f.Tag, child}) } continue } // a property propertyFields = append(propertyFields, field{f.Name, f.Tag, fv.Interface()}) } // has only one child and nothing more : coalesce if len(Children(n)) == 1 && SourceText(Children(n)[0]) == SourceText(n) { pprintASTRec(Children(n)[0], wr, indent, leading+nt.Name()+"/") return } // print heading //b := n.n() //fmt.Fprintf(wr, "%*s%s%s %s %d-%d", indent, "", // wr.leading, nt.Name(), compactQuote(b.source(src)), b.begin, b.end) fmt.Fprintf(wr, "%*s%s%s", indent, "", leading, nt.Name()) // print properties for _, pf := range propertyFields { fmtstring := pf.tag.Get("fmt") if len(fmtstring) > 0 { fmt.Fprintf(wr, " %s="+fmtstring, pf.name, pf.value) } else { value := pf.value if s, ok := value.(string); ok { value = compactQuote(s) } fmt.Fprintf(wr, " %s=%v", pf.name, value) } } fmt.Fprint(wr, "\n") // print lone children recursively for _, chf := range childFields { // TODO the name is omitted pprintASTRec(chf.value.(Node), wr, indent+indentInc, "") } // print children list recursively for _, chf := range childrenFields { children := reflect.ValueOf(chf.value) if children.Len() == 0 { continue } // fmt.Fprintf(wr, "%*s.%s:\n", indent, "", chf.name) for i := 0; i < children.Len(); i++ { n := children.Index(i).Interface().(Node) pprintASTRec(n, wr, indent+indentInc, "") } } } // Pretty-prints the parse tree part of a Node to a Writer. func pprintParseTree(n Node, w io.Writer) { pprintParseTreeRec(n, w, 0) } func pprintParseTreeRec(n Node, wr io.Writer, indent int) { leading := "" for len(Children(n)) == 1 { leading += reflect.TypeOf(n).Elem().Name() + "/" n = Children(n)[0] } fmt.Fprintf(wr, "%*s%s%s\n", indent, "", leading, summary(n)) for _, ch := range Children(n) { pprintParseTreeRec(ch, wr, indent+indentInc) } } func summary(n Node) string { return fmt.Sprintf("%s %s %d-%d", reflect.TypeOf(n).Elem().Name(), compactQuote(SourceText(n)), n.Range().From, n.Range().To) } func compactQuote(text string) string { if len(text) > maxL+maxR+3 { text = text[0:maxL] + "..." + text[len(text)-maxR:] } return strconv.Quote(text) } elvish-0.17.0/pkg/parse/pprint_test.go000066400000000000000000000046111415471104000176650ustar00rootroot00000000000000package parse import ( "strings" "testing" "src.elv.sh/pkg/tt" ) var n = mustParse("ls $x[0]$y[1];echo done >/redir-dest") var pprintASTTests = tt.Table{ tt.Args(n).Rets( `Chunk Pipeline/Form Compound/Indexing/Primary ExprCtx=CmdExpr Type=Bareword LegacyLambda=false Value="ls" Compound ExprCtx=NormalExpr Indexing ExprCtx=NormalExpr Primary ExprCtx=NormalExpr Type=Variable LegacyLambda=false Value="x" Array/Compound/Indexing/Primary ExprCtx=NormalExpr Type=Bareword LegacyLambda=false Value="0" Indexing ExprCtx=NormalExpr Primary ExprCtx=NormalExpr Type=Variable LegacyLambda=false Value="y" Array/Compound/Indexing/Primary ExprCtx=NormalExpr Type=Bareword LegacyLambda=false Value="1" Pipeline/Form Compound/Indexing/Primary ExprCtx=CmdExpr Type=Bareword LegacyLambda=false Value="echo" Compound/Indexing/Primary ExprCtx=NormalExpr Type=Bareword LegacyLambda=false Value="done" Redir Mode=Write RightIsFd=false Compound/Indexing/Primary ExprCtx=NormalExpr Type=Bareword LegacyLambda=false Value="/redir-dest" `), } func TestPPrintAST(t *testing.T) { tt.Test(t, tt.Fn("PPrintAST (to string)", func(n Node) string { var b strings.Builder pprintAST(n, &b) return b.String() }), pprintASTTests) } var pprintParseTreeTests = tt.Table{ tt.Args(n).Rets( `Chunk "ls $x[0]$y...redir-dest" 0-36 Pipeline/Form "ls $x[0]$y[1]" 0-13 Compound/Indexing/Primary "ls" 0-2 Sep " " 2-3 Compound "$x[0]$y[1]" 3-13 Indexing "$x[0]" 3-8 Primary "$x" 3-5 Sep "[" 5-6 Array/Compound/Indexing/Primary "0" 6-7 Sep "]" 7-8 Indexing "$y[1]" 8-13 Primary "$y" 8-10 Sep "[" 10-11 Array/Compound/Indexing/Primary "1" 11-12 Sep "]" 12-13 Sep ";" 13-14 Pipeline/Form "echo done >/redir-dest" 14-36 Compound/Indexing/Primary "echo" 14-18 Sep " " 18-19 Compound/Indexing/Primary "done" 19-23 Sep " " 23-24 Redir ">/redir-dest" 24-36 Sep ">" 24-25 Compound/Indexing/Primary "/redir-dest" 25-36 `), } func TestPPrintParseTree(t *testing.T) { tt.Test(t, tt.Fn("PPrintParseTree (to string)", func(n Node) string { var b strings.Builder pprintParseTree(n, &b) return b.String() }), pprintParseTreeTests) } func mustParse(src string) Node { tree, err := Parse(SourceForTest(src), Config{}) if err != nil { panic(err) } return tree.Root } elvish-0.17.0/pkg/parse/quote.go000066400000000000000000000052061415471104000164500ustar00rootroot00000000000000package parse import ( "bytes" "unicode" ) // Quote returns a valid Elvish expression that evaluates to the given string. // If s is a valid bareword, it is returned as is; otherwise it is quoted, // preferring the use of single quotes. func Quote(s string) string { s, _ = QuoteAs(s, Bareword) return s } // QuoteVariableName is like Quote, but quotes s if it contains any character // that may not appear unquoted in variable names. func QuoteVariableName(s string) string { if s == "" { return "''" } // Keep track of whether it is a valid (unquoted) variable name. bare := true for _, r := range s { if !unicode.IsPrint(r) { // Contains unprintable character; force double quote. return quoteDouble(s) } if !allowedInVariableName(r) { bare = false break } } if bare { return s } return quoteSingle(s) } // QuoteAs returns a representation of s in elvish syntax, preferring the syntax // specified by q, which must be one of Bareword, SingleQuoted, or DoubleQuoted. // It returns the quoted string and the actual quoting. func QuoteAs(s string, q PrimaryType) (string, PrimaryType) { if q == DoubleQuoted { // Everything can be quoted using double quotes, return directly. return quoteDouble(s), DoubleQuoted } if s == "" { return "''", SingleQuoted } // Keep track of whether it is a valid bareword. bare := s[0] != '~' for _, r := range s { if !unicode.IsPrint(r) { // Contains unprintable character; force double quote. return quoteDouble(s), DoubleQuoted } if !allowedInBareword(r, strictExpr) { bare = false } } if q == Bareword && bare { return s, Bareword } return quoteSingle(s), SingleQuoted } func quoteSingle(s string) string { var buf bytes.Buffer buf.WriteByte('\'') for _, r := range s { buf.WriteRune(r) if r == '\'' { buf.WriteByte('\'') } } buf.WriteByte('\'') return buf.String() } func rtohex(r rune, w int) []byte { bytes := make([]byte, w) for i := w - 1; i >= 0; i-- { d := byte(r % 16) r /= 16 if d <= 9 { bytes[i] = '0' + d } else { bytes[i] = 'a' + d - 10 } } return bytes } func quoteDouble(s string) string { var buf bytes.Buffer buf.WriteByte('"') for _, r := range s { if e, ok := doubleUnescape[r]; ok { // Takes care of " and \ as well. buf.WriteByte('\\') buf.WriteRune(e) } else if !unicode.IsPrint(r) { buf.WriteByte('\\') if r <= 0xff { buf.WriteByte('x') buf.Write(rtohex(r, 2)) } else if r <= 0xffff { buf.WriteByte('u') buf.Write(rtohex(r, 4)) } else { buf.WriteByte('U') buf.Write(rtohex(r, 8)) } } else { buf.WriteRune(r) } } buf.WriteByte('"') return buf.String() } elvish-0.17.0/pkg/parse/quote_test.go000066400000000000000000000033341415471104000175070ustar00rootroot00000000000000package parse import ( "testing" . "src.elv.sh/pkg/tt" ) func TestQuote(t *testing.T) { Test(t, Fn("Quote", Quote).ArgsFmt("(%q)").RetsFmt("%q"), Table{ // Empty string is single-quoted. Args("").Rets(`''`), // Bareword when possible. Args("x-y:z@h/d").Rets("x-y:z@h/d"), // Single quote when there are special characters but no unprintable // characters. Args("x$y[]ef'").Rets("'x$y[]ef'''"), // Tilde needs quoting only leading the expression. Args("~x").Rets("'~x'"), Args("x~").Rets("x~"), // Double quote when there is unprintable char. Args("a\nb").Rets(`"a\nb"`), Args("\x1b\"\\").Rets(`"\e\"\\"`), Args("\x00").Rets(`"\x00"`), Args("\u0600").Rets(`"\u0600"`), // Arabic number sign Args("\U000110BD").Rets(`"\U000110bd"`), // Kathi number sign // Commas and equal signs are always quoted, so that the quoted string is // safe for use everywhere. Args("a,b").Rets(`'a,b'`), Args("a=b").Rets(`'a=b'`), }) } func TestQuoteAs(t *testing.T) { Test(t, Fn("QuoteAs", QuoteAs).ArgsFmt("(%q, %s)").RetsFmt("(%q, %s)"), Table{ // DoubleQuote is always respected. Args("", DoubleQuoted).Rets(`""`, DoubleQuoted), Args("a", DoubleQuoted).Rets(`"a"`, DoubleQuoted), // SingleQuoted is respected when there is no unprintable character. Args("", SingleQuoted).Rets(`''`, SingleQuoted), Args("a", SingleQuoted).Rets(`'a'`, SingleQuoted), Args("\n", SingleQuoted).Rets(`"\n"`, DoubleQuoted), // Bareword tested above in TestQuote. }) } func TestQuoteVariableName(t *testing.T) { Test(t, Fn("QuoteVariableName", QuoteVariableName).ArgsFmt("(%q)").RetsFmt("%q"), Table{ Args("").Rets("''"), Args("foo").Rets("foo"), Args("a/b").Rets("'a/b'"), Args("\x1b").Rets(`"\e"`), }) } elvish-0.17.0/pkg/parse/source.go000066400000000000000000000013351415471104000166120ustar00rootroot00000000000000package parse import ( "fmt" ) // TODO(xiaq): Move this into the diag package after implementing phantom types. // Source describes a piece of source code. type Source struct { Name string Code string IsFile bool } // SourceForTest returns a Source used for testing. func SourceForTest(code string) Source { return Source{Name: "[test]", Code: code} } // IsStructMap marks that Source is a structmap. func (src Source) IsStructMap() {} // Repr returns the representation of Source as if it were a map, except that // the code field is replaced by "...", since it is typically very large. func (src Source) Repr(int) string { return fmt.Sprintf( "[&name=%s &code=<...> &is-file=$%v]", Quote(src.Name), src.IsFile) } elvish-0.17.0/pkg/parse/source_test.go000066400000000000000000000006401415471104000176470ustar00rootroot00000000000000package parse_test import ( "testing" "src.elv.sh/pkg/eval/vals" . "src.elv.sh/pkg/parse" ) func TestSourceAsStructMap(t *testing.T) { vals.TestValue(t, Source{Name: "[tty]", Code: "echo"}). Kind("structmap"). Repr("[&name='[tty]' &code=<...> &is-file=$false]"). AllKeys("name", "code", "is-file") vals.TestValue(t, Source{Name: "/etc/rc.elv", Code: "echo", IsFile: true}). Index("is-file", true) } elvish-0.17.0/pkg/parse/string.go000066400000000000000000000043711415471104000166230ustar00rootroot00000000000000// Code generated by "stringer -type=PrimaryType,RedirMode,ExprCtx -output=string.go"; DO NOT EDIT. package parse import "strconv" func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} _ = x[BadPrimary-0] _ = x[Bareword-1] _ = x[SingleQuoted-2] _ = x[DoubleQuoted-3] _ = x[Variable-4] _ = x[Wildcard-5] _ = x[Tilde-6] _ = x[ExceptionCapture-7] _ = x[OutputCapture-8] _ = x[List-9] _ = x[Lambda-10] _ = x[Map-11] _ = x[Braced-12] } const _PrimaryType_name = "BadPrimaryBarewordSingleQuotedDoubleQuotedVariableWildcardTildeExceptionCaptureOutputCaptureListLambdaMapBraced" var _PrimaryType_index = [...]uint8{0, 10, 18, 30, 42, 50, 58, 63, 79, 92, 96, 102, 105, 111} func (i PrimaryType) String() string { if i < 0 || i >= PrimaryType(len(_PrimaryType_index)-1) { return "PrimaryType(" + strconv.FormatInt(int64(i), 10) + ")" } return _PrimaryType_name[_PrimaryType_index[i]:_PrimaryType_index[i+1]] } func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} _ = x[BadRedirMode-0] _ = x[Read-1] _ = x[Write-2] _ = x[ReadWrite-3] _ = x[Append-4] } const _RedirMode_name = "BadRedirModeReadWriteReadWriteAppend" var _RedirMode_index = [...]uint8{0, 12, 16, 21, 30, 36} func (i RedirMode) String() string { if i < 0 || i >= RedirMode(len(_RedirMode_index)-1) { return "RedirMode(" + strconv.FormatInt(int64(i), 10) + ")" } return _RedirMode_name[_RedirMode_index[i]:_RedirMode_index[i+1]] } func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} _ = x[NormalExpr-0] _ = x[CmdExpr-1] _ = x[LHSExpr-2] _ = x[BracedElemExpr-3] _ = x[strictExpr-4] } const _ExprCtx_name = "NormalExprCmdExprLHSExprBracedElemExprstrictExpr" var _ExprCtx_index = [...]uint8{0, 10, 17, 24, 38, 48} func (i ExprCtx) String() string { if i < 0 || i >= ExprCtx(len(_ExprCtx_index)-1) { return "ExprCtx(" + strconv.FormatInt(int64(i), 10) + ")" } return _ExprCtx_name[_ExprCtx_index[i]:_ExprCtx_index[i+1]] } elvish-0.17.0/pkg/persistent/000077500000000000000000000000001415471104000160475ustar00rootroot00000000000000elvish-0.17.0/pkg/persistent/.gitignore000066400000000000000000000005251415471104000200410ustar00rootroot00000000000000/cover [._]*.s[a-w][a-z] [._]s[a-w][a-z] *.un~ Session.vim .netrwhist *~ # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof elvish-0.17.0/pkg/persistent/.travis.yml000066400000000000000000000001451415471104000201600ustar00rootroot00000000000000language: go go: - 1.14 - 1.15 sudo: false os: - linux - osx script: make travis elvish-0.17.0/pkg/persistent/LICENSE000066400000000000000000000260701415471104000170610ustar00rootroot00000000000000Eclipse Public License - v 1.0 THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 1. DEFINITIONS "Contribution" means: a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and b) in the case of each subsequent Contributor: i) changes to the Program, and ii) additions to the Program; where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program. "Contributor" means any person or entity that distributes the Program. "Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. "Program" means the Contributions distributed in accordance with this Agreement. "Recipient" means anyone who receives the Program under this Agreement, including all Contributors. 2. GRANT OF RIGHTS a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form. b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. 3. REQUIREMENTS A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that: a) it complies with the terms and conditions of this Agreement; and b) its license agreement: i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. When the Program is made available in source code form: a) it must be made available under this Agreement; and b) a copy of this Agreement must be included with each copy of the Program. Contributors may not remove or alter any copyright notices contained within the Program. Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution. 4. COMMERCIAL DISTRIBUTION Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. 5. NO WARRANTY EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement, including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. 6. DISCLAIMER OF LIABILITY EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 7. GENERAL If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation. elvish-0.17.0/pkg/persistent/README.md000066400000000000000000000072631415471104000173360ustar00rootroot00000000000000# Persistent data structure in Go This is a Go clone of Clojure's persistent data structures. The API is not stable yet. **DO NOT USE** unless you are willing to cope with API changes. License is [Eclipse Public License 1.0](http://opensource.org/licenses/eclipse-1.0.php) (like Clojure). ## Implementation notes The list provided here is a singly-linked list and is very trivial to implement. The implementation of persistent vector and hash map and based on a series of [excellent](http://blog.higher-order.net/2009/02/01/understanding-clojures-persistentvector-implementation) [blog](http://blog.higher-order.net/2009/09/08/understanding-clojures-persistenthashmap-deftwice) [posts](http://blog.higher-order.net/2010/08/16/assoc-and-clojures-persistenthashmap-part-ii.html) as well as the Clojure source code. Despite the hash map appearing more complicated, the vector is slightly harder to implement due to the "tail array" optimization and some tricky transformation of the tree structure, which is fully replicated here. ## Benchmarking results ### Vectors Compared to native slices, - Adding elements is anywhere from 2x to 8x as slow. - Sequential read is about 9x as slow. - Random read is about 7x as slow. Benchmarked on an early 2015 MacBook Pro, with Go 1.9: ``` goos: darwin goarch: amd64 pkg: github.com/xiaq/persistent/vector BenchmarkConsNativeN1-4 1000000 2457 ns/op BenchmarkConsNativeN2-4 300000 4418 ns/op BenchmarkConsNativeN3-4 30000 55424 ns/op BenchmarkConsNativeN4-4 300 4493289 ns/op BenchmarkConsPersistentN1-4 100000 12250 ns/op 4.99x BenchmarkConsPersistentN2-4 50000 26394 ns/op 5.97x BenchmarkConsPersistentN3-4 3000 452146 ns/op 8.16x BenchmarkConsPersistentN4-4 100 13057887 ns/op 2.91x BenchmarkNthSeqNativeN4-4 30000 43156 ns/op BenchmarkNthSeqPersistentN4-4 3000 399193 ns/op 9.25x BenchmarkNthRandNative-4 20000 73860 ns/op BenchmarkNthRandPersistent-4 3000 546124 ns/op 7.39x BenchmarkEqualNative-4 50000 23828 ns/op BenchmarkEqualPersistent-4 2000 1020893 ns/op 42.84x ``` ### Hash map Compared to native maps, adding elements is about 3-6x slow. Difference is more pronunced when keys are sequential integers, but that workload is very rare in the real world. Benchmarked on an early 2015 MacBook Pro, with Go 1.9: ``` goos: darwin goarch: amd64 pkg: github.com/xiaq/persistent/hashmap BenchmarkSequentialConsNative1-4 300000 4143 ns/op BenchmarkSequentialConsNative2-4 10000 130423 ns/op BenchmarkSequentialConsNative3-4 300 4600842 ns/op BenchmarkSequentialConsPersistent1-4 100000 14005 ns/op 3.38x BenchmarkSequentialConsPersistent2-4 2000 641820 ns/op 4.92x BenchmarkSequentialConsPersistent3-4 20 55180306 ns/op 11.99x BenchmarkRandomStringsConsNative1-4 200000 7536 ns/op BenchmarkRandomStringsConsNative2-4 5000 264489 ns/op BenchmarkRandomStringsConsNative3-4 100 12132244 ns/op BenchmarkRandomStringsConsPersistent1-4 50000 29109 ns/op 3.86x BenchmarkRandomStringsConsPersistent2-4 1000 1327321 ns/op 5.02x BenchmarkRandomStringsConsPersistent3-4 20 74204196 ns/op 6.12x ``` elvish-0.17.0/pkg/persistent/add-slowdown000077500000000000000000000020211415471104000203720ustar00rootroot00000000000000#!/usr/bin/env elvish # Parse an output of "go test -bench .", annotating benchmark results for # persistent operations with the slowdown ratio compared to their native # counterparts. use re fn extract [line]{ # Extract the name and ns/op of a benchmark entry. fields = [(re:split '\s+' $line)] if (not (eq $fields[-1] ns/op)) { fail 'Last column of '(repr $line)' not ns/op' } put $fields[0] $fields[-2] } native = [&] each [line]{ if (re:match Native $line) { # Remember the result so that it can be used later. name data = (extract $line) native[$name] = $data } elif (re:match Persistent $line) { # Calculate slowdown and append to the end of the line. name data = (extract $line) native-name = (re:replace Persistent Native $name) if (not (has-key $native $native-name)) { fail 'Native counterpart for '$name' not found' } line = $line' '(printf '%.2f' (/ $data $native[$native-name]))'x' } echo $line } elvish-0.17.0/pkg/persistent/hash/000077500000000000000000000000001415471104000167725ustar00rootroot00000000000000elvish-0.17.0/pkg/persistent/hash/hash.go000066400000000000000000000017771415471104000202600ustar00rootroot00000000000000// Package hash contains some common hash functions suitable for use in hash // maps. package hash import "unsafe" const DJBInit uint32 = 5381 func DJBCombine(acc, h uint32) uint32 { return mul33(acc) + h } func DJB(hs ...uint32) uint32 { acc := DJBInit for _, h := range hs { acc = DJBCombine(acc, h) } return acc } func UInt32(u uint32) uint32 { return u } func UInt64(u uint64) uint32 { return mul33(uint32(u>>32)) + uint32(u&0xffffffff) } func Pointer(p unsafe.Pointer) uint32 { switch unsafe.Sizeof(p) { case 4: return UInt32(uint32(uintptr(p))) case 8: return UInt64(uint64(uintptr(p))) default: panic("unhandled pointer size") } } func UIntPtr(p uintptr) uint32 { switch unsafe.Sizeof(p) { case 4: return UInt32(uint32(p)) case 8: return UInt64(uint64(p)) default: panic("unhandled pointer size") } } func String(s string) uint32 { h := DJBInit for i := 0; i < len(s); i++ { h = DJBCombine(h, uint32(s[i])) } return h } func mul33(u uint32) uint32 { return u<<5 + u } elvish-0.17.0/pkg/persistent/hashmap/000077500000000000000000000000001415471104000174705ustar00rootroot00000000000000elvish-0.17.0/pkg/persistent/hashmap/hashmap.go000066400000000000000000000345211415471104000214450ustar00rootroot00000000000000// Package hashmap implements persistent hashmap. package hashmap import ( "bytes" "encoding" "encoding/json" "fmt" "reflect" "strconv" ) const ( chunkBits = 5 nodeCap = 1 << chunkBits chunkMask = nodeCap - 1 ) // Equal is the type of a function that reports whether two keys are equal. type Equal func(k1, k2 interface{}) bool // Hash is the type of a function that returns the hash code of a key. type Hash func(k interface{}) uint32 // New takes an equality function and a hash function, and returns an empty // Map. func New(e Equal, h Hash) Map { return &hashMap{0, emptyBitmapNode, nil, e, h} } type hashMap struct { count int root node nilV *interface{} equal Equal hash Hash } func (m *hashMap) Len() int { return m.count } func (m *hashMap) Index(k interface{}) (interface{}, bool) { if k == nil { if m.nilV == nil { return nil, false } return *m.nilV, true } return m.root.find(0, m.hash(k), k, m.equal) } func (m *hashMap) Assoc(k, v interface{}) Map { if k == nil { newCount := m.count if m.nilV == nil { newCount++ } return &hashMap{newCount, m.root, &v, m.equal, m.hash} } newRoot, added := m.root.assoc(0, m.hash(k), k, v, m.hash, m.equal) newCount := m.count if added { newCount++ } return &hashMap{newCount, newRoot, m.nilV, m.equal, m.hash} } func (m *hashMap) Dissoc(k interface{}) Map { if k == nil { newCount := m.count if m.nilV != nil { newCount-- } return &hashMap{newCount, m.root, nil, m.equal, m.hash} } newRoot, deleted := m.root.without(0, m.hash(k), k, m.equal) newCount := m.count if deleted { newCount-- } return &hashMap{newCount, newRoot, m.nilV, m.equal, m.hash} } func (m *hashMap) Iterator() Iterator { if m.nilV != nil { return &nilVIterator{true, *m.nilV, m.root.iterator()} } return m.root.iterator() } type nilVIterator struct { atNil bool nilV interface{} tail Iterator } func (it *nilVIterator) Elem() (interface{}, interface{}) { if it.atNil { return nil, it.nilV } return it.tail.Elem() } func (it *nilVIterator) HasElem() bool { return it.atNil || it.tail.HasElem() } func (it *nilVIterator) Next() { if it.atNil { it.atNil = false } else { it.tail.Next() } } func (m *hashMap) MarshalJSON() ([]byte, error) { var buf bytes.Buffer buf.WriteByte('{') first := true for it := m.Iterator(); it.HasElem(); it.Next() { if first { first = false } else { buf.WriteByte(',') } k, v := it.Elem() kString, err := convertKey(k) if err != nil { return nil, err } kBytes, err := json.Marshal(kString) if err != nil { return nil, err } vBytes, err := json.Marshal(v) if err != nil { return nil, err } buf.Write(kBytes) buf.WriteByte(':') buf.Write(vBytes) } buf.WriteByte('}') return buf.Bytes(), nil } // convertKey converts a map key to a string. The implementation matches the // behavior of how json.Marshal encodes keys of the builtin map type. func convertKey(k interface{}) (string, error) { kref := reflect.ValueOf(k) if kref.Kind() == reflect.String { return kref.String(), nil } if t, ok := k.(encoding.TextMarshaler); ok { b2, err := t.MarshalText() if err != nil { return "", err } return string(b2), nil } switch kref.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return strconv.FormatInt(kref.Int(), 10), nil case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return strconv.FormatUint(kref.Uint(), 10), nil } return "", fmt.Errorf("unsupported key type %T", k) } // node is an interface for all nodes in the hash map tree. type node interface { // assoc adds a new pair of key and value. It returns the new node, and // whether the key did not exist before (i.e. a new pair has been added, // instead of replaced). assoc(shift, hash uint32, k, v interface{}, h Hash, eq Equal) (node, bool) // without removes a key. It returns the new node and whether the key did // not exist before (i.e. a key was indeed removed). without(shift, hash uint32, k interface{}, eq Equal) (node, bool) // find finds the value for a key. It returns the found value (if any) and // whether such a pair exists. find(shift, hash uint32, k interface{}, eq Equal) (interface{}, bool) // iterator returns an iterator. iterator() Iterator } // arrayNode stores all of its children in an array. The array is always at // least 1/4 full, otherwise it will be packed into a bitmapNode. type arrayNode struct { nChildren int children [nodeCap]node } func (n *arrayNode) withNewChild(i uint32, newChild node, d int) *arrayNode { newChildren := n.children newChildren[i] = newChild return &arrayNode{n.nChildren + d, newChildren} } func (n *arrayNode) assoc(shift, hash uint32, k, v interface{}, h Hash, eq Equal) (node, bool) { idx := chunk(shift, hash) child := n.children[idx] if child == nil { newChild, _ := emptyBitmapNode.assoc(shift+chunkBits, hash, k, v, h, eq) return n.withNewChild(idx, newChild, 1), true } newChild, added := child.assoc(shift+chunkBits, hash, k, v, h, eq) return n.withNewChild(idx, newChild, 0), added } func (n *arrayNode) without(shift, hash uint32, k interface{}, eq Equal) (node, bool) { idx := chunk(shift, hash) child := n.children[idx] if child == nil { return n, false } newChild, _ := child.without(shift+chunkBits, hash, k, eq) if newChild == child { return n, false } if newChild == emptyBitmapNode { if n.nChildren <= nodeCap/4 { // less than 1/4 full; shrink return n.pack(int(idx)), true } return n.withNewChild(idx, nil, -1), true } return n.withNewChild(idx, newChild, 0), true } func (n *arrayNode) pack(skip int) *bitmapNode { newNode := bitmapNode{0, make([]mapEntry, n.nChildren-1)} j := 0 for i, child := range n.children { // TODO(xiaq): Benchmark performance difference after unrolling this // into two loops without the if if i != skip && child != nil { newNode.bitmap |= 1 << uint(i) newNode.entries[j].value = child j++ } } return &newNode } func (n *arrayNode) find(shift, hash uint32, k interface{}, eq Equal) (interface{}, bool) { idx := chunk(shift, hash) child := n.children[idx] if child == nil { return nil, false } return child.find(shift+chunkBits, hash, k, eq) } func (n *arrayNode) iterator() Iterator { it := &arrayNodeIterator{n, 0, nil} it.fixCurrent() return it } type arrayNodeIterator struct { n *arrayNode index int current Iterator } func (it *arrayNodeIterator) fixCurrent() { for ; it.index < nodeCap && it.n.children[it.index] == nil; it.index++ { } if it.index < nodeCap { it.current = it.n.children[it.index].iterator() } else { it.current = nil } } func (it *arrayNodeIterator) Elem() (interface{}, interface{}) { return it.current.Elem() } func (it *arrayNodeIterator) HasElem() bool { return it.current != nil } func (it *arrayNodeIterator) Next() { it.current.Next() if !it.current.HasElem() { it.index++ it.fixCurrent() } } var emptyBitmapNode = &bitmapNode{} type bitmapNode struct { bitmap uint32 entries []mapEntry } // mapEntry is a map entry. When used in a collisionNode, it is also an entry // with non-nil key. When used in a bitmapNode, it is also abused to represent // children when the key is nil. type mapEntry struct { key interface{} value interface{} } func chunk(shift, hash uint32) uint32 { return (hash >> shift) & chunkMask } func bitpos(shift, hash uint32) uint32 { return 1 << chunk(shift, hash) } func index(bitmap, bit uint32) uint32 { return popCount(bitmap & (bit - 1)) } const ( m1 uint32 = 0x55555555 m2 uint32 = 0x33333333 m4 uint32 = 0x0f0f0f0f m8 uint32 = 0x00ff00ff m16 uint32 = 0x0000ffff ) // TODO(xiaq): Use an optimized implementation. func popCount(u uint32) uint32 { u = (u & m1) + ((u >> 1) & m1) u = (u & m2) + ((u >> 2) & m2) u = (u & m4) + ((u >> 4) & m4) u = (u & m8) + ((u >> 8) & m8) u = (u & m16) + ((u >> 16) & m16) return u } func createNode(shift uint32, k1 interface{}, v1 interface{}, h2 uint32, k2 interface{}, v2 interface{}, h Hash, eq Equal) node { h1 := h(k1) if h1 == h2 { return &collisionNode{h1, []mapEntry{{k1, v1}, {k2, v2}}} } n, _ := emptyBitmapNode.assoc(shift, h1, k1, v1, h, eq) n, _ = n.assoc(shift, h2, k2, v2, h, eq) return n } func (n *bitmapNode) unpack(shift, idx uint32, newChild node, h Hash, eq Equal) *arrayNode { var newNode arrayNode newNode.nChildren = len(n.entries) + 1 newNode.children[idx] = newChild j := 0 for i := uint(0); i < nodeCap; i++ { if (n.bitmap>>i)&1 != 0 { entry := n.entries[j] j++ if entry.key == nil { newNode.children[i] = entry.value.(node) } else { newNode.children[i], _ = emptyBitmapNode.assoc( shift+chunkBits, h(entry.key), entry.key, entry.value, h, eq) } } } return &newNode } func (n *bitmapNode) withoutEntry(bit, idx uint32) *bitmapNode { if n.bitmap == bit { return emptyBitmapNode } return &bitmapNode{n.bitmap ^ bit, withoutEntry(n.entries, idx)} } func withoutEntry(entries []mapEntry, idx uint32) []mapEntry { newEntries := make([]mapEntry, len(entries)-1) copy(newEntries[:idx], entries[:idx]) copy(newEntries[idx:], entries[idx+1:]) return newEntries } func (n *bitmapNode) withReplacedEntry(i uint32, entry mapEntry) *bitmapNode { return &bitmapNode{n.bitmap, replaceEntry(n.entries, i, entry.key, entry.value)} } func replaceEntry(entries []mapEntry, i uint32, k, v interface{}) []mapEntry { newEntries := append([]mapEntry(nil), entries...) newEntries[i] = mapEntry{k, v} return newEntries } func (n *bitmapNode) assoc(shift, hash uint32, k, v interface{}, h Hash, eq Equal) (node, bool) { bit := bitpos(shift, hash) idx := index(n.bitmap, bit) if n.bitmap&bit == 0 { // Entry does not exist yet nEntries := len(n.entries) if nEntries >= nodeCap/2 { // Unpack into an arrayNode newNode, _ := emptyBitmapNode.assoc(shift+chunkBits, hash, k, v, h, eq) return n.unpack(shift, chunk(shift, hash), newNode, h, eq), true } // Add a new entry newEntries := make([]mapEntry, len(n.entries)+1) copy(newEntries[:idx], n.entries[:idx]) newEntries[idx] = mapEntry{k, v} copy(newEntries[idx+1:], n.entries[idx:]) return &bitmapNode{n.bitmap | bit, newEntries}, true } // Entry exists entry := n.entries[idx] if entry.key == nil { // Non-leaf child child := entry.value.(node) newChild, added := child.assoc(shift+chunkBits, hash, k, v, h, eq) return n.withReplacedEntry(idx, mapEntry{nil, newChild}), added } // Leaf if eq(k, entry.key) { // Identical key, replace return n.withReplacedEntry(idx, mapEntry{k, v}), false } // Create and insert new inner node newNode := createNode(shift+chunkBits, entry.key, entry.value, hash, k, v, h, eq) return n.withReplacedEntry(idx, mapEntry{nil, newNode}), true } func (n *bitmapNode) without(shift, hash uint32, k interface{}, eq Equal) (node, bool) { bit := bitpos(shift, hash) if n.bitmap&bit == 0 { return n, false } idx := index(n.bitmap, bit) entry := n.entries[idx] if entry.key == nil { // Non-leaf child child := entry.value.(node) newChild, deleted := child.without(shift+chunkBits, hash, k, eq) if newChild == child { return n, false } if newChild == emptyBitmapNode { return n.withoutEntry(bit, idx), true } return n.withReplacedEntry(idx, mapEntry{nil, newChild}), deleted } else if eq(entry.key, k) { // Leaf, and this is the entry to delete. return n.withoutEntry(bit, idx), true } // Nothing to delete. return n, false } func (n *bitmapNode) find(shift, hash uint32, k interface{}, eq Equal) (interface{}, bool) { bit := bitpos(shift, hash) if n.bitmap&bit == 0 { return nil, false } idx := index(n.bitmap, bit) entry := n.entries[idx] if entry.key == nil { child := entry.value.(node) return child.find(shift+chunkBits, hash, k, eq) } else if eq(entry.key, k) { return entry.value, true } return nil, false } func (n *bitmapNode) iterator() Iterator { it := &bitmapNodeIterator{n, 0, nil} it.fixCurrent() return it } type bitmapNodeIterator struct { n *bitmapNode index int current Iterator } func (it *bitmapNodeIterator) fixCurrent() { if it.index < len(it.n.entries) { entry := it.n.entries[it.index] if entry.key == nil { it.current = entry.value.(node).iterator() } else { it.current = nil } } else { it.current = nil } } func (it *bitmapNodeIterator) Elem() (interface{}, interface{}) { if it.current != nil { return it.current.Elem() } entry := it.n.entries[it.index] return entry.key, entry.value } func (it *bitmapNodeIterator) HasElem() bool { return it.index < len(it.n.entries) } func (it *bitmapNodeIterator) Next() { if it.current != nil { it.current.Next() } if it.current == nil || !it.current.HasElem() { it.index++ it.fixCurrent() } } type collisionNode struct { hash uint32 entries []mapEntry } func (n *collisionNode) assoc(shift, hash uint32, k, v interface{}, h Hash, eq Equal) (node, bool) { if hash == n.hash { idx := n.findIndex(k, eq) if idx != -1 { return &collisionNode{ n.hash, replaceEntry(n.entries, uint32(idx), k, v)}, false } newEntries := make([]mapEntry, len(n.entries)+1) copy(newEntries[:len(n.entries)], n.entries[:]) newEntries[len(n.entries)] = mapEntry{k, v} return &collisionNode{n.hash, newEntries}, true } // Wrap in a bitmapNode and add the entry wrap := bitmapNode{bitpos(shift, n.hash), []mapEntry{{nil, n}}} return wrap.assoc(shift, hash, k, v, h, eq) } func (n *collisionNode) without(shift, hash uint32, k interface{}, eq Equal) (node, bool) { idx := n.findIndex(k, eq) if idx == -1 { return n, false } if len(n.entries) == 1 { return emptyBitmapNode, true } return &collisionNode{n.hash, withoutEntry(n.entries, uint32(idx))}, true } func (n *collisionNode) find(shift, hash uint32, k interface{}, eq Equal) (interface{}, bool) { idx := n.findIndex(k, eq) if idx == -1 { return nil, false } return n.entries[idx].value, true } func (n *collisionNode) findIndex(k interface{}, eq Equal) int { for i, entry := range n.entries { if eq(k, entry.key) { return i } } return -1 } func (n *collisionNode) iterator() Iterator { return &collisionNodeIterator{n, 0} } type collisionNodeIterator struct { n *collisionNode index int } func (it *collisionNodeIterator) Elem() (interface{}, interface{}) { entry := it.n.entries[it.index] return entry.key, entry.value } func (it *collisionNodeIterator) HasElem() bool { return it.index < len(it.n.entries) } func (it *collisionNodeIterator) Next() { it.index++ } elvish-0.17.0/pkg/persistent/hashmap/hashmap_test.go000066400000000000000000000223441415471104000225040ustar00rootroot00000000000000package hashmap import ( "math/rand" "reflect" "strconv" "testing" "time" "src.elv.sh/pkg/persistent/hash" ) const ( NSequential = 0x1000 NCollision = 0x100 NRandom = 0x4000 NReplace = 0x200 SmallRandomPass = 0x100 NSmallRandom = 0x400 SmallRandomHighBound = 0x50 SmallRandomLowBound = 0x200 NArrayNode = 0x100 NIneffectiveDissoc = 0x200 N1 = nodeCap + 1 N2 = nodeCap*nodeCap + 1 N3 = nodeCap*nodeCap*nodeCap + 1 ) type testKey uint64 type anotherTestKey uint32 func equalFunc(k1, k2 interface{}) bool { switch k1 := k1.(type) { case testKey: t2, ok := k2.(testKey) return ok && k1 == t2 case anotherTestKey: return false default: return k1 == k2 } } func hashFunc(k interface{}) uint32 { switch k := k.(type) { case uint32: return k case string: return hash.String(k) case testKey: // Return the lower 32 bits for testKey. This is intended so that hash // collisions can be easily constructed. return uint32(k & 0xffffffff) case anotherTestKey: return uint32(k) default: return 0 } } var empty = New(equalFunc, hashFunc) type refEntry struct { k testKey v string } func hex(i uint64) string { return "0x" + strconv.FormatUint(i, 16) } func init() { rand.Seed(time.Now().UTC().UnixNano()) } var randomStrings []string // getRandomStrings returns a slice of N3 random strings. It builds the slice // once and caches it. If the slice is built for the first time, it stops the // timer of the benchmark. func getRandomStrings(b *testing.B) []string { if randomStrings == nil { b.StopTimer() defer b.StartTimer() randomStrings = make([]string, N3) for i := 0; i < N3; i++ { randomStrings[i] = makeRandomString() } } return randomStrings } // makeRandomString builds a random string consisting of n bytes (randomized // between 0 and 99) and each byte is randomized between 0 and 255. The string // need not be valid UTF-8. func makeRandomString() string { bytes := make([]byte, rand.Intn(100)) for i := range bytes { bytes[i] = byte(rand.Intn(256)) } return string(bytes) } func TestHashMap(t *testing.T) { var refEntries []refEntry add := func(k testKey, v string) { refEntries = append(refEntries, refEntry{k, v}) } for i := 0; i < NSequential; i++ { add(testKey(i), hex(uint64(i))) } for i := 0; i < NCollision; i++ { add(testKey(uint64(i+1)<<32), "collision "+hex(uint64(i))) } for i := 0; i < NRandom; i++ { // Avoid rand.Uint64 for compatibility with pre 1.8 Go k := uint64(rand.Int63())>>31 | uint64(rand.Int63())<<32 add(testKey(k), "random "+hex(k)) } for i := 0; i < NReplace; i++ { k := uint64(rand.Int31n(NSequential)) add(testKey(k), "replace "+hex(k)) } testHashMapWithRefEntries(t, refEntries) } func TestHashMapSmallRandom(t *testing.T) { for p := 0; p < SmallRandomPass; p++ { var refEntries []refEntry add := func(k testKey, v string) { refEntries = append(refEntries, refEntry{k, v}) } for i := 0; i < NSmallRandom; i++ { k := uint64(uint64(rand.Int31n(SmallRandomHighBound))<<32 | uint64(rand.Int31n(SmallRandomLowBound))) add(testKey(k), "random "+hex(k)) } testHashMapWithRefEntries(t, refEntries) } } var marshalJSONTests = []struct { in Map wantOut string wantErr bool }{ {makeHashMap(uint32(1), "a", "2", "b"), `{"1":"a","2":"b"}`, false}, // Invalid key type {makeHashMap([]interface{}{}, "x"), "", true}, } func TestMarshalJSON(t *testing.T) { for i, test := range marshalJSONTests { out, err := test.in.MarshalJSON() if string(out) != test.wantOut { t.Errorf("m%d.MarshalJSON -> out %s, want %s", i, out, test.wantOut) } if (err != nil) != test.wantErr { var wantErr string if test.wantErr { wantErr = "non-nil" } else { wantErr = "nil" } t.Errorf("m%d.MarshalJSON -> err %v, want %s", i, err, wantErr) } } } func makeHashMap(data ...interface{}) Map { m := empty for i := 0; i+1 < len(data); i += 2 { k, v := data[i], data[i+1] m = m.Assoc(k, v) } return m } // testHashMapWithRefEntries tests the operations of a Map. It uses the supplied // list of entries to build the map, and then test all its operations. func testHashMapWithRefEntries(t *testing.T, refEntries []refEntry) { m := empty // Len of Empty should be 0. if m.Len() != 0 { t.Errorf("m.Len = %d, want %d", m.Len(), 0) } // Assoc and Len, test by building a map simultaneously. ref := make(map[testKey]string, len(refEntries)) for _, e := range refEntries { ref[e.k] = e.v m = m.Assoc(e.k, e.v) if m.Len() != len(ref) { t.Errorf("m.Len = %d, want %d", m.Len(), len(ref)) } } // Index. testMapContent(t, m, ref) got, in := m.Index(anotherTestKey(0)) if in { t.Errorf("m.Index returns entry %v", got) } // Iterator. testIterator(t, m, ref) // Dissoc. // Ineffective ones. for i := 0; i < NIneffectiveDissoc; i++ { k := anotherTestKey(uint32(rand.Int31())>>15 | uint32(rand.Int31())<<16) m = m.Dissoc(k) if m.Len() != len(ref) { t.Errorf("m.Dissoc removes item when it shouldn't") } } // Effective ones. for x := 0; x < len(refEntries); x++ { i := rand.Intn(len(refEntries)) k := refEntries[i].k delete(ref, k) m = m.Dissoc(k) if m.Len() != len(ref) { t.Errorf("m.Len() = %d after removing, should be %v", m.Len(), len(ref)) } _, in := m.Index(k) if in { t.Errorf("m.Index(%v) still returns item after removal", k) } // Checking all elements is expensive. Only do this 1% of the time. if rand.Float64() < 0.01 { testMapContent(t, m, ref) testIterator(t, m, ref) } } } func testMapContent(t *testing.T, m Map, ref map[testKey]string) { for k, v := range ref { got, in := m.Index(k) if !in { t.Errorf("m.Index 0x%x returns no entry", k) } if got != v { t.Errorf("m.Index(0x%x) = %v, want %v", k, got, v) } } } func testIterator(t *testing.T, m Map, ref map[testKey]string) { ref2 := map[interface{}]interface{}{} for k, v := range ref { ref2[k] = v } for it := m.Iterator(); it.HasElem(); it.Next() { k, v := it.Elem() if ref2[k] != v { t.Errorf("iterator yields unexpected pair %v, %v", k, v) } delete(ref2, k) } if len(ref2) != 0 { t.Errorf("iterating was not exhaustive") } } func TestNilKey(t *testing.T) { m := empty testLen := func(l int) { if m.Len() != l { t.Errorf(".Len -> %d, want %d", m.Len(), l) } } testIndex := func(wantVal interface{}, wantOk bool) { val, ok := m.Index(nil) if val != wantVal { t.Errorf(".Index -> %v, want %v", val, wantVal) } if ok != wantOk { t.Errorf(".Index -> ok %v, want %v", ok, wantOk) } } testLen(0) testIndex(nil, false) m = m.Assoc(nil, "nil value") testLen(1) testIndex("nil value", true) m = m.Assoc(nil, "nil value 2") testLen(1) testIndex("nil value 2", true) m = m.Dissoc(nil) testLen(0) testIndex(nil, false) } func TestIterateMapWithNilKey(t *testing.T) { m := empty.Assoc("k", "v").Assoc(nil, "nil value") var collected []interface{} for it := m.Iterator(); it.HasElem(); it.Next() { k, v := it.Elem() collected = append(collected, k, v) } wantCollected := []interface{}{nil, "nil value", "k", "v"} if !reflect.DeepEqual(collected, wantCollected) { t.Errorf("collected %v, want %v", collected, wantCollected) } } func BenchmarkSequentialConsNative1(b *testing.B) { nativeSequentialAdd(b.N, N1) } func BenchmarkSequentialConsNative2(b *testing.B) { nativeSequentialAdd(b.N, N2) } func BenchmarkSequentialConsNative3(b *testing.B) { nativeSequentialAdd(b.N, N3) } // nativeSequntialAdd starts with an empty native map and adds elements 0...n-1 // to the map, using the same value as the key, repeating for N times. func nativeSequentialAdd(N int, n uint32) { for r := 0; r < N; r++ { m := make(map[uint32]uint32) for i := uint32(0); i < n; i++ { m[i] = i } } } func BenchmarkSequentialConsPersistent1(b *testing.B) { sequentialCons(b.N, N1) } func BenchmarkSequentialConsPersistent2(b *testing.B) { sequentialCons(b.N, N2) } func BenchmarkSequentialConsPersistent3(b *testing.B) { sequentialCons(b.N, N3) } // sequentialCons starts with an empty hash map and adds elements 0...n-1 to the // map, using the same value as the key, repeating for N times. func sequentialCons(N int, n uint32) { for r := 0; r < N; r++ { m := empty for i := uint32(0); i < n; i++ { m = m.Assoc(i, i) } } } func BenchmarkRandomStringsConsNative1(b *testing.B) { nativeRandomStringsAdd(b, N1) } func BenchmarkRandomStringsConsNative2(b *testing.B) { nativeRandomStringsAdd(b, N2) } func BenchmarkRandomStringsConsNative3(b *testing.B) { nativeRandomStringsAdd(b, N3) } // nativeSequntialAdd starts with an empty native map and adds n random strings // to the map, using the same value as the key, repeating for b.N times. func nativeRandomStringsAdd(b *testing.B, n int) { ss := getRandomStrings(b) for r := 0; r < b.N; r++ { m := make(map[string]string) for i := 0; i < n; i++ { s := ss[i] m[s] = s } } } func BenchmarkRandomStringsConsPersistent1(b *testing.B) { randomStringsCons(b, N1) } func BenchmarkRandomStringsConsPersistent2(b *testing.B) { randomStringsCons(b, N2) } func BenchmarkRandomStringsConsPersistent3(b *testing.B) { randomStringsCons(b, N3) } func randomStringsCons(b *testing.B, n int) { ss := getRandomStrings(b) for r := 0; r < b.N; r++ { m := empty for i := 0; i < n; i++ { s := ss[i] m = m.Assoc(s, s) } } } elvish-0.17.0/pkg/persistent/hashmap/map.go000066400000000000000000000026771415471104000206100ustar00rootroot00000000000000package hashmap import "encoding/json" // Map is a persistent associative data structure mapping keys to values. It // is immutable, and supports near-O(1) operations to create modified version of // the map that shares the underlying data structure. Because it is immutable, // all of its methods are safe for concurrent use. type Map interface { json.Marshaler // Len returns the length of the map. Len() int // Index returns whether there is a value associated with the given key, and // that value or nil. Index(k interface{}) (interface{}, bool) // Assoc returns an almost identical map, with the given key associated with // the given value. Assoc(k, v interface{}) Map // Dissoc returns an almost identical map, with the given key associated // with no value. Dissoc(k interface{}) Map // Iterator returns an iterator over the map. Iterator() Iterator } // Iterator is an iterator over map elements. It can be used like this: // // for it := m.Iterator(); it.HasElem(); it.Next() { // key, value := it.Elem() // // do something with elem... // } type Iterator interface { // Elem returns the current key-value pair. Elem() (interface{}, interface{}) // HasElem returns whether the iterator is pointing to an element. HasElem() bool // Next moves the iterator to the next position. Next() } // HasKey reports whether a Map has the given key. func HasKey(m Map, k interface{}) bool { _, ok := m.Index(k) return ok } elvish-0.17.0/pkg/persistent/list/000077500000000000000000000000001415471104000170225ustar00rootroot00000000000000elvish-0.17.0/pkg/persistent/list/list.go000066400000000000000000000013471415471104000203310ustar00rootroot00000000000000// Package list implements persistent list. package list // List is a persistent list. type List interface { // Len returns the number of values in the list. Len() int // Cons returns a new list with an additional value in the front. Cons(interface{}) List // First returns the first value in the list. First() interface{} // Rest returns the list after the first value. Rest() List } // Empty is an empty list. var Empty List = &list{} type list struct { first interface{} rest *list count int } func (l *list) Len() int { return l.count } func (l *list) Cons(val interface{}) List { return &list{val, l, l.count + 1} } func (l *list) First() interface{} { return l.first } func (l *list) Rest() List { return l.rest } elvish-0.17.0/pkg/persistent/persistent.go000066400000000000000000000001761415471104000206020ustar00rootroot00000000000000// Package persistent contains subpackages for persistent data structures, // similar to those of Clojure. package persistent elvish-0.17.0/pkg/persistent/vector/000077500000000000000000000000001415471104000173515ustar00rootroot00000000000000elvish-0.17.0/pkg/persistent/vector/vector.go000066400000000000000000000245601415471104000212110ustar00rootroot00000000000000// Package vector implements persistent vector. // // This is a Go clone of Clojure's PersistentVector type // (https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/PersistentVector.java). // For an introduction to the internals, see // https://hypirion.com/musings/understanding-persistent-vector-pt-1. package vector import ( "bytes" "encoding/json" "fmt" ) const ( chunkBits = 5 nodeSize = 1 << chunkBits tailMaxLen = nodeSize chunkMask = nodeSize - 1 ) // Vector is a persistent sequential container for arbitrary values. It supports // O(1) lookup by index, modification by index, and insertion and removal // operations at the end. Being a persistent variant of the data structure, it // is immutable, and provides O(1) operations to create modified versions of the // vector that shares the underlying data structure, making it suitable for // concurrent access. The empty value is a valid empty vector. type Vector interface { json.Marshaler // Len returns the length of the vector. Len() int // Index returns the i-th element of the vector, if it exists. The second // return value indicates whether the element exists. Index(i int) (interface{}, bool) // Assoc returns an almost identical Vector, with the i-th element // replaced. If the index is smaller than 0 or greater than the length of // the vector, it returns nil. If the index is equal to the size of the // vector, it is equivalent to Cons. Assoc(i int, val interface{}) Vector // Cons returns an almost identical Vector, with an additional element // appended to the end. Cons(val interface{}) Vector // Pop returns an almost identical Vector, with the last element removed. It // returns nil if the vector is already empty. Pop() Vector // SubVector returns a subvector containing the elements from i up to but // not including j. SubVector(i, j int) Vector // Iterator returns an iterator over the vector. Iterator() Iterator } // Iterator is an iterator over vector elements. It can be used like this: // // for it := v.Iterator(); it.HasElem(); it.Next() { // elem := it.Elem() // // do something with elem... // } type Iterator interface { // Elem returns the element at the current position. Elem() interface{} // HasElem returns whether the iterator is pointing to an element. HasElem() bool // Next moves the iterator to the next position. Next() } type vector struct { count int // height of the tree structure, defined to be 0 when root is a leaf. height uint root node tail []interface{} } // Empty is an empty Vector. var Empty Vector = &vector{} // node is a node in the vector tree. It is always of the size nodeSize. type node *[nodeSize]interface{} func newNode() node { return node(&[nodeSize]interface{}{}) } func clone(n node) node { a := *n return node(&a) } func nodeFromSlice(s []interface{}) node { var n [nodeSize]interface{} copy(n[:], s) return &n } // Count returns the number of elements in a Vector. func (v *vector) Len() int { return v.count } // treeSize returns the number of elements stored in the tree (as opposed to the // tail). func (v *vector) treeSize() int { if v.count < tailMaxLen { return 0 } return ((v.count - 1) >> chunkBits) << chunkBits } func (v *vector) Index(i int) (interface{}, bool) { if i < 0 || i >= v.count { return nil, false } // The following is very similar to sliceFor, but is implemented separately // to avoid unnecessary copying. if i >= v.treeSize() { return v.tail[i&chunkMask], true } n := v.root for shift := v.height * chunkBits; shift > 0; shift -= chunkBits { n = n[(i>>shift)&chunkMask].(node) } return n[i&chunkMask], true } // sliceFor returns the slice where the i-th element is stored. The index must // be in bound. func (v *vector) sliceFor(i int) []interface{} { if i >= v.treeSize() { return v.tail } n := v.root for shift := v.height * chunkBits; shift > 0; shift -= chunkBits { n = n[(i>>shift)&chunkMask].(node) } return n[:] } func (v *vector) Assoc(i int, val interface{}) Vector { if i < 0 || i > v.count { return nil } else if i == v.count { return v.Cons(val) } if i >= v.treeSize() { newTail := append([]interface{}(nil), v.tail...) newTail[i&chunkMask] = val return &vector{v.count, v.height, v.root, newTail} } return &vector{v.count, v.height, doAssoc(v.height, v.root, i, val), v.tail} } // doAssoc returns an almost identical tree, with the i-th element replaced by // val. func doAssoc(height uint, n node, i int, val interface{}) node { m := clone(n) if height == 0 { m[i&chunkMask] = val } else { sub := (i >> (height * chunkBits)) & chunkMask m[sub] = doAssoc(height-1, m[sub].(node), i, val) } return m } func (v *vector) Cons(val interface{}) Vector { // Room in tail? if v.count-v.treeSize() < tailMaxLen { newTail := make([]interface{}, len(v.tail)+1) copy(newTail, v.tail) newTail[len(v.tail)] = val return &vector{v.count + 1, v.height, v.root, newTail} } // Full tail; push into tree. tailNode := nodeFromSlice(v.tail) newHeight := v.height var newRoot node // Overflow root? if (v.count >> chunkBits) > (1 << (v.height * chunkBits)) { newRoot = newNode() newRoot[0] = v.root newRoot[1] = newPath(v.height, tailNode) newHeight++ } else { newRoot = v.pushTail(v.height, v.root, tailNode) } return &vector{v.count + 1, newHeight, newRoot, []interface{}{val}} } // pushTail returns a tree with tail appended. func (v *vector) pushTail(height uint, n node, tail node) node { if height == 0 { return tail } idx := ((v.count - 1) >> (height * chunkBits)) & chunkMask m := clone(n) child := n[idx] if child == nil { m[idx] = newPath(height-1, tail) } else { m[idx] = v.pushTail(height-1, child.(node), tail) } return m } // newPath creates a left-branching tree of specified height and leaf. func newPath(height uint, leaf node) node { if height == 0 { return leaf } ret := newNode() ret[0] = newPath(height-1, leaf) return ret } func (v *vector) Pop() Vector { switch v.count { case 0: return nil case 1: return Empty } if v.count-v.treeSize() > 1 { newTail := make([]interface{}, len(v.tail)-1) copy(newTail, v.tail) return &vector{v.count - 1, v.height, v.root, newTail} } newTail := v.sliceFor(v.count - 2) newRoot := v.popTail(v.height, v.root) newHeight := v.height if v.height > 0 && newRoot[1] == nil { newRoot = newRoot[0].(node) newHeight-- } return &vector{v.count - 1, newHeight, newRoot, newTail} } // popTail returns a new tree with the last leaf removed. func (v *vector) popTail(level uint, n node) node { idx := ((v.count - 2) >> (level * chunkBits)) & chunkMask if level > 1 { newChild := v.popTail(level-1, n[idx].(node)) if newChild == nil && idx == 0 { return nil } m := clone(n) if newChild == nil { // This is needed since `m[idx] = newChild` would store an // interface{} with a non-nil type part, which is non-nil m[idx] = nil } else { m[idx] = newChild } return m } else if idx == 0 { return nil } else { m := clone(n) m[idx] = nil return m } } func (v *vector) SubVector(begin, end int) Vector { if begin < 0 || begin > end || end > v.count { return nil } return &subVector{v, begin, end} } func (v *vector) Iterator() Iterator { return newIterator(v) } func (v *vector) MarshalJSON() ([]byte, error) { return marshalJSON(v.Iterator()) } type subVector struct { v *vector begin int end int } func (s *subVector) Len() int { return s.end - s.begin } func (s *subVector) Index(i int) (interface{}, bool) { if i < 0 || s.begin+i >= s.end { return nil, false } return s.v.Index(s.begin + i) } func (s *subVector) Assoc(i int, val interface{}) Vector { if i < 0 || s.begin+i > s.end { return nil } else if s.begin+i == s.end { return s.Cons(val) } return s.v.Assoc(s.begin+i, val).SubVector(s.begin, s.end) } func (s *subVector) Cons(val interface{}) Vector { return s.v.Assoc(s.end, val).SubVector(s.begin, s.end+1) } func (s *subVector) Pop() Vector { switch s.Len() { case 0: return nil case 1: return Empty default: return s.v.SubVector(s.begin, s.end-1) } } func (s *subVector) SubVector(i, j int) Vector { return s.v.SubVector(s.begin+i, s.begin+j) } func (s *subVector) Iterator() Iterator { return newIteratorWithRange(s.v, s.begin, s.end) } func (s *subVector) MarshalJSON() ([]byte, error) { return marshalJSON(s.Iterator()) } type iterator struct { v *vector treeSize int index int end int path []pathEntry } type pathEntry struct { node node index int } func (e pathEntry) current() interface{} { return e.node[e.index] } func newIterator(v *vector) *iterator { return newIteratorWithRange(v, 0, v.Len()) } func newIteratorWithRange(v *vector, begin, end int) *iterator { it := &iterator{v, v.treeSize(), begin, end, nil} if it.index >= it.treeSize { return it } // Find the node for begin, remembering all nodes along the path. n := v.root for shift := v.height * chunkBits; shift > 0; shift -= chunkBits { idx := (begin >> shift) & chunkMask it.path = append(it.path, pathEntry{n, idx}) n = n[idx].(node) } it.path = append(it.path, pathEntry{n, begin & chunkMask}) return it } func (it *iterator) Elem() interface{} { if it.index >= it.treeSize { return it.v.tail[it.index-it.treeSize] } return it.path[len(it.path)-1].current() } func (it *iterator) HasElem() bool { return it.index < it.end } func (it *iterator) Next() { if it.index+1 >= it.treeSize { // Next element is in tail. Just increment the index. it.index++ return } // Find the deepest level that can be advanced. var i int for i = len(it.path) - 1; i >= 0; i-- { e := it.path[i] if e.index+1 < len(e.node) { break } } if i == -1 { panic("cannot advance; vector iterator bug") } // Advance on this node, and re-populate all deeper levels. it.path[i].index++ for i++; i < len(it.path); i++ { it.path[i] = pathEntry{it.path[i-1].current().(node), 0} } it.index++ } type marshalError struct { index int cause error } func (err *marshalError) Error() string { return fmt.Sprintf("element %d: %s", err.index, err.cause) } func marshalJSON(it Iterator) ([]byte, error) { var buf bytes.Buffer buf.WriteByte('[') index := 0 for ; it.HasElem(); it.Next() { if index > 0 { buf.WriteByte(',') } elemBytes, err := json.Marshal(it.Elem()) if err != nil { return nil, &marshalError{index, err} } buf.Write(elemBytes) index++ } buf.WriteByte(']') return buf.Bytes(), nil } elvish-0.17.0/pkg/persistent/vector/vector_test.go000066400000000000000000000224371415471104000222510ustar00rootroot00000000000000package vector import ( "errors" "math/rand" "strconv" "testing" "time" ) // Nx is the minimum number of elements for the internal tree of the vector to // be x levels deep. const ( N1 = tailMaxLen + 1 // 33 N2 = nodeSize + tailMaxLen + 1 // 65 N3 = nodeSize*nodeSize + tailMaxLen + 1 // 1057 N4 = nodeSize*nodeSize*nodeSize + tailMaxLen + 1 // 32801 ) func init() { rand.Seed(time.Now().UTC().UnixNano()) } func TestVector(t *testing.T) { run := func(n int) { t.Run(strconv.Itoa(n), func(t *testing.T) { v := testCons(t, n) testIndex(t, v, 0, n) testAssoc(t, v, "233") testIterator(t, v.Iterator(), 0, n) testPop(t, v) }) } for i := 0; i <= N3; i++ { run(i) } run(N4) } // Regression test against #4. func TestIterator_VectorWithNil(t *testing.T) { run := func(n int) { t.Run(strconv.Itoa(n), func(t *testing.T) { v := Empty for i := 0; i < n; i++ { v = v.Cons(nil) } iterated := 0 for it := v.Iterator(); it.HasElem(); it.Next() { iterated++ if it.Elem() != nil { t.Errorf("element not nil") } } if iterated != n { t.Errorf("did not iterate %d items", n) } }) } for i := 0; i <= N3; i++ { run(i) } run(N4) } // testCons creates a vector containing 0...n-1 with Cons, and ensures that the // length of the old and new vectors are expected after each Cons. It returns // the created vector. func testCons(t *testing.T, n int) Vector { v := Empty for i := 0; i < n; i++ { oldv := v v = v.Cons(i) if count := oldv.Len(); count != i { t.Errorf("oldv.Count() == %v, want %v", count, i) } if count := v.Len(); count != i+1 { t.Errorf("v.Count() == %v, want %v", count, i+1) } } return v } // testIndex tests Index, assuming that the vector contains begin...int-1. func testIndex(t *testing.T, v Vector, begin, end int) { n := v.Len() for i := 0; i < n; i++ { elem, _ := v.Index(i) if elem != i { t.Errorf("v.Index(%v) == %v, want %v", i, elem, i) } } for _, i := range []int{-2, -1, n, n + 1, n * 2} { if elem, _ := v.Index(i); elem != nil { t.Errorf("v.Index(%d) == %v, want nil", i, elem) } } } // testIterator tests the iterator, assuming that the result is begin...end-1. func testIterator(t *testing.T, it Iterator, begin, end int) { i := begin for ; it.HasElem(); it.Next() { elem := it.Elem() if elem != i { t.Errorf("iterator produce %v, want %v", elem, i) } i++ } if i != end { t.Errorf("iterator produces up to %v, want %v", i, end) } } // testAssoc tests Assoc by replacing each element. func testAssoc(t *testing.T, v Vector, subst interface{}) { n := v.Len() for i := 0; i <= n; i++ { oldv := v v = v.Assoc(i, subst) if i < n { elem, _ := oldv.Index(i) if elem != i { t.Errorf("oldv.Index(%v) == %v, want %v", i, elem, i) } } elem, _ := v.Index(i) if elem != subst { t.Errorf("v.Index(%v) == %v, want %v", i, elem, subst) } } n++ for _, i := range []int{-1, n + 1, n + 2, n * 2} { newv := v.Assoc(i, subst) if newv != nil { t.Errorf("v.Assoc(%d) = %v, want nil", i, newv) } } } // testPop tests Pop by removing each element. func testPop(t *testing.T, v Vector) { n := v.Len() for i := 0; i < n; i++ { oldv := v v = v.Pop() if count := oldv.Len(); count != n-i { t.Errorf("oldv.Count() == %v, want %v", count, n-i) } if count := v.Len(); count != n-i-1 { t.Errorf("oldv.Count() == %v, want %v", count, n-i-1) } } newv := v.Pop() if newv != nil { t.Errorf("v.Pop() = %v, want nil", newv) } } func TestSubVector(t *testing.T) { v := Empty for i := 0; i < 10; i++ { v = v.Cons(i) } sv := v.SubVector(0, 4) testIndex(t, sv, 0, 4) testAssoc(t, sv, "233") testIterator(t, sv.Iterator(), 0, 4) testPop(t, sv) sv = v.SubVector(1, 4) if !checkVector(sv, 1, 2, 3) { t.Errorf("v[0:4] is not expected") } if !checkVector(sv.Assoc(1, "233"), 1, "233", 3) { t.Errorf("v[0:4].Assoc is not expected") } if !checkVector(sv.Cons("233"), 1, 2, 3, "233") { t.Errorf("v[0:4].Cons is not expected") } if !checkVector(sv.Pop(), 1, 2) { t.Errorf("v[0:4].Pop is not expected") } if !checkVector(sv.SubVector(1, 2), 2) { t.Errorf("v[0:4][1:2] is not expected") } testIterator(t, sv.Iterator(), 1, 4) if !checkVector(v.SubVector(1, 1)) { t.Errorf("v[1:1] is not expected") } // Begin is allowed to be equal to n if end is also n if !checkVector(v.SubVector(10, 10)) { t.Errorf("v[10:10] is not expected") } bad := v.SubVector(-1, 0) if bad != nil { t.Errorf("v.SubVector(-1, 0) = %v, want nil", bad) } bad = v.SubVector(5, 100) if bad != nil { t.Errorf("v.SubVector(5, 100) = %v, want nil", bad) } bad = v.SubVector(-1, 100) if bad != nil { t.Errorf("v.SubVector(-1, 100) = %v, want nil", bad) } bad = v.SubVector(4, 2) if bad != nil { t.Errorf("v.SubVector(4, 2) = %v, want nil", bad) } } // Regression test for https://b.elv.sh/1287: crash when tree has a height >= 1 // and start of subvector is in the tail. func TestSubVector_BeginFromTail(t *testing.T) { v := Empty for i := 0; i < 65; i++ { v = v.Cons(i) } sv := v.SubVector(64, 65) testIterator(t, sv.Iterator(), 64, 65) } func checkVector(v Vector, values ...interface{}) bool { if v.Len() != len(values) { return false } for i, a := range values { if x, _ := v.Index(i); x != a { return false } } return true } func TestVectorEqual(t *testing.T) { v1, v2 := Empty, Empty for i := 0; i < N3; i++ { elem := rand.Int63() v1 = v1.Cons(elem) v2 = v2.Cons(elem) if !eqVector(v1, v2) { t.Errorf("Not equal after Cons'ing %d elements", i+1) } } } func eqVector(v1, v2 Vector) bool { if v1.Len() != v2.Len() { return false } for i := 0; i < v1.Len(); i++ { a1, _ := v1.Index(i) a2, _ := v2.Index(i) if a1 != a2 { return false } } return true } var marshalJSONTests = []struct { in Vector wantOut string wantErr error }{ {makeVector("1", 2, nil), `["1",2,null]`, nil}, {makeVector("1", makeVector(2)), `["1",[2]]`, nil}, {makeVector(0, 1, 2, 3, 4, 5).SubVector(1, 5), `[1,2,3,4]`, nil}, {makeVector(0, func() {}), "", errors.New("element 1: json: unsupported type: func()")}, } func TestMarshalJSON(t *testing.T) { for i, test := range marshalJSONTests { out, err := test.in.MarshalJSON() if string(out) != test.wantOut { t.Errorf("v%d.MarshalJSON -> out %q, want %q", i, out, test.wantOut) } if err == nil || test.wantErr == nil { if err != test.wantErr { t.Errorf("v%d.MarshalJSON -> err %v, want %v", i, err, test.wantErr) } } else { if err.Error() != test.wantErr.Error() { t.Errorf("v%d.MarshalJSON -> err %v, want %v", i, err, test.wantErr) } } } } func makeVector(elements ...interface{}) Vector { v := Empty for _, element := range elements { v = v.Cons(element) } return v } func BenchmarkConsNativeN1(b *testing.B) { benchmarkNativeAppend(b, N1) } func BenchmarkConsNativeN2(b *testing.B) { benchmarkNativeAppend(b, N2) } func BenchmarkConsNativeN3(b *testing.B) { benchmarkNativeAppend(b, N3) } func BenchmarkConsNativeN4(b *testing.B) { benchmarkNativeAppend(b, N4) } func benchmarkNativeAppend(b *testing.B, n int) { for r := 0; r < b.N; r++ { var s []interface{} for i := 0; i < n; i++ { s = append(s, i) } _ = s } } func BenchmarkConsPersistentN1(b *testing.B) { benchmarkCons(b, N1) } func BenchmarkConsPersistentN2(b *testing.B) { benchmarkCons(b, N2) } func BenchmarkConsPersistentN3(b *testing.B) { benchmarkCons(b, N3) } func BenchmarkConsPersistentN4(b *testing.B) { benchmarkCons(b, N4) } func benchmarkCons(b *testing.B, n int) { for r := 0; r < b.N; r++ { v := Empty for i := 0; i < n; i++ { v = v.Cons(i) } } } var ( sliceN4 = make([]interface{}, N4) vectorN4 = Empty ) func init() { for i := 0; i < N4; i++ { vectorN4 = vectorN4.Cons(i) } } var x interface{} func BenchmarkIndexSeqNativeN4(b *testing.B) { benchmarkIndexSeqNative(b, N4) } func benchmarkIndexSeqNative(b *testing.B, n int) { for r := 0; r < b.N; r++ { for i := 0; i < n; i++ { x = sliceN4[i] } } } func BenchmarkIndexSeqPersistentN4(b *testing.B) { benchmarkIndexSeqPersistent(b, N4) } func benchmarkIndexSeqPersistent(b *testing.B, n int) { for r := 0; r < b.N; r++ { for i := 0; i < n; i++ { x, _ = vectorN4.Index(i) } } } var randIndices []int func init() { randIndices = make([]int, N4) for i := 0; i < N4; i++ { randIndices[i] = rand.Intn(N4) } } func BenchmarkIndexRandNative(b *testing.B) { for r := 0; r < b.N; r++ { for _, i := range randIndices { x = sliceN4[i] } } } func BenchmarkIndexRandPersistent(b *testing.B) { for r := 0; r < b.N; r++ { for _, i := range randIndices { x, _ = vectorN4.Index(i) } } } func nativeEqual(s1, s2 []int) bool { if len(s1) != len(s2) { return false } for i, v1 := range s1 { if v1 != s2[i] { return false } } return true } func BenchmarkEqualNative(b *testing.B) { b.StopTimer() var s1, s2 []int for i := 0; i < N4; i++ { s1 = append(s1, i) s2 = append(s2, i) } b.StartTimer() for r := 0; r < b.N; r++ { eq := nativeEqual(s1, s2) if !eq { panic("not equal") } } } func BenchmarkEqualPersistent(b *testing.B) { b.StopTimer() v1, v2 := Empty, Empty for i := 0; i < N4; i++ { v1 = v1.Cons(i) v2 = v2.Cons(i) } b.StartTimer() for r := 0; r < b.N; r++ { eq := eqVector(v1, v2) if !eq { panic("not equal") } } } elvish-0.17.0/pkg/prog/000077500000000000000000000000001415471104000146165ustar00rootroot00000000000000elvish-0.17.0/pkg/prog/prog.go000066400000000000000000000132541415471104000161210ustar00rootroot00000000000000// Package prog provides the entry point to Elvish. Its subpackages correspond // to subprograms of Elvish. package prog // This package sets up the basic environment and calls the appropriate // "subprogram", one of the daemon, the terminal interface, or the web // interface. import ( "errors" "flag" "fmt" "io" "os" "runtime/pprof" "src.elv.sh/pkg/logutil" ) // DeprecationLevel is a global flag that controls which deprecations to show. // If its value is X, Elvish shows deprecations that should be shown for version // 0.X. var DeprecationLevel = 17 // Flags keeps command-line flags. type Flags struct { Log, CPUProfile string Help, Version, BuildInfo, JSON bool CodeInArg, CompileOnly, NoRc bool RC string Port int Daemon bool Forked int DB, Sock string } func newFlagSet(f *Flags) *flag.FlagSet { fs := flag.NewFlagSet("elvish", flag.ContinueOnError) // Error and usage will be printed explicitly. fs.SetOutput(io.Discard) fs.StringVar(&f.Log, "log", "", "a file to write debug log to except for the daemon") fs.StringVar(&f.CPUProfile, "cpuprofile", "", "write cpu profile to file") fs.BoolVar(&f.Help, "help", false, "show usage help and quit") fs.BoolVar(&f.Version, "version", false, "show version and quit") fs.BoolVar(&f.BuildInfo, "buildinfo", false, "show build info and quit") fs.BoolVar(&f.JSON, "json", false, "show output in JSON. Useful with -buildinfo and -compileonly") // The `-i` option is for compatibility with POSIX shells so that programs, such as the `script` // command, will work when asked to launch an interactive Elvish shell. fs.Bool("i", false, "force interactive mode; currently ignored") fs.BoolVar(&f.CodeInArg, "c", false, "take first argument as code to execute") fs.BoolVar(&f.CompileOnly, "compileonly", false, "Parse/Compile but do not execute") fs.BoolVar(&f.NoRc, "norc", false, "run elvish without invoking rc.elv") fs.StringVar(&f.RC, "rc", "", "path to rc.elv") fs.BoolVar(&f.Daemon, "daemon", false, "[internal flag] run the storage daemon instead of shell") fs.StringVar(&f.DB, "db", "", "[internal flag] path to the database") fs.StringVar(&f.Sock, "sock", "", "[internal flag] path to the daemon socket") fs.IntVar(&DeprecationLevel, "deprecation-level", DeprecationLevel, "show warnings for all features deprecated as of version 0.X") return fs } func usage(out io.Writer, fs *flag.FlagSet) { fmt.Fprintln(out, "Usage: elvish [flags] [script]") fmt.Fprintln(out, "Supported flags:") fs.SetOutput(out) fs.PrintDefaults() } // Run parses command-line flags and runs the first applicable subprogram. It // returns the exit status of the program. func Run(fds [3]*os.File, args []string, p Program) int { f := &Flags{} fs := newFlagSet(f) err := fs.Parse(args[1:]) if err != nil { if err == flag.ErrHelp { // (*flag.FlagSet).Parse returns ErrHelp when -h or -help was // requested but *not* defined. Elvish defines -help, but not -h; so // this means that -h has been requested. Handle this by printing // the same message as an undefined flag. fmt.Fprintln(fds[2], "flag provided but not defined: -h") } else { fmt.Fprintln(fds[2], err) } usage(fds[2], fs) return 2 } // Handle flags common to all subprograms. if f.CPUProfile != "" { f, err := os.Create(f.CPUProfile) if err != nil { fmt.Fprintln(fds[2], "Warning: cannot create CPU profile:", err) fmt.Fprintln(fds[2], "Continuing without CPU profiling.") } else { pprof.StartCPUProfile(f) defer pprof.StopCPUProfile() } } if f.Daemon { // We expect our stdout file handle is open on a unique log file for the daemon to write its // log messages. See daemon.Spawn() in pkg/daemon. logutil.SetOutput(fds[1]) } else if f.Log != "" { err = logutil.SetOutputFile(f.Log) if err != nil { fmt.Fprintln(fds[2], err) } } if f.Help { usage(fds[1], fs) return 0 } err = p.Run(fds, f, fs.Args()) if err == nil { return 0 } if msg := err.Error(); msg != "" { fmt.Fprintln(fds[2], msg) } switch err := err.(type) { case badUsageError: usage(fds[2], fs) case exitError: return err.exit } return 2 } // Composite returns a Program that tries each of the given programs, // terminating at the first one that doesn't return NotSuitable(). func Composite(programs ...Program) Program { return compositeProgram(programs) } type compositeProgram []Program func (cp compositeProgram) Run(fds [3]*os.File, f *Flags, args []string) error { for _, p := range cp { err := p.Run(fds, f, args) if err != ErrNotSuitable { return err } } // If we have reached here, all subprograms have returned errNotSuitable return ErrNotSuitable } // ErrNotSuitable is a special error that may be returned by Program.Run, to // signify that this Program should not be run. It is useful when a Program is // used in Composite. var ErrNotSuitable = errors.New("internal error: no suitable subprogram") // BadUsage returns a special error that may be returned by Program.Run. It // causes the main function to print out a message, the usage information and // exit with 2. func BadUsage(msg string) error { return badUsageError{msg} } type badUsageError struct{ msg string } func (e badUsageError) Error() string { return e.msg } // Exit returns a special error that may be returned by Program.Run. It causes // the main function to exit with the given code without printing any error // messages. Exit(0) returns nil. func Exit(exit int) error { if exit == 0 { return nil } return exitError{exit} } type exitError struct{ exit int } func (e exitError) Error() string { return "" } // Program represents a subprogram. type Program interface { // Run runs the subprogram. Run(fds [3]*os.File, f *Flags, args []string) error } elvish-0.17.0/pkg/prog/prog_test.go000066400000000000000000000053331415471104000171570ustar00rootroot00000000000000package prog_test import ( "os" "testing" . "src.elv.sh/pkg/prog" "src.elv.sh/pkg/prog/progtest" "src.elv.sh/pkg/testutil" ) var ( Test = progtest.Test ThatElvish = progtest.ThatElvish ) func TestCommonFlagHandling(t *testing.T) { testutil.InTempDir(t) Test(t, testProgram{}, ThatElvish("-bad-flag"). ExitsWith(2). WritesStderrContaining("flag provided but not defined: -bad-flag\nUsage:"), // -h is treated as a bad flag ThatElvish("-h"). ExitsWith(2). WritesStderrContaining("flag provided but not defined: -h\nUsage:"), ThatElvish("-help"). WritesStdoutContaining("Usage: elvish [flags] [script]"), ThatElvish("-cpuprofile", "cpuprof").DoesNothing(), ThatElvish("-cpuprofile", "/a/bad/path"). WritesStderrContaining("Warning: cannot create CPU profile:"), ) // Check for the effect of -cpuprofile. There isn't much to test beyond a // sanity check that the profile file now exists. _, err := os.Stat("cpuprof") if err != nil { t.Errorf("CPU profile file does not exist: %v", err) } } func TestShowDeprecations(t *testing.T) { progtest.SetDeprecationLevel(t, 0) Test(t, testProgram{}, ThatElvish("-deprecation-level", "42").DoesNothing(), ) if DeprecationLevel != 42 { t.Errorf("ShowDeprecations = %d, want 42", DeprecationLevel) } } func TestNoSuitableSubprogram(t *testing.T) { Test(t, testProgram{notSuitable: true}, ThatElvish(). ExitsWith(2). WritesStderr("internal error: no suitable subprogram\n"), ) } func TestComposite(t *testing.T) { Test(t, Composite(testProgram{notSuitable: true}, testProgram{writeOut: "program 2"}), ThatElvish().WritesStdout("program 2"), ) } func TestComposite_NoSuitableSubprogram(t *testing.T) { Test(t, Composite(testProgram{notSuitable: true}, testProgram{notSuitable: true}), ThatElvish(). ExitsWith(2). WritesStderr("internal error: no suitable subprogram\n"), ) } func TestComposite_PreferEarlierSubprogram(t *testing.T) { Test(t, Composite( testProgram{writeOut: "program 1"}, testProgram{writeOut: "program 2"}), ThatElvish().WritesStdout("program 1"), ) } func TestBadUsageError(t *testing.T) { Test(t, testProgram{returnErr: BadUsage("lorem ipsum")}, ThatElvish().ExitsWith(2).WritesStderrContaining("lorem ipsum\n"), ) } func TestExitError(t *testing.T) { Test(t, testProgram{returnErr: Exit(3)}, ThatElvish().ExitsWith(3), ) } func TestExitError_0(t *testing.T) { Test(t, testProgram{returnErr: Exit(0)}, ThatElvish().ExitsWith(0), ) } type testProgram struct { notSuitable bool writeOut string returnErr error } func (p testProgram) Run(fds [3]*os.File, _ *Flags, args []string) error { if p.notSuitable { return ErrNotSuitable } fds[1].WriteString(p.writeOut) return p.returnErr } elvish-0.17.0/pkg/prog/progtest/000077500000000000000000000000001415471104000164655ustar00rootroot00000000000000elvish-0.17.0/pkg/prog/progtest/progtest.go000066400000000000000000000121241415471104000206630ustar00rootroot00000000000000// Package progtest provides a framework for testing subprograms. // // The entry point for the framework is the Test function, which accepts a // *testing.T, the Program implementation under test, and any number of test // cases. // // Test cases are constructed using the ThatElvish function, followed by method // calls that add additional information to it. // // Example: // // Test(t, someProgram, // ThatElvish("-c", "echo hello").WritesStdout("hello\n")) package progtest import ( "fmt" "io" "os" "strings" "testing" "src.elv.sh/pkg/prog" "src.elv.sh/pkg/testutil" ) // Case is a test case that can be used in Test. type Case struct { args []string stdin string want result } type result struct { exitCode int stdout output stderr output } type output struct { content string partial bool } func (o output) String() string { if o.partial { return fmt.Sprintf("text containing %q", o.content) } return fmt.Sprintf("%q", o.content) } // ThatElvish returns a new Case with the specified CLI arguments. // // The new Case expects the program run to exit with 0, and write nothing to // stdout or stderr. // // When combined with subsequent method calls, a test case reads like English. // For example, a test for the fact that "elvish -c hello" writes "hello\n" to // stdout reads: // // ThatElvish("-c", "hello").WritesStdout("hello\n") func ThatElvish(args ...string) Case { return Case{args: append([]string{"elvish"}, args...)} } // WithStdin returns an altered Case that provides the given input to stdin of // the program. func (c Case) WithStdin(s string) Case { c.stdin = s return c } // DoesNothing returns c itself. It is useful to mark tests that otherwise don't // have any expectations, for example: // // ThatElvish("-c", "nop").DoesNothing() func (c Case) DoesNothing() Case { return c } // ExitsWith returns an altered Case that requires the program run to return // with the given exit code. func (c Case) ExitsWith(code int) Case { c.want.exitCode = code return c } // WritesStdout returns an altered Case that requires the program run to write // exactly the given text to stdout. func (c Case) WritesStdout(s string) Case { c.want.stdout = output{content: s} return c } // WritesStdoutContaining returns an altered Case that requires the program run // to write output to stdout that contains the given text as a substring. func (c Case) WritesStdoutContaining(s string) Case { c.want.stdout = output{content: s, partial: true} return c } // WritesStderr returns an altered Case that requires the program run to write // exactly the given text to stderr. func (c Case) WritesStderr(s string) Case { c.want.stderr = output{content: s} return c } // WritesStderrContaining returns an altered Case that requires the program run // to write output to stderr that contains the given text as a substring. func (c Case) WritesStderrContaining(s string) Case { c.want.stderr = output{content: s, partial: true} return c } // Test runs test cases against a given program. func Test(t *testing.T, p prog.Program, cases ...Case) { t.Helper() for _, c := range cases { t.Run(strings.Join(c.args, " "), func(t *testing.T) { t.Helper() r := run(p, c.args, c.stdin) if r.exitCode != c.want.exitCode { t.Errorf("got exit code %v, want %v", r.exitCode, c.want.exitCode) } if !matchOutput(r.stdout, c.want.stdout) { t.Errorf("got stdout %v, want %v", r.stdout, c.want.stdout) } if !matchOutput(r.stderr, c.want.stderr) { t.Errorf("got stderr %v, want %v", r.stderr, c.want.stderr) } }) } } // Run runs a Program with the given arguments. It returns the Program's exit // code and output to stdout and stderr. func Run(p prog.Program, args ...string) (exit int, stdout, stderr string) { r := run(p, args, "") return r.exitCode, r.stdout.content, r.stderr.content } func run(p prog.Program, args []string, stdin string) result { r0, w0 := testutil.MustPipe() // TODO: This assumes that stdin fits in the pipe buffer. Don't assume that. _, err := w0.WriteString(stdin) if err != nil { panic(err) } w0.Close() defer r0.Close() w1, get1 := capturedOutput() w2, get2 := capturedOutput() exitCode := prog.Run([3]*os.File{r0, w1, w2}, args, p) return result{exitCode, output{content: get1()}, output{content: get2()}} } func matchOutput(got, want output) bool { if want.partial { return strings.Contains(got.content, want.content) } return got.content == want.content } func capturedOutput() (*os.File, func() string) { r, w := testutil.MustPipe() output := make(chan string, 1) go func() { b, err := io.ReadAll(r) if err != nil { panic(err) } r.Close() output <- string(b) }() return w, func() string { // Close the write side so captureOutput goroutine sees EOF and // terminates allowing us to capture and cache the output. w.Close() return <-output } } // SetDeprecationLevel sets prog.DeprecationLevel to the given value for the // duration of a test. func SetDeprecationLevel(c testutil.Cleanuper, level int) { save := prog.DeprecationLevel c.Cleanup(func() { prog.DeprecationLevel = save }) prog.DeprecationLevel = level } elvish-0.17.0/pkg/prog/progtest/progtest_test.go000066400000000000000000000013651415471104000217270ustar00rootroot00000000000000package progtest import ( "os" "testing" "src.elv.sh/pkg/prog" ) // Verify we don't deadlock if more output is written to stdout than can be // buffered by a pipe. func TestOutputCaptureDoesNotDeadlock(t *testing.T) { Test(t, noisyProgram{}, ThatElvish().WritesStdoutContaining("hello"), ) } type noisyProgram struct{} func (noisyProgram) Run(fds [3]*os.File, f *prog.Flags, args []string) error { // We need enough data to verify whether we're likely to deadlock due to // filling the pipe before the test completes. Pipes typically buffer 8 to // 128 KiB. bytes := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} for i := 0; i < 128*1024/len(bytes); i++ { fds[1].Write(bytes) } fds[1].WriteString("hello") return nil } elvish-0.17.0/pkg/rpc/000077500000000000000000000000001415471104000144335ustar00rootroot00000000000000elvish-0.17.0/pkg/rpc/client.go000066400000000000000000000200021415471104000162320ustar00rootroot00000000000000// Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package rpc import ( "bufio" "encoding/gob" "errors" "io" "log" "net" "sync" ) // ServerError represents an error that has been returned from // the remote side of the RPC connection. type ServerError string func (e ServerError) Error() string { return string(e) } var ErrShutdown = errors.New("connection is shut down") // Call represents an active RPC. type Call struct { ServiceMethod string // The name of the service and method to call. Args interface{} // The argument to the function (*struct). Reply interface{} // The reply from the function (*struct). Error error // After completion, the error status. Done chan *Call // Receives *Call when Go is complete. } // Client represents an RPC Client. // There may be multiple outstanding Calls associated // with a single Client, and a Client may be used by // multiple goroutines simultaneously. type Client struct { codec ClientCodec reqMutex sync.Mutex // protects following request Request mutex sync.Mutex // protects following seq uint64 pending map[uint64]*Call closing bool // user has called Close shutdown bool // server has told us to stop } // A ClientCodec implements writing of RPC requests and // reading of RPC responses for the client side of an RPC session. // The client calls WriteRequest to write a request to the connection // and calls ReadResponseHeader and ReadResponseBody in pairs // to read responses. The client calls Close when finished with the // connection. ReadResponseBody may be called with a nil // argument to force the body of the response to be read and then // discarded. // See NewClient's comment for information about concurrent access. type ClientCodec interface { WriteRequest(*Request, interface{}) error ReadResponseHeader(*Response) error ReadResponseBody(interface{}) error Close() error } func (client *Client) send(call *Call) { client.reqMutex.Lock() defer client.reqMutex.Unlock() // Register this call. client.mutex.Lock() if client.shutdown || client.closing { client.mutex.Unlock() call.Error = ErrShutdown call.done() return } seq := client.seq client.seq++ client.pending[seq] = call client.mutex.Unlock() // Encode and send the request. client.request.Seq = seq client.request.ServiceMethod = call.ServiceMethod err := client.codec.WriteRequest(&client.request, call.Args) if err != nil { client.mutex.Lock() call = client.pending[seq] delete(client.pending, seq) client.mutex.Unlock() if call != nil { call.Error = err call.done() } } } func (client *Client) input() { var err error var response Response for err == nil { response = Response{} err = client.codec.ReadResponseHeader(&response) if err != nil { break } seq := response.Seq client.mutex.Lock() call := client.pending[seq] delete(client.pending, seq) client.mutex.Unlock() switch { case call == nil: // We've got no pending call. That usually means that // WriteRequest partially failed, and call was already // removed; response is a server telling us about an // error reading request body. We should still attempt // to read error body, but there's no one to give it to. err = client.codec.ReadResponseBody(nil) if err != nil { err = errors.New("reading error body: " + err.Error()) } case response.Error != "": // We've got an error response. Give this to the request; // any subsequent requests will get the ReadResponseBody // error if there is one. call.Error = ServerError(response.Error) err = client.codec.ReadResponseBody(nil) if err != nil { err = errors.New("reading error body: " + err.Error()) } call.done() default: err = client.codec.ReadResponseBody(call.Reply) if err != nil { call.Error = errors.New("reading body " + err.Error()) } call.done() } } // Terminate pending calls. client.reqMutex.Lock() client.mutex.Lock() client.shutdown = true closing := client.closing if err == io.EOF { if closing { err = ErrShutdown } else { err = io.ErrUnexpectedEOF } } for _, call := range client.pending { call.Error = err call.done() } client.mutex.Unlock() client.reqMutex.Unlock() if debugLog && err != io.EOF && !closing { log.Println("rpc: client protocol error:", err) } } func (call *Call) done() { select { case call.Done <- call: // ok default: // We don't want to block here. It is the caller's responsibility to make // sure the channel has enough buffer space. See comment in Go(). if debugLog { log.Println("rpc: discarding Call reply due to insufficient Done chan capacity") } } } // NewClient returns a new Client to handle requests to the // set of services at the other end of the connection. // It adds a buffer to the write side of the connection so // the header and payload are sent as a unit. // // The read and write halves of the connection are serialized independently, // so no interlocking is required. However each half may be accessed // concurrently so the implementation of conn should protect against // concurrent reads or concurrent writes. func NewClient(conn io.ReadWriteCloser) *Client { encBuf := bufio.NewWriter(conn) client := &gobClientCodec{conn, gob.NewDecoder(conn), gob.NewEncoder(encBuf), encBuf} return NewClientWithCodec(client) } // NewClientWithCodec is like NewClient but uses the specified // codec to encode requests and decode responses. func NewClientWithCodec(codec ClientCodec) *Client { client := &Client{ codec: codec, pending: make(map[uint64]*Call), } go client.input() return client } type gobClientCodec struct { rwc io.ReadWriteCloser dec *gob.Decoder enc *gob.Encoder encBuf *bufio.Writer } func (c *gobClientCodec) WriteRequest(r *Request, body interface{}) (err error) { if err = c.enc.Encode(r); err != nil { return } if err = c.enc.Encode(body); err != nil { return } return c.encBuf.Flush() } func (c *gobClientCodec) ReadResponseHeader(r *Response) error { return c.dec.Decode(r) } func (c *gobClientCodec) ReadResponseBody(body interface{}) error { return c.dec.Decode(body) } func (c *gobClientCodec) Close() error { return c.rwc.Close() } // Dial connects to an RPC server at the specified network address. func Dial(network, address string) (*Client, error) { conn, err := net.Dial(network, address) if err != nil { return nil, err } return NewClient(conn), nil } // Close calls the underlying codec's Close method. If the connection is already // shutting down, ErrShutdown is returned. func (client *Client) Close() error { client.mutex.Lock() if client.closing { client.mutex.Unlock() return ErrShutdown } client.closing = true client.mutex.Unlock() return client.codec.Close() } // Go invokes the function asynchronously. It returns the Call structure representing // the invocation. The done channel will signal when the call is complete by returning // the same Call object. If done is nil, Go will allocate a new channel. // If non-nil, done must be buffered or Go will deliberately crash. func (client *Client) Go(serviceMethod string, args interface{}, reply interface{}, done chan *Call) *Call { call := new(Call) call.ServiceMethod = serviceMethod call.Args = args call.Reply = reply if done == nil { done = make(chan *Call, 10) // buffered. } else { // If caller passes done != nil, it must arrange that // done has enough buffer for the number of simultaneous // RPCs that will be using that channel. If the channel // is totally unbuffered, it's best not to run at all. if cap(done) == 0 { log.Panic("rpc: done channel is unbuffered") } } call.Done = done client.send(call) return call } // Call invokes the named function, waits for it to complete, and returns its error status. func (client *Client) Call(serviceMethod string, args interface{}, reply interface{}) error { call := <-client.Go(serviceMethod, args, reply, make(chan *Call, 1)).Done return call.Error } elvish-0.17.0/pkg/rpc/debug.go000066400000000000000000000006041415471104000160500ustar00rootroot00000000000000// Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package rpc /* Some HTML presented at http://machine:port/debug/rpc Lists services, their methods, and some statistics, still rudimentary. */ // If set, print log statements for internal and I/O errors. var debugLog = false elvish-0.17.0/pkg/rpc/server.go000066400000000000000000000503651415471104000163010ustar00rootroot00000000000000// Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. /* Package rpc is a trimmed down version of net/rpc in the standard library. Original doc: Package rpc provides access to the exported methods of an object across a network or other I/O connection. A server registers an object, making it visible as a service with the name of the type of the object. After registration, exported methods of the object will be accessible remotely. A server may register multiple objects (services) of different types but it is an error to register multiple objects of the same type. Only methods that satisfy these criteria will be made available for remote access; other methods will be ignored: - the method's type is exported. - the method is exported. - the method has two arguments, both exported (or builtin) types. - the method's second argument is a pointer. - the method has return type error. In effect, the method must look schematically like func (t *T) MethodName(argType T1, replyType *T2) error where T1 and T2 can be marshaled by encoding/gob. These requirements apply even if a different codec is used. (In the future, these requirements may soften for custom codecs.) The method's first argument represents the arguments provided by the caller; the second argument represents the result parameters to be returned to the caller. The method's return value, if non-nil, is passed back as a string that the client sees as if created by errors.New. If an error is returned, the reply parameter will not be sent back to the client. The server may handle requests on a single connection by calling ServeConn. More typically it will create a network listener and call Accept or, for an HTTP listener, HandleHTTP and http.Serve. A client wishing to use the service establishes a connection and then invokes NewClient on the connection. The convenience function Dial (DialHTTP) performs both steps for a raw network connection (an HTTP connection). The resulting Client object has two methods, Call and Go, that specify the service and method to call, a pointer containing the arguments, and a pointer to receive the result parameters. The Call method waits for the remote call to complete while the Go method launches the call asynchronously and signals completion using the Call structure's Done channel. Unless an explicit codec is set up, package encoding/gob is used to transport the data. Here is a simple example. A server wishes to export an object of type Arith: package server import "errors" type Args struct { A, B int } type Quotient struct { Quo, Rem int } type Arith int func (t *Arith) Multiply(args *Args, reply *int) error { *reply = args.A * args.B return nil } func (t *Arith) Divide(args *Args, quo *Quotient) error { if args.B == 0 { return errors.New("divide by zero") } quo.Quo = args.A / args.B quo.Rem = args.A % args.B return nil } The server calls (for HTTP service): arith := new(Arith) rpc.Register(arith) rpc.HandleHTTP() l, e := net.Listen("tcp", ":1234") if e != nil { log.Fatal("listen error:", e) } go http.Serve(l, nil) At this point, clients can see a service "Arith" with methods "Arith.Multiply" and "Arith.Divide". To invoke one, a client first dials the server: client, err := rpc.DialHTTP("tcp", serverAddress + ":1234") if err != nil { log.Fatal("dialing:", err) } Then it can make a remote call: // Synchronous call args := &server.Args{7,8} var reply int err = client.Call("Arith.Multiply", args, &reply) if err != nil { log.Fatal("arith error:", err) } fmt.Printf("Arith: %d*%d=%d", args.A, args.B, reply) or // Asynchronous call quotient := new(Quotient) divCall := client.Go("Arith.Divide", args, quotient, nil) replyCall := <-divCall.Done // will be equal to divCall // check errors, print, etc. A server implementation will often provide a simple, type-safe wrapper for the client. The net/rpc package is frozen and is not accepting new features. */ package rpc import ( "bufio" "encoding/gob" "errors" "go/token" "io" "log" "net" "reflect" "strings" "sync" ) // Precompute the reflect type for error. Can't use error directly // because Typeof takes an empty interface value. This is annoying. var typeOfError = reflect.TypeOf((*error)(nil)).Elem() type methodType struct { sync.Mutex // protects counters method reflect.Method ArgType reflect.Type ReplyType reflect.Type numCalls uint } type service struct { name string // name of service rcvr reflect.Value // receiver of methods for the service typ reflect.Type // type of the receiver method map[string]*methodType // registered methods } // Request is a header written before every RPC call. It is used internally // but documented here as an aid to debugging, such as when analyzing // network traffic. type Request struct { ServiceMethod string // format: "Service.Method" Seq uint64 // sequence number chosen by client next *Request // for free list in Server } // Response is a header written before every RPC return. It is used internally // but documented here as an aid to debugging, such as when analyzing // network traffic. type Response struct { ServiceMethod string // echoes that of the Request Seq uint64 // echoes that of the request Error string // error, if any. next *Response // for free list in Server } // Server represents an RPC Server. type Server struct { serviceMap sync.Map // map[string]*service reqLock sync.Mutex // protects freeReq freeReq *Request respLock sync.Mutex // protects freeResp freeResp *Response } // NewServer returns a new Server. func NewServer() *Server { return &Server{} } // DefaultServer is the default instance of *Server. var DefaultServer = NewServer() // Is this type exported or a builtin? func isExportedOrBuiltinType(t reflect.Type) bool { for t.Kind() == reflect.Ptr { t = t.Elem() } // PkgPath will be non-empty even for an exported type, // so we need to check the type name as well. return token.IsExported(t.Name()) || t.PkgPath() == "" } // Register publishes in the server the set of methods of the // receiver value that satisfy the following conditions: // - exported method of exported type // - two arguments, both of exported type // - the second argument is a pointer // - one return value, of type error // It returns an error if the receiver is not an exported type or has // no suitable methods. It also logs the error using package log. // The client accesses each method using a string of the form "Type.Method", // where Type is the receiver's concrete type. func (server *Server) Register(rcvr interface{}) error { return server.register(rcvr, "", false) } // RegisterName is like Register but uses the provided name for the type // instead of the receiver's concrete type. func (server *Server) RegisterName(name string, rcvr interface{}) error { return server.register(rcvr, name, true) } func (server *Server) register(rcvr interface{}, name string, useName bool) error { s := new(service) s.typ = reflect.TypeOf(rcvr) s.rcvr = reflect.ValueOf(rcvr) sname := reflect.Indirect(s.rcvr).Type().Name() if useName { sname = name } if sname == "" { s := "rpc.Register: no service name for type " + s.typ.String() log.Print(s) return errors.New(s) } if !token.IsExported(sname) && !useName { s := "rpc.Register: type " + sname + " is not exported" log.Print(s) return errors.New(s) } s.name = sname // Install the methods s.method = suitableMethods(s.typ, true) if len(s.method) == 0 { str := "" // To help the user, see if a pointer receiver would work. method := suitableMethods(reflect.PtrTo(s.typ), false) if len(method) != 0 { str = "rpc.Register: type " + sname + " has no exported methods of suitable type (hint: pass a pointer to value of that type)" } else { str = "rpc.Register: type " + sname + " has no exported methods of suitable type" } log.Print(str) return errors.New(str) } if _, dup := server.serviceMap.LoadOrStore(sname, s); dup { return errors.New("rpc: service already defined: " + sname) } return nil } // suitableMethods returns suitable Rpc methods of typ, it will report // error using log if reportErr is true. func suitableMethods(typ reflect.Type, reportErr bool) map[string]*methodType { methods := make(map[string]*methodType) for m := 0; m < typ.NumMethod(); m++ { method := typ.Method(m) mtype := method.Type mname := method.Name // Method must be exported. if method.PkgPath != "" { continue } // Method needs three ins: receiver, *args, *reply. if mtype.NumIn() != 3 { if reportErr { log.Printf("rpc.Register: method %q has %d input parameters; needs exactly three\n", mname, mtype.NumIn()) } continue } // First arg need not be a pointer. argType := mtype.In(1) if !isExportedOrBuiltinType(argType) { if reportErr { log.Printf("rpc.Register: argument type of method %q is not exported: %q\n", mname, argType) } continue } // Second arg must be a pointer. replyType := mtype.In(2) if replyType.Kind() != reflect.Ptr { if reportErr { log.Printf("rpc.Register: reply type of method %q is not a pointer: %q\n", mname, replyType) } continue } // Reply type must be exported. if !isExportedOrBuiltinType(replyType) { if reportErr { log.Printf("rpc.Register: reply type of method %q is not exported: %q\n", mname, replyType) } continue } // Method needs one out. if mtype.NumOut() != 1 { if reportErr { log.Printf("rpc.Register: method %q has %d output parameters; needs exactly one\n", mname, mtype.NumOut()) } continue } // The return type of the method must be error. if returnType := mtype.Out(0); returnType != typeOfError { if reportErr { log.Printf("rpc.Register: return type of method %q is %q, must be error\n", mname, returnType) } continue } methods[mname] = &methodType{method: method, ArgType: argType, ReplyType: replyType} } return methods } // A value sent as a placeholder for the server's response value when the server // receives an invalid request. It is never decoded by the client since the Response // contains an error when it is used. var invalidRequest = struct{}{} func (server *Server) sendResponse(sending *sync.Mutex, req *Request, reply interface{}, codec ServerCodec, errmsg string) { resp := server.getResponse() // Encode the response header resp.ServiceMethod = req.ServiceMethod if errmsg != "" { resp.Error = errmsg reply = invalidRequest } resp.Seq = req.Seq sending.Lock() err := codec.WriteResponse(resp, reply) if debugLog && err != nil { log.Println("rpc: writing response:", err) } sending.Unlock() server.freeResponse(resp) } func (m *methodType) NumCalls() (n uint) { m.Lock() n = m.numCalls m.Unlock() return n } func (s *service) call(server *Server, sending *sync.Mutex, wg *sync.WaitGroup, mtype *methodType, req *Request, argv, replyv reflect.Value, codec ServerCodec) { if wg != nil { defer wg.Done() } mtype.Lock() mtype.numCalls++ mtype.Unlock() function := mtype.method.Func // Invoke the method, providing a new value for the reply. returnValues := function.Call([]reflect.Value{s.rcvr, argv, replyv}) // The return value for the method is an error. errInter := returnValues[0].Interface() errmsg := "" if errInter != nil { errmsg = errInter.(error).Error() } server.sendResponse(sending, req, replyv.Interface(), codec, errmsg) server.freeRequest(req) } type gobServerCodec struct { rwc io.ReadWriteCloser dec *gob.Decoder enc *gob.Encoder encBuf *bufio.Writer closed bool } func (c *gobServerCodec) ReadRequestHeader(r *Request) error { return c.dec.Decode(r) } func (c *gobServerCodec) ReadRequestBody(body interface{}) error { return c.dec.Decode(body) } func (c *gobServerCodec) WriteResponse(r *Response, body interface{}) (err error) { if err = c.enc.Encode(r); err != nil { if c.encBuf.Flush() == nil { // Gob couldn't encode the header. Should not happen, so if it does, // shut down the connection to signal that the connection is broken. log.Println("rpc: gob error encoding response:", err) c.Close() } return } if err = c.enc.Encode(body); err != nil { if c.encBuf.Flush() == nil { // Was a gob problem encoding the body but the header has been written. // Shut down the connection to signal that the connection is broken. log.Println("rpc: gob error encoding body:", err) c.Close() } return } return c.encBuf.Flush() } func (c *gobServerCodec) Close() error { if c.closed { // Only call c.rwc.Close once; otherwise the semantics are undefined. return nil } c.closed = true return c.rwc.Close() } // ServeConn runs the server on a single connection. // ServeConn blocks, serving the connection until the client hangs up. // The caller typically invokes ServeConn in a go statement. // ServeConn uses the gob wire format (see package gob) on the // connection. To use an alternate codec, use ServeCodec. // See NewClient's comment for information about concurrent access. func (server *Server) ServeConn(conn io.ReadWriteCloser) { buf := bufio.NewWriter(conn) srv := &gobServerCodec{ rwc: conn, dec: gob.NewDecoder(conn), enc: gob.NewEncoder(buf), encBuf: buf, } server.ServeCodec(srv) } // ServeCodec is like ServeConn but uses the specified codec to // decode requests and encode responses. func (server *Server) ServeCodec(codec ServerCodec) { sending := new(sync.Mutex) wg := new(sync.WaitGroup) for { service, mtype, req, argv, replyv, keepReading, err := server.readRequest(codec) if err != nil { if debugLog && err != io.EOF { log.Println("rpc:", err) } if !keepReading { break } // send a response if we actually managed to read a header. if req != nil { server.sendResponse(sending, req, invalidRequest, codec, err.Error()) server.freeRequest(req) } continue } wg.Add(1) go service.call(server, sending, wg, mtype, req, argv, replyv, codec) } // We've seen that there are no more requests. // Wait for responses to be sent before closing codec. wg.Wait() codec.Close() } // ServeRequest is like ServeCodec but synchronously serves a single request. // It does not close the codec upon completion. func (server *Server) ServeRequest(codec ServerCodec) error { sending := new(sync.Mutex) service, mtype, req, argv, replyv, keepReading, err := server.readRequest(codec) if err != nil { if !keepReading { return err } // send a response if we actually managed to read a header. if req != nil { server.sendResponse(sending, req, invalidRequest, codec, err.Error()) server.freeRequest(req) } return err } service.call(server, sending, nil, mtype, req, argv, replyv, codec) return nil } func (server *Server) getRequest() *Request { server.reqLock.Lock() req := server.freeReq if req == nil { req = new(Request) } else { server.freeReq = req.next *req = Request{} } server.reqLock.Unlock() return req } func (server *Server) freeRequest(req *Request) { server.reqLock.Lock() req.next = server.freeReq server.freeReq = req server.reqLock.Unlock() } func (server *Server) getResponse() *Response { server.respLock.Lock() resp := server.freeResp if resp == nil { resp = new(Response) } else { server.freeResp = resp.next *resp = Response{} } server.respLock.Unlock() return resp } func (server *Server) freeResponse(resp *Response) { server.respLock.Lock() resp.next = server.freeResp server.freeResp = resp server.respLock.Unlock() } func (server *Server) readRequest(codec ServerCodec) (service *service, mtype *methodType, req *Request, argv, replyv reflect.Value, keepReading bool, err error) { service, mtype, req, keepReading, err = server.readRequestHeader(codec) if err != nil { if !keepReading { return } // discard body codec.ReadRequestBody(nil) return } // Decode the argument value. argIsValue := false // if true, need to indirect before calling. if mtype.ArgType.Kind() == reflect.Ptr { argv = reflect.New(mtype.ArgType.Elem()) } else { argv = reflect.New(mtype.ArgType) argIsValue = true } // argv guaranteed to be a pointer now. if err = codec.ReadRequestBody(argv.Interface()); err != nil { return } if argIsValue { argv = argv.Elem() } replyv = reflect.New(mtype.ReplyType.Elem()) switch mtype.ReplyType.Elem().Kind() { case reflect.Map: replyv.Elem().Set(reflect.MakeMap(mtype.ReplyType.Elem())) case reflect.Slice: replyv.Elem().Set(reflect.MakeSlice(mtype.ReplyType.Elem(), 0, 0)) } return } func (server *Server) readRequestHeader(codec ServerCodec) (svc *service, mtype *methodType, req *Request, keepReading bool, err error) { // Grab the request header. req = server.getRequest() err = codec.ReadRequestHeader(req) if err != nil { req = nil if err == io.EOF || err == io.ErrUnexpectedEOF { return } err = errors.New("rpc: server cannot decode request: " + err.Error()) return } // We read the header successfully. If we see an error now, // we can still recover and move on to the next request. keepReading = true dot := strings.LastIndex(req.ServiceMethod, ".") if dot < 0 { err = errors.New("rpc: service/method request ill-formed: " + req.ServiceMethod) return } serviceName := req.ServiceMethod[:dot] methodName := req.ServiceMethod[dot+1:] // Look up the request. svci, ok := server.serviceMap.Load(serviceName) if !ok { err = errors.New("rpc: can't find service " + req.ServiceMethod) return } svc = svci.(*service) mtype = svc.method[methodName] if mtype == nil { err = errors.New("rpc: can't find method " + req.ServiceMethod) } return } // Accept accepts connections on the listener and serves requests // for each incoming connection. Accept blocks until the listener // returns a non-nil error. The caller typically invokes Accept in a // go statement. func (server *Server) Accept(lis net.Listener) { for { conn, err := lis.Accept() if err != nil { log.Print("rpc.Serve: accept:", err.Error()) return } go server.ServeConn(conn) } } // Register publishes the receiver's methods in the DefaultServer. func Register(rcvr interface{}) error { return DefaultServer.Register(rcvr) } // RegisterName is like Register but uses the provided name for the type // instead of the receiver's concrete type. func RegisterName(name string, rcvr interface{}) error { return DefaultServer.RegisterName(name, rcvr) } // A ServerCodec implements reading of RPC requests and writing of // RPC responses for the server side of an RPC session. // The server calls ReadRequestHeader and ReadRequestBody in pairs // to read requests from the connection, and it calls WriteResponse to // write a response back. The server calls Close when finished with the // connection. ReadRequestBody may be called with a nil // argument to force the body of the request to be read and discarded. // See NewClient's comment for information about concurrent access. type ServerCodec interface { ReadRequestHeader(*Request) error ReadRequestBody(interface{}) error WriteResponse(*Response, interface{}) error // Close can be called multiple times and must be idempotent. Close() error } // ServeConn runs the DefaultServer on a single connection. // ServeConn blocks, serving the connection until the client hangs up. // The caller typically invokes ServeConn in a go statement. // ServeConn uses the gob wire format (see package gob) on the // connection. To use an alternate codec, use ServeCodec. // See NewClient's comment for information about concurrent access. func ServeConn(conn io.ReadWriteCloser) { DefaultServer.ServeConn(conn) } // ServeCodec is like ServeConn but uses the specified codec to // decode requests and encode responses. func ServeCodec(codec ServerCodec) { DefaultServer.ServeCodec(codec) } // ServeRequest is like ServeCodec but synchronously serves a single request. // It does not close the codec upon completion. func ServeRequest(codec ServerCodec) error { return DefaultServer.ServeRequest(codec) } // Accept accepts connections on the listener and serves requests // to DefaultServer for each incoming connection. // Accept blocks; the caller typically invokes it in a go statement. func Accept(lis net.Listener) { DefaultServer.Accept(lis) } elvish-0.17.0/pkg/shell/000077500000000000000000000000001415471104000147565ustar00rootroot00000000000000elvish-0.17.0/pkg/shell/interact.go000066400000000000000000000113411415471104000171160ustar00rootroot00000000000000package shell import ( "bufio" "fmt" "io" "os" "path/filepath" "strings" "syscall" "time" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/daemon/daemondefs" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/edit" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/mods/daemon" "src.elv.sh/pkg/mods/store" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/strutil" "src.elv.sh/pkg/sys" ) // InteractiveRescueShell determines whether a panic results in a rescue shell // being launched. It should be set to false by interactive mode unit tests. var interactiveRescueShell bool = true // Configuration for the interactive mode. type interactCfg struct { RC string ActivateDaemon daemondefs.ActivateFunc SpawnConfig *daemondefs.SpawnConfig } // Interface satisfied by the line editor. Used for swapping out the editor with // minEditor when necessary. type editor interface { ReadCode() (string, error) RunAfterCommandHooks(src parse.Source, duration float64, err error) } // Runs an interactive shell session. func interact(ev *eval.Evaler, fds [3]*os.File, cfg *interactCfg) { if interactiveRescueShell { defer handlePanic() } if cfg.ActivateDaemon != nil && cfg.SpawnConfig != nil { // TODO(xiaq): Connect to daemon and install daemon module // asynchronously. cl, err := cfg.ActivateDaemon(fds[2], cfg.SpawnConfig) if err != nil { fmt.Fprintln(fds[2], "Cannot connect to daemon:", err) fmt.Fprintln(fds[2], "Daemon-related functions will likely not work.") } defer func() { err := cl.Close() if err != nil { fmt.Fprintln(fds[2], "warning: failed to close connection to daemon:", err) } }() // Even if error is not nil, we install daemon-related functionalities // anyway. Daemon may eventually come online and become functional. ev.DaemonClient = cl ev.AddBeforeExit(func() { cl.Close() }) ev.AddModule("store", store.Ns(cl)) ev.AddModule("daemon", daemon.Ns(cl)) } // Build Editor. var ed editor if sys.IsATTY(fds[0]) { newed := edit.NewEditor(cli.NewTTY(fds[0], fds[2]), ev, ev.DaemonClient) ev.ExtendBuiltin(eval.BuildNs().AddNs("edit", newed)) ev.BgJobNotify = newed.Notify ed = newed } else { ed = newMinEditor(fds[0], fds[2]) } // Source rc.elv. if cfg.RC != "" { err := sourceRC(fds, ev, ed, cfg.RC) if err != nil { diag.ShowError(fds[2], err) } } term.Sanitize(fds[0], fds[2]) cooldown := time.Second cmdNum := 0 for { cmdNum++ line, err := ed.ReadCode() if err == io.EOF { break } else if err != nil { fmt.Fprintln(fds[2], "Editor error:", err) if _, isMinEditor := ed.(*minEditor); !isMinEditor { fmt.Fprintln(fds[2], "Falling back to basic line editor") ed = newMinEditor(fds[0], fds[2]) } else { fmt.Fprintln(fds[2], "Don't know what to do, pid is", os.Getpid()) fmt.Fprintln(fds[2], "Restarting editor in", cooldown) time.Sleep(cooldown) if cooldown < time.Minute { cooldown *= 2 } } continue } // No error; reset cooldown. cooldown = time.Second // Execute the command line only if it is not entirely whitespace. This keeps side-effects, // such as executing `$edit:after-command` hooks, from occurring when we didn't actually // evaluate any code entered by the user. if strings.TrimSpace(line) == "" { continue } src := parse.Source{Name: fmt.Sprintf("[tty %v]", cmdNum), Code: line} duration, err := evalInTTY(ev, fds, src) ed.RunAfterCommandHooks(src, duration, err) term.Sanitize(fds[0], fds[2]) if err != nil { diag.ShowError(fds[2], err) } } } // Interactive mode panic handler. func handlePanic() { r := recover() if r != nil { println() print(sys.DumpStack()) println() fmt.Println(r) println("\nExecing recovery shell /bin/sh") syscall.Exec("/bin/sh", []string{"/bin/sh"}, os.Environ()) } } func sourceRC(fds [3]*os.File, ev *eval.Evaler, ed editor, rcPath string) error { absPath, err := filepath.Abs(rcPath) if err != nil { return fmt.Errorf("cannot get full path of rc.elv: %v", err) } code, err := readFileUTF8(absPath) if err != nil { if os.IsNotExist(err) { return nil } return err } src := parse.Source{Name: absPath, Code: code, IsFile: true} duration, err := evalInTTY(ev, fds, src) ed.RunAfterCommandHooks(src, duration, err) return err } type minEditor struct { in *bufio.Reader out io.Writer } func newMinEditor(in, out *os.File) *minEditor { return &minEditor{bufio.NewReader(in), out} } func (ed *minEditor) RunAfterCommandHooks(src parse.Source, duration float64, err error) { // no-op; minEditor doesn't support this hook. } func (ed *minEditor) ReadCode() (string, error) { wd, err := os.Getwd() if err != nil { wd = "?" } fmt.Fprintf(ed.out, "%s> ", wd) line, err := ed.in.ReadString('\n') return strutil.ChopLineEnding(line), err } elvish-0.17.0/pkg/shell/interact_test.go000066400000000000000000000025041415471104000201560ustar00rootroot00000000000000package shell import ( "path/filepath" "testing" . "src.elv.sh/pkg/prog/progtest" . "src.elv.sh/pkg/testutil" ) func TestInteract(t *testing.T) { setupHomePaths(t) InTempDir(t) MustWriteFile("rc.elv", "echo hello from rc.elv") MustWriteFile("rc-dnc.elv", "echo $a") MustWriteFile("rc-fail.elv", "fail bad") Test(t, Program{}, thatElvishInteract().WithStdin("echo hello\n").WritesStdout("hello\n"), thatElvishInteract().WithStdin("fail mock\n").WritesStderrContaining("fail mock"), thatElvishInteract("-rc", "rc.elv").WritesStdout("hello from rc.elv\n"), // rc file does not compile thatElvishInteract("-rc", "rc-dnc.elv"). WritesStderrContaining("variable $a not found"), // rc file throws exception thatElvishInteract("-rc", "rc-fail.elv").WritesStderrContaining("fail bad"), // rc file not existing is OK thatElvishInteract("-rc", "rc-nonexistent.elv").DoesNothing(), ) } func TestInteract_DefaultRCPath(t *testing.T) { home := setupHomePaths(t) // Legacy RC path MustWriteFile( filepath.Join(home, ".elvish", "rc.elv"), "echo hello legacy rc.elv") // Note: non-legacy path is tested in interact_unix_test.go Test(t, Program{}, thatElvishInteract().WritesStdout("hello legacy rc.elv\n"), ) } func thatElvishInteract(args ...string) Case { return ThatElvish(args...).WritesStderrContaining("") } elvish-0.17.0/pkg/shell/interact_unix_test.go000066400000000000000000000031411415471104000212170ustar00rootroot00000000000000//go:build !windows && !plan9 && !js // +build !windows,!plan9,!js package shell import ( "fmt" "os" "path/filepath" "testing" "time" "src.elv.sh/pkg/daemon" "src.elv.sh/pkg/env" . "src.elv.sh/pkg/prog/progtest" . "src.elv.sh/pkg/testutil" ) func TestInteract_NewRcFile_Default(t *testing.T) { home := setupHomePaths(t) MustWriteFile( filepath.Join(home, ".config", "elvish", "rc.elv"), "echo hello new rc.elv") Test(t, Program{}, thatElvishInteract().WritesStdout("hello new rc.elv\n"), ) } func TestInteract_NewRcFile_XDG_CONFIG_HOME(t *testing.T) { setupHomePaths(t) xdgConfigHome := Setenv(t, env.XDG_CONFIG_HOME, TempDir(t)) MustWriteFile( filepath.Join(xdgConfigHome, "elvish", "rc.elv"), "echo hello XDG_CONFIG_HOME rc.elv") Test(t, Program{}, thatElvishInteract().WritesStdout("hello XDG_CONFIG_HOME rc.elv\n"), ) } func TestInteract_ConnectsToDaemon(t *testing.T) { InTempDir(t) // Run the daemon in the same process for simplicity. daemonDone := make(chan struct{}) defer func() { select { case <-daemonDone: case <-time.After(Scaled(2 * time.Second)): t.Errorf("timed out waiting for daemon to quit") } }() readyCh := make(chan struct{}) go func() { daemon.Serve("sock", "db", daemon.ServeOpts{Ready: readyCh}) close(daemonDone) }() select { case <-readyCh: // Do nothing case <-time.After(Scaled(2 * time.Second)): t.Fatalf("timed out waiting for daemon to start") } Test(t, Program{daemon.Activate}, thatElvishInteract("-sock", "sock", "-db", "db"). WithStdin("use daemon; echo $daemon:pid\n"). WritesStdout(fmt.Sprintln(os.Getpid())), ) } elvish-0.17.0/pkg/shell/paths.go000066400000000000000000000031621415471104000164260ustar00rootroot00000000000000package shell import ( "os" "path/filepath" "src.elv.sh/pkg/daemon/daemondefs" "src.elv.sh/pkg/fsutil" "src.elv.sh/pkg/prog" ) func rcPath() (string, error) { if legacyRC, exists := legacyDataPath("rc.elv", false); exists { return legacyRC, nil } return newRCPath() } func libPaths() ([]string, error) { paths, err := newLibPaths() if legacyLib, exists := legacyDataPath("lib", true); exists { paths = append(paths, legacyLib) } return paths, err } // Returns a SpawnConfig containing all the paths needed by the daemon. It // respects overrides of sock and db from CLI flags. func daemonPaths(flags *prog.Flags) (*daemondefs.SpawnConfig, error) { runDir, err := secureRunDir() if err != nil { return nil, err } sock := flags.Sock if sock == "" { sock = filepath.Join(runDir, "sock") } db := flags.DB if db == "" { var err error db, err = dbPath() if err != nil { return nil, err } err = os.MkdirAll(filepath.Dir(db), 0700) if err != nil { return nil, err } } return &daemondefs.SpawnConfig{DbPath: db, SockPath: sock, RunDir: runDir}, nil } func dbPath() (string, error) { if legacyDB, exists := legacyDataPath("db", false); exists { return legacyDB, nil } return newDBPath() } // Returns a path in the legacy data directory path, and whether it exists and // matches the expected file/directory property. func legacyDataPath(name string, dir bool) (string, bool) { home, err := fsutil.GetHome("") if err != nil { return "", false } p := filepath.Join(home, ".elvish", name) info, err := os.Stat(p) if err != nil || info.IsDir() != dir { return "", false } return p, true } elvish-0.17.0/pkg/shell/paths_test.go000066400000000000000000000000161415471104000174600ustar00rootroot00000000000000package shell elvish-0.17.0/pkg/shell/paths_unix.go000066400000000000000000000054071415471104000174750ustar00rootroot00000000000000//go:build !windows && !plan9 // +build !windows,!plan9 package shell import ( "fmt" "os" "path/filepath" "syscall" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/env" "src.elv.sh/pkg/fsutil" ) func newRCPath() (string, error) { return xdgHomePath(env.XDG_CONFIG_HOME, ".config", "elvish/rc.elv") } const elvishLib = "elvish/lib" func newLibPaths() ([]string, error) { var paths []string libConfig, errConfig := xdgHomePath(env.XDG_CONFIG_HOME, ".config", elvishLib) if errConfig == nil { paths = append(paths, libConfig) } libData, errData := xdgHomePath(env.XDG_DATA_HOME, ".local/share", elvishLib) if errData == nil { paths = append(paths, libData) } libSystem := os.Getenv(env.XDG_DATA_DIRS) if libSystem == "" { libSystem = "/usr/local/share:/usr/share" } for _, p := range filepath.SplitList(libSystem) { paths = append(paths, filepath.Join(p, elvishLib)) } return paths, diag.Errors(errConfig, errData) } func newDBPath() (string, error) { return xdgHomePath(env.XDG_STATE_HOME, ".local/state", "elvish/db.bolt") } func xdgHomePath(envName, fallback, suffix string) (string, error) { dir := os.Getenv(envName) if dir == "" { home, err := fsutil.GetHome("") if err != nil { return "", fmt.Errorf("resolve ~/%s/%s: %w", fallback, suffix, err) } dir = filepath.Join(home, fallback) } return filepath.Join(dir, suffix), nil } // Returns a "run directory" for storing ephemeral files, which is guaranteed // to be only accessible to the current user. // // The path of the run directory is either $XDG_RUNTIME_DIR/elvish or // $tmpdir/elvish-$uid (where $tmpdir is the system temporary directory). The // former is used if the XDG_RUNTIME_DIR environment variable exists and the // latter directory does not exist. func secureRunDir() (string, error) { runDirs := runDirCandidates() for _, runDir := range runDirs { if checkExclusiveAccess(runDir) { return runDir, nil } } runDir := runDirs[0] err := os.MkdirAll(runDir, 0700) if err != nil { return "", fmt.Errorf("mkdir: %v", err) } if !checkExclusiveAccess(runDir) { return "", fmt.Errorf("cannot create %v as a secure run directory", runDir) } return runDir, nil } // Returns one or more candidates for the run directory, in descending order of // preference. func runDirCandidates() []string { tmpDirPath := filepath.Join(os.TempDir(), fmt.Sprintf("elvish-%d", os.Getuid())) if os.Getenv(env.XDG_RUNTIME_DIR) != "" { xdgDirPath := filepath.Join(os.Getenv(env.XDG_RUNTIME_DIR), "elvish") return []string{xdgDirPath, tmpDirPath} } return []string{tmpDirPath} } func checkExclusiveAccess(runDir string) bool { info, err := os.Stat(runDir) if err != nil { return false } stat := info.Sys().(*syscall.Stat_t) return info.IsDir() && int(stat.Uid) == os.Getuid() && stat.Mode&077 == 0 } elvish-0.17.0/pkg/shell/paths_unix_test.go000066400000000000000000000034701415471104000205320ustar00rootroot00000000000000//go:build !windows && !plan9 // +build !windows,!plan9 package shell import ( "fmt" "os" "path/filepath" "testing" "src.elv.sh/pkg/env" "src.elv.sh/pkg/testutil" ) var elvishDashUID = fmt.Sprintf("elvish-%d", os.Getuid()) func TestSecureRunDir_PrefersXDGWhenNeitherExists(t *testing.T) { xdg, _ := setupForSecureRunDir(t) testSecureRunDir(t, filepath.Join(xdg, "elvish"), false) } func TestSecureRunDir_PrefersXDGWhenBothExist(t *testing.T) { xdg, tmp := setupForSecureRunDir(t) os.MkdirAll(filepath.Join(xdg, "elvish"), 0700) os.MkdirAll(filepath.Join(tmp, elvishDashUID), 0700) testSecureRunDir(t, filepath.Join(xdg, "elvish"), false) } func TestSecureRunDir_PrefersTmpWhenOnlyItExists(t *testing.T) { _, tmp := setupForSecureRunDir(t) os.MkdirAll(filepath.Join(tmp, elvishDashUID), 0700) testSecureRunDir(t, filepath.Join(tmp, elvishDashUID), false) } func TestSecureRunDir_PrefersTmpWhenXdgEnvIsEmpty(t *testing.T) { _, tmp := setupForSecureRunDir(t) os.Setenv(env.XDG_RUNTIME_DIR, "") testSecureRunDir(t, filepath.Join(tmp, elvishDashUID), false) } func TestSecureRunDir_ReturnsErrorWhenUnableToMkdir(t *testing.T) { xdg, _ := setupForSecureRunDir(t) os.WriteFile(filepath.Join(xdg, "elvish"), nil, 0600) testSecureRunDir(t, "", true) } func setupForSecureRunDir(c testutil.Cleanuper) (xdgRuntimeDir, tmpDir string) { xdg := testutil.Setenv(c, env.XDG_RUNTIME_DIR, testutil.TempDir(c)) tmp := testutil.Setenv(c, "TMPDIR", testutil.TempDir(c)) return xdg, tmp } func testSecureRunDir(t *testing.T, wantRunDir string, wantErr bool) { runDir, err := secureRunDir() if runDir != wantRunDir { t.Errorf("got rundir %q, want %q", runDir, wantRunDir) } if wantErr && err == nil { t.Errorf("got nil err, want non-nil") } else if !wantErr && err != nil { t.Errorf("got err %v, want nil err", err) } } elvish-0.17.0/pkg/shell/paths_windows.go000066400000000000000000000025721415471104000202040ustar00rootroot00000000000000package shell import ( "fmt" "os" "path/filepath" "golang.org/x/sys/windows" "src.elv.sh/pkg/env" ) func newRCPath() (string, error) { d, err := roamingAppData() if err != nil { return "", err } return filepath.Join(d, "elvish", "rc.elv"), nil } func newLibPaths() ([]string, error) { local, err := localAppData() if err != nil { return nil, err } localLib := filepath.Join(local, "elvish", "lib") roaming, err := roamingAppData() if err != nil { return nil, err } roamingLib := filepath.Join(roaming, "elvish", "lib") return []string{roamingLib, localLib}, nil } func newDBPath() (string, error) { d, err := localAppData() if err != nil { return "", err } return filepath.Join(d, "elvish", "db.bolt"), nil } func localAppData() (string, error) { return windows.KnownFolderPath(windows.FOLDERID_LocalAppData, windows.KF_FLAG_CREATE) } func roamingAppData() (string, error) { return windows.KnownFolderPath(windows.FOLDERID_RoamingAppData, windows.KF_FLAG_CREATE) } // getSecureRunDir stats elvish-$USERNAME under the default temp dir, creating // it if it doesn't yet exist, and return the directory name. func secureRunDir() (string, error) { username := os.Getenv(env.USERNAME) runDir := filepath.Join(os.TempDir(), "elvish-"+username) err := os.MkdirAll(runDir, 0700) if err != nil { return "", fmt.Errorf("mkdir: %v", err) } return runDir, nil } elvish-0.17.0/pkg/shell/script.go000066400000000000000000000047771415471104000166300ustar00rootroot00000000000000package shell import ( "encoding/json" "errors" "fmt" "os" "path/filepath" "unicode/utf8" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/parse" ) // Configuration for the script mode. type scriptCfg struct { Cmd bool CompileOnly bool JSON bool } // Executes a shell script. func script(ev *eval.Evaler, fds [3]*os.File, args []string, cfg *scriptCfg) int { arg0 := args[0] ev.SetArgs(args[1:]) var name, code string if cfg.Cmd { name = "code from -c" code = arg0 } else { var err error name, err = filepath.Abs(arg0) if err != nil { fmt.Fprintf(fds[2], "cannot get full path of script %q: %v\n", arg0, err) return 2 } code, err = readFileUTF8(name) if err != nil { fmt.Fprintf(fds[2], "cannot read script %q: %v\n", name, err) return 2 } } src := parse.Source{Name: name, Code: code, IsFile: true} if cfg.CompileOnly { parseErr, compileErr := ev.Check(src, fds[2]) if cfg.JSON { fmt.Fprintf(fds[1], "%s\n", errorsToJSON(parseErr, compileErr)) } else { if parseErr != nil { diag.ShowError(fds[2], parseErr) } if compileErr != nil { diag.ShowError(fds[2], compileErr) } } if parseErr != nil || compileErr != nil { return 2 } } else { _, err := evalInTTY(ev, fds, src) if err != nil { diag.ShowError(fds[2], err) return 2 } } return 0 } var errSourceNotUTF8 = errors.New("source is not UTF-8") func readFileUTF8(fname string) (string, error) { bytes, err := os.ReadFile(fname) if err != nil { return "", err } if !utf8.Valid(bytes) { return "", errSourceNotUTF8 } return string(bytes), nil } // An auxiliary struct for converting errors with diagnostics information to JSON. type errorInJSON struct { FileName string `json:"fileName"` Start int `json:"start"` End int `json:"end"` Message string `json:"message"` } // Converts parse and compilation errors into JSON. func errorsToJSON(parseErr *parse.Error, compileErr *diag.Error) []byte { var converted []errorInJSON if parseErr != nil { for _, e := range parseErr.Entries { converted = append(converted, errorInJSON{e.Context.Name, e.Context.From, e.Context.To, e.Message}) } } if compileErr != nil { converted = append(converted, errorInJSON{compileErr.Context.Name, compileErr.Context.From, compileErr.Context.To, compileErr.Message}) } jsonError, errMarshal := json.Marshal(converted) if errMarshal != nil { return []byte(`[{"message":"Unable to convert the errors to JSON"}]`) } return jsonError } elvish-0.17.0/pkg/shell/script_test.go000066400000000000000000000036161415471104000176560ustar00rootroot00000000000000package shell import ( "testing" . "src.elv.sh/pkg/prog/progtest" "src.elv.sh/pkg/testutil" ) func TestScript(t *testing.T) { testutil.InTempDir(t) testutil.MustWriteFile("a.elv", "echo hello") Test(t, Program{}, ThatElvish("a.elv").WritesStdout("hello\n"), ThatElvish("-c", "echo hello").WritesStdout("hello\n"), ThatElvish("non-existent.elv"). ExitsWith(2). WritesStderrContaining("cannot read script"), // parse error ThatElvish("-c", "echo ["). ExitsWith(2). WritesStderrContaining("parse error"), // parse error with -compileonly ThatElvish("-compileonly", "-json", "-c", "echo ["). ExitsWith(2). WritesStdout(`[{"fileName":"code from -c","start":6,"end":6,"message":"should be ']'"}]`+"\n"), // multiple parse errors with -compileonly -json ThatElvish("-compileonly", "-json", "-c", "echo [{"). ExitsWith(2). WritesStdout(`[{"fileName":"code from -c","start":7,"end":7,"message":"should be ',' or '}'"},{"fileName":"code from -c","start":7,"end":7,"message":"should be ']'"}]`+"\n"), // compilation error ThatElvish("-c", "echo $a"). ExitsWith(2). WritesStderrContaining("compilation error"), // compilation error with -compileonly ThatElvish("-compileonly", "-json", "-c", "echo $a"). ExitsWith(2). WritesStdout(`[{"fileName":"code from -c","start":5,"end":7,"message":"variable $a not found"}]`+"\n"), // parse error and compilation error with -compileonly ThatElvish("-compileonly", "-json", "-c", "echo [$a"). ExitsWith(2). WritesStdout(`[{"fileName":"code from -c","start":8,"end":8,"message":"should be ']'"},{"fileName":"code from -c","start":6,"end":8,"message":"variable $a not found"}]`+"\n"), // exception ThatElvish("-c", "fail failure"). ExitsWith(2). WritesStdout(""). WritesStderrContaining("fail failure"), // exception with -compileonly ThatElvish("-compileonly", "-c", "fail failure"). ExitsWith(0), ) } elvish-0.17.0/pkg/shell/shell.go000066400000000000000000000060301415471104000164130ustar00rootroot00000000000000// Package shell is the entry point for the terminal interface of Elvish. package shell import ( "fmt" "io" "os" "os/signal" "strconv" "time" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/daemon/daemondefs" "src.elv.sh/pkg/env" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/logutil" "src.elv.sh/pkg/mods" "src.elv.sh/pkg/mods/unix" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/prog" "src.elv.sh/pkg/sys" ) var logger = logutil.GetLogger("[shell] ") // Program is the shell subprogram. type Program struct { ActivateDaemon daemondefs.ActivateFunc } func (p Program) Run(fds [3]*os.File, f *prog.Flags, args []string) error { cleanup1 := IncSHLVL() defer cleanup1() cleanup2 := initTTYAndSignal(fds[2]) defer cleanup2() ev := MakeEvaler(fds[2]) if len(args) > 0 { exit := script( ev, fds, args, &scriptCfg{ Cmd: f.CodeInArg, CompileOnly: f.CompileOnly, JSON: f.JSON}) return prog.Exit(exit) } var spawnCfg *daemondefs.SpawnConfig if p.ActivateDaemon != nil { var err error spawnCfg, err = daemonPaths(f) if err != nil { fmt.Fprintln(fds[2], "Warning:", err) fmt.Fprintln(fds[2], "Storage daemon may not function.") } } rc := "" switch { case f.NoRc: // Leave rc empty case f.RC != "": // Use explicit -rc flag value rc = f.RC default: // Use default path to rc.elv var err error rc, err = rcPath() if err != nil { fmt.Fprintln(fds[2], "Warning:", err) } } interact(ev, fds, &interactCfg{ RC: rc, ActivateDaemon: p.ActivateDaemon, SpawnConfig: spawnCfg}) return nil } // MakeEvaler creates an Evaler, sets the module search directories and installs // all the standard builtin modules. It writes a warning message to the supplied // Writer if it could not initialize module search directories. func MakeEvaler(stderr io.Writer) *eval.Evaler { ev := eval.NewEvaler() libs, err := libPaths() if err != nil { fmt.Fprintln(stderr, "Warning:", err) } ev.LibDirs = libs mods.AddTo(ev) if unix.ExposeUnixNs { ev.AddModule("unix", unix.Ns) } return ev } // IncSHLVL increments the SHLVL environment variable. It returns a function to // restore the original value of SHLVL. func IncSHLVL() func() { oldValue, hadValue := os.LookupEnv(env.SHLVL) i, err := strconv.Atoi(oldValue) if err != nil { i = 0 } os.Setenv(env.SHLVL, strconv.Itoa(i+1)) if hadValue { return func() { os.Setenv(env.SHLVL, oldValue) } } else { return func() { os.Unsetenv(env.SHLVL) } } } func initTTYAndSignal(stderr io.Writer) func() { restoreTTY := term.SetupGlobal() sigCh := sys.NotifySignals() go func() { for sig := range sigCh { logger.Println("signal", sig) handleSignal(sig, stderr) } }() return func() { signal.Stop(sigCh) restoreTTY() } } func evalInTTY(ev *eval.Evaler, fds [3]*os.File, src parse.Source) (float64, error) { start := time.Now() ports, cleanup := eval.PortsFromFiles(fds, ev.ValuePrefix()) defer cleanup() err := ev.Eval(src, eval.EvalCfg{ Ports: ports, Interrupt: eval.ListenInterrupts, PutInFg: true}) end := time.Now() return end.Sub(start).Seconds(), err } elvish-0.17.0/pkg/shell/shell_test.go000066400000000000000000000036221415471104000174560ustar00rootroot00000000000000package shell import ( "os" "path/filepath" "testing" "src.elv.sh/pkg/env" . "src.elv.sh/pkg/prog/progtest" . "src.elv.sh/pkg/testutil" ) func TestShell_LegacyLibPath(t *testing.T) { home := setupHomePaths(t) MustWriteFile(filepath.Join(home, ".elvish", "lib", "a.elv"), "echo mod a") Test(t, Program{}, ThatElvish("-c", "use a").WritesStdout("mod a\n"), ) } // Most high-level tests against Program are specific to either script mode or // interactive mode, and are found in script_test.go and interact_test.go. var incSHLVLTests = []struct { name string old string unset bool wantNew string }{ {name: "normal", old: "10", wantNew: "11"}, {name: "unset", unset: true, wantNew: "1"}, {name: "invalid", old: "invalid", wantNew: "1"}, // Other shells don't agree on what to do when SHLVL is negative: // // ~> E:SHLVL=-100 bash -c 'echo $SHLVL' // 0 // ~> E:SHLVL=-100 zsh -c 'echo $SHLVL' // -99 // ~> E:SHLVL=-100 fish -c 'echo $SHLVL' // 1 // // Elvish follows Zsh here. {name: "negative", old: "-100", wantNew: "-99"}, } func TestIncSHLVL(t *testing.T) { Setenv(t, env.SHLVL, "") for _, test := range incSHLVLTests { t.Run(test.name, func(t *testing.T) { if test.unset { os.Unsetenv(env.SHLVL) } else { os.Setenv(env.SHLVL, test.old) } restore := IncSHLVL() shlvl := os.Getenv(env.SHLVL) if shlvl != test.wantNew { t.Errorf("got SHLVL = %q, want %q", shlvl, test.wantNew) } restore() // Test that state of SHLVL is restored. restored, restoredSet := os.LookupEnv(env.SHLVL) if test.unset { if restoredSet { t.Errorf("SHLVL not unset") } } else { if restored != test.old { t.Errorf("SHLVL restored to %q, want %q", restored, test.old) } } }) } } // Common test utilities. func setupHomePaths(t Cleanuper) string { Unsetenv(t, env.XDG_CONFIG_HOME) Unsetenv(t, env.XDG_DATA_HOME) return TempHome(t) } elvish-0.17.0/pkg/shell/signal_unix.go000066400000000000000000000005231415471104000176250ustar00rootroot00000000000000//go:build !windows && !plan9 // +build !windows,!plan9 package shell import ( "fmt" "io" "os" "syscall" "src.elv.sh/pkg/sys" ) func handleSignal(sig os.Signal, stderr io.Writer) { switch sig { case syscall.SIGHUP: syscall.Kill(0, syscall.SIGHUP) os.Exit(0) case syscall.SIGUSR1: fmt.Fprint(stderr, sys.DumpStack()) } } elvish-0.17.0/pkg/shell/signal_windows.go000066400000000000000000000001231415471104000203300ustar00rootroot00000000000000package shell import ( "io" "os" ) func handleSignal(os.Signal, io.Writer) { } elvish-0.17.0/pkg/store/000077500000000000000000000000001415471104000150035ustar00rootroot00000000000000elvish-0.17.0/pkg/store/buckets.go000066400000000000000000000002711415471104000167720ustar00rootroot00000000000000package store const ( bucketCmd = "cmd" bucketDir = "dir" bucketSharedVar = "shared_var" ) // The following buckets were used before and are thus reserved: // "schema" elvish-0.17.0/pkg/store/cmd.go000066400000000000000000000072171415471104000161040ustar00rootroot00000000000000package store import ( "bytes" "encoding/binary" bolt "go.etcd.io/bbolt" . "src.elv.sh/pkg/store/storedefs" ) func init() { initDB["initialize command history table"] = func(tx *bolt.Tx) error { _, err := tx.CreateBucketIfNotExists([]byte(bucketCmd)) return err } } // NextCmdSeq returns the next sequence number of the command history. func (s *dbStore) NextCmdSeq() (int, error) { var seq uint64 err := s.db.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(bucketCmd)) seq = b.Sequence() + 1 return nil }) return int(seq), err } // AddCmd adds a new command to the command history. func (s *dbStore) AddCmd(cmd string) (int, error) { var ( seq uint64 err error ) err = s.db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(bucketCmd)) seq, err = b.NextSequence() if err != nil { return err } return b.Put(marshalSeq(seq), []byte(cmd)) }) return int(seq), err } // DelCmd deletes a command history item with the given sequence number. func (s *dbStore) DelCmd(seq int) error { return s.db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(bucketCmd)) return b.Delete(marshalSeq(uint64(seq))) }) } // Cmd queries the command history item with the specified sequence number. func (s *dbStore) Cmd(seq int) (string, error) { var cmd string err := s.db.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(bucketCmd)) v := b.Get(marshalSeq(uint64(seq))) if v == nil { return ErrNoMatchingCmd } cmd = string(v) return nil }) return cmd, err } // IterateCmds iterates all the commands in the specified range, and calls the // callback with the content of each command sequentially. func (s *dbStore) IterateCmds(from, upto int, f func(Cmd)) error { return s.db.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(bucketCmd)) c := b.Cursor() for k, v := c.Seek(marshalSeq(uint64(from))); k != nil && unmarshalSeq(k) < uint64(upto); k, v = c.Next() { f(Cmd{Text: string(v), Seq: int(unmarshalSeq(k))}) } return nil }) } // CmdsWithSeq returns all commands within the specified range. func (s *dbStore) CmdsWithSeq(from, upto int) ([]Cmd, error) { var cmds []Cmd err := s.IterateCmds(from, upto, func(cmd Cmd) { cmds = append(cmds, cmd) }) return cmds, err } // NextCmd finds the first command after the given sequence number (inclusive) // with the given prefix. func (s *dbStore) NextCmd(from int, prefix string) (Cmd, error) { var cmd Cmd err := s.db.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(bucketCmd)) c := b.Cursor() p := []byte(prefix) for k, v := c.Seek(marshalSeq(uint64(from))); k != nil; k, v = c.Next() { if bytes.HasPrefix(v, p) { cmd = Cmd{Text: string(v), Seq: int(unmarshalSeq(k))} return nil } } return ErrNoMatchingCmd }) return cmd, err } // PrevCmd finds the last command before the given sequence number (exclusive) // with the given prefix. func (s *dbStore) PrevCmd(upto int, prefix string) (Cmd, error) { var cmd Cmd err := s.db.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(bucketCmd)) c := b.Cursor() p := []byte(prefix) var v []byte k, _ := c.Seek(marshalSeq(uint64(upto))) if k == nil { // upto > LAST k, v = c.Last() if k == nil { return ErrNoMatchingCmd } } else { k, v = c.Prev() // upto exists, find the previous one } for ; k != nil; k, v = c.Prev() { if bytes.HasPrefix(v, p) { cmd = Cmd{Text: string(v), Seq: int(unmarshalSeq(k))} return nil } } return ErrNoMatchingCmd }) return cmd, err } func marshalSeq(seq uint64) []byte { b := make([]byte, 8) binary.BigEndian.PutUint64(b, seq) return b } func unmarshalSeq(key []byte) uint64 { return binary.BigEndian.Uint64(key) } elvish-0.17.0/pkg/store/cmd_test.go000066400000000000000000000002631415471104000171350ustar00rootroot00000000000000package store_test import ( "testing" "src.elv.sh/pkg/store" "src.elv.sh/pkg/store/storetest" ) func TestCmd(t *testing.T) { storetest.TestCmd(t, store.MustTempStore(t)) } elvish-0.17.0/pkg/store/db_store.go000066400000000000000000000035431415471104000171400ustar00rootroot00000000000000package store import ( "fmt" "sync" "time" bolt "go.etcd.io/bbolt" "src.elv.sh/pkg/logutil" . "src.elv.sh/pkg/store/storedefs" ) var logger = logutil.GetLogger("[store] ") var initDB = map[string](func(*bolt.Tx) error){} // DBStore is the permanent storage backend for elvish. It is not thread-safe. // In particular, the store may be closed while another goroutine is still // accessing the store. To prevent bad things from happening, every time the // main goroutine spawns a new goroutine to operate on the store, it should call // wg.Add(1) in the main goroutine before spawning another goroutine, and // call wg.Done() in the spawned goroutine after the operation is finished. type DBStore interface { Store Close() error } type dbStore struct { db *bolt.DB wg sync.WaitGroup // used for registering outstanding operations on the store } func dbWithDefaultOptions(dbname string) (*bolt.DB, error) { db, err := bolt.Open(dbname, 0644, &bolt.Options{ Timeout: 1 * time.Second, }) return db, err } // NewStore creates a new Store from the given file. func NewStore(dbname string) (DBStore, error) { db, err := dbWithDefaultOptions(dbname) if err != nil { return nil, err } return NewStoreFromDB(db) } // NewStoreFromDB creates a new Store from a bolt DB. func NewStoreFromDB(db *bolt.DB) (DBStore, error) { logger.Println("initializing store") defer logger.Println("initialized store") st := &dbStore{ db: db, wg: sync.WaitGroup{}, } err := db.Update(func(tx *bolt.Tx) error { for name, fn := range initDB { err := fn(tx) if err != nil { return fmt.Errorf("failed to %s: %v", name, err) } } return nil }) return st, err } // Close waits for all outstanding operations to finish, and closes the // database. func (s *dbStore) Close() error { if s == nil || s.db == nil { return nil } s.wg.Wait() return s.db.Close() } elvish-0.17.0/pkg/store/dir.go000066400000000000000000000046601415471104000161160ustar00rootroot00000000000000package store import ( "sort" "strconv" bolt "go.etcd.io/bbolt" . "src.elv.sh/pkg/store/storedefs" ) // Parameters for directory history scores. const ( DirScoreDecay = 0.986 // roughly 0.5^(1/50) DirScoreIncrement = 10 DirScorePrecision = 6 ) func init() { initDB["initialize directory history table"] = func(tx *bolt.Tx) error { _, err := tx.CreateBucketIfNotExists([]byte(bucketDir)) return err } } func marshalScore(score float64) []byte { return []byte(strconv.FormatFloat(score, 'E', DirScorePrecision, 64)) } func unmarshalScore(data []byte) float64 { f, _ := strconv.ParseFloat(string(data), 64) return f } // AddDir adds a directory to the directory history. func (s *dbStore) AddDir(d string, incFactor float64) error { return s.db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(bucketDir)) c := b.Cursor() for k, v := c.First(); k != nil; k, v = c.Next() { score := unmarshalScore(v) * DirScoreDecay b.Put(k, marshalScore(score)) } k := []byte(d) score := float64(0) if v := b.Get(k); v != nil { score = unmarshalScore(v) } score += DirScoreIncrement * incFactor return b.Put(k, marshalScore(score)) }) } // AddDir adds a directory and its score to history. func (s *dbStore) AddDirRaw(d string, score float64) error { return s.db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(bucketDir)) return b.Put([]byte(d), marshalScore(score)) }) } // DelDir deletes a directory record from history. func (s *dbStore) DelDir(d string) error { return s.db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(bucketDir)) return b.Delete([]byte(d)) }) } // Dirs lists all directories in the directory history whose names are not // in the blacklist. The results are ordered by scores in descending order. func (s *dbStore) Dirs(blacklist map[string]struct{}) ([]Dir, error) { var dirs []Dir err := s.db.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(bucketDir)) c := b.Cursor() for k, v := c.First(); k != nil; k, v = c.Next() { d := string(k) if _, ok := blacklist[d]; ok { continue } dirs = append(dirs, Dir{ Path: d, Score: unmarshalScore(v), }) } sort.Sort(sort.Reverse(dirList(dirs))) return nil }) return dirs, err } type dirList []Dir func (dl dirList) Len() int { return len(dl) } func (dl dirList) Less(i, j int) bool { return dl[i].Score < dl[j].Score } func (dl dirList) Swap(i, j int) { dl[i], dl[j] = dl[j], dl[i] } elvish-0.17.0/pkg/store/dir_test.go000066400000000000000000000002631415471104000171500ustar00rootroot00000000000000package store_test import ( "testing" "src.elv.sh/pkg/store" "src.elv.sh/pkg/store/storetest" ) func TestDir(t *testing.T) { storetest.TestDir(t, store.MustTempStore(t)) } elvish-0.17.0/pkg/store/shared_var.go000066400000000000000000000022131415471104000174460ustar00rootroot00000000000000package store import ( "errors" bolt "go.etcd.io/bbolt" ) // ErrNoSharedVar is returned by Store.SharedVar when there is no such variable. var ErrNoSharedVar = errors.New("no such shared variable") func init() { initDB["initialize shared variable table"] = func(tx *bolt.Tx) error { _, err := tx.CreateBucketIfNotExists([]byte(bucketSharedVar)) return err } } // SharedVar gets the value of a shared variable. func (s *dbStore) SharedVar(n string) (string, error) { var value string err := s.db.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(bucketSharedVar)) v := b.Get([]byte(n)) if v == nil { return ErrNoSharedVar } value = string(v) return nil }) return value, err } // SetSharedVar sets the value of a shared variable. func (s *dbStore) SetSharedVar(n, v string) error { return s.db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(bucketSharedVar)) return b.Put([]byte(n), []byte(v)) }) } // DelSharedVar deletes a shared variable. func (s *dbStore) DelSharedVar(n string) error { return s.db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(bucketSharedVar)) return b.Delete([]byte(n)) }) } elvish-0.17.0/pkg/store/shared_var_test.go000066400000000000000000000002771415471104000205150ustar00rootroot00000000000000package store_test import ( "testing" "src.elv.sh/pkg/store" "src.elv.sh/pkg/store/storetest" ) func TestSharedVar(t *testing.T) { storetest.TestSharedVar(t, store.MustTempStore(t)) } elvish-0.17.0/pkg/store/staticcheck.conf000066400000000000000000000000721415471104000201360ustar00rootroot00000000000000dot_import_whitelist = ["src.elv.sh/pkg/store/storedefs"] elvish-0.17.0/pkg/store/store.go000066400000000000000000000001061415471104000164630ustar00rootroot00000000000000// Package store defines the permanent storage service. package store elvish-0.17.0/pkg/store/storedefs/000077500000000000000000000000001415471104000170015ustar00rootroot00000000000000elvish-0.17.0/pkg/store/storedefs/storedefs.go000066400000000000000000000024501415471104000213270ustar00rootroot00000000000000// Package storedefs contains definitions of the store API. // // It is a separate package so that packages that only depend on the store API // does not need to depend on the concrete implementation. package storedefs import "errors" // NoBlacklist is an empty blacklist, to be used in GetDirs. var NoBlacklist = map[string]struct{}{} // ErrNoMatchingCmd is the error returned when a LastCmd or FirstCmd query // completes with no result. var ErrNoMatchingCmd = errors.New("no matching command line") // Store is an interface satisfied by the storage service. type Store interface { NextCmdSeq() (int, error) AddCmd(text string) (int, error) DelCmd(seq int) error Cmd(seq int) (string, error) CmdsWithSeq(from, upto int) ([]Cmd, error) NextCmd(from int, prefix string) (Cmd, error) PrevCmd(upto int, prefix string) (Cmd, error) AddDir(dir string, incFactor float64) error DelDir(dir string) error Dirs(blacklist map[string]struct{}) ([]Dir, error) SharedVar(name string) (string, error) SetSharedVar(name, value string) error DelSharedVar(name string) error } // Dir is an entry in the directory history. type Dir struct { Path string Score float64 } func (Dir) IsStructMap() {} // Cmd is an entry in the command history. type Cmd struct { Text string Seq int } func (Cmd) IsStructMap() {} elvish-0.17.0/pkg/store/storetest/000077500000000000000000000000001415471104000170375ustar00rootroot00000000000000elvish-0.17.0/pkg/store/storetest/cmd.go000066400000000000000000000056311415471104000201360ustar00rootroot00000000000000package storetest import ( "reflect" "testing" "src.elv.sh/pkg/store/storedefs" ) var ( cmds = []string{"echo foo", "put bar", "put lorem", "echo bar"} searches = []struct { next bool seq int prefix string wantedSeq int wantedCmd string wantedErr error }{ {false, 5, "echo", 4, "echo bar", nil}, {false, 5, "put", 3, "put lorem", nil}, {false, 4, "echo", 1, "echo foo", nil}, {false, 3, "f", 0, "", storedefs.ErrNoMatchingCmd}, {false, 1, "", 0, "", storedefs.ErrNoMatchingCmd}, {true, 1, "echo", 1, "echo foo", nil}, {true, 1, "put", 2, "put bar", nil}, {true, 2, "echo", 4, "echo bar", nil}, {true, 4, "put", 0, "", storedefs.ErrNoMatchingCmd}, } ) // TestCmd tests the command history functionality of a Store. func TestCmd(t *testing.T, store storedefs.Store) { startSeq, err := store.NextCmdSeq() if startSeq != 1 || err != nil { t.Errorf("store.NextCmdSeq() => (%v, %v), want (1, nil)", startSeq, err) } // AddCmd for i, cmd := range cmds { wantSeq := startSeq + i seq, err := store.AddCmd(cmd) if seq != wantSeq || err != nil { t.Errorf("store.AddCmd(%v) => (%v, %v), want (%v, nil)", cmd, seq, err, wantSeq) } } endSeq, err := store.NextCmdSeq() wantedEndSeq := startSeq + len(cmds) if endSeq != wantedEndSeq || err != nil { t.Errorf("store.NextCmdSeq() => (%v, %v), want (%v, nil)", endSeq, err, wantedEndSeq) } // CmdsWithSeq wantCmdWithSeqs := make([]storedefs.Cmd, len(cmds)) for i, cmd := range cmds { wantCmdWithSeqs[i] = storedefs.Cmd{Text: cmd, Seq: i + 1} } for i := 0; i < len(cmds); i++ { for j := i; j <= len(cmds); j++ { cmdWithSeqs, err := store.CmdsWithSeq(i+1, j+1) if !equalCmds(cmdWithSeqs, wantCmdWithSeqs[i:j]) || err != nil { t.Errorf("store.CmdsWithSeq(%v, %v) -> (%v, %v), want (%v, nil)", i+1, j+1, cmdWithSeqs, err, wantCmdWithSeqs[i:j]) } } } // Cmd for i, wantedCmd := range cmds { seq := i + startSeq cmd, err := store.Cmd(seq) if cmd != wantedCmd || err != nil { t.Errorf("store.Cmd(%v) => (%v, %v), want (%v, nil)", seq, cmd, err, wantedCmd) } } // PrevCmd and NextCmd for _, tt := range searches { f := store.PrevCmd funcname := "store.PrevCmd" if tt.next { f = store.NextCmd funcname = "store.NextCmd" } cmd, err := f(tt.seq, tt.prefix) wantedCmd := storedefs.Cmd{Text: tt.wantedCmd, Seq: tt.wantedSeq} if cmd != wantedCmd || !matchErr(err, tt.wantedErr) { t.Errorf("%s(%v, %v) => (%v, %v), want (%v, %v)", funcname, tt.seq, tt.prefix, cmd, err, wantedCmd, tt.wantedErr) } } // DelCmd if err := store.DelCmd(1); err != nil { t.Error("Failed to remove cmd") } if seq, err := store.Cmd(1); !matchErr(err, storedefs.ErrNoMatchingCmd) { t.Errorf("Cmd(1) => (%v, %v), want (%v, %v)", seq, err, "", storedefs.ErrNoMatchingCmd) } } func equalCmds(a, b []storedefs.Cmd) bool { return (len(a) == 0 && len(b) == 0) || reflect.DeepEqual(a, b) } elvish-0.17.0/pkg/store/storetest/dir.go000066400000000000000000000025021415471104000201430ustar00rootroot00000000000000package storetest import ( "reflect" "testing" "src.elv.sh/pkg/store" "src.elv.sh/pkg/store/storedefs" ) var ( dirsToAdd = []string{"/usr/local", "/usr", "/usr/bin", "/usr"} black = map[string]struct{}{"/usr/local": {}} wantedDirs = []storedefs.Dir{ { Path: "/usr", Score: store.DirScoreIncrement*store.DirScoreDecay*store.DirScoreDecay + store.DirScoreIncrement, }, { Path: "/usr/bin", Score: store.DirScoreIncrement * store.DirScoreDecay, }, } dirToDel = "/usr" wantedDirsAfterDel = []storedefs.Dir{ { Path: "/usr/bin", Score: store.DirScoreIncrement * store.DirScoreDecay, }, } ) // TestDir tests the directory history functionality of a Store. func TestDir(t *testing.T, tStore storedefs.Store) { for _, path := range dirsToAdd { err := tStore.AddDir(path, 1) if err != nil { t.Errorf("tStore.AddDir(%q) => %v, want ", path, err) } } dirs, err := tStore.Dirs(black) if err != nil || !reflect.DeepEqual(dirs, wantedDirs) { t.Errorf(`tStore.ListDirs() => (%v, %v), want (%v, )`, dirs, err, wantedDirs) } tStore.DelDir(dirToDel) dirs, err = tStore.Dirs(black) if err != nil || !reflect.DeepEqual(dirs, wantedDirsAfterDel) { t.Errorf(`After DelDir("/usr"), tStore.ListDirs() => (%v, %v), want (%v, )`, dirs, err, wantedDirsAfterDel) } } elvish-0.17.0/pkg/store/storetest/shared_var.go000066400000000000000000000025311415471104000215050ustar00rootroot00000000000000package storetest import ( "testing" "src.elv.sh/pkg/store" "src.elv.sh/pkg/store/storedefs" ) // TestSharedVar tests the shared variable functionality of a Store. func TestSharedVar(t *testing.T, tStore storedefs.Store) { varname := "foo" value1 := "lorem ipsum" value2 := "o mores, o tempora" // Getting an nonexistent variable should return ErrNoSharedVar. _, err := tStore.SharedVar(varname) if !matchErr(err, store.ErrNoSharedVar) { t.Error("want ErrNoSharedVar, got", err) } // Setting a variable for the first time creates it. err = tStore.SetSharedVar(varname, value1) if err != nil { t.Error("want no error, got", err) } v, err := tStore.SharedVar(varname) if v != value1 || err != nil { t.Errorf("want %q and no error, got %q and %v", value1, v, err) } // Setting an existing variable updates its value. err = tStore.SetSharedVar(varname, value2) if err != nil { t.Error("want no error, got", err) } v, err = tStore.SharedVar(varname) if v != value2 || err != nil { t.Errorf("want %q and no error, got %q and %v", value2, v, err) } // After deleting a variable, access to it cause ErrNoSharedVar. err = tStore.DelSharedVar(varname) if err != nil { t.Error("want no error, got", err) } _, err = tStore.SharedVar(varname) if !matchErr(err, store.ErrNoSharedVar) { t.Error("want ErrNoSharedVar, got", err) } } elvish-0.17.0/pkg/store/storetest/storetest.go000066400000000000000000000003211415471104000214160ustar00rootroot00000000000000// Package storetest keeps test suites against storedefs.Store. package storetest func matchErr(e1, e2 error) bool { return (e1 == nil && e2 == nil) || (e1 != nil && e2 != nil && e1.Error() == e2.Error()) } elvish-0.17.0/pkg/store/temp_store.go000066400000000000000000000015761415471104000175240ustar00rootroot00000000000000package store import ( "fmt" "os" "time" bolt "go.etcd.io/bbolt" "src.elv.sh/pkg/testutil" ) // MustTempStore returns a Store backed by a temporary file for testing. The // Store and its underlying file will be cleaned up properly after the test is // finished. func MustTempStore(c testutil.Cleanuper) DBStore { f, err := os.CreateTemp("", "elvish.test") if err != nil { panic(fmt.Sprintf("open temp file: %v", err)) } db, err := bolt.Open(f.Name(), 0644, &bolt.Options{ Timeout: time.Second, NoSync: true, NoFreelistSync: true}) if err != nil { panic(fmt.Sprintf("open boltdb: %v", err)) } st, err := NewStoreFromDB(db) if err != nil { panic(fmt.Sprintf("create Store instance: %v", err)) } c.Cleanup(func() { st.Close() f.Close() err = os.Remove(f.Name()) if err != nil { fmt.Fprintln(os.Stderr, "failed to remove temp file:", err) } }) return st } elvish-0.17.0/pkg/strutil/000077500000000000000000000000001415471104000153555ustar00rootroot00000000000000elvish-0.17.0/pkg/strutil/camel_to_dashed.go000066400000000000000000000006701415471104000210020ustar00rootroot00000000000000package strutil import ( "bytes" "unicode" ) // CamelToDashed converts a CamelCaseIdentifier to a dash-separated-identifier, // or a camelCaseIdentifier to a -dash-separated-identifier. func CamelToDashed(camel string) string { var buf bytes.Buffer for i, r := range camel { if (i == 0 && unicode.IsLower(r)) || (i > 0 && unicode.IsUpper(r)) { buf.WriteRune('-') } buf.WriteRune(unicode.ToLower(r)) } return buf.String() } elvish-0.17.0/pkg/strutil/camel_to_dashed_test.go000066400000000000000000000006621415471104000220420ustar00rootroot00000000000000package strutil import "testing" var tests = []struct { camel string want string }{ {"CamelCase", "camel-case"}, {"camelCase", "-camel-case"}, {"123", "123"}, {"你好", "你好"}, } func TestCamelToDashed(t *testing.T) { for _, test := range tests { camel, want := test.camel, test.want dashed := CamelToDashed(camel) if dashed != want { t.Errorf("CamelToDashed(%q) => %q, want %q", camel, dashed, want) } } } elvish-0.17.0/pkg/strutil/chop.go000066400000000000000000000012221415471104000166320ustar00rootroot00000000000000package strutil // ChopLineEnding removes a line ending ("\r\n" or "\n") from the end of s. It // returns s if it doesn't end with a line ending. func ChopLineEnding(s string) string { if len(s) >= 2 && s[len(s)-2:] == "\r\n" { // Windows line ending return s[:len(s)-2] } else if len(s) >= 1 && s[len(s)-1] == '\n' { // UNIX line ending return s[:len(s)-1] } return s } // ChopTerminator removes a specific terminator byte from the end of s. It // returns s if it doesn't end with the specified terminator. func ChopTerminator(s string, terminator byte) string { if len(s) >= 1 && s[len(s)-1] == terminator { return s[:len(s)-1] } return s } elvish-0.17.0/pkg/strutil/chop_test.go000066400000000000000000000013751415471104000177020ustar00rootroot00000000000000package strutil import ( "testing" . "src.elv.sh/pkg/tt" ) func TestChopLineEnding(t *testing.T) { Test(t, Fn("ChopLineEnding", ChopLineEnding), Table{ Args("").Rets(""), Args("text").Rets("text"), Args("text\n").Rets("text"), Args("text\r\n").Rets("text"), // Only chop off one line ending Args("text\n\n").Rets("text\n"), // Preserve internal line endings Args("text\ntext 2\n").Rets("text\ntext 2"), }) } func TestChopTerminator(t *testing.T) { Test(t, Fn("ChopTerminator", ChopTerminator), Table{ Args("", byte('\x00')).Rets(""), Args("foo", byte('\x00')).Rets("foo"), Args("foo\x00", byte('\x00')).Rets("foo"), Args("foo\x00\x00", byte('\x00')).Rets("foo\x00"), Args("foo\x00bar\x00", byte('\x00')).Rets("foo\x00bar"), }) } elvish-0.17.0/pkg/strutil/eol_sol.go000066400000000000000000000006161415471104000173430ustar00rootroot00000000000000package strutil import ( "strings" ) // FindFirstEOL returns the index of the first '\n'. When there is no '\n', the // length of s is returned. func FindFirstEOL(s string) int { eol := strings.IndexRune(s, '\n') if eol == -1 { eol = len(s) } return eol } // FindLastSOL returns an index just after the last '\n'. func FindLastSOL(s string) int { return strings.LastIndex(s, "\n") + 1 } elvish-0.17.0/pkg/strutil/eol_sol_test.go000066400000000000000000000010371415471104000204000ustar00rootroot00000000000000package strutil import "testing" var EOLSOLTests = []struct { s string wantFirstEOL, wantLastSOL int }{ {"0", 1, 0}, {"\n12", 0, 1}, {"01\n", 2, 3}, {"01\n34", 2, 3}, } func TestEOLSOL(t *testing.T) { for _, tc := range EOLSOLTests { eol := FindFirstEOL(tc.s) if eol != tc.wantFirstEOL { t.Errorf("FindFirstEOL(%q) => %d, want %d", tc.s, eol, tc.wantFirstEOL) } sol := FindLastSOL(tc.s) if sol != tc.wantLastSOL { t.Errorf("FindLastSOL(%q) => %d, want %d", tc.s, sol, tc.wantLastSOL) } } } elvish-0.17.0/pkg/strutil/strutil.go000066400000000000000000000000761415471104000174150ustar00rootroot00000000000000// Package strutil provides string utilities. package strutil elvish-0.17.0/pkg/strutil/subseq.go000066400000000000000000000006351415471104000172120ustar00rootroot00000000000000package strutil import "strings" // HasSubseq determines whether s has t as its subsequence. A string t is a // subsequence of a string s if and only if there is a possible sequence of // steps of deleting characters from s that result in t. func HasSubseq(s, t string) bool { for _, p := range t { i := strings.IndexRune(s, p) if i == -1 { return false } s = s[i+len(string(p)):] } return true } elvish-0.17.0/pkg/strutil/subseq_test.go000066400000000000000000000011571415471104000202510ustar00rootroot00000000000000package strutil import "testing" var hasSubseqTests = []struct { s, t string want bool }{ {"", "", true}, {"a", "", true}, {"a", "a", true}, {"ab", "a", true}, {"ab", "b", true}, {"abc", "ac", true}, {"abcdefg", "bg", true}, {"abcdefg", "ga", false}, {"foo lorem ipsum", "f l i", true}, {"foo lorem ipsum", "oo o pm", true}, {"你好世界", "好", true}, {"你好世界", "好界", true}, } func TestHasSubseq(t *testing.T) { for _, test := range hasSubseqTests { if b := HasSubseq(test.s, test.t); b != test.want { t.Errorf("HasSubseq(%q, %q) = %v, want %v", test.s, test.t, b, test.want) } } } elvish-0.17.0/pkg/sys/000077500000000000000000000000001415471104000144655ustar00rootroot00000000000000elvish-0.17.0/pkg/sys/dumpstack.go000066400000000000000000000004061415471104000170070ustar00rootroot00000000000000package sys import "runtime" const dumpStackBufSizeInit = 4096 func DumpStack() string { buf := make([]byte, dumpStackBufSizeInit) for { n := runtime.Stack(buf, true) if n < cap(buf) { return string(buf[:n]) } buf = make([]byte, cap(buf)*2) } } elvish-0.17.0/pkg/sys/eunix/000077500000000000000000000000001415471104000156155ustar00rootroot00000000000000elvish-0.17.0/pkg/sys/eunix/eunix.go000066400000000000000000000001161415471104000172720ustar00rootroot00000000000000// Package eunix provides extra UNIX-specific system utilities. package eunix elvish-0.17.0/pkg/sys/eunix/tc.go000066400000000000000000000004111415471104000165460ustar00rootroot00000000000000//go:build !windows && !plan9 // +build !windows,!plan9 package eunix import ( "golang.org/x/sys/unix" ) // Tcsetpgrp sets the terminal foreground process group. func Tcsetpgrp(fd int, pid int) error { return unix.IoctlSetPointerInt(fd, unix.TIOCSPGRP, pid) } elvish-0.17.0/pkg/sys/eunix/termios.go000066400000000000000000000032741415471104000176340ustar00rootroot00000000000000//go:build !windows && !plan9 // +build !windows,!plan9 // Copyright 2015 go-termios Author. All Rights Reserved. // https://github.com/go-termios/termios // Author: John Lenton package eunix import ( "unsafe" "golang.org/x/sys/unix" ) // Termios represents terminal attributes. type Termios unix.Termios // TermiosForFd returns a pointer to a Termios structure if the file // descriptor is open on a terminal device. func TermiosForFd(fd int) (*Termios, error) { term, err := unix.IoctlGetTermios(fd, getAttrIOCTL) return (*Termios)(term), err } // ApplyToFd applies term to the given file descriptor. func (term *Termios) ApplyToFd(fd int) error { return unix.IoctlSetTermios(fd, setAttrNowIOCTL, (*unix.Termios)(unsafe.Pointer(term))) } // Copy returns a copy of term. func (term *Termios) Copy() *Termios { v := *term return &v } // SetVTime sets the timeout in deciseconds for noncanonical read. func (term *Termios) SetVTime(v uint8) { term.Cc[unix.VTIME] = v } // SetVMin sets the minimal number of characters for noncanonical read. func (term *Termios) SetVMin(v uint8) { term.Cc[unix.VMIN] = v } // SetICanon sets the canonical flag. func (term *Termios) SetICanon(v bool) { setFlag(&term.Lflag, unix.ICANON, v) } // SetIExten sets the iexten flag. func (term *Termios) SetIExten(v bool) { setFlag(&term.Lflag, unix.IEXTEN, v) } // SetEcho sets the echo flag. func (term *Termios) SetEcho(v bool) { setFlag(&term.Lflag, unix.ECHO, v) } // SetICRNL sets the CRNL iflag bit func (term *Termios) SetICRNL(v bool) { setFlag(&term.Iflag, unix.ICRNL, v) } func setFlag(flag *termiosFlag, mask termiosFlag, v bool) { if v { *flag |= mask } else { *flag &= ^mask } } elvish-0.17.0/pkg/sys/eunix/termios_32bitflag.go000066400000000000000000000001631415471104000214630ustar00rootroot00000000000000//go:build !(darwin && (amd64 || arm64)) // +build !darwin !amd64,!arm64 package eunix type termiosFlag = uint32 elvish-0.17.0/pkg/sys/eunix/termios_64bitflag.go000066400000000000000000000004261415471104000214720ustar00rootroot00000000000000//go:build darwin && (amd64 || arm64) // +build darwin // +build amd64 arm64 package eunix // Only Darwin uses 64-bit flags in Termios on 64-bit architectures. See: // https://cs.opensource.google/search?q=%5BIOCL%5Dflag.*uint64&sq=&ss=go%2Fx%2Fsys type termiosFlag = uint64 elvish-0.17.0/pkg/sys/eunix/termios_bsd.go000066400000000000000000000007551415471104000204650ustar00rootroot00000000000000//go:build darwin || dragonfly || freebsd || netbsd || openbsd // +build darwin dragonfly freebsd netbsd openbsd // Copyright 2015 go-termios Author. All Rights Reserved. // https://github.com/go-termios/termios // Author: John Lenton package eunix import "golang.org/x/sys/unix" const ( getAttrIOCTL = unix.TIOCGETA setAttrNowIOCTL = unix.TIOCSETA setAttrDrainIOCTL = unix.TIOCSETAW setAttrFlushIOCTL = unix.TIOCSETAF flushIOCTL = unix.TIOCFLUSH ) elvish-0.17.0/pkg/sys/eunix/termios_notbsd.go000066400000000000000000000006451415471104000212040ustar00rootroot00000000000000//go:build linux || solaris // +build linux solaris // Copyright 2015 go-termios Author. All Rights Reserved. // https://github.com/go-termios/termios // Author: John Lenton package eunix import "golang.org/x/sys/unix" const ( getAttrIOCTL = unix.TCGETS setAttrNowIOCTL = unix.TCSETS setAttrDrainIOCTL = unix.TCSETSW setAttrFlushIOCTL = unix.TCSETSF flushIOCTL = unix.TCFLSH ) elvish-0.17.0/pkg/sys/eunix/waitforread.go000066400000000000000000000016321415471104000204550ustar00rootroot00000000000000//go:build !windows && !plan9 // +build !windows,!plan9 package eunix import ( "os" "time" "golang.org/x/sys/unix" ) // WaitForRead blocks until any of the given files is ready to be read or // timeout. A negative timeout means no timeout. It returns a boolean array // indicating which files are ready to be read and any possible error. func WaitForRead(timeout time.Duration, files ...*os.File) (ready []bool, err error) { maxfd := 0 fdset := &unix.FdSet{} for _, file := range files { fd := int(file.Fd()) if maxfd < fd { maxfd = fd } fdset.Set(fd) } _, err = unix.Select(maxfd+1, fdset, nil, nil, optionalTimeval(timeout)) ready = make([]bool, len(files)) for i, file := range files { ready[i] = fdset.IsSet(int(file.Fd())) } return ready, err } func optionalTimeval(d time.Duration) *unix.Timeval { if d < 0 { return nil } timeval := unix.NsecToTimeval(int64(d)) return &timeval } elvish-0.17.0/pkg/sys/eunix/waitforread_test.go000066400000000000000000000010651415471104000215140ustar00rootroot00000000000000//go:build !windows && !plan9 // +build !windows,!plan9 package eunix import ( "io" "testing" "src.elv.sh/pkg/testutil" ) func TestWaitForRead(t *testing.T) { r0, w0 := testutil.MustPipe() r1, w1 := testutil.MustPipe() defer closeAll(r0, w0, r1, w1) w0.WriteString("x") ready, err := WaitForRead(-1, r0, r1) if err != nil { t.Error("WaitForRead errors:", err) } if !ready[0] { t.Error("Want ready[0]") } if ready[1] { t.Error("Don't want ready[1]") } } func closeAll(files ...io.Closer) { for _, file := range files { file.Close() } } elvish-0.17.0/pkg/sys/ewindows/000077500000000000000000000000001415471104000163245ustar00rootroot00000000000000elvish-0.17.0/pkg/sys/ewindows/console.go000066400000000000000000000032321415471104000203150ustar00rootroot00000000000000//go:build windows // +build windows package ewindows import ( "unsafe" "golang.org/x/sys/windows" ) // https://docs.microsoft.com/en-us/windows/console/readconsoleinput // // BOOL WINAPI ReadConsoleInput( // _In_ HANDLE hConsoleInput, // _Out_ PINPUT_RECORD lpBuffer, // _In_ DWORD nLength, // _Out_ LPDWORD lpNumberOfEventsRead // ); var readConsoleInput = kernel32.NewProc("ReadConsoleInputW") // ReadConsoleInput input wraps the homonymous Windows API call. func ReadConsoleInput(h windows.Handle, buf []InputRecord) (int, error) { var nr uintptr r, _, err := readConsoleInput.Call(uintptr(h), uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)), uintptr(unsafe.Pointer(&nr))) if r != 0 { err = nil } return int(nr), err } // InputEvent is either a KeyEvent, MouseEvent, WindowBufferSizeEvent, // MenuEvent or FocusEvent. type InputEvent interface { isInputEvent() } func (*KeyEvent) isInputEvent() {} func (*MouseEvent) isInputEvent() {} func (*WindowBufferSizeEvent) isInputEvent() {} func (*MenuEvent) isInputEvent() {} func (*FocusEvent) isInputEvent() {} // GetEvent converts InputRecord to InputEvent. func (input *InputRecord) GetEvent() InputEvent { switch input.EventType { case KEY_EVENT: return (*KeyEvent)(unsafe.Pointer(&input.Event)) case MOUSE_EVENT: return (*MouseEvent)(unsafe.Pointer(&input.Event)) case WINDOW_BUFFER_SIZE_EVENT: return (*WindowBufferSizeEvent)(unsafe.Pointer(&input.Event)) case MENU_EVENT: return (*MenuEvent)(unsafe.Pointer(&input.Event)) case FOCUS_EVENT: return (*FocusEvent)(unsafe.Pointer(&input.Event)) default: return nil } } elvish-0.17.0/pkg/sys/ewindows/ewindows.go000066400000000000000000000004761415471104000205210ustar00rootroot00000000000000//go:generate cmd /c go tool cgo -godefs types.go > ztypes_windows.go && gofmt -w ztypes_windows.go //go:build windows // +build windows // Package ewindows provides extra Windows-specific system utilities. package ewindows import "golang.org/x/sys/windows" var kernel32 = windows.NewLazySystemDLL("kernel32.dll") elvish-0.17.0/pkg/sys/ewindows/types.go000066400000000000000000000011371415471104000200210ustar00rootroot00000000000000//go:build ignore // +build ignore package ewindows /* #include */ import "C" type ( Coord C.COORD InputRecord C.INPUT_RECORD KeyEvent C.KEY_EVENT_RECORD MouseEvent C.MOUSE_EVENT_RECORD WindowBufferSizeEvent C.WINDOW_BUFFER_SIZE_RECORD MenuEvent C.MENU_EVENT_RECORD FocusEvent C.FOCUS_EVENT_RECORD ) const ( KEY_EVENT = C.KEY_EVENT MOUSE_EVENT = C.MOUSE_EVENT WINDOW_BUFFER_SIZE_EVENT = C.WINDOW_BUFFER_SIZE_EVENT MENU_EVENT = C.MENU_EVENT FOCUS_EVENT = C.FOCUS_EVENT ) elvish-0.17.0/pkg/sys/ewindows/types_src_windows.go000066400000000000000000000011371415471104000224420ustar00rootroot00000000000000//go:build ignore // +build ignore package ewindows /* #include */ import "C" type ( Coord C.COORD InputRecord C.INPUT_RECORD KeyEvent C.KEY_EVENT_RECORD MouseEvent C.MOUSE_EVENT_RECORD WindowBufferSizeEvent C.WINDOW_BUFFER_SIZE_RECORD MenuEvent C.MENU_EVENT_RECORD FocusEvent C.FOCUS_EVENT_RECORD ) const ( KEY_EVENT = C.KEY_EVENT MOUSE_EVENT = C.MOUSE_EVENT WINDOW_BUFFER_SIZE_EVENT = C.WINDOW_BUFFER_SIZE_EVENT MENU_EVENT = C.MENU_EVENT FOCUS_EVENT = C.FOCUS_EVENT ) elvish-0.17.0/pkg/sys/ewindows/wait.go000066400000000000000000000025511415471104000176220ustar00rootroot00000000000000//go:build windows // +build windows package ewindows import ( "errors" "unsafe" "golang.org/x/sys/windows" ) const ( INFINITE = 0xFFFFFFFF ) const ( WAIT_OBJECT_0 = 0 WAIT_ABANDONED_0 = 0x00000080 WAIT_TIMEOUT = 0x00000102 WAIT_FAILED = 0xFFFFFFFF ) var ( waitForMultipleObjects = kernel32.NewProc("WaitForMultipleObjects") errTimeout = errors.New("WaitForMultipleObjects timeout") ) // WaitForMultipleObjects blocks until any of the objects is triggered or // timeout. // // DWORD WINAPI WaitForMultipleObjects( // _In_ DWORD nCount, // _In_ const HANDLE *lpHandles, // _In_ BOOL bWaitAll, // _In_ DWORD dwMilliseconds // ); func WaitForMultipleObjects(handles []windows.Handle, waitAll bool, timeout uint32) (trigger int, abandoned bool, err error) { count := uintptr(len(handles)) ret, _, err := waitForMultipleObjects.Call(count, uintptr(unsafe.Pointer(&handles[0])), boolToUintptr(waitAll), uintptr(timeout)) switch { case WAIT_OBJECT_0 <= ret && ret < WAIT_OBJECT_0+count: return int(ret - WAIT_OBJECT_0), false, nil case WAIT_ABANDONED_0 <= ret && ret < WAIT_ABANDONED_0+count: return int(ret - WAIT_ABANDONED_0), true, nil case ret == WAIT_TIMEOUT: return -1, false, errTimeout default: return -1, false, err } } func boolToUintptr(b bool) uintptr { if b { return 1 } return 0 } elvish-0.17.0/pkg/sys/ewindows/ztypes_windows.go000066400000000000000000000016001415471104000217600ustar00rootroot00000000000000// Code generated by cmd/cgo -godefs; DO NOT EDIT. // cgo.exe -godefs C:\Users\xiaq\on\elvish\pkg\sys\ewindows\types.go package ewindows type ( Coord struct { X int16 Y int16 } InputRecord struct { EventType uint16 Pad_cgo_0 [2]byte Event [16]byte } KeyEvent struct { BKeyDown int32 WRepeatCount uint16 WVirtualKeyCode uint16 WVirtualScanCode uint16 UChar [2]byte DwControlKeyState uint32 } MouseEvent struct { DwMousePosition Coord DwButtonState uint32 DwControlKeyState uint32 DwEventFlags uint32 } WindowBufferSizeEvent struct { DwSize Coord } MenuEvent struct { DwCommandId uint32 } FocusEvent struct { BSetFocus int32 } ) const ( KEY_EVENT = 0x1 MOUSE_EVENT = 0x2 WINDOW_BUFFER_SIZE_EVENT = 0x4 MENU_EVENT = 0x8 FOCUS_EVENT = 0x10 ) elvish-0.17.0/pkg/sys/signal_nonunix.go000066400000000000000000000004541415471104000200520ustar00rootroot00000000000000//go:build windows || plan9 || js // +build windows plan9 js package sys import ( "os" "os/signal" ) func notifySignals() chan os.Signal { // This catches every signal regardless of whether it is ignored. sigCh := make(chan os.Signal, sigsChanBufferSize) signal.Notify(sigCh) return sigCh } elvish-0.17.0/pkg/sys/signal_unix.go000066400000000000000000000013041415471104000173320ustar00rootroot00000000000000//go:build !windows && !plan9 && !js // +build !windows,!plan9,!js package sys import ( "os" "os/signal" "syscall" ) func notifySignals() chan os.Signal { // This catches every signal regardless of whether it is ignored. sigCh := make(chan os.Signal, sigsChanBufferSize) signal.Notify(sigCh) // Calling signal.Notify will reset the signal ignore status, so we need to // call signal.Ignore every time we call signal.Notify. // // TODO: Remove this if, and when, job control is implemented. This // handles the case of running an external command from an interactive // prompt. // // See https://b.elv.sh/988. signal.Ignore(syscall.SIGTTIN, syscall.SIGTTOU, syscall.SIGTSTP) return sigCh } elvish-0.17.0/pkg/sys/sys.go000066400000000000000000000013671415471104000156410ustar00rootroot00000000000000// Package sys provide system utilities with the same API across OSes. // // The subpackages eunix and ewindows provide OS-specific utilities. package sys import ( "os" "github.com/mattn/go-isatty" ) const sigsChanBufferSize = 256 // NotifySignals returns a channel on which all signals gets delivered. func NotifySignals() chan os.Signal { return notifySignals() } // SIGWINCH is the window size change signal. const SIGWINCH = sigWINCH // Winsize queries the size of the terminal referenced by the given file. func WinSize(file *os.File) (row, col int) { return winSize(file) } // IsATTY determines whether the given file is a terminal. func IsATTY(file *os.File) bool { return isatty.IsTerminal(file.Fd()) || isatty.IsCygwinTerminal(file.Fd()) } elvish-0.17.0/pkg/sys/winsize_unix.go000066400000000000000000000013121415471104000175440ustar00rootroot00000000000000//go:build !windows && !plan9 // +build !windows,!plan9 // Copyright 2015 go-termios Author. All Rights Reserved. // https://github.com/go-termios/termios // Author: John Lenton package sys import ( "fmt" "os" "golang.org/x/sys/unix" ) const sigWINCH = unix.SIGWINCH func winSize(file *os.File) (row, col int) { fd := int(file.Fd()) ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ) if err != nil { fmt.Printf("error in winSize: %v", err) return -1, -1 } // Pick up a reasonable value for row and col // if they equal zero in special case, // e.g. serial console if ws.Col == 0 { ws.Col = 80 } if ws.Row == 0 { ws.Row = 24 } return int(ws.Row), int(ws.Col) } elvish-0.17.0/pkg/sys/winsize_windows.go000066400000000000000000000010021415471104000202470ustar00rootroot00000000000000package sys import ( "fmt" "os" "syscall" "golang.org/x/sys/windows" ) // Windows doesn't have SIGCH, so use an impossible value. const sigWINCH = syscall.Signal(-1) func winSize(file *os.File) (row, col int) { var info windows.ConsoleScreenBufferInfo err := windows.GetConsoleScreenBufferInfo(windows.Handle(file.Fd()), &info) if err != nil { fmt.Printf("error in winSize: %v", err) return -1, -1 } window := info.Window return int(window.Bottom - window.Top), int(window.Right - window.Left) } elvish-0.17.0/pkg/testutil/000077500000000000000000000000001415471104000155245ustar00rootroot00000000000000elvish-0.17.0/pkg/testutil/must.go000066400000000000000000000032531415471104000170460ustar00rootroot00000000000000package testutil import ( "io" "os" "path/filepath" ) // MustPipe calls os.Pipe. It panics if an error occurs. func MustPipe() (*os.File, *os.File) { r, w, err := os.Pipe() Must(err) return r, w } // MustReadAllAndClose reads all bytes and closes the ReadCloser. It panics if // an error occurs. func MustReadAllAndClose(r io.ReadCloser) []byte { bs, err := io.ReadAll(r) Must(err) Must(r.Close()) return bs } // MustMkdirAll calls os.MkdirAll for each argument. It panics if an error // occurs. func MustMkdirAll(names ...string) { for _, name := range names { Must(os.MkdirAll(name, 0700)) } } // MustCreateEmpty creates empty file, after creating all ancestor directories // that don't exist. It panics if an error occurs. func MustCreateEmpty(names ...string) { for _, name := range names { Must(os.MkdirAll(filepath.Dir(name), 0700)) file, err := os.Create(name) Must(err) Must(file.Close()) } } // MustWriteFile writes data to a file, after creating all ancestor directories // that don't exist. It panics if an error occurs. func MustWriteFile(filename, data string) { Must(os.MkdirAll(filepath.Dir(filename), 0700)) Must(os.WriteFile(filename, []byte(data), 0600)) } // MustChdir calls os.Chdir and panics if it fails. func MustChdir(dir string) { Must(os.Chdir(dir)) } // Must panics if the error value is not nil. It is typically used like this: // // testutil.Must(someFunction(...)) // // Where someFunction returns a single error value. This is useful with // functions like os.Mkdir to succinctly ensure the test fails to proceed if an // operation required for the test setup results in an error. func Must(err error) { if err != nil { panic(err) } } elvish-0.17.0/pkg/testutil/recover.go000066400000000000000000000001541415471104000175200ustar00rootroot00000000000000package testutil func Recover(f func()) (r interface{}) { defer func() { r = recover() }() f() return } elvish-0.17.0/pkg/testutil/recover_test.go000066400000000000000000000003571415471104000205640ustar00rootroot00000000000000package testutil import ( "testing" . "src.elv.sh/pkg/tt" ) func TestRecover(t *testing.T) { Test(t, Fn("Recover", Recover), Table{ Args(func() {}).Rets(nil), Args(func() { panic("unreachable") }).Rets("unreachable"), }) } elvish-0.17.0/pkg/testutil/scaled.go000066400000000000000000000010541415471104000173060ustar00rootroot00000000000000package testutil import ( "os" "strconv" "time" "src.elv.sh/pkg/env" ) // Scaled returns d scaled by $E:ELVISH_TEST_TIME_SCALE. If the environment // variable does not exist or contains an invalid value, the scale defaults to // 1. func Scaled(d time.Duration) time.Duration { return time.Duration(float64(d) * getTestTimeScale()) } func getTestTimeScale() float64 { env := os.Getenv(env.ELVISH_TEST_TIME_SCALE) if env == "" { return 1 } scale, err := strconv.ParseFloat(env, 64) if err != nil || scale <= 0 { return 1 } return scale } elvish-0.17.0/pkg/testutil/scaled_test.go000066400000000000000000000020261415471104000203450ustar00rootroot00000000000000package testutil import ( "os" "testing" "time" "src.elv.sh/pkg/env" ) var scaledMsTests = []struct { name string env string d time.Duration want time.Duration }{ {"default 10ms", "", 10 * time.Millisecond, 10 * time.Millisecond}, {"2x 10ms", "2", 10 * time.Millisecond, 20 * time.Millisecond}, {"2x 3s", "2", 3 * time.Second, 6 * time.Second}, {"0.5x 10ms", "0.5", 10 * time.Millisecond, 5 * time.Millisecond}, {"invalid treated as 1", "a", 10 * time.Millisecond, 10 * time.Millisecond}, {"0 treated as 1", "0", 10 * time.Millisecond, 10 * time.Millisecond}, {"negative treated as 1", "-1", 10 * time.Millisecond, 10 * time.Millisecond}, } func TestScaled(t *testing.T) { envSave := os.Getenv(env.ELVISH_TEST_TIME_SCALE) defer os.Setenv(env.ELVISH_TEST_TIME_SCALE, envSave) for _, test := range scaledMsTests { t.Run(test.name, func(t *testing.T) { os.Setenv(env.ELVISH_TEST_TIME_SCALE, test.env) got := Scaled(test.d) if got != test.want { t.Errorf("got %v, want %v", got, test.want) } }) } } elvish-0.17.0/pkg/testutil/temp_env.go000066400000000000000000000013171415471104000176720ustar00rootroot00000000000000package testutil import "os" // Setenv sets the value of an environment variable for the duration of a test. // It returns value. func Setenv(c Cleanuper, name, value string) string { SaveEnv(c, name) os.Setenv(name, value) return value } // Setenv unsets an environment variable for the duration of a test. func Unsetenv(c Cleanuper, name string) { SaveEnv(c, name) os.Unsetenv(name) } // SaveEnv saves the current value of an environment variable so that it will be // restored after a test has finished. func SaveEnv(c Cleanuper, name string) { oldValue, existed := os.LookupEnv(name) if existed { c.Cleanup(func() { os.Setenv(name, oldValue) }) } else { c.Cleanup(func() { os.Unsetenv(name) }) } } elvish-0.17.0/pkg/testutil/temp_env_test.go000066400000000000000000000030131415471104000207240ustar00rootroot00000000000000package testutil import ( "os" "testing" ) const envName = "ELVISH_TEST_ENV" func TestSetenv_ExistingEnv(t *testing.T) { os.Setenv(envName, "old value") defer os.Unsetenv(envName) c := &cleanuper{} v := Setenv(c, envName, "new value") if v != "new value" { t.Errorf("did not return new value") } if os.Getenv(envName) != "new value" { t.Errorf("did not set to new value") } c.runCleanups() if os.Getenv(envName) != "old value" { t.Errorf("did not restore to old value") } } func TestSetenv_NewEnv(t *testing.T) { os.Unsetenv(envName) c := &cleanuper{} v := Setenv(c, envName, "new value") if v != "new value" { t.Errorf("did not return new value") } if os.Getenv(envName) != "new value" { t.Errorf("did not set to new value") } c.runCleanups() if _, exists := os.LookupEnv(envName); exists { t.Errorf("did not remove") } } func TestUnsetenv_ExistingEnv(t *testing.T) { os.Setenv(envName, "old value") defer os.Unsetenv(envName) c := &cleanuper{} Unsetenv(c, envName) if _, exists := os.LookupEnv(envName); exists { t.Errorf("did not unset") } c.runCleanups() if os.Getenv(envName) != "old value" { t.Errorf("did not restore to old value") } } func TestUnsetenv_NewEnv(t *testing.T) { os.Unsetenv(envName) c := &cleanuper{} Unsetenv(c, envName) if _, exists := os.LookupEnv(envName); exists { t.Errorf("did not unset") } c.runCleanups() if _, exists := os.LookupEnv(envName); exists { t.Errorf("did not remove") } } // SaveEnv tested as a dependency of Setenv and Unsetenv elvish-0.17.0/pkg/testutil/testdir.go000066400000000000000000000045351415471104000175400ustar00rootroot00000000000000package testutil import ( "fmt" "os" "path/filepath" "src.elv.sh/pkg/env" ) // TempDir creates a temporary directory for testing that will be removed // after the test finishes. It is different from testing.TB.TempDir in that it // resolves symlinks in the path of the directory. // // It panics if the test directory cannot be created or symlinks cannot be // resolved. It is only suitable for use in tests. func TempDir(c Cleanuper) string { dir, err := os.MkdirTemp("", "elvishtest.") if err != nil { panic(err) } dir, err = filepath.EvalSymlinks(dir) if err != nil { panic(err) } c.Cleanup(func() { err := os.RemoveAll(dir) if err != nil { fmt.Fprintf(os.Stderr, "failed to remove temp dir %s: %v\n", dir, err) } }) return dir } // TempHome is equivalent to Setenv(c, env.HOME, TempDir(c)) func TempHome(c Cleanuper) string { return Setenv(c, env.HOME, TempDir(c)) } // Chdir changes into a directory, and restores the original working directory // when a test finishes. It returns the directory for easier chaining. func Chdir(c Cleanuper, dir string) string { oldWd, err := os.Getwd() if err != nil { panic(err) } Must(os.Chdir(dir)) c.Cleanup(func() { Must(os.Chdir(oldWd)) }) return dir } // InTempDir is equivalent to Chdir(c, TempDir(c)). func InTempDir(c Cleanuper) string { return Chdir(c, TempDir(c)) } // InTempHome is equivalent to Setenv(c, env.HOME, InTempDir(c)) func InTempHome(c Cleanuper) string { return Setenv(c, env.HOME, InTempDir(c)) } // Dir describes the layout of a directory. The keys of the map represent // filenames. Each value is either a string (for the content of a regular file // with permission 0644), a File, or a Dir. type Dir map[string]interface{} // File describes a file to create. type File struct { Perm os.FileMode Content string } // ApplyDir creates the given filesystem layout in the current directory. func ApplyDir(dir Dir) { applyDir(dir, "") } func applyDir(dir Dir, prefix string) { for name, file := range dir { path := filepath.Join(prefix, name) switch file := file.(type) { case string: Must(os.WriteFile(path, []byte(file), 0644)) case File: Must(os.WriteFile(path, []byte(file.Content), file.Perm)) case Dir: Must(os.MkdirAll(path, 0755)) applyDir(file, path) default: panic(fmt.Sprintf("file is neither string, Dir, or Symlink: %v", file)) } } } elvish-0.17.0/pkg/testutil/testdir_nonwindows_test.go000066400000000000000000000014451415471104000230610ustar00rootroot00000000000000//go:build !windows // +build !windows package testutil import ( "os" "testing" ) func TestApplyDir_CreatesFileWithPerm(t *testing.T) { InTempDir(t) ApplyDir(Dir{ // For some unknown reason, termux on Android does not set the // group and other permission bits correctly, so we use 700 here. "a": File{0700, "a content"}, }) testFileContent(t, "a", "a content") testFilePerm(t, "a", 0700) } func testFilePerm(t *testing.T, filename string, wantPerm os.FileMode) { t.Helper() info, err := os.Stat(filename) if err != nil { t.Errorf("Could not stat %v: %v", filename, err) return } if perm := info.Mode().Perm(); perm != wantPerm { t.Errorf("File %v has perm %o, want %o", filename, perm, wantPerm) wd, err := os.Getwd() if err == nil { t.Logf("pwd is %v", wd) } } } elvish-0.17.0/pkg/testutil/testdir_test.go000066400000000000000000000045651415471104000206020ustar00rootroot00000000000000package testutil import ( "os" "path/filepath" "testing" ) func TestTempDir_DirIsValid(t *testing.T) { dir := TempDir(t) stat, err := os.Stat(dir) if err != nil { t.Errorf("TestDir returns %q which cannot be stated", dir) } if !stat.IsDir() { t.Errorf("TestDir returns %q which is not a dir", dir) } } func TestTempDir_DirHasSymlinksResolved(t *testing.T) { dir := TempDir(t) resolved, err := filepath.EvalSymlinks(dir) if err != nil { panic(err) } if dir != resolved { t.Errorf("TestDir returns %q, but it resolves to %q", dir, resolved) } } func TestTempDir_CleanupRemovesDirRecursively(t *testing.T) { c := &cleanuper{} dir := TempDir(c) err := os.WriteFile(filepath.Join(dir, "a"), []byte("test"), 0600) if err != nil { panic(err) } c.runCleanups() if _, err := os.Stat(dir); err == nil { t.Errorf("Dir %q still exists after cleanup", dir) } } func TestChdir(t *testing.T) { dir := TempDir(t) original := getWd() c := &cleanuper{} Chdir(c, dir) after := getWd() if after != dir { t.Errorf("pwd is now %q, want %q", after, dir) } c.runCleanups() restored := getWd() if restored != original { t.Errorf("pwd restored to %q, want %q", restored, original) } } func TestApplyDir_CreatesFiles(t *testing.T) { InTempDir(t) ApplyDir(Dir{ "a": "a content", "b": "b content", }) testFileContent(t, "a", "a content") testFileContent(t, "b", "b content") } func TestApplyDir_CreatesDirectories(t *testing.T) { InTempDir(t) ApplyDir(Dir{ "d": Dir{ "d1": "d1 content", "d2": "d2 content", "dd": Dir{ "dd1": "dd1 content", }, }, }) testFileContent(t, "d/d1", "d1 content") testFileContent(t, "d/d2", "d2 content") testFileContent(t, "d/dd/dd1", "dd1 content") } func TestApplyDir_AllowsExistingDirectories(t *testing.T) { InTempDir(t) ApplyDir(Dir{"d": Dir{}}) ApplyDir(Dir{"d": Dir{"a": "content"}}) testFileContent(t, "d/a", "content") } func getWd() string { dir, err := os.Getwd() if err != nil { panic(err) } dir, err = filepath.EvalSymlinks(dir) if err != nil { panic(err) } return dir } func testFileContent(t *testing.T, filename string, wantContent string) { t.Helper() content, err := os.ReadFile(filename) if err != nil { t.Errorf("Could not read %v: %v", filename, err) return } if string(content) != wantContent { t.Errorf("File %v is %q, want %q", filename, content, wantContent) } } elvish-0.17.0/pkg/testutil/testutil.go000066400000000000000000000003511415471104000177270ustar00rootroot00000000000000// Package testutil contains common test utilities. package testutil // Cleanuper wraps the Cleanup method. It is a subset of testing.TB, thus // satisfied by *testing.T and *testing.B. type Cleanuper interface { Cleanup(func()) } elvish-0.17.0/pkg/testutil/testutil_test.go000066400000000000000000000003351415471104000207700ustar00rootroot00000000000000package testutil type cleanuper struct{ fns []func() } func (c *cleanuper) Cleanup(fn func()) { c.fns = append(c.fns, fn) } func (c *cleanuper) runCleanups() { for i := len(c.fns) - 1; i >= 0; i-- { c.fns[i]() } } elvish-0.17.0/pkg/testutil/umask.go000066400000000000000000000002741415471104000171760ustar00rootroot00000000000000package testutil // Umask sets the umask for the duration of the test, and restores it afterwards. func Umask(c Cleanuper, m int) { save := umask(m) c.Cleanup(func() { umask(save) }) } elvish-0.17.0/pkg/testutil/umask_unix.go000066400000000000000000000002151415471104000202340ustar00rootroot00000000000000//go:build !windows && !plan9 && !js // +build !windows,!plan9,!js package testutil import "golang.org/x/sys/unix" var umask = unix.Umask elvish-0.17.0/pkg/testutil/umask_windows.go000066400000000000000000000000631415471104000207440ustar00rootroot00000000000000package testutil func umask(int) int { return 0 } elvish-0.17.0/pkg/tt/000077500000000000000000000000001415471104000142765ustar00rootroot00000000000000elvish-0.17.0/pkg/tt/tt.go000066400000000000000000000110541415471104000152550ustar00rootroot00000000000000// Package tt supports table-driven tests with little boilerplate. // // See the test case for this package for example usage. package tt import ( "bytes" "fmt" "reflect" ) // Table represents a test table. type Table []*Case // Case represents a test case. It is created by the C function, and offers // setters that augment and return itself; those calls can be chained like // C(...).Rets(...). type Case struct { args []interface{} retsMatchers [][]interface{} } // Args returns a new Case with the given arguments. func Args(args ...interface{}) *Case { return &Case{args: args} } // Rets modifies the test case so that it requires the return values to match // the given values. It returns the receiver. The arguments may implement the // Matcher interface, in which case its Match method is called with the actual // return value. Otherwise, reflect.DeepEqual is used to determine matches. func (c *Case) Rets(matchers ...interface{}) *Case { c.retsMatchers = append(c.retsMatchers, matchers) return c } // FnToTest describes a function to test. type FnToTest struct { name string body interface{} argsFmt string retsFmt string } // Fn makes a new FnToTest with the given function name and body. func Fn(name string, body interface{}) *FnToTest { return &FnToTest{name: name, body: body} } // ArgsFmt sets the string for formatting arguments in test error messages, and // return fn itself. func (fn *FnToTest) ArgsFmt(s string) *FnToTest { fn.argsFmt = s return fn } // RetsFmt sets the string for formatting return values in test error messages, // and return fn itself. func (fn *FnToTest) RetsFmt(s string) *FnToTest { fn.retsFmt = s return fn } // T is the interface for accessing testing.T. type T interface { Helper() Errorf(format string, args ...interface{}) } // Test tests a function against test cases. func Test(t T, fn *FnToTest, tests Table) { t.Helper() for _, test := range tests { rets := call(fn.body, test.args) for _, retsMatcher := range test.retsMatchers { if !match(retsMatcher, rets) { var argsString, retsString, wantRetsString string if fn.argsFmt == "" { argsString = sprintArgs(test.args...) } else { argsString = fmt.Sprintf(fn.argsFmt, test.args...) } if fn.retsFmt == "" { retsString = sprintRets(rets...) wantRetsString = sprintRets(retsMatcher...) } else { retsString = fmt.Sprintf(fn.retsFmt, rets...) wantRetsString = fmt.Sprintf(fn.retsFmt, retsMatcher...) } t.Errorf("%s(%s) -> %s, want %s", fn.name, argsString, retsString, wantRetsString) } } } } // RetValue is an empty interface used in the Matcher interface. type RetValue interface{} // Matcher wraps the Match method. type Matcher interface { // Match reports whether a return value is considered a match. The argument // is of type RetValue so that it cannot be implemented accidentally. Match(RetValue) bool } // Any is a Matcher that matches any value. var Any Matcher = anyMatcher{} type anyMatcher struct{} func (anyMatcher) Match(RetValue) bool { return true } func match(matchers, actual []interface{}) bool { for i, matcher := range matchers { if !matchOne(matcher, actual[i]) { return false } } return true } func matchOne(m, a interface{}) bool { if m, ok := m.(Matcher); ok { return m.Match(a) } return reflect.DeepEqual(m, a) } func sprintArgs(args ...interface{}) string { return sprintCommaDelimited(args...) } func sprintRets(rets ...interface{}) string { if len(rets) == 1 { return fmt.Sprint(rets[0]) } return "(" + sprintCommaDelimited(rets...) + ")" } func sprintCommaDelimited(args ...interface{}) string { var b bytes.Buffer for i, arg := range args { if i > 0 { b.WriteString(", ") } fmt.Fprint(&b, arg) } return b.String() } func call(fn interface{}, args []interface{}) []interface{} { argsReflect := make([]reflect.Value, len(args)) for i, arg := range args { if arg == nil { // reflect.ValueOf(nil) returns a zero Value, but this is not what // we want. Work around this by taking the ValueOf a pointer to nil // and then get the Elem. // TODO(xiaq): This is now always using a nil value with type // interface{}. For more usability, inspect the type of fn to see // which type of nil this argument should be. var v interface{} argsReflect[i] = reflect.ValueOf(&v).Elem() } else { argsReflect[i] = reflect.ValueOf(arg) } } retsReflect := reflect.ValueOf(fn).Call(argsReflect) rets := make([]interface{}, len(retsReflect)) for i, retReflect := range retsReflect { rets[i] = retReflect.Interface() } return rets } elvish-0.17.0/pkg/tt/tt_test.go000066400000000000000000000031521415471104000163140ustar00rootroot00000000000000package tt import ( "fmt" "testing" ) // testT implements the T interface and is used to verify the Test function's // interaction with T. type testT []string func (t *testT) Helper() {} func (t *testT) Errorf(format string, args ...interface{}) { *t = append(*t, fmt.Sprintf(format, args...)) } // Simple functions to test. func add(x, y int) int { return x + y } func addsub(x int, y int) (int, int) { return x + y, x - y } func TestTTPass(t *testing.T) { var testT testT Test(&testT, Fn("addsub", addsub), Table{ Args(1, 10).Rets(11, -9), }) if len(testT) > 0 { t.Errorf("Test errors when test should pass") } } func TestTTFailDefaultFmtOneReturn(t *testing.T) { var testT testT Test(&testT, Fn("add", add), Table{Args(1, 10).Rets(12)}, ) assertOneError(t, testT, "add(1, 10) -> 11, want 12") } func TestTTFailDefaultFmtMultiReturn(t *testing.T) { var testT testT Test(&testT, Fn("addsub", addsub), Table{Args(1, 10).Rets(11, -90)}, ) assertOneError(t, testT, "addsub(1, 10) -> (11, -9), want (11, -90)") } func TestTTFailCustomFmt(t *testing.T) { var testT testT Test(&testT, Fn("addsub", addsub).ArgsFmt("x = %d, y = %d").RetsFmt("(a = %d, b = %d)"), Table{Args(1, 10).Rets(11, -90)}, ) assertOneError(t, testT, "addsub(x = 1, y = 10) -> (a = 11, b = -9), want (a = 11, b = -90)") } func assertOneError(t *testing.T, testT testT, want string) { switch len(testT) { case 0: t.Errorf("Test didn't error when it should") case 1: if testT[0] != want { t.Errorf("Test wrote message %q, want %q", testT[0], want) } default: t.Errorf("Test wrote too many error messages") } } elvish-0.17.0/pkg/ui/000077500000000000000000000000001415471104000142645ustar00rootroot00000000000000elvish-0.17.0/pkg/ui/color.go000066400000000000000000000062521415471104000157360ustar00rootroot00000000000000package ui import ( "fmt" "strconv" "strings" ) // Color represents a color. type Color interface { fgSGR() string bgSGR() string String() string } // Builtin ANSI colors. var ( Black Color = ansiColor(0) Red Color = ansiColor(1) Green Color = ansiColor(2) Yellow Color = ansiColor(3) Blue Color = ansiColor(4) Magenta Color = ansiColor(5) Cyan Color = ansiColor(6) White Color = ansiColor(7) BrightBlack Color = ansiBrightColor(0) BrightRed Color = ansiBrightColor(1) BrightGreen Color = ansiBrightColor(2) BrightYellow Color = ansiBrightColor(3) BrightBlue Color = ansiBrightColor(4) BrightMagenta Color = ansiBrightColor(5) BrightCyan Color = ansiBrightColor(6) BrightWhite Color = ansiBrightColor(7) ) // XTerm256Color returns a color from the xterm 256-color palette. func XTerm256Color(i uint8) Color { return xterm256Color(i) } // TrueColor returns a 24-bit true color. func TrueColor(r, g, b uint8) Color { return trueColor{r, g, b} } var colorNames = []string{ "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", } var colorByName = map[string]Color{ "black": Black, "red": Red, "green": Green, "yellow": Yellow, "blue": Blue, "magenta": Magenta, "cyan": Cyan, "white": White, "bright-black": BrightBlack, "bright-red": BrightRed, "bright-green": BrightGreen, "bright-yellow": BrightYellow, "bright-blue": BrightBlue, "bright-magenta": BrightMagenta, "bright-cyan": BrightCyan, "bright-white": BrightWhite, } type ansiColor uint8 func (c ansiColor) fgSGR() string { return strconv.Itoa(30 + int(c)) } func (c ansiColor) bgSGR() string { return strconv.Itoa(40 + int(c)) } func (c ansiColor) String() string { return colorNames[c] } type ansiBrightColor uint8 func (c ansiBrightColor) fgSGR() string { return strconv.Itoa(90 + int(c)) } func (c ansiBrightColor) bgSGR() string { return strconv.Itoa(100 + int(c)) } func (c ansiBrightColor) String() string { return "bright-" + colorNames[c] } type xterm256Color uint8 func (c xterm256Color) fgSGR() string { return "38;5;" + strconv.Itoa(int(c)) } func (c xterm256Color) bgSGR() string { return "48;5;" + strconv.Itoa(int(c)) } func (c xterm256Color) String() string { return "color" + strconv.Itoa(int(c)) } type trueColor struct{ r, g, b uint8 } func (c trueColor) fgSGR() string { return "38;2;" + c.rgbSGR() } func (c trueColor) bgSGR() string { return "48;2;" + c.rgbSGR() } func (c trueColor) String() string { return fmt.Sprintf("#%02x%02x%02x", c.r, c.g, c.b) } func (c trueColor) rgbSGR() string { return fmt.Sprintf("%d;%d;%d", c.r, c.g, c.b) } func parseColor(name string) Color { if color, ok := colorByName[name]; ok { return color } if strings.HasPrefix(name, "color") { i, err := strconv.Atoi(name[5:]) if err == nil && 0 <= i && i < 256 { return XTerm256Color(uint8(i)) } } else if strings.HasPrefix(name, "#") && len(name) == 7 { r, rErr := strconv.ParseUint(name[1:3], 16, 8) g, gErr := strconv.ParseUint(name[3:5], 16, 8) b, bErr := strconv.ParseUint(name[5:7], 16, 8) if rErr == nil && gErr == nil && bErr == nil { return TrueColor(uint8(r), uint8(g), uint8(b)) } } return nil } elvish-0.17.0/pkg/ui/color_test.go000066400000000000000000000024311415471104000167700ustar00rootroot00000000000000package ui import ( "reflect" "testing" ) func TestColorSGR(t *testing.T) { // Test the SGR sequences of colors indirectly via VTString of Text, since // that is how they are used. testTextVTString(t, []textVTStringTest{ {T("foo", FgRed), "\033[31mfoo\033[m"}, {T("foo", BgRed), "\033[41mfoo\033[m"}, {T("foo", FgBrightRed), "\033[91mfoo\033[m"}, {T("foo", BgBrightRed), "\033[101mfoo\033[m"}, {T("foo", Fg(XTerm256Color(30))), "\033[38;5;30mfoo\033[m"}, {T("foo", Bg(XTerm256Color(30))), "\033[48;5;30mfoo\033[m"}, {T("foo", Fg(TrueColor(30, 40, 50))), "\033[38;2;30;40;50mfoo\033[m"}, {T("foo", Bg(TrueColor(30, 40, 50))), "\033[48;2;30;40;50mfoo\033[m"}, }) } var colorStringTests = []struct { color Color str string }{ {Red, "red"}, {BrightRed, "bright-red"}, {XTerm256Color(30), "color30"}, {TrueColor(0x33, 0x44, 0x55), "#334455"}, } func TestColorString(t *testing.T) { for _, test := range colorStringTests { s := test.color.String() if s != test.str { t.Errorf("%v.String() -> %q, want %q", test.color, s, test.str) } } } func TestParseColor(t *testing.T) { for _, test := range colorStringTests { c := parseColor(test.str) if !reflect.DeepEqual(c, test.color) { t.Errorf("parseError(%q) -> %v, want %v", test.str, c, test.color) } } } elvish-0.17.0/pkg/ui/key.go000066400000000000000000000126151415471104000154100ustar00rootroot00000000000000package ui import ( "bytes" "fmt" "strings" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/persistent/hash" ) // Key represents a single keyboard input, typically assembled from a escape // sequence. type Key struct { Rune rune Mod Mod } // K constructs a new Key. func K(r rune, mods ...Mod) Key { var mod Mod for _, m := range mods { mod |= m } return Key{r, mod} } // Default is used in the key binding table to indicate a default binding. var Default = Key{DefaultBindingRune, 0} // Mod represents a modifier key. type Mod byte // Values for Mod. const ( // Shift is the shift modifier. It is only applied to special keys (e.g. // Shift-F1). For instance 'A' and '@' which are typically entered with the // shift key pressed, are not considered to be shift-modified. Shift Mod = 1 << iota // Alt is the alt modifier, traditionally known as the meta modifier. Alt Ctrl ) const functionKeyOffset = 1000 // Special negative runes to represent function keys, used in the Rune field // of the Key struct. This also has a few function names that are aliases for // simple runes. See keyNames below for mapping these values to strings. const ( // DefaultBindingRune is a special value to represent a default binding. DefaultBindingRune rune = iota - functionKeyOffset F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12 Up Down Right Left Home Insert Delete End PageUp PageDown // Function key names that are aliases for their ASCII representation. Tab = '\t' Enter = '\n' Backspace = 0x7f ) // keyNames maps runes, whether simple or function, to symbolic key names. var keyNames = map[rune]string{ DefaultBindingRune: "Default", F1: "F1", F2: "F2", F3: "F3", F4: "F4", F5: "F5", F6: "F6", F7: "F7", F8: "F8", F9: "F9", F10: "F10", F11: "F11", F12: "F12", Up: "Up", Down: "Down", Right: "Right", Left: "Left", Home: "Home", Insert: "Insert", Delete: "Delete", End: "End", PageUp: "PageUp", PageDown: "PageDown", Tab: "Tab", Enter: "Enter", Backspace: "Backspace", } func (k Key) Kind() string { return "edit:key" } func (k Key) Equal(other interface{}) bool { return k == other } func (k Key) Hash() uint32 { return hash.DJB(uint32(k.Rune), uint32(k.Mod)) } func (k Key) Repr(int) string { return "(edit:key " + parse.Quote(k.String()) + ")" } func (k Key) String() string { var b bytes.Buffer if k.Mod&Ctrl != 0 { b.WriteString("Ctrl-") } if k.Mod&Alt != 0 { b.WriteString("Alt-") } if k.Mod&Shift != 0 { b.WriteString("Shift-") } if name, ok := keyNames[k.Rune]; ok { b.WriteString(name) } else { if k.Rune >= 0 { b.WriteRune(k.Rune) } else { fmt.Fprintf(&b, "(bad function key %d)", k.Rune) } } return b.String() } // modifierByName maps a name to an modifier. It is used for parsing keys where // the modifier string is first turned to lower case, so that all of C, c, // CTRL, Ctrl and ctrl can represent the Ctrl modifier. var modifierByName = map[string]Mod{ "S": Shift, "Shift": Shift, "A": Alt, "Alt": Alt, "M": Alt, "Meta": Alt, "C": Ctrl, "Ctrl": Ctrl, } // ParseKey parses a symbolic key. The syntax is: // // Key = { Mod ('+' | '-') } BareKey // // BareKey = FunctionKeyName | SingleRune func ParseKey(s string) (Key, error) { var k Key // Parse modifiers. for { i := strings.IndexAny(s, "+-") if i == -1 { break } modname := s[:i] if mod, ok := modifierByName[modname]; ok { k.Mod |= mod s = s[i+1:] } else { return Key{}, fmt.Errorf("bad modifier: %s", parse.Quote(modname)) } } if len(s) == 1 { k.Rune = rune(s[0]) if k.Rune < 0x20 { if k.Mod&Ctrl != 0 { //lint:ignore ST1005 We want this error to begin with "Ctrl" rather than "ctrl" // since the user has to use the capitalized form when creating a key binding. return Key{}, fmt.Errorf("Ctrl modifier with literal control char: %q", k.Rune) } // Convert literal control char to the equivalent canonical form; // e.g., "\e" to Ctrl-'[' and "\t" to Ctrl-I. k.Mod |= Ctrl k.Rune += 0x40 } // TODO(xiaq): The following assumptions about keys with Ctrl are not // checked with all terminals. if k.Mod&Ctrl != 0 { // Keys with Ctrl as one of the modifiers and a single ASCII letter // as the base rune do not distinguish between cases. So we // normalize the base rune to upper case. if 'a' <= k.Rune && k.Rune <= 'z' { k.Rune += 'A' - 'a' } // Normalize Ctrl-I to Tab, Ctrl-J to Enter, and Ctrl-? to Backspace. if k.Rune == 'I' { k.Mod &= ^Ctrl k.Rune = Tab } else if k.Rune == 'J' { k.Mod &= ^Ctrl k.Rune = Enter } } return k, nil } // Is this is a symbolic key name, such as `Enter`, we recognize? for r, name := range keyNames { if s == name { k.Rune = r return k, nil } } return Key{}, fmt.Errorf("bad key: %s", parse.Quote(s)) } // Keys implements sort.Interface. type Keys []Key func (ks Keys) Len() int { return len(ks) } func (ks Keys) Swap(i, j int) { ks[i], ks[j] = ks[j], ks[i] } func (ks Keys) Less(i, j int) bool { return ks[i].Mod < ks[j].Mod || (ks[i].Mod == ks[j].Mod && ks[i].Rune < ks[j].Rune) } elvish-0.17.0/pkg/ui/key_test.go000066400000000000000000000057151415471104000164520ustar00rootroot00000000000000package ui import ( "testing" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/persistent/hash" ) var KTests = []struct { k1 Key k2 Key }{ {K('a'), Key{'a', 0}}, {K('a', Alt), Key{'a', Alt}}, {K('a', Alt, Ctrl), Key{'a', Alt | Ctrl}}, } func TestK(t *testing.T) { for _, test := range KTests { if test.k1 != test.k2 { t.Errorf("%v != %v", test.k1, test.k2) } } } func TestKeyAsElvishValue(t *testing.T) { vals.TestValue(t, K('a')). Kind("edit:key"). Hash(hash.DJB('a', 0)). Repr("(edit:key a)"). Equal(K('a')). NotEqual(K('A'), K('a', Alt)) vals.TestValue(t, K('a', Alt)).Repr("(edit:key Alt-a)") vals.TestValue(t, K('a', Ctrl, Alt, Shift)).Repr("(edit:key Ctrl-Alt-Shift-a)") vals.TestValue(t, K('\t')).Repr("(edit:key Tab)") vals.TestValue(t, K(F1)).Repr("(edit:key F1)") vals.TestValue(t, K(-1)).Repr("(edit:key '(bad function key -1)')") vals.TestValue(t, K(-2000)).Repr("(edit:key '(bad function key -2000)')") } var parseKeyTests = []struct { s string wantKey Key wantErr string }{ {s: "x", wantKey: K('x')}, {s: "Tab", wantKey: K(Tab)}, {s: "F1", wantKey: K(F1)}, // Alt- keys are case-sensitive. {s: "A-x", wantKey: Key{'x', Alt}}, {s: "A-X", wantKey: Key{'X', Alt}}, // Ctrl- keys are case-insensitive. {s: "C-x", wantKey: Key{'X', Ctrl}}, {s: "C-X", wantKey: Key{'X', Ctrl}}, // Literal control chars are equivalent to the preferred Ctrl- // formulation. {s: "\033", wantKey: Key{'[', Ctrl}}, // + is the same as -. {s: "C+X", wantKey: Key{'X', Ctrl}}, // Full names and alternative names can also be used. {s: "M-x", wantKey: Key{'x', Alt}}, {s: "Meta-x", wantKey: Key{'x', Alt}}, // Multiple modifiers can appear in any order and with alternative // separator chars. {s: "Alt-Ctrl+Delete", wantKey: Key{Delete, Alt | Ctrl}}, {s: "Ctrl+Alt-Delete", wantKey: Key{Delete, Alt | Ctrl}}, // Confirm alternative symbolic keys are turned into the canonical form. {s: "\t", wantKey: K(Tab)}, // literal tab is normalized to Tab {s: "\n", wantKey: K(Enter)}, // literal newline is normalized to Enter {s: "Ctrl-I", wantKey: K(Tab)}, // Ctrl-I is normalized to Tab {s: "Ctrl-J", wantKey: K(Enter)}, // Ctrl-J is normalized to Enter {s: "Alt-\t", wantKey: Key{Tab, Alt}}, {s: "\x7F", wantKey: K(Backspace)}, // Errors. {s: "F123", wantErr: "bad key: F123"}, {s: "Super-X", wantErr: "bad modifier: Super"}, {s: "a-x", wantErr: "bad modifier: a"}, {s: "Ctrl-\t", wantErr: `Ctrl modifier with literal control char: '\t'`}, } func TestParseKey(t *testing.T) { for _, test := range parseKeyTests { key, err := ParseKey(test.s) if key != test.wantKey { t.Errorf("ParseKey(%q) => %v, want %v", test.s, key, test.wantKey) } if test.wantErr == "" { if err != nil { t.Errorf("ParseKey(%q) => error %v, want nil", test.s, err) } } else { if err == nil || err.Error() != test.wantErr { t.Errorf("ParseKey(%q) => error %v, want error with message %q", test.s, err, test.wantErr) } } } } elvish-0.17.0/pkg/ui/mark_lines.go000066400000000000000000000044651415471104000167500ustar00rootroot00000000000000package ui // RuneStylesheet maps runes to stylings. type RuneStylesheet map[rune]Styling // MarkLines provides a way to construct a styled text by separating the content // and the styling. // // The arguments are groups of either // // * A single string, in which case it represents an unstyled line; // // * Three arguments that can be passed to MarkLine, in which case they are passed // to MarkLine and the return value is used as a styled line. // // Lines represented by all the groups are joined together. // // This function is mainly useful for constructing multi-line Text's with // alignment across those lines. An example: // // var stylesheet = map[rune]string{ // '-': Reverse, // 'x': Stylings(Blue, BgGreen), // } // var text = FromMarkedLines( // "foo bar foobar", stylesheet, // "--- xxx ------" // "lorem ipsum dolar", // ) func MarkLines(args ...interface{}) Text { var text Text for i := 0; i < len(args); i++ { line, ok := args[i].(string) if !ok { // TODO(xiaq): Surface the error. continue } if i+2 < len(args) { if stylesheet, ok := args[i+1].(RuneStylesheet); ok { if style, ok := args[i+2].(string); ok { text = Concat(text, MarkText(line, stylesheet, style)) i += 2 continue } } } text = Concat(text, T(line)) } return text } // MarkText applies styles to all the runes in the line, using the runes in // the style string. The stylesheet argument specifies which style each rune // represents. func MarkText(line string, stylesheet RuneStylesheet, style string) Text { var text Text styleRuns := toRuns(style) for _, styleRun := range styleRuns { i := bytesForFirstNRunes(line, styleRun.n) text = Concat(text, T(line[:i], stylesheet[styleRun.r])) line = line[i:] } if len(line) > 0 { text = Concat(text, T(line)) } return text } type run struct { r rune n int } func toRuns(s string) []run { var runs []run current := run{} for _, r := range s { if r != current.r { if current.n > 0 { runs = append(runs, current) } current = run{r, 1} } else { current.n++ } } if current.n > 0 { runs = append(runs, current) } return runs } func bytesForFirstNRunes(s string, n int) int { k := 0 for i := range s { if k == n { return i } k++ } return len(s) } elvish-0.17.0/pkg/ui/mark_lines_test.go000066400000000000000000000016331415471104000200010ustar00rootroot00000000000000package ui import ( "testing" "src.elv.sh/pkg/tt" ) func TestMarkLines(t *testing.T) { stylesheet := RuneStylesheet{ '-': Inverse, 'x': Stylings(FgBlue, BgGreen), } tt.Test(t, tt.Fn("MarkLines", MarkLines), tt.Table{ tt.Args("foo bar foobar").Rets(T("foo bar foobar")), tt.Args( "foo bar foobar", stylesheet, "--- xxx ------", ).Rets( Concat( T("foo", Inverse), T(" "), T("bar", FgBlue, BgGreen), T(" "), T("foobar", Inverse)), ), tt.Args( "foo bar foobar", stylesheet, "---", ).Rets( Concat( T("foo", Inverse), T(" bar foobar")), ), tt.Args( "plain1", "plain2", "foo bar foobar\n", stylesheet, "--- xxx ------", "plain3", ).Rets( Concat( T("plain1"), T("plain2"), T("foo", Inverse), T(" "), T("bar", FgBlue, BgGreen), T(" "), T("foobar", Inverse), T("\n"), T("plain3")), ), }) } elvish-0.17.0/pkg/ui/parse_sgr.go000066400000000000000000000070561415471104000166100ustar00rootroot00000000000000package ui import ( "strconv" "strings" ) type sgrTokenizer struct { text string styling Styling content string } const sgrPrefix = "\033[" func (st *sgrTokenizer) Next() bool { for strings.HasPrefix(st.text, sgrPrefix) { trimmed := strings.TrimPrefix(st.text, sgrPrefix) // Find the terminator of this sequence. termIndex := strings.IndexFunc(trimmed, func(r rune) bool { return r != ';' && (r < '0' || r > '9') }) if termIndex == -1 { // The string ends with an unterminated escape sequence; ignore // it. st.text = "" return false } term := trimmed[termIndex] sgr := trimmed[:termIndex] st.text = trimmed[termIndex+1:] if term == 'm' { st.styling = StylingFromSGR(sgr) st.content = "" return true } // If the terminator is not 'm'; we have seen a non-SGR escape sequence; // ignore it and continue. } if st.text == "" { return false } // Parse a content segment until the next SGR prefix. content := "" nextSGR := strings.Index(st.text, sgrPrefix) if nextSGR == -1 { content = st.text } else { content = st.text[:nextSGR] } st.text = st.text[len(content):] st.styling = nil st.content = content return true } func (st *sgrTokenizer) Token() (Styling, string) { return st.styling, st.content } // ParseSGREscapedText parses SGR-escaped text into a Text. It also removes // non-SGR CSI sequences sequences in the text. func ParseSGREscapedText(s string) Text { var text Text var style Style tokenizer := sgrTokenizer{text: s} for tokenizer.Next() { styling, content := tokenizer.Token() if styling != nil { styling.transform(&style) } if content != "" { text = append(text, &Segment{style, content}) } } return text } var sgrStyling = map[int]Styling{ 0: Reset, 1: Bold, 2: Dim, 4: Underlined, 5: Blink, 7: Inverse, } // StyleFromSGR builds a Style from an SGR sequence. func StyleFromSGR(s string) Style { var ret Style StylingFromSGR(s).transform(&ret) return ret } // StylingFromSGR builds a Style from an SGR sequence. func StylingFromSGR(s string) Styling { styling := jointStyling{} codes := getSGRCodes(s) if len(codes) == 0 { return Reset } for len(codes) > 0 { code := codes[0] consume := 1 var moreStyling Styling switch { case sgrStyling[code] != nil: moreStyling = sgrStyling[code] case 30 <= code && code <= 37: moreStyling = Fg(ansiColor(code - 30)) case 40 <= code && code <= 47: moreStyling = Bg(ansiColor(code - 40)) case 90 <= code && code <= 97: moreStyling = Fg(ansiBrightColor(code - 90)) case 100 <= code && code <= 107: moreStyling = Bg(ansiBrightColor(code - 100)) case code == 38 && len(codes) >= 3 && codes[1] == 5: moreStyling = Fg(xterm256Color(codes[2])) consume = 3 case code == 48 && len(codes) >= 3 && codes[1] == 5: moreStyling = Bg(xterm256Color(codes[2])) consume = 3 case code == 38 && len(codes) >= 5 && codes[1] == 2: moreStyling = Fg(trueColor{ uint8(codes[2]), uint8(codes[3]), uint8(codes[4])}) consume = 5 case code == 48 && len(codes) >= 5 && codes[1] == 2: moreStyling = Bg(trueColor{ uint8(codes[2]), uint8(codes[3]), uint8(codes[4])}) consume = 5 default: // Do nothing; skip this code } codes = codes[consume:] if moreStyling != nil { styling = append(styling, moreStyling) } } return styling } func getSGRCodes(s string) []int { var codes []int for _, part := range strings.Split(s, ";") { if part == "" { codes = append(codes, 0) } else { code, err := strconv.Atoi(part) if err == nil { codes = append(codes, code) } } } return codes } elvish-0.17.0/pkg/ui/parse_sgr_test.go000066400000000000000000000025561415471104000176470ustar00rootroot00000000000000package ui import ( "testing" "src.elv.sh/pkg/tt" ) func TestParseSGREscapedText(t *testing.T) { tt.Test(t, tt.Fn("ParseSGREscapedText", ParseSGREscapedText), tt.Table{ tt.Args("").Rets(Text(nil)), tt.Args("text").Rets(T("text")), tt.Args("\033[1mbold").Rets(T("bold", Bold)), tt.Args("\033[1mbold\033[31mbold red").Rets( Concat(T("bold", Bold), T("bold red", Bold, FgRed))), tt.Args("\033[1mbold\033[;31mred").Rets( Concat(T("bold", Bold), T("red", FgRed))), // Non-SGR CSI sequences are removed. tt.Args("\033[Atext").Rets(T("text")), // Control characters not part of CSI escape sequences are left // untouched. tt.Args("t\x01ext").Rets(T("t\x01ext")), }) } func TestStyleFromSGR(t *testing.T) { tt.Test(t, tt.Fn("StyleFromSGR", StyleFromSGR), tt.Table{ tt.Args("1").Rets(Style{Bold: true}), // Invalid codes are ignored tt.Args("1;invalid;10000").Rets(Style{Bold: true}), // ANSI colors. tt.Args("31;42").Rets(Style{Foreground: Red, Background: Green}), // ANSI bright colors. tt.Args("91;102"). Rets(Style{Foreground: BrightRed, Background: BrightGreen}), // XTerm 256 color. tt.Args("38;5;1;48;5;2"). Rets(Style{Foreground: XTerm256Color(1), Background: XTerm256Color(2)}), // True colors. tt.Args("38;2;1;2;3;48;2;10;20;30"). Rets(Style{ Foreground: TrueColor(1, 2, 3), Background: TrueColor(10, 20, 30)}), }) } elvish-0.17.0/pkg/ui/style.go000066400000000000000000000037641415471104000157650ustar00rootroot00000000000000package ui import ( "fmt" "strings" ) // Style specifies how something (mostly a string) shall be displayed. type Style struct { Foreground Color Background Color Bold bool Dim bool Italic bool Underlined bool Blink bool Inverse bool } // SGR returns SGR sequence for the style. func (s Style) SGR() string { var sgr []string addIf := func(b bool, code string) { if b { sgr = append(sgr, code) } } addIf(s.Bold, "1") addIf(s.Dim, "2") addIf(s.Italic, "3") addIf(s.Underlined, "4") addIf(s.Blink, "5") addIf(s.Inverse, "7") if s.Foreground != nil { sgr = append(sgr, s.Foreground.fgSGR()) } if s.Background != nil { sgr = append(sgr, s.Background.bgSGR()) } return strings.Join(sgr, ";") } // MergeFromOptions merges all recognized values from a map to the current // Style. func (s *Style) MergeFromOptions(options map[string]interface{}) error { assignColor := func(val interface{}, colorField *Color) string { if val == "default" { *colorField = nil return "" } else if s, ok := val.(string); ok { color := parseColor(s) if color != nil { *colorField = color return "" } } return "valid color string" } assignBool := func(val interface{}, attrField *bool) string { if b, ok := val.(bool); ok { *attrField = b } else { return "bool value" } return "" } for k, v := range options { var need string switch k { case "fg-color": need = assignColor(v, &s.Foreground) case "bg-color": need = assignColor(v, &s.Background) case "bold": need = assignBool(v, &s.Bold) case "dim": need = assignBool(v, &s.Dim) case "italic": need = assignBool(v, &s.Italic) case "underlined": need = assignBool(v, &s.Underlined) case "blink": need = assignBool(v, &s.Blink) case "inverse": need = assignBool(v, &s.Inverse) default: return fmt.Errorf("unrecognized option '%s'", k) } if need != "" { return fmt.Errorf("value for option '%s' must be a %s", k, need) } } return nil } elvish-0.17.0/pkg/ui/style_regions.go000066400000000000000000000032631415471104000175050ustar00rootroot00000000000000package ui import ( "sort" "src.elv.sh/pkg/diag" ) // StylingRegion represents a region to apply styling. type StylingRegion struct { diag.Ranging Styling Styling Priority int } // StyleRegions applies styling to the specified regions in s. // // The regions are sorted by start position. If multiple Regions share the same // starting position, the one with the highest priority is kept; the other // regions are removed. If a Region starts before the end of the previous // Region, it is also removed. func StyleRegions(s string, regions []StylingRegion) Text { regions = fixRegions(regions) var text Text lastTo := 0 for _, r := range regions { if r.From > lastTo { // Add text between regions or before the first region. text = append(text, &Segment{Text: s[lastTo:r.From]}) } text = append(text, StyleSegment(&Segment{Text: s[r.From:r.To]}, r.Styling)) lastTo = r.To } if len(s) > lastTo { // Add text after the last region. text = append(text, &Segment{Text: s[lastTo:]}) } return text } func fixRegions(regions []StylingRegion) []StylingRegion { regions = append([]StylingRegion(nil), regions...) // Sort regions by their start positions. Regions with the same start // position are sorted by decreasing priority. sort.Slice(regions, func(i, j int) bool { a, b := regions[i], regions[j] return a.From < b.From || (a.From == b.From && a.Priority > b.Priority) }) // Remove overlapping regions, preferring the ones that appear earlier. var newRegions []StylingRegion lastTo := 0 for _, r := range regions { if r.From < lastTo { // Overlaps with the last one continue } newRegions = append(newRegions, r) lastTo = r.To } return newRegions } elvish-0.17.0/pkg/ui/style_regions_test.go000066400000000000000000000032721415471104000205440ustar00rootroot00000000000000package ui_test import ( "reflect" "testing" "src.elv.sh/pkg/diag" . "src.elv.sh/pkg/ui" ) var styleRegionsTests = []struct { Name string String string Regions []StylingRegion WantText Text }{ { Name: "empty string and regions", String: "", Regions: nil, WantText: nil, }, { Name: "a single region", String: "foobar", Regions: []StylingRegion{ {r(1, 3), FgRed, 0}, }, WantText: Concat(T("f"), T("oo", FgRed), T("bar")), }, { Name: "multiple continuous regions", String: "foobar", Regions: []StylingRegion{ {r(1, 3), FgRed, 0}, {r(3, 4), FgGreen, 0}, }, WantText: Concat(T("f"), T("oo", FgRed), T("b", FgGreen), T("ar")), }, { Name: "multiple discontinuous regions in wrong order", String: "foobar", Regions: []StylingRegion{ {r(4, 5), FgGreen, 0}, {r(1, 3), FgRed, 0}, }, WantText: Concat(T("f"), T("oo", FgRed), T("b"), T("a", FgGreen), T("r")), }, { Name: "regions with the same starting position but differeng priorities", String: "foobar", Regions: []StylingRegion{ {r(1, 3), FgRed, 0}, {r(1, 2), FgGreen, 1}, }, WantText: Concat(T("f"), T("o", FgGreen), T("obar")), }, { Name: "overlapping regions with different starting positions", String: "foobar", Regions: []StylingRegion{ {r(1, 3), FgRed, 0}, {r(2, 4), FgGreen, 0}, }, WantText: Concat(T("f"), T("oo", FgRed), T("bar")), }, } func r(a, b int) diag.Ranging { return diag.Ranging{From: a, To: b} } func TestStyleRegions(t *testing.T) { for _, test := range styleRegionsTests { text := StyleRegions(test.String, test.Regions) if !reflect.DeepEqual(text, test.WantText) { t.Errorf("got %v, want %v", text, test.WantText) } } } elvish-0.17.0/pkg/ui/style_test.go000066400000000000000000000050271415471104000170160ustar00rootroot00000000000000package ui import ( "testing" ) func TestStyleSGR(t *testing.T) { // Test the SGR sequences of style attributes indirectly via VTString of // Text, since that is how they are used. testTextVTString(t, []textVTStringTest{ {T("foo", Bold), "\033[1mfoo\033[m"}, {T("foo", Dim), "\033[2mfoo\033[m"}, {T("foo", Italic), "\033[3mfoo\033[m"}, {T("foo", Underlined), "\033[4mfoo\033[m"}, {T("foo", Blink), "\033[5mfoo\033[m"}, {T("foo", Inverse), "\033[7mfoo\033[m"}, {T("foo", FgRed), "\033[31mfoo\033[m"}, {T("foo", BgRed), "\033[41mfoo\033[m"}, {T("foo", Bold, FgRed, BgBlue), "\033[1;31;44mfoo\033[m"}, }) } type mergeFromOptionsTest struct { style Style options map[string]interface{} wantStyle Style wantErr string } var mergeFromOptionsTests = []mergeFromOptionsTest{ // Parsing of each possible key. kv("fg-color", "red", Style{Foreground: Red}), kv("bg-color", "red", Style{Background: Red}), kv("bold", true, Style{Bold: true}), kv("dim", true, Style{Dim: true}), kv("italic", true, Style{Italic: true}), kv("underlined", true, Style{Underlined: true}), kv("blink", true, Style{Blink: true}), kv("inverse", true, Style{Inverse: true}), // Merging with existing options. { style: Style{Bold: true, Dim: true}, options: map[string]interface{}{ "bold": false, "fg-color": "red", }, wantStyle: Style{Dim: true, Foreground: Red}, }, // Bad key. { options: map[string]interface{}{"bad": true}, wantErr: "unrecognized option 'bad'", }, // Bad type for color field. { options: map[string]interface{}{"fg-color": true}, wantErr: "value for option 'fg-color' must be a valid color string", }, // Bad type for bool field. { options: map[string]interface{}{"bold": ""}, wantErr: "value for option 'bold' must be a bool value", }, } // A helper for constructing a test case whose input is a single key-value pair. func kv(k string, v interface{}, s Style) mergeFromOptionsTest { return mergeFromOptionsTest{ options: map[string]interface{}{k: v}, wantStyle: s, } } func TestMergeFromOptions(t *testing.T) { for _, test := range mergeFromOptionsTests { style := test.style err := style.MergeFromOptions(test.options) if style != test.wantStyle { t.Errorf("(%v).MergeFromOptions(%v) -> %v, want %v", test.style, test.options, style, test.wantStyle) } if err == nil { if test.wantErr != "" { t.Errorf("got error nil, want %v", test.wantErr) } } else { if err.Error() != test.wantErr { t.Errorf("got error %v, want error with message %s", err, test.wantErr) } } } } elvish-0.17.0/pkg/ui/styling.go000066400000000000000000000152071415471104000163110ustar00rootroot00000000000000package ui import ( "strings" ) // Styling specifies how to change a Style. It can also be applied to a Segment // or Text. type Styling interface{ transform(*Style) } // StyleText returns a new Text with the given Styling's applied. It does not // modify the given Text. func StyleText(t Text, ts ...Styling) Text { newt := make(Text, len(t)) for i, seg := range t { newt[i] = StyleSegment(seg, ts...) } return newt } // StyleSegment returns a new Segment with the given Styling's applied. It does // not modify the given Segment. func StyleSegment(seg *Segment, ts ...Styling) *Segment { return &Segment{Text: seg.Text, Style: ApplyStyling(seg.Style, ts...)} } // ApplyStyling returns a new Style with the given Styling's applied. func ApplyStyling(s Style, ts ...Styling) Style { for _, t := range ts { if t != nil { t.transform(&s) } } return s } // Stylings joins several transformers into one. func Stylings(ts ...Styling) Styling { return jointStyling(ts) } // Common stylings. var ( Reset Styling = reset{} FgDefault Styling = setForeground{nil} FgBlack Styling = setForeground{Black} FgRed Styling = setForeground{Red} FgGreen Styling = setForeground{Green} FgYellow Styling = setForeground{Yellow} FgBlue Styling = setForeground{Blue} FgMagenta Styling = setForeground{Magenta} FgCyan Styling = setForeground{Cyan} FgWhite Styling = setForeground{White} FgBrightBlack Styling = setForeground{BrightBlack} FgBrightRed Styling = setForeground{BrightRed} FgBrightGreen Styling = setForeground{BrightGreen} FgBrightYellow Styling = setForeground{BrightYellow} FgBrightBlue Styling = setForeground{BrightBlue} FgBrightMagenta Styling = setForeground{BrightMagenta} FgBrightCyan Styling = setForeground{BrightCyan} FgBrightWhite Styling = setForeground{BrightWhite} BgDefault Styling = setBackground{nil} BgBlack Styling = setBackground{Black} BgRed Styling = setBackground{Red} BgGreen Styling = setBackground{Green} BgYellow Styling = setBackground{Yellow} BgBlue Styling = setBackground{Blue} BgMagenta Styling = setBackground{Magenta} BgCyan Styling = setBackground{Cyan} BgWhite Styling = setBackground{White} BgBrightBlack Styling = setBackground{BrightBlack} BgBrightRed Styling = setBackground{BrightRed} BgBrightGreen Styling = setBackground{BrightGreen} BgBrightYellow Styling = setBackground{BrightYellow} BgBrightBlue Styling = setBackground{BrightBlue} BgBrightMagenta Styling = setBackground{BrightMagenta} BgBrightCyan Styling = setBackground{BrightCyan} BgBrightWhite Styling = setBackground{BrightWhite} Bold Styling = boolOn{boldField{}} Dim Styling = boolOn{dimField{}} Italic Styling = boolOn{italicField{}} Underlined Styling = boolOn{underlinedField{}} Blink Styling = boolOn{blinkField{}} Inverse Styling = boolOn{inverseField{}} NoBold Styling = boolOff{boldField{}} NoDim Styling = boolOff{dimField{}} NoItalic Styling = boolOff{italicField{}} NoUnderlined Styling = boolOff{underlinedField{}} NoBlink Styling = boolOff{blinkField{}} NoInverse Styling = boolOff{inverseField{}} ToggleBold Styling = boolToggle{boldField{}} ToggleDim Styling = boolToggle{dimField{}} ToggleItalic Styling = boolToggle{italicField{}} ToggleUnderlined Styling = boolToggle{underlinedField{}} ToggleBlink Styling = boolToggle{blinkField{}} ToggleInverse Styling = boolToggle{inverseField{}} ) // Fg returns a Styling that sets the foreground color. func Fg(c Color) Styling { return setForeground{c} } // Bg returns a Styling that sets the background color. func Bg(c Color) Styling { return setBackground{c} } type reset struct{} type setForeground struct{ c Color } type setBackground struct{ c Color } type boolOn struct{ f boolField } type boolOff struct{ f boolField } type boolToggle struct{ f boolField } func (reset) transform(s *Style) { *s = Style{} } func (t setForeground) transform(s *Style) { s.Foreground = t.c } func (t setBackground) transform(s *Style) { s.Background = t.c } func (t boolOn) transform(s *Style) { *t.f.get(s) = true } func (t boolOff) transform(s *Style) { *t.f.get(s) = false } func (t boolToggle) transform(s *Style) { p := t.f.get(s); *p = !*p } type boolField interface{ get(*Style) *bool } type boldField struct{} type dimField struct{} type italicField struct{} type underlinedField struct{} type blinkField struct{} type inverseField struct{} func (boldField) get(s *Style) *bool { return &s.Bold } func (dimField) get(s *Style) *bool { return &s.Dim } func (italicField) get(s *Style) *bool { return &s.Italic } func (underlinedField) get(s *Style) *bool { return &s.Underlined } func (blinkField) get(s *Style) *bool { return &s.Blink } func (inverseField) get(s *Style) *bool { return &s.Inverse } type jointStyling []Styling func (t jointStyling) transform(s *Style) { for _, t := range t { t.transform(s) } } // ParseStyling parses a text representation of Styling, which are kebab // case counterparts to the names of the builtin Styling's. For example, // ToggleInverse is expressed as "toggle-inverse". // // Multiple stylings can be joined by spaces, which is equivalent to calling // Stylings. // // If the given string is invalid, ParseStyling returns nil. func ParseStyling(s string) Styling { if !strings.ContainsRune(s, ' ') { return parseOneStyling(s) } var joint jointStyling for _, subs := range strings.Split(s, " ") { parsed := parseOneStyling(subs) if parsed == nil { return nil } joint = append(joint, parseOneStyling(subs)) } return joint } var boolFields = map[string]boolField{ "bold": boldField{}, "dim": dimField{}, "italic": italicField{}, "underlined": underlinedField{}, "blink": blinkField{}, "inverse": inverseField{}, } func parseOneStyling(name string) Styling { switch { case name == "default" || name == "fg-default": return FgDefault case strings.HasPrefix(name, "fg-"): if color := parseColor(name[len("fg-"):]); color != nil { return setForeground{color} } case name == "bg-default": return BgDefault case strings.HasPrefix(name, "bg-"): if color := parseColor(name[len("bg-"):]); color != nil { return setBackground{color} } case strings.HasPrefix(name, "no-"): if f, ok := boolFields[name[len("no-"):]]; ok { return boolOff{f} } case strings.HasPrefix(name, "toggle-"): if f, ok := boolFields[name[len("toggle-"):]]; ok { return boolToggle{f} } default: if f, ok := boolFields[name]; ok { return boolOn{f} } if color := parseColor(name); color != nil { return setForeground{color} } } return nil } elvish-0.17.0/pkg/ui/styling_test.go000066400000000000000000000051541415471104000173500ustar00rootroot00000000000000package ui import ( "reflect" "testing" "src.elv.sh/pkg/tt" ) func TestStyleText(t *testing.T) { tt.Test(t, tt.Fn("StyleText", StyleText), tt.Table{ // Foreground color tt.Args(T("foo"), FgRed). Rets(Text{&Segment{Style{Foreground: Red}, "foo"}}), // Override existing foreground tt.Args(Text{&Segment{Style{Foreground: Green}, "foo"}}, FgRed). Rets(Text{&Segment{Style{Foreground: Red}, "foo"}}), // Multiple segments tt.Args(Text{ &Segment{Style{}, "foo"}, &Segment{Style{Foreground: Green}, "bar"}}, FgRed). Rets(Text{ &Segment{Style{Foreground: Red}, "foo"}, &Segment{Style{Foreground: Red}, "bar"}, }), // Background color tt.Args(T("foo"), BgRed). Rets(Text{&Segment{Style{Background: Red}, "foo"}}), // Bold, false -> true tt.Args(T("foo"), Bold). Rets(Text{&Segment{Style{Bold: true}, "foo"}}), // Bold, true -> true tt.Args(Text{&Segment{Style{Bold: true}, "foo"}}, Bold). Rets(Text{&Segment{Style{Bold: true}, "foo"}}), // No Bold, true -> false tt.Args(Text{&Segment{Style{Bold: true}, "foo"}}, NoBold). Rets(Text{&Segment{Style{}, "foo"}}), // No Bold, false -> false tt.Args(T("foo"), NoBold).Rets(T("foo")), // Toggle Bold, true -> false tt.Args(Text{&Segment{Style{Bold: true}, "foo"}}, ToggleBold). Rets(Text{&Segment{Style{}, "foo"}}), // Toggle Bold, false -> true tt.Args(T("foo"), ToggleBold). Rets(Text{&Segment{Style{Bold: true}, "foo"}}), // For the remaining bool transformers, we only check one case; the rest // should be similar to "bold". // Dim. tt.Args(T("foo"), Dim). Rets(Text{&Segment{Style{Dim: true}, "foo"}}), // Italic. tt.Args(T("foo"), Italic). Rets(Text{&Segment{Style{Italic: true}, "foo"}}), // Underlined. tt.Args(T("foo"), Underlined). Rets(Text{&Segment{Style{Underlined: true}, "foo"}}), // Blink. tt.Args(T("foo"), Blink). Rets(Text{&Segment{Style{Blink: true}, "foo"}}), // Inverse. tt.Args(T("foo"), Inverse). Rets(Text{&Segment{Style{Inverse: true}, "foo"}}), // TODO: Test nil styling. }) } var parseStylingTests = []struct { s string wantStyling Styling }{ {"default", FgDefault}, {"red", FgRed}, {"fg-default", FgDefault}, {"fg-red", FgRed}, {"bg-default", BgDefault}, {"bg-red", BgRed}, {"bold", Bold}, {"no-bold", NoBold}, {"toggle-bold", ToggleBold}, {"red bold", Stylings(FgRed, Bold)}, } func TestParseStyling(t *testing.T) { for _, test := range parseStylingTests { styling := ParseStyling(test.s) if !reflect.DeepEqual(styling, test.wantStyling) { t.Errorf("ParseStyling(%q) -> %v, want %v", test.s, styling, test.wantStyling) } } } elvish-0.17.0/pkg/ui/text.go000066400000000000000000000126621415471104000156060ustar00rootroot00000000000000package ui import ( "bytes" "fmt" "math/big" "strconv" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/wcwidth" ) // Text contains of a list of styled Segments. type Text []*Segment // T constructs a new Text with the given content and the given Styling's // applied. func T(s string, ts ...Styling) Text { return StyleText(Text{&Segment{Text: s}}, ts...) } // Concat concatenates multiple Text's into one. func Concat(texts ...Text) Text { var ret Text for _, text := range texts { ret = append(ret, text...) } return ret } // Kind returns "styled-text". func (Text) Kind() string { return "ui:text" } // Repr returns the representation of the current Text. It is just a wrapper // around the containing Segments. func (t Text) Repr(indent int) string { buf := new(bytes.Buffer) for _, s := range t { buf.WriteString(s.Repr(indent + 1)) } return fmt.Sprintf("(ui:text %s)", buf.String()) } // IterateKeys feeds the function with all valid indices of the styled-text. func (t Text) IterateKeys(fn func(interface{}) bool) { for i := 0; i < len(t); i++ { if !fn(strconv.Itoa(i)) { break } } } // Index provides access to the underlying styled-segment. func (t Text) Index(k interface{}) (interface{}, error) { index, err := vals.ConvertListIndex(k, len(t)) if err != nil { return nil, err } else if index.Slice { return t[index.Lower:index.Upper], nil } else { return t[index.Lower], nil } } // Concat implements Text+string, Text+number, Text+Segment and Text+Text. func (t Text) Concat(rhs interface{}) (interface{}, error) { switch rhs := rhs.(type) { case string: return Concat(t, T(rhs)), nil case int, *big.Int, *big.Rat, float64: return Concat(t, T(vals.ToString(rhs))), nil case *Segment: return Concat(t, Text{rhs}), nil case Text: return Concat(t, rhs), nil } return nil, vals.ErrConcatNotImplemented } // RConcat implements string+Text and number+Text. func (t Text) RConcat(lhs interface{}) (interface{}, error) { switch lhs := lhs.(type) { case string: return Concat(T(lhs), t), nil case int, *big.Int, *big.Rat, float64: return Concat(T(vals.ToString(lhs)), t), nil } return nil, vals.ErrConcatNotImplemented } // Partition partitions the Text at n indices into n+1 Text values. func (t Text) Partition(indices ...int) []Text { out := make([]Text, len(indices)+1) segs := t.Clone() for i, idx := range indices { toConsume := idx if i > 0 { toConsume -= indices[i-1] } for len(segs) > 0 && toConsume > 0 { if len(segs[0].Text) <= toConsume { out[i] = append(out[i], segs[0]) toConsume -= len(segs[0].Text) segs = segs[1:] } else { out[i] = append(out[i], &Segment{segs[0].Style, segs[0].Text[:toConsume]}) segs[0] = &Segment{segs[0].Style, segs[0].Text[toConsume:]} toConsume = 0 } } } if len(segs) > 0 { // Don't use segs directly to avoid memory leak out[len(indices)] = append(Text(nil), segs...) } return out } // Clone returns a deep copy of Text. func (t Text) Clone() Text { newt := make(Text, len(t)) for i, seg := range t { newt[i] = seg.Clone() } return newt } // CountRune counts the number of times a rune occurs in a Text. func (t Text) CountRune(r rune) int { n := 0 for _, seg := range t { n += seg.CountRune(r) } return n } // CountLines counts the number of lines in a Text. It is equal to // t.CountRune('\n') + 1. func (t Text) CountLines() int { return t.CountRune('\n') + 1 } // SplitByRune splits a Text by the given rune. func (t Text) SplitByRune(r rune) []Text { // Call SplitByRune for each constituent Segment, and "paste" the pairs of // subsegments across the segment border. For instance, if Text has 3 // Segments a, b, c that results in a1, a2, a3, b1, b2, c1, then a3 and b1 // as well as b2 and c1 are pasted together, and the return value is [a1], // [a2], [a3, b1], [b2, c1]. Pasting can happen coalesce: for instance, if // Text has 3 Segments a, b, c that results in a1, a2, b1, c1, the return // value will be [a1], [a2, b1, c1]. var result []Text var paste Text for _, seg := range t { subSegs := seg.SplitByRune(r) if len(subSegs) == 1 { // Only one subsegment. Just paste. paste = append(paste, subSegs[0]) continue } // Paste the previous trailing segments with the first subsegment, and // add it as a Text. result = append(result, append(paste, subSegs[0])) // For the subsegments in the middle, just add then as is. for i := 1; i < len(subSegs)-1; i++ { result = append(result, Text{subSegs[i]}) } // The last segment becomes the new paste. paste = Text{subSegs[len(subSegs)-1]} } if len(paste) > 0 { result = append(result, paste) } return result } // TrimWcwidth returns the largest prefix of t that does not exceed the given // visual width. func (t Text) TrimWcwidth(wmax int) Text { var newt Text for _, seg := range t { w := wcwidth.Of(seg.Text) if w >= wmax { newt = append(newt, &Segment{seg.Style, wcwidth.Trim(seg.Text, wmax)}) break } wmax -= w newt = append(newt, seg) } return newt } // String returns a string representation of the styled text. This now always // assumes VT-style terminal output. // // TODO: Make string conversion sensible to environment, e.g. use HTML when // output is web. func (t Text) String() string { return t.VTString() } // VTString renders the styled text using VT-style escape sequences. func (t Text) VTString() string { var buf bytes.Buffer for _, seg := range t { buf.WriteString(seg.VTString()) } return buf.String() } elvish-0.17.0/pkg/ui/text_segment.go000066400000000000000000000077521415471104000173340ustar00rootroot00000000000000package ui import ( "bytes" "fmt" "math/big" "strings" "src.elv.sh/pkg/eval/vals" ) // Segment is a string that has some style applied to it. type Segment struct { Style Text string } // Kind returns "styled-segment". func (*Segment) Kind() string { return "ui:text-segment" } // Repr returns the representation of this Segment. The string can be used to // construct an identical Segment. Unset or default attributes are skipped. If // the Segment represents an unstyled string only this string is returned. func (s *Segment) Repr(int) string { buf := new(bytes.Buffer) addIfNotEqual := func(key string, val, cmp interface{}) { if val != cmp { var valString string if c, ok := val.(Color); ok { valString = c.String() } else { valString = vals.Repr(val, 0) } fmt.Fprintf(buf, "&%s=%s ", key, valString) } } addIfNotEqual("fg-color", s.Foreground, nil) addIfNotEqual("bg-color", s.Background, nil) addIfNotEqual("bold", s.Bold, false) addIfNotEqual("dim", s.Dim, false) addIfNotEqual("italic", s.Italic, false) addIfNotEqual("underlined", s.Underlined, false) addIfNotEqual("blink", s.Blink, false) addIfNotEqual("inverse", s.Inverse, false) if buf.Len() == 0 { return s.Text } return fmt.Sprintf("(ui:text-segment %s %s)", s.Text, strings.TrimSpace(buf.String())) } // IterateKeys feeds the function with all valid attributes of styled-segment. func (*Segment) IterateKeys(fn func(v interface{}) bool) { vals.Feed(fn, "text", "fg-color", "bg-color", "bold", "dim", "italic", "underlined", "blink", "inverse") } // Index provides access to the attributes of a styled-segment. func (s *Segment) Index(k interface{}) (v interface{}, ok bool) { switch k { case "text": v = s.Text case "fg-color": if s.Foreground == nil { return "default", true } return s.Foreground.String(), true case "bg-color": if s.Background == nil { return "default", true } return s.Background.String(), true case "bold": v = s.Bold case "dim": v = s.Dim case "italic": v = s.Italic case "underlined": v = s.Underlined case "blink": v = s.Blink case "inverse": v = s.Inverse } return v, v != nil } // Concat implements Segment+string, Segment+float64, Segment+Segment and // Segment+Text. func (s *Segment) Concat(v interface{}) (interface{}, error) { switch rhs := v.(type) { case string: return Text{s, &Segment{Text: rhs}}, nil case *Segment: return Text{s, rhs}, nil case Text: return Text(append([]*Segment{s}, rhs...)), nil case int, *big.Int, *big.Rat, float64: return Text{s, &Segment{Text: vals.ToString(rhs)}}, nil } return nil, vals.ErrConcatNotImplemented } // RConcat implements string+Segment and float64+Segment. func (s *Segment) RConcat(v interface{}) (interface{}, error) { switch lhs := v.(type) { case string: return Text{&Segment{Text: lhs}, s}, nil case int, *big.Int, *big.Rat, float64: return Text{&Segment{Text: vals.ToString(lhs)}, s}, nil } return nil, vals.ErrConcatNotImplemented } // Clone returns a copy of the Segment. func (s *Segment) Clone() *Segment { value := *s return &value } // CountRune counts the number of times a rune occurs in a Segment. func (s *Segment) CountRune(r rune) int { return strings.Count(s.Text, string(r)) } // SplitByRune splits a Segment by the given rune. func (s *Segment) SplitByRune(r rune) []*Segment { splitTexts := strings.Split(s.Text, string(r)) splitSegs := make([]*Segment, len(splitTexts)) for i, splitText := range splitTexts { splitSegs[i] = &Segment{s.Style, splitText} } return splitSegs } // String returns a string representation of the styled segment. This now always // assumes VT-style terminal output. // TODO: Make string conversion sensible to environment, e.g. use HTML when // output is web. func (s *Segment) String() string { return s.VTString() } // VTString renders the styled segment using VT-style escape sequences. func (s *Segment) VTString() string { sgr := s.SGR() if sgr == "" { return s.Text } return fmt.Sprintf("\033[%sm%s\033[m", sgr, s.Text) } elvish-0.17.0/pkg/ui/text_segment_test.go000066400000000000000000000013551415471104000203640ustar00rootroot00000000000000package ui import ( "testing" "src.elv.sh/pkg/eval/vals" ) func TestTextSegmentAsElvishValue(t *testing.T) { vals.TestValue(t, &Segment{Style{}, "foo"}). Kind("ui:text-segment"). Repr("foo"). AllKeys("text", "fg-color", "bg-color", "bold", "dim", "italic", "underlined", "blink", "inverse"). Index("text", "foo"). Index("fg-color", "default"). Index("bg-color", "default"). Index("bold", false). Index("dim", false). Index("italic", false). Index("underlined", false). Index("blink", false). Index("inverse", false) vals.TestValue(t, &Segment{Style{Foreground: Red, Background: Blue}, "foo"}). Repr("(ui:text-segment foo &fg-color=red &bg-color=blue)"). Index("fg-color", "red"). Index("bg-color", "blue") } elvish-0.17.0/pkg/ui/text_test.go000066400000000000000000000104531415471104000166410ustar00rootroot00000000000000package ui import ( "testing" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/tt" ) var Args = tt.Args func TestT(t *testing.T) { tt.Test(t, tt.Fn("T", T), tt.Table{ Args("test").Rets(Text{&Segment{Text: "test"}}), Args("test red", FgRed).Rets(Text{&Segment{ Text: "test red", Style: Style{Foreground: Red}}}), Args("test red", FgRed, Bold).Rets(Text{&Segment{ Text: "test red", Style: Style{Foreground: Red, Bold: true}}}), }) } func TestTextAsElvishValue(t *testing.T) { vals.TestValue(t, T("text")). Kind("ui:text"). Repr("(ui:text text)"). AllKeys("0"). Index("0", &Segment{Text: "text"}) vals.TestValue(t, T("text", FgRed)). Repr("(ui:text (ui:text-segment text &fg-color=red))") vals.TestValue(t, T("text", Bold)). Repr("(ui:text (ui:text-segment text &bold=$true))") } var ( text0 = Text{} text1 = Text{red("lorem")} text2 = Text{red("lorem"), blue("foobar")} ) func red(s string) *Segment { return &Segment{Style{Foreground: Red}, s} } func blue(s string) *Segment { return &Segment{Style{Foreground: Blue}, s} } var partitionTests = tt.Table{ Args(text0).Rets([]Text{nil}), Args(text1).Rets([]Text{text1}), Args(text1, 0).Rets([]Text{nil, text1}), Args(text1, 1).Rets([]Text{{red("l")}, {red("orem")}}), Args(text1, 5).Rets([]Text{text1, nil}), Args(text2).Rets([]Text{text2}), Args(text2, 0).Rets([]Text{nil, text2}), Args(text2, 1).Rets([]Text{ {red("l")}, {red("orem"), blue("foobar")}}), Args(text2, 2).Rets([]Text{ {red("lo")}, {red("rem"), blue("foobar")}}), Args(text2, 5).Rets([]Text{{red("lorem")}, {blue("foobar")}}), Args(text2, 6).Rets([]Text{ {red("lorem"), blue("f")}, {blue("oobar")}}), Args(text2, 11).Rets([]Text{text2, nil}), Args(text1, 1, 2).Rets([]Text{{red("l")}, {red("o")}, {red("rem")}}), Args(text1, 1, 2, 3, 4).Rets([]Text{ {red("l")}, {red("o")}, {red("r")}, {red("e")}, {red("m")}}), Args(text2, 2, 4, 6).Rets([]Text{ {red("lo")}, {red("re")}, {red("m"), blue("f")}, {blue("oobar")}}), Args(text2, 6, 8).Rets([]Text{ {red("lorem"), blue("f")}, {blue("oo")}, {blue("bar")}}), } func TestPartition(t *testing.T) { tt.Test(t, tt.Fn("Text.Partition", Text.Partition), partitionTests) } func TestCountRune(t *testing.T) { text := Text{red("lorem"), blue("ipsum")} tt.Test(t, tt.Fn("Text.CountRune", Text.CountRune), tt.Table{ Args(text, 'l').Rets(1), Args(text, 'i').Rets(1), Args(text, 'm').Rets(2), Args(text, '\n').Rets(0), }) } func TestCountLines(t *testing.T) { tt.Test(t, tt.Fn("Text.CountLines", Text.CountLines), tt.Table{ Args(Text{red("lorem")}).Rets(1), Args(Text{red("lorem"), blue("ipsum")}).Rets(1), Args(Text{red("lor\nem"), blue("ipsum")}).Rets(2), Args(Text{red("lor\nem"), blue("ip\nsum")}).Rets(3), }) } func TestSplitByRune(t *testing.T) { tt.Test(t, tt.Fn("Text.SplitByRune", Text.SplitByRune), tt.Table{ Args(Text{}, '\n').Rets([]Text(nil)), Args(Text{red("lorem")}, '\n').Rets([]Text{{red("lorem")}}), Args(Text{red("lorem"), blue("ipsum"), red("dolar")}, '\n').Rets( []Text{ {red("lorem"), blue("ipsum"), red("dolar")}, }), Args(Text{red("lo\nrem")}, '\n').Rets([]Text{ {red("lo")}, {red("rem")}, }), Args(Text{red("lo\nrem"), blue("ipsum")}, '\n').Rets( []Text{ {red("lo")}, {red("rem"), blue("ipsum")}, }), Args(Text{red("lo\nrem"), blue("ip\nsum")}, '\n').Rets( []Text{ {red("lo")}, {red("rem"), blue("ip")}, {blue("sum")}, }), Args(Text{red("lo\nrem"), blue("ip\ns\num"), red("dolar")}, '\n').Rets( []Text{ {red("lo")}, {red("rem"), blue("ip")}, {blue("s")}, {blue("um"), red("dolar")}, }), }) } func TestTrimWcwidth(t *testing.T) { tt.Test(t, tt.Fn("Text.TrimWcwidth", Text.TrimWcwidth), tt.Table{ Args(Text{}, 1).Rets(Text(nil)), Args(Text{red("lorem")}, 3).Rets(Text{red("lor")}), Args(Text{red("lorem"), blue("ipsum")}, 6).Rets( Text{red("lorem"), blue("i")}), Args(Text{red("你好")}, 3).Rets(Text{red("你")}), Args(Text{red("你好"), blue("精灵语"), red("x")}, 7).Rets( Text{red("你好"), blue("精")}), }) } type textVTStringTest struct { text Text wantVTString string } func testTextVTString(t *testing.T, tests []textVTStringTest) { for _, test := range tests { vtString := test.text.VTString() if vtString != test.wantVTString { t.Errorf("got %q, want %q", vtString, test.wantVTString) } } } elvish-0.17.0/pkg/ui/ui.go000066400000000000000000000001301415471104000152220ustar00rootroot00000000000000// Package ui contains types that may be used by different editor frontends. package ui elvish-0.17.0/pkg/wcwidth/000077500000000000000000000000001415471104000153205ustar00rootroot00000000000000elvish-0.17.0/pkg/wcwidth/wcwidth.go000066400000000000000000000130421415471104000173200ustar00rootroot00000000000000// Package wcwidth provides utilities for determining the column width of // characters when displayed on the terminal. package wcwidth import ( "sort" "strings" "sync" ) var ( overrideMutex sync.RWMutex override = map[rune]int{} ) // Taken from http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c (public domain) var combining = [][2]rune{ {0x0300, 0x036F}, {0x0483, 0x0486}, {0x0488, 0x0489}, {0x0591, 0x05BD}, {0x05BF, 0x05BF}, {0x05C1, 0x05C2}, {0x05C4, 0x05C5}, {0x05C7, 0x05C7}, {0x0600, 0x0603}, {0x0610, 0x0615}, {0x064B, 0x065E}, {0x0670, 0x0670}, {0x06D6, 0x06E4}, {0x06E7, 0x06E8}, {0x06EA, 0x06ED}, {0x070F, 0x070F}, {0x0711, 0x0711}, {0x0730, 0x074A}, {0x07A6, 0x07B0}, {0x07EB, 0x07F3}, {0x0901, 0x0902}, {0x093C, 0x093C}, {0x0941, 0x0948}, {0x094D, 0x094D}, {0x0951, 0x0954}, {0x0962, 0x0963}, {0x0981, 0x0981}, {0x09BC, 0x09BC}, {0x09C1, 0x09C4}, {0x09CD, 0x09CD}, {0x09E2, 0x09E3}, {0x0A01, 0x0A02}, {0x0A3C, 0x0A3C}, {0x0A41, 0x0A42}, {0x0A47, 0x0A48}, {0x0A4B, 0x0A4D}, {0x0A70, 0x0A71}, {0x0A81, 0x0A82}, {0x0ABC, 0x0ABC}, {0x0AC1, 0x0AC5}, {0x0AC7, 0x0AC8}, {0x0ACD, 0x0ACD}, {0x0AE2, 0x0AE3}, {0x0B01, 0x0B01}, {0x0B3C, 0x0B3C}, {0x0B3F, 0x0B3F}, {0x0B41, 0x0B43}, {0x0B4D, 0x0B4D}, {0x0B56, 0x0B56}, {0x0B82, 0x0B82}, {0x0BC0, 0x0BC0}, {0x0BCD, 0x0BCD}, {0x0C3E, 0x0C40}, {0x0C46, 0x0C48}, {0x0C4A, 0x0C4D}, {0x0C55, 0x0C56}, {0x0CBC, 0x0CBC}, {0x0CBF, 0x0CBF}, {0x0CC6, 0x0CC6}, {0x0CCC, 0x0CCD}, {0x0CE2, 0x0CE3}, {0x0D41, 0x0D43}, {0x0D4D, 0x0D4D}, {0x0DCA, 0x0DCA}, {0x0DD2, 0x0DD4}, {0x0DD6, 0x0DD6}, {0x0E31, 0x0E31}, {0x0E34, 0x0E3A}, {0x0E47, 0x0E4E}, {0x0EB1, 0x0EB1}, {0x0EB4, 0x0EB9}, {0x0EBB, 0x0EBC}, {0x0EC8, 0x0ECD}, {0x0F18, 0x0F19}, {0x0F35, 0x0F35}, {0x0F37, 0x0F37}, {0x0F39, 0x0F39}, {0x0F71, 0x0F7E}, {0x0F80, 0x0F84}, {0x0F86, 0x0F87}, {0x0F90, 0x0F97}, {0x0F99, 0x0FBC}, {0x0FC6, 0x0FC6}, {0x102D, 0x1030}, {0x1032, 0x1032}, {0x1036, 0x1037}, {0x1039, 0x1039}, {0x1058, 0x1059}, {0x1160, 0x11FF}, {0x135F, 0x135F}, {0x1712, 0x1714}, {0x1732, 0x1734}, {0x1752, 0x1753}, {0x1772, 0x1773}, {0x17B4, 0x17B5}, {0x17B7, 0x17BD}, {0x17C6, 0x17C6}, {0x17C9, 0x17D3}, {0x17DD, 0x17DD}, {0x180B, 0x180D}, {0x18A9, 0x18A9}, {0x1920, 0x1922}, {0x1927, 0x1928}, {0x1932, 0x1932}, {0x1939, 0x193B}, {0x1A17, 0x1A18}, {0x1B00, 0x1B03}, {0x1B34, 0x1B34}, {0x1B36, 0x1B3A}, {0x1B3C, 0x1B3C}, {0x1B42, 0x1B42}, {0x1B6B, 0x1B73}, {0x1DC0, 0x1DCA}, {0x1DFE, 0x1DFF}, {0x200B, 0x200F}, {0x202A, 0x202E}, {0x2060, 0x2063}, {0x206A, 0x206F}, {0x20D0, 0x20EF}, {0x302A, 0x302F}, {0x3099, 0x309A}, {0xA806, 0xA806}, {0xA80B, 0xA80B}, {0xA825, 0xA826}, {0xFB1E, 0xFB1E}, {0xFE00, 0xFE0F}, {0xFE20, 0xFE23}, {0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, {0x10A01, 0x10A03}, {0x10A05, 0x10A06}, {0x10A0C, 0x10A0F}, {0x10A38, 0x10A3A}, {0x10A3F, 0x10A3F}, {0x1D167, 0x1D169}, {0x1D173, 0x1D182}, {0x1D185, 0x1D18B}, {0x1D1AA, 0x1D1AD}, {0x1D242, 0x1D244}, {0xE0001, 0xE0001}, {0xE0020, 0xE007F}, {0xE0100, 0xE01EF}, } func isCombining(r rune) bool { n := len(combining) i := sort.Search(n, func(i int) bool { return r <= combining[i][1] }) return i < n && r >= combining[i][0] } // OfRune returns the column width of a rune. func OfRune(r rune) int { if w, ok := getOverride(r); ok { return w } if r == 0 || r < 32 || (0x7f <= r && r < 0xa0) || // Control character isCombining(r) { return 0 } if r >= 0x1100 && (r <= 0x115f || /* Hangul Jamo init. consonants */ r == 0x2329 || r == 0x232a || (r >= 0x2e80 && r <= 0xa4cf && r != 0x303f) || /* CJK ... Yi */ (r >= 0xac00 && r <= 0xd7a3) || /* Hangul Syllables */ (r >= 0xf900 && r <= 0xfaff) || /* CJK Compatibility Ideographs */ (r >= 0xfe10 && r <= 0xfe19) || /* Vertical forms */ (r >= 0xfe30 && r <= 0xfe6f) || /* CJK Compatibility Forms */ (r >= 0xff00 && r <= 0xff60) || /* Fullwidth Forms */ (r >= 0xffe0 && r <= 0xffe6) || /* Fullwidth Forms */ (r >= 0x20000 && r <= 0x2fffd) || /* CJK Extensions */ (r >= 0x30000 && r <= 0x3fffd) || /* Reserved for historical Chinese scripts */ (r >= 0x1f300 && r <= 0x1f6ff)) { // Miscellaneous Symbols and Pictographs ... Geometric Shapes Extended return 2 } return 1 } func getOverride(r rune) (int, bool) { overrideMutex.RLock() defer overrideMutex.RUnlock() w, ok := override[r] return w, ok } // Override overrides the column width of a rune to be a specific non-negative // value. If w < 0, it removes the override. func Override(r rune, w int) { if w < 0 { Unoverride(r) return } overrideMutex.Lock() defer overrideMutex.Unlock() override[r] = w } // Unoverride removes the column width override of a rune. func Unoverride(r rune) { overrideMutex.Lock() defer overrideMutex.Unlock() delete(override, r) } // Of returns the column width of a string, assuming no soft line breaks. func Of(s string) (w int) { for _, r := range s { w += OfRune(r) } return } // Trim trims the string s so that it has a column width of at most wmax. func Trim(s string, wmax int) string { w := 0 for i, r := range s { w += OfRune(r) if w > wmax { return s[:i] } } return s } // Force forces the string s to the given column width by trimming and padding. func Force(s string, width int) string { w := 0 for i, r := range s { w0 := OfRune(r) w += w0 if w > width { w -= w0 s = s[:i] break } } return s + strings.Repeat(" ", width-w) } // TrimEachLine trims each line of s so that it is no wider than the specified // width. func TrimEachLine(s string, width int) string { lines := strings.Split(s, "\n") for i := range lines { lines[i] = Trim(lines[i], width) } return strings.Join(lines, "\n") } elvish-0.17.0/pkg/wcwidth/wcwidth_test.go000066400000000000000000000033071415471104000203620ustar00rootroot00000000000000package wcwidth import ( "testing" "src.elv.sh/pkg/tt" ) var Args = tt.Args func TestOf(t *testing.T) { tt.Test(t, tt.Fn("Of", Of), tt.Table{ Args("\u0301").Rets(0), // Combining acute accent Args("a").Rets(1), Args("Ω").Rets(1), Args("好").Rets(2), Args("か").Rets(2), Args("abc").Rets(3), Args("你好").Rets(4), }) } func TestOverride(t *testing.T) { r := '❱' oldw := OfRune(r) w := oldw + 1 Override(r, w) if OfRune(r) != w { t.Errorf("Wcwidth(%q) != %d after OverrideWcwidth", r, w) } Unoverride(r) if OfRune(r) != oldw { t.Errorf("Wcwidth(%q) != %d after UnoverrideWcwidth", r, oldw) } } func TestOverride_NegativeWidthRemovesOverride(t *testing.T) { Override('x', 2) Override('x', -1) if OfRune('x') != 1 { t.Errorf("Override with negative width did not remove override") } } func TestConcurrentOverride(t *testing.T) { go Override('x', 2) _ = OfRune('x') } func TestTrim(t *testing.T) { tt.Test(t, tt.Fn("Trim", Trim), tt.Table{ Args("abc", 1).Rets("a"), Args("abc", 2).Rets("ab"), Args("abc", 3).Rets("abc"), Args("abc", 4).Rets("abc"), Args("你好", 1).Rets(""), Args("你好", 2).Rets("你"), Args("你好", 3).Rets("你"), Args("你好", 4).Rets("你好"), Args("你好", 5).Rets("你好"), }) } func TestForce(t *testing.T) { tt.Test(t, tt.Fn("Force", Force), tt.Table{ // Trimming Args("abc", 2).Rets("ab"), Args("你好", 2).Rets("你"), // Padding Args("abc", 4).Rets("abc "), Args("你好", 5).Rets("你好 "), // Trimming and Padding Args("你好", 3).Rets("你 "), }) } func TestTrimEachLine(t *testing.T) { tt.Test(t, tt.Fn("TrimEachLine", TrimEachLine), tt.Table{ Args("abcdefg\n你好", 3).Rets("abc\n你"), }) } elvish-0.17.0/tools/000077500000000000000000000000001415471104000142265ustar00rootroot00000000000000elvish-0.17.0/tools/buildall.sh000077500000000000000000000056761415471104000163730ustar00rootroot00000000000000#!/bin/sh -e # buildall.sh $SRC_DIR $DST_DIR $SUFFIX # # Builds Elvish binaries for all supported platforms, using the code in $SRC_DIR # and building $DST_DIR/$GOOS-$GOARCH/elvish-$SUFFIX for each supported # combination of $GOOS and $GOARCH. # # It also creates and an archive for each binary file, and puts it in the same # directory. For GOOS=windows, the archive is a .zip file. For all other GOOS, # the archive is a .tar.gz file. # # If the sha256sum command is available, this script also creates a sha256sum # file for each binary and archive file, and puts it in the same directory. # # The ELVISH_REPRODUCIBLE environment variable, if set, instructs the script to # mark the binary as a reproducible build. It must take one of the two following # values: # # - release: SRC_DIR must contain the source code for a tagged release. # # - dev: SRC_DIR must be a Git repository checked out from the latest master # branch. # # This script is not whitespace-correct; avoid whitespaces in directory names. if test $# != 3; then # Output the comment block above, stripping any leading "#" or "# " sed < $0 -En ' /^# /,/^$/{ /^$/q s/^# ?// p }' exit 1 fi SRC_DIR=$1 DST_DIR=$2 SUFFIX=$3 LD_FLAGS= if test -n "$ELVISH_REPRODUCIBLE"; then LD_FLAGS="-X src.elv.sh/pkg/buildinfo.Reproducible=true" if test "$ELVISH_REPRODUCIBLE" = dev; then LD_FLAGS="$LD_FLAGS -X src.elv.sh/pkg/buildinfo.VersionSuffix=-dev.$(git -C $SRC_DIR rev-parse HEAD)" elif test "$ELVISH_REPRODUCIBLE" = release; then : # nothing to do else echo "$ELVISH_REPRODUCIBLE must be 'dev' or 'release' when set" fi fi export GOOS GOARCH GOFLAGS export CGO_ENABLED=0 main() { buildarch amd64 linux darwin freebsd openbsd netbsd windows buildarch 386 linux windows buildarch arm64 linux darwin } # buildarch $arch $os... # # Builds one GOARCH, multiple GOOS. buildarch() { local GOARCH=$1 GOOS shift for GOOS in $@; do buildone done } # buildone # # Builds one GOARCH and one GOOS. # # Uses: $GOARCH $GOOS $DST_DIR buildone() { local BIN_DIR=$DST_DIR/$GOOS-$GOARCH mkdir -p $BIN_DIR local STEM=elvish-$SUFFIX if test $GOOS = windows; then local BIN=$STEM.exe local ARCHIVE=$STEM.zip else local BIN=$STEM local ARCHIVE=$STEM.tar.gz fi if go env GOOS GOARCH | egrep -qx '(windows .*|linux (amd64|arm64))'; then local GOFLAGS=-buildmode=pie fi printf '%s' "Building for $GOOS-$GOARCH... " go build -trimpath -ldflags "$LD_FLAGS"\ -o $BIN_DIR/$BIN $SRC_DIR/cmd/elvish || { echo "Failed" return } ( cd $BIN_DIR if test $GOOS = windows; then zip -q $ARCHIVE $BIN else tar cfz $ARCHIVE $BIN fi echo "Done" if which sha256sum > /dev/null; then sha256sum $BIN > $BIN.sha256sum sha256sum $ARCHIVE > $ARCHIVE.sha256sum fi ) } main elvish-0.17.0/tools/checkstyle-go.sh000077500000000000000000000005741415471104000173340ustar00rootroot00000000000000#!/bin/sh -e # Check if the style of the Go source files is correct without modifying those # files. # The grep is needed because `goimports -d` and `gofmt -d` always exits with 0. echo 'Go files need these changes:' if find . -name '*.go' | xargs goimports -d | grep .; then exit 1 fi if find . -name '*.go' | xargs gofmt -s -d | grep .; then exit 1 fi echo ' None!' elvish-0.17.0/tools/checkstyle-md.sh000077500000000000000000000022041415471104000173170ustar00rootroot00000000000000#!/bin/sh -e # Check if the style of the Markdown files is correct without modifying those # files. # The `prettier` utility doesn't provide a "diff" option. Therefore, we have # to do this in a convoluted fashion to get a diff from the current state to # what `prettier` considers correct and reporting, via the exit status, that # the check failed. # We predicate the detailed diff on being in a CI environment since we don't # care if the files are modified. If not, just list the pathnames that need to # be reformatted without actually modifying those files. if test "$CI" = "" then echo 'Markdown files that need changes:' ! find . -name '*.md' | xargs prettier --tab-width 4 --prose-wrap always --list-different | sed 's/^/ /' | grep . && echo ' None!' else echo 'Markdown files need these changes:' if ! find . -name '*.md' | xargs prettier --tab-width 4 --prose-wrap always --check >/dev/null then find . -name '*.md' | xargs prettier --tab-width 4 --prose-wrap always --write >/dev/null find . -name '*.md' | xargs git diff exit 1 fi echo ' None!' fi elvish-0.17.0/tools/lint.sh000077500000000000000000000000541415471104000155320ustar00rootroot00000000000000#!/bin/sh -e go vet ./... staticcheck ./... elvish-0.17.0/tools/prune-cover.sh000077500000000000000000000006471415471104000170410ustar00rootroot00000000000000#!/bin/sh -e # Prune the same objects from the "make cover" report that we tell Codecov # (https://codecov.io/gh/elves/elvish/) to ignore. if test $# != 2 then echo 'Usage: cover_prune.sh ${codecov.yml} $cover' >&2 exit 1 fi yaml="$1" data="$2" sed -En '/^ignore:/,/^[^ ]/s/^ *- "(.*)"/src.elv.sh\/\1/p' $yaml > $yaml.ignore grep -F -v -f $yaml.ignore $data > $data.pruned mv $data.pruned $data rm $yaml.ignore elvish-0.17.0/tools/run-race.sh000077500000000000000000000011721415471104000163020ustar00rootroot00000000000000#!/bin/sh # Prints "-race" if running on a platform that supports the race detector. # This should be kept in sync of the official list here: # https://golang.org/doc/articles/race_detector#Supported_Systems if test `go env CGO_ENABLED` = 1; then if echo `go env GOOS GOARCH` | egrep -qx '((linux|darwin|freebsd|netbsd) amd64|(linux|darwin) arm64|linux ppc64le)'; then printf %s -race elif echo `go env GOOS GOARCH` | egrep -qx 'windows amd64'; then # Race detector on windows amd64 requires gcc: # https://github.com/golang/go/issues/27089 if which gcc > /dev/null; then printf %s -race fi fi fi elvish-0.17.0/website/000077500000000000000000000000001415471104000145305ustar00rootroot00000000000000elvish-0.17.0/website/.gitignore000066400000000000000000000001221415471104000165130ustar00rootroot00000000000000/home.html /*/*.html !/ttyshot/*.html /tools/*.bin /_* /Elvish.docset /Elvish.tgz elvish-0.17.0/website/Makefile000066400000000000000000000022411415471104000161670ustar00rootroot00000000000000DST_DIR := ./_dst PUBLISH_DIR := ./_publish DOCSET_TMP_DIR := ./_docset_tmp DOCSET_DST_DIR := ./Elvish.docset MDS := home.md $(filter-out %/README.md,$(wildcard [^_]*/*.md)) HTMLS := $(MDS:.md=.html) TOOL_PKGS := highlight macros elvdoc genblog TOOL_BINS := $(addprefix tools/,$(addsuffix .bin,$(TOOL_PKGS))) # Generates the website into $(DST_DIR). gen: $(TOOL_BINS) $(HTMLS) tools/genblog.bin . $(DST_DIR) ln -sf `pwd`/fonts `pwd`/favicons/* $(DST_DIR)/ # Generates docset into $(DOCSET_DST_DIR). docset: $(TOOL_BINS) $(HTMLS) ELVISH_DOCSET_MODE=1 tools/genblog.bin . $(DOCSET_TMP_DIR) ./tools/mkdocset $(DOCSET_TMP_DIR) $(DOCSET_DST_DIR) # Synchronizes the generated website into $(PUBLISH_DIR), which is passed to # rsync and can be a remote place. publish: gen rsync -aLv --delete ./_dst/ $(PUBLISH_DIR)/ check-rellinks: gen python3 tools/check-rellinks.py $(DST_DIR) clean: rm -rf $(TOOL_BINS) $(HTMLS) $(DST_DIR) $(DOCSET_TMP_DIR) $(DOCSET_DST_DIR) .PHONY: gen docset publish clean .SECONDEXPANSION: tools/%.bin: cmd/% $$(wildcard cmd/%/*.go) go build -o $@ ./$< %.html: %.md $(TOOL_BINS) tools/md-to-html $$(shell tools/ref-deps $$@) tools/md-to-html $< $@ elvish-0.17.0/website/README.md000066400000000000000000000032041415471104000160060ustar00rootroot00000000000000This directory contains source for Elvish's official website. The documents are written in GitHub-flavored markdown sprinkled with some HTML and custom macros. Most of them can be viewed directly in GitHub; notable exceptions are the homepage (`home.md`) and the download page (`get/prelude.md`). # Building The website is a purely static one. It is built with a custom toolchain with the following dependencies: - GNU Make (any "reasonably modern" version should do). - Pandoc 2.2.1 (other versions in the 2.x series might also work). - A Go toolchain, for building [genblog](https://github.com/xiaq/genblog) and some custom preprocessors in the `tools` directory. To build the website, just run `make`. The built website is in the `_dst` directory. You can then open `_dst/index.html` or run an HTTP server within `_dst` to preview. **NOTE**: Although the website degrades gracefully when JavaScript is disabled, local viewing works best with JavaScript enabled. This is because relative paths like `./get` will cause the browser to open the corresponding directory, instead of the `index.html` file under it, and we use JavaScript to patch such URLs dynamically. ## Building the docset Building the docset requires the following additional dependencies: - Python 3 with Beautiful Soup 4 (install with `pip install bs4`). - SQLite3 CLI. # Commit History These files used to live in a [separate repository](https://github.com/elves/elvish.io). However, because @xiaq did not merge the repositories in the correct way (he simply copied all the files), the commit history is lost. Please see that repository for a full list of contributors. elvish-0.17.0/website/blog/000077500000000000000000000000001415471104000154535ustar00rootroot00000000000000elvish-0.17.0/website/blog/0.10-release-notes.md000066400000000000000000000151341415471104000211230ustar00rootroot00000000000000Version 0.10 has been released two and a half months after 0.9, bringing many new features and enhancements. The [second issue](newsletter-sep-2017.html) of Elvish Newsletter accompanies this release. # Breaking changes - If you are upgrading from an earlier version, Elvish will complain that your database is not valid. This is because Elvish now uses BoltDB for storage. A [database migration tool](https://github.com/elves/upgrade-db-for-0.10) is available. - Breaking changes to the editor API: - The `$edit:completer` map is now known as `$edit:arg-completer`. - The keybinding API has been changed (again). Keybindings for different now live in their own subnamespaces. For instance, keybindings for insert mode used to be `edit:binding[insert]` but is now `edit:insert:binding`. - Module names of some editor modes have also been changed for consistency. The completion mode now uses the `edit:completion` module (used to be `edit:compl`). Location mode: `edit:location`; navigation mode: `edit:navigation`. - Byte output from prompts now preserve newlines. For instance, if you have `edit:prompt = { echo haha }`, you will now have a trailing newline in the prompt, making your command appear on the next line. To fix this, simple replace `echo` with `print`, which does not print a trailing newline. ([#354](https://github.com/elves/elvish/issues/354)) - Breaking changes to the language core: - Due to the switch to persistent data structures, assignments of maps now behave as if they copy the entire container. See the section in [some unique semantics](../learn/unique-semantics.html) for an explanation. - The implicit `$args` variable is gone, as well as its friends: positional variables `$0`, `$1`, ..., and the special `$@` shorthand for `$@args`. Lambdas defined without argument list (`{ some code }`) now behave as if they have an empty argument list. ([#397](https://github.com/elves/elvish/issues/397)) Old lambdas that rely on `$args` or its friends now must declare their arguments explicitly. For instance, `fn ls { e:ls --color=auto $@ }` needs to be rewritten to `fn ls [@a]{ e:ls --color=auto $@a }`. - Support for using backquotes for output capture (e.g. `` echo uname is `uname` ``) has been removed. Use parentheses instead (e.g. `echo uname is (uname)`). - Backquotes are repurposed for line continuation. A backquote followed by a newline is equivalent to a space. ([#417](https://github.com/elves/elvish/issues/417)) - The signature of the `splits` builtin has been changed. The separator used to be an option `sep` but is now the first argument. For instance, `splits &sep=: a:b:c` should now be written as `splits : a:b:c`. # Notable fixes and enhancements - Thanks to the BoltDB migration, Elvish is now a pure Go project! This allows for fully statically linked executables and easy cross compilation. ([#377](https://github.com/elves/elvish/issues/377)) - Enhancements to the language core: - It is now possible to define options when declaring functions ([#82](https://github.com/elves/elvish/issues/82)). - Interrupting Elvish code with Ctrl-C now works more reliably ([#388](https://github.com/elves/elvish/issues/388)). - Piping value outputs into a command that does not read the value input (e.g. `range 1000 | echo haha`) no longer hangs ([#389](https://github.com/elves/elvish/issues/389)). - New builtins functions and variables (documented in the [builtin module reference](../ref/builtin.html)): - New `assoc` and `dissoc` builtin that outputs modified versions of container types. - New `keys`, `has-keys` and `has-values` builtins ([#432](https://github.com/elves/elvish/issues/432), [#398](https://github.com/elves/elvish/issues/398), [#407](https://github.com/elves/elvish/issues/407)). - A new blackhole variable `$_` has been added. ([#401](https://github.com/elves/elvish/issues/401)) - New `replaces` builtin for replacing strings. ([#463](https://github.com/elves/elvish/issues/463)) - New `not-eq` builtin for inequality. - New `drop` builtin, mirroring `take`. - Enhancements to the editor: - Matching algorithm used in completion is now programmable with `$edit:-matcher` ([#430](https://github.com/elves/elvish/issues/430)); see [documentation](../ref/edit.html). - Elvish can now able to complete arguments with variables. For instance, if you have a directory with `a.mp4` and `a.txt`, and variable `$foo` containing `a`, `echo $foo.` now works ([#446](https://github.com/elves/elvish/issues/446)). However, the completion will expand `$foo` into `a`, which is not intended ([#474](https://github.com/elves/elvish/issues/474)). - It is now possible to manipulate the cursor position using the experimental `$edit:-dot` variable ([415](https://github.com/elves/elvish/issues/415)). - The default prompt now replaces `>` with a red `#` when uid = 0. - An experimental custom listing mode (known as "narrow mode" for now) has been introduced and can be started with `edit:-narrow-read`. This means that it is now to implement listing modes entirely in Elvish script. Experimental re-implementations of the several standard listing modes (location mode, history listing mode and lastcmd mode) are provided as the bundled `narrow` module. Read [its source in eval/narrow.elv](https://github.com/elves/elvish/blob/e7a8b96d7d4fccb7bafe01f27db9c0fe06c568b4/eval/narrow.elv) for more details. - Improvements to the daemon: - The daemon now quits automatically when all Elvish sessions are closed. ([#419](https://github.com/elves/elvish/issues/419)) - The daemon can now spawned be correctly when Elvish is not installed in `PATH`. - Elvish no longer quits on SIGQUIT (usually triggered by `Ctrl-\`), matching the behavior of other shells. It still prints a stack trace though, which can be useful for debugging. ([#411](https://github.com/elves/elvish/issues/411)) - A `-compileonly` flag for the Elvish binary is added. It makes Elvish compiles a script (in memory) but does not execute it. It can be used for checking the well-formedness of programs and is useful in editor plugins. ([#458](https://github.com/elves/elvish/issues/458)) elvish-0.17.0/website/blog/0.11-release-notes.md000066400000000000000000000137751415471104000211350ustar00rootroot00000000000000Version 0.11 has been released four months after 0.10, bringing many new features and bugfixes. There is no newsletter accompanying this release (instead, there is a [tweet](https://twitter.com/RealElvishShell/status/953781788706557952)). As usual, prebuilt binaries can be found in [get](../get/). # Breaking Changes - The syntax for importing modules in nested directories with `use` has changed. Previously, `use` accepts colon-delimited components, and replace the colons with slashes to derive the path: for instance, `use a:b:c` imports the module `a/b/c.elv` under `~/.elvish/lib`, under the namespace `a:b:c`. Now, to import this module, you should use `use a/b/c` instead, and it will import the same file under the namespace `c`. It is not yet possible to rename the module when importing it; this makes it hard to import modules with the same filename living under different directories and will be addressed in the next version. The current implementation of `use` still supports the use of colons to affect the name under which the module is imported: for instance, `use a/b:c` imports the file `a/b/c.elv` under the name `b:c`. However, this feature should be considered undocumented and will be removed in the next version. - Module imports are now scoped lexically, akin to how variables are scoped. For instance, `use re` in one module does not affect other files; neither does `{ use re }` affect the outer scope. - The variable a function `func` maps to is now `$func~` instead of `$&func`. The ampersand `&` is now no longer allowed in variable names, while the tilde `~` is. A [tool](https://github.com/elves/upgrade-scripts-for-0.11) has been provided to rewrite old code. - Strings are no longer callable ([#552](https://github.com/elves/elvish/issues/552)). A new [external](../ref/builtin.html#external) has been added to support calling external programs dynamically. - It is now forbidden to assign non-strings to environment variables. For instance, `E:X = []` used to assign the environment variable `X` the string value `"[]"`; now it causes an exception. # Notable Fixes and Enhancements ## Supported Platforms - Support for Go 1.7 has been dropped, and support for Go 1.9 has been added. - Elvish now has experimental support for Windows 10. Terminal and filesystem features may be buggy. [Prebuilt binaries](../get/) for Windows are also available. - Prebuilt binaries for AMD64 and ARM64 architectures on Linux are provided. ## Language - It is now possible to `use` relative paths. For instance, in module `a/b/c/foo.elv` (under `~/.elvish/lib`), `use ./bar` is the same as `use a/b/c/bar`, and `use ../bar` is the same as `use a/b/bar`. The resolved path must not escape the `lib` directory; for instance, `use ../bar` from `~/.elvish/lib/foo.elv` will throw cause a compilation error. - A new builtin variable, [`$value-out-indicator`](../ref/builtin.html#value-out-indicator), can now be used to customize the marker for value outputs ([#473](https://github.com/elves/elvish/issues/473)). - A new builtin command [`to-string`](../ref/builtin.html#to-string) has been added. - Special forms like `if` now works correctly with redirections and temporary assignments ([#486](https://github.com/elves/elvish/issues/486)). - A primitive for running functions in parallel, [`run-parellel`](../ref/builtin.html#run-parallel), has been added ([#485](https://github.com/elves/elvish/issues/485)). - The [`splits`](../ref/builtin.html#splits) builtin now supports a `&max` option. - A new [`src`](../ref/builtin.html#src) builtin can now be used to get information about the current source. - A new builtin variable [`$args`](../ref/builtin.html#args) can now be used to access command-line arguments. - The [`del`](../ref/language.html#deleting-variable-or-element-del) special command can now delete map elements ([#79](https://github.com/elves/elvish/issues/79)). ## Editor - A maximum wait time can be specified with [\$edit:-prompts-max-wait](../ref/edit.html#edit-prompts-max-wait) to prevent slow prompt functions from blocking UI updates ([#482](https://github.com/elves/elvish/issues/482)). - Execution of hook functions are now correctly isolated ([#515](https://github.com/elves/elvish/issues/515)). - A new [matcher](../ref/edit.html#matcher) `edit:match-substr` has been added. - The editor is now able to handle Alt-modified function keys in more terminals ([#181](https://github.com/elves/elvish/issues/181)). - Location mode now always hides the current directory ([#531](https://github.com/elves/elvish/issues/531)). - Ctrl-H is now treated the same as Backspace ([#539](https://github.com/elves/elvish/issues/539)). - It is now possible to scroll file previews with Alt-Up and Alt-Down. - The height the editor can take up can now be restricted with [`$edit:max-height`](../ref/edit.html#editmax-height). ## Misc - The Elvish command supports a new `-buildinfo` flag, which causes Elvish to print out the version and builder of the binary and exit. Another `-json` flag has also been introduced; when present, it causes `-buildinfo` to print out a JSON object. - Elvish now handles SIGHUP by relaying it to the entire process group ([#494](https://github.com/elves/elvish/issues/494)). - The daemon now detects the path of the Elvish executable more reliabily, notably when Elvish is used as login shell ([#496](https://github.com/elves/elvish/issues/496)). - When an exception is thrown and the traceback contains only one entry, the traceback is now shown more compactly. Before: ```elvish-transcript ~> fail x Exception: x Traceback: [interactive], line 1: fail x ``` Now: ```elvish-transcript ~> fail x Exception: x [tty], line 1: fail x ``` elvish-0.17.0/website/blog/0.12-release-notes.md000066400000000000000000000112231415471104000211200ustar00rootroot00000000000000Version 0.12 has been released six months after 0.11, bringing many new features and bugfixes. As usual, prebuilt binaries can be found in [get](../get). # Breaking Changes - The `shared:` namespace has been removed. - Line continuations now use backslashes instead of backquotes, in line with POSIX syntax. - The `resolve` builtin now returns a string. - The variables `$edit:loc-{pinned,hidden}` have been moved into the `edit:location:` namespace, now `$edit:location:{pinned,hidden}` - The variable `$edit:arg-completer` has been moved to `$edit:completion:arg-completer`. # Notable Fixes and Enhancements - The [Elvish package manager](../ref/epm.html) has landed (thanks to @zzamboni!). - A `str:` module has been added ([#576](https://github.com/elves/elvish/issues/576)). - Styling of the web interface (`elvish -web`) has been reworked, now featuring a dark theme as well as a light theme. - Namespaces can now be accessed by as variables with a trailing `:` in the name (e.g. the `edit:` namespace variable can be accessed as `$edit:`). These variables can be indexed like maps ([#492](https://github.com/elves/elvish/issues/492), ([#631](https://github.com/elves/elvish/issues/631))). - Support for urxvt-style key sequences has been improved ([#579](https://github.com/elves/elvish/issues/579)). - Numbers can now be used as normal variable names (e.g. `$1`). - The interactive namespace can now be built dynamically by assigning to the `$-exports-` variable in `rc.elv` ([#613](https://github.com/elves/elvish/issues/613)). - Closures can now be introspected ([#560](https://github.com/elves/elvish/issues/560), [#617](https://github.com/elves/elvish/issues/617)). - The variable for customizing matchers in completion mode has graduated from `$edit:-matcher` to `$edit:completion:matcher`. - The `joins` command no longer ignores leading empty values ([#616](https://github.com/elves/elvish/issues/616)). - The `while` special command no longer swallows exceptions ([#615](https://github.com/elves/elvish/issues/615)). - The `finally` block of the `try` special command no longer swallows exceptions ([#619](https://github.com/elves/elvish/issues/619)). - A set of builtin commands for manipulating environment variables - `has-env`, `get-env`, `set-env`, `unset-env` - has been added. - The prompts are now rendered asynchronously. The appearance of [stale prompts](../ref/edit.html#stale-prompt) can be customized. - Experimental support for customizing the [eagerness of prompts](https://elv.sh/ref/edit.html#prompt-eagerness). - Elvish now writes a `\r` to the terminal before suspending the editor ([#629](https://github.com/elves/elvish/issues/629); thanks to @krader1961 for the analysis!). - New `edit:history:fast-forward` command to import command history after the current session started. - The completion mode no longer completes the longest common prefix ([#637](https://github.com/elves/elvish/issues/637)). - New `store:del-dir` command for deleting directory history. - Add chdir hooks [`$before-chdir`](../ref/builtin.html#before-chdir) and [`$after-chdir`](../ref/builtin.html#after-chdir). - Location mode now supports the notion of workspaces ([#643](https://github.com/elves/elvish/issues/643)). - The output of `elvish -buildinfo -json` is now actually valid JSON ([#682](https://github.com/elves/elvish/issues/682)). - New [`styled`](../ref/builtin.html#styled) and [`styled-segment`](../ref/builtin.html#styled-segment) commands (thanks to @fehnomenal!) ([#520](https://github.com/elves/elvish/issues/520), [#674](https://github.com/elves/elvish/issues/674)). - New builtin `$notify-bg-job-success` variable for suppressing notification of the success of background jobs (thanks to @iwoloschin!) ([#689](https://github.com/elves/elvish/issues/689)). - New builtin `$num-bg-jobs` variable for tracking number of background jobs ([#692](https://github.com/elves/elvish/issues/692)). - The `edit:complete-getopt` command now supports supplying a description for arguments of options (thanks to @zzamboni!) ([#693](https://github.com/elves/elvish/issues/693)). - Complex candidates built with `edit:complex-candidate` are now indexable (thanks to @zzamboni!) ([#691](https://github.com/elves/elvish/issues/691)). - New `-norc` flag for skipping `rc.elv` (thanks to @iwoloschin!) ([#707](https://github.com/elves/elvish/issues/707)). - Elvish now guards against commands messing up terminal attributes ([#706](https://github.com/elves/elvish/issues/706)). elvish-0.17.0/website/blog/0.13-release-notes.md000066400000000000000000000126551415471104000211330ustar00rootroot00000000000000Version 0.13 has been released on 2020-01-01, 18 months after 0.12, bringing many new features and bugfixes. As usual, [prebuilt binaries](https://elv.sh/get) are offered for most common platforms. # Breaking changes - This release sees a total rewrite of the line editor. As a result, there have been some changes to its API, the `edit:` module: - Binding tables no longer support the `default` key for overriding the default behavior of modes. All the `edit::default` functions have been removed: `edit:completion:default`, `edit:history:default`, `edit:insert:default`, `edit:listing:default` and `edit:navigation:default`. The `edit:insert-key` and `edit:listing:backspace` functions have also been removed. Their functionalities are now baked into the default behavior of the insert and listing modes. - The `edit:history:list` function has been removed. Use `edit:command history` instead. - The `edit:lastcmd:accept-line` function has been removed. Use `edit:listing:accept` instead. - The `edit:-narrow-read` function and the `edit:narrow:` module have been removed. Used `edit:listing:start-custom` instead. - The `edit:styled` function has been removed. Used `styled` instead. - The `edit:insert:start` function has been removed. Use `edit:close-listing` instead. - The `edit:location:matcher` variable and `edit:location:match-dir-pattern` function have been removed. There is no replacement yet; the location matcher is not customizable now, although it may be made customizable again in a future version. - The `edit:completion:trigger-filter` function has been removed. The completion mode now always focuses on the filter, and it is no longer possible to focus on the main buffer during completion. - The `edit:history:list` function has been removed. There is no replacement yet. - The names of basic colors used in `styled` has changed to be more standard: - The `lightgray` color (ANSI code 37) is now called `white`. - The `gray` color (ANSI code 90) is now called `bright-black`. - The `white` color (ANSI code 97) is now called `bright-white`. - All the `lightX` (ANSI codes 90 to 97) colors have been renamed to `bright-X`. - Builtin math functions now output values of an explicit `float64` number type instead of strings. # Notable fixes and enhancements - The editor now has a minibuffer, bound to Alt-x by default. The minibuffer allows you to execute editor commands without binding them to a key. - The editor now has an experimental "instant mode" that can be activated with `edit:-instant:start`. It is not bound by default. The instant mode executes the code on the command line every time it changes. **WARNING**: Beware of unintended consequences when using destructive commands. For example, if you type `sudo rm -rf /tmp/*` in instant mode, Elvish will attempt to execute `sudo rm -rf /` when you typed so far. - The `styled` builtin now supports more color spaces: - Colors from the xterm 256-color palette can be specified as `colorN`, such as `color22`. - 24-bit RGB colors can be specified as `#RRGGBB`, such as `#00ffa0`. Proper terminal support is required to display those colors. - Elvish can now output results in JSON in compile-only mode, by specifying `-compileonly -json`, thanks to @jiujieti ([PR #858](https://pr.elv.sh/858)) and @sblundy ([PR #874](https://pr.elv.sh/874)). - In redirections, the 3 standard file descriptors may be specified as names (`stdin`, `stdout`, `stderr` ) instead of numbers, thanks to @jiujieti ([PR #869](https://pr.elv.sh/869)). - Code such as `x = $x` where `$x` has not been defined now correctly results in a compilation error, thanks to @jiujieti ([PR #872](https://pr.elv.sh/872)). - The `while` special form now supports an `else` clause, thanks to @0x005c ([PR #863](https://pr.elv.sh/863)). This feature was previously documented but missing implementation. - The command `%` no longer crashes Elvish when the divisor is 0, thanks to @0x005c ([PR #866](https://pr.elv.sh/866)). - Elvish is now resilient against terminal programs that leave the terminal in non-blocking IO state ([issue #588](https://b.elv.sh/588) and [issue #822](https://b.elv.sh/822)). - Wildcard patterns of multiple question marks (like `a??`) are now parsed correctly ([issue #848](https://b.elv.sh/848)). - A new floating-point numeric type has been introduced, and can be constructed with the `float64` builtin function ([issue #816](https://b.elv.sh/816)). - A new `$nil` value has been introduced to represent lack of meaningful values. JSON `null` values are converted to Elvish `$nil`. - Two new builtins, `only-bytes` and `only-values` have been introduced. They can read a mixture of byte and value inputs and only keep one type and discard the other type. - The `use` special form now accepts an optional second argument for renaming the imported module. - A new `chr` builtin that converts a number to its corresponding Unicode character has been added. - New editor builtin commands `edit:kill-word-right` and `edit:kill-word-right` has been added, thanks to @kwshi ([PR #721](https://pr.elv.sh/721)). elvish-0.17.0/website/blog/0.13.1-release-notes.md000066400000000000000000000011701415471104000212600ustar00rootroot00000000000000Version 0.13.1 has been released on 2020-03-21, less than 3 months after 0.13. This is a bugfix release. If you upgraded from 0.12, please also read the [0.13 release notes](0.13-release-notes.html). As usual, [prebuilt binaries](https://elv.sh/get) are offered for most common platforms. The following bugs have been fixed: [#883](https://b.elv.sh/883), [#891](https://b.elv.sh/891), [#852](https://b.elv.sh/852), [#788](https://b.elv.sh/788). A single new feature has been introduced: the `edit:redraw` function now accepts a `full` option. When true, it forces Elvish to do a full redraw, as opposed to an incremental redraw. elvish-0.17.0/website/blog/0.14.0-release-notes.md000066400000000000000000000136501415471104000212660ustar00rootroot00000000000000Version 0.14.0 has been released on 2020-07-05, 6 months after 0.13, bringing many new features and bugfixes. As usual, [prebuilt binaries](https://elv.sh/get) are offered for most common platforms. # Breaking changes - The `type` field of the value written by `src` have been removed. - The `all` command no longer preserves byte inputs as is; instead it turns them into values, one each line. It also accepts an optional list argument, consistent with other value-taking commands. - Output captures now strip trailing carriage returns from each line, effectively making `\r\n` accepted as a line separator ([#970](https://b.elv.sh/970)). - Map-like values written by the `dir-history` and `re:find` functions can no longer be assoc'ed. # Deprecated features Elvish now has a deprecation mechanism to give advance notice for breaking changes. Deprecated features trigger warnings, and will be removed in the next release. The following deprecated features trigger a warning whenever the code is parsed or compiled, even if it is not executed: - The `explode` command is now deprecated. Use `all` instead. - The `joins`, `replaces` and `splits` commands are now deprecated. Use `str:join`, `str:replace` and `str:split` instead. - The `^` command is now deprecated. Use `math:pow` instead. - The `-time` command has been promoted to `time`. The `-time` command is now a deprecated alias for `time`. - Using `\` for line continuation is now deprecated. Use `^` instead. The following deprecated features trigger a warning when the code is evaluated: - The `&display-suffix` option of the `edit:complex-candidate` is now deprecated. Use the `&display` option instead. The following deprecated features, unfortunately, do not trigger any warnings: - The `path` field of the value returned by `src` is now deprecated. Use the `name` field instead. # Notable new features New features in the language: - Exceptions can now be introspected by accessing their fields ([#208](https://b.elv.sh/208)). - Two new wildcard modifiers, `type:dir` and `type:regular` are added, which restricts the wildcard pattern to only match directories or regular files, respectively. - The printing of floating-point numbers has been tweaked to feel much more natural ([#811](https://b.elv.sh/811)). - Scripts may now use relative `use` to import modules outside `~/.elvish/lib`. - Dynamic strings may now be used as command as long as they contain slashes ([#764](https://b.elv.sh/764)). - Elvish now supports CRLF line endings in source files ([#918](https://b.elv.sh/918)). - Comments are now allowed anywhere newlines serve as separators, notably inside list and map literals ([#924](https://b.elv.sh/924)). - The `^` character can now be used for line continuation. New features in the standard library: - A new `order` command for sorting values has been introduced [#651](https://b.elv.sh/651). - A new `platform:` module has been introduced. - A new `unix:` module has been introduced. - A new `math:` module has been introduced. - The `fail` command now takes an argument of any type. In particular, if the argument is an exception, it rethrows the exception ([#941](https://b.elv.sh/941)). - A new `show` command has been added, which is currently useful for printing the stack trace of an exception to the terminal. - A new `make-map` command creates a map from a sequence of pairs ([#943](https://b.elv.sh/943)). - A new `read-line` command can be used to read a single line from the byte input ([#975](https://b.elv.sh/975)). - The `-time` command has been promoted to `time`, and it now accepts an `&on-end` callback to specify how to save the duration of the execution ([#295](https://b.elv.sh/295)). - A new `one` command has been added. - A new `read-upto` command can now be added to read byte inputs up to a delimiter ([#831](https://b.elv.sh/831)). New features in the interactive editor: - When a callback of the interactive editor throws an exception, the exception is now saved in a `$edit:exceptions` variable for closer examination ([#945](https://b.elv.sh/945)). - A new alternative abbreviation mechanism, "small word abbreviation", is now available and configurable via `$edit:small-word-abbr`. - The ratios of the column widths in navigation mode can now be configured with `$edit:navigation:width-ratio` ([#464](https://b.elv.sh/464)) - A new `$edit:add-cmd-filters` variable is now available for controlling whether a command is added to the history. The default value of this variable filters out commands that start with a space. - The `edit:complex-candidate` now supports a `&display` option to specify the full display text. Other improvements: - Elvish now uses `$XDG_RUNTIME_DIR` to keep runtime files if possible. - Elvish now increments the `$SHLVL` environment variable ([#834](https://b.elv.sh/834)). # Notable bugfixes - Invalid option names or values passed to builtin functions now correctly trigger an exception, instead of being silently ignored ([#958](https://b.elv.sh/958)). - Elvish no longer crashes when redirecting to a high FD ([#788](https://b.elv.sh/788)). - Indexing access to nonexistent variables now correctly triggers a compilation error ([#889](https://b.elv.sh/889)). - The interactive REPL no longer highlights complex commands as red ([#881](https://b.elv.sh/881)). - Glob patterns after `~username` now evaluate correctly ([#793](https://b.elv.sh/793)). - On Windows, tab completions for directories no longer add superfluous quotes backslashes ([#897](https://b.elv.sh/897)). - The `edit:move-dot-left-small-word` command has been fixed to actually move by a small word instead of a word. - A lot of race conditions have been fixed ([#73](https://b.elv.sh), [#754](https://b.elv.sh/754)). elvish-0.17.0/website/blog/0.14.1-release-notes.md000066400000000000000000000007151415471104000212650ustar00rootroot00000000000000Version 0.14.1 has been released on 2020-08-16, a month and a half after 0.14.0. This is a bugfix release. If you upgraded from 0.13.x, please also read the [0.14.0 release notes](0.14.0-release-notes.html). As usual, [prebuilt binaries](https://elv.sh/get) are offered for most common platforms. This release fixes the lack of deprecation warnings in [imported modules](https://b.elv.sh/1072) and when using the `explode` builtin. There are no new features. elvish-0.17.0/website/blog/0.15.0-release-notes.md000066400000000000000000000077471415471104000213010ustar00rootroot00000000000000Version 0.15.0 has been released on 2021-01-30, 6 months after 0.14.0, bringing many new features and bugfixes. As usual, [prebuilt binaries](https://elv.sh/get) are offered for most common platforms. # Breaking changes - Builtin functions and subnamespaces of `edit:` are now read-only. - Introspection for rest arguments has changed: - The rest argument is now contained in the `arg-names` field of a closure. - The `rest-arg` field now contains the index of the rest argument, instead of the name. - The `-source` command now runs in a temporary namespace and can no longer affect the local scope of its caller. - Key modifiers are no longer case insensitive. For example, `Alt` is still recognized but `alt` is not. This makes key modifier parsing consistent with key names. See [#1163](https://b.elv.sh/1163). # Deprecated features Deprecated features will be removed in 0.16.0. The following deprecated features trigger a warning whenever the code is parsed or compiled, even if it is not executed: - Using the syntax of temporary assignment (`var=value`) for non-temporary assignment is now deprecated. The syntax is still valid for temporary assignment. For example, using `foo=bar` as a standalone command is deprecated, but using it as part of command, like `foo=bar ls`, is not deprecated. - The `chr` command is now deprecated. Use `str:from-codepoints` instead. - The `ord` command is now deprecated. Use `str:to-codepoints` instead. - The `has-prefix` command is now deprecated. Use `str:has-prefix` instead. - The `has-suffix` command is now deprecated. Use `str:has-suffix` instead. - The `-source` command is now deprecated. Use `eval` instead. - The undocumented `esleep` command is now deprecated. Use `sleep` instead. - The `eval-symlinks` command is deprecated. Use `path:eval-symlinks` instead. - The `path-abs` command is deprecated. Use `path:abs` instead. - The `path-base` command is deprecated. Use `path:base` instead. - The `path-clean` command is deprecated. Use `path:clean` instead. - The `path-dir` command is deprecated. Use `path:dir` instead. - The `path-ext` command is deprecated. Use `path:ext` instead. - The `-is-dir` command is deprecated. Use `path:is-dir` instead. The following deprecated features trigger a warning when the code is evaluated: - Using `:` in slice indices is deprecated. Use `..` instead. - The mechanism of assigning to `$-exports-` in `rc.elv` to export variables to the REPL namespace is deprecated. Use `edit:add-vars` instead. # Notable new features New features in the language: - A new `var` special command can be used to explicitly declare variables, and optionally assign them initial values. - A new `set` special command can be used to set the values of variables or elements. - Slice indices can now use `..` for left-closed, right-open ranges, and `..=` for closed ranges. - Rest variables and rest arguments are no longer restricted to the last variable. - Variables containing any character can now be assigned and used by quoting their name, for example `'name!' = foo; put $'name!'`. New features in the standard library: - A new `eval` command supports evaluating a dynamic piece of code in a restricted namespace. - A new `sleep` command. - A new `path:` module has been introduced for manipulating and testing filesystem paths. - A new `deprecate` command. New features in the interactive editor: - The new commands `edit:add-var` and `edit:add-vars` provide an API for manipulating the REPL's namespace from anywhere. - SGR escape sequences written from the prompt callback are now supported. New features in the main program: - When using `-compileonly` to check Elvish sources that contain parse errors, Elvish will still try to compile the source code and print out compilation errors. # Notable bugfixes - Using large lists that contain `$nil` no longer crashes Elvish. elvish-0.17.0/website/blog/0.16.0-release-notes.md000066400000000000000000000112221415471104000212610ustar00rootroot00000000000000Version 0.16.0 has been released on 2021-08-21, 6 months after 0.15.0, bringing many new features and bugfixes. As usual, [prebuilt binaries](https://elv.sh/get) are offered for most common platforms. **Note**: Versions 0.16.1, 0.16.2 and 0.16.3 were released to fix some packaging issues. They are otherwise identical to 0.16.0. # Breaking changes - Exceptions caused by a command trying to write to a closed pipe are now suppressed if the command is part of a pipeline and not the last command of the pipeline. - The `builtin:` namespace, useful for referring to builtin variables and commands explicitly, now requires `use builtin` before use, consistent with other standard library modules. - As a side effect of support for a integer numbers, contexts that require integers no longer accept floating point numbers with a zero fraction part (e.g. `$li[1.0]` is now illegal; `$li[1]` is required). - The following commands are now replaced by `edit:close-mode`: `edit:close-listing`, `edit:completion:close`, `edit:history:close`, `edit:listing:close`. - The `edit:histlist:toggle-case-sensitivity` command has been removed. Instead, the history listing mode now applies smart-case matching by default. - Declaring a variable with a `~` suffix, without an explicit initial value, now initializes its value to the builtin `nop` function rather than `$nil` ([#1248](https://b.elv.sh/1248)). # Deprecated features Deprecated features will be removed in 0.17.0. The following deprecated features trigger a warning whenever the code is parsed or compiled, even if it is not executed: - The `fopen` and `fclose` commands are deprecated. Use `file:open` and `file:close` instead. - The `prclose` and `pwclose` commands are deprecated. Use `file:close` instead. The following deprecated features unfortunately doesn't trigger any warnings: - The `math:pow10` command is deprecated. Use `math:pow 10 $exponent` instead. # Notable bugfixes - Iterating over certain list slices no longer crash Elvish ([#1287](https://b.elv.sh/1287)). - Globbing no longer crashes when there are files whose names contain invalid UTF-8 sequences ([#1220](https://b.elv.sh/1220)). - The `path:is-dir` and `path:is-regular` commands default behavior no longer follows a final symlink as advertised in the original documentation. A `&follow-symlink` option has been added to get the old, undocumented, behavior since it can be useful and avoids the need to use `path:eval-symlinks` to transform the path in common use cases. * Evaluating `~username` no longer appends a slash ([#1246](https://b.elv.sh/1246)). # Notable new features New features in the language: - Elvish's number type has been extended with support for arbitrary-precision integers and rationals. Many numerical commands in the builtin module and the `math:` module have been extended with support for them. - Experimental support for importing modules written in Go with `use`. New features in the standard library: - A new `file:` module contains utilities for manipulating files. - Commands for creating temporary files and directories, `path:temp-file` and `path:temp-dir` ([#1255](https://b.elv.sh/1255)). - New options to the `edit:command-history` command: `&dedup`, `&newest-first`, and `&cmd-only` ([#1053](https://b.elv.sh/1053)). - New `from-terminated` and `to-terminated` commands to allow efficient streaming of byte sequences terminated by ASCII NUL or any other terminator ([#1070](https://b.elv.sh/1070)). New features in the interactive editor: - The editor now supports setting global bindings via `$edit:global-binding`. Global bindings are consulted for keys not present in mode-specific bindings. - A new `edit:clear` builtin to clear the screen has been added. - The editor now uses a DSL for filtering items in completion, history listing, location and navigation modes. - A new `edit:after-command` hook that is invoked after each interactive command line is run ([#1029](https://b.elv.sh/1029)). - A new `edit:command-duration` variable that is the number of seconds to execute the most recent interactive command line ([#1029](https://b.elv.sh/1029)). New features in the command behavior: - Elvish now follows the XDG directory spec for placing the database and searching for `rc.elv` and libraries ([#383](https://b.elv.sh/383)). The legacy directory `~/.elvish` is still respected for now, but may issue deprecation warnings in a future version. The exact paths are documented in the page for [the Elvish command](https://elv.sh/ref/command.html). elvish-0.17.0/website/blog/0.9-release-notes.md000066400000000000000000000072661415471104000210620ustar00rootroot00000000000000Version 0.9 has been released to coincide with the official publication of the Elvish website, which will be hosting all release notes in the future. This version is released slightly more than a month after 0.8. Despite the short interval, there are some interesting additions and changes. # Breaking Changes - Lists have become immutable. Support for assigning individual list elements has been temporarily removed. For instance, the following is no longer possible: ```elvish li = [lorem ipsum foo bar] li[1] = not-ipsum ``` You need to use this for now: ```elvish li = [(explode $li[:1]) not-ipsum (explode $li[2:])] ``` Element assignment will be reintroduced as a syntax sugar, after the conversion to persistent data structure is finished. Assignments to map elements are not affected. - The `true` and `false` builtin commands have been removed. They have been equivalent to `put $true` and `put $false` for a while. - The default keybinding for last command mode has been changed from Alt-, to Alt-1. - The "bang mode" is now known as "last command mode". - The `le:` module (for accessing the Elvish editor) has been renamed to `edit:`. You can do a simple substitution `s/le:/edit:/g` to fix your `rc.elv`. - The 3 listing modes -- location mode, history listing mode and last command mode -- are being merged into one generic listing mode. Most of their builtins have been merged: for instance, they use to come with their own builtins for changing focused candidate, `le:loc:up`, `le:histlist:up` and `le:bang:up`. These have been merged into simply `edit:listing:up`, that operates on whichever listing mode is active. A new binding table, `$edit:binding[listing]` has also been introduced. Bindings put there will be available in all 3 listing modes, with bindings in their own tables (`$edit:binding[loc]`, `$edit:binding[histlist]` and `$edit:binding[lastcmd]`) having higher precedence. - The readline-style binding module has been renamed from `embedded:readline-binding` to just `readline-binding`. Future embedded modules will no longer have an `embedded:` prefix either. # Notable Fixes and Enhancements - This release has seen more progress towards breaking up the huge, untested [edit](https://github.com/elves/elvish/tree/master/edit) package. For instance, the syntax highlighter and command history helpers now live in their own packages, and have better test coverages. - An experimental web interface has been added. It can be used by supplying the `-web` flag when running Elvish, i.e. `elvish -web`. The default port is 3171, which is [a way](https://en.wikipedia.org/wiki/Leet) to write "ELVI". An alternative port can be specified using `-port`, e.g. `elvish -web -port 2333`. - Per-session command history has been reintroduced ([#355](https://github.com/elves/elvish/issues/355)). - Elvish now forks a daemon for mediating access to the database. This is to prepare for the switch to a pure Go database and removing the current C dependency on SQLite. A new `daemon:` module has been introduced. - A new `edit:complex-candidate` builtin has been introduced to construct complex candidates from completers. - A new `re:` module, containing regular expression utilities, has been introduced. # Known Issues The daemon implementation has a known issue of some intermediate process not being reaped correctly and there is an outstanding [pull request](https://github.com/elves/elvish/pull/373/) for it. In the worst case, this will leave 2 processes hanging in the system. elvish-0.17.0/website/blog/index.toml000066400000000000000000000026051415471104000174620ustar00rootroot00000000000000prelude = "prelude" autoIndex = true [[articles]] name = "0.16.0-release-notes" title = "Elvish 0.16.0 Release Notes" timestamp = "2021-08-21" [[articles]] name = "0.15.0-release-notes" title = "Elvish 0.15.0 Release Notes" timestamp = "2021-01-30" [[articles]] name = "0.14.1-release-notes" title = "Elvish 0.14.1 Release Notes" timestamp = "2020-08-16" [[articles]] name = "0.14.0-release-notes" title = "Elvish 0.14.0 Release Notes" timestamp = "2020-07-05" [[articles]] name = "0.13.1-release-notes" title = "Elvish 0.13.1 Release Notes" timestamp = "2020-03-21" [[articles]] name = "0.13-release-notes" title = "Elvish 0.13 Release Notes" timestamp = "2020-01-01" [[articles]] name = "0.12-release-notes" title = "Elvish 0.12 Release Notes" timestamp = "2018-08-01" [[articles]] name = "0.11-release-notes" title = "Elvish 0.11 Release Notes" timestamp = "2018-01-17" [[articles]] name = "newsletter-sep-2017" title = "Elvish Newsletter, Sep 2017 Issue" timestamp = "2017-09-17" [[articles]] name = "0.10-release-notes" title = "Elvish 0.10 Release Notes" timestamp = "2017-09-17" [[articles]] name = "newsletter-july-2017" title = "Elvish Newsletter, July 2017 Issue" timestamp = "2017-07-10" [[articles]] name = "0.9-release-notes" title = "Elvish 0.9 Release Notes" timestamp = "2017-07-03" [[articles]] name = "live" title = "The Elvish Website is Officially Live!" timestamp = "2017-07-03" elvish-0.17.0/website/blog/live.md000066400000000000000000000030301415471104000167300ustar00rootroot00000000000000After being "in construction" for ages, the Elvish website is now officially live. It is (still) not complete yet, but in the spirit of "release early", here it is :) This website will be the entry point of all Elvish information. It will host documents (including both [learning materials](../learn/) and [references](../ref/)), as well as release notes or other announcements. The [GitHub repository](https://github.com/elves/elvish) continues to host code and issues, of course. The website has no comment facilities: commenting is outsourced to [Hacker News](https://news.ycombinator.com) and [r/elv](https://www.reddit.com/r/elv/). The [homepage](../) introduces each section, and highlights some key features of Elvish with "ttyshots". Please start there to explore this website! # Technical Stack The website is built with a [static website generator](https://github.com/xiaq/genblog) from a bunch of [MarkDown, configuration files and messy scripts](https://github.com/elves/elvish.io), and hosted on a [DigitalOcean](https://www.digitalocean.com) server with [nginx](http://nginx.org). Thanks to [Cloudflare](https://www.cloudflare.com), this website has a good chance of withstanding kisses of death from Hacker News, should there be any. It uses Cloudflare's "strict" SSL configuration, meaning that both traffic from you to Cloudflare and Cloudflare to the origin server are fully encrypted with verified certificates. The origin server obtains and renews its certificate from the wonderful [Let's Encrypt](https://letsencrypt.org/) service. elvish-0.17.0/website/blog/newsletter-july-2017.md000066400000000000000000000124531415471104000215460ustar00rootroot00000000000000Welcome to the first issue of Elvish Newsletter! Elvish is a shell that seeks to combine a full-fledged programming language with a friendly user interface. This newsletter is a summary of its progress and future plans. # Status Updates - 18 pull requests to the [main repo](https://github.com/elves/elvish) have been merged in the past four weeks. Among them 13 were made by @xofyargs, and the rest by @myfreeweb, @jiujieti, @HeavyHorst, @silvasur and @ALSchwalm. The [website repo](https://github.com/elves/elvish.io) has also merged 3 pull requests from @bengesoff, @zhsj and @silvasur. Many kudos! - The [website](https://elvish.io) was [officially live](../blog/live.html) on 3 July. Although the initial [submission](https://news.ycombinator.com/item?id=14691639) to HN was a failure, Elvish gained [quite](https://www.reddit.com/r/programming/comments/6l38nd/elvish_friendly_and_expressive_shell/) [some](https://www.reddit.com/r/golang/comments/6l3aev/elvish_friendly_and_expressive_shell_written_in_go/) [popularity](https://www.reddit.com/r/linux/comments/6l6wcs/elvish_friendly_and_expressive_shell_now_ready/) on Reddit, and [another](https://news.ycombinator.com/item?id=14698187) HN submission made to the homepage. These, among others, have brought 40k unique visitors to the website, totalling 340k HTTP requests. Thank you Internet :) - A lot of discussions have happened over the IM channels and the issue tracker, and it has become necessary to better document the current status of Elvish and organize the development effort, and this newsletter is part of the response. There is no fixed schedule yet, but the current plan is to publish newsletters roughly every month. Preview releases of Elvish, which used to happen quite arbitrarily, will also be done to coincide with the publication of newsletters. - There are now IM channels for developers, see below for details. # Short-Term and Mid-Term Plans The next preview release will be 0.10, and there is now a [milestone](https://github.com/elves/elvish/milestone/2) for it, a list of issues considered vital for the release. If you would like to contribute, you are more than welcome to pick an issue from that list, although you are also more than welcome to pick just any issue. Aside from the short-term goal of releasing 0.10, here are the current mid-term focus areas of Elvish development: - Stabilizing the language core. The core of Elvish is still pretty immature, and it is definitely not as usable as any other dynamic language, say Python or Clojure. Among others, the 0.10 milestone now plans changes to the implementation of maps ([#414](https://github.com/elves/elvish/issues/414)), a new semantics of element assignment ([#422](https://github.com/elves/elvish/issues/422)) and enhanced syntax for function definition ([#82](https://github.com/elves/elvish/issues/82) and [#397](https://github.com/elves/elvish/issues/397)). You probably wouldn't expect such fundamental changes in a mature language :) A stable language core is a prerequisite for a 1.0 release. Elvish 1.x will maintain backwards compatibility with code written for earlier 1.x versions. - Enhance usability of the user interface, and provide basic programmability. The goal is to build a fully programmable user interface, and there are a lot to be done. Among others, the 0.10 milestone plans to support manipulating the cursor ([#415](https://github.com/elves/elvish/issues/415)) programmatically, scrolling of previews in navigation mode previews ([#381](https://github.com/elves/elvish/issues/381)), and invoking external editors for editing code ([#393](https://github.com/elves/elvish/issues/393)). The user interface is important for two reasons. Enhancements to the UI can improve the power of Elvish directly and significantly; its API is also a very good place for testing the language. By developing the language and the user interface in parallel, we can make sure that they work well together. Like many other open source projects, you are welcome to discuss and challenge the current plan, or come up with your ideas regarding the design and implementation. (So what's the long-term goal of Elvish? The long-term goal is to remove the "seeks to" part from the introduction of Elvish at the beginning of the post.) # Development IM Channels To better coordinate development, there are now IM channels for Elvish development: [#elvish-dev](http://webchat.freenode.net/?channels=elvish-dev) on freenode, [elves/elvish-dev](https://gitter.im/elves/elvish-dev) on Gitter and [@elvish_dev](https://telegram.me/elvish_dev) on Telegram. These channels are all connected together thanks to [fishroom](https://github.com/tuna/fishroom). For general questions, you are welcome in [#elvish](https://webchat.freenode.net/?channels=elvish) on Freenode, [elves/elvish-public](https://gitter.im/elves/elvish-public) on Gitter, or [@elvish](https://telegram.me/elvish) on Telegram. # Conclusion This concludes this first issue of the newsletter. Hopefully future issues of this newsletter will also feature blog posts from Elvish users like _Elvish for Python Users_ and popular Elvish modules like _Tetris in Your Shell_ :) Have Fun with Elvish! \- xiaq elvish-0.17.0/website/blog/newsletter-sep-2017.md000066400000000000000000000061761415471104000213570ustar00rootroot00000000000000Welcome to the second issue of Elvish Newsletter! Elvish is a shell that seeks to combine a full-fledged programming language with a friendly user interface. This newsletter is a summary of its progress and future plans. # Release of 0.10 Version This newsletter accompanies the release of the 0.10 version. This release contains 125 commits, with contributions from @xofyarg, @tw4452852, @ALSchwalm, @zhsj, @HeavyHorst, @silvasur, @zzamboni, @Chilledheart, @myfreeweb, @xchenan and @jiujieti. Elvish used to depend on SQLite for storage. As a result, compiling Elvish relied on cgo and required a C compiler. This release sees the switch to BoltDB, making Elvish a pure-Go project. Elvish can now be compiled much faster, and into a fully statically linked binary. Cross-compilation is also much easier, as the Go compiler has fantastic cross-compiling support. Maps (`[&k=v &k2=v2]`) are now implemented using persistent hash maps. This concludes the transition to persistent data structures for all primary data types (strings, lists, maps). Persistent data structures are immutable, and thus have a simpler semantics and are automatically concurrency-safe. This does have an interesting impact on the semantics of assignments, which is now documented in a new section on the [unique semantics](../learn/unique-semantics.html) page. For a complete list of changes, see the [release notes](0.10-release-notes.html). # Community - We now have an official list of awesome unofficial Elvish libraries: [elves/awesome-elvish](https://github.com/elves/awesome-elvish). Among others, we now have at least two very advanced prompt themes, chain.elv from @zzamboni and powerline.elv from @muesli :) - Diego Zamboni (@zzamboni), the author of chain.elv, has written very passionately on Elvish: [Elvish, an awesome Unix shell](http://zzamboni.org/post/elvish-an-awesome-unix-shell/). - Patrick Callahan has given an awesome talk on [Delightful Command-Line Experiences](https://dl.elvish.io/resources/callahan-delightful-commandline-experiences.pdf), featuring Elvish as a "very lively, ambitious shell". - Elvish is now [packaged](https://packages.debian.org/elvish) in Debian. - The number of followers to [@RealElvishShell](https://twitter.com/RealElvishShell/) has grown to 23. # Plans The mid-term remains the same as in the [previous issue](newsletter-july-2017.html): stabilizing the language core and enhancing usability of the user interface. The short-term plan is captured in the [milestone](https://github.com/elves/elvish/milestone/3) for the 0.11 version. Among other things, 0.11 is expected to ship with `epm`, [the standard package manager](https://github.com/elves/elvish/issues/239) for Elvish, and a more responsive interface by running [prompts](https://github.com/elves/elvish/issues/482) and [completions](https://github.com/elves/elvish/issues/483) asynchronously. Stay very tuned. # Conclusions In the last newsletter, I predicted that we will be featuring _Elvish for Python Users_ and _Tetris in Your Shell_ in a future newsletter. It seems we are getting close to that pretty steadily. Have fun with Elvish! \- xiaq elvish-0.17.0/website/blog/prelude.md000066400000000000000000000002211415471104000174300ustar00rootroot00000000000000The draft release notes for the next release can be found [here on GitHub](https://github.com/elves/elvish/blob/master/0.17.0-release-notes.md). elvish-0.17.0/website/cmd/000077500000000000000000000000001415471104000152735ustar00rootroot00000000000000elvish-0.17.0/website/cmd/elvdoc/000077500000000000000000000000001415471104000165475ustar00rootroot00000000000000elvish-0.17.0/website/cmd/elvdoc/main.go000066400000000000000000000133431415471104000200260ustar00rootroot00000000000000package main import ( "bufio" "flag" "fmt" "html" "io" "log" "net/url" "os" "path/filepath" "sort" "strings" ) func main() { run(os.Args[1:], os.Stdin, os.Stdout) } func run(args []string, in io.Reader, out io.Writer) { flags := flag.NewFlagSet("", flag.ExitOnError) var ns = flags.String("ns", "", "namespace prefix") err := flags.Parse(args) if err != nil { log.Fatal(err) } args = flags.Args() if len(args) > 0 { extractPaths(args, *ns, out) } else { extract(in, *ns, out) } } func extractPaths(paths []string, ns string, out io.Writer) { var files []string for _, path := range paths { stat, err := os.Stat(path) if err != nil { log.Fatal(err) } if stat.IsDir() { goFiles := mustGlob(filepath.Join(path, "*.go")) files = append(files, goFiles...) elvFiles := mustGlob(filepath.Join(path, "*.elv")) files = append(files, elvFiles...) } else { files = append(files, path) } } reader, cleanup, err := multiFile(files) if err != nil { log.Fatal(err) } defer cleanup() extract(reader, ns, out) } func mustGlob(p string) []string { files, err := filepath.Glob(p) if err != nil { log.Fatal(err) } return files } // Makes a reader that concatenates multiple files. func multiFile(names []string) (io.Reader, func(), error) { readers := make([]io.Reader, len(names)) closers := make([]io.Closer, len(names)) for i, name := range names { file, err := os.Open(name) if err != nil { for j := 0; j < i; j++ { closers[j].Close() } return nil, nil, err } readers[i] = file closers[i] = file } return io.MultiReader(readers...), func() { for _, closer := range closers { closer.Close() } }, nil } func extract(r io.Reader, ns string, w io.Writer) { bufr := bufio.NewReader(r) fnDocs := make(map[string]string) varDocs := make(map[string]string) // Reads a block of line comments, i.e. a continuous range of lines that // start with a comment leader (// or #). Returns the content, with the // comment leader and any spaces after it removed. The content always ends // with a newline, even if the last line of the comment is the last line of // the file without a trailing newline. // // Discards the first line after the comment block. readCommentBlock := func() (string, error) { builder := &strings.Builder{} for { line, err := bufr.ReadString('\n') if err == io.EOF && len(line) > 0 { // We pretend that the file always have a trailing newline even // if it does not. The next ReadString will return io.EOF again // with an empty line. line += "\n" err = nil } line, isComment := stripCommentLeader(line) if isComment && err == nil { // Trim any spaces after the comment leader. The line already // has a trailing newline, so no need to write \n. builder.WriteString(strings.TrimPrefix(line, " ")) } else { // Discard this line, finalize the builder, and return. return builder.String(), err } } } for { line, err := bufr.ReadString('\n') line, isComment := stripCommentLeader(line) const ( varDocPrefix = "elvdoc:var " fnDocPrefix = "elvdoc:fn " ) if isComment && err == nil { switch { case strings.HasPrefix(line, varDocPrefix): varName := line[len(varDocPrefix) : len(line)-1] varDocs[varName], err = readCommentBlock() case strings.HasPrefix(line, fnDocPrefix): fnName := line[len(fnDocPrefix) : len(line)-1] fnDocs[fnName], err = readCommentBlock() } } if err != nil { if err != io.EOF { log.Fatalf("read: %v", err) } break } } write := func(heading, entryType, prefix string, m map[string]string) { fmt.Fprintf(w, "# %s\n", heading) names := make([]string, 0, len(m)) for k := range m { names = append(names, k) } sort.Slice(names, func(i, j int) bool { return symbolForSort(names[i]) < symbolForSort(names[j]) }) for _, name := range names { fmt.Fprintln(w) fullName := prefix + name // Create anchors for Docset. These anchors are used to show a ToC; // the mkdsidx.py script also looks for those anchors to generate // the SQLite index. // // Some builtin commands are documented together. Create an anchor // for each of them. for _, s := range strings.Fields(fullName) { if strings.HasPrefix(s, "{#") { continue } fmt.Fprintf(w, "\n\n", entryType, url.QueryEscape(html.UnescapeString(s))) } if strings.Contains(fullName, "{#") { fmt.Fprintf(w, "## %s\n", fullName) } else { // pandoc strips punctuations from the ID, turning "mod:name" // into "modname". Explicitly preserve the original full name // by specifying an attribute. We still strip the leading $ for // variables since pandoc will treat "{#$foo}" as part of the // title. id := strings.TrimPrefix(fullName, "$") fmt.Fprintf(w, "## %s {#%s}\n", fullName, id) } // The body is guaranteed to have a trailing newline, hence Fprint // instead of Fprintln. fmt.Fprint(w, m[name]) } } if len(varDocs) > 0 { write("Variables", "Variable", "$"+ns, varDocs) } if len(fnDocs) > 0 { if len(varDocs) > 0 { fmt.Fprintln(w) fmt.Fprintln(w) } write("Functions", "Function", ns, fnDocs) } } func stripCommentLeader(s string) (string, bool) { if strings.HasPrefix(s, "#") { return s[1:], true } else if strings.HasPrefix(s, "//") { return s[2:], true } return s, false } var sortSymbol = map[string]string{ "+": " a", "-": " b", "*": " c", "/": " d", } func symbolForSort(s string) string { // Hack to sort + - * / in that order. if t, ok := sortSymbol[strings.Fields(s)[0]]; ok { return t } // If there is a leading dash, move it to the end. if strings.HasPrefix(s, "-") { return s[1:] + "-" } return s } elvish-0.17.0/website/cmd/elvdoc/main_test.go000066400000000000000000000107751415471104000210730ustar00rootroot00000000000000package main import ( "io" "os" "path" "strings" "testing" "src.elv.sh/pkg/testutil" ) var extractTests = []struct { name string src string ns string wantDoc string }{ {name: "Empty source", src: "", wantDoc: ""}, {name: "Source without elvdoc", src: "package x\n// not elvdoc", wantDoc: ""}, { name: "Source with elvdoc:fn", src: `package x //elvdoc:fn cd // // Changes directory. `, wantDoc: `# Functions ## cd {#cd} Changes directory. `, }, { name: "symbol with punctuation and specified ID", src: `package x //elvdoc:fn + {#add} // // Add. `, wantDoc: `# Functions ## + {#add} Add. `, }, { name: "Source with unstable symbols", src: `package x //elvdoc:fn -b // -B. //elvdoc:fn a // A. //elvdoc:fn b // B. `, ns: "ns:", wantDoc: `# Functions ## ns:a {#ns:a} A. ## ns:b {#ns:b} B. ## ns:-b {#ns:-b} -B. `, }, { name: "Source with multiple doc-fn", src: `package x //elvdoc:fn b // B. //elvdoc:fn a // A. //elvdoc:fn c // C. `, wantDoc: `# Functions ## a {#a} A. ## b {#b} B. ## c {#c} C. `, }, { name: "Source with both doc-fn and var-fn", src: `package x //elvdoc:fn a // A. //elvdoc:var b // B. `, wantDoc: `# Variables ## $b {#b} B. # Functions ## a {#a} A. `, }, { name: "Elvish source", src: ` #elvdoc:fn a # A. #elvdoc:var b # B. `, wantDoc: `# Variables ## $b {#b} B. # Functions ## a {#a} A. `, }, { name: "Source without trailing newline", src: `package x //elvdoc:fn a // A.`, wantDoc: `# Functions ## a {#a} A. `, }, { name: "Source with both doc-fn and var-fn", src: `package x //elvdoc:fn a // A. //elvdoc:var b // B. `, ns: "ns:", wantDoc: `# Variables ## $ns:b {#ns:b} B. # Functions ## ns:a {#ns:a} A. `, }} var emptyReader = io.MultiReader() func TestExtract(t *testing.T) { for _, test := range extractTests { t.Run(test.name, func(t *testing.T) { r := strings.NewReader(test.src) w := new(strings.Builder) extract(r, test.ns, w) compare(t, w.String(), test.wantDoc) }) } } func TestRun_MultipleFiles(t *testing.T) { setupDir(t) w := new(strings.Builder) run([]string{"a.go", "b.go"}, emptyReader, w) compare(t, w.String(), `# Variables ## $v2 {#v2} Variable 2 from b. # Functions ## f1 {#f1} Function 1 from b. ## f2 {#f2} Function 2 from a. Some indented code. `) } func compare(t *testing.T, got, want string) { t.Helper() if got != want { t.Errorf("\n<<<<< Got\n%s\n=====\n%s\n>>>>> Want", got, want) } } // Set up a temporary directory with several .go files and directories // containing .go files. func setupDir(c testutil.Cleanuper) { testutil.InTempDir(c) writeFile("a.go", `package x //elvdoc:fn f2 // // Function 2 from a. // // Some indented code. `) writeFile("b.go", `package x //elvdoc:fn f1 // // Function 1 from b. //elvdoc:var v2 // // Variable 2 from b. `) writeFile("c.go", `package x //elvdoc:var v1 // // Variable 1 from c. `) writeFile("notgo.gox", `package x //elvdoc:var wontappear // // This won't appear because it is not in a .go file. `) // Subdirectories are ignored with -dir. writeFile("subpkg/a.go", `package subpkg //elvdoc:fn subpkg:f // // Function f from subpkg/a. //elvdoc:var subpkg:v // // Variable v from subpkg/a. `) } func writeFile(name, data string) { dir := path.Dir(name) err := os.MkdirAll(dir, 0700) if err != nil { panic(err) } os.WriteFile(name, []byte(data), 0600) } elvish-0.17.0/website/cmd/genblog/000077500000000000000000000000001415471104000167105ustar00rootroot00000000000000elvish-0.17.0/website/cmd/genblog/.gitignore000066400000000000000000000005141415471104000207000ustar00rootroot00000000000000# Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof [._]*.s[a-w][a-z] [._]s[a-w][a-z] *.un~ Session.vim .netrwhist *~ elvish-0.17.0/website/cmd/genblog/blog.go000066400000000000000000000055561415471104000201750ustar00rootroot00000000000000package main import ( "log" "os" "path/filepath" "strings" "github.com/BurntSushi/toml" ) // This file contains functions and types for parsing and manipulating the // in-memory representation of the blog. // blogConf represents the global blog configuration. type blogConf struct { Title string Author string Categories []categoryMeta Index articleMeta FeedPosts int RootURL string Template string BaseCSS []string } // categoryMeta represents the metadata of a cateogory, found in the global // blog configuration. type categoryMeta struct { Name string Title string } // categoryConf represents the configuration of a category. Note that the // metadata is found in the global blog configuration and not duplicated here. type categoryConf struct { Prelude string AutoIndex bool ExtraCSS []string ExtraJS []string Articles []articleMeta } // articleMeta represents the metadata of an article, found in a category // configuration. type articleMeta struct { Name string Title string Timestamp string ExtraCSS []string ExtraJS []string } // article represents an article, including all information that is needed to // render it. type article struct { articleMeta IsHomepage bool Category string Content string ExtraCSS string ExtraJS string LastModified rfc3339Time } type recentArticles struct { articles []article max int } func (ra *recentArticles) insert(a article) { // Find a place to insert. var i int for i = len(ra.articles); i > 0; i-- { if ra.articles[i-1].Timestamp > a.Timestamp { break } } // If we are at the end, insert only if we haven't reached the maximum // number of articles. if i == len(ra.articles) { if i < ra.max { ra.articles = append(ra.articles, a) } return } // If not, make space and insert. if len(ra.articles) < ra.max { ra.articles = append(ra.articles, article{}) } copy(ra.articles[i+1:], ra.articles[i:]) ra.articles[i] = a } // decodeFile decodes the named file in TOML into a pointer. func decodeTOML(fname string, v interface{}) { _, err := toml.DecodeFile(fname, v) if err != nil { log.Fatalln(err) } } func readFile(fname string) string { content, err := os.ReadFile(fname) if err != nil { log.Fatal(err) } return string(content) } func catInDir(dirname string, fnames []string) string { var sb strings.Builder for _, fname := range fnames { sb.WriteString(readFile(filepath.Join(dirname, fname))) } return sb.String() } func getArticle(a article, am articleMeta, dir string) article { fname := filepath.Join(dir, am.Name+".html") content := readFile(fname) fileInfo, err := os.Stat(fname) if err != nil { log.Fatal(err) } modTime := fileInfo.ModTime() css := catInDir(dir, am.ExtraCSS) js := catInDir(dir, am.ExtraJS) return article{ am, a.IsHomepage, a.Category, content, css, js, rfc3339Time(modTime)} } elvish-0.17.0/website/cmd/genblog/feed_tmpl.go000066400000000000000000000013641415471104000212020ustar00rootroot00000000000000package main const feedTemplText = ` {{ .BlogTitle }} {{ .LastModified }} {{ .RootURL }}/ {{ $rootURL := .RootURL }} {{ $author := .Author }} {{ range $info := .Articles}} {{ $info.Title }} {{ $link := print $rootURL "/" $info.Category "/" $info.Name ".html" }} {{ $link }} {{ $info.LastModified }} {{ $author }} {{ $info.Content | html }} {{ end }} ` elvish-0.17.0/website/cmd/genblog/main.go000066400000000000000000000111021415471104000201560ustar00rootroot00000000000000package main import ( "fmt" "log" "os" "path/filepath" "sort" "time" ) func main() { args := os.Args[1:] if len(args) != 2 { log.Fatal("Usage: genblog ") } srcDir, dstDir := args[0], args[1] srcFile := func(elem ...string) string { elem = append([]string{srcDir}, elem...) return filepath.Join(elem...) } dstFile := func(elem ...string) string { elem = append([]string{dstDir}, elem...) return filepath.Join(elem...) } // Read blog configuration. conf := &blogConf{} decodeTOML(srcFile("index.toml"), conf) if conf.RootURL == "" { log.Fatal("RootURL must be specified; needed by feed and sitemap") } if conf.Template == "" { log.Fatal("Template must be specified") } if conf.BaseCSS == nil { log.Fatal("BaseCSS must be specified") } template := readFile(srcFile(conf.Template)) baseCSS := catInDir(srcDir, conf.BaseCSS) // Initialize templates. They are all initialized from the same source code, // plus a snippet to fix the "content" reference. categoryTmpl := newTemplate("category", "..", template, contentIs("category")) articleTmpl := newTemplate("article", "..", template, contentIs("article")) homepageTmpl := newTemplate("homepage", ".", template, contentIs("article")) feedTmpl := newTemplate("feed", ".", feedTemplText) // Base for the {{ . }} object used in all templates. base := newBaseDot(conf, baseCSS) // Up to conf.FeedPosts recent posts, used in the feed. recents := recentArticles{nil, conf.FeedPosts} // Last modified time of the newest post, used in the feed. var lastModified time.Time // Whether the "all" category has been requested. hasAllCategory := false // Meta of all articles, used to generate the index of the "all", if if is // requested. allArticleMetas := []articleMeta{} // Paths of all generated URLs, relative to the destination directory, // always without "index.html". Used to generate the sitemap. allPaths := []string{""} // Render a category index. renderCategoryIndex := func(name, prelude, css, js string, articles []articleMeta) { // Add category index to the sitemap, without "/index.html" allPaths = append(allPaths, name) // Create directory catDir := dstFile(name) err := os.MkdirAll(catDir, 0755) if err != nil { log.Fatal(err) } // Generate index cd := &categoryDot{base, name, prelude, articles, css, js} executeToFile(categoryTmpl, cd, filepath.Join(catDir, "index.html")) } for _, cat := range conf.Categories { if cat.Name == "all" { // The "all" category has been requested. It is a pseudo-category in // that it doesn't need to have any associated category // configuration file. We cannot render the category index now // because we haven't seen all articles yet. Render it later. hasAllCategory = true continue } catConf := &categoryConf{} decodeTOML(srcFile(cat.Name, "index.toml"), catConf) prelude := "" if catConf.Prelude != "" { prelude = readFile(srcFile(cat.Name, catConf.Prelude+".html")) } css := catInDir(srcFile(cat.Name), catConf.ExtraCSS) js := catInDir(srcFile(cat.Name), catConf.ExtraJS) var articles []articleMeta if catConf.AutoIndex { articles = catConf.Articles } renderCategoryIndex(cat.Name, prelude, css, js, articles) // Generate articles for _, am := range catConf.Articles { // Add article URL to sitemap. p := filepath.Join(cat.Name, am.Name+".html") allPaths = append(allPaths, p) a := getArticle(article{Category: cat.Name}, am, srcFile(cat.Name)) modTime := time.Time(a.LastModified) if modTime.After(lastModified) { lastModified = modTime } // Generate article page. ad := &articleDot{base, a} executeToFile(articleTmpl, ad, dstFile(p)) allArticleMetas = append(allArticleMetas, a.articleMeta) recents.insert(a) } } // Generate "all category" if hasAllCategory { sort.Slice(allArticleMetas, func(i, j int) bool { return allArticleMetas[i].Timestamp > allArticleMetas[j].Timestamp }) renderCategoryIndex("all", "", "", "", allArticleMetas) } // Generate index page. XXX(xiaq): duplicated code with generating ordinary // article pages. a := getArticle(article{IsHomepage: true, Category: "homepage"}, conf.Index, srcDir) ad := &articleDot{base, a} executeToFile(homepageTmpl, ad, dstFile("index.html")) // Generate feed. feedArticles := recents.articles fd := feedDot{base, feedArticles, rfc3339Time(lastModified)} executeToFile(feedTmpl, fd, dstFile("feed.atom")) // Generate site map. file := openForWrite(dstFile("sitemap.txt")) defer file.Close() for _, p := range allPaths { fmt.Fprintf(file, "%s/%s\n", conf.RootURL, p) } } elvish-0.17.0/website/cmd/genblog/render.go000066400000000000000000000046271415471104000205270ustar00rootroot00000000000000package main import ( "fmt" "log" "os" "text/template" "time" ) // This file contains functions and types for rendering the blog. // baseDot is the base for all "dot" structures used as the environment of the // HTML template. type baseDot struct { BlogTitle string Author string RootURL string HomepageTitle string Categories []categoryMeta CategoryMap map[string]string BaseCSS string } func newBaseDot(bc *blogConf, css string) *baseDot { b := &baseDot{bc.Title, bc.Author, bc.RootURL, bc.Index.Title, bc.Categories, make(map[string]string), css} for _, m := range bc.Categories { b.CategoryMap[m.Name] = m.Title } return b } type articleDot struct { *baseDot article } type categoryDot struct { *baseDot Category string Prelude string Articles []articleMeta ExtraCSS string ExtraJS string } type feedDot struct { *baseDot Articles []article LastModified rfc3339Time } // rfc3339Time wraps time.Time to provide a RFC3339 String() method. type rfc3339Time time.Time func (t rfc3339Time) String() string { return time.Time(t).Format(time.RFC3339) } // contentIs generates a code snippet to fix the free reference "content" in // the HTML template. func contentIs(what string) string { return fmt.Sprintf( `{{ define "content" }} {{ template "%s-content" . }} {{ end }}`, what) } const fontFaceTemplate = `@font-face { font-family: %v; font-weight: %v; font-style: %v; font-stretch: normal; font-display: block; src: url("%v/fonts/%v.woff2") format("woff");}` func newTemplate(name, root string, sources ...string) *template.Template { t := template.New(name).Funcs(template.FuncMap(map[string]interface{}{ "is": func(s string) bool { return s == name }, "rootURL": func() string { return root }, "getEnv": os.Getenv, "fontFace": func(family string, weight int, style string, fname string) string { return fmt.Sprintf(fontFaceTemplate, family, weight, style, root, fname) }, })) for _, source := range sources { template.Must(t.Parse(source)) } return t } func openForWrite(fname string) *os.File { file, err := os.OpenFile(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { log.Fatal(err) } return file } func executeToFile(t *template.Template, data interface{}, fname string) { file := openForWrite(fname) defer file.Close() err := t.Execute(file, data) if err != nil { log.Fatalf("rendering %q: %s", fname, err) } } elvish-0.17.0/website/cmd/highlight/000077500000000000000000000000001415471104000172425ustar00rootroot00000000000000elvish-0.17.0/website/cmd/highlight/highlight.go000066400000000000000000000054631415471104000215500ustar00rootroot00000000000000// The highlight program highlights Elvish code fences in Markdown. package main import ( "bufio" "bytes" "fmt" "html" "log" "os" "strings" "src.elv.sh/pkg/edit/highlight" ) var ( scanner = bufio.NewScanner(os.Stdin) lineno = 0 highlighter = highlight.NewHighlighter(highlight.Config{}) ) func scan() bool { lineno++ return scanner.Scan() } func main() { for scan() { line := scanner.Text() trimmed := strings.TrimLeft(line, " ") indent := line[:len(line)-len(trimmed)] if trimmed == "```elvish" || trimmed == "```elvish-bad" { bad := trimmed == "```elvish-bad" highlighted := convert(collectFenced(indent), bad) fmt.Printf("%s
%s
\n", indent, highlighted) } else if trimmed == "```elvish-transcript" { highlighted := convertTranscript(collectFenced(indent)) fmt.Printf("%s
%s
\n", indent, highlighted) } else { fmt.Println(line) } } } func collectFenced(indent string) string { var buf bytes.Buffer for scan() { line := scanner.Text() if !strings.HasPrefix(line, indent) { log.Fatalf("bad indent of line %d: %q", lineno, line) } unindented := line[len(indent):] if unindented == "```" { break } if buf.Len() > 0 { buf.WriteRune('\n') } buf.WriteString(unindented) } return buf.String() } const ( ps1 = "~> " ps2 = " " ) func convertTranscript(transcript string) string { scanner := bufio.NewScanner(bytes.NewBufferString(transcript)) var buf bytes.Buffer overread := false var line string for overread || scanner.Scan() { if overread { overread = false } else { line = scanner.Text() } if strings.HasPrefix(line, ps1) { elvishBuf := bytes.NewBufferString(line[len(ps1):] + "\n") for scanner.Scan() { line = scanner.Text() if strings.HasPrefix(line, ps2) { elvishBuf.WriteString(line + "\n") } else { overread = true break } } buf.WriteString(html.EscapeString(ps1)) buf.WriteString(convert(elvishBuf.String(), false)) } else { buf.WriteString(html.EscapeString(line)) buf.WriteString("
") } } return buf.String() } func convert(text string, bad bool) string { highlighted, errs := highlighter.Get(text) if len(errs) != 0 && !bad { log.Printf("parsing %q: %v", text, errs) } var buf strings.Builder for _, seg := range highlighted { var classes []string for _, sgrCode := range strings.Split(seg.Style.SGR(), ";") { classes = append(classes, "sgr-"+sgrCode) } jointClass := strings.Join(classes, " ") if len(jointClass) > 0 { fmt.Fprintf(&buf, ``, jointClass) } for _, r := range seg.Text { if r == '\n' { buf.WriteString("
") } else { buf.WriteString(html.EscapeString(string(r))) } } if len(jointClass) > 0 { buf.WriteString("
") } } return buf.String() } elvish-0.17.0/website/cmd/macros/000077500000000000000000000000001415471104000165575ustar00rootroot00000000000000elvish-0.17.0/website/cmd/macros/macros.go000066400000000000000000000071071415471104000203770ustar00rootroot00000000000000// The macros program implements an ad-hoc preprocessor for Markdown files, used // in Elvish's website. package main import ( "bufio" "bytes" "flag" "fmt" "io" "log" "os" "os/exec" "path" "path/filepath" "strings" ) var ( repoPath = flag.String("repo", "", "root of repo") elvdocPath = flag.String("elvdoc", "", "Path to the elvdoc binary") ) func main() { flag.Parse() filter(os.Stdin, os.Stdout) } func filter(in io.Reader, out io.Writer) { f := filterer{} f.filter(in, out) } type filterer struct { module, path string } var macros = map[string]func(*filterer, string) string{ "@module ": (*filterer).expandModule, "@ttyshot ": (*filterer).expandTtyshot, "@cf ": (*filterer).expandCf, "@dl ": (*filterer).expandDl, } func (f *filterer) filter(in io.Reader, out io.Writer) { scanner := bufio.NewScanner(in) for scanner.Scan() { line := scanner.Text() for leader, expand := range macros { i := strings.Index(line, leader) if i >= 0 { line = line[:i] + expand(f, line[i+len(leader):]) break } } fmt.Fprintln(out, line) } if f.module != "" { callElvdoc(out, f.module, f.path) } } func (f *filterer) expandModule(rest string) string { if *repoPath == "" || *elvdocPath == "" { log.Println("-repo and -elvdoc are required to expand @module ", rest) return "" } fields := strings.Fields(rest) switch len(fields) { case 1: f.module = fields[0] f.path = "pkg/mods/" + strings.ReplaceAll(f.module, "-", "") case 2: f.module = fields[0] f.path = fields[1] default: log.Println("bad macro: @module ", rest) } // Module doc will be added at end of file return fmt.Sprintf( "", f.module) } func callElvdoc(out io.Writer, module, path string) { fullPath := filepath.Join(*repoPath, path) ns := module + ":" if module == "builtin" { ns = "" } cmd := exec.Command(*elvdocPath, "-ns", ns, fullPath) r, w := io.Pipe() cmd.Stdout = w cmd.Stderr = os.Stderr filterDone := make(chan struct{}) go func() { filter(r, out) close(filterDone) }() err := cmd.Run() w.Close() <-filterDone r.Close() if err != nil { log.Fatalln(err) } } func (f *filterer) expandTtyshot(name string) string { content, err := os.ReadFile(path.Join("ttyshot", name+".html")) if err != nil { log.Fatal(err) } return fmt.Sprintf(`
%s
`, bytes.Replace(content, []byte("\n"), []byte("
"), -1)) } func (f *filterer) expandCf(rest string) string { targets := strings.Split(rest, " ") var buf strings.Builder buf.WriteString("See also") for i, target := range targets { if i == 0 { buf.WriteString(" ") } else if i == len(targets)-1 { buf.WriteString(" and ") } else { buf.WriteString(", ") } fmt.Fprintf(&buf, "[`%s`](%s)", target, cfHref(target)) } buf.WriteString(".") return buf.String() } // Returns the href for a `@cf` reference. func cfHref(target string) string { i := strings.IndexRune(target, ':') if i == -1 { // A link within the builtin page. Use unqualified name (e.g. #put). return "#" + target } module, symbol := target[:i], target[i+1:] if module == "builtin" { // A link from outside the builtin page to the builtin page. Use // unqualified name (e.g. #put). return "builtin.html#" + symbol } // A link to a non-builtin page. return module + ".html#" + target } func (f *filterer) expandDl(rest string) string { fields := strings.SplitN(rest, " ", 2) name := fields[0] url := name if len(fields) == 2 { url = fields[1] } return fmt.Sprintf(`%s`, url, name) } elvish-0.17.0/website/cmd/runefreq/000077500000000000000000000000001415471104000171225ustar00rootroot00000000000000elvish-0.17.0/website/cmd/runefreq/main.go000066400000000000000000000007241415471104000204000ustar00rootroot00000000000000package main import ( "bufio" "fmt" "io" "log" "os" "sort" ) func main() { freq := map[int]int{} rd := bufio.NewReader(os.Stdin) for { r, _, err := rd.ReadRune() if err == io.EOF { break } else if err != nil { log.Fatal(err) } if r > 0x7f { freq[int(r)]++ } } var keys []int for k := range freq { keys = append(keys, k) } sort.Ints(keys) for _, k := range keys { fmt.Printf("%d U+%04d %s\n", freq[k], k, string(rune(k))) } } elvish-0.17.0/website/favicons/000077500000000000000000000000001415471104000163405ustar00rootroot00000000000000elvish-0.17.0/website/favicons/android-chrome-192x192.png000066400000000000000000000213641415471104000227040ustar00rootroot00000000000000PNG  IHDRRlgAMA a cHRMz&u0`:pQ<bKGD pHYsetIME 4(2![IDATxy\WnԶ[j, l @ 8s=ဇ91s0L !x5d 6g-a˒mdj[Vz޻ZnITW}Uι~u{wߪP .zߣ@¥ 3 Pf%- >L~6U] Ь@ҟ$Gϵg,,0ZjH8_e)HeʴA:՝,&q`NǑ‘.}FTx 7;?rⲺeb9 \xjZAonF۱^+rHx8 8\fVې@ꎙD5z)0򐝳cflv!Xo>Er?~<T~4K+"m4:L!-Y[iGx\ȥ#"Nm z4*QC865dp?҉,!M#~ Joqw4һt(x3Cљ7ޭ* ZV8նfXv%?E:4Q@\odvg*mһf2A`_iy풿*.Vd|/iִ_o^[qSЁ;{A_Ӭmo" ˏt \_/n x-[H$zygddL&‚P%gO?@nLl܇̮*@={pmqc~?nMBP,f g?ɓӎ8|[ypx5EMWUqy7 qa˻^K t?AqIaƺ"a>|DA_I j#kZw[[[Ž+*W2;;+>ϋ_^~7S' ~"d/f}'Z[[~N[_{&WCdV턦qw/~笚ӟ4ͿLV[1\|aDQ`,EC:Zq Hj%-אV|;aǎ:s9Нu=~!( syyYCi.ΑN3?1<# 2AL ޺~?Ї~[r]wǿBK -2ޫ9f_e /M2qfl*ۑcȢ 5E|0e?t{/77G&A,~K6{$'ffYH7DǑ5ѪVlzzzDM!j Ť6*Cq<؉7kzx {(V.du]/"uy b!@ ڵ SktȺ5X| h[/z<n3UAU dpkІL{ U+Qd J~P(P, )06/;߷~:g%X}H!Vsj@GJ/055E6k(o2DBlc+.#kFz-XLMMz8˒Y00O^xzK[zꄐcb7J TpU=eO2@i+}$$En+pt&}8~ia^z|ʾ+ݝ7сw"cNM%M|>w܁S$s9o3,:c'0  Ve܅ԿV='x衇?ɣ='҅b7MN5Yפ`;_Z:399ɭJ[ۺM e344Ľa}Qz>En:guw AfOُ!Z900妛nF|>k|N|o} |"cύI rDULdޖX{],#F}4[@zBLMX#BLHl~H$®]j"yg>K'/^Xx|p-:#O8╉ +YcU}QF6jb|宻"\Jַŗ%FFF(y׍% lu]rW0kK,OS 2;~dck}"\1^[oロni];v?f~~;.;`|[q #/A~S_r###?.\̌lv} .K}* 'qbމ+^d?f7l] Mp\eCpKKrݻCw_ ,~Q_q_/􁵴@GfyUa]0 ii%I&?g^*껨 2#bc:|JB[Gu Q/&y~ U)W[ꍉ3<ŅZpO%7 $^ \ L44b@b_‰[7c*V.*v9JvUY*VQfv7x|/ Ҿ۵;ۖrAfuP1 #Ro\7v:Y$VǏ2}UU QE p5)Ve/22[0yesSj3;Y ?aڝȱ}W @L'5 =&|IHޘDoo Gƴ\' =;(g~r^JT47j-۝cW 6z{Hgn`NiO!d.(;cK_ 1?1OTJmY$!Ţ;?k|ȊR 5f0Za"(6odIeD3?ynBB]!"-8R`w1~)a!1rjyVv#<  ADQ0ܘLjCTho`%E"+9dȨS[O'!y( :!V.{x#^A. tQvX*UqXͥ<:&/ҶE] p 2^o6%0d[2.:8P[bc6L(6ǦJytHC]:n`jRhoІ˷5mb+оȎZϕҩGwE"{+ 23"{:Lu1Lh]K_">7$=imms %#ajL6Q*xB:׬U t^(EH-Kf><_ЀM:4":-Z›t\_~}&c_+3X6Tc~# }ntZ\ѝQ1 4Jvtll0C!ŖZAb@"`z%8i/:WIq`g Y`u9WY&_Coic[=f%{´oiv@ qy]t- _.ã }qKU+"$ѽ P3tw"y,OCNۖ+Bn%:Cvh͖۟DwF 9{Qn>YEWBmN%u ou/~Qpj #!#JkOt" y0'h)mfE=~}w]ͨΠDW2Vii%3jRb6qGd-.JsiDGmL, @__JCnM(#( =W{RXgӇOh omׁVq$'zl_!bΦ%pQ^0 m I7T" A5mzݜpTB4{℺V+tN_R4ҁ`-o1'ݹjx;#~ ё0buoꉎJA߬FE5{S/hFMEܽsu/P#e>nF|gM攦V4͘D0~'.,℺CJgfMH93^UZSA+(9?'||Qp<\`%ս`2HbR\,ק?=Nz$v7鸮CwX!}%!`[Av:S\TD"O闧yeFyPN FIgYj;gY踼.6ܼRdžɦ2 S(5bb:lZ<ej0& ,nT,::Tew().DGu׭6(ã+}`-]ƒ@wa&G::t3LOU|x+B[ϕ%O#?+C "c'prw`c`(ٱ5"CdžHZuWNz>2wE-RvhFmsMH=1ZM1;0|vnM>b{xSQbSԯ_䋓UmY4VZݹ( d!ޟpssj Y'&2sSfGx 'B%QTeqSlTu{:!6/ވ*K~i+Ɓ/'~A56~)NGevErJ =gT:{Ub4Oc=6*-ui2"wW0Ҥrms=HOnEh℻ʃgϨںBSbISC-.VgR2OCbRżLy67b.#>*V_16 M)uq57gf8;E z殭_i>!ͥ!/Q&2~j\4J*TVb9Ek-_ÆT^6ܼA֠8_dA9qv,ɳhB]!JAEdb%#(9':oԥ}K2ݹE۟EyƞS-HGwFqy)lĕVeȢdӶN5]#=fz5d9 *Ue{ 2V>ϚQS/N1=tf:7;!o Ӿ֗~ǎVGE} N .>qe>6{tNd-A/"f (Qimۈ)9;NȎFDH0C?MNy-::M)uz2cH8(mWubWO#3Vc#t]$&V:ҥK4M~jX+r%sN0ڒJ>Cj_AD2=ƌ$ISJOɪ-6d8Q%4M%= Roj&r+[ۡ?D s:ťN5y}ftǷrR.BJ|CéU ^>BN5s5Iwb3jSZ(ZOw#$YD> |5f!Zyc'OEYf/;4zavwi m 1yvU[HYȠv˒.ۡiD{tfpO+SjQ0E*j gn:ciCkҽ4eH=Ex 5d8ZœVvx?z x=\ZFSjЕ*A3 34&4jPO@ޡCLhum8di4"CfsV50`鍂 y&&2D4ƐYvbseC~S4Y[;q0 HgqEfґ`ՃсH i;Q['gn;~3/,%@ff3>}^[!Poyxkwzi 2ǃajC/`=n~xS6e|ߏ\ fD=w LH5 F՘f}-@pp;) a;fYdzHOGA iVXJ "U|>QL24 /G.'4lnހrT/?9A(0#тI7AE:|e!0r? <iLηS/8>RA6ZZVARGR;̮25* 8P. "Z )]ErGd\"F,r&Jm.RK?FA9T(nK%tEXtdate:create2018-11-04T22:52:40+01:00mP%tEXtdate:modify2018-11-04T22:52:40+01:00RtEXtSoftwarewww.inkscape.org<WzTXtRaw profile type iptcx qV((OIR# .c #K D4d#T ˀHJ.tB5IENDB`elvish-0.17.0/website/favicons/android-chrome-256x256.png000066400000000000000000000215271415471104000227070ustar00rootroot00000000000000PNG  IHDR\rfgAMA a cHRMz&u0`:pQ<bKGD pHYsetIME 4(2!IDATxyx\ՕU*eK%[%` NiY&Lh;LH&!|$$=Й& cۀȲK’,˒TǕw-{羪ٖ;|{=K%cR33$k3 ؟@1&m@ I+8",1)֭c5&aaݔ)P`r2̄Xo07WU X\ r(: x 4J+Ψp9p#=(>O*뀵|SNxhVHQ80T9LEqB)` :6i*U,!?YR]_cOԅU(F~R TI%}X s-=T \,=8e%fgz2<9qQ@Ip&[%>|bPQ" |.=Ug AO|| eb'bԥZz)p\Yz`+Oa,4e:mL``T!G@PT$|8TC1=zt✃I=p*vʳKJd"?TcU`R&`zkQJ?`R cVD019H%5d;>t!?hTRKq+qIӕ(*-y i)bjK1Gʊ\.U#-oo fS%?c:9m'@[@E%zblLieҊ(),H<,̕VDQơc A4ɯJS\& ,hoog߾}ݻaURpCiE$q43mڴا>c=MEooolÆ }s3fH+|㹒r|?؏~X__ߔ~"b?cG%uϚ+$gee瞤&xJԐ/0%:}v&ٳ'xb7>̣@IhbŊXggg8===uI(dK9ۀ0W^*b7pR D H&}]tQ۷؊+JeX8񼩰W^y?_D鶶6.]J[[Lڵz ##D v228"+1E4I oT}Qnje]=W?Jg?݃wƣ棌~JR4cQ,7YgyPHz7|3wynn:vupCt`ȠGǫ*K̢0MyXB@ٻw/7M\wӼ-Hq`N |C"$x n:<>3gdo^+U9IrKr)[ZyGŕD#4`7 0'ayS &Ƿ-/^ʓS[˃/K1!99//ܛ%,ޖ^zRg _78x8wϡCQ}GUqD,v|7](gsx7 H>uuuM~ ۵*P(D\\ i˘+T<$5KV\铪s U Ք.*VG9%~1^d…>*$L~Y>E_D(,#^kP/7.4oYs˥UHPF/o_EfA:I1>My9$E^$5Z:iUt_Kp&rrrNA>^WW7|G~3/ϗV#ݹ%7'j2{%뮻NM7Ğ= |f |r6@kF]]^mmm^۷K?UTU2}ti5ҝEy$;do8%%%n[lng.ހpH^HNpf,rKs)*yҲaOCM ;I?Y q~Yfrn^xߏ#97Kʨ\YɴN ~&/yC tB>swR^XjGG??1sG/`sUd׶Ilǜ!^pS:!pWs뭷v)666qF~߲a3,k99vHb%NIQރW8sn:G׈|2<%N 8iS O6HqJjAaO8yRϻ)snV3>+*25ۿ}[ZT"#ێ}4|LWIqb>#$} IZ0SH5Tco8Aji S4-3PO,8 S.)#k&)N2z Zxo{jn|x_NffWJkǻWZTVA1D&d)X@닭lZ:v%-!&T@ QZH'+ D\Zk%175JpX@0n! qu^;Dg^XJn' 焁'x\+8Xuj( @-is '+Xۧ1 (VI=I@Hw +x^GĠysYs{<д"7@Ol`JP%8azaX<9Y 3*iVJkK󳖸@UV3 5(H9hQ뻥`9꣨ʄ XZ;}l u,`Npx (PZTJAdcpTZ3}2WXUhU749\$>s͵BnsWx*C͵5*E5\x/ @&@Z+]1_e 0j34 cuUuD49atb#ak,IHZ]**.Cެ<K,޵J0%Z(Dyp?ZeRGEZUE(~5pHkGOܙ,*V#]c(.Jk=  cPQ,P7**ZIAB0 eJlf.a&bZ(`c**e @im!hde3b-S5)BQ?N v0gJG3%؞̂LʖInZ(Q;3œR!T &צF*G<0aH J"yʗKN:#)JVSH "!g=g"9i5]Y T23}li55A&U6DȋIŕKqBdeJ!i% j22BTPu|a0 JkGPBݞ]RsT‘0sVhR0‘0ku;Kwjk&M[ 8/sJ%3n.)-bv| ඃgek QiZSn@6XM?>Y?vi,UDZFlKi04 8e6a).'}FG_CVWY1q^M?mJXۤ 8Ejx`F\kQ6MW7 53[;R:4oif;AQiuS*,ag1=386Ik6XㄑZ_|vQ63̔Vvβ ^nOؔs({'hPPwNOd##ű)qo}ީ?p$RQ~1'8.6%FG93u_F~?v$Hf~5}}WIu& 3W_x!U?3;VU6G1S$BmIQkVy.,V8_t }]Ǧğ8L՟DP7,x0W{-`gdzLrp +' alp3$ 3w\i5;lѼ7 8 ܀KCMaAƦğ')\)% J ۔D ѱÚ}ʺJB$[l?9)cw=U"fʅ@vQ6.F0}5D y 7󛚫-J ߙ> |Ka"c]y&_I+1%!gdžM=+u#OIYcG.i%&æğMDȯO[XVAex}smt?&@ g*l4_DžgngOϑGڗV|9+琑znױD;_a`*kX/deyǘbV8Ҵɞe4nKndRnb0ŘV3v$4omO"m:ʑ#jPqEH2>\Y"LZCJ6~?lfdgP~iyo? \%S X =Ŧğ>nGmi(7> +;%HϛYaOO"';N6f_6̼x J4?^gDpS a:UIN`6XO LAR5ŧJ([ebyzӥ K0L5.J0, ;Kz0(2AI2X.8*x-VDOӥ %cʥ'QdE 6=7bZF i&IG59RvLH+$ iXZtB <ӀK1Fʱ?Sl3altGc`ζv4't& Q+3{p ,T64-[++~t 9*2jO'A9tLJd*#Q0^=وS0B'fމi4ɀ$c,Rf`%tEXtdate:create2018-11-04T22:52:40+01:00mP%tEXtdate:modify2018-11-04T22:52:40+01:00RtEXtSoftwarewww.inkscape.org<WzTXtRaw profile type iptcx qV((OIR# .c #K D4d#T ˀHJ.tB5IENDB`elvish-0.17.0/website/favicons/apple-touch-icon.png000066400000000000000000000201771415471104000222240ustar00rootroot00000000000000PNG  IHDR=2gAMA a cHRMz&u0`:pQ<bKGD pHYsetIME 4(2IDATxyp}?'.AR},k"iձtM݉֊#{[e(NIdJVRIȊUI,IQ0$E"cX`=,H±׳g!{|JA x> /[{| `zɈM?o'Т`u#0#GVY ^ep8d}Z C+b]l! 8XR꬞um@/}r!DaM¼:Mn{;Q`Œp}b "0ON@Rk haJBkaft2y*ۻɤDCx't"_fnwǁb\C܋%`ΞښW3P# |~x!ĢHoH!<$A)qF,) &GE6By9wfKbFD9wH V $G!puDhAX{ Nod3?GoHI#&bVj 1FFx *Č۾kő~İčEgyto8 fa (9vEĂCq!c`Q8 |.{61 {ʓ_0 GxD:0b!FrFzY7fWQrJ~d2Ynv;\bePD|;߬_UzwUl srm188bP,I&DQ; /'H&$<0-dDOU^UE;xk/_ֲ٬|^Ҟ}Y\.ޯZc|p"&`x5|G?^X,}_6lؠ43DFL[XX'|RKR5yb=ڮ]~X FEtWo[+ uy)m61>8eMdN֢x8v;>(_җX,}Y~asxo]j)_/QɧEJBbH1_ȠZ2 ~jV>O|'xP\|+;A""], 3e3FR Ml=\Iv 7>9aHbX6&̅&auZ#7# 5"?gtX!\{uшO&Wʣ Kn 6MJMκYj1*m] <k06>H$u coݟOn6*\N|+~D5Kq:l5Yx+hkg̮wN-0•  <ĚHif n`קwEhWX=pW=զR)rIM] Cw 8`>VM* \MSI;RTS=2!= ˰0kJͫEЋ^ϝ;G4ms\H^hc) p[ͽGE_Sc/rܙ(/ `{% *jGӂxa׌i(G?Q[ߚݗ˙ҙn}7݌{ԉ)r엹~D9ߗyyh{2<<,펜98&Kkm^f[1Jy#̍t*jL 6L7)͖f|0rz Q&n >D(r j/#\* [_p(8qP(Ν;Qƈ_G⥋xo AUEYuph6v|֭[ZkEyyFFFp l P\(RH(dV\RDXBAAWg&KQA1cx~}tV2 ޗ䳟,bƬX,O?c=[=V!皫EU0L,&,N vg؉#5=d5iMPg '$@,sy|UO#Pњf'?I~3vP H/SOꫯJ{Nv3Έﰗuw#'f*lEU83f6# |jnEt?Daٸq#w}7wy'6lv_)Jü⋜>})n+YfNfȟ0􈑟x>GTѭl fffkTTݸ]fjfߝ/F:ZU:2_1vE-`Wili&''Rز˞̏γ0@pWM^zJj,Y(cCL?`}jn7.Cr,ios_El*E5(&CUBe3W[ [bg¬4AЋfi?B˲7!C[{MGR)}] rly!'$?M"e߸?.vϼc4)BhwޗJlg}FDwұ4cƤJޛxnhN|)xߖpg6h{+Fj"%m6LDG Z>5+|mRA\Bꎞ m^6B  "G&($-5Q)-V}IY9ad t*l纐XY# '&o8Vb bHhU!wco^?X*~ mP#3gg8/a Le?BLh3Ҏ;龓 ^KD;RDX|*/-''؃_Km%QWmnňݨɷ'IrXz,DG"IP_@lsW戟ˋVɋkx.aG%A(&&J躵و]B?BW*ZJgf<&L6 _&aC}.5ELӄ,EE87b,r'ގаIEho؇o)M6UJmFlUТal&o4l(x7yq3>e) qu!'ܴ~9l5]?S݆No7b3AyAڈ+Ѐ݌e%?.%o !\7}ե#vYbօjV ףKy2Ť-bmh ֭֞o؇s0;>Cu3jGj}v{-C ~hP  LvmnTDC 軹 G>~WNx_{ޔJΰ)J9ߠ:!=!Tstvj57k'}[aеZTB[XnvX[}^8#N{ZO8&ОgQ*+4_1X[51R4{S:T[VpKi)3t}O6RT ޮ\/@VhTJx_Ϧ_5J}[p tesNf>v[gYj0 W]wb:TJho7Y AWfb;v黹 AWo߰-^ZIvtcZbzE;vT)Im ;c*pQhuz"+j%nqQ&0\w۴H C":g:QzMTjɝ5Q r ގ8e*]J\b1L3*>V%r #䐺3X*HG/7H;b0}6FhO$g3E#^IoK`Ƿ&UGˆGMoK] ^z~xRQJ{;=tQ@Rj%TJ~V)(dY&LDkpGuOxV?}Vk#r@ifߛ%5"~:.ѐʻ}GhX^CxgS\dRIII ;5`o8.\t8Y &r`2&ޜSd2܃fȭEc+1DYECcXғiNMI[*&'z;JW'0(Ha@>g5 18݊H{;VH ]m]]~Hy+!r "5+eIKTRۓrr% zaLxe6Wz6YB{% $D*d >NqX]c¬4obVkۆ'+ % cIr H%Vc4Sqv۵y//SVC$nſ/^c՗` ɻp9\}axw,IY)75M3IRޅ&Nvv09e+Idz ;B|$N|/ Lų#49JxZ5?*dŏgGjVJͨD1y|9I6d1+,YK_!$jVsC^VJ:fH}{T>3-a C-,̊Tb<=4z}ӭib' 3Y#ZI#==ގ߭J=|=薶՝OގHv?etywa^Q@^Q⧖J; /[d9־ 89sYU^ERDh&r2&LH﹡%IϨ`v t_ZQ?4(}*$ǓD߬ngp-bGc,ʉҰs{Kx~AOO -=4H"$-ŪjJ3dQ/xWYaR /0@쭘һZI#-s)5 BsS3z_i-XB{B,)+eYfyM&KK+h lk,»VUs-#ц}YwfdlBέIO0ƿ**Zor]lP5%Ī%U!7,Cee|{RZE1W$VꪮUx—kuN_JX\H nBuDFJd4l^ѵ@G.ggGB[ wSO(̞d&3LYWj"-&:V 0gTLjÖJ5~j.Fɪۡ4z5|(QUZ#^K47ՖR 3ggUW4mMoewpo$-k% #2b5797 elvish-0.17.0/website/favicons/favicon-16x16.png000066400000000000000000000016471415471104000212660ustar00rootroot00000000000000PNG  IHDR(-SgAMA a cHRMz&u0`:pQ<PLTE---(  !;!4  TTTnnSSLL#M#{{*R*1m1RRNN%8z8WW,`,-c-CC  CCVVQQ07RRVV-d-4r4VVHHTTDD  KKQQC2#N#?? 3q3/g/$N$<TTEE'LLGG  MM/   G^tRNSHIbKGD[t4 pHYsetIME 4(2IDATc````dbfaeeacgdN.10y%$e@B|@~Y9yE%e?H@YLLEELWOl3 @C}#c- 曘[PK+k3XX 3Hl([#&i'rXpHXcx3<:2b*Q"/>Dzv4%tEXtdate:create2018-11-04T22:52:40+01:00mP%tEXtdate:modify2018-11-04T22:52:40+01:00RtEXtSoftwarewww.inkscape.org<WzTXtRaw profile type iptcx qV((OIR# .c #K D4d#T ˀHJ.tB5IENDB`elvish-0.17.0/website/favicons/favicon-32x32.png000066400000000000000000000027171415471104000212610ustar00rootroot00000000000000PNG  IHDR DgAMA a cHRMz&u0`:pQ<PLTE*AAAmmmNNN4`4-c-,b,'V'9~~TTVVVVSSEE>zzTTUUOO????DDFFIIRRUUGG  ,,,fffPPP   CNNVV(Y(/g/CC -PPQQ0.d.)OODD-b-RR3FF/h/EE7z76 MMTTQQ1k1FF0i0AA$NNSSRR,==2n2>>%Q%GGUU"L"!MM<"J"JJ  ;;4q4==2o2#M#HH  LL+_+TTTT@@9|9'87  ]Q%tRNS)lm675A*k8bKGD,q pHYsetIME 4(2IDAT8}S_A]R5 )@bBulXlƆ"6KĎ vMC\>>{73Qc;.\Vêj|Z_c僡:nA](H0B+Ȣ[%HFW*k֮[(9Eټe:KE1S;Psn @&9B(9߳Wٷ@WvO 4Cd =NR> .|||SSUUUUUUUUUUUUUUVVLL,|,,,ԓRRUUUUUUUUUUUUVVLL<;υTTVVVVVVVVVVQQ;;+;^EE??@@@@==6w6(X(-^q111qq 888XXXDDDq]^;;||-.'rs( 6mo7 ??( @ )kl)675'878!  877*5A+_+TTSSTT@@ 9|9UUSSUU4q4A5)OOUUUURR3LLVVUUTT?6==VVUUVV2o2#M#UUUUVVHH  "J"UUUUVVJJ  ;;VVUUVV4q47  IIVVUUUU"L"!MMVVUUSS<72n2VVUUVV>>%Q%UUUUVVGG  *3RRUUUUPP,==VVUUVV2n2)l AAVVUUVV.d.$NNUUUUSS9l'V'UUUUVVFF0i0UUUUVVFF MMVVUUTTQQUUUUVV1k17z7VVUUUUUUUUUURR6>SSUUUUUUUUVVEEFFVVUUUUUUVV/h/-b-VVUUUUUURR3)OOUUUUVVDDFFVVUUVV.d.l-PPUUUUQQ0l*/g/VVUUVVCC ),,,fffPPP   CNNUUUUVV(Y(7???DDFFIIRRUUUUVVGG  7zzTTVVUUUUVVOO?~~TTVVVVSSEE>6NNN4`4-c-,b,'V'96AAAAmmmA5667)lm)??(  HH%    (  'LLGG   DDMM/??VV$N$<TTEEI#N#VV?? 3q3WW/g/I  KKQQCHHRR24r4VVHHTTDD7RRVVVV-d- CCVVQQ0-c-WWCC 8z8WW,`,I{{*R*1m1RRNN%ITTTnnSSLL#M#---!;!4  HIelvish-0.17.0/website/favicons/mstile-150x150.png000066400000000000000000000145221415471104000212700ustar00rootroot00000000000000PNG  IHDRxgAMA a cHRMz&u0`:pQ<bKGDtIME 4(2IDATxy}3;f`qPҐ*FhTjPiI!iTI%MTEMBPe{O!&xg%=hw;wxRbj`=f߽@V@^4 $i` S@?з`~R^ K+Vr @y@ XF7#a&9/Kv0Aa؍ q5)1%84Q=uk 1IQ&QJS℁M+NL/bQDL"{o' [ۏGv=|a彉c<Ɓއc!LC1}LbtH,X`Kx}CWaNQSqY:r++E]iXZEAlz5S^ߨzF"/ߜ~̯\^W~Nu-+rQ ף('BY`o>m+Tf5w 79WT*g}cA{޽<`?~Nd2v7xci7{]"nrdy] _MZG 2J[1"G"{dAA㌥%S8\j0-aAH?,|[no>uuuz 'qu+pᦪ\&G.#&=fynDtR;g{}|3;h²Ph|3ʱ?زe =6l(O?ɝ߹Fp}3m-$3,/J14SG<:j3ZiJ4Ѷ=\CV,biz│0`&)po Osロ^'jpp l؇_ *eQ\GlmkirőEr_?k'P78 };y֭|3!up(aFXìs%BQwwl,˳.ZixF ujǀ?mGb1WN EŰqq;glr\5w]CN } RK8ܳ32pPȝά h۶iʮ;wѳ<穌(pf1 q;1ݮyIIR4KʺZcl=Qv;ݻz2ۀ3UM~_<}4㮜t.=xj"9&6Z7\e!L]C mnΥCrc'^&`M˥-l4VQSW.ߩWřdBtM:ϾI Qr:4f51)8D#c9:~+~wɲ,.{e~iDz(]#fQd2ɱcǸ(o&wqSY}jG9?B!>0͘؎(pF/)qA=ʎ;,ۻ;fڷMe$3o٥,L˲&I">6]+]g^ď4ɭ4~N_h۶mu]|k,,,cq~0s-+`E!DCH"B|}+hXitDa,=Cj]e}NqI 7 3͍F\|޽v0L 8?̓O>L i^̺ױk\&s=ljGO{u+g3^o8LbbtttDH&133C:v< !6|t>QC}+_}SQqo Qt/H&3448r29hlo$.cL`ahQ);@-Mc $3ϣ]AlkgOOŻ]SRFRsx'4vv4i]\jX(pg7BiAJ~s6(mW SR Yٕq ]}kԭo@)p 3諦9CҎn-4%| :(pn;5:r$sJns6M]M~OW6`R)GC4 z#]Ww#Ա\a1jy6 #Ϗ<(]iFSc5<8 3fMM1{rQIDhte pׅ¼p6IJ-.3]@Ju!5\aWb6iǦɦ+6mj#jlN(p \u!*eMIomHvߊW`\.@#pׅ(X=v>oe[JطQi^ Z9v5mۈ9j8͗K5J\/={{GŽD#omDsG~k0f5Ϋ{]rj`لrSj|DĽ}RΤ+}1\G~^B={{\{a6t\YU 6Y/D|]Fw |XJPo(˲M}q;ٶM|C-^R(a7(pKc{#]ʴ :2]YuG~]L3+i^\ͣIWBo푚s(pWu!Xwob^&m_eҕs7)p3-[fn<8.R]Wwzo;YΝj2^8ކS{ mmqZteT;jCԺ՝ mJWjݦ_ ZueB[ X$'頦8nD+Xi6c\teT;jCԹ m.ZGbG_i P8n EBd1a+`ѹ_+^)p7uR mmK}*ߠv ^1Mh+ hΪOW) Q8; .S mvz,Ϋ:M^)BT;^.SnLhKϧY-}H;g4F첪NW0u.i`B8̄6kZSObhPDBQL(p7u!]3$a2~q}CdJo,m4īvc: Po8u!躺{S,boקxuᅒ95-|ח|^)p>m } mvfA icI_v6! U+)%  9OGnn%vY,bqtÀyb>8L&)Pt\ACkե+S^8 sxB 0͂Gx}I+[5_Gaf}^Xў(W9X,؂bge^*,9d𘣲"++_Ϣт(pi|hؑ)T ˲91+giض2I flIc[լ <ϓI9j ѵр2=UGl1|t馾cyIgK_NcnK ُg#g dSY$|QSǦwD{nj~Qq'w7ǜNh,Ss&d+ !Yq3X#,ϕ)9hF$R 8y7 ع2)&N3'fyAbD:"fwLI8cL8g ml}CKO3U] cy󴡞8-V[E| m$K3F_eivYr]Ew#NxBۙUߛ99tߴt=Bǻ,>ta?~X(pc*lƄbZŌt%PsG!qSG\8%qcB&3$eQRS)gfӕy+:(pTh]`]=&l:*_s47qk |3\\$p/JeM [;MhKMzvȵ2eR:HW;]IW~|uNE;v;6vx~wHN&K_0ZD(.8c?yʸڲKY*_;5Sjo468MWF1AcoNr𷔩ms mG9r䲥ҜIWB R|\s(pS3򫸜_[Ak{Mhm$˓'90]Jt%`@@aF+mֹр%)9&_tVT|G\8o3㛸aNHID 1<2vK>0hPLXP | X(@R1_a龶لto+,.ހkC|}~3/T6%9Ga .]nX3h6MNW:u^0lA/,)pT"s-%ֵp4sl,2o.c٥lIe=cԹPߣb΋Prcd,4J-jY.X:Z7b7IexuM0=.JYLGxR/eoj?W%^WH5hK7g*l,7jMN伢C\mٕki @1߬0k{ M\cm=e-\57t j]{W޻8P܀Yp M1꽘bqQ# ̦B^mLPTƴ 4 ژcYK>N<nශ@Sg1 Hك Ii%x{-*0  Created by potrace 1.11, written by Peter Selinger 2001-2013 elvish-0.17.0/website/favicons/site.webmanifest000066400000000000000000000006521415471104000215350ustar00rootroot00000000000000{ "name": "", "short_name": "", "icons": [ { "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/android-chrome-256x256.png", "sizes": "256x256", "type": "image/png" } ], "theme_color": "#ffffff", "background_color": "#ffffff", "display": "standalone" } elvish-0.17.0/website/fonts/000077500000000000000000000000001415471104000156615ustar00rootroot00000000000000elvish-0.17.0/website/fonts/FiraMono-Bold.woff2000066400000000000000000000165441415471104000212300ustar00rootroot00000000000000wOF2OTTOd $4 q F`N6$P #"8f_%t`B## k9b, d=Pٙϙ@*$Mi0?(!CLؐXG1̡#}~.!?a+ē/a@ %#ܾ9]sZUژјNO=Z7vx{GuiiM]@MɧD"tA/硰t̉yI}/,d~ǮcRH wATzݨܛ-fcۻ~&R+_!ȰA+Nu}Ÿ=2lPQX5l6/=pb8-m@,eĊw󅙁0oh:;{kjG5uˬ X+Tl^2C9,+)r#'ZeqL.%rCk`x\&}9%Z5~cm+>)ؕN /fb ysfUI{B,f%PGm?)cb./ Kכo {zz6V6ˆ}>Ci3o4Wކ3}AA:x @tq p1Az`串Be qt53+L9]oؓ^nII$5Imsi ss1lJ~$x{Ktfv;X7l<(7*HbPPtjBWaz+-)1Z*T=%g^vΉl ܍Oə`$sگ)L%c\`eՋ|s|0)o޻\NS)q)E\up.߽-7)̚|6s!}`ﱉÊtx#ˢ+mf~qV5SU]@zy9DW[4GYtpRXcDʦ"0W҅4 j=l`AqE}\] B)&9k?B$T*:n,z@yrԻG偏rI9sU> IsDhEy KYR`` q!BaG[Aq)tNN|  ?{>iSp>Fբ'?ڡfa 0Q48L% A3LR1ZH%]Nf"I[!e{ 2Ga~9R 7q>L^,ə68tN?W9Se͹abY=Bs£6Ř^kM)<|M2ːE+Bl@z%Ȁ);[*ia2H>(R fKOE67kR +.#LJ:9ӈs?B}|wTyZ0[Vҙ'V^"8 V vV$ N_-u DjXmA Q`67._m̑JE;b{s$TTv,UhzhX=T`&PIL|5.*m+ 4X5fwݝ`PY@!2A!^=@}x @r8}0΃)"H|JuU_J `A2 *d`>1r0Je;TT.HF J|MZrPeX!%P xj> A "Z  &^28 Pf A 0Р D>@O"DIlma'v^}SV2(2FYtI0fò1îao|/'6ToK&>|$6IlȷoxՈ*k큾f_Ւwp;v`'vm]nÎr4X?n&^M3#JdJF^8,pzE-5!j ݽS5* (`qܽcqB.%@9X!WH-><['䒇rꂧB[#]T4*,,zC=3&tݰc?#EyHy@?#:2kFM@u3Z]# ZjBTC^L%_M_i(,#܋yrNY1 :~!r9i D. .[-=B̃ɰs4H!-| 3( 8M8dm .#v fÐ QWWb]֜l`$EI\Ez@"Zz"DQt`jXTT` |^kin۲jn[@*s*+ ^?zhgd;&W (ޘ_H"{e7DETkCSY5m(eN&6(_ʼntݨqTH1wAu2.ꂤt~ʜ?Sԗ(֬T:-;Xd>;*̆qzi9miARRpqbA+eZW"{ 9<4;_L#MOGfgmzχ>-[9l G M2ntnLt'\;!%gűz;늦Zm\Iq4Zlf(e4.M0G0 B>p>3BBb\(Y{kcCﲫt(; !Y%#,|>y%{EH]ʵG<Γ?Ce(Z v\zZ!dnh4eݛ);;hޤ)4!g6iW:^H<q2$: 7:ƪr|}Rk9sI`9Ăw%zPvW$K<@> B4aбto5 J; 7P7 wO)ҡ슇&Gia(]w7/?5t&&0BWJ*R(]>-((jxU&J{ $#tve@J雯w;Ӝ /cf&HQ05Td@6y<+Ǿq&uRg4RX7Ib&M槦@GWm0'_VW> Μ\m`q+7fd%hL }V~$O 3ju}SšMYjw_^g|,$=[cC˒;Ǡ]f:5"};j3"sWVέ~1@Rs(n_+4+.*V)nJ'b:t՘c.,y2~EG̔3.f;PC* x_C>4K8o&@ MMSP_zO[knMw%P TL逭j[i-n$[`&/<'k& FC8>L7L1oŶgSõ9 PH dz JLٰ 㒥H$9z(5>z#p]1Yo)%}XLPxǻF|z$ӗTA '=6*e2ȭAJUR4\|z8qT"Δ ?i$]RP_!d3.\ڴ0yeL/#|8&H\)(PW?t ~"fMdG&9CI7ڬ} 3?S(gS*Jډ%نl$Ň{7J- a 2'fD/}2$phtxs(:~;$˰󋼄v_ih%X588>ވ?Mzѩ񈗺~6n%bÐ[>X&7=^/~^ s3w7_0[Bu\Glѥj''ovwPA6a};sqa'S9M4K탙'>EL'D#ęt%T-7k>N3'̃f3{;$ TOB:"7h: *O@O~ J݆`9B[2\z#3.{ 0̣\l^яBBN,eB{bJ.EBX17 Pt3t)4K\xI>|瓦 Zln :dsK#a&xF^F'. ./!\2`hE\YTlYX,~J:·/_?8Ih"%z9{ zS$.X25yRb Sr1EJċI )%W-Oxq0k[]k&*,,4v[bZK1,*]vx\:/HtQJ_Cef7εAq(ⓛxYsKB[>BY ڟm{4 пeZDLmC=;+6;Hm ]mcɶ)m_֧eЫL)!7pRpÜLnQ);=P Y|qj@0OfcZ*QY(db=tC$SYwPGģa[R ==RW>pK0$0ꬰ IiGZ^xeyhgp':y¥y\k C/lK2EmEyEO墶mZ#̢\ D 9FZܱ20=m.l\>VL3c͗;>kMx"z]0!(kumC6nO˹|8^ŧ˒ܺ?tGY~[ Τ` y[B6I롶Ce ^ރ^8 j 9=pMwhEC}>*C" tALo5+[N I},k4}y2s80m>"AIC6S 44T>P29pDA<5w}xWM_C%'=sWxHvZ~8L"9*@>eLqc@h# >~>F(cauu) ?t(qNJ~&dmB'%zc{y2G,6pOtr( s)%:İw1]OM:.kUcN"y IzsI hqȓ~VFsʑsQ1}72 i;"e.~tA޳vw^̳/NkQ珜\ڲ:z,U s]b0<bj"IHQ\a ae l*9SR6͔хύ}sllƯ!sύHz\7' oFnm3s lGfOX3,_PK}Gɂ#^˜GZlz/μNz!{wj>ϻeهo88-LmERB.9s}Jqrj"OH dICx(CisP)XanG{ɮ%V=KDkƮ{ /7F?Rse;XTz%Cc nKug''2ej6OZH_nvaIoe |4WꒈzINմ(*P0^\! yV\x[3Fx=as&ȰKxUb221Rb'Ÿ]Ϟ PB5]Q]b rLBH[+hv)(s֐qc-FXRRS\_g3.h0…CPH7,su`وk"AE Ĉw/ ƈv{ȿvs.4ix=D Sb]Dzw,H&{;$9C鈁0kBLn #J @. 8 H@!xB*+0A( < 4ub@h P0 0`H6c6*VEcfp1\w#(ORhZEC4Bst)7yo~%~o;Q+B1L;c[͛$6x|PR2A%z2%l[9^{i(G]ZڬzmR59܈Im x2a+46qﶦ>FSժz7} w4%UZY8Xl[o@PIV#,-f0k#ILsW3mhnLsqMvz2huc4 zQ[5RB>e^p8.0UĨA16qhf`Ԣ$. MF#17aP266x}et#tQ])=HuAP:IuI/zGgl1O7s`,5ժ.uofF&Ϫ(c!F4ÈIjeqʂ~v0 Us"[39ylj,W ]ܙ[Z_vN+37R%I14Ïc[f\ Iy%mu: DEX.,MRY q!-MygXr Skc x=HRZc.Qzrs9:YkXGpc)jb[:ĈUegX*j#kV/Tu[uls_?!! *B0e'R&ezk?&,d\j,AQ6H>+P#=%(QbO5bឡ{"$et!5R:Q2~༐x$}Xq>:H#ID;$ ͢;%\q vIF^ß qTݵ>)c##GOy L9`V:Jpƅ 4bX818pqAtol5/݌I=xi约$m؋!%2JSA} ' Ž:C:Ƞ !TE!"}5坚E*GX4tB  ]^pJ @0-{6BHF'o<嬡}*zZ|eTuN4jEjpn'ݜ <4qi ίԣtw!{o&Bm}nDJqma&8J&)]ȮOA{9~̑Y#ӳCJFD˿ X&5ENuu uXy8@Kv/OQok ^;mRp7~#.AVȥL՜(GsiLĠ |pn%'X%ZZNdv_G8[SB;x~@퀮$Y#-v޾ayQWXGާԹTF*%e͊Y: ؽHR2w!-"E*0fQsAI,ܺP {^ J1Vq3";ݰCr5Z5V87j5D  H/ny A3,BraKU YhVXVj*mX Q|f9EvЪoR_ZRZ/Щ͵xx|`oxxY2Z~'DQ -2;ǃUhŷd+c X Zg! \ )NDjR]E+:$;3 cXwهr1І$çLuMtzkq~پv`.X\ Ncd(rjh\cIsM&uFk<a Z[CE?Eh\~ ˜Zr]ȣQMZdžEo9mÝ(6)`5 Q `ʽuwo:D; |L4doD+\"hn.8Yl:I7?Ե$r/YɀFƁ (s /Hi> ~mmx?u/|#c.ܻv惾.uZ֎R :>91!}T[^5,?yJBDA7x {rT#P3c܈&5eB7)?c?)gw5ݍTFÛo#"W#P_M9yP[Y-{y{eҘJnDQu.Ch(G#FъNC_X-Zэ,e8,+! rS%}>'.:L oE)A*S:4}jTRhR󺦇z3)~!11]!}r zvTpNqS4a0\ dž56~m  /Br=>_=~eJZ!2'h \-ChAso cPAF}~/܊D:HK~ Dp#pM \'*7/tU6H\ KچYڪaewm^) *`gcws}N+PWelv\ "ܐyGUuX_`%506%cܓ {nLQMGp&(Wev%RDnf&~[8ǩR6˄\,JWs븪I: ɒT zӉ`eQ#=~l1Է U|ݺ||/d{sBcb^wJCR?KkrxʵIw?EVA6l0J^tZnËwGksf^)V *e{뛠ٿm{o.Ȣy~s!Ո4,󮿍e> JX7 VHd[f. mǼ*wfpLܓb}moS2H-*pTYr?g9 :qsrrB 3re\Y$&qދOڸ9@剫QH+ӢKnv̠Z)z4AX=n%v+\o߇9)&D:$Aɏ:a#ʔ8XS6@ifĜ@bUHY03DNN> 0yd%d 8轳{C{=` FŸ 3|?E.+ .9=|O93%y^{@䘤B\ԺZoOfd_ߛ6jylyCr2CzNڜ~z-&-lvֆ R0n /$/0N96'*Q=hF -a‌S)b: ^PzfQEQ  s(IȀJ,~`0\PڍLj wn<<D^ +9u_XC1`G@p`R H+pV׵ tЄO80ZUD@}ٳ=_tvWHS=<|駪Y k"6ox_lI)bHZsE\T  m ~{VHŰ&҈N"@aWRzڞ.d+$v!͍s :6VJMo2@bXYR`V&ү(p+"t]TŚ&T*W/o['ܡ4VGkqNnCT..#XVv+] H"epyt"P|Cc!Q葫䋩+QU[@m9לZŒ (WXkPitF@,L 2O:vrT-*D9wIIfeP:Ɉ|ӾlT덳Ĺ5v?@œpdV9 = ;ѩ.\SLڷ+5̱K,- .|-g Ud_]jBo^RJ9s@}ʥ0ɺTOM(|~Z3in*PU9Bcԭoj;@=ljj #Paar"#FF_{H~1\.:Eb7g; "+A !|< D|%溚m0zL>|S$^/!D?"-wßhb2 jz^| -AC1,__Ԃ:0|VUPUPUPEHPs%Ptw:o RuP?8ZYlLq4  &[B@f }'y2D1?PXEIDӬ }gB` &T$rsK ,s;'k"-!eonO{ݷᾋ]v!u%8ILaR-jjU55X!i^"ؾN9>oRCK2ŧfQfc?1B0 SYhW]Q*Q0p΋r{8#_+W/Y?fsߧG=תTΪ)*$_ w%~حw18+t֑YNΪuuVK}Kmpi4OZ)Oտ] oT񼫘"=ƅ ~a7k*K,IPNkTp(b-_s^̻\EU!+rcH?jACN&_q"k"a ='΢(."  ׼.'w P_ $dۺS$Vmv f˕ưf5Z@O :0P劉kpRӜ@Lb PD^cLi! rF]eD'Z˦ Wojx~qJB2>lPd֥{rqIr/K~;4[L~%8cnde_4\})p"XCLӮӂXunMY/u-;5^#F&|NћeP3r98֝փEfm72.46;5GdeQ˭'y^%ڡ+ӭ'XX~T yRrr9u/pf+,$~hOw_:H.PoZ.N#x^Yg}92x8:+ѵF'RѦ{H}S*,ެʖ+n#.VTʑe_9Q uPcv(=0;^+VM[k+u#,Ꜯ~OTeedԽvE)p%~Go>$Yz; dKRkWtS?)O6_Vo]Z})ټ=]Knݨ[GRfn.c0:n_,~ nK5%-`[Fyt]?ul 0ɠ (?%^ ïjf*m>S_a$@%NڇN ϝ;jXD'f=8nffuqLrt[WdjNL'kEM~ʱu8щ:O(9*epFiE@_u" 1nG1KڪeI^>oDzQV-wEȚFf쇙AkAD_G,#k$Ƒ r?d:||f!$zU+X )\4L~*ϟ}{tLݓ7>ʯbvw 5xvEͤ<]r[@gK_a˨r{.5]k:RGwjMsK+Jw% W%MƖq-8LaXy-{ڵW+U5ky'%WoMdHIXIk 4Q,U -P"qrGOS#DmE%)#MH7P~?\SҏBsr0 Dqg_$AhKe;mܜT-vڌO,Vͧ[rЦܞ~uteK@Րd'KS2ֶԥ]}*{/Bg37z ]FmƾfCIuvBߠ#z ș&*BF\elwaĂvw̩-ΦׅIo}FȀ*01{tα9eYj&j%aKc*oE]bd$zݎnb\ƙ n$ 3A6+-ClZ&HI9 ?yb'y!Xxz;D0J;V{kf=<1z[6ɾGYfm\d^SնF͙rmMJ)w /5dZQgj;NϔdX{( 493#Tu-5A] ~a sƯM\9}n9^ KĎ֦6t۰%YJH=IAq{a)P:`-r{:Sb=)c!1{Ŗ".L5 /-#v6D}@;)4~o|5^j> *G(dt-}81Woׯ؉T4SF]G" nW-%I`[MŚc5&Y;ꧼS#&mnYJ=EѩVoSgdo/+wzdz;-ۼ5Fkepos.HoIraXwE٣tY`pFP K St7_HM)[n\F3E1Nk1hLAG_KjKI4d6k XEǃ/4w{ ݞgC0Њ.6t H=UkFqQP Ǝ7GZxQIZn))kКBK6L(|~taDZ~~& 0qI}M޵mϚ[vdGE~A&2r,뽶ٓ7_^8xŠLSν|m[Ux 3d ^Å?(^6`|B' ڔs7C7n]ll/M~[照1C秜xY`49-4n溎u3]&Nkۍ)Θlq0~ď `P&U$b1nei87㋇_  6L>s4MѼ( 8WiCʃc sW<c%%fUtѮGˁ|mlՇRƃ3ge\œ\UXyhR}M]rJƻ˟qT}kmm5oaؘ̒cBts*>iKPI)EP1ԟT?J 2zjc) ;1^&SRradrqYx[v?PCpC1Ǚٴ(Am;n[,'lM,[VN||e`dn IJݢlAGxwKW.K!LL~]bƦrA>.6)Qެ+# c՞P{H6LBitJu.R|qjsX~7I*'6 9v_4ZY%c"͇^{Usi`׏vy驇y?]isf QTB+ŀp\]s+]~E{{"- 1 X`QJA`Q9u|T<1>a˄t5{wkGF)e]:=_wF׏d7 esGo>,#}89 -KШ9nʏ -hnE$ߜKFc|:BJBynif!zRiئwtӱڊ"1:@  G_ P=ҝʞ e@KރwkVhͤFڮ3-!1)e7|qA/s߳D]EȩK. խ^Xp◾w"S}ĩ|uk r_V]] B0"7[R<%UuiեCQhҜ0(dGM8CX(3r@ Νʪ,I:S'9*!kͤk7Hg*ePEep>Tz)pe:*gw8<-q}KP,`DYڙS5S7\cҪuF-Xu г01}+RӲsuC%N+Q{|,xE2iGmg`_GpY.ΨwϤ%̠:sF!]PWpMT;Hہl$~}ݒsSR_6$gR]U]t 9. +19-<<r~Ƅ90!̸@I/hFA-ԁ@A_bzH` O@`8hҴ[h[iu:3-zx19idr,&y(r4Y[e6=6tW`%\jɂ7|C{M-~ِZs{u} 5j6ԢwM-Uz~̶nni޿.Y{K+.[qG{>d510bF*Dis8(jBHdG[1%Эp׈0.$/le |ztxb.]A i8cУQCdk` \B= NT͚ wld~vxt9_ L^.BrBE!TZП( l뾜Gv`Rƿ@n`0 &vb1+)m5S[2mAgQb0gCMl (2~LQs'7 ۆ7\= gn+dD~7xF dgF.-XIhmeqpJv) a4)YdK-ExXgQeTz$pLebI}ʕ?`Yl0 I$.oLlwB/BmkZȡRXX>0˒~ؙNΓf`UMQ1孳{e3˗=p {S^JѦ'J44fZz(B$va[ָ?sP4t?d-[L#"ԵGwo.cDZȦys 2l#Ĺ)S!aGK&AUEHE)l6d#u(6JEw4VXG"ĸ;İu,ԃi!1-@yإ{< V%: gOdئ xy?-at =+ǤU) R9jC%'l{ñ;ka_cKBZhQWܠ *io':9uFQEqOٚ}- >J!&ϓdqi+}9 L] n4?&?#HhZf; CI.F%+E"FֶAZ4{a%k.c۲6vŕ!ݥ!?N/ cdž,4+.8Qܩa4 wN Ni2p&ϓ caj)ꇴq҄l! kwDU@4M[Zm阐yRPd1rf40A 4K o] 7+v ᕡz~>j JC.)[*f7p퉑MS+w_ɫZ+O۪ Ӛغ([_oL5/bF5DU|j5|.>>0cy;WFw/( $Qj =o;8/YYNB'||l®#L)=lmc8 [I}AV~&GgwҶ̼$>I܀rp㑇ܿ0~0N75tɛvXqDWYG{$guSȔmfu5؉uʮ($ UjR}r`?'4gEϦΤ\S}K}qLΦ!f$'W{Z~F1LSF-t )b1Xc.46.:쩷.% D-elRAL34Jw1><'f@Rfi!TC)oZCOw]#1~;+yP7` :5}^0cjOIOLEC$T ?-THL:1ȉ=۔2v-=zٹbnQW'=>^4?xprE FMؽ2C$%TP;á&FJo<y"I*N[/HR)a`@~W뒖Bmg)3aNP~덆 Sd^&N30::8s?"ovNv"5;jJkƒ)D\b D0aOǚEcb,3ͮ;vr($Ǣ\?B=^+4zl8c)~P Nn,+cSa?n#Y2vg"=7tv\'41a$#G䣡/H 06QV7BT (6/4e .k.ҊUmيxO]{PĖ"6w K.{%0r B 5t Nײ,'<]`SHۓ3:ǠGgm(~x:engk oć4QvWaPW6H{rf`)VJVȤml7J[3uE=^!j!=m5i/[4\/"SK8U. ((A-k{Xrl4:Kccnڧwq0N6yE4/T|uD )WAWI\$BL* J5';4e^纃0: dA̮2m&TPC8s81?qxbzR$@] NyY* I9ZUAzZx{QTImw{aj-fpQ[<|*MM_⫦KL3cL\5{B޸;lZ)3;JO@t40ӿ3}^dꗮ}鋂SeC)sc p]*oׯ8O3 L~vOzۻxgo 0 J|*O{bsg ?{*=CǕa^NCn@Otۧ&EV!P2ɴ 4נaݝ[;UӅ[iCgA+^#8$LY*)-Ǝ0B♘ A/IsXL"~ԯ,y]CkB֋&R!n'J~ ؉E~=6e=%qItO2(>R8p`jz"c\ac3i48`?P8)T*S*E NY+|( ն5u WQ熬Y,^C5%% SW<{:Vڮ|> THLO5L)~I61M30zszÐGCžLGkQ>G.Ugl' }Ս\>"_wsXfo~<`/1p~@7iQŪRWdQvETh@[۽p?Ts6l R`t!TcP.N%,7$+KȔ^&x)4B.65*KGRX zG.?BmR;m$XI FʜK2\zg(NhɅB^*0 g !R֎(PbۦI+"UζlrN{㈅A.bpibhJ.K,(^+}S17r(C-5%婊f \a_p{no'ܴ{Coq*2c[c mb\#UNmKݵrQx^-'9P ";Ga&䐨HiM:o|@]3xOZrQr`oA@ }!z+xVv:f;0gV[! IipZ1-I w۳ABai.Er,3fcjcNJeBB97d̦N(t 8N.ܑq zx:( !AN_)|Dhv/ Bc#Lŷ T&'d"sb#~j*nGfщfn׉h}7cR]ӄPk·shtP 'J2𢒺pf?U3 Co62N} ZVx#43uhD8.,V>ATLJi zVA$rG HNӁ@:qTHsr)7<[  IBI-@8 N.nuHjQ;B8 q(ԥ^ϜŸpReMA lq!ٍQ0Z갈 ua9i8 pEIh /5 16Fux DȘҗ0bPJ!ǭHEt|zjFc8 |tCᣨ"cWxix_x8HpQ$bb&/H(zGxq"xpРOOrÅ_2֛֜Q5dM*AA? Y7H/IY+yWza}2}SK0TqL CU;0{@u!tWd5~Xe49:HQ|RcMt.^XLVī YƐ9$>6tcG3sZ50b _8u_Uٻ9r8 :5KBa#q;b%:⫌u=fs`zV^1"ɢγ[ i< 3c1@ ͉vR%/*26~|QPP?|2 ["7r tOBLШF1!QKaq/_G_v½E#jDm?Ж!mR_j/Ř$XQh#4RLѹJJ&-DB'/>wF"\~6}}{@2D%U_!f'LTي6PzƘd¹&zI9"\oiZ:GsEA6^bEwkLd@ge#9`$0\( L8f_w!oy` ;3Eqj1l_JonBc 'E}=AWϥ|L\988sU+C{R_Wkbq4(jUbw&N-1˟sT߄_.^OExz6B,=s_t~lIЫngcodѥ<f`8vLg}|x.@qƊFmȥàA{~XΒ|`/ kE~qD6RL#l?Tٱ %, $ aיnz{ѤN@!ɓ^.(Sy" Pv9Kxhڪ YuJW*>_m󠟲 U701ILdb%25@&hAgĔb /=`,8wcϰ68 g=ѥGJ&# lAD #F}Qy(vOuޯŔ848Fmc6`&dA@@LL4Y;NP]nɻ}r:P);?7IX vHe]vlZl== _waoo! pG A($^o)sc;ϲe$zofOWJG$?rZi…aBCa7ģĚ7WwpOזgj&"*"&$4z꣛זL?l~a߯?U/7*cy.ǕcrK@hL'TRO{SU b"v!*mdh6EȗSZ:,^wUw< :kbUYm]{?d{’*Ҫkh};w:>E.}ܬ- q%Dȧit3,e [`8̓C'xc6+2͇n+jlUUն#=o\)ɀ lpGLdSn,d## >t)~z9sڮbwdNpwkUI}w܍v{hx$I3O[X{ZC%S$ %5Zhe}nZoF߬ AS'MJ]x/ |pe~Gg8 G vdhyā<=L[|,Q(މj[rglǃ&bҴ뎰qП ;7i>{*0yj٣GS^> a1 M36"pvjj\_ ZU ~VmjKJ[*$ K ͤ[.5HvNǪpaEs'MI=1bN{<'r/bL8ܗz+=x:93{ᙂۡy}/G@IRNsOoKUpPkiKbۇ3%CZOՖsly3<sD-/U ߡ=oyrE89lBf.Tmyx?|2k,_{\5.II:*Zut `=4@`}Sw:DQ c=sdEjPpX >%j>YΛ>롑Zڵc ҥk-36#K>$T^<\pAo#񪏌idʮ+mP/5HO宊^ vD]ۢVytit/*Pr~O%aAGC/«~bMR)2;DGt5t?s}o ԡ+ŽQ0:['ɗ5{cw+9hLSUn#q˱\EeknrN#P?I'!0_%}PrMgU٢Fj0e >Ϝk3<-}}y]NZT(dGNc1/z[hnLO gL9h32oëa=Sr3COBX=ǞQ~ș܊:eW_ld}Gmg8,p/]ĵCuܣ%$\˷'mڷ?ۻN~=.w4~M;TOp/'O> &#ۣFg? /hb4A[T2*~LAݥ=ɛ[x79Ԙ)Rd ' mXV..WG@@ꐝšc~ Y_s;`]hh`,uVߑ|5]uqPD-~ljDOYTek,pϒ6IΉf㿵f '7ZLcDO͝N2pMr""Eޏ,KLj7 ŧϓkv _;z|>EiB%YFCu9'`hF[@#Tˋ PI|Sx`'sWĊ| n-=7e;Ks<[ $@iP*h$kEP˵w4A=_՟ǐNa' "x+F"i ~FYʐxJ@Ħ&=&D= ^r0&a`X p,(@KB 2X7ǀ{ `sں2| -m% C\%N䙠tO,QYcE/vvyWg| P"hgam12;:S Nխ{si#;G{.hh-OɍBRUďR zѪ{E'A.eK1Sr-j Pu&K0V/χC^f]5 Hc_w1#n/XCe3k799B(yD"*& ̥CpAO]hOI㲯_P"_ڎaAJUe i^=uli&cnlOً֠ӲM]Є\n~'>5$ȨKˑO8Ū%(5Q줅sd3[ŮWNDrVc-oӗwxK6\uߑU<+8\T/Z}0jslONN'4euhySk-?,cہ 8 xƣjȞz\I=^^9Xұ (7o?.ӏ!?+yLqB5~ǵ2 "Ɇ'q{ROR{ZA-3?J]G蛽v fgpu{8u 7Bm(l*.{,:yH9JsY/Fv}z#Y_z"90B`txʝZVbڊX=JwvPk D\&!z@a`[иvW"Hɿ %(=_P6Y %+b1b٭q |C V{ )8N*\ Nҥg>}-1M1VdAGBҷypb 'T٭/t1#Zx?:\`&3 s>ͭA%kRh?I'\?K/oq@y]ͪU|kxuRCiWsV{#`j"I.a\ D)'q纗g+#rCwJU܏wrB9-; )е6Ò֡1H=-L+ TA>uHL­CbagVCTO:#+2Q+. d_Kx*¿On6OͰ^dG8qO[ᮔEKHΖK(CdHCwU-²lyY6!xGkBTy*A!.x$qvfK>,*E߫\zǏ't4gef]ή(OI.Zn_'gn7 n]`]Aw)??;> ԃ\ɻ)7h!Kk`ԫ&:ɠ~ylTrK~?fiI 7e72p^.؉ ` DMKyʍ))x6R#.waeibB78d}5Y %ڶz7/i90?@_ h8h^t߆0.#XldFX W pE F^5$4x㸺SRRk6#{#nQWEHO>ppz⩏Y8nmړ)lAn_x'~u`|5պ%|OQe0΁-Y{Ӯՙ)Po'Zt-^.<|MzF] ZΜu8%ݰtXe'Tg@e;R#>Ji3S?ü q1{X״CrÎQ1̇7= ˫Z̏|%8vB؊MXPJ])Uܫ\*bcqM8b\0U3S5/sgN;ޘ#GCͫ*LMsle,=J9kbrX-9meȻںkEhk ΑE܃p?]l+4{ޘHPC+p29KB3Q!Ak43a ^nݼEiz'_r$dNڑ=+s_y-#˧FcY= W"rٸs!v~zFGsu}jmoqr^% ~P3yȶgo|m#UY4Eٌ'wtH#rvCc^- 7Kܝ Z Բ@\1RX^t1 */Ke, ^*4 q {$YI_qT vbwr {dwIu!s|q(eNG'@Uìp=A9?iMp>͞g&wee(e%>o |Ȯ]`‡]˱!>< ]w=:~'闌!O@:(DxqSx4fXIb7xi(Dul3u=K3W_Y2'F!邙 2ޘPG \O-P/E{@ ؊S1RGC7`zc  B[Z6!5p2ۤ Ebq93 $Ko?=}g Fa ڙi3H f%c>䄥.gq7ccx6Z/O> ǘ#2|{ũg. "C N x*D+B>ZfaTho#g4S XZ 8QQ-KIAz{. .۱T:T!D8++IX=sss'GQ}͠_u9@p#&rIq|ԃU({ ܦ1[+jǻ'W QкC`c~IsVKR;Po~qa >]uI vBD?! WV#k⛜6 l?'|O޿os5/QϪK u_@? 1n\JwBfIYƶ5sөWgO_ ݤiQg}?5=4d#v̅@_d3CKxCgF8OOcLq  |ku8> )aYOFB3ߠ4skS󇝤LmM):w.XnwgGc1tb$Mn$ ]=p fʓIi4l2 B&Ye X0~uRp떯4mVɺd!8;=\ Qc0to\Xx9 w=4gjCa졞*c]u| bM*d{N˚ ^㦿L<J1w-R>~:QL|aXHsc8ȴ 5NKȩT=t䉑 g'em݅S+,oz}{J}?J(e$M kJq9 PW^Te3&HJ:g,2M=`0UNsԼQ4ǧf)K<,V@*P:Cx#K-xwUsA6-i)!Hx$mq,3L!/uyt<8A8b$BZFoH7DUXrƒlỵjK&GzSu>F#ʸHq5* MFŜ"'1*I)?MHjPn.vzE6$K rX18A ap!3 |%2?3M$TH8aOI9Ub:A}dyY]J% 28N}\sEL_L@F cMO*(G@)}23qeJA ÝK7H2i~Z4J-NxTj:~R,NdVpQ"(h>z鱁G ҖI-Tw(6c 3B9S_w %\5ƾ(\h05%I< uqt6xqEKʵ quz\_&76wmκ4(!2 bw1Z-8WDZsVSP^ Ǒyoxw&n #9~Fy5IHAŤ#<_uyKh| Oƹ%!X{eW(BhAyNo:2ՂMr!*Ê-,NO.rr}ýN|ziy|)̰?ԟdM\ڜ 0I jݽp+8J\ķMIBnSi6a0Y,2ݺ6k81Rjd< y"*P5,:OQw" IP{b+!](QN) 6%Q /HG[G Pǣ$A{&OIEaqe uU }IK]W.1,Չj!RA@8!̤DKmkz3y6hTvgEZ:!N^k&.Ul EƈyDA=r']^31WB&#Pp=S)j@~! qw,KU̮ Y]R){?[O/.t q6_@$N бZoUsP]Lrӵ)tse߈;Ac_GG76GF 3"=-9P*K -s+m0TӴZ.@ QlMĦ:w_V|QK;:6"m:動. ("8K1Rr* ~> @ lת gW$}fzo m})!aS =udp0 PDQx$"S>y;jތ¨( G_UBWmbA'3:!.Q)5pc!j?tiJ K`.%!l90ᔗ`7+qU@Y .Gt_ "J'Xϑ~ T5"ܚa@vĀ .OHjw:J\Wjf sa ^p\^elvish-0.17.0/website/fonts/SourceSerif4-Semibold.woff2000066400000000000000000000333201415471104000227000ustar00rootroot00000000000000wOF2OTTO6 Z6 M``F6$8 DYUUABAQj(F$逓@$(D!fDY*m\tá˲ 诳Gbq@0.wgtkb"E?sMTkam1Q;(ޟ9q&ԝ(`wAh[FFAgւhݿJhۈ!6#TR" C%$254t{)m[LC3`VjD@6!z#1-y[~͇pI?uME{f&ؒ !d-A,JIh_1ۤg *XjaX7FLF@Hhgzk23!1/{ߪxyP6 $C̍ 1BFaĜQ`Ct X{?ERtpU;;~NS`NT$y&4H8g`I0=/-Sv[Iv[m _9dt9hJ[JeS@.#!3ȏ1{{HAj-C-AR (Z*Ύv½tfj"""5Uc3mm=:'jޏk\sob>| ~|StƑY8luB?A'=u>blp`pZ!OoE1zπesZCAP7%.AK":x_QMm-Da+s$f |s*GH;׼#ٿpEӜcu]n/X#h #0)y-ܥkAp/0Ê82(8P*Z1ʫOjSM,vfGw}yhe"s3a)wq+(7UnLⴌq0s{w}qqgFrIB#OR~{TGsͫ뚺dY'S#/]~ɑ[gtW]kaQ S?o/굪ץ໣^~z]z,0_:F)] ])n7Nn#3CGRL'n erC0A=aI:x1 6?| w YfW}(͉ lP(Xɴ13) @JVȭ ܫm>² v Kh+ mG҇#^G6j |Zefр@;+&EP#ժRhq:Ss|<+q\œfV1P >{EM^sQRXeZ[25?! =DsG U$>5 RHEi^%̏D+T!w4qVZDVOe,RDY^7YF]{C8qroJ8)^$g6 x؀&gLqc"_U[KDֵE͇ISEnrHtf䧎wEc=;`b#ZlVK$Մ2߯uET|B3:W8;;+W̯aOY"{IF#U_S38plLҟeo0|33>}rS?*x!4R7B!İ ]b矬C]aa4b%.0?ѕ }X (wZ9T ) t0.( qq/(,&dJJ1 =}Ĵ6ٴ&n7O(a~V_Mu);#{;vvL*0(g&ojHdK^jx‰{x8ߣ*Jdf=RN骮g2gZUfJ~C@t>Nλ<ҢTp6θ_ !ڂ HH`gEJ~ވ(xgH`/b( 2fsmܴ\DO\4YE%ۥ#ɒX3ňedSoP8硘Z1n)rQ>bH]xy*uf"6q$0wYv>l&;/ OS8aCI/P(] |.޶q/-n|u*+8󯑨@WxB >}-4_uv 8WUaϞ(ϸkŗ}5~ +k &(v滔}UJ}G'jShJ_d?}Q hGJNVc%/zQ;1mn5:@7k~{NL=-q5m;N}b5^jn-kl1E`NKY,w'X*_jRSqU5y \/hKs R"EQ2@'m+W̯ !IXkh|`Ӏxa7$VssS[,'W&0xE2׏%,99/c'2k/]W[*O[pgwnFH.>< ŷw'`G0RzcZ[5ܠ欄'-qH'#z%#:;Ll%t*s5(C\SdI3oImׇ4aP5u*,b/oһD.4 #}ؕ.L<۱B 2'ryOfe v)I)DҽnҠ(zwƗ]R=-WX,유vMV+MkkBµVJѸ;uݪ8kyބ!M O<~@(!Q߻ѧް5>;'s1^F&X@7 =6M<|֗X /xR2[E7T:9;]>2!7zA!}.ܰW@v.}yXr@44HɎC7H& %sk$QP)v?l@;RT1fǗ1;U|ŻYb0@;]A;p4)^Ru#gЮj%ڜcU hƏ__\7JWS$Q1jol/|vwn`?qMc6.LK#d1U+OJ]d+~\4UJeJd[4/N,t,D}j['r%X# hRҀt_!˃.mZ]{Gg |2KFw{&We~4ܐwR5T )S@:>~"r<92, LN2SѼV+tcE\C[pNmR[_A16/ɺK96-~5wE߁!Y۟פ)b.7#ss|c?ޅlШ6̳Z;ImR" W/=ҩYBa3YZ޼n&Z8@`A&`08D P{^VϡlzaHJ? [|Ntz+џ^&IoܱQdGD H":f8񽰙 9̓ *;{vd@.geGLޠ#tcDT$22 +waqoaYc,Eo95Ãj!xS@w6%v<]5t986ߦ pt>/,ccbM甙XyBnA'3l!xSSb\y6ohSG"ȷ $C/!wG}[/j㍡ƭo MglORl=W׷2/T<, dHَOGRM`#GFpMPaDl:)*U?n8QM /)mC xߘ!Ֆ$T񢕀\sqxmh^MUnUU@z+UJh# vS#o]2-dt=]WYcY.$ϳҶp,ug9i7!Bw->TZ{٤#Q۷jzfJX?gae v̈H/dp9x.ɜ~Z6wvu v-<xQ;?R 5ab7so~ @~Lˠ/wZ2y*ϔΣlqQ@r8 .0$7Ґ҇)_{]I.ɵ,"u-6qiܑmtFcA-Mj Gpc/Niץ>;&+7M Oq>3ir:p0Ҡ)ܫlnHBgW#bVҋ 7ٗ3`oloY'tH3!/H#LN/BVq;S8-X[N4s`v N;,zx N dcvکm{S?zj[4INd^O;? K;073n!{9{$"EYm9|0بEo%^-N(xma2*aBZfg ܨx<U}9{gve?wӣJq0'孥,P ?@?(WUz5]J?1V> wkܓudgz%$QZyė'M#~`2y'Cq*e|ܠro&h}Y#w=[9 CECdl *=F98S@s)Е4G-Tw6n]77=?)yTb{usyбՆ57 fOr9ɱ@yw_$uAZwA 8`X0`n<@M dd`dy`  81 )J{]p=iz^r!=I'mZ3sxV=kkKMDabP9^H3p|$?׊Gy.#rXWEg>;Һ`.UřqP\omF <ZD3W UnUULq\Bqª"(UU_)>`$9$<(X'N=(+Uv}%k(5n3+ 6SEQAK VY 5Nw(dOShbYEّfv[OH=kjP3{#U$'lˏ!vyv/`߽Q&xoqv' h4<{)%q#?&}c= FDؙZn)[}c{ݺQm.ق3{*$.ei!sS]O^[xsK7ΈmxsbX vLٿӎ *pp ZZ6['a[^vҖ;Z ȳ67i[ћܾz8AZc)USrLsrA^0L!IQ7n20k|J1jӄ_E2cq/0 +ҚJG9m])joZ? ʦ[c[f*9hЧl=>H|BH@}e,?DMꉃ!eI?yã:?HEvo/[~?NvRMAg⤠{ñPZ>l 4on QTc`mPV]f.QܢCŇ bόG .e@A?|@ \Mm0diRlzIDU:5.JIߋR<'Dr*uVtUxM#_@KSbfH&HvtU?Voh}K}ȱ֊l` Y !0*_ҨX,ʫK&'uty !K*4ZknwkHޙ2TYjMiaJ}p:'\S Dnc5.yϖP]_F)ߙٷE>>Eg6z0f̍Tⰱhu9cƇ[1d)R,嬚ؑ | GҨ ( ۾RKW>.~S=I#O1]^OrM@-5f1GZ{g,Q5&z¿a7%ZLUnu^m/ َt1:Tm [uaZѡ^ҩx dؽdܪe ɡ^+ .p( 3m_ŏ,l(##\atug]Mê^c2_Os-PcsOQt3wfNs=J̥&\̏b9>99#>LVőԊXyK՟}/fߢݺkdVAQ*tMJ8}΂ &ֶ\RiKbރV ƥ)N4ۄ1,]~qb)W:}vx L5WmftkoMo)w ʷw~6r1᥄BrH$]b՚&;1;#?$HMr{p̽^fK\xm^ߖlͶ'5޶uR^Z{a6mͷS{|tPfGT\4wTK~k]^|?~2'iZV~^iH|mXyovi*%S.^XtD*q'd^ ԏ)熕*]7o/Cd3ސ_>PX0l\INEbͽp`m{(%T@ ^FqŠER.'oidRieg[X޶L胨$Rtlpqp+ qSRx6"3dA+GMiD_`Q+H|45[ -bl_OCa68]=Iv _4<"^9~0e!WMi8"SqP9+__]Xf &}LC }{:[ũHvxLx*N{v&VK}.TqĴ+i^F[ǶB? mTA{8VЉ1c:ьMBP ,+! "2շྭeP̗Ӌ|u1=q)ma;c_w V/̫ = 므z$˷!F}44i,fQ8|:Z.-a1t p8,I枑!2ݢp6guLRN,Dv4, +rxg2F,!RAbrӛ k|;:Hzjτ-ۆsUշ1q{H [lRk5tCB]! []UGbq͔Ҕɡ EK:=Ȼ)Ŗj1k bdX6UXրW(xt"WLS\ /7XEjhan QD]DWn̘ͲM{?_Հ eA:=#ܚ?΅=݀wc-r߼˶N9C7n(2_v`Dg}`5I3(R1oN{gx;p rT>+4 v乆K;;[v}a]uߢc6~{&re%ە"LUi?֙ux7JnKB|ksKl V,Vr)Ӱ1.0~w9f<{@̤) y\A[hg> dկ(L#J gkTvÝږFH@$$P7O$gXޓy * )wQL`_|~Ah:-Yk%c ҵ!{3GO 6tu+r-:Ҝ vG1vk-(+=T"4v\?mŃ5>\8);Gw.[->'fO^..M0'EG ; Ke7.Qa GQA)rA/0k\ 4 GiA8;_JFB )p,PomHBS)`]) ?>q(O4Ӫ!EUpG:ul`z$2>#?o"2sc5:m@Kၾ{g<[b'CC%D*̈1$gqu R$' [Za-" $4 V TmU(@j5/6JY!iAAe4p&-S7as-.mZaklw8bYKHc+ps;SA68K똣z:DI$ºx f7a06pl*0Nw LtbvOx>Y7=e&mcMGct*@ @ql10L;s@  !j푕T]-LB 07 -9t9,1&ϓ-WJU URQA:_˙ib:^@'`8,I`c={ 3ON;FqnukNeα4BN'iR#6S[n\kcYYO>hu2_m .zEϳ3QvSD gL)% Կ." 3L +O41XX)n (n.b:P>:vX~-[~SS;t tuL/4@IJ+bQMQ @bsFJ'Q&j>6M2fPVvhkX;2H |Klj:,ir.Dx xoMX+;f7Z)hOwk+Ÿ4^V}zg Z~H8o'& "pլ[ ֳz͞HYڲ@@+ v\.]g3E[28o=])8l;9(T-DDO8IeJ8&_R kY'Wh6Tg*ϑB qc0pJA?\\@"5@Gz ƀi6k@i@,{֊z ?]<g#e΁IT` s|״DG9WcNa-A3$CcV mƙz_{p&YbsZNѫK+9@D7P2( هlaA z@~}ۨR" TcКn.ZJӎfP`ق;`R9m7փ9Z{uT~ݾ3;a 0e62 %x*цֺK ^ bŅfð1$a(e\JP('S`2:@[i:.4ybȉ9ښ$ђ&'+彵܁sӠOT1) 4k42A"<,-ˆl`F^GӀdR[C%dV}xp2zorl.ili;cFL ݡ;S[ؕD+F7!#C'xxF*UD |r=unΘtmn4#Ps18fYp衯̟]{0cJ+N٢E/)8«m =!#{v-'J~&+ p0 qMt]4Pc&?gC[N|ԓRe&K"k"A#9aFs(ׯP|+ܚkJpA֪Jq6Ɉ%&gZiM PqD5Qa̰akdX#D [ F"e蕭5h"\Zc,V^㛻e̝݄N}+#'ψGzE 𪕗o5ބѩ Cs|vN k?vPLT*S_tY>rz4r'o@i'CKvS'vp,I6'74hv"Nt5jțo`O=[+ij gb(L6=02+Ʈmf !fG5#ês5HHs*$k: !cs = M_qQmfo c=Ax~4{ !"I/USw7B}^pou]BҬ#o ʢvfX|TM.^" ))F%Q車˝IDE-a@oREc+{4(LFTL:4 +ge1ld iTaECܒ${:_1f%6 y1jsmO+JtcF"2`mN4j_SnLW@aĂE'U`<շ.g1_웉%ֹߌL4d>=.(U%!D)ɼ ur\ v5ψlq'O{5*+6~u:WMܔ}  ~tŝf=# l]a [N'KʴٵP$)c殘*8StpO7૘DB2t?[Zz 5i֪S>H9;]q8Ȏy#Tx१H+;LDk,H6L e jIQ"VKxeޣ-(ʕbbQ4sGWk?(VH b'(Mيd\2/ȚN%%xz{,O) A%A"B +E`xxTz뾐)F/.2AW5^ Z,l&{c jjq٘.k~-틔/pF= lݦ]l?j1M[e"oԫV&4 b#{I3sxJ89r3[;`!OF3̏xEg ՘ h@7pe[{Uza QS qI1z tcL3p!:;,!o<BE+p'+d+ *3.jCu΋r3rbnb5SݽyBnd:H 2u[M(:m5~elvish-0.17.0/website/fonts/SourceSerif4-SemiboldIt.woff2000066400000000000000000000351401415471104000231770ustar00rootroot00000000000000wOF2OTTO:` \: CH `F6$T F[#@wT6QE*oō!!h?JujrffVsw7#(C(Ɣ qp|lƕɽb0] ח~<4^[T+A#4IwAy("Ve6Vb`:^Db2]>4LOHNWZۊ75[UhHʩVts|/ dA]C,TXlz)`EV m08h6R3~\@rBMkv"O%tK#'zƻGO?8xmu1#+QZӢs @JB.вtiU06,D*@4ţ jKrݝv*">)SfwkvN.LQďqEi-m{._/`ۮVi04ΛoHFb% &a<^TyI % .m--)Tx\j6um`f/:` M{ V-1$@ DT9&YXKNV4P@pܘb>jY<\2Ki}i=wTW粑q+ V)c樁 ;+N OIewB, 8kmG\7iANKZfT̳>lrĿZyk9"Ae3Ec 6 h2/NeNTTnYVn{^4W&ӛ57廖 [>E"+W/0h]l)xO٩|.ĩF26^X'n$ZJC75/,|bY^>uAڤJ/̴$.dEV86?UQC=@~`tW_/t8t\Аۙʒ 6ir~>gN MM>:>~Eˋ3)@K $*) x]YL 7 QED ?SPk!LzEAWϐ!V<i@Lw'Ftec!qHB( gR E8L$vsVEcXR>đYnzz1KTՠVGzgl޵avpwy$K괾~R`U(p^EP1/Y1`iP""^l<ՌDKnn f'>죀if/xQ]WY[Y׆?4éXMŖ:lj|VswBh\j`{mʹW0@+׮"pN˟SK'Ϛ`He2 mrbdͳ: L#]OI u462 Z4D t'*@ۏ@$[@T|ȴ)_٥Xۊtd`1H۬ո},ED]Y"Xۏ/75Z1KQRK1FT4[]8?Ԧgm`f3@ 4]6voBya0E4Ҵ`xukMSg0'S'g2t#Q2AݮpqZPt) Am>'#3WE?gєI*8 x\~;,UAwL`ص^8]YU)#x0WI'RTX\B U<I$,5"yIBqעU$D^"Ux߄}$]I.N$M:!͋~rШȝͩOgH"1/K8u9:ʠ `Z@7b* "<2+!yߟ0䞥e`IiZ: ?h} 73CC^P\^6) "mtH} Jl0y-?dc ˋ9"gjAΜuz!b&'hWliBbM Vnyhq,Ƚ"lH0;S.@qq%۞O?Nl 9Kzp1Ξ?eO)b1$M%VMwgCKxVS܊eis8 t8~x朻ksK 0ho04qKgN;Y{00%̀;bLiP,%E]H89|..R5ݸq8q#x@x?%O=ZKr[K+VhvfK5MXoTMjI=.~5s6otK 4=ǷZl 6wCm#Um aa%\cJrJUW +&G#1>>pl>o-h XPKJ6VO4ZJfRfc8 ]גwwA88!&#W46+YAܓH& cviY+-xsu-:Zsq{K0ALU(},z RcW)w yK&Hwҗ'~[yɞJ}!Y_snʍK&WZ<;yy@۪ppR"-zuӷ8 'BɊv11pΓ/dx!{BiChZښ#UBo" kO8н61lFt첽\'TMӀzdz'eys{#'Ξj#f6WzggI6unk*UCY͈RDb_E:/M]Dime g(ei`Q1h@ ǚGFqۉ;_XU a:7e~f$C`{QЌ[]2#ywS'x H #% Zvii۳.}YƷ>\x0aWt9^WyʮohdwEDgW 2-M^Uġ+wFqJE9[ 𒂶툉3A։BDjyәn٭iׂVV™vP3`ٗe"3ڸcGʖ(&>!-!CbL؛0Tl< # i-lS@xOyy@凂O0qF|EO[c=\%vth=%,*P-)لGw=M{X fWLnl+LF9*k$V*!dDɵzSr gScxMCυAש7?oG8 TS:n%FKm+WXƋxh^PwvZ +phÚeƐ߻:Q>z& 57~7?>FY]RgslBeNɬcA;,6ChVm`W`R !$բti'N\zW>B^fAl sQ']lvTE%b/×um-l~6х!k7CNwG$[] z>?)+''Gn%Izr +%0H&E+DnM޷1`.>>[!dYkx)30%GQݪ_Gb;"]JA+iC#G]ћ=+_jm5SAeNBJ BM=ͣOxR&zV8Kn6@d *Km;h1%,֗]{SNy lJPB_R!ΰ9J QSܿx'mUJ8qjlJAohIZJM֮:H^N_o`!xC FSbMC K#V2Z%Sњ)R2_>;F!Łg_\IGn kŤ2d"$:]hɳAYcR.jcкQ !ң b[ @6Dl#@(@)@ @` ـ@`%499 44 0Tt"J$E,9)qe\U롷~xdCaVZ-k?;hǟp5*J~鲞jMwG01i7:W"bZ+d5fwNm}NI?E8?om3<s7o{7Cf#."1 KCPwY"]-(aKZr+y} Ӧ3f*aBE#!o<:#q!1&tL> -i%Aa/IJD>Cߛ).*%60dɧ=J lhQ%Icx|9|A-.'1RkJ4?{!/J0Y͏-Eb{ $ Y) 4-Y9ja)y69!?4:,E"@fAqS3(*%ts|M=>Nm> hk&l A[1p(8~ئW3',hyHK8ˉ$$5 1h;y`7I/\r aE1Iڶ O$SnōZ~?v%$gu+t<[ ߂גJbשpn^<א3\ \%6Q?# JTYq-\q*stw|)V۶0X/kLr2EVը#Mff#!-}}5"nHPE 2ncxuWȦQY&_ȒBt<ޑ*%3$i(S^F#Fe3_Xb~FeM<ۚ:%2a`wL3IJ?CL5藿C5{|K!JiNV` 3F@~y,TC1Q\gر(M|_89t_gfTNHzvukAj8G11s4>[F:%X:8EuEsڜ^cl ,dPT ;)zaVV`o #ܼ!ٽ*QC%Lv \Yt\\ȔS-[EvH{STZ*@$BT }%= "yrЎ Ҵ1kDIN}=s|pΫPڃpwD,Tf1kPKh\l@~SIZ|KB )@g=]OkCɜ(Sov'!ž>;RCe޻6|şNϞ k cWwbnnWayV*qzpuE5u$b`Mֶ|Isoy?>i[2F/FeѳhbBW0%\pWJft{UJR#T*ͣD:dө+ppl56Hx3KD%t? Ӷx;ȋ*#iܰz#,(i AThM5HנS=MHsΟ#u 쁖*I;0 ~H}}q?oW0ǤXxW0v[0^*I;/*b[W8'T;7D:̈́1Iˢ{=˹ T}}t8 !ܬ ='e?0w'\㫏[3Jt1^ݵ7tynG!Ј8U48 fF]]rY E6GPz'D#QE,H~bV֛;ǹ0"9➂wg!P?tÕpӝ< /Qv19t!pR#giHeB,jg.3))R9"<&XQK>-t13Z .'fh6L-'p*W@Caf 9뽡+uhlC+'Èy8 @)Ē|%P0.:VTKr"[indD{]jW9C)AxwD|j1Z}r1M^j+@T󄂡^xo-ńZ{ۦajD#[EE+ $n+Pƣ :(~3d* }lta"̰HZQdI+miAM`;6ՑDs~7lQ+KtRr᧌?r<ֵe7mX6/vMk-Th: +I2L^owSvr@+j6,GP/94Z}"tގ FڮUofun;;98CVu-3oCzl\'W"d]r l} ]LꄺN[e~@[[ 6ZVMl<^alj,Yg|K}-UOe]/]$as,ۤ33)c㲁GYZgd ITA~`]u|@ǫ߈[{{8`AYӭ9"koo1tXAHc- 0 oDqPL83<#k OX!)+ňd co_uFS=\f)f|;lqy0E_Rtqه}g4 (+ &ʜ:R)Kﴅ}WIAˣnq`ƴE D cz.:Gz۴LБ}JXV?,\z @E ;X,o`?BHovPof|aj; 9&:$43jʙ?Sv^|)TD. %8UB ̑;Nܟҩ4QOғQ%h$Kv^rV@n! '#wszPw_̧yt;2阗\>{e^Caezu'VSK,X,8@Wߡ?OV~.WdݒrB-6r٬^?7,kyĂ 6شl"q ~ ~4D yJ>H%s@x|ϣ``/s9;BWOBT+ο1k@Vw$ѻSm*|ƀhMP9{ysģ n0TJDJ3Й|Fq 71چ`Dάx{w͙x8_9ޝ['p)8S.XPE#w'3PLzU}+>Ad_{huZ6%Et&)" >:g|kkAS [chi7t5Z[e%J{r*?\Á&@ r?99h,"ou~}]aԾO,J . ,pt#ĜnH {6@r/RǠo.o~d!)MW0J7w0bN`+pQ#Uw5iH['}fw|IB hXR[NO̝,#}gIB}585Af$wY4 ^tQ=vyg[b;rwF;b#wT+nl촩زfsOn.X4 '^T%: SЁzI;xwX˺ݗȞ'N1^u5=f|i,?MD^ ^$ $XN'+%z28.ۉyl/R]`i#zm]t@ͬ,ȦMB_I呵x Sޡ D=H>6(',)*9%]"6`^Мn(Kq)qA{Rv!qkB1Cmv?jBw]7uj!i^PC`&>ȅ/nvyZ؏?( bR$JDu/!!68x]-TuA`WTn K\oL9s98I^6_+{xζ f $Ex$&F~g>p_М)I=aTX\V]mQ9x>AFF ++ *<=ZB0bOŗF1 +rJtyf *S j: 6wXTmGPQֹe#j2.)hTPn(oRʁC.RbkY%nPE:;Bг<~\#'{;|1 g>U,4֣@5zk-^N^*3NbPfyU2!փʈ a񡫿%dNkRZXdc0bc+9%X;rLDQm+ȑ!!ƽW ~o`B \3X J~^y/L`8M,p&:-Xv7(:OP\@O9Q@ 3Tzv `GWL.a:,j{b>c'Y2(~b.S1M glO1m (_$5!¦-Y͖UkGJ@yQCh1b,S /ԶA˼HxQdzƾ^n%q ~"&*+o,t :_L . ܄ez˭o%9h2rcfmg`ҩWIK|(g\`t%~ԂF"gr[x1^PEؑvS9 #Y!ߗܲUUkm2%q;].OKo}xqԺDn|i{ҕH\wZv"CX[UVgrJgF+FzQQFto`ŋq/mMVvjPa gEȌRbuYrrF=g,i (8RjpEm&Y61M,{z= ӈ)0CrPQ|9NId„OJ-8 s)JIk]hQqpD&W}6YZ+^O'%]Q?M28+C`X.HAPS AMQ)3E8+Y̬3(ӹ/S#FC2fmW;dVC9K~ɬe&0aT}[8P\0smD%:H8Kj|]]ebU dIC-ͻ.[`9d:H^aՖ++JKhS;ɈD /pJ$(Hy8Q~!E(%ܡ d"E.5uHY *{0͔M%]wQ,m4mY1@T]go U2efY?ك štn.t,Ck3#ٖe}{8rG{⩷>A爧5OkJG!m|#UZDr]cr9֢VZ5YSe P-g. HcW]ݓԸGcSf_Ne̓+tmߢ8S]P 1m<_Q͐AXמ =t+`wC#.Ѻw80?oIAKt4T\QQdmRK_ 32ECje6w+tqnVf}JN/C+PTji0xL -TE*0 c\<x_ ;ޙ%y-C9{Vǚ3O"KQV&A$ w)V.;Er}/i%m#@ v` ~֊: +ϒcNQA@MÈPS_"2!co_PhG`' Ҝbps<أej95&Q##us(\e< 8.-G' ๕tO|h$:"}ѵy`n#uJ"LmQLyU#)aݼ0nL.X-эғWHǩ2rh `(pƁTielvish-0.17.0/website/gen-fonts.elv000066400000000000000000000021351415471104000171410ustar00rootroot00000000000000# Download the fonts needed by the website and downsize them by subsetting. # # External dependencies: # curl: for downloading files # fonttools: for processing font files # Subset of glyphs to include, other than ASCII. Discovered with: # # cat **.html | go run ./cmd/runefreq | sort -nr var subset = …’“” mkdir -p _fonts_tmp pwd=_fonts_tmp { @ssp-files-base = SourceSerif4-{Regular It Semibold SemiboldIt} for base $ssp-files-base { curl -C - -L -o $base.otf -s https://github.com/adobe-fonts/source-serif/raw/release/OTF/$base.otf } @fm-files-base = FiraMono-{Regular Bold} for base $fm-files-base { curl -C - -L -o $base.otf -s https://github.com/mozilla/Fira/raw/master/otf/$base.otf } for base [$@ssp-files-base $@fm-files-base] { # For some reason I don't understand, without U+386, the space (U+20) in # Fira Mono will be more narrow than other glyphs, so we keep it. fonttools subset $base.otf --output-file=../fonts/$base.woff2 --flavor=woff2 --with-zopfli ^ --unicodes=00-7f,386 --text=$subset --layout-features-=dnom,frac,locl,numr --name-IDs= } } elvish-0.17.0/website/get/000077500000000000000000000000001415471104000153075ustar00rootroot00000000000000elvish-0.17.0/website/get/index.toml000066400000000000000000000000571415471104000173150ustar00rootroot00000000000000prelude = "prelude" extraCSS = ["prelude.css"] elvish-0.17.0/website/get/prelude.css000066400000000000000000000001571415471104000174640ustar00rootroot00000000000000.notice { text-align: center; background-color: #ddd; } .dark .notice { background-color: #333; } elvish-0.17.0/website/get/prelude.md000066400000000000000000000442511415471104000172770ustar00rootroot00000000000000 # Installing an official binary The recommended way to install Elvish is by downloading an official binary. First, choose the version to install. At any given time, two versions of Elvish are supported: - The HEAD version tracks the latest development, and is updated shortly after every commit. Use HEAD if you want to use the latest features, and can live with occasional bugs and breaking changes. - The release version is updated with new features every 6 months, and gets occasional patch releases that fix severe issues. Use the release version if you want a stable foundation. You still need to update when a new release comes out, since only the latest release is supported. Now find your platform in the table, and download the corresponding binary archive:
Version amd64 386 arm64
HEAD (Draft Release Note) @dl Linux linux-amd64/elvish-HEAD.tar.gz @dl macOS darwin-amd64/elvish-HEAD.tar.gz @dl FreeBSD freebsd-amd64/elvish-HEAD.tar.gz @dl NetBSD netbsd-amd64/elvish-HEAD.tar.gz @dl OpenBSD openbsd-amd64/elvish-HEAD.tar.gz @dl Windows windows-amd64/elvish-HEAD.zip @dl Linux linux-386/elvish-HEAD.tar.gz @dl Windows windows-386/elvish-HEAD.zip @dl Linux linux-arm64/elvish-HEAD.tar.gz @dl macOS darwin-arm64/elvish-HEAD.tar.gz
0.16.3 (Release Note) @dl Linux linux-amd64/elvish-v0.16.3.tar.gz @dl macOS darwin-amd64/elvish-v0.16.3.tar.gz @dl FreeBSD freebsd-amd64/elvish-v0.16.3.tar.gz @dl NetBSD netbsd-amd64/elvish-v0.16.3.tar.gz @dl OpenBSD openbsd-amd64/elvish-v0.16.3.tar.gz @dl Windows windows-amd64/elvish-v0.16.3.zip @dl Linux linux-386/elvish-v0.16.3.tar.gz @dl Windows windows-386/elvish-v0.16.3.zip @dl Linux linux-arm64/elvish-v0.16.3.tar.gz @dl macOS darwin-arm64/elvish-v0.16.3.tar.gz
(If your platform is not listed, you may still be able to build Elvish from [source](https://github.com/elves/elvish). For users in China, [TUNA's mirror](https://mirrors.tuna.tsinghua.edu.cn/elvish) may be faster.) After downloading the binary archive, following these steps to install it: ```elvish cd ~/Downloads # or wherever the binary archive was downloaded to tar xvf elvish-HEAD.tar.gz # or elvish-v0.15.0.tar.gz for release version chmod +x elvish-HEAD # or elvish-v0.15.0 for release version sudo cp elvish-HEAD /usr/local/bin/elvish # or anywhere else on PATH ``` On Windows, simply unzip the downloaded archive and move it to the desktop. If additionally you'd like to invoke `elvish` from `cmd`, move it to somewhere in the `PATH` instead and create a desktop shortcut. # Using Elvish as your default shell The best way to use Elvish as your default shell is to configure your terminal to launch Elvish:
Terminal Instructions
Terminals for macOS
Terminal.app Open Terminal > Preferences. Ensure you are on the Profiles tab, which should be the default tab. In the right-hand panel, select the Shell tab. Tick Run command, put the path to Elvish in the textbox, and untick Run inside shell.
iTerm2 Open iTerm > Preferences. Select the Profiles tab. In the right-hand panel under Command, change the dropdown from Login Shell to Custom Shell, and put the path to Elvish in the textbox.
Terminals for Windows
Windows Terminal Press Ctrl+, to open Settings. Go to Add a new profile > New empty profile. Fill in the 'Name' and enter path to Elvish in the 'Command line' textbox. Go to Startup option and select Elvish as the 'Default profile'. Hit Save.
ConEmu Press Win+Alt+ T to open the Startup Tasks dialog. Click on ± button to create a new task, give it Elvish alias, enter the path to Elvish in the 'Commands' textbox and tick the 'Default task for new console' checkbox. Click on Save settings to finish.
Terminals for Linux and BSDs
GNOME Terminal Open Edit > Preferences. In the right-hand panel, select the Command tab, tick Run a custom command instead of my shell, and set Custom command to the path to Elvish.
Konsole Open Settings > Edit Current Profile. Set Command to the path to Elvish.
XFCE Terminal Open Edit > Preferences. Check Run a custom command instead of my shell, and set Custom command to the path to Elvish.
The following terminals only support a command-line flag for changing the shell
LXTerminal Pass --command $path_to_elvish.
rxvt Pass -e $path_to_elvish.
xterm Pass -e $path_to_elvish.
Terminal multiplexers
tmux Add set -g default-command $path_to_elvish to ~/.tmux.conf.
It is **not** recommended to change your login shell to Elvish. Some programs assume that user's login shell is a traditional POSIX-like shell, and may have issues when you change your login shell to Elvish. # Installing from a package manager Elvish is available from many package managers. Installing Elvish with the package manager makes it easy to upgrade Elvish alongside the rest of your system. Beware that these packages are not maintained by Elvish developers and are sometimes out of date. For a comprehensive list of packages and their freshness, see [this Repology page](https://repology.org/project/elvish/versions). ## Arch Linux Elvish is available in the official repository. This will install the latest packaged release: ```elvish pacman -S elvish ``` To install the HEAD version, install [`elvish-git`](https://aur.archlinux.org/packages/elvish-git/) from AUR with your favorite AUR helper: ```elvish yay -S elvish-git ``` ## Fedora RPM packages are available from [the FZUG Repo](https://github.com/FZUG/repo/wiki/Add-FZUG-Repository): ```elvish # Add FZUG repo dnf config-manager --add-repo=http://repo.fdzh.org/FZUG/FZUG.repo # Install latest packaged release dnf install elvish ``` ## Debian / Ubuntu Elvish is packaged by [Debian](https://packages.debian.org/elvish) since buster and by [Ubuntu](http://packages.ubuntu.com/elvish) since 17.10: ```elvish apt install elvish ``` However, only testing versions of Debian and Ubuntu tend to have the latest Elvish release. If you are running a stable release of Debian or Ubuntu, it is recommended to [install an official binaries](#installing-an-official-binary) instead. ## macOS Elvish is packaged by both [Homebrew](https://brew.sh) and [MacPorts](https://www.macports.org). To install from Homebrew: ```elvish # Install latest packaged release brew install elvish # Or install HEAD: brew install --HEAD elvish ``` To install from MacPorts: ```elvish sudo port selfupdate sudo port install elvish ``` ## Windows Elvish is available in the Main [bucket](https://github.com/ScoopInstaller/Main/blob/master/bucket/elvish.json) of [Scoop](https://scoop.sh). This will install the latest packaged release: ```elvish scoop install elvish ``` ## FreeBSD Elvish is available in the FreeBSD ports tree and as a prebuilt package. Both methods will install the latest packaged release. To install with `pkg`: ```elvish pkg install elvish ``` To build from the ports tree: ```elvish cd /usr/ports/shells/elvish make install ``` ## OpenBSD Elvish is available in the official OpenBSD package repository. This will install the latest packaged release: ```elvish doas pkg_add elvish ``` ## NixOS (nix) Elvish is packaged in [nixpkgs](https://github.com/NixOS/nixpkgs/blob/master/pkgs/shells/elvish/default.nix): ```elvish # Install latest packaged release nix-env -i elvish ``` # Old versions The following old versions are no longer supported. They are only listed here for historical interest.
Version amd64 386 arm64
0.16.2 (Release Note) @dl Linux linux-amd64/elvish-v0.16.2.tar.gz @dl macOS darwin-amd64/elvish-v0.16.2.tar.gz @dl FreeBSD freebsd-amd64/elvish-v0.16.2.tar.gz @dl NetBSD netbsd-amd64/elvish-v0.16.2.tar.gz @dl OpenBSD openbsd-amd64/elvish-v0.16.2.tar.gz @dl Windows windows-amd64/elvish-v0.16.2.zip @dl Linux linux-386/elvish-v0.16.2.tar.gz @dl Windows windows-386/elvish-v0.16.2.zip @dl Linux linux-arm64/elvish-v0.16.2.tar.gz @dl macOS darwin-arm64/elvish-v0.16.2.tar.gz
0.16.1 (Release Note) @dl Linux linux-amd64/elvish-v0.16.1.tar.gz @dl macOS darwin-amd64/elvish-v0.16.1.tar.gz @dl FreeBSD freebsd-amd64/elvish-v0.16.1.tar.gz @dl NetBSD netbsd-amd64/elvish-v0.16.1.tar.gz @dl OpenBSD openbsd-amd64/elvish-v0.16.1.tar.gz @dl Windows windows-amd64/elvish-v0.16.1.zip @dl Linux linux-386/elvish-v0.16.1.tar.gz @dl Windows windows-386/elvish-v0.16.1.zip @dl Linux linux-arm64/elvish-v0.16.1.tar.gz @dl macOS darwin-arm64/elvish-v0.16.1.tar.gz
0.16.0 (Release Note) @dl Linux linux-amd64/elvish-v0.16.0.tar.gz @dl macOS darwin-amd64/elvish-v0.16.0.tar.gz @dl FreeBSD freebsd-amd64/elvish-v0.16.0.tar.gz @dl NetBSD netbsd-amd64/elvish-v0.16.0.tar.gz @dl OpenBSD openbsd-amd64/elvish-v0.16.0.tar.gz @dl Windows windows-amd64/elvish-v0.16.0.zip @dl Linux linux-386/elvish-v0.16.0.tar.gz @dl Windows windows-386/elvish-v0.16.0.zip @dl Linux linux-arm64/elvish-v0.16.0.tar.gz @dl macOS darwin-arm64/elvish-v0.16.0.tar.gz
0.15.0 (Release Note) @dl Linux linux-amd64/elvish-v0.15.0.tar.gz @dl macOS darwin-amd64/elvish-v0.15.0.tar.gz @dl FreeBSD freebsd-amd64/elvish-v0.15.0.tar.gz @dl NetBSD netbsd-amd64/elvish-v0.15.0.tar.gz @dl OpenBSD openbsd-amd64/elvish-v0.15.0.tar.gz @dl Windows windows-amd64/elvish-v0.15.0.zip @dl Linux linux-386/elvish-v0.15.0.tar.gz @dl Windows windows-386/elvish-v0.15.0.zip @dl Linux linux-arm64/elvish-v0.15.0.tar.gz
0.14.1 (Release Note) @dl Linux linux-amd64/elvish-v0.14.1.tar.gz @dl macOS darwin-amd64/elvish-v0.14.1.tar.gz @dl FreeBSD freebsd-amd64/elvish-v0.14.1.tar.gz @dl NetBSD netbsd-amd64/elvish-v0.14.1.tar.gz @dl OpenBSD openbsd-amd64/elvish-v0.14.1.tar.gz @dl Windows windows-amd64/elvish-v0.14.1.zip @dl Linux linux-386/elvish-v0.14.1.tar.gz @dl Windows windows-386/elvish-v0.14.1.zip @dl Linux linux-arm64/elvish-v0.14.1.tar.gz
0.14.0 (Release Note) @dl Linux linux-amd64/elvish-v0.14.0.tar.gz @dl macOS darwin-amd64/elvish-v0.14.0.tar.gz @dl FreeBSD freebsd-amd64/elvish-v0.14.0.tar.gz @dl NetBSD netbsd-amd64/elvish-v0.14.0.tar.gz @dl OpenBSD openbsd-amd64/elvish-v0.14.0.tar.gz @dl Windows windows-amd64/elvish-v0.14.0.zip @dl Linux linux-386/elvish-v0.14.0.tar.gz @dl Windows windows-386/elvish-v0.14.0.zip @dl Linux linux-arm64/elvish-v0.14.0.tar.gz
0.13.1 (Release Note) @dl Linux linux-amd64/elvish-v0.13.1.tar.gz @dl macOS darwin-amd64/elvish-v0.13.1.tar.gz @dl FreeBSD freebsd-amd64/elvish-v0.13.1.tar.gz @dl NetBSD netbsd-amd64/elvish-v0.13.1.tar.gz @dl OpenBSD openbsd-amd64/elvish-v0.13.1.tar.gz @dl Windows windows-amd64/elvish-v0.13.1.zip @dl Linux linux-386/elvish-v0.13.1.tar.gz @dl Windows windows-386/elvish-v0.13.1.zip @dl Linux linux-arm64/elvish-v0.13.1.tar.gz
0.13 (Release Note) @dl Linux linux-amd64/elvish-v0.13.tar.gz @dl macOS darwin-amd64/elvish-v0.13.tar.gz @dl Windows windows-amd64/elvish-v0.13.zip @dl Linux linux-386/elvish-v0.13.tar.gz @dl Windows windows-386/elvish-v0.13.zip @dl Linux linux-arm64/elvish-v0.13.tar.gz
0.12 (Release Note) @dl Linux linux-amd64/elvish-v0.12.tar.gz @dl macOS darwin-amd64/elvish-v0.12.tar.gz @dl Windows windows-amd64/elvish-v0.12.zip @dl Linux linux-386/elvish-v0.12.tar.gz @dl Windows windows-386/elvish-v0.12.zip @dl Linux linux-arm64/elvish-v0.12.tar.gz
0.11 (Release Note) @dl Linux linux-amd64/elvish-v0.11.tar.gz @dl macOS darwin-amd64/elvish-v0.11.tar.gz @dl Windows windows-amd64/elvish-v0.11.zip @dl Linux linux-386/elvish-v0.11.tar.gz @dl Windows windows-386/elvish-v0.11.zip @dl Linux linux-arm64/elvish-v0.11.tar.gz
Versions before 0.11 do not build on Windows
0.10 (Release Note) @dl Linux linux-amd64/elvish-v0.10.tar.gz @dl macOS darwin-amd64/elvish-v0.10.tar.gz @dl Linux linux-386/elvish-v0.10.tar.gz @dl Linux linux-arm64/elvish-v0.10.tar.gz
Versions before 0.10 require cgo
0.9 (Release Note) @dl Linux linux-amd64/elvish-v0.9.tar.gz @dl macOS darwin-amd64/elvish-v0.9.tar.gz N/A N/A
0.8 (Release Note) @dl Linux linux-amd64/elvish-v0.8.tar.gz @dl macOS darwin-amd64/elvish-v0.8.tar.gz N/A N/A
0.7 (Release Note) @dl Linux linux-amd64/elvish-v0.7.tar.gz @dl macOS darwin-amd64/elvish-v0.7.tar.gz N/A N/A
0.6 (Release Note) @dl Linux linux-amd64/elvish-v0.6.tar.gz @dl macOS darwin-amd64/elvish-v0.6.tar.gz N/A N/A
0.5 (Release Note) @dl Linux linux-amd64/elvish-v0.5.tar.gz @dl macOS darwin-amd64/elvish-v0.5.tar.gz N/A N/A
0.4 @dl Linux linux-amd64/elvish-v0.4.tar.gz @dl macOS darwin-amd64/elvish-v0.4.tar.gz N/A N/A
Versions before 0.4 do not use vendoring and cannot be reproduced
0.3 @dl Linux linux-amd64/elvish-v0.3.tar.gz @dl macOS darwin-amd64/elvish-v0.3.tar.gz N/A N/A
0.2 @dl Linux linux-amd64/elvish-v0.2.tar.gz @dl macOS darwin-amd64/elvish-v0.2.tar.gz N/A N/A
0.1 @dl Linux linux-amd64/elvish-v0.1.tar.gz @dl macOS darwin-amd64/elvish-v0.1.tar.gz N/A N/A
elvish-0.17.0/website/go.mod000066400000000000000000000002351415471104000156360ustar00rootroot00000000000000module src.elv.sh/website go 1.16 require ( github.com/BurntSushi/toml v0.4.1 src.elv.sh v0.0.0-00010101000000-000000000000 ) replace src.elv.sh => ../ elvish-0.17.0/website/go.sum000066400000000000000000000020671415471104000156700ustar00rootroot00000000000000github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw= github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/creack/pty v1.1.15 h1:cKRCLMj3Ddm54bKSpemfQ8AtYFBhAI2MPmdys22fBdc= github.com/creack/pty v1.1.15/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210820121016-41cdb8703e55 h1:rw6UNGRMfarCepjI8qOepea/SXwIBVfTKjztZ5gBbq4= golang.org/x/sys v0.0.0-20210820121016-41cdb8703e55/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= elvish-0.17.0/website/home.css000066400000000000000000000043511415471104000161750ustar00rootroot00000000000000#demo-window { background-color: #f0ede2; margin-bottom: 16px; border-radius: 5px; overflow: hidden; } .animated-transition { transition: all 300ms cubic-bezier(0.165, 0.84, 0.44, 1); } /* Slide view. */ #demo-container { transform: translateX(0); width: 500%; } .demo-wrapper { width: 20%; float: left; } /* Expanded view. */ #demo-container.expanded, body.no-js #demo-container { width: 100%; } #demo-container.expanded .demo-wrapper, body.no-js #demo-container .demo-wrapper { width: 100%; } /* Styles for the demos. */ .demo:after { content: ""; display: table; clear: both; } .demo { padding: 16px; } .demo-col { box-sizing: border-box; width: 100%; height: 100%; float: left; } .demo-ttyshot { text-align: center; } .demo-ttyshot .ttyshot { text-align: left; } .demo-description { padding: 16px 16px 0; } .demo-ttyshot { padding-top: 16px; } @media screen and (min-width: 1024px) { .demo-ttyshot { width: 60%; } .demo-description { width: 40%; } .animated-transition { transition-duration: 400ms; } } .demo-description h2 { margin-top: 0; } .demo-col p { margin-top: 16px; margin-bottom: 0; } ul#demo-switcher { display: inline; margin: 0; } ul#demo-switcher > li { list-style: none; display: inline-block; } ul#demo-switcher > li > a { color: black; padding: 4px 14px; } ul#demo-switcher > li > a.current, ul#demo-switcher > li > a.current:hover { color: white; background-color: black; } ul#demo-switcher > li > a:hover { background-color: #ccc; cursor: pointer; } /* Styles for the body layout. */ @media screen and (min-width: 800px) { #columns { display: flex; } .column { flex: 1; } .column:first-child { margin-right: 24px; } } /* Overriding default styles */ .article h1 { border-bottom: none; padding-bottom: 0; } .article li > p { margin-top: 0.5em; margin-bottom: 0.5em; } /** * Dark theme. */ .dark #demo-window { background-color: #101818; } .dark ul#demo-switcher > li > a { color: #eee; } .dark ul#demo-switcher > li > a.current, .dark ul#demo-switcher > li > a.current:hover { color: black; background-color: #eee; } .dark ul#demo-switcher > li > a:hover { background-color: #444; } elvish-0.17.0/website/home.js000066400000000000000000000121531415471104000160200ustar00rootroot00000000000000document.addEventListener('DOMContentLoaded', function() { var current = 0, expanded = false, demoWindow = document.getElementById("demo-window"), demoContainer = document.getElementById("demo-container"), demoSwitcher = document.getElementById("demo-switcher"), demoWrappers = document.getElementsByClassName("demo-wrapper"), nDemos = demoWrappers.length, switcherLinks = []; /* Functions for scrolling to a certain demo. */ function scrollTo(to, instant) { if (expanded) { return; } switcherLinks[current].className = ""; switcherLinks[to].className = "current"; var translate = -demoWrappers[0].offsetWidth * to; demoContainer.className = instant ? "" : "animated-transition"; demoContainer.style.transform = "translateX(" + translate + "px)"; current = to; }; function scrollToNext() { scrollTo(current < nDemos - 1 ? current + 1 : current); }; function scrollToPrev() { scrollTo(current > 0 ? current - 1 : current); }; /* Build the expander. */ var li = document.createElement("li"), expander = document.createElement("a"); expander.textContent = "↧"; li.appendChild(expander); demoSwitcher.appendChild(li); function expand() { expanded = true; expander.className = "current"; switcherLinks[current].className = ""; demoContainer.className = "expanded"; demoContainer.style.transform = ""; expander.textContent = "↥"; } function collapse() { switcherLinks[current].className = "current"; expander.className = ""; demoContainer.className = ""; expander.textContent = "↧"; } function toggleExpand() { expanded = !expanded; if (expanded) { expand(); } else { collapse(); scrollTo(current, true); } } expander.onclick = toggleExpand; /* Build demo switchers. */ for (var i = 0; i < nDemos; i++) { var li = document.createElement("li"), link = document.createElement("a"); link.textContent = i + 1; link.onclick = (function(to) { return function() { if (expanded) { expanded = false; collapse(); scrollTo(to, true); } else { scrollTo(to); } }; })(i); if (i == 0) { link.className = "current"; } switcherLinks.push(link); li.appendChild(link); demoSwitcher.appendChild(li); } /* Resizing breaks sliding, fix it. */ window.addEventListener('resize', function() { scrollTo(current, true); }); /* Scrolling primitives. */ var scrollXTrigger = 5, scrollYTrigger = 5; var scrollX = false, scrollY = false; var offsetX = 0, offsetY = 0, baseOffset = 0; function handleScroll(ev) { if (!scrollX && !scrollY) { if (Math.abs(offsetX) > scrollXTrigger) { baseOffset = offsetX; scrollX = true; } else if (Math.abs(offsetY) > scrollYTrigger) { baseOffset = offsetY; scrollY = true; } } if (!scrollX) { return; } // No overscrolling. var calculatedOffset = offsetX - baseOffset; if ((current == 0 && calculatedOffset > 0) || (current == nDemos-1 && calculatedOffset < 0)) { calculatedOffset = 0; } var translate = calculatedOffset - demoWrappers[0].offsetWidth * current; demoContainer.style.transform = "translateX(" + translate + "px)"; ev.preventDefault(); } function settleScroll() { if (scrollX) { var threshold = Math.min(60, demoWindow.offsetWidth / 4); if (offsetX < -threshold) { scrollToNext(); } else if (offsetX > threshold) { scrollToPrev(); } else { scrollTo(current); } } offsetX = offsetY = baseOffset = 0; scrollX = scrollY = false; } /* Support scrolling by touch. */ var initX, initY; demoWindow.addEventListener('touchstart', function(ev) { if (expanded) { return; } initX = ev.touches[0].clientX; initY = ev.touches[0].clientY; demoContainer.className = ""; }); demoWindow.addEventListener('touchmove', function(ev) { if (expanded) { return; } if (ev.touches.length == 1) { var lastX = ev.touches[0].clientX; var lastY = ev.touches[0].clientY; offsetX = lastX - initX; offsetY = lastY - initY; handleScroll(ev); // document.getElementById('demo-debug').innerText = '(' + offsetX + ', ' + offsetY + '), ' + scrollX + ', ' + scrollY; } }); demoWindow.addEventListener('touchcancel', function() { if (expanded) { return; } scrollTo(current); }); demoWindow.addEventListener('touchend', function() { if (expanded) { return; } settleScroll(); }); // Keyboard bindings. window.addEventListener('keypress', function(ev) { var char = String.fromCodePoint(ev.keyCode || ev.charCode); if (char == 'h') { scrollToPrev(); } else if (char == 'l') { scrollToNext(); } else if (char == 'o') { toggleExpand(); } else { var i = parseInt(char); if (1 <= i && i <= nDemos) { if (expanded) { expanded = false; collapse(); } scrollTo(i-1); } } }); }); elvish-0.17.0/website/home.md000066400000000000000000000113541415471104000160060ustar00rootroot00000000000000**Elvish** is an expressive programming language and a versatile interactive shell, combined into one seamless package. It runs on Linux, BSDs, macOS and Windows.

Demos

Enable JavaScript to see demos as slides.

Powerful Pipelines

Text pipelines are intuitive and powerful. However, if your data have inherently complex structures, processing them with the pipeline often requires a lot of ad-hoc, hard-to-maintain text processing code.

Pipelines in Elvish can carry structured data, not just text. You can stream lists, maps and even functions through the pipeline.

@ttyshot pipelines

Intuitive Control Structures

If you know programming, you probably already know how if looks in C. So why learn another syntax?

Elvish comes with a standard set of control structures: conditional control with if, loops with for and while, and exception handling with try. All of them have a familiar C-like syntax.

@ttyshot control-structures

Directory History

Do you type far too many cd commands? Do you struggle to remember which deeply/nested/directory your source codes, logs and configuration files are?

Backed by a real database, Elvish remembers all the directories you have been to, all the time. Just press Ctrl-L and search, as you do in a browser.

@ttyshot location-mode

Command History

Want to find the magical ffmpeg command that you used to transcode a video file two months ago, but it is buried under a million other commands?

No more cycling through history one command at a time. Press Ctrl-R and start searching your entire command history.

@ttyshot histlist-mode

Built-in File Manager

Want the convenience of a file manager, but can't give up the power of a shell?

You no longer have to make a choice. Press Ctrl-N to start exploring directories and preview files, with the full power of a shell still under your fingertip.

@ttyshot navigation-mode
# Run Elvish - [Download](get/) a binary - [Source code](https://github.com/elves/elvish) on GitHub - [Try Elvish](https://try.elv.sh) directly from the browser (Beta) # Use this Site Start your Elvish journey in this very website! - [Learn](learn/) the fundamentals and interesting topics - Peruse the definitive [reference](ref/) documents - Read the [blog](blog/) for news, tips, and developers' musings - Subscribe to the [feed](feed.atom) to keep updated
# Join the Community Join any of the following channels -- they are all all bridged together thanks to [Matrix](https://matrix.org)! - Telegram: [@elvish](https://telegram.me/elvish) - IRC: [#elvish](https://web.libera.chat/#elvish) on Libera Chat - Gitter: [elves/elvish](https://gitter.im/elves/elvish) - Matrix: [#users:elv.sh](https://matrix.to/#/#users:elv.sh) # More Resources - [Awesome Elvish](https://github.com/elves/awesome-elvish): Official list of unofficial Elvish modules - [@ElvishShell](https://twitter.com/elvishshell) on Twitter
elvish-0.17.0/website/icon-font.css000066400000000000000000000155301415471104000171420ustar00rootroot00000000000000/* Generated with https://fontello.com, from Font Awesome 4.7.0 (license: SIL) */ @font-face { font-family: 'fa-elv-sh'; src: url('data:application/octet-stream;base64,d09GRgABAAAAABCIAA8AAAAAG+AAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABWAAAADsAAABUIIslek9TLzIAAAGUAAAARAAAAGA+I1NwY21hcAAAAdgAAAByAAABwMTd+7ZjdnQgAAACTAAAAAsAAAAOAAAAAGZwZ20AAAJYAAAG7QAADgxiLvl6Z2FzcAAACUgAAAAIAAAACAAAABBnbHlmAAAJUAAABGMAAAXabNW0gmhlYWQAAA20AAAAMAAAADYeoZUvaGhlYQAADeQAAAAdAAAAJAc9A1hobXR4AAAOBAAAABMAAAAYFuAAAGxvY2EAAA4YAAAADgAAAA4FcwP8bWF4cAAADigAAAAgAAAAIAErDrNuYW1lAAAOSAAAAYIAAALZqTdts3Bvc3QAAA/MAAAAQAAAAFNrJUZ3cHJlcAAAEAwAAAB6AAAAnH62O7Z4nGNgZGBg4GIwYLBjYHJx8wlh4MtJLMljkGJgYYAAkDwymzEnMz2RgQPGA8qxgGkOIGaDiAIAJjsFSAB4nGNgYb7AOIGBlYGBqYppDwMDQw+EZnzAYMjIBBRlYGVmwAoC0lxTGA68YPhoyhz0P4shinkNwzSgMCOKIiYAmpcNGnic7ZHNCYAwDIW/9EdEHMWrC3h2Fk/O4Zw51gk0aQsu4StfSR6hhRcgA9FYjARyIrgOc6X6kan6ic36iZFAUClX0Xt9HlC+uktsbq7H62BvJP9JBn7N9d57lz2/huetHcvMUm34foo2fEf32iC/TtQeOwAAeJxjYEAGAAAOAAEAeJytV2tbG8cVntUNjAEDQtjNuu4oY1GXHckkcRxiKw7ZZVEcJanAuN11brtIuE2TXpLe6DW9X5Q/c1a0T51v+Wl5z8xKAQfcp89TPui8M/POnOucWUhoSeJ+FMZSdh+J+Z0uVe49iOiGS9fi5KEc3o+o0Eg/mxbTot9X+269TiImEaitkXBEkPhNcjTJ5GGTClrVVb1JRS0HR8XlmvADqgYySfyssBz4WaMYUCHYO5Q0qwCCdECl3uGoUCjgGKofXK7z7Gi+5viXJaDyR1WnijVFohcdxKMVp2AUljQVPaoFEeujlSDICa4cSPq8R6XVB6NrzlwQ9kOqhFGdio14960IZHcYSer1MLUJNm0w2ohjmVk2LLqGqXwkaZ3X15n5eS+SiMYwlTTTixLMSF6bYXST0c3ETeI4dhEtmg36JHYjEl0m1zF2u3SF0ZVu+mhB9JnxqCz243iQxuR4cZx7EMsB/FF+3KSylrCg1Ejh01TQi2hK+TStfGQAW5ImVUy4EQk5yKb2fcmL7K5rzedfEknYp/JaHYuBHMohdGXr5QYitBMlPTfdjSMV12NJm/cirLkcl9yUJk1pOhd4I1GwaZ7GUPkK5aL8lAr7D8npwxCaWmvSOS3Z2nm4VRL7kk+gzSRmSrJlrJ3Ro3PzIgj9tfqkcM7rk4U0a09xPJgQwPVEhkOVclJNsIXLCSHpwsixlUitSresirkzttNV7BLul64d3zSvjUNHc7OiGEKLq+rxGor4gs4KhZAG6VaTFjSoUtKF4DU+AAAZogUe7WK0YPK1iIMWTFAkYtCHZloMEjlMJC0ibE1a0t29KCsNtuKrNHegDptU1d2dqHvPTrp1zFfN/LLOxFJwP8qWlgJyUp8WPb5yKC0/u8A/C/ghZwW5KDZ6Ucbhg7/+EBmG2oW1usK2MXbtOm/BTeaZGJ50YH8HsyeTdUYKMyGqCvFCQd0ZOY5jslXTIhOFcC+iJeXLkOZRfnOIcOLL5D+XLjliUVSF7/scgWWsOWm2PO3Rp577NMK1Ah9rXpMu6sxheQnxZvk1nRVZPqWzEktXZ2WWl3VWYfl1nU2xvKKzaZbf0Nk5lp5W4/hTJUGklWyR8w7flibpY4srk8WP7GLz2OLqZPFjuyi1oAvemX7CqX9bV9nP4/7V4Z+EXU/DP5YK/rG8Cv9YNuAfy1X4x/Kb8I/lNfjH8lvwj+Ua/GPZ0rJtCva6htpLiUTTc5LApBSXsMU1u67pukfXcR+fwVXoyDOyqdINxY39iQyXvX92nOJsvhJyxdEza1nZqYURmiJ7+dyx8JzFuaHl88by53Ga5YRf1Ylre6otPC9W/iX4b+uO2shuODX29SbiAQdOtx+XJd1o0gu6dbHdpI3/RkVh90F/ESkSKw3Zkh1uCQjt3eGwozroIREePnRdvEgbjlNbRoRvoXet0EXQSminDUPLZoVP5wPvYNhSUraHOPP2SZps2fOoovwxW1LCPWVzJzoqybJ0j0qr5adinzvtDJq2MjvUdkKV4PHrmnC3s69SKUgGisp4VLFcClIXOOFO9/ieFKah/6tt5FhBwza/WDOB0YLzTlGibE+toIkgGWUUXPkrp+JENqLBRhTxm3fSL3WhENrjWEjMllfzWKg2wvTSZIlmzPq26rBSzuKdSQjZGRtpEntRS7bxoLP1+aRku/JUUKWB0d3j3y42iadVe54txSX/8jFLgnG6Ev7AedzlcYo30T9aHMVtuhhEPRdvqmzHrWzdWca9feXE6q7bO7Hqn7r3STsCTbe8Jync0nTbG8I2rjE4dSYVCW3ROnaExmWuz1Ub+RQfaL51nQtU4fq0cPPs+ds6m8FbM97yP5Z05/9VxewT97G2Qqs6Vi/1OLezgwZ8yxtH5VWMbnt1lccl92YSgrsIQc1ee3yN4IZXW3QTt/y1M+a7OM5ZrtILwK9rehHiDY5iiHDLbTy842i9qbmg6Q3Ab+uRENsAPQCHwY4eOWZmF8DM3GNOB2CPOQzuM4fBd5jD4Lv6CL0wAIqAHINifeTYuQdAdu4t5jmM3maeQe8wz6B3mWfQe6wzBEhYJ4OUdTLYZ50M+sx5FWDAHAYHzGHwkDkMvmfs2gL6vrGL0fvGLkY/MHYx+sDYxehDYxejHxq7GP3I2MXox4hxe5LAn5gRbQJ+ZOErgB9z0M3Ix+ineGtzzs8sZM7PDcfJOb/A5pcmp/7SjMyOQwt5x68sZPqvcU5O+I2FTPithUz4Hbh3Juf93owM/RMLmf4HC5n+R+zMCX+ykAl/tpAJfwH35cl5fzUjQ/+bhUz/u4VM/wd25oR/WsiEoYVM+FSPzpsvW6q4o1KhGOKfJrTB2Pdo+oCKV3uH48e6+QUl2gFBAAAAAAEAAf//AA94nI1US28bVRQ+5947987D9YwTZyY0juu3S+zMOBN7XPWRWAktpHEfsfoKQqlaCUETFqixKAuKskhaEBILhAQqlRASsGFTSiUkFmzYsapUKQvErpv+ADZsXM64AQmkSmhm7p3zurrnO985wACe3uNP2C9QhmWY7xxdyjDAeeTwIjLOuyZyXA6RLQEC7gBw2AHOGF8FztkVYJytLHTmjhYLRSG9GqaTWCxUpZKqWKi0Ks2oNc8i1Z5jbR+rzTkMs+ilpcriAXQP8LRKYrVCj6QgdzaM2nPkVc0iH1l/sNEIT5wfm2AygQwF55jRkklx/DSuP9h9sL579iUtoU8YQhMcmaky6QsnwsZnb+dSr369cLyL+145h9+c2u4aM54mTIVCIJd0SFYbH3EO9hdPb3e7278vvlNNumbe4po0kekmCs2bMU7yMGjeOlmbqr5L+cLTn/jnfAkycAReg287+w+joV18mYEYtRgq3r1QY8YJyRRbXr5nnr3UiUDjdK/rgIqgvA46cE3n18AAUAZcAwF0HbUOTEq2CozJKyCZXJnoHIoDuWZsxZEK2db/DV3tJFcveV7moDe+fzxtygO1cjPASkF5MdCEd8HHAGWaQA/nMcKh0d5TtKs+azUjL3TjmpA1TZYCeTTb1Wc7hYRkSytvuPNpp+/4jsupMDlXp6K4o86mPe30nMENe9Op04/Tt/2Ux02R9YyksoRujuAfjZ5/29/0Z2Yat4N+EPSCW8E/0nee3XdGXO5Imy4tTOGmfLtv2ysOfuk6m47ds+t0KJ2ZTBpujgqlc8NKDT5eCFaCxmZwuzEzQ8fc8nt+0Pc/eCYBQMzvP/kT3oMqzEO3s3QQUaaIUW2fCZZHTYguSJQ7IJjGhLZFNWAaf5/ozgSyN2IC8ItEdrgck//M4YlyKyrPKjlZw7G0LOaJ5qlm1M6H7iQOZalSadfLh9ExbEZHMHRHU9QVAf4NKjG+Gc2SPx9ZWxw0FtfWFvFDZZpqcKPcxKiED8tNUy/p5q6bsa4OPtUc0ZES37pquUmctNPYvT+MebiwhkO/ZnnQGEbe100Tfxs8ttMsQYFSdrTkMDDjugQFpQRPvycsLDgPpzvLkLASO/vQMq0d21Aa15ABQ9jSBYG2nUSTMfMibSa7LKnF2JlzvTOnuiePLxw7cvhQa7aVT5Xjb9aJ+UZY1LBQiXP2yDQ2G7renq7aeq6Olnyr+EyMoYrNqjhG4KjYmz/JuffdXM5tDR63iBYkDdf/ilNZnPyXAicn60iaOz/E0bTgxkbWe45Arhu5sT05ixtT2Y3skDfv8a8IKx2mIejU979gJ8VwDFJTU2sytj3kRjwI4UrMjZXaVKU8OiLkeA2blYJMuzTQKB9J/bTXUvNYVTTrhn8RvTTv4jRLuXqO3psXrk53LNR3RUKWFA/u3mFCaSVT3ZTGo6h+7edPVIJdcmPX3I8f/fp6PXpkyJtMKynzi7sBaiXL2tXR6ky/eTYBfwGMquTQAHicY2BkYGAAYq1myQPx/DZfGfiZXwBFGO6e1tiHoP/XML9gDgJyORiYQKIATAkMA3icY2BkYGAO+p8FJF8wMPz/DySBIiiADQCHzwWbAAAAeJxjfsHAwAzCC6A0lA0AQrQE8wAAAAAAAJABcAH2AowC7QAAAAEAAAAGAF0AAwAAAAAAAgAgAEgAjQAAAHQODAAAAAB4nHWQzU4CMRSFTxU0QuJCEtfdaCSG4ce4YWFICLBzwQLWBTo/OExJp5Cw8i18Bx/Irc/iYWiI8WeaTr9z7m3vbQFc4RMCh++R88ACF1QHPsE5njyf0h94LpGfPZdRxdTzGf2Z5wru8eK5ihreeIIoXVAt8e5Z4ErUPJ/gUtx4PqX/4LlEHngu41pMPZ/RX3muYCJePVdxKz76Zr2zSRQ7edevy06r05aznTS0kkylUm1cbGwuezI0mdNpaoK5WYWqodNtI4/HOtqkyh71ESba5onJZDtoHb2RzrRVTi/2FfJt1HEulKE1Kzn0Z8u1NUs9d0Hs3LrbbH6viT4M1tjBIkGEGA4Sd3TrXDtocbZJM2ZIZh6yEmRQSOkobLgjLiI5dY8zpMroamak5ABz/lf0FRqFu+Wac9eYKuIJKSP2j/hvZ0K1r5QUNSR7C9jj77wRVVbkqqKTxfEOOXMi3spxhEW3tuhOYvijb8l32ceWdOb0g+J1HN0umhz/3PMLNXeDrQAAeJxjYGKAAC4G7ICNkYmRmZGFkZWRjZGdgSU5I7GEJSczL5sDROimVpSwF2eWpOYmFrAV5Sdnp5YwMAAA67wMRnicY/DewXAiKGIjI2Nf5AbGnRwMHAzJBRsZ2J02MjBoQWguFHonAwMDNxJrJwMzA4PLRhXGjsCIDQ4dESB+istGDRB/BwcDRIDBJVJ6ozpIaBdHAwMji0NHcghMAgQ2MvBp7WD837qBpXcjE4PLZtYUNgYXFwCUHCoHAAA=') format('woff'); } [class^="icon-"]:before, [class*=" icon-"]:before { font-family: "fa-elv-sh"; font-style: normal; font-weight: normal; speak: never; display: inline-block; text-decoration: inherit; width: 1em; margin-right: .2em; text-align: center; /* opacity: .8; */ /* For safety - reset parent styles, that can break glyph codes*/ font-variant: normal; text-transform: none; /* fix buttons height, for twitter bootstrap */ line-height: 1em; /* Animation center compensation - margins should be symmetric */ /* remove if not needed */ margin-left: .2em; /* you can be more comfortable with increased icons size */ /* font-size: 120%; */ /* Font smoothing. That was taken from TWBS */ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; /* Uncomment for 3D effect */ /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ } .icon-chat:before { content: '\e800'; } /* '' */ .icon-link:before { content: '\e801'; } /* '' */ .icon-link-ext:before { content: '\f08e'; } /* '' */ .icon-sitemap:before { content: '\f0e8'; } /* '' */ .icon-rocket:before { content: '\f135'; } /* '' */ elvish-0.17.0/website/index.toml000066400000000000000000000006761415471104000165450ustar00rootroot00000000000000title = "Elvish Shell" author = "Qi Xiao" feedPosts = 10 template = "template.html" baseCSS = ["reset.css", "style.css", "icon-font.css"] rootURL = "https://elv.sh" [index] name = "home" extraCSS = ["home.css"] extraJS = ["home.js"] [[categories]] name = "get" title = "Get Elvish" [[categories]] name = "learn" title = "Learn Elvish" [[categories]] name = "ref" title = "Elvish Reference" [[categories]] name = "blog" title = "Elvish Blog" elvish-0.17.0/website/learn/000077500000000000000000000000001415471104000156315ustar00rootroot00000000000000elvish-0.17.0/website/learn/effective-elvish.md000066400000000000000000000344611415471104000214130ustar00rootroot00000000000000 Elvish is not an entirely new language. Its programming techniques have two primary sources: traditional Unix shells and functional programming languages, both dating back to many decades ago. However, the way Elvish combines those two paradigms is unique in many ways, which enables new ways to write code. This document is an advanced tutorial focusing on how to write idiomatic Elvish code, code that is concise and clear, and takes full advantage of Elvish's features. An appropriate adjective for idiomatic Elvish code, like _Pythonic_ for Python or _Rubyesque_ for Ruby, is **Elven**. In [Roguelike games](https://en.wikipedia.org/wiki/Roguelike), Elven items are known to be high-quality, artful and resilient. So is Elven code. # Style ## Naming Use `dash-delimited-words` for names of variables and functions. Underscores are allowed in variable and function names, but their use should be limited to environment variables (e.g. `$E:LC_ALL`) and external commands (e.g. `pkg_add`). When building a module, use a leading dash to communicate that a variable or function is subject to change in future and cannot be relied upon, either because it is an experimental feature or implementation detail. Elvish's core libraries follow the naming convention above. ## Indentation Indent by two spaces. ## Code Blocks In Elvish, code blocks in control structures are delimited by curly braces. This is perhaps the most visible difference of Elvish from most other shells like bash, zsh or fish. The following bash code: ```bash if true; then echo true fi ``` Is written like this in Elvish: ```elvish if $true { echo true } ``` If you have used lambdas in Elvish, you will notice that code blocks are syntactically just parameter-list-less lambdas. In Elvish, you cannot put opening braces of code blocks on the next line. This won't work: ```elvish if $true { # wrong! echo true } ``` Instead, you must write: ```elvish if $true { echo true } ``` This is because in Elvish, control structures like `if` follow the same syntax as normal commands, hence newlines terminate them. To make the code block part of the `if` command, it must appear on the same line. # Using the Pipeline Elvish is equipped with a powerful tool for passing data: the pipeline. Like in traditional shells, it is an intuitive notation for data processing: data flows from left to right, undergoing one transformation after another. Unlike in traditional shells, it is not restricted to unstructured bytes: all Elvish values, including lists, maps and even closures, can flow in the pipeline. This section documents how to make the most use of pipelines. ## Returning Values with Structured Output Unlike functions in most other programming languages, Elvish commands do not have return values. Instead, they can write to _structured output_, which is similar to the traditional byte-based stdout, but preserves all internal structures of aribitrary Elvish values. The most fundamental command that does this is `put`: ```elvish-transcript ~> put foo ▶ foo ~> x = (put foo) ~> put $x ▶ foo ``` This is hardly impressive - you can output and recover simple strings using good old byte-based output as well. But let's try this: ```elvish-transcript ~> put "a\nb" [foo bar] ▶ "a\nb" ▶ [foo bar] ~> s li = (put "a\nb" [foo bar]) ~> put $s ▶ "a\nb" ~> put $li[0] ▶ foo ``` Here, two things are worth mentioning: the first value we `put` contains a newline, and the second value is a list. When we capture the output, we get those exact values back. Passing structured data is difficult with byte-based output, but trivial with value output. Besides `put`, many other builtin commands and commands in builtin modules also write to structured output, like `str:split`: ```elvish-transcript ~> use str ~> str:split , foo,bar ▶ foo ▶ bar ~> words = [(str:split , foo,bar)] ~> put $words ▶ [foo bar] ``` User-defined functions behave in the same way: they "return" values by writing to structured stdout. Without realizing that "return values" are just outputs in Elvish, it is easy to think of `put` as **the** command to "return" values and write code like this: ```elvish-transcript ~> fn split-by-comma [s]{ use str; put (str:split , $s) } ~> split-by-comma foo,bar ▶ foo ▶ bar ``` The `split-by-comma` function works, but it can be written more concisely as: ```elvish-transcript ~> fn split-by-comma [s]{ use str; str:split , $s } ~> split-by-comma foo,bar ▶ foo ▶ bar ``` In fact, the pattern `put (some-cmd)` is almost always redundant and equivalent to just `some-command`. Similarly, it is seldom necessary to write `echo (some-cmd)`: it is almost always equivalent to just `some-cmd`. As an exercise, try simplifying the following function: ```elvish fn git-describe { echo (git describe --tags --always) } ``` ## Mixing Bytes and Values Each pipe in Elvish comprises two components: one traditional byte pipe that carries unstructured bytes, and one value pipe that carries Elvish values. You can write to both, and output capture will capture both: ```elvish-transcript ~> fn f { echo bytes; put value } ~> f bytes ▶ value ~> outs = [(f)] ~> put $outs ▶ [bytes value] ``` This also illustrates that the output capture operator `(...)` works with both byte and value outputs, and it can recover the output sent to `echo`. When byte output contains multiple lines, each line becomes one value: ```elvish-transcript ~> x = [(echo "lorem\nipsum")] ~> put $x ▶ [lorem ipsum] ``` Most Elvish builtin functions also work with both byte and value inputs. Similarly to output capture, they split their byte input by newlines. For example: ```elvish-transcript ~> use str ~> put lorem ipsum | each $str:to-upper~ ▶ LOREM ▶ IPSUM ~> echo "lorem\nipsum" | each $str:to-upper~ ▶ LOREM ▶ IPSUM ``` This line-oriented processing of byte input is consistent with traditional Unix tools like `grep`, `sed` and `awk`. In fact, it is easy to write your own `grep` in Elvish: ```elvish-transcript ~> use re ~> fn mygrep [p]{ each [line]{ if (re:match $p $line) { echo $line } } } ~> cat in.txt abc 123 lorem 456 ~> cat in.txt | mygrep '[0-9]' 123 456 ``` (Note that it is more concise to write `mygrep ... < in.txt`, but due to [a bug](https://github.com/elves/elvish/issues/600) this does not work.) However, this line-oriented behavior is not always desirable: not all Unix commands output newline-separated data. When you want to get the output as is, as a single string, you can use the `slurp` command: ```elvish-transcript ~> echo "a\nb\nc" | slurp ▶ "a\nb\nc\n" ``` One immediate use of `slurp` is to read a whole file into a string: ```elvish-transcript ~> cat hello.go package main import "fmt" func main() { fmt.Println("vim-go") } ~> hello-go = (slurp < hello.go) ~> put $hello-go ▶ "package main\n\nimport \"fmt\"\n\nfunc main() {\n\tfmt.Println(\"vim-go\")\n}\n" ``` It is also useful, for example, when working with NUL-separated output: ```elvish-transcript ~> touch "a\nb.go" ~> mkdir d ~> touch d/f.go ~> use str ~> find . -name '*.go' -print0 | str:split "\000" (slurp) ▶ "./a\nb.go" ▶ ./d/f.go ▶ '' ``` In the above command, `slurp` turns the input into one string, which is then used as an argument to `str:split`. The `str:split` command then splits the whole input by NUL bytes. Note that in Elvish, strings can contain NUL bytes; in fact, they can contain any byte; this makes Elvish suitable for working with binary data. (Also, note that the `find` command terminates its output with a NUL byte, hence we see a trailing empty string in the output.) One side note: In the first example, we saw that `bytes` appeared before `value`. This is not guaranteed: byte output and value output are separate, it is possible to get `value` before `bytes` in more complex cases. Writes to one component, however, always have their orders preserved, so in `put x; put y`, `x` will always appear before `y`. ## Prefer Pipes Over Parentheses If you have experience with Lisp, you will discover that you can write Elvish code very similar to Lisp. For instance, to split a string containing comma-separated value, reduplicate each value (using commas as separators), and rejoin them with semicolons, you can write: ```elvish-transcript ~> csv = a,b,foo,bar ~> use str ~> str:join ';' [(each [x]{ put $x,$x } [(str:split , $csv)])] ▶ 'a,a;b,b;foo,foo;bar,bar' ``` This code works, but it is a bit unreadable. In particular, since `str:split` outputs multiple values but `each` wants a list argument, you have to wrap the output of `str:split` in a list with `[(str:split ...)]`. Then you have to do this again in order to pass the output of `each` to `str:join`. You might wonder why commands like `str:split` and `each` do not simply output a list to make this easier. The answer to that particular question is in the next subsection, but for the program at hand, there is a much better way to write it: ```elvish-transcript ~> csv = a,b,foo,bar ~> use str ~> str:split , $csv | each [x]{ put $x,$x } | str:join ';' ▶ 'a,a;b,b;foo,foo;bar,bar' ``` Besides having fewer pairs of parentheses (and brackets), this program is also more readable, because the data flows from left to right, and there is no nesting. You can see that `$csv` is first split by commas, then each value gets reduplicated, and then finally everything is joined by semicolons. It matches exactly how you would describe the algorithm in spoken English -- or for that matter, any spoken language! Both versions work, because commands like `each` and `str:join` that work with multiple inputs can take their inputs in two ways: they can take the inputs as one list argument, like in the first version; or from the pipeline, like the second version. Whenever possible, you should prefer the input-from-pipeline form: it makes for programs that have little nesting, read naturally. One exception to the recommendation is when the input is a small set of things known beforehand. For example: ```elvish-transcript ~> each $str:to-upper~ [lorem ipsum] ▶ LOREM ▶ IPSUM ``` Here, using the input-from-argument is completely fine: if you want to use the input-from-input form, you have to supply the input using `put`, which is also OK but a bit more wordy: ```elvish-transcript ~> put lorem ipsum | each $str:to-upper~ ▶ LOREM ▶ IPSUM ``` However, not all commands support taking input from the pipeline. For example, if we want to first join some values with space and then split at commas, this won't work: ```elvish-transcript ~> use str ~> str:join ' ' [a,b c,d] | str:split , Exception: arity mismatch: arguments here must be 2 values, but is 1 value [tty], line 1: str:join ' ' [a,b c,d] | str:split , ``` This is because the `str:split` command only ever works with one input (one string to split), and was not implemented to support taking input from pipeline; hence it always takes 2 arguments and we got an exception. It is easy to remedy this situation however. The `all` command passes its input to its output, and by capturing its output, we can turn the input into an argument: ```elvish-transcript ~> use str ~> str:join ' ' [a,b c,d] | str:split , (all) ▶ a ▶ 'b c' ▶ d ``` ## Streaming Multiple Outputs In the previous subsection, we remarked that commands like `str:split` and `each` write multiple output values instead of one list. Why? This has to do with another advantage of passing data through the pipeline: in a pipeline, all commands are executed in parallel. A command in a pipeline does not need to wait for its previous command to finish running before it can start processing data. Try this in your terminal: ```elvish-transcript ~> each $str:to-upper~ | each [x]{ put $x$x } (Start typing) abc ▶ ABCABC xyz ▶ XYZXYZ (Press ^D) ``` You will notice that as soon as you press Enter after typing `abc`, the output `ABCABC` is shown. As soon as one input is available, it goes through the entire pipeline, each command doing its work. This gives you immediate feedback, and makes good use of multi-core CPUs on modern computers. Pipelines are like assembly lines in the manufacturing industry. If instead of passing multiple values, we pass a list through the pipeline: that means that each command will now be waiting for its previous command to do all the processing and pack the results in a list before it can start doing anything. Now, although the commands themselves are run in parallel, they all need to be waiting for their previous commands to finish before they can start doing real work. This is why commands like `each` and `str:split` produce multiple values instead of one list. When writing your functions, try to make them produce multiple values as well: they will cooperate better with builtin commands, and they can benefit from the efficiency of parallel computations. # Working with Multiple Values In Elvish, many constructs can evaluate to multiple values. This can be surprising if you are not familiar with it. To start with, output captures evaluate to all the captured values, instead of a list: ```elvish-transcript ~> use str ~> str:split , a,b,c ▶ a ▶ b ▶ c ~> li = (str:split , a,b,c) Exception: arity mismatch: assignment right-hand-side must be 1 value, but is 3 values [tty], line 1: li = (str:split , a,b,c) ``` The assignment fails with "arity mismatch" because the right hand side evaluates to 3 values, but you are attempting to assign them to just one variable. If you want to capture the results into a list, you have to explicitly do so, either by constructing a list or using rest variables: ```elvish-transcript ~> use str ~> li = [(str:split , a,b,c)] ~> put $li ▶ [a b c] ~> @li = (str:split , a,b,c) # equivalent and slightly shorter ``` ## Assigning Multiple Variables # To Be Continued... As of writing, Elvish is neither stable nor complete. The builtin libraries still have missing pieces, the package manager is in its early days, and things like a type system and macros have been proposed and considered, but not yet worked on. Deciding best practices for using feature _x_ can be a bit tricky when that feature _x_ doesn't yet exist! The current version of the document is what the lead developer of Elvish (@xiaq) has collected as best practices for writing Elvish code in early 2018, between the release of Elvish 0.11 and 0.12. They apply to aspects of the Elvish language that are relatively complete and stable; but as Elvish evolves, the document will co-evolve. You are invited to revisit this document once in a while! elvish-0.17.0/website/learn/faq.md000066400000000000000000000055071415471104000167310ustar00rootroot00000000000000 # Why a new shell? The author of Elvish found the concept of a shell -- a programmable, interactive text interface for an OS -- to be simple yet powerful. However, he also felt that the more traditional shells, such as `bash` and `zsh`, didn't take it far enough. They all had _some_ programming capabilities, but they were only really sufficient for manipulating strings or lists of strings. They also had _some_ nice interactive features, such as tab completion, but more advanced UI features were either nonexistent, hidden behind obscure configuration options, or required external programs with suboptimal integration with the rest of the shell experience. So the author set out to build a shell that he personally wished existed. This was done by starting from the basic concept of a shell -- a programmable, interactive text interface for an OS -- and rethinking how it could be executed with modern techniques. Elvish is that product, and the rethinking is still ongoing. There are a lot of other projects with similar motivations; [this list](https://github.com/oilshell/oil/wiki/Alternative-Shells) on Oil's wiki offers a quick overview. # Can I use Elvish as my shell? Yes. Many people already use Elvish as their daily drivers. Follow the instructions in the [Get Elvish](../get) page to install Elvish and use it as your default shell. # Why is Elvish incompatible with bash? The author felt it would be easier and more fun to start with a clean slate. However, it is certainly possible to build a powerful new shell that is also compatible; Elvish just happens to not be that. If you definitely need compatibility with traditional shells, [Oil](http://www.oilshell.org/) is a promising project. # Why is Elvish restricted to terminals? There are a lot of things to dislike about VT100-like terminals, but they are still the most portable API for building a text interface. All major desktop operating systems (including [Windows](https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences)) support this API, and many other programs target this API. That said, the few parts of Elvish that rely on a VT100-like terminal is only loosely coupled with the rest of Elvish. It will be very easy to port Elvish to other interfaces, and the author might explore this space at a later stage. # Why is Elvish called Elvish? Elvish is named after **elven** items in in [roguelikes](https://en.wikipedia.org/wiki/Roguelike), which has a reputation of high quality. You can think of Elvish as an abbreviation of "elven shell". The name is not directly related to [Tolkien's Elvish languages](), but you're welcome to create something related to both Elvishes. Alternatively, Elvish is a backronym for "Expressive programming Language and Versatile Interactive SHell". elvish-0.17.0/website/learn/fundamentals.md000066400000000000000000000337741415471104000206520ustar00rootroot00000000000000 **Work in progress.** This tutorial introduces the fundamentals of shell programming with Elvish. Familiarity with other shells or programming languages is useful but not required. # Commands and strings Let's begin with the traditional "hello world" program: ```elvish-transcript ~> echo Hello, world! Hello, world! ``` Here, we call the `echo` **command** with two **arguments**: `Hello,` and `world!`. The `echo` command prints them both, inserting a space in between and adding a newline at the end, and voilà, we get back the message `Hello, world!`. ## Quoting the argument We used a single space between the two arguments. Using more also works: ```elvish-transcript ~> echo Hello, world! Hello, world! ``` The output still only has one space, because `echo` didn't know how many spaces were used to separate the two arguments; all it sees is the two arguments, `Hello,` and `world!`. However, you can preserve the two spaces by **quoting** the entire text: ```elvish-transcript ~> echo "Hello, world!" Hello, world! ``` In this version, `echo` only sees one argument containing Hello,  world (with two spaces). A pair of **double quotes** tells Elvish that the text inside it is a single argument; the quotes themselves are not part of the argument. On contrary, the `Hello,` and `world!` arguments are implicitly delimited by spaces (instead of explicitly by quotes); as such, they are known as **barewords**. Some special characters also delimit barewords, which we will see later. Barewords are useful to write command names, filenames and command-line switches, which usually do not contain spaces or special characters. ## Editing the command line TODO ## Builtin and external commands We demonstrated the basic command structure using `echo`, a very simple command. The same structure applies to all the commands. For instance, Elvish comes with a `randint` command that takes two arguments `a` and `b` and generates a random integer in the range a...b-1. You can use the command as a digital dice: ```elvish-transcript ~> randint 1 7 ▶ 3 ``` Arithmetic operations are also commands. Like other commands, the command names comes first, making the syntax a bit different from common mathematical notations: ```elvish-transcript ~> * 17 28 # multiplication ▶ 476 ~> ^ 2 10 # exponention ▶ 1024 ``` The commands above all come with Elvish; they are **builtin commands**. There are [many more](../ref/builtin.html) of them. Another kind of commands is **external commands**. They are separate programs from Elvish, and either come with the operating system or are installed by you manually. Chances are you have already used some of them, like `ls` for listing files, or `cat` for showing files. There are really a myriad of external commands; to start with, you can manage code repositories with [git](https://git-scm.com), convert documents with [pandoc](http://pandoc.org), process images with [ImageImagick](https://www.imagemagick.org/script/index.php), transcode videos with [ffmpeg](http://ffmpeg.org), test the security of websites with [nmap](https://nmap.org) and analyze network traffic with [tcpdump](http://www.tcpdump.org). Many free and open-source software come with a command-line interface. Here we show you how to obtain the latest version of Elvish entirely from the command line: we use `curl` to download the binary and its checksum, `shasum` to check the checksum, and `chmod` to make it executable (assuming that you are running macOS on x86-64): ```elvish-transcript ~> curl -s -o elvish https://dl.elv.sh/darwin-amd64/elvish-HEAD ~> curl -s https://dl.elv.sh/darwin-amd64/elvish-HEAD.sha256sum 8b3db8cf5a614d24bf3f2ecf907af6618c6f4e57b1752e5f0e2cf4ec02bface0 elvish-HEAD ~> shasum -a 256 elvish 8b3db8cf5a614d24bf3f2ecf907af6618c6f4e57b1752e5f0e2cf4ec02bface0 elvish ~> chmod +x elvish ~> ./elvish ``` ## History and scripting Some commands are useful to rerun; for instance, you may want to roll the digital dice several times in a roll, or for another occasion. Of course, you can just retype the command: ```elvish-transcript ~> randint 1 7 ▶ 1 ``` The command is short, but still, it can become a chore if you want to run it repeatedly. Fortunately, Elvish remembers all the commands you have typed; you can just ask Elvish to recall it by pressing Up: @ttyshot fundamentals/history-1 This will give you the last command you have run. However, it may have been a while when you have last run the `randint` command, and this will not give you what you need. You can either continue pressing Up until you find the command, or you can give Elvish a hint by typing some characters from the command line you want, e.g. `ra`, before pressing Up: @ttyshot fundamentals/history-2 Another way to rerun commands is saving them in a **script**, which is simply a text file containing the commands you want to run. Using your favorite text editor, save the command to `dice.elv` under your home directory: ```elvish # dice.elv randint 1 7 ``` After saving the script, you can run it with: ```elvish-transcript ~> elvish dice.elv ▶ 4 ``` Since the above command runs `elvish` explicitly, it works in other shells as well, not just from Elvish itself. # Variables and lists To change what a command does, we now need to change the commands themselves. For instance, instead of saying "Hello, world!", we might want our command to say "Hello, John!": ```elvish-transcript ~> echo Hello, John! Hello, John! ``` Which works until you want a different message. One way to solve this is using **variables**: ```elvish-transcript ~> name = John ~> echo Hello, $name! Hello, John! ``` The command `echo Hello, $name!` uses the `$name` variable you just assigned in the previous command. To greet a different person, you can just change the value of the variable, and the command doesn't need to change: ```elvish-transcript ~> name = Jane ~> echo Hello, $name! Hello, Jane! ``` Using variables has another advantage: after defining a variable, you can use it as many times as you want: ```elvish-transcript ~> name = Jane ~> echo Hello, $name! Hello, Jane! ~> echo Bye, $name! Bye, Jane! ``` Now, if you change the value of `$name`, the output of both commands will change. ## Environment variables In the examples above, we have assigned value of `$name` ourselves. We can also make the `$name` variable automatically take the name of the current user, which is usually kept in an **environment variable** called `USER`. In Elvish, environment variables are used like other variables, except that they have an `E:` at the front of the name: ```elvish-transcript ~> echo Hello, $E:USER! Hello, elf! ~> echo Bye, $E:USER! Bye, elf! ``` The outputs will likely differ on your machine. ## Lists and indexing The values we have stored in variables so far are all strings. It is possible to store a **list** of values in one variable; a list can be written by surrounding some values with `[` and `]`. For example: ```elvish-transcript ~> list = [linux bsd macos windows] ~> echo $list [linux bsd macos windows] ``` Each element of this list has an **index**, starting from 0. In the list above, the index of `linux` is 0, that of `bsd` is 1, and so on. We can retrieve an element by writing its index after the list, also surrounded by `[` and `]`: ```elvish-transcript ~> echo $list[0] is at index 0 linux is at index 0 ``` We can even do: ```elvish-transcript ~> echo [linux bsd macos windows][0] is at index 0 linux is at index 0 ``` Note that in this example, the two pairs of `[]` have different meanings: the first pair denotes lists, while the second pair denotes an indexing operation. ## Script arguments Recall the `dice.elv` script above: ```elvish # dice.elv randint 1 7 ``` And how we ran it: ```elvish-transcript ~> elvish dice.elv ▶ 4 ``` We were using `elvish` itself as a command, with the sole argument `dice.elv`. We can also supply additional arguments: ```elvish-transcript ~> elvish dice.elv a b c ▶ 4 ``` But this hasn't made any difference, because well, our `dice.elv` script doesn't make use of the arguments. The arguments are kept in a `$args` variable, as a list. Let's try put this into a `show-args.elv` file in your home directory: ```elvish echo $args ``` And we can run it: ```elvish-transcript ~> elvish show-args.elv [] ~> elvish show-args.elv foo [foo] ~> elvish show-args.elv foo bar [foo bar] ``` Since `$args` is a list, we can retrieve the individual elements with `$args[0]`, `$args[1]`, etc.. Let's rewrite our greet-and-bye script, taking the name as an argument. Put this in `greet-and-bye.elv`: ``` name = $args[0] echo Hello, $name! echo Bye, $name! ``` We can run it like this: ```elvish-transcript ~> elvish greet-and-bye.elv Jane Hello, Jane! Bye, Jane! ~> elvish greet-and-bye.elv John Hello, John! Bye, John! ``` # Output capture and multiple values Environment variables are not the only way to learn about a computer system; we can also gain more information by invoking commands. The `uname` command tells you which operation system the computer is running; for instance, if you are running Linux, it prints `Linux` (unsurprisingly): ```elvish-transcript ~> uname Linux ``` (If you are running macOS, `uname` will print `Darwin`, the [open-source core]() of macOS.) Let's try to integrate this information into our "hello" message. The Elvish command-line allows us to run multiple commands in a batch, as long as they are separated by semicolons. We can build the message by running multiple commands, using `uname` for the OS part: ```elvish-transcript ~> echo Hello, $E:USER, ; uname ; echo user! Hello, xiaq, Linux user! ``` This has the undesirable effect that "Linux" appears on its own line. Instead of running this command directly, we can first **capture** its output in a variable: ```elvish-transcript ~> os = (uname) ~> echo Hello, $E:USER, $os user! Hello, elf, Linux user! ``` You can also use the output capture construct directly as an argument to `echo`, without storing the result in a variable first: ```elvish-transcript ~> echo Hello, $E:USER, (uname) user! Hello, elf, Linux user! ``` ## More arithmetic You can use output captures to construct do complex arithmetic involving more than one operation: ```elvish-transcript ~> # compute the answer to life, universe and everything * (+ 3 4) (- 100 94) ▶ 42 ``` elvish-0.17.0/website/learn/index.toml000066400000000000000000000005111415471104000176320ustar00rootroot00000000000000autoIndex = true [[articles]] name = "tour" title = "Quick Tour" extraCSS = ["tour.css"] [[articles]] name = "fundamentals" title = "Fundamentals" [[articles]] name = "unique-semantics" title = "Some Unique Semantics" [[articles]] name = "effective-elvish" title = "Effective Elvish" [[articles]] name = "faq" title = "FAQ" elvish-0.17.0/website/learn/tour.css000066400000000000000000000000461415471104000173340ustar00rootroot00000000000000td[colspan] { text-align: center; } elvish-0.17.0/website/learn/tour.md000066400000000000000000000604371415471104000171560ustar00rootroot00000000000000 # Introduction Welcome to the quick tour of Elvish. This tour works best if you have used another shell or programming language before. If you are mainly interested in using Elvish interactively, jump directly to [interactive features](#interactive-features). # Basic shell language Many basic language features of Elvish are very familiar to traditional shells. A notable exception is control structures, covered [below](#control-structures) in the _advanced language features_ section. ## Comparison with bash If you are familiar with bash, the following table shows some (rough) correspondence between Elvish and bash syntax:
Feature Elvish bash equivalent
Barewords echo foo
Single-quoted strings echo 'foo'
echo 'It''s good' echo 'It'\''s good'
Double-quoted strings echo "foo"
echo "foo\nbar" echo $'foo\nbar'
echo "foo: "$foo echo "foo: $foo"
Comments # comment
Line continuation echo foo ^
bar
echo foo \
bar
Brace expansion echo {foo bar}.txt echo {foo,bar}.txt
Wildcards echo *.?
echo **.go find . -name '*.go'
echo *.?[set:ch] echo *.[ch]
Tilde expansion echo ~/foo
Setting variables var foo = bar foo=bar
set foo = bar foo=bar
foo=bar cmd
Using variables echo $foo
echo $E:HOME echo $HOME
Redirections head -n10 < a.txt > b.txt
Byte pipelines head -n4 a.txt | grep x
Output capture ls -l (which elvish) ls -l $(which elvish)
Background jobs echo foo &
## Barewords Like traditional shells, unquoted words that don't contain special characters are treated as strings (such words are called **barewords**): ```elvish-transcript ~> echo foobar foobar ~> ls / bin dev home lib64 mnt proc run srv tmp var boot etc lib lost+found opt root sbin sys usr ~> vim a.c ``` This is one of the most distinctive syntactical features of shells; non-shell programming languages typically treat unquoted words as names of functions and variables. Read the language reference on [barewords](../ref/language.html#bareword) to learn more. ## Single-quoted strings Like traditional shells, single-quoted strings expand nothing; every character represents itself (except the single quote itself): ```elvish-transcript ~> echo 'hello\world$' hello\world$ ``` Like [Plan 9 rc](http://doc.cat-v.org/plan_9/4th_edition/papers/rc), or zsh with the `RC_QUOTES` option turned on, the single quote itself can be written by doubling it ```elvish-transcript ~> echo 'it''s good' it's good ``` Read the language reference on [single-quoted strings](../ref/language.html#single-quoted-string) to learn more. ## Double-quoted strings Like many non-shell programming languages and `$''` in bash, double-quoted strings support C-like escape sequences (e.g. `\\n` for newline): ```elvish-transcript ~> echo "foo\nbar" foo bar ``` Unlike traditional shells, Elvish does **not** support interpolation inside double-quoted strings. Instead, you can just write multiple words together, and they will be concatenated: ```elvish-transcript ~> var x = foo ~> put 'x is '$x ▶ 'x is foo' ``` Read the language reference on [double-quoted strings](../ref/language.html#double-quoted-string) to learn more. ## Comments Comments start with `#` and extends to the end of the line: ```elvish-transcript ~> echo foo # this is a comment foo ``` ## Line continuation Line continuation in Elvish uses `^` instead of `\`: ```elvish-transcript ~> echo foo ^ bar foo bar ``` Unlike traditional shells, line continuation is treated as whitespace. In Elvish, the following code outputs `foo bar`: ```elvish echo foo^ bar ``` However, in bash, the following code outputs `foobar`: ```bash echo foo\ bar ``` ## Brace expansion Brace expansions in Elvish work like in traditional shells, but use spaces instead of commas: ```elvish-transcript ~> echo {foo bar}.txt foo.txt bar.txt ``` The opening brace `{` **must not** be followed by a whitespace, to disambiguate from [lambdas](#lambdas). **Note**: commas might still work as a separator in Elvish's brace expansions, but it will eventually be deprecated and removed soon. Read the language reference on [braced lists](../ref/language.html#braced-list) to learn more. ## Wildcards The basic wildcard characters, `*` and `?`, work like in traditional shells: ```elvish-transcript ~> ls bar.ch d1 d2 d3 foo.c foo.h lorem.go lorem.txt ~> echo *.? foo.c foo.h ``` Elvish also supports `**`, which matches multiple path components: ```elvish-transcript ~> find . -name '*.go' ./d1/a.go ./d2/b.go ./lorem.go ./d3/d4/c.go ~> echo **.go d1/a.go d2/b.go d3/d4/c.go lorem.go ``` Character classes are a bit more verbose in Elvish. First, a character set is written like `[set:ch]`, instead of just `[ch]`. Second, they don't appear on their own, but as a suffix to `?`. For example, to match files ending in either `.c` or `.h`, use: ```elvish-transcript ~> echo *.?[set:ch] foo.c foo.h ``` A character set suffix can also be applied to `*`. For example, to match files who extension only contains `c` and `h`: ```elvish-transcript ~> echo *.*[set:ch] bar.ch foo.c foo.h ``` Read the language reference on [wildcard expansion](../ref/language.html#wildcard-expansion) to learn more. ## Tilde expansion Tilde expansion works likes in traditional shells. Assuming that the home directory of the current user is `/home/me`, and the home directory of `elf` is `/home/elf`: ```elvish-transcript ~> echo ~/foo /home/me/foo ~> echo ~elf/foo /home/elf/foo ``` Read the language reference on [tilde expansion](../ref/language.html#tilde-expansion) to learn more. ## Setting variables Variables are declared with the `var` command, and set with the `set` command: ```elvish-transcript ~> var foo = bar ~> echo $foo bar ~> set foo = quux ~> echo $foo quux ``` The spaces around `=` are mandatory. Unlike traditional shells, variables must be declared before they can be set; setting an undeclared variable results in an error. Like traditional shells, Elvish supports setting a variable temporarily for the duration of a command, by prefixing the command with `foo=bar`. For example: ```elvish-transcript ~> var foo = original ~> fn f { echo $foo } ~> foo=temporary f temporary ~> echo $foo original ``` Read the language reference on [the `var` command](../ref/language.html#var), [the `set` command](../ref/language.html#set) and [temporary assignments](../ref/language.html#temporary-assignment) to learn more. ## Using variables Like traditional shells, using the value of a variable requires the `$` prefix. ```elvish-transcript ~> var foo = bar ~> echo $foo bar ``` Unlike traditional shells, variables must be declared before being used; if the `foo` variable wasn't declared with `var` first, `echo $foo` results in an error. Also unlike traditional shells, environment variables in Elvish live in a separate `E:` namespace: ```elvish-transcript ~> echo $E:HOME /home/elf ``` Read the language reference on [variables](../ref/language.html#variable) and [special namespaces](../ref/language.html#special-namespaces) to learn more. ## Redirections Redirections in Elvish work like in traditional shells. For example, to save the first 10 lines of `a.txt` to `a1.txt`: ```elvish-transcript ~> head -n10 < a.txt > a1.txt ``` Read the language reference on [redirections](../ref/language.html#redirection) to learn more. ## Byte pipelines UNIX pipelines in Elvish (called **byte pipelines**, to distinguish from [value pipelines](#value-pipelines)) work like in traditional shells. For example, to find occurrences of `x` in the first 4 lines of `a.txt`: ```elvish-transcript ~> cat a.txt foo barx lorem quux lux nox ~> head -n4 a.txt | grep x barx quux ``` Read the language reference on [pipelines](../ref/language.html#pipeline) to learn more. ## Output capture Output of commands can be captured and used as values with `()`. For example, the following command shows details of the `elvish` binary: ```elvish-transcript ~> ls -l (which elvish) -rwxr-xr-x 1 xiaq users 7813495 Mar 2 21:32 /home/xiaq/go/bin/elvish ``` **Note**: the same feature is usually known as _command substitution_ in traditonal shells. Unlike traditional shells, Elvish only splits the output on newlines, not any other whitespace characters. Read the language reference on [output capture](../ref/language.html#output-capture) to learn more. ## Background jobs Add `&` to the end of a pipeline to make it run in the background, similar to traditional shells: ```elvish-transcript ~> echo foo & foo job echo foo & finished ``` Unlike traditional shells, the `&` character does not serve to separate commands. In bash you can write `echo foo & echo bar`; in Elvish you still need to terminate the first command with `;` or newline: `echo foo &; echo bar`. Read the language reference on [background pipelines](../ref/language.html#background-pipeline) to learn more. # Advanced language features Building on a core of familiar shell-like syntax, the Elvish language incorporates many advanced features that make it a modern dynamic programming language. ## Value output Like in traditional shells, commands in Elvish can output **bytes**. The `echo` command outputs bytes: ```elvish-transcript ~> echo foo bar foo bar ``` Additionally, commands can also output **values**. Values include not just strings, but also [lambdas](#lambdas), [numbers](#numbers), [lists and maps](#lists-and-maps). The `put` command outputs values: ```elvish-transcript ~> put foo [foo] [&foo=bar] { put foo } ▶ foo ▶ [foo] ▶ [&foo=bar] ▶ ``` Many builtin commands output values. For example, string functions in the `str:` module outputs their results as values. This makes those functions work seamlessly with strings that contain newlines or even NUL bytes: ```elvish-transcript ~> use str ~> str:join ',' ["foo\nbar" "lorem\x00ipsum"] ▶ "foo\nbar,lorem\x00ipsum" ``` Unlike most programming languages, Elvish commands don't have return values. Instead, they use the value output to "return" their results. Read the reference for [builtin commands](../ref/builtin.html) to learn which commands work with value inputs and outputs. Among them, here are some general-purpose primitives:
Command Functionality
all Passes value inputs to value outputs
each Applies a function to all values from value input
put Writes arguments as value outputs
slurp Convert byte input to a single string in value output
## Value pipelines Pipelines work with value outputs too. When forming pipelines, a command that writes value outputs can be followed by a command that takes value inputs. For example, the `each` command takes value inputs, and applies a lambda to each one of them: ```elvish-transcript ~> put foo bar | each [x]{ echo 'I got '$x } I got foo I got bar ``` Read the language reference on [pipelines](../ref/language.html#pipeline) to learn more about pipelines in general. ## Value output capture Output capture works with value output too. Capturing value outputs always recovers the exact values there were written. For example, the `str:join` command joins a list of strings with a separator, and its output can be captured and saved in a variable: ```elvish-transcript ~> use str ~> var s = (str:join ',' ["foo\nbar" "lorem\x00ipsum"]) ~> put $s ▶ "foo\nbar,lorem\x00ipsum" ``` Read the language reference on [output capture](../ref/language.html#output-capture) to learn more. ## Lists and maps Lists look like `[a b c]`, and maps look like `[&key1=value1 &key2=value2]`: ```elvish-transcript ~> var li = [foo bar lorem ipsum] ~> put $li ▶ [foo bar lorem ipsum] ~> var map = [&k1=v2 &k2=v2] ~> put $map ▶ [&k1=v2 &k2=v2] ``` You can get elements of lists and maps by indexing them. Lists are zero-based and support slicing too: ```elvish-transcript ~> put $li[0] ▶ foo ~> put $li[1..3] ▶ [bar lorem] ~> put $map[k1] ▶ v2 ``` Read the language reference on [lists](../ref/language.html#list) and [maps](../ref/language.html#map) to learn more. ## Numbers Elvish has a double-precision floating-point number type, `float64`. There is no dedicated syntax for it; instead, it can constructed using the `float64` builtin: ```elvish-transcript ~> float64 1 ▶ (float64 1) ~> float64 1e2 ▶ (float64 100) ``` Most arithmetic commands in Elvish support both typed numbers and strings that can be converted to numbers. They usually output typed numbers: ```elvish-transcript ~> + 1 2 ▶ (float64 3) ~> use math ~> math:pow (float64 10) 3 ▶ (float64 1000) ``` **Note**: The set of number types will likely expand in future. Read the language reference on [numbers](../ref/language.html#number) and the reference for the [math module](../ref/math.html) to learn more. ## Booleans Elvish has two boolean values, `$true` and `$false`. Read the language reference on [booleans](../ref/language.html#boolean) to learn more. ## Options Many Elvish commands take **options**, which look like map pairs (`&key=value`). For example, the `echo` command takes a `sep` option that can be used to override the default separator of space: ```elvish-transcript ~> echo &sep=',' foo bar foo,bar ~> echo &sep="\n" foo bar foo bar ``` ## Lambdas Lambdas are first-class values in Elvish. They can be saved in variables, used as commands, passed to commands, and so on. Lambdas can be written by enclosing its body with `{` and `}`: ```elvish-transcript ~> var f = { echo "I'm a lambda" } ~> $f I'm a lambda ~> put $f ▶ ~> var g = (put $f) ~> $g I'm a lambda ``` The opening brace `{` **must** be followed by some whitespace, to disambiguate from [brace expansion](#brace-expansion). Lambdas can take arguments and options, which can be written in a **signature**: ```elvish-transcript ~> var f = [a b &opt=default]{ echo "a = "$a echo "b = "$b echo "opt = "$opt } ~> $f foo bar a = foo b = bar opt = default ~> $f foo bar &opt=option a = foo b = bar opt = option ``` There must be no space between the `]` and `{` in this case. Read the language reference on [functions](../ref/language.html#function) to learn more about functions. ## Control structures Control structures in Elvish look very different from traditional shells. For example, this is how an `if` command looks: ```elvish-transcript ~> if (eq (uname) Linux) { echo "You're on Linux" } You're on Linux ``` The `if` command takes a conditional expression (an output capture in this case), and the body to execute as a lambda. Since lambdas allow internal newlines, you can also write it like this: ```elvish-transcript ~> if (eq (uname) Linux) { echo "You're on Linux" } You're on Linux ``` However, you must write the opening brace `{` on the same line as `if`. If you write it on a separate line, Elvish would parse it as two separate commands. The `for` command looks like this: ```elvish-transcript ~> for x [expressive versatile] { echo "Elvish is "$x } Elvish is expressive Elvish is versatile ``` Read the language reference on [the `if` command](../ref/language.html#if), [the `for` command](../ref/language.html#for), and additionally [the `while` command](../ref/language.html#while) to learn more. ## Exceptions Elvish uses exceptions to signal errors. For example, calling a function with the wrong number of arguments throws an exception: ```elvish-transcript ~> var f = { echo foo } # doesn't take arguments ~> $f a b Exception: arity mismatch: arguments here must be 0 values, but is 2 values [tty 2], line 1: $f a b ``` Moreover, non-zero exits from external commands are also turned into exceptions: ```elvish-transcript ~> false Exception: false exited with 1 [tty 3], line 1: false ``` Exceptions can be caught using the `try` command: ```elvish-transcript ~> try { false } except e { echo 'got an exception' } got an exception ``` Read the language reference on [the exception value type](../ref/language.html#exception) and [the `try` command](../ref/language.html#try) to learn more. ## Namespaces and modules The names of variables and functions can have **namespaces** prepended to their names. Namespaces always end with `:`. The [using variables](#using-variables) section has already shown the `E:` namespace. Other namespaces can be added by importing modules with `use`. For example, [the `str:` module](../ref/str.html) provides string utilities: ```elvish-transcript ~> use str ~> str:to-upper foo ▶ FOO ``` You can define your own modules by putting `.elv` files in `~/.config/elvish/lib` (or `~\AppData\Roaming\elvish\lib`). For example, to define a module called `foo`, put the following in `foo.elv` under the aforementioned directory: ```elvish fn f { echo 'in a function in foo' } ``` This module can now be used like this: ```elvish-transcript ~> use foo ~> foo:f in a function in foo ``` Read the language reference on [namespaces and modules](../ref/language.html#namespaces-and-modules) to learn more. ## External command support As shown in examples above, Elvish supports calling external commands directly by writing their name. If an external command exits with a non-zero code, it throws an exception. Unfortunately, many of the advanced language features are only available for internal commands and functions. For example: - They can only write byte output, not value output. - They only take string arguments; non-string arguments are implicitly coerced to strings. - They don't take options. Read the language reference on [ordinary commands](../ref/language.html#ordinary-command) to learn more about when Elvish decides that a command is an external command. # Interactive features ## Tab completion Press Tab to start completion. For example, after typing `vim` and Space, press Tab to complete filenames: @ttyshot tour/completion Basic operations should be quite intuitive: - To navigate the candidate list, use arrow keys or Tab and Shift-Tab. - To accept the selected candidate, press Enter. - To cancel, press Escape. As indicated by the horizontal scrollbar, you can scroll to the right to find additional results that don't fit in the terminal. You may have noticed that the cursor has moved to the right of "COMPLETING argument". This indicates that you can continue typing to filter candidates. For example, after typing `.md`, the UI looks like this: @ttyshot tour/completion-filter Read the reference on [completion API](../ref/edit.html#completion-api) to learn how to program and customize tab completion. ## Command history Elvish has several UI features for working with command history. ### History walking Press to fetch the last command. This is called **history walking** mode: @ttyshot tour/history-walk Press to go further back, to go forward, or Escape to cancel. To restrict to commands that start with a prefix, simply type the prefix before pressing . For example, to walk through commands starting with `echo`, type `echo` before pressing : @ttyshot tour/history-walk-prefix ### History listing Press Ctrl-R to list the full command history: @ttyshot tour/history-list Like in completion mode, type to filter the list, press and to navigate the list, Enter to insert the selected entry, or Escape to cancel. ### Last command Finally, Elvish has a **last command** mode dedicated to inserting parts of the last command. Press Alt-, to trigger it: @ttyshot tour/lastcmd ## Directory history Elvish remembers which directories you have visited. Press Ctrl-L to list visited directories. Use and to navigate the list, Enter to change to that directory, or Escape to cancel. @ttyshot tour/location Type to filter: @ttyshot tour/location-filter ## Navigation mode Press Ctrl-N to start the builtin filesystem navigator, or **navigation mode**. @ttyshot tour/navigation Unlike other modes, the cursor stays in the main buffer in navigation mode. This allows you to continue typing commands; while doing that, you can press Enter to insert the selected filename. You can also press Alt-Enter to insert the filename without exiting navigation mode; this is useful when you want to insert multiple filenames. ## Startup script Elvish's startup script is `~/.elvish/rc.elv`. Elvish doesn't support aliases, but you can get a similar experience simply by defining functions: ```elvish fn ls [@a]{ e:ls --color $@a } ``` The `e:` prefix (for "external") ensures that the external command named `ls` will be called. Otherwise this definition will result in infinite recursion. The left and right prompts can be customized by assigning functions to `edit:prompt` and `edit:rprompt`. The following configuration simulates the default prompts, but uses fancy Unicode: ```elvish # "tilde-abbr" abbreviates home directory to a tilde. edit:prompt = { tilde-abbr $pwd; put '❱ ' } # "constantly" returns a function that always writes the same value(s) to # output; "styled" writes styled output. edit:rprompt = (constantly (styled (whoami)✸(hostname) inverse)) ``` This is how it looks: @ttyshot tour/unicode-prompts Another common task in the startup script is to set the search path. You can do it directly via `$E:PATH`, but you can also manipulate as a list in [`$paths`](../ref/builtin.html#paths): ```elvish set paths = [/opts/bin /bin /usr/bin] ``` Read [the API of the interactive editor](../ref/edit.html) to learn more about UI customization options. elvish-0.17.0/website/learn/unique-semantics.md000066400000000000000000000343101415471104000214460ustar00rootroot00000000000000
The semantics of Elvish is unique in many aspects when compared to other shells. This can be surprising if you are used to other shells, and it is a result of the design choice of making Elvish a full-fledged programming language. # Structureful IO Elvish offers the ability to build elaborate data structures, "return" them from functions, and pass them through pipelines. ## Motivation Traditional shells use strings for all kinds of data. They can be stored in variables, used as function arguments, written to output and read from input. Strings are very simple to use, but they fall short if your data has an inherent structure. A common solution is using pseudo-structures of "each line representing a record, and each (whitespace-separated) field represents a property", which is fine as long as your data do not contain whitespaces. If they do, you will quickly run into problems with escaping and quotation and find yourself doing black magics with strings. Some shells provide data structures like lists and maps, but they are usually not first-class values. You can store them in variables, but you might not to be able to nest them, pass them to functions, or returned them from functions. ## Data Structures and "Returning" Them Elvish offers first-class support for data structures such as lists and maps. Here is an example that uses a list: ```elvish-transcript ~> li = [foo bar 'lorem ipsum'] ~> kind-of $li # "kind" is like type ▶ list ~> count $li # count the number of elements in a list ▶ 3 ``` (See the [language reference](../ref/language.html) for a more complete description of the builtin data structures.) As you can see, you can store lists in variables and use them as command arguments. But they would be much less useful if you cannot **return** them from a function. A naive way to do this is by `echo`ing the list and use output capture to recover it: ```elvish-transcript ~> fn f { echo [foo bar 'lorem ipsum'] } ~> li = (f) # (...) is output capture, like $(...) in other shells ~> kind-of $li ▶ string ~> count $li # count the number of bytes, since $li is now a string ▶ 23 ``` As we have seen, our attempt to output a list has turned it into a string. This is because the `echo` command in Elvish, like in other shells, is string-oriented. To echo a list, it has to be first converted to a string. Elvish provides a `put` command to output structured values as they are: ```elvish-transcript ~> fn f { put [foo bar 'lorem ipsum'] } ~> li = (f) ~> kind-of $li ▶ list ~> count $li ▶ 3 ``` So how does `put` work differently from `echo` under the hood? In Elvish, the standard output is made up of two parts: one traditional byte-oriented **file**, and one internal **value-oriented channel**. The `echo` command writes to the file, so it has to serialize its arguments into strings; the `put` command writes to the value-oriented channel, preserving all the internal structures of the values. If you invoke `put` directly from the command prompt, the values it output have a leading `▶`: ```elvish-transcript ~> put [foo bar] ▶ [foo bar] ``` The leading arrow is a way to visualize that a command has written something onto the value channel, and not part of the value itself. In retrospect, you may discover that the `kind-of` and `count` builtin commands also write their output to the value channel. ## Passing Data Structures Through the Pipeline When I said that standard output in Elvish comprises two parts, I was not telling the full story: pipelines in Elvish also have these two parts, in a very similar way. Data structures can flow in the value-oriented part of the pipeline as well. For instance, the `each` command takes **input** from the value-oriented channel, and apply a function to each value: ```elvish-transcript ~> put lorem ipsum | each [x]{ echo "Got "$x } Got lorem Got ipsum ``` There are many builtin commands that inputs or outputs values. As another example, the `take` commands retains a fixed number of items: ```elvish-transcript ~> put [lorem ipsum] "foo\nbar" [&key=value] | take 2 ▶ [lorem ipsum] ▶ "foo\nbar" ``` ## Interoperability with External Commands Unfortunately, the ability of passing structured values is not available to external commands. However, Elvish comes with a pair of commands for JSON serialization/deserialization. The following snippet illustrates how to interoperate with a Python script: ```elvish-transcript ~> cat sort-list.py import json, sys li = json.load(sys.stdin) li.sort() json.dump(li, sys.stdout) ~> put [lorem ipsum foo bar] | to-json | python sort-list.py | from-json ▶ [bar foo ipsum lorem] ``` It is easy to write a wrapper for such external commands: ```elvish-transcript ~> fn sort-list { to-json | python sort-list.py | from-json } ~> put [lorem ipsum foo bar] | sort-list ▶ [bar foo ipsum lorem] ``` More serialization/deserialization commands may be added to the language in the future. # Exit Status and Exceptions Unix commands exit with a non-zero value to signal errors. This is available traditionally as a `$?` variable in other shells: ```bash true echo $? # prints "0" false echo $? # prints "1" ``` Builtin commands and user-defined functions also do this to signal errors, although they are not Unix commands: ```bash bad() { return 2 } bad echo $? # prints "2" ``` This model is fine, only if most errors are non-fatal (so that errors from a previous command normally do not affect the execution of subsequence ones) and the script author remembers to check `$?` for the rare fatal errors. Elvish has no concept of exit status. Instead, it has exceptions that, when thrown, interrupt the flow of execution. The equivalency of the `bad` function in elvish is as follows: ```elvish fn bad { fail "bad things have happened" # throw an exception } bad # will print a stack trace and stop execution echo "after bad" # not executed ``` (If you run this interactively, you need to enter a literal newline after `bad` by pressing Alt-Enter to make sure that it is executed in the same chunk as `echo "after bad"`.) And, non-zero exit status from external commands are turned into exceptions: ```elvish false # will print a stack trace and stop execution echo "after false" ``` An alternative way to describe this is that Elvish **does** have exit statuses, but non-zero exit statuses terminates execution by default. You can handle non-zero exit statuses by wrapping the command in a [`try`](../ref/language.html#try) block. Compare with POSIX shells, the behavior of Elvish is similar to `set -e` or `set -o errexit`, or having implicit `&&` operators joining all the commands. Defaulting to stopping execution when bad things happen makes Elvish safer and code behavior more predictable. ## Predicates and `if` The use of exit status is not limited to errors, however. In the Unix toolbox, quite a few commands exit with 0 to signal "true" and 1 to signal "false". Notably ones are: - `test` aka `[`: testing file types, comparing numbers and strings; - `grep`: exits with 0 when there are matches, with 1 otherwise; - `diff`: exits with 0 when files are the same, with 1 otherwise; - `true` and `false`, always exit with 0 and 1 respectively. The `if` control structure in POSIX shell is designed to work with such predicate commands: it takes a pipeline, and executes the body if the last command in the pipeline exits with 0. Examples: ```sh # command 1 if true; then echo 'always executes' fi # command 2 n=10 if test $n -gt 2; then echo 'executes when $n > 2' fi # command 3 if diff a.txt b.txt; then echo 'a.txt and b.txt are the same' fi ``` Since Elvish treats non-zero exit status as a kind of exception, the way that predicate commands and `if` work in POSIX shell does not work well for Elvish. Instead, Elvish's `if` is like most non-shell programming languages: it takes a value, and executes the body if the value is booleanly true. The first command above is written in Elvish as: ```elvish if $true { echo 'always executes' } ``` The way to write the second command in Elvish warrants an explanation of how predicates work in Elvish first. Predicates in Elvish simply write a boolean output, either `$true` or `$false`: ```elvish-transcript ~> > 10 2 ▶ $true ~> > 1 2 ▶ $false ``` To use predicates in `if`, you simply capture its output with `()`. So the second command is written in Elvish as: ```elvish n = 10 if (> $n 2) { echo 'executes when $n > 2' } ``` The parentheses after `if` are different to those in C: In C it is a syntactical requirement to put them around the condition; in Elvish, it functions as output capture operator. Sometimes it can be useful to have a condition on whether an external commands exits with 0. In this case, you can use the exception capture operator `?()`: ```elvish if ?(diff a.txt b.txt) { echo 'a.txt and b.txt are the same' } ``` In Elvish, all exceptions are booleanly false, while the special `$ok` value is booleanly true. If the `diff` exits with 0, the `?(...)` construct evaluates to `$ok`, which is booleanly true. Otherwise, it evaluates to an exception, which is booleanly false. Overall, this leads to a similar semantics with the POSIX `if` command. Note that the following code does have a severe downside: `?()` will prevent any kind of exceptions from throwing. In this case, we only want to turn one sort of exception into a boolean: `diff` exits with 1. If `diff` exits with 2, it usually means that there was a genuine error (e.g. `a.txt` does not exist). Swallowing this error defeats Elvish's philosophy of erring on the side of caution; a more sophisticated system of handling exit status is still being considered. # Phases of Code Execution A piece of code that gets evaluated as a whole is called a **chunk** (a loanword from Lua). If you run `elvish some-script.elv` from the command line, the entire script is one chunk; in interactive mode, each time you hit Enter, the code you have written is one chunk. Elvish interprets a code chunk in 3 phases: it first **parse**s the code into a syntax tree, then **compile**s the syntax tree code to an internal representation, and finally **evaluate**s the just-generated internal representation. If any error happens during the first two phases, Elvish rejects the chunk without executing any of it. For instance, in Elvish unclosed parenthesis is an error during the parsing phase. The following code, when executed as a chunk, does nothing other than printing the parse error: ```elvish-bad echo before echo ( ``` The same code, interpreted as bash, also contains a syntax error. However, if you save this file to `bad.bash` and run `bash bad.bash`, bash will execute the first line before complaining about the syntax error on the second line. Likewise, in Elvish using an unassigned variable is a compilation error, so the following code does nothing either: ```elvish # assuming $nonexistent was not assigned echo before echo $nonexistent ``` There seems to be no equivalency of compilation errors in other shells, but this extra compilation phase makes the language safer. In future, optional type checking may be introduced, which will fit into the compilation phase. # Assignment Semantics In Python, JavaScript and many other languages, if you assign a container (e.g. a map) to multiple variables, modifications via those variables mutate the same container. This is best illustrated with an example: ```python m = {'foo': 'bar', 'lorem': 'ipsum'} m2 = m m2['foo'] = 'quux' print(m['foo']) # prints "quux" ``` This is because in such languages, variables do not hold the "actual" map, but a reference to it. After the assignment `m2 = m`, both variables refer to the same map. The subsequent element assignment `m2['foo'] = 'quux'` mutates the underlying map, so `m['foo']` is also changed. This is not the case for Elvish: ```elvish-transcript ~> m = [&foo=bar &lorem=ipsum] ~> m2 = $m ~> m2[foo] = quux ~> put $m[foo] ▶ bar ``` It seems that when you assign `m2 = $m`, the entire map is copied from `$m` into `$m2`, so any subsequent changes to `$m2` does not affect the original map in `$m`. You can entirely think of it this way: thinking **assignment as copying** correctly models the behavior of Elvish. But wouldn't it be expensive to copy an entire list or map every time assignment happens? No, the "copying" is actually very cheap. Is it implemented as [copy-on-write](https://en.wikipedia.org/wiki/Copy-on-write) -- i.e. the copying is delayed until `$m2` gets modified? No, subsequent modifications to the new `$m2` is also very cheap. Read on if you are interested in how it is possible. ## Implementation Detail: Persistent Data Structures Like in Python and JavaScript, Elvish variables like `$m` and `$m2` also only hold a reference to the underlying map. However, that map is **immutable**, meaning that they never change after creation. That explains why `$m` did not change: because the map `$m` refers to never changes. But how is it possible to do `m2[foo] = quux` if the map is immutable? The map implementation of Elvish has another property: although the map is immutable, it is easy to create a slight variation of one map. Given a map, it is easy to create another map that is almost the same, either 1) with one more key/value pair, or 2) with the value for one key changed, or 3) with one fewer key/value pair. This operation is fast, even if the original map is very large. This low-level functionality is exposed by the `assoc` (associate) and `dissoc` (dissociate) builtins: ```elvish-transcript ~> assoc [&] foo quux # "add" one pair ▶ [&foo=quux] ~> assoc [&foo=bar &lorem=ipsum] foo quux # "modify" one pair ▶ [&lorem=ipsum &foo=quux] ~> dissoc [&foo=bar &lorem=ipsum] foo # "remove" one pair ▶ [&lorem=ipsum] ``` Now, although maps are immutable, variables are mutable. So when you try to assign an element of `$m2`, Elvish turns that into an assignment of `$m2` itself: ```elvish m2[foo] = quux # is just syntax sugar for: m2 = (assoc $m2 foo quux) ``` The sort of immutable data structures that support cheap creation of "slight variations" are called [persistent data structures](https://en.wikipedia.org/wiki/Persistent_data_structure) and is used in functional programming languages. However, the way Elvish turns assignment to `$m2[foo]` into an assignment to `$m2` itself seems to be a new approach. elvish-0.17.0/website/ref/000077500000000000000000000000001415471104000153045ustar00rootroot00000000000000elvish-0.17.0/website/ref/builtin.md000066400000000000000000000125771415471104000173100ustar00rootroot00000000000000 @module builtin pkg/eval # Introduction The builtin module contains facilities that are potentially useful to all users. ## Using builtin: explicitly The builtin module is consulted implicitly when resolving unqualified names, so you usually don't need to specify `builtin:` explicitly. However, there are some cases where it is useful to do that: - When a builtin function is shadowed by a local function, you can still use the builtin function by specifying `builtin:`. This is especially useful when wrapping a builtin function: ```elvish use builtin fn cd [@args]{ echo running my cd function builtin:cd $@args } ``` - Introspecting the builtin module, for example `keys $builtin:`. ## Usage Notation The usage of a builtin command is described by giving an example usage, using variables as arguments. For instance, The `repeat` command takes two arguments and are described as: ```elvish repeat $n $v ``` Optional arguments are represented with a trailing `?`, while variadic arguments with a trailing `...`. For instance, the `count` command takes an optional list: ```elvish count $input-list? ``` While the `put` command takes an arbitrary number of arguments: ```elvish put $values... ``` Options are given along with their default values. For instance, the `echo` command takes an `sep` option and arbitrary arguments: ```elvish echo &sep=' ' $value... ``` (When you calling functions, options are always optional.) ## Supplying Input Some builtin functions, e.g. `count` and `each`, can take their input in one of two ways: 1. From pipe: ```elvish-transcript ~> put lorem ipsum | count # count number of inputs 2 ~> put 10 100 | each [x]{ + 1 $x } # apply function to each input ▶ 11 ▶ 101 ``` Byte pipes are also possible; one line becomes one input: ```elvish-transcript ~> echo "a\nb\nc" | count # count number of lines ▶ 3 ``` 1. From an argument -- an iterable value: ```elvish-transcript ~> count [lorem ipsum] # count number of elements in argument 2 ~> each [x]{ + 1 $x } [10 100] # apply to each element in argument ▶ 11 ▶ 101 ``` Strings, and in future, other sequence types are also possible: ```elvish-transcript ~> count lorem ▶ 5 ``` When documenting such commands, the optional argument is always written as `$input-list?`. On the other hand, a trailing `$input-list?` always indicates that a command can take its input in one of two ways above: this fact is not repeated below. **Note**: You should prefer the first form, unless using it requires explicit `put` commands. Avoid `count [(some-command)]` or `each $some-func [(some-command)]`; they are, most of the time, equivalent to `some-command | count` or `some-command | each $some-func`. **Rationale**: An alternative way to design this is to make (say) `count` take an arbitrary number of arguments, and count its arguments; when there is 0 argument, count inputs. However, this leads to problems in code like `count *`; the intention is clearly to count the number of files in the current directory, but when the current directory is empty, `count` will wait for inputs. Hence it is required to put the input in a list: `count [*]` unambiguously supplies input in the argument, even if there is no file. ## Numeric commands Anywhere a command expects a number argument, that argument can be supplied either with a [typed number](language.html#number) or a string that can be converted to a number. This includes numeric comparison commands like `==`. When a command outputs numbers, it always outputs a typed number. Examples: ```elvish-transcript ~> + 2 10 ▶ (num 12) ~> == 2 (num 2) ▶ $true ``` ### Exactness-preserving commands {#exactness-preserving} Some numeric commands are designated **exactness-preserving**. When such commands are called with only [exact numbers](./language.html#exactness) (i.e. integers or rationals), they will always output an exact number. Examples: ```elvish-transcript ~> + 10 1/10 ▶ (num 101/10) ~> * 12 5/17 ▶ (num 60/17) ``` If the condition above is not satisfied - i.e. when a numeric command is not designated exactness-preserving, or when at least one of the arguments is inexact (i.e. a floating-point number), the result is an inexact number, unless otherwise documented. Examples: ```elvish-transcript ~> + 10 0.1 ▶ (num 10.1) ~> + 10 1e1 ▶ (num 20.0) ~> use math ~> math:sin 1 ▶ (num 0.8414709848078965) ``` There are some cases where the result is exact despite the use of inexact arguments or non-exactness-preserving commands. Such cases are always documented in their respective commands. ## Predicates Predicates are functions that write exactly one output that is either `$true` or `$false`. They are described like "Determine ..." or "Test ...". See [`is`](#is) for one example. ## "Do Not Use" Functions and Variables The name of some variables and functions have a leading `-`. This is a convention to say that it is subject to change and should not be depended upon. They are either only useful for debug purposes, or have known issues in the interface or implementation, and in the worst case will make Elvish crash. (Before 1.0, all features are subject to change, but those ones are sure to be changed.) Those functions and variables are documented near the end of the respective sections. Their known problem is also discussed. elvish-0.17.0/website/ref/command.md000066400000000000000000000061271415471104000172520ustar00rootroot00000000000000 # Introduction The Elvish command, `elvish`, contains the Elvish shell and is the main way for using the Elvish programming language. This documentation describes its behavior that is not part of the [language](language.html) or any of the standard modules. # Using Elvish interactively Invoking Elvish with no argument runs it in **interactive mode** (unless there are flags that suppress this behavior). In this mode, Elvish runs a REPL ([read-eval-print loop](https://en.wikipedia.org/wiki/Read–eval–print_loop)) that evaluates input continuously. The "read" part of the REPL is a rich interactive editor, and its API is exposed by the [`edit:` module](edit.html). Each unit of code read is executed as a [code chunk](language.html#code-chunk). ## RC file Before the REPL starts, Elvish will execute the **RC file**. Its path is determined as follows: - If the legacy `~/.elvish/rc.elv` exists, it is used (this will be ignored in a future version). - Otherwise: - On UNIX (including macOS), `$XDG_CONFIG_HOME/elvish/rc.elv` is used, defaulting to `~/.config/elvish/rc.elv` if `$XDG_CONFIG_HOME` is unset or empty. - On Windows, `%AppData%\elvish\rc.elv` is used. If the RC file doesn't exist, Elvish does not execute any RC file. ## Database file Elvish in interactive mode uses a database file to keep command and directory history. Its path is determined as follows: - If the legacy `~/.elvish/db` exists, it is used (this will be ignored in a future version). - Otherwise: - On UNIX (including macOS), `$XDG_DATA_HOME/elvish/db.bolt` is used, defaulting to `~/.local/state/elvish/db.bolt` if `$XDG_DATA_HOME` is unset or empty. - On Windows, `%LocalAppData%\elvish\db.bolt` is used. # Running a script Invoking Elvish with one or more arguments will cause Elvish to execute a script (unless there are flags that suppress this behavior). If the `-c` flag is given, the first argument is executed as a single [code chunk](language.html#code-chunk). If the `-c` flag is not given, the first argument is taken as a filename, and the content of the file is executed as a single code chunk. The remaining arguments are put in [`$args`](builtin.html#args). # Module search directories When importing [modules](language.html#modules), Elvish searches the following directories: - On UNIX: 1. `$XDG_CONFIG_HOME/elvish/lib`, defaulting to `~/.config/elvish/lib` if `$XDG_CONFIG_HOME` is unset or empty; 2. `$XDG_DATA_HOME/elvish/lib`, defaulting to `~/.local/share/elvish/lib` if `$XDG_DATA_HOME` is unset or empty; 3. Paths specified in the colon-delimited `$XDG_DATA_DIRS`, followed by `elvish/lib`, defaulting to `/usr/local/share/elvish/lib` and `/usr/share/elvish/lib` if `$XDG_DATA_DIRS` is unset or empty. - On Windows: `%AppData%\elvish\lib`, followed by `%LocalAppData%\elvish\lib`. If the legacy `~/.elvish/lib` directory exists, it is also searched. # Other command-line flags Running `elvish -help` lists all supported command-line flags, which are not repeated here. elvish-0.17.0/website/ref/edit.md000066400000000000000000000464201415471104000165610ustar00rootroot00000000000000 @module edit pkg/edit The `edit:` module is the interface to the Elvish editor. Function usages are given in the same format as in the [reference for the builtin module](builtin.html). _This document is incomplete._ # Overview ## Modes and Submodules The Elvish editor has different **modes**, and exactly one mode is active at the same time. Each mode has its own UI and keybindings. For instance, the default **insert mode** lets you modify the current command. The **completion mode** (triggered by Tab by default) shows you all candidates for completion, and you can use arrow keys to navigate those candidates. @ttyshot completion-mode Each mode has its own submodule under `edit:`. For instance, builtin functions and configuration variables for the completion mode can be found in the `edit:completion:` module. The primary modes supported now are `insert`, `command`, `completion`, `navigation`, `history`, `histlist`, `location`, and `lastcmd`. The last 4 are "listing modes", and their particularity is documented below. ## Prompts Elvish has two prompts: the (normal) left-hand prompt and the right-side prompt (rprompt). Most of this section only documents the left-hand prompt, but API for rprompt is the same other than the variable name: just replace `prompt` with `rprompt`. To customize the prompt, assign a function to `edit:prompt`. The function may write value outputs or byte outputs: - Value outputs may be either strings or `styled` values; they are joiend with no spaces in between. - Byte outputs are output as-is, including any newlines. Any [SGR escape sequences](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters) included in the byte outputs will be parsed, but any other escape sequences or control character will be removed. If you mix value and byte outputs, the order in which they appear is non-deterministic. Prefer using `styled` to output styled text; the support for SGR escape sequences is mostly for compatibility with external cross-shell prompts. The default prompt and rprompt are equivalent to: ```elvish edit:prompt = { tilde-abbr $pwd; put '> ' } edit:rprompt = (constantly (styled (whoami)@(hostname) inverse)) ``` More prompt functions: ```elvish-transcript ~> edit:prompt = { tilde-abbr $pwd; styled '> ' green } ~> # ">" is now green ~> edit:prompt = { echo '$' } $ # Cursor will be on the next line as `echo` outputs a trailing newline ``` ### Stale Prompt Elvish never waits for the prompt function to finish. Instead, the prompt function is always executed on a separate thread, and Elvish updates the screen when the function finishes. However, this can be misleading when the function is slow: this means that the prompt on the screen may not contain the latest information. To deal with this, if the prompt function does not finish within a certain threshold - by default 0.2 seconds, Elvish marks the prompt as **stale**: it still shows the old stale prompt content, but transforms it using a **stale transformer**. The default stale transformer applies reverse-video to the whole prompt. The threshold is customizable with `$edit:prompt-stale-threshold`; it specifies the threshold in seconds. The transformer is customizable with `$edit:prompt-stale-transform`. It is a function; the function is called with one argument, a `styled` text, and the output is interpreted in the same way as prompt functions. Some examples are: ```elvish # The following effectively disables marking of stale prompt. edit:prompt-stale-transform = [x]{ put $x } # Show stale prompts in inverse; equivalent to the default. edit:prompt-stale-transform = [x]{ styled $x inverse } # Gray out stale prompts. edit:prompt-stale-transform = [x]{ styled $x bright-black } ``` To see the transformer in action, try the following example (assuming default `$edit:prompt-stale-transform`): ```elvish n = 0 edit:prompt = { sleep 2; put $n; n = (+ $n 1); put ': ' } edit:-prompt-eagerness = 10 # update prompt on each keystroke edit:prompt-stale-threshold = 0.5 ``` And then start typing. Type one character; the prompt becomes inverse after 0.5 second: this is when Elvish starts to consider the prompt as stale. The prompt will return normal after 2 seconds, and the counter in the prompt is updated: this is when the prompt function finishes. Another thing you will notice is that, if you type a few characters quickly (in less than 2 seconds, to be precise), the prompt is only updated twice. This is because Elvish never does two prompt updates in parallel: prompt updates are serialized. If a prompt update is required when the prompt function is still running, Elvish simply queues another update. If an update is already queued, Elvish does not queue another update. The reason why exactly two updates happen in this case, and how this algorithm ensures freshness of the prompt is left as an exercise to the reader. ### Prompt Eagerness The occasions when the prompt should get updated can be controlled with `$edit:-prompt-eagerness`: - The prompt is always updated when the editor becomes active -- when Elvish starts, or a command finishes execution, or when the user presses Enter. - If `$edit:-prompt-eagerness` >= 5, it is updated when the working directory changes. - If `$edit:-prompt-eagerness` >= 10, it is updated on each keystroke. The default value is 5. ### RPrompt Persistency By default, the rprompt is only shown while the editor is active: as soon as you press Enter, it is erased. If you want to keep it, simply set `$edit:rprompt-persistent` to `$true`: ```elvish edit:rprompt-persistent = $true ``` ## Keybindings Each mode has its own keybinding, accessible as the `binding` variable in its module. For instance, the binding table for insert mode is `$edit:insert:binding`. To see current bindings, simply print the binding table: `pprint $edit:insert:binding` (replace `insert` with any other mode). The global key binding table, `$edit:global-binding` is consulted when a key is not handled by the active mode. A binding tables is simply a map that maps keys to functions. For instance, to bind `Alt-x` in insert mode to exit Elvish, simply do: ```elvish edit:insert:binding[Alt-x] = { exit } ``` Outputs from a bound function always appear above the Elvish prompt. You can see this by doing the following: ```elvish edit:insert:binding[Alt-x] = { echo 'output from a bound function!' } ``` and press Alt-x in insert mode. It allows you to put debugging outputs in bound functions without messing up the terminal. Internally, this is implemented by connecting their output to a pipe. This does the correct thing in most cases, but if you are sure you want to do something to the terminal, redirect the output to `/dev/tty`. Since this will break Elvish's internal tracking of the terminal state, you should also do a full redraw with `edit:redraw &full=$true`. For instance, the following binds Ctrl-L to clearing the terminal: ```elvish edit:insert:binding[Ctrl-L] = { clear > /dev/tty; edit:redraw &full=$true } ``` (The same functionality is already available as a builtin, `edit:clear`.) Bound functions have their inputs redirected to /dev/null. ### Format of Keys Key modifiers and names are case sensitive. This includes single character key names such as `x` and `Y` as well as function key names such as `Enter`. Key names have zero or more modifiers from the following symbols: ``` A Alt C Ctrl M Meta S Shift ``` Modifiers, if present, end with either a `-` or `+`; e.g., `S-F1`, `Ctrl-X` or `Alt+Enter`. You can stack modifiers; e.g., `C+A-X`. The key name may be a simple character such as `x` or a function key from these symbols: ``` F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12 Up Down Right Left Home Insert Delete End PageUp PageDown Tab Enter Backspace ``` **Note:** `Tab` is an alias for `"\t"` (aka `Ctrl-I`), `Enter` for `"\n"` (aka `Ctrl-J`), and `Backspace` for `"\x7F"` (aka `Ctrl-?`). **Note:** The `Shift` modifier is only applicable to function keys such as `F1`. You cannot write `Shift-m` as a synonym for `M`. **TODO:** Document the behavior of the `Shift` modifier. ### Listing Modes The modes `histlist`, `loc` and `lastcmd` are all **listing modes**: They all show a list, and you can filter items and accept items. Because they are very similar, you may want to change their bindings at the same time. This is made possible by the `$edit:listing:binding` binding table (`listing` is not a "real" mode but an "abstract" mode). These modes still have their own binding tables like `$edit:histlist:binding`, and bindings there have higher precedence over those in the shared `$edit:listing:binding` table. Moreover, there are a lot of builtin functions in the `edit:listing` module like `edit:listing:down` (for moving down selection). They always apply to whichever listing mode is active. ### Caveat: Bindings to Start Modes Note that keybindings to **start** modes live in the binding table of the insert mode, not the target mode. For instance, if you want to be able to use Alt-l to start location mode, you should modify `$edit:insert:binding[Alt-l]`: ```elvish edit:insert:binding[Alt-l] = { edit:location:start } ``` One tricky case is the history mode. You can press ▲︎ to start searching for history, and continue pressing it to search further. However, when the first press happens, the editor is in insert mode, while with subsequent presses, the editor is in history mode. Hence this binding actually relies on two entries, `$edit:insert:binding[Up]` and `$edit:history:binding[Up]`. So for instance if you want to be able to use Ctrl-P for this, you need to modify both bindings: ```elvish edit:insert:binding[Ctrl-P] = { edit:history:start } edit:history:binding[Ctrl-P] = { edit:history:up } ``` ## Filter DSL The completion, history listing, location and navigation modes all support filtering the items to show using a filter DSL. It uses a small subset of Elvish's expression syntax, and can be any of the following: - A literal string (barewords and single-quoted or double-quoted strings all work) matches items containing the string. If the string is all lower case, the match is done case-insensitively; otherwise the match is case-sensitive. - A list `[re $string]` matches items matching the regular expression `$string`. The `$string` must be a literal string. - A list `[and $expr...]` matches items matching all of the `$expr`s. - A list `[or $expr...]` matches items matching any of the `$expr`s. If the filter contains multiple expressions, they are ANDed, as if surrounded by an implicit `[and ...]`. ## Completion API ### Argument Completer There are two types of completions in Elvish: completion for internal data and completion for command arguments. The former includes completion for variable names (e.g. `echo $`Tab) and indices (e.g. `echo $edit:insert:binding[`Tab). These are the completions that Elvish can provide itself because they only depend on the internal state of Elvish. The latter, in turn, is what happens when you type e.g. `cat`Tab. Elvish cannot provide completions for them without full knowledge of the command. Command argument completions are programmable via the `$edit:completion:arg-completer` variable. When Elvish is completing an argument of command `$x`, it will call the value stored in `$edit:completion:arg-completer[$x]`, with all the existing arguments, plus the command name in the front. For example, if the user types `man 1`Tab, Elvish will call: ```elvish $edit:completion:arg-completer[man] man 1 ``` If the user is starting a new argument when hitting Tab, Elvish will call the completer with a trailing empty string. For instance, if you do `man 1`SpaceTab, Elvish will call: ```elvish $edit:completion:arg-completer[man] man 1 "" ``` The output of this call becomes candidates. There are several ways of outputting candidates: - Writing byte output, e.g. "echo cand1; echo cand2". Each line becomes a candidate. This has the drawback that you cannot put newlines in candidates. Only use this if you are sure that you candidates will not contain newlines -- e.g. package names, usernames, but **not** file names, etc.. - Write strings to value output, e.g. "put cand1 cand2". Each string output becomes a candidate. - Use the `edit:complex-candidate` command, e.g.: ```elvish edit:complex-candidate &code-suffix='' &display=$stem' ('$description')' $stem ``` See [`edit:complex-candidate`](#edit:complex-candidate) for the full description of the arguments is accepts. After receiving your candidates, Elvish will match your candidates against what the user has typed. Hence, normally you don't need to (and shouldn't) do any matching yourself. That means that in many cases you can (and should) simply ignore the last argument to your completer. However, they can be useful for deciding what **kind** of things to complete. For instance, if you are to write a completer for `ls`, you want to see whether the last argument starts with `-` or not: if it does, complete an option; and if not, complete a filename. Here is a very basic example of configuring a completer for the `apt` command. It only supports completing the `install` and `remove` command and package names after that: ```elvish all-packages = [(apt-cache search '' | eawk [0 1 @rest]{ put $1 })] edit:completion:arg-completer[apt] = [@args]{ n = (count $args) if (== $n 2) { # apt x -- complete a subcommand name put install uninstall } elif (== $n 3) { put $@all-packages } } ``` Here is another slightly more complex example for the `git` command. It supports completing some common subcommands and then branch names after that: ```elvish fn all-git-branches { # Note: this assumes a recent version of git that supports the format # string used. git branch -a --format="%(refname:strip=2)" | eawk [0 1 @rest]{ put $1 } } common-git-commands = [ add branch checkout clone commit diff init log merge pull push rebase reset revert show stash status ] edit:arg-completer[git] = [@args]{ n = (count $args) if (== $n 2) { put $@common-git-commands } elif (>= $n 3) { all-git-branches } } ``` ### Matcher As stated above, after the completer outputs candidates, Elvish matches them with them with what the user has typed. For clarity, the part of the user input that is relevant to tab completion is called for the **seed** of the completion. For instance, in `echo x`Tab, the seed is `x`. Elvish first indexes the matcher table -- `$edit:completion:matcher` -- with the completion type to find a **matcher**. The **completion type** is currently one of `variable`, `index`, `command`, `redir` or `argument`. If the `$edit:completion:matcher` lacks the suitable key, `$edit:completion:matcher['']` is used. Elvish then calls the matcher with one argument -- the seed, and feeds the _text_ of all candidates to the input. The mather must output an identical number of booleans, indicating whether the candidate should be kept. As an example, the following code configures a prefix matcher for all completion types: ```elvish edit:completion:matcher[''] = [seed]{ each [cand]{ has-prefix $cand $seed } } ``` Elvish provides three builtin matchers, `edit:match-prefix`, `edit:match-substr` and `edit:match-subseq`. In addition to conforming to the matcher protocol, they accept two options `&ignore-case` and `&smart-case`. For example, if you want completion of arguments to use prefix matching and ignore case, use: ```elvish edit:completion:matcher[argument] = [seed]{ edit:match-prefix $seed &ignore-case=$true } ``` The default value of `$edit:completion:matcher` is `[&''=$edit:match-prefix~]`, hence that candidates for all completion types are matched by prefix. ## Hooks Hooks are functions that are executed at certain points in time. In Elvish this functionality is provided by variables that are a list of functions. **NOTE**: Hook variables may be initialized with a non-empty list, and you may have modules that add their own hooks. In general you should append to a hook variable rather than assign a list of functions to it. That is, rather than doing `set edit:some-hook = [ []{ put 'I ran' } ]` you should do `set edit:some-hook = [ $@hook-var []{ put 'I ran' } ]`. These are the editor/REPL hooks: - [`$edit:before-readline`](https://elv.sh/ref/edit.html#editbefore-readline): The functions are called before the editor runs. Each function is called with no arguments. - [`$edit:after-readline`](https://elv.sh/ref/edit.html#editafter-readline): The functions are called after the editor accepts a command for execution. Each function is called with a sole argument: the line just read. - [`$edit:after-command`](https://elv.sh/ref/edit.html#editafter-command): The functions are called after the shell executes the command you entered (typically by pressing the `Enter` key). Each function is called with a sole argument: a map that provides information about the executed command. This hook is also called after your interactive RC file is executed and before the first prompt is output. Example usage: ```elvish edit:before-readline = [{ echo 'going to read' }] edit:after-readline = [[line]{ echo 'just read '$line }] edit:after-command = [[m]{ echo 'command took '$m[duration]' seconds' }] ``` Given the above hooks... 1. Every time you accept a chunk of code (normally by pressing Enter) `just read` is printed. 1. At the very beginning of an Elvish session, or after a chunk of code is handled, `going to read` is printed. 1. After each non empty chunk of code is accepted and executed the string "command took ... seconds` is output. ## Word types The editor supports operating on entire "words". As intuitive as the concept of "word" is, there is actually no single definition for the concept. The editor supports the following three definitions of words: - A **big word**, or simply **word**, is a sequence of non-whitespace characters. This definition corresponds to the concept of "WORD" in vi. - A **small word** is a sequence of alphanumerical characters ("alnum small word"), or a sequence of non-alphanumerical, non-whitespace characters ("punctuation small word"). This definition corresponds to the concept of "word" in vi and zsh. - An **alphanumerical word** is a sequence of alphanumerical characters. This definition corresponds to the concept of "word" in bash. Whitespace characters are those with the Unicode [Whitespace](https://en.wikipedia.org/wiki/Whitespace_character#Unicode) property. Alphanumerical characters are those in the Unicode Letter or Number category. A **word boundary** is an imaginary zero-length boundary around a word. To see the difference between these definitions, consider the following string: `abc++ /* xyz`: - It contains three (big) words: `abc++`, `/*` and `xyz`. - It contains four small words, `abc`, `++`, `/*` and `xyz`. Among them, `abc` and `xyz` are alnum small words, while `++` and `/*` are punctuation small words. - It contains two alnum words, `abc` and `xyz`. elvish-0.17.0/website/ref/epm.md000066400000000000000000000106161415471104000164130ustar00rootroot00000000000000 @module epm # Introduction The Elvish Package Manager (`epm`) is a module bundled with Elvish for managing third-party packages. In Elvish terminology, a **module** is a `.elv` file that can be imported with the `use` command, while a **package** is a collection of modules that are usually kept in the same repository as one coherent project and may have interdependencies. The Elvish language itself only deals with modules; the concept of package is a matter of how to organize modules. Like the `go` command, Elvish does **not** have a central registry of packages. A package is simply identified by the URL of its code repository, e.g. [github.com/elves/sample-pkg](https://github.com/elves/sample-pkg). To install the package, one simply uses the following: ```elvish use epm epm:install github.com/elves/sample-pkg ``` `epm` knows out-of-the-box how to manage packages hosted in GitHub, BitBucket and GitLab, and requires the `git` command to be available. It can also copy files via `git` or `rsync` from arbitrary locations (see [Custom package domains](#custom-package-domains) for details). Once installed, modules in this package can be imported with `use github.com/elves/sample-pkg/...`. This package has a module named `sample-mod` containing a function `sample-fn`, and can be used like this: ```elvish-transcript ~> use github.com/elves/sample-pkg/sample-mod ~> sample-mod:sample-fn This is a sample function in a sample module in a sample package ``` # The `epm`-managed directory Elvish searches for modules in [multiple directories](command.html#module-search-directories), and `epm` only manages one of them: - On UNIX, `epm` manages `$XDG_DATA_HOME/elvish/lib`, defaulting to `~/.local/share/elvish/lib` if `$XDG_DATA_HOME` is unset or empty; - On Windows, `epm` manages `%LocalAppData%\elvish\lib`. This directory is called the `epm`-managed directory, and its path is available as [`$epm:managed-dir`](#epm:managed-dir). # Custom package domains Package names in `epm` have the following structure: `domain/path`. The `domain` is usually the hostname from where the package is to be fetched, such as `github.com`. The `path` can have one or more components separated by slashes. Usually, the full name of the package corresponds with the URL from where it can be fetched. For example, the package hosted at https://github.com/elves/sample-pkg is identified as `github.com/elves/sample-pkg`. Packages are stored under the `epm`-managed directory in a path identical to their name. For example, the package mentioned above is stored at `$epm:managed-dir/github.com/elves/sample-pkg`. Each domain must be configured with the following information: - The method to use to fetch packages from the domain. The two supported methods are `git` and `rsync`. - The number of directory levels under the domain directory in which the packages are found. For example, for `github.com` the number of levels is 2, since package paths have two levels (e.g. `elves/sample-pkg`). All packages from a given domain have the same number of levels. - Depending on the method, other attributes are needed: - `git` needs a `protocol` attribute, which can be `https` or `http`, and determines how the URL is constructed. - `rsync` needs a `location` attribute, which must be a valid source directory recognized by the `rsync` command. `epm` includes default domain configurations for `github.com`, `gitlab.com` and `bitbucket.org`. These three domains share the same configuration: ```json { "method": "git", "protocol": "https", "levels": "2" } ``` You can define your own domain by creating a file named `epm-domain.cfg` in the appropriate directory under `$epm:managed-dir`. For example, if you want to define an `elvish-dev` domain which installs packages from your local `~/dev/elvish/` directory, you must create the file `$epm:managed-dir/elvish-dev/epm-domain.cfg` with the following JSON content: ```json { "method": "rsync", "location": "~/dev/elvish", "levels": "1" } ``` You can then install any directory under `~/dev/elvish/` as a package. For example, if you have a directory `~/dev/elvish/utilities/`, the following command will install it under `$epm:managed-dir/elvish-dev/utilities`: ```elvish epm:install elvish-dev/utilities ``` When you make any changes to your source directory, `epm:upgrade` will synchronize those changes to `$epm:managed-dir`. elvish-0.17.0/website/ref/file.md000066400000000000000000000003371415471104000165500ustar00rootroot00000000000000 @module file # Introduction The `file:` module provides utilities for manipulating file objects. Function usages are given in the same format as in the reference doc for the [builtin module](builtin.html). elvish-0.17.0/website/ref/index.toml000066400000000000000000000020151415471104000173060ustar00rootroot00000000000000autoIndex = true prelude = "prelude" [[articles]] name = "language" title = "Language Specification" [[articles]] name = "command" title = "The Elvish Command" [[articles]] name = "builtin" title = "Builtin Functions and Variables" [[articles]] name = "edit" title = "edit: API for the Interactive Editor" [[articles]] name = "epm" title = "epm: The Elvish Package Manager" [[articles]] name = "file" title = "file: File Utilities" [[articles]] name = "math" title = "math: Math Utilities" [[articles]] name = "path" title = "path: Filesystem Path Utilities" [[articles]] name = "platform" title = "platform: Information About the Platform" [[articles]] name = "re" title = "re: Regular Expression Utilities" [[articles]] name = "readline-binding" title = "readline-binding: Readline-like Key Bindings" [[articles]] name = "store" title = "store: API for the Elvish Persistent Data Store" [[articles]] name = "str" title = "str: String Manipulation" [[articles]] name = "unix" title = "unix: Support for UNIX-like systems" elvish-0.17.0/website/ref/language.md000066400000000000000000002350541415471104000174220ustar00rootroot00000000000000 # Introduction This document describes the Elvish programming language. It is both a specification and an advanced tutorial. The parts of this document marked with either **notes** or called out as **examples** are non-normative, and only serve to help you understand the more formal descriptions. Examples in this document might use constructs that have not yet been introduced, so some familiarity with the language is assumed. If you are new to Elvish, start with the [learning materials](../learn/). # Source code encoding Elvish source code must be Unicode text encoded in UTF-8. In this document, **character** is a synonym of [Unicode codepoint](https://en.wikipedia.org/wiki/Code_point) or its UTF-8 encoding. # Lexical elements ## Whitespace In this document, an **inline whitespace** is any of the following: - A space (U+0020); - A tab (U+0009); - A comment: starting with `#` and ending before (but not including) the next carriage return, newline or end of file; - A line continuation: a `^` followed by a newline (`"\n"`), or a carriage return and newline (`"\r\n"`). A **whitespace** is any of the following: - An inline whitespace; - A carriage return (U+000D); - A newline (U+000A). ## Metacharacters The following **metacharacters** serve to introduce or delimit syntax constructs: | Metacharacter | Use | | ------------- | ----------------------------------------------------------- | | `$` | Referencing variables | | `*` and `?` | Forming wildcard | | `\|` | Separating forms in a pipeline | | `&` | Marking background pipelines; introducing key-value pairs | | `;` | Separating pipelines | | `<` and `>` | Introducing IO redirections | | `(` and `)` | Enclosing output captures | | `[` and `]` | Enclosing list literals, map literals or function signature | | `{` and `}` | Enclosing lambda literals or brace expressions | The following characters are parsed as metacharacters under certain conditions: - `~` is a metacharacter if it appears at the beginning of a compound expression, in which case it is subject to [tilde expansion](#tilde-expansion); - `=` is a metacharacter when used for terminating [map keys](#map) or option keys, or denoting [legacy assignment form](#legacy-assignment-form) or [temporary assignments](#temporary-assignment). ## Single-quoted string A single-quoted string consists of zero or more characters enclosed in single quotes (`'`). All enclosed characters represent themselves, except the single quote. Two consecutive single quotes are handled as a special case: they represent one single quote, instead of terminating a single-quoted string and starting another. **Examples**: `'*\'` evaluates to `*\`, and `'it''s'` evaluates to `it's`. ## Double-quoted string A double-quoted string consists of zero or more characters enclosed in double quotes (`"`). All enclosed characters represent themselves, except backslashes (`\`), which introduces **escape sequences**. Double quotes are not allowed inside double-quoted strings, except after backslashes. The following escape sequences are supported: - `\cX`, where _X_ is a character with codepoint between 0x40 and 0x5F, represents the codepoint that is 0x40 lower than _X_. For example, `\cI` is the tab character: 0x49 (`I`) - 0x40 = 0x09 (tab). There is one special case: A question-mark is converted to del; i.e., `\c?` or `\^?` is equivalent to `\x7F`. - `\^X` is the same as `\cX`. - `\[0..7][0..7][0..7]` is a byte written as an octal value. There must be three octal digits following the backslash. For example, `\000` is the nul character, and `\101` is the same as `A`, but `\0` is an invalid escape sequence (too few digits). - `\x..` is a Unicode code point represented by two hexadecimal digits. - `\u....` is a Unicode code point represented by four hexadecimal digits. - `\U......` is a Unicode code point represented by eight hexadecimal digits. - The following single character escape sequences: - `\a` is the "bell" character, equivalent to `\007` or `\x07`. - `\b` is the "backspace" character, equivalent to `\010` or `\x08`. - `\f` is the "form feed" character, equivalent to `\014` or `\x0c`. - `\n` is the "new line" character, equivalent to `\012` or `\x0a`. - `\r` is the "carriage return" character, equivalent to `\015` or `\x0d`. - `\t` is the "tab" character, equivalent to `\011` or `\x09`. - `\v` is the "vertical tab" character, equivalent to `\013` or `\x0b`. - `\\` is the "backslash" character, equivalent to `\134` or `\x5c`. - `\"` is the "double-quote" character, equivalent to `\042` or `\x22`. An unsupported escape sequence results in a parse error. **Note**: Unlike most other shells, double-quoted strings in Elvish do not support interpolation. For instance, `"$name"` simply evaluates to a string containing `$name`. To get a similar effect, simply concatenate strings: instead of `"my name is $name"`, write `"my name is "$name`. Under the hood this is a [compounding](#compounding) operation. ## Bareword A string can be written without quoting -- a **bareword**, if it only includes the characters from the following set: - ASCII letters (a-z and A-Z) and numbers (0-9); - The symbols `!%+,-./:@\_`; - Non-ASCII codepoints that are printable, as defined by [unicode.IsPrint](https://godoc.org/unicode#IsPrint) in Go's standard library. **Examples**: `a.txt`, `long-bareword`, `elf@elv.sh`, `/usr/local/bin`, `你好世界`. Moreover, `~` and `=` are allowed to appear without quoting when they are not parsed as [metacharacters](#metacharacters). **Note**: since the backslash (`\`) is a valid bareword character in Elvish, it cannot be used to escape metacharacter. Use quotes instead: for example, to echo a star, write `echo "*"` or `echo '*'`, not `echo \*`. The last command just writes out `\*`. # Value types ## String A string is a (possibly empty) sequence of bytes. [Single-quoted string literals](#single-quoted-string), [double-quoted string literals](#double-quoted-string) and [barewords](#bareword) all evaluate to string values. Unless otherwise noted, different syntaxes of string literals are equivalent in the code. For instance, `xyz`, `'xyz'` and `"xyz"` are different syntaxes for the same string with content `xyz`. Strings that contain UTF-8 encoded text can be [indexed](#indexing) with a **byte index** where a codepoint starts, which results in the codepoint that starts there. The index can be given as either a typed [number](#number), or a string that parses to a number. Examples: - In the string `elv`, every codepoint is encoded with only one byte, so 0, 1, 2 are all valid indices: ```elvish-transcript ~> put elv[0] ▶ e ~> put elv[1] ▶ l ~> put elv[2] ▶ v ``` - In the string `世界`, each codepoint is encoded with three bytes. The first codepoint occupies byte 0 through 2, and the second occupies byte 3 through 5\. Hence valid indices are 0 and 3: ```elvish-transcript ~> put 世界[0] ▶ 世 ~> put 世界[3] ▶ 界 ``` Such strings may also be indexed with a slice (see documentation of [list](#list) for slice syntax). The range determined by the slice is also interpreted as byte indices, and the range must begin and end at codepoint boundaries. The behavior of indexing a string that does not contain valid UTF-8-encoded Unicode text is unspecified. **Note**: String indexing will likely change. ## Number Elvish supports several types of numbers. There is no literal syntax, but they can be constructed by passing their **string representation** to the [`num`](builtin.html#num) builtin command: - **Integers** are written in decimal (e.g. `10`), hexadecimal (e.g. `0xA`), octal (e.g. `0o12`) or binary (e.g. `0b1010`). **NOTE**: Integers with leading zeros are now parsed as octal (e.g. `010` is the same as `0o10`, or `8`), but this is subject to change ([#1372](https://b.elv.sh/1371)). - **Rationals** are written as two exact integers joined by `/`, e.g. `1/2` or `0x10/100` (16/100). - **Floating-point numbers** are written with a decimal point (e.g. `10.0`) or using scientific notation (e.g. `1e1` or `1.0e1`). There are also three additional special floating-point values: `+Inf`, `-Inf` and `NaN`. Digits may be separated by underscores, which are ignored; this permits separating the digits into groups to improve readability. For example, `1000000` and `1_000_000` are equivalent, so are `1.234_56e3` and `1.23456e3`, or `1_2_3` and `123`. The string representation is case-insensitive. ### Strings and numbers Strings and numbers are distinct types; for example, `2` and `(num 2)` are distinct values. However, by convention, all language constructs that expect numbers (e.g. list indices) also accept strings that can be converted to numbers. This means that most of the time, you can just use the string representation of numbers, instead of explicitly constructing number values. Builtin [numeric commands](./builtin.html#numeric-commands) follow the same convention. When the word **number** appears unqualified in other sections of this document, it means either an explicitly number-typed value (**typed number**), or its string representation. When a typed number is converted to a string (e.g. with `to-string`), the result is guaranteed to convert back to the original number. In other words, `eq $x (num (to-string $x))` always outputs `$true` if `$x` is a typed number. ### Exactness Integers and rationals are **exact** numbers; their precision is only limited by the available memory, and many (but not all) operations on them are guaranteed to produce mathematically correct results. Floating-point numbers are [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754) double-precision. Since operations on floating-point numbers in general are not guaranteed to be precise, they are always considered **inexact**. This distinction is important for some builtin commands; see [exactness-preserving commands](./builtin.html#exactness-preserving). ## List A list is a value containing a sequence of values. Values in a list are called its **elements**. Each element has an index, starting from zero. List literals are surrounded by square brackets `[ ]`, with elements separated by whitespace. Examples: ```elvish-transcript ~> put [lorem ipsum] ▶ [lorem ipsum] ~> put [lorem ipsum foo bar] ▶ [lorem ipsum foo bar] ``` **Note**: In Elvish, commas have no special meanings and are valid bareword characters, so don't use them to separate elements: ```elvish-transcript ~> li = [a, b] ~> put $li ▶ [a, b] ~> put $li[0] ▶ a, ``` A list can be [indexed](#indexing) with the index of an element to obtain the element, which can take one of two forms: - A non-negative integer, an offset counting from the beginning of the list. For example, `$li[0]` is the first element of `$li`. - A negative integer, an offset counting from the back of the list. For instance, `$li[-1]` is the last element `$li`. In both cases, the index can be given either as a [typed number](#number) or a number-like string. A list can also be indexed with a **slice** to obtain a sublist, which can take one of two forms: - A slice `$a..$b`, where both `$a` and `$b` are integers. The result is sublist of `$li[$a]` up to, but not including, `$li[$b]`. For instance, `$li[4..7]` equals `[$li[4] $li[5] $li[6]]`, while `$li[1..-1]` contains all elements from `$li` except the first and last one. Both integers may be omitted; `$a` defaults to 0 while `$b` defaults to the length of the list. For instance, `$li[..2]` is equivalent to `$li[0..2]`, `$li[2..]` is equivalent to `$li[2..(count $li)]`, and `$li[..]` makes a copy of `$li`. The last form is rarely useful, as lists are immutable. Note that the slice needs to be a **single** string, so there cannot be any spaces within the slice. For instance, `$li[2..10]` cannot be written as `$li[2.. 10]`; the latter contains two indices and is equivalent to `$li[2..] $li[10]` (see [Indexing](#indexing)). - A slice `$a..=$b`, which is similar to `$a..$b`, but includes `$li[$b]`. Examples: ```elvish-transcript ~> li = [lorem ipsum foo bar] ~> put $li[0] ▶ lorem ~> put $li[-1] ▶ bar ~> put $li[0..2] ▶ [lorem ipsum] ``` ## Map A map is a value containing unordered key-value pairs. Map literals are surrounded by square brackets; a key/value pair is written `&key=value` (reminiscent to HTTP query parameters), and pairs are separated by whitespaces. Whitespaces are allowed after `=`, but not before `=`. Examples: ```elvish-transcript ~> put [&foo=bar &lorem=ipsum] ▶ [&foo=bar &lorem=ipsum] ~> put [&a= 10 &b= 23 &sum= (+ 10 23)] ▶ [&a=10 &b=23 &sum=33] ``` The literal of an empty map is `[&]`. Specifying a key without `=` or a value following it is equivalent to specifying `$true` as the value. Specifying a key with `=` but no value following it is equivalent to specifying the empty string as the value. Example: ```elvish-transcript ~> echo [&a &b=] [&a=$true &b=''] ``` A map can be indexed by any of its keys. Unlike strings and lists, there is no support for slices, and `..` and `..=` have no special meanings. Examples: ```elvish-transcript ~> map = [&a=lorem &b=ipsum &a..b=haha] ~> echo $map[a] lorem ~> echo $map[a..b] haha ``` You can test if a key is present using [`has-key`](./builtin.html#has-key) and enumerate the keys using the [`keys`](./builtin.html#keys) builtins. **Note**: Since `&` is a [metacharacter](#metacharacters), key-value pairs do not have to follow whitespaces; `[&a=lorem&b=ipsum]` is equivalent to `[&a=lorem &b=ipsum]`, just less readable. This might change in future. ## Pseudo-map A pseudo-map is not a single concrete data type. It refers to concrete types that behave like maps with some restrictions. A pseudo-map has a fixed set of keys whose values can be accessed by [indexing](#indexing) like you would for a regular [map](#map). Similarly, you can use commands like [`keys`](./builtin.html#keys) and [`has-key`](./builtin.html#keys) on such objects. Unlike a normal map, it is currently not possible to create a modified version of an existing pseudo-map: it is not possible to create a pseudo-map with new keys, without existing keys, or with a different value for a given key. The pseudo-map mechanism is often used for introspection. For example, [exceptions](#exception), [user-defined functions](#function), and [`$buildinfo`](./builtin.html#buildinfo) are pseudo-maps. ## Nil The value `$nil` serves as the initial value of variables that are declared but not assigned. ## Boolean There are two boolean values, `$true` and `$false`. When converting non-boolean values to the boolean type, `$nil` and exceptions convert to `$false`; such values and `$false` itself are **booleanly false**. All the other non-boolean values convert to `$true`; such values and `$true` itself are **booleanly true**. ## Exception An exception carries information about errors during the execution of code. There is no literal syntax for exceptions. See the discussion of [exception and flow commands](#exception-and-flow-commands) for more information about this data type. An exception is a [pseudo-map](#pseudo-map) with a `reason` field, which is in turn a pseudo-map. The reason pseudo-map has has a `type` field identifying how the exception was raised, and further fields depending on the type: - If the `type` field is `fail`, the exception was raised by the [fail](builtin.html#fail) command. In this case, the `content` field contains the argument to `fail`. - If the `type` field is `flow`, the exception was raised by one of the flow commands. In this case, the `name` field contains the name of the flow command. - If the `type` field is `pipeline`, the exception was a result of multiple commands in the same pipeline raising exceptions. In this case, the `exceptions` field contains the exceptions from the individual commands. - If the `type` field starts with `external-cmd/`, the exception was caused by one of several conditions of an external command. In this case, the following fields are available: - The `cmd-name` field contains the name of the command. - The `pid` field contains the PID of the command. - If the `type` field is `external-cmd/exited`, the external command exited with a non-zero status code. In this case, the `exit-status` field contains the exit status. - If the `type` field is `external-cmd/signaled`, the external command was killed by a signal. In this case, the following extra fields are available: - The `signal-name` field contains the name of the signal. - The `signal-number` field contains the numerical value of the signal, as a string. - The `core-dumped` field is a boolean reflecting whether a core dump was generated. - If the `type` field is `external-cmd/stopped`, the external command was stopped. In this case, the following extra fields are available: - The `signal-name` field contains the name of the signal. - The `signal-number` field contains the numerical value of the signal, as a string. - The `trap-cause` field contains the number indicating the trap cause. Examples: ```elvish-transcript ~> put ?(fail foo)[reason] ▶ [&content=foo &type=fail] ~> put ?(return)[reason] ▶ [&name=return &type=flow] ~> put ?(false)[reason] ▶ [&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#file:open) and [path:temp-file](path.html#path: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 [ordinary command](#ordinary-command), and takes its arguments and options. Functions are first-class values; they can be kept in variables, used as arguments, output on the value channel and embedded in other data structures. Elvish comes with a set of **builtin functions**, and Elvish code can also create **user-defined functions**. **Note**: Unlike most programming languages, functions in Elvish do not have return values. Instead, they can output values, which can be [captured](#output-capture) later. A **function literal**, or alternatively a **lambda**, evaluates to a user-defined function. The literal syntax consists of an optional **signature list**, followed by a [code chunk](#code-chunk) that defines the body of the function. Here is an example without a signature: ```elvish-transcript ~> f = { echo "Inside a lambda" } ~> put $f ▶ ``` One or more whitespace characters after `{` is required: Elvish relies on the presence of whitespace to disambiguate function literals and [braced lists](#braced-list). **Note**: It is good style to put some whitespace before the closing `}` for symmetry, but this is not required by the syntax. Functions defined without a signature list do not accept any arguments or options. To do so, write a signature list. Here is an example: ```elvish-transcript ~> f = [a b]{ put $b $a } ~> $f lorem ipsum ▶ ipsum ▶ lorem ``` There must be no space between `]` and `{`; otherwise Elvish will parse the signature as a list, followed by a lambda without signature: ```elvish-transcript ~> put [a]{ nop } ▶ ~> put [a] { nop } ▶ [a] ▶ ``` Like in the left hand of assignments, if you prefix one of the arguments with `@`, it becomes a **rest argument**, and its value is a list containing all the remaining arguments: ```elvish-transcript ~> f = [a @rest]{ put $a $rest } ~> $f lorem ▶ lorem ▶ [] ~> $f lorem ipsum dolar sit ▶ lorem ▶ [ipsum dolar sit] ~> f = [a @rest b]{ put $a $rest $b } ~> $f lorem ipsum dolar sit ▶ lorem ▶ [ipsum dolar] ▶ sit ``` You can also declare options in the signature. The syntax is `&name=default` (like a map pair), where `default` is the default value for the option; the value of the option will be kept in a variable called `name`: ```elvish-transcript ~> f = [&opt=default]{ echo "Value of $opt is "$opt } ~> $f Value of $opt is default ~> $f &opt=foobar Value of $opt is foobar ``` Options must have default values: Options should be **option**al. If you call a function with too few arguments, too many arguments or unknown options, an exception is thrown: ```elvish-transcript ~> [a]{ echo $a } foo bar Exception: need 1 arguments, got 2 [tty], line 1: [a]{ echo $a } foo bar ~> [a b]{ echo $a $b } foo Exception: need 2 arguments, got 1 [tty], line 1: [a b]{ echo $a $b } foo ~> [a b @rest]{ echo $a $b $rest } foo Exception: need 2 or more arguments, got 1 [tty], line 1: [a b @rest]{ echo $a $b $rest } foo ~> [&k=v]{ echo $k } &k2=v2 Exception: unknown option k2 [tty], line 1: [&k=v]{ echo $k } &k2=v2 ``` A user-defined function is a [pseudo-map](#pseudo-map). If `$f` is a user-defined function, it has the following fields: - `$f[arg-names]` is a list containing the names of the arguments. - `$f[rest-arg]` is the index of the rest argument. If there is no rest argument, it is `-1`. - `$f[opt-names]` is a list containing the names of the options. - `$f[opt-defaults]` is a list containing the default values of the options, in the same order as `$f[opt-names]`. - `$f[def]` is a string containing the definition of the function, including the signature and the body. - `$f[body]` is a string containing the body of the function, without the enclosing brackets. - `$f[src]` is a map-like data structure containing information about the source code that the function is defined in. It contains the same value that the [src](builtin.html#src) function would output if called from the function. # Variable A variable is a named storage location for holding a value. The following characters can be used in variable names (a subset of bareword characters) without quoting: A variable exist after it is declared (either explicitly using [`var`](#var) or implicitly using the [legacy assignment form](#legacy-assignment-form)), and its value may be mutated by further assignments. It can be [used](#variable-use) as an expression or part of an expression. **Note**: In most other shells, variables can map directly to environmental variables: `$PATH` is the same as the `PATH` environment variable. This is not the case in Elvish. Instead, environment variables are put in a dedicated `E:` namespace; the environment variable `PATH` is known as `$E:PATH`. The `$PATH` variable, on the other hand, does not exist initially, and if you have defined it, only lives in a certain lexical scope within the Elvish interpreter. You will notice that variables sometimes have a leading dollar `$`, and sometimes not. The tradition is that they do when they are used for their values, and do not otherwise (e.g. in assignment). This is consistent with most other shells. ## Variable suffix There are two characters that have special meanings and extra type constraints when used as the suffix of a variable name: - If a variable name ends with `~`, it can only take callable values, which are functions and external commands. Such variables are consulted when resolving [ordinary commands](#ordinary-command). The default value is the builtin [`nop`](builtin.html#nop) command. - If a variable name ends with `:`, it can only take namespaces as values. They are used for accessing namespaced variables. ## Scoping rule Elvish has lexical scoping. A file or an interactive prompt starts with a top-level scope, and a [function literal](#function) introduce new lexical scopes. When you use a variable, Elvish looks for it in the current lexical scope, then its parent lexical scope and so forth, until the outermost scope: ```elvish-transcript ~> x = 12 ~> { echo $x } # $x is in the global scope 12 ~> { y = bar; { echo $y } } # $y is in the outer scope bar ``` If a variable is not in any of the lexical scopes, Elvish tries to resolve it in the `builtin:` namespace, and if that also fails, fails with an error: ```elvish-transcript ~> echo $pid # builtin 36613 ~> echo $nonexistent Compilation error: variable $nonexistent not found [interactive], line 1: echo $nonexistent ``` Note that Elvish resolves all variables in a code chunk before starting to execute any of it; that is why the error message above says _compilation error_. This can be more clearly observed in the following example: ```elvish-transcript ~> echo pre-error; echo $nonexistent Compilation error: variable $nonexistent not found [tty], line 1: echo pre-error; echo $nonexistent ``` When you assign a variable, Elvish does a similar searching. If the variable cannot be found, instead of causing an error, it will be created in the current scope: ```elvish-transcript ~> x = 12 ~> { x = 13 } # assigns to x in the global scope ~> echo $x 13 ~> { z = foo } # creates z in the inner scope ~> echo $z Compilation error: variable $z not found [tty], line 1: echo $z ``` One implication of this behavior is that Elvish will not shadow your variable in outer scopes. There is a `local:` namespace that always refers to the current scope, and by using it it is possible to force Elvish to shadow variables: ```elvish-transcript ~> x = 12 ~> { local:x = 13; echo $x } # force shadowing 13 ~> echo $x 12 ``` After force shadowing, you can still access the variable in the outer scope using the `up:` namespace, which always **skips** the innermost scope: ```elvish-transcript ~> x = 12 ~> { local:x = 14; echo $x $up:x } 14 12 ``` The `local:` and `up:` namespaces can also be used on unshadowed variables, although they are not useful in those cases: ```elvish-transcript ~> foo = a ~> { echo $up:foo } # $up:foo is the same as $foo a ~> { bar = b; echo $local:bar } # $local:bar is the same as $bar b ``` It is not possible to refer to a specific outer scope. You cannot create new variables in the `builtin:` namespace, although existing variables in it can be assigned new values. ## Closure semantics When a function literal refers to a variable in an outer scope, the function will keep that variable alive, even if that variable is the local variable of an outer function that that function has returned. This is called [closure semantics](), because the function literal "closes" over the environment it is defined in. In the following example, the `make-adder` function outputs two functions, both referring to a local variable `$n`. Closure semantics means that: 1. Both functions can continue to refer to the `$n` variable after `make-adder` has returned. 2. Multiple calls to the `make-adder` function generates distinct instances of the `$n` variables. ```elvish-transcript ~> fn make-adder { n = 0 put { put $n } { n = (+ $n 1) } } ~> getter adder = (make-adder) ~> $getter # $getter outputs $n ▶ 0 ~> $adder # $adder increments $n ~> $getter # $getter and $setter refer to the same $n ▶ 1 ~> getter2 adder2 = (make-adder) ~> $getter2 # $getter2 and $getter refer to different $n ▶ 0 ~> $getter ▶ 1 ``` Variables that get "captured" in closures are called **upvalues**; this is why the pseudo-namespace for variables in outer scopes is called `up:`. When capturing upvalues, Elvish only captures the variables that are used. In the following example, `$m` is not an upvalue of `$g` because it is not used: ```elvish-transcript ~> fn f { m = 2; n = 3; put { put $n } } ~> g = (f) ``` This effect is not currently observable, but will become so when namespaces [become introspectable](https://github.com/elves/elvish/issues/492). # Expressions Elvish has a few types of expressions. Some of those are new compared to most other languages, but some are very similar. Unlike most other languages, expressions in Elvish may evaluate to any number of values. The concept of multiple values is distinct from a list of multiple elements. ## Literal Literals of [strings](#string), [lists](#list), [maps](#map) and [functions](#function) all evaluate to one value of their corresponding types. They are described in their respective sections. ## Variable use A **variable use** expression is formed by a `$` followed by the name of the variable. Examples: ```elvish-transcript ~> foo = bar ~> x y = 3 4 ~> put $foo ▶ bar ~> put $x ▶ 3 ``` If the variable name only contains the following characters (a subset of bareword characters), the name can appear unquoted after `$` and the variable use expression extends to the longest sequence of such characters: - ASCII letters (a-z and A-Z) and numbers (0-9); - The symbols `-_:~`. The colon `:` is special; it is normally used for separating namespaces or denoting namespace variables; - Non-ASCII codepoints that are printable, as defined by [unicode.IsPrint](https://godoc.org/unicode#IsPrint) in Go's standard library. Alternatively, `$` may be followed immediately by a [single-quoted string](https://elv.sh/ref/language.html#single-quoted-string) or a [double-quoted string](https://elv.sh/ref/language.html#double-quoted-string), in which cases the value of the string specifies the name of the variable. Examples: ```elvish-transcript ~> "\n" = foo ~> put $"\n" ▶ foo ~> '!!!' = bar ~> put $'!!!' ▶ bar ``` Unlike other shells and other dynamic languages, local namespaces in Elvish are statically checked. This means that referencing a nonexistent variable results in a compilation error, which is triggered before any code is actually evaluated: ```elvish-transcript ~> echo $x Compilation error: variable $x not found [tty], line 1: echo $x ~> f = { echo $x } compilation error: variable $x not found [tty 1], line 1: f = { echo $x } ``` If a variable contains a list value, you can add `@` before the variable name; this evaluates to all the elements within the list. This is called **exploding** the variable: ```elvish-transcript ~> li = [lorem ipsum foo bar] ~> put $li ▶ [lorem ipsum foo bar] ~> put $@li ▶ lorem ▶ ipsum ▶ foo ▶ bar ``` **Note**: Since variable uses have higher precedence than [indexing](#indexing), this does not work for exploding a list that is an element of another list. For doing that, and exploding the result of other expressions (such as an output capture), use the builtin [all](builtin.html#all) command.) ## Output capture An **output capture** expression is formed by putting parentheses `()` around a [code chunk](#code-chunk). It redirects the output of the chunk into an internal pipe, and evaluates to all the values that have been output. ```elvish-transcript ~> + 1 10 100 ▶ 111 ~> x = (+ 1 10 100) ~> put $x ▶ 111 ~> put lorem ipsum ▶ lorem ▶ ipsum ~> x y = (put lorem ipsum) ~> put $x ▶ lorem ~> put $y ▶ ipsum ``` If the chunk outputs bytes, Elvish strips the last newline (if any), and split them by newlines, and consider each line to be one string value: ```elvish-transcript ~> put (echo "a\nb") ▶ a ▶ b ``` Trailing carriage returns are also stripped from each line, which effectively makes `\r\n` also valid line separators: ```elvish-transcript ~> put (echo "a\r\nb") ▶ a ▶ b ``` **Note 1**. Only the last newline is ever removed, so empty lines are preserved; `(echo "a\n")` evaluates to two values, `"a"` and `""`. **Note 2**. One consequence of this mechanism is that you can not distinguish outputs that lack a trailing newline from outputs that have one; `(echo what)` evaluates to the same value as `(print what)`. If such a distinction is needed, use [`slurp`](builtin.html#slurp) to preserve the original bytes output. If the chunk outputs both values and bytes, the values of output capture will contain both value outputs and lines. However, the ordering between value output and byte output might not agree with the order in which they happened: ```elvish-transcript ~> put (put a; echo b) # value order need not be the same as output order ▶ b ▶ a ``` **Note**: If you want to capture the stdout and stderr byte streams independent of each other, see the example in the [run-parallel](./builtin.html#run-parallel) documentation. ## Exception capture An **exception capture** expression is formed by putting `?()` around a code chunk. It runs the chunk and evaluates to the exception it throws. ```elvish-transcript ~> fail bad Exception: bad Traceback: [interactive], line 1: fail bad ~> put ?(fail bad) ▶ ?(fail bad) ``` If there was no error, it evaluates to the special value `$ok`: ```elvish-transcript ~> nop ~> put ?(nop) ▶ $ok ``` Exceptions are booleanly false and `$ok` is booleanly true. This is useful in `if` (introduced later): ```elvish-transcript if ?(test -d ./a) { # ./a is a directory } ``` **Note**: Exception captures do not affect the output of the code chunk. You can combine output capture and exception capture: ```elvish output = (error = ?(commands-that-may-fail)) ``` ## Braced list A **braced list** consists of multiple expressions separated by whitespaces and surrounded by braces (`{}`). There must be no space after the opening brace. A braced list evaluates to whatever the expressions inside it evaluate to. Its most typical use is grouping multiple values in a [compound expression](#compounding). Example: ```elvish-transcript ~> put {a b}-{1 2} ▶ a-1 ▶ a-2 ▶ b-1 ▶ b-2 ``` It can also be used to affect the [order of evaluation](#order-of-evaluation). Examples: ```elvish-transcript ~> put * ▶ foo ▶ bar ~> put *o ▶ foo ~> put {*}o ▶ fooo ▶ baro ``` **Note**: When used to affect the order of evaluation, braced lists are very similar to parentheses in C-like languages. **Note**: A braced list is an expression. It is a syntactical construct and not a separate data structure. Elvish currently also supports using commas to separate items in a braced list. This will likely be removed in future, but it also means that literal commas must be quoted right now. ## Indexing An **indexing expression** is formed by appending one or more indices inside a pair of brackets (`[]`) after another expression (the indexee). Examples: ```elvish-transcript ~> li = [foo bar] ~> put $li[0] ▶ foo ~> li = [[foo bar] quux] ~> put $li[0][0] ▶ foo ~> put [[foo bar]][0][0] ▶ foo ``` If the expression being indexed evaluates to multiple values, the indexing operation is applied on each value. Example: ```elvish-transcript ~> put (put [foo bar] [lorem ipsum])[0] ▶ foo ▶ lorem ~> put {[foo bar] [lorem ipsum]}[0] ▶ foo ▶ lorem ``` If there are multiple index expressions, or the index expression evaluates to multiple values, the indexee is indexed once for each of the index value. Examples: ```elvish-transcript ~> put elv[0 2 0..2] ▶ e ▶ v ▶ el ~> put [lorem ipsum foo bar][0 2 0..2] ▶ lorem ▶ foo ▶ [lorem ipsum] ~> put [&a=lorem &b=ipsum &a..b=haha][a a..b] ▶ lorem ▶ haha ``` If both the indexee and index evaluate to multiple values, the results generated from the first indexee appear first. Example: ```elvish-transcript ~> put {[foo bar] [lorem ipsum]}[0 1] ▶ foo ▶ bar ▶ lorem ▶ ipsum ``` ## Compounding A **compound expression** is formed by writing several expressions together with no space in between. A compound expression evaluates to a string concatenation of all the constituent expressions. Examples: ```elvish-transcript ~> put 'a'b"c" # compounding three string literals ▶ abc ~> v = value ~> put '$v is '$v # compounding one string literal with one string variable ▶ '$v is value' ``` When one or more of the constituent expressions evaluate to multiple values, the result is all possible combinations: ```elvish-transcript ~> li = [foo bar] ~> put {a b}-$li[0 1] ▶ a-foo ▶ a-bar ▶ b-foo ▶ b-bar ``` The order of the combinations is determined by first taking the first value in the leftmost expression that generates multiple values, and then taking the second value, and so on. ## Tilde expansion An unquoted tilde at the beginning of a compound expression triggers **tilde expansion**. The remainder of this expression must be a string. The part from the beginning of the string up to the first `/` (or the end of the word if the string does not contain `/`), is taken as a user name; and they together evaluate to the home directory of that user. If the user name is empty, the current user is assumed. In the following example, the home directory of the current user is `/home/xiaq`, while that of the root user is `/root`: ```elvish-transcript ~> put ~ ▶ /home/xiaq ~> put ~root ▶ /root ~> put ~/xxx ▶ /home/xiaq/xxx ~> put ~root/xxx ▶ /root/xxx ``` Note that tildes are not special when they appear elsewhere in a word: ```elvish-transcript ~> put a~root ▶ a~root ``` If you need them to be, use a [braced list](#braced-list): ```elvish-transcript ~> put a{~root} ▶ a/root ``` ## Wildcard expansion **Wildcard patterns** are expressions that contain **wildcards**. Wildcard patterns evaluate to all filenames they match. In examples in this section, we will assume that the current directory has the following structure: ``` .x.conf a.cc ax.conf foo.cc d/ |__ .x.conf |__ ax.conf |__ y.cc .d2/ |__ .x.conf |__ ax.conf ``` Elvish supports the following wildcards: - `?` matches one arbitrary character except `/`. For example, `?.cc` matches `a.cc`; - `*` matches any number of arbitrary characters except `/`. For example, `*.cc` matches `a.cc` and `foo.cc`; - `**` matches any number of arbitrary characters including `/`. For example, `**.cc` matches `a.cc`, `foo.cc` and `b/y.cc`. The following behaviors are default, although they can be altered by modifiers: - When the entire wildcard pattern has no match, an error is thrown. - None of the wildcards matches `.` at the beginning of filenames. For example: - `?x.conf` does not match `.x.conf`; - `d/*.conf` does not match `d/.x.conf`; - `**.conf` does not match `d/.x.conf`. Wildcards can be **modified** using the same syntax as indexing. For instance, in `*[match-hidden]` the `*` wildcard is modified with the `match-hidden` modifier. Multiple matchers can be chained like `*[set:abc][range:0-9]`. In which case they are OR'ed together. There are two kinds of modifiers: **Global modifiers** apply to the whole pattern and can be placed after any wildcard: - `nomatch-ok` tells Elvish not to throw an error when there is no match for the pattern. For instance, in the example directory `put bad*` will be an error, but `put bad*[nomatch-ok]` does exactly nothing. - `but:xxx` (where `xxx` is any filename) excludes the filename from the final result. - `type:xxx` (where `xxx` is a recognized file type from the list below). Only one type modifier is allowed. For example, to find the directories at any level below the current working directory: `**[type:dir]`. - `dir` will match if the path is a directory. - `regular` will match if the path is a regular file. Although global modifiers affect the entire wildcard pattern, you can add it after any wildcard, and the effect is the same. For example, `put */*[nomatch-ok].cpp` and `put *[nomatch-ok]/*.cpp` do the same thing. On the other hand, you must add it after a wildcard, instead of after the entire pattern: `put */*.cpp[nomatch-ok]` unfortunately does not do the correct thing. (This will probably be fixed.) **Local modifiers** only apply to the wildcard it immediately follows: - `match-hidden` tells the wildcard to match `.` at the beginning of filenames, e.g. `*[match-hidden].conf` matches `.x.conf` and `ax.conf`. Being a local modifier, it only applies to the wildcard it immediately follows. For instance, `*[match-hidden]/*.conf` matches `d/ax.conf` and `.d2/ax.conf`, but not `d/.x.conf` or `.d2/.x.conf`. - Character matchers restrict the characters to match: - Character sets, like `set:aeoiu`; - Character ranges like `range:a-z` (including `z`) or `range:a~z` (excluding `z`); - Character classes: `control`, `digit`, `graphic`, `letter`, `lower`, `mark`, `number`, `print`, `punct`, `space`, `symbol`, `title`, and `upper`. See the Is\* functions [here](https://godoc.org/unicode) for their definitions. Note the following caveats: - Local matchers chained together in separate modifiers are OR'ed. For instance, `?[set:aeoiu][digit]` matches all files with the chars `aeoiu` or containing a digit. - Local matchers combined in the same modifier, such as `?[set:aeoiu digit]`, behave in a hard to explain manner. Do not use this form as **the behavior is likely to change in the future.** - Dots at the beginning of filenames always require an explicit `match-hidden`, even if the matcher includes `.`. For example, `?[set:.a]x.conf` does **not** match `.x.conf`; you have to `?[set:.a match-hidden]x.conf`. - Likewise, you always need to use `**` to match slashes, even if the matcher includes `/`. For example `*[set:abc/]` is the same as `*[set:abc]`. ## Order of evaluation An expression can use a combination of indexing, tilde expansion, wildcard and compounding. The order of evaluation is as follows: 1. Literals, variable uses, output captures and exception captures and braced lists have the highest precedence and are evaluated first. 2. Indexing has the next highest precedence and is then evaluated first. 3. Expression compounding then happens. Tildes and wildcards are kept unevaluated. 4. If the expression starts with a tilde, tilde expansion happens. If the tilde is followed by a wildcard, an exception is raised. 5. If the expression contains any wildcard, wildcard expansion happens. Here an example: in `~/$li[0 1]/*` (where `$li` is a list `[foo bar]`), the expression is evaluated as follows: 1. The variable use `$li` evaluates to the list `[foo bar]`. 2. The indexing expression `$li[0]` evaluates to two strings `foo` and `bar`. 3. Compounding the expression, the result is `~/foo/*` and `~/bar/*`. 4. Tilde expansion happens; assuming that the user's home directory is `/home/elf`, the values are now `/home/elf/foo/*` and `/home/elf/bar/*`. 5. Wildcard expansion happens, evaluating the expression to all the filenames within `/home/elf/foo` and `/home/elf/bar`. If any directory is empty or nonexistent, an exception is thrown. To force a particular order of evaluation, group expressions using a [braced list](#braced-list). # Command forms A **command form** is either an [ordinary command](#ordinary-command), a [special command](#special-command) or an [legacy assignment form](#legacy-assignment-form). All of three different types have access to [IO ports](#io-ports), which can be modified via [redirections](#redirection). When Elvish parses a command form, it applies the following process to decide its type: - If the command form contains an unquoted equal sign surrounded by inline whitespaces, it is an ordinary assignment. - If the first expression in the command form contains a single string literal, and the string value matches one of the special commands, it is a special command. - Otherwise, it is an ordinary command. ## Ordinary command An **ordinary command** form consists of a command head, and any number of arguments and options. The first expression in an ordinary command is the command **head**. If the head is a single string literal, it is subject to **static resolution**: - If a variable with name `head~` (where `head` is the value of the head) exists, the head will evaluate as if it is `$head~`; i.e., a function invocation. - If the head contains at least one slash, it is treated as an external command with the value as its path relative to the current directory. - Otherwise, the head is considered "unknown", and the behavior is controlled by the `unknown-command` [pragma](#pragma): - If the `unknown-command` pragma is set to `external` (the default), the head is treated as the name of an external command, to be searched in the `$E:PATH` during runtime. - If the `unknown-command` pragma is set to `disallow`, such command heads trigger a compilation error. If the head is not a single string literal, it is evaluated as a normal expression. The expression must evaluate to one value, and the value must be one of the following: - A callable value: a function or external command. - A string containing at least one slash, in which case it is treated like an external command with the string value as its path. Examples of commands using static resolution: ```elvish-transcript ~> put x # resolves to builtin function $put~ ▶ x ~> f~ = { put 'this is f' } ~> f # resolves to user-defined function $f~ ▶ 'this is f' ~> whoami # resolves to external command whoami elf ``` Examples of commands using a dynamic callable head: ```elvish-transcript ~> $put~ x ▶ x ~> (external whoami) elf ~> { put 'this is a lambda' } ▶ 'this is a lambda' ``` **Note**: The last command resembles a code block in C-like languages in syntax, but is quite different under the hood: it works by defining a function on the fly and calling it immediately. Examples of commands using a dynamic string head: ```elvish-transcript ~> x = /bin/whoami ~> $x elf ~> x = whoami ~> $x # dynamic strings can only used when containing slash Exception: bad value: command must be callable or string containing slash, but is string [tty 10], line 1: $x ``` The definition of barewords is relaxed when parsing the head, and includes `<`, `>`, and `*`. These are all names of numeric builtins: ```elvish-transcript ~> < 3 5 # less-than ▶ $true ~> > 3 5 # greater-than ▶ $false ~> * 3 5 # multiplication ▶ 15 ``` **Arguments** and **options** can be supplied to commands. Arguments are arbitrary words, while options have exactly the same syntax as key-value pairs in [map literals](#map). They are separated by inline whitespaces and may be intermixed: ```elvish-transcript ~> echo &sep=, a b c # &seq=, is an option; a b c are arguments a,b,c ~> echo a b &sep=, c # same, with the option mixed within arguments a,b,c ``` **Note**: Since options have the same syntax as key-value pairs in maps, `&key` is equivalent to `&key=$true`: ```elvish-transcript ~> fn f [&opt=$false]{ put $opt } ~> f &opt ▶ $true ``` **Note**: Since `&` is a [metacharacter](#metacharacters), it can be used to start an option immediately after the command name; `echo&sep=, a b` is equivalent to `echo &sep=, a b`, just less readable. This might change in future. ## Special command A **special command** form has the same syntax with an ordinary command, but how it is executed depends on the command head. See [special commands](#special-commands). ## Legacy assignment form If any argument in a command form is an unquoted equal sign (`=`), the command form is treated as an assignment form: the arguments to the left of `=`, including the head, are treated as lvalues, and the arguments to the right of `=` are treated as values to assign to those lvalues. If any lvalue refers to a variable that doesn't yet exist, it is created first. This is a legacy syntax that will be deprecated in future. Use the [`var`](#var) special command to declare variables, and the [`set`](#set) special command set the values of variables. ## Temporary assignment You can prepend any command form with **temporary assignments**, which gives variables temporarily values during the execution of that command. In the following example, `$x` and `$y` are temporarily assigned 100 and 200: ```elvish-transcript ~> x y = 1 2 ~> x=100 y=200 + $x $y ▶ 300 ~> echo $x $y 1 2 ``` In contrary to normal assignments, there should be no whitespaces around the equal sign `=`. To have multiple variables in the left-hand side, use braces: ```elvish-transcript ~> x y = 1 2 ~> fn f { put 100 200 } ~> {x,y}=(f) + $x $y ▶ 300 ``` If you use a previously undefined variable in a temporary assignment, its value will become the empty string after the command finishes. This behavior will likely change; don't rely on it. Since ordinary assignments are also command forms, they can also be prepended with temporary assignments: ```elvish-transcript ~> x=1 ~> x=100 y = (+ 133 $x) ~> put $x $y ▶ 1 ▶ 233 ``` Temporary assignments must all appear at the beginning of the command form. As soon as something that is not a temporary assignments is parsed, Elvish no longer parses temporary assignments. For instance, in `x=1 echo x=1`, the second `x=1` is not a temporary assignment, but a bareword. **Note**: Elvish's behavior differs from bash (or zsh) in one important place. In bash, temporary assignments to variables do not affect their direct appearance in the command: ```sh-transcript bash-4.4$ x=1 bash-4.4$ x=100 echo $x 1 ``` **Note**: Elvish currently supports using the syntax of temporary assignments for ordinary assignments, when they are not followed by a command form; for example, `a=x` behaves like an ordinary assignment `a = x`. This will likely go away; don't rely on it. ## IO ports A command have access to a number of **IO ports**. Each IO port is identified by a number starting from 0, and combines a traditional file object, which conveys bytes, and a **value channel**, which conveys values. Elvish starts with 3 IO ports at the top level with special significance for commands: - Port 0, known as standard input or stdin, and is used as the default input port by builtin commands. - Port 1, known as standard output or stdout, and is used as the default output port by builtin commands. - Port 2, known as standard error or stderr, is currently not special for builtin commands, but usually has special significance for external commands. Value channels are typically created by a [pipeline](#pipeline), and used to pass values between commands in the same pipeline. At the top level, they are initialized with special values: - The value channel for port 0 never produces any values when read. - The value channels for port 1 and 2 are special channels that forward the values written to them to their file counterparts. Each value is put on a separate line, with a prefix controlled by [`$value-out-indicator`](builtin.html#value-out-indicator). The default prefix is `▶` followed by a space. When running an external command, the file object from each port is used to create its file descriptor table. Value channels only work inside the Elvish process, and are not accessible to external commands. IO ports can be modified with [redirections](#redirection) or by [pipelines](#pipeline). ## Redirection A **redirection** modifies the IO ports a command operate with. There are several variants. A **file redirection** opens a file and associates it with an IO port. The syntax consists of an optional destination IO port (like `2`), a redirection operator (like `>`) and a filename (like `error.log`): - The **destination IO port** determines which IO port to modify. It can be given either as the number of the IO port, or one of `stdin`, `stdout` and `stderr`, which are equivalent to 0, 1 and 2 respectively. The destination IO port can be omitted, in which case it is inferred from the redirection operator. When the destination IO port is given, it must precede the redirection operator directly, without whitespaces in between; if there are whitespaces, otherwise Elvish will parse it as an argument instead. - The **redirection operator** determines the mode to open the file, and the destination IO port if it is not explicitly specified. - The **filename** names the file to open. Possible redirection operators and their default FDs are: - `<` for reading. The default IO port is 0 (stdin). - `>` for writing. The default IO port is 1 (stdout). - `>>` for appending. The default IO port is 1 (stdout). - `<>` for reading and writing. The default IO port is 1 (stdout). Examples: ```elvish-transcript ~> echo haha > log ~> cat log haha ~> cat < log haha ~> ls --bad-arg 2> error Exception: ls exited with 2 Traceback: [interactive], line 1: ls --bad-arg 2> error ~> cat error /bin/ls: unrecognized option '--bad-arg' Try '/bin/ls --help' for more information. ``` IO ports modified by file redirections do not support value channels. To be more exact: - A file redirection using `<` sets the value channel to one that never produces any values. - A file redirection using `>`, `>>` or `<>` sets the value channel to one that throws an exception when written to. Examples: ```elvish-transcript ~> put foo > file # will truncate file if it exists Exception: port has no value output [tty 2], line 1: put foo > file ~> echo content > file ~> only-values < file ~> # previous command produced nothing ``` Redirections can also be used for closing or duplicating IO ports. Instead of writing a filename, use `&fd` (where `fd` is a number, or any of `stdin`, `stdout` and `stderr`) for duplicating, or `&-` for closing. In this case, the redirection operator only determines the default destination FD (and is totally irrevelant if a destination IO port is specified). Examples: ```elvish-transcript ~> date >&- date: stdout: Bad file descriptor Exception: date exited with 1 [tty 3], line 1: date >&- ~> put foo >&- Exception: port has no value output [tty 37], line 1: put foo >&- ``` If you have multiple related redirections, they are applied in the order they appear. For instance: ```elvish-transcript ~> fn f { echo out; echo err >&2 } # echoes "out" on stdout, "err" on stderr ~> f >log 2>&1 # use file "log" for stdout, then use (changed) stdout for stderr ~> cat log out err ``` Redirections may appear anywhere in the command, except at the beginning, (this may be restricted in future). It's usually good style to write redirections at the end of command forms. # Special commands **Special commands** obey the same syntax rules as normal commands, but have evaluation rules that are custom to each command. Consider the following example: ```elvish-transcript ~> or ?(echo x) ?(echo y) ?(echo z) x ▶ $ok ``` In the example, the `or` command first evaluates its first argument, which has the value `$ok` (a truish value) and the side effect of outputting `x`. Due to the custom evaluation rule of `or`, the rest of the arguments are not evaluated. If `or` were a normal command, the code above is still syntactically correct. However, Elvish would then evaluate all its arguments, with the side effect of outputting `x`, `y` and `z`, before calling `or`. ## Declaring variables: `var` {#var} The `var` special command declares local variables. It takes any number of unqualified variable names (without the leading `$`). The variables will start out having value `$nil`. Examples: ```elvish-transcript ~> var a ~> put $a ▶ $nil ~> var foo bar ~> put $foo $bar ▶ $nil ▶ $nil ``` To set alternative initial values, add an unquoted `=` and the initial values. Examples: ```elvish-transcript ~> var a b = foo bar ~> put $a $b ▶ foo ▶ bar ``` Similar to [`set`](#set), at most one of variables may be prefixed with `@` to function as a rest variable. When declaring a variable that already exists, the existing variable is shadowed. The shadowed variable may still be accessed indirectly if it is referenced by a function. Example: ```elvish-transcript ~> var x = old ~> fn f { put $x } ~> var x = new ~> put $x ▶ new ~> f ▶ old ``` ## Setting the value of variables or elements: `set` {#set} The `set` special command sets the value of variables or elements. It takes any number of **lvalues** (which refer to either variables or elements), followed by an equal sign (`=`) and any number of expressions. The equal sign must appear unquoted, as a single argument. An **lvalue** is one of the following: - A variable name (without `$`). - A variable name prefixed with `@`, for packing a variable number of values into a list and assigning to the variable. This variant is called a **rest variable**. There could be at most one rest variable. **Note**: Schematically this is the reverse operation of exploding a variable when [using](#variable-use) it, which is why they share the `@` sign. - A variable name followed by one or more indices in brackets (`[]`), for assigning to an element. The number of values the expressions evaluate to and lvalues must be compatible. To be more exact: - If there is no rest variable, the number of values and lvalues must match exactly. - If there is a rest variable, the number of values should be at least the number of lvalues minus one. All the variables to set must already exist; use the [`var`](#var) special command to declare new variables. Examples: ```elvish-transcript ~> var x y z ~> set x = foo ~> put $x ▶ foo ~> x y = lorem ipsum ~> put $x $y ▶ lorem ▶ ipsum ~> set x @y z = a b ~> put $x $y $z ▶ a ▶ [] ▶ b ~> set x @y z = a b c d ~> put $x $y $z ▶ a ▶ [b c] ▶ d ~> set y[0] = foo ~> put $y ▶ [foo c] ``` If the variable name contains any character that may not appear unquoted in [variable use expressions](#variable-use), it must be quoted even if it is otherwise a valid bareword: ```elvish-transcript ~> var 'a/b' ~> set a/b = foo compilation error: lvalue must be valid literal variable names [tty 23], line 1: a/b = foo ~> set 'a/b' = foo ~> put $'a/b' ▶ foo ``` Lists and maps in Elvish are immutable. As a result, when assigning to the element of a variable that contains a list or map, Elvish does not mutate the underlying list or map. Instead, Elvish creates a new list or map with the mutation applied, and assigns it to the variable. Example: ```elvish-transcript ~> var li = [foo bar] ~> var li2 = $li ~> set li[0] = lorem ~> put $li $li2 ▶ [lorem bar] ▶ [foo bar] ``` ## Deleting variables or elements: `del` {#del} The `del` special command can be used to delete variables or map elements. Operands should be specified without a leading dollar sign, like the left-hand side of assignments. Example of deleting variable: ```elvish-transcript ~> x = 2 ~> echo $x 2 ~> del x ~> echo $x Compilation error: variable $x not found [tty], line 1: echo $x ``` If the variable name contains any character that cannot appear unquoted after `$`, it must be quoted, even if it is otherwise a valid bareword: ```elvish-transcript ~> 'a/b' = foo ~> del 'a/b' ``` Deleting a variable does not affect closures that have already captured it; it only removes the name. Example: ```elvish-transcript ~> x = value ~> fn f { put $x } ~> del x ~> f ▶ value ``` Example of deleting map element: ```elvish-transcript ~> m = [&k=v &k2=v2] ~> del m[k2] ~> put $m ▶ [&k=v] ~> l = [[&k=v &k2=v2]] ~> del l[0][k2] ~> put $l ▶ [[&k=v]] ``` ## Logics: `and`, `or`, `coalesce` {#and-or-coalesce} The `and` special command outputs the first [booleanly false](#boolean) value the arguments evaluate to, or `$true` when given no value. Examples: ```elvish-transcript ~> and $true $false ▶ $false ~> and a b c ▶ c ~> and a $false ▶ $false ``` The `or` special command outputs the first [booleanly true](#boolean) value the arguments evaluate to, or `$false` when given no value. Examples: ```elvish-transcript ~> or $true $false ▶ $true ~> or a b c ▶ a ~> or $false a b ▶ a ``` The `coalesce` special command outputs the first non-[nil](#nil) value the arguments evaluate to, or `$nil` when given no value. Examples: ```elvish-transcript ~> coalesce $nil a b ▶ a ~> coalesce $nil $nil ▶ $nil ~> coalesce $nil $nil a ▶ a ~> coalesce a b ▶ a ``` All three commands use short-circuit evaluation, and stop evaluating arguments as soon as it sees a value satisfying the termination condition. For example, none of the following throws an exception: ```elvish-transcript ~> and $false (fail foo) ▶ $false ~> or $true (fail foo) ▶ $true ~> coalesce a (fail foo) ▶ a ``` ## Condition: `if` {#if} **TODO**: Document the syntax notation, and add more examples. Syntax: ```elvish-transcript if { } elif { } else { } ``` The `if` special command goes through the conditions one by one: as soon as one evaluates to a booleanly true value, its corresponding body is executed. If none of conditions are booleanly true and an else body is supplied, it is executed. The condition part is an expression, not a command like in other shells. Example: ```elvish fn tell-language [fname]{ if (has-suffix $fname .go) { echo $fname" is a Go file!" } elif (has-suffix $fname .c) { echo $fname" is a C file!" } else { echo $fname" is a mysterious file!" } } ``` The condition part must be syntactically a single expression, but it can evaluate to multiple values, in which case they are and'ed: ```elvish if (put $true $false) { echo "will not be executed" } ``` If the expression evaluates to 0 values, it is considered true, consistent with how `and` works. Tip: a combination of `if` and `?()` gives you a semantics close to other shells: ```elvish if ?(test -d .git) { # do something } ``` However, for Elvish's builtin predicates that output values instead of throw exceptions, the output capture construct `()` should be used. ## Conditional loop: `while` {#while} Syntax: ```elvish-transcript while { } else { } ``` Execute the body as long as the condition evaluates to a booleanly true value. The else body, if present, is executed if the body has never been executed (i.e. the condition evaluates to a booleanly false value in the very beginning). ## Iterative loop: `for` {#for} Syntax: ```elvish-transcript for { } else { } ``` Iterate the container (e.g. a list). In each iteration, assign the variable to an element of the container and execute the body. The else body, if present, is executed if the body has never been executed (i.e. the iteration value has no elements). ## Exception control: `try` {#try} (If you just want to capture the exception, you can use the more concise exception capture construct `?()` instead.) Syntax: ```elvish-transcript try { } except except-varname { } else { } finally { } ``` Only `try` and `try-block` are required. This control structure behaves as follows: 1. The `try-block` is always executed first. 2. If `except` is present and an exception occurs in `try-block`, it is caught and stored in `except-varname`, and `except-block` is then executed. Example: ```elvish-transcript ~> try { fail bad } except e { put $e } ▶ ?(fail bad) ``` Note that if `except` is not present, exceptions thrown from `try` are not caught: for instance, `try { fail bad }` throws `bad`; it is equivalent to a plain `fail bad`. Note that the word after `except` names a variable, not a matching condition. Exception matching is not supported yet. For instance, you may want to only match exceptions that were created with `fail bad` with `except bad`, but in fact this creates a variable `$bad` that contains whatever exception was thrown. 3. If no exception occurs and `else` is present, `else-block` is executed. Example: ```elvish-transcript ~> try { nop } else { echo well } well ``` 4. If `finally-block` is present, it is executed. Examples: ```elvish-transcript ~> try { fail bad } finally { echo final } final Exception: bad Traceback: [tty], line 1: try { fail bad } finally { echo final } ~> try { echo good } finally { echo final } good final ``` 5. If the exception was not caught (i.e. `except` is not present), it is rethrown. Exceptions thrown in blocks other than `try-block` are not caught. If an exception was thrown and either `except-block` or `finally-block` throws another exception, the original exception is lost. Examples: ```elvish-transcript ~> try { fail bad } except e { fail worse } Exception: worse Traceback: [tty], line 1: try { fail bad } except e { fail worse } ~> try { fail bad } except e { fail worse } finally { fail worst } Exception: worst Traceback: [tty], line 1: try { fail bad } except e { fail worse } finally { fail worst } ``` ## Function definition: `fn` {#fn} Syntax: ```elvish-transcript fn ``` Define a function with a given name. The function behaves in the same way to the lambda used to define it, except that it "captures" `return`. In other words, `return` will fall through lambdas not defined with `fn`, and continues until it exits a function defined with `fn`: ```elvish-transcript ~> fn f { { echo a; return } echo b # will not execute } ~> f a ~> { f echo c # executed, because f "captures" the return } a c ``` **TODO**: Find a better way to describe this. Hopefully the example is illustrative enough, though. The lambda may refer to the function being defined. This makes it easy to define recursive functions: ```elvish-transcript ~> fn f [n]{ if (== $n 0) { put 1 } else { * $n (f (- $n 1)) } } ~> f 3 ▶ (float64 6) ``` Under the hood, `fn` defines a variable with the given name plus `~` (see [variable suffix](#variable-suffix)). Example: ```elvish-transcript ~> fn f { echo hello from f } ~> var v = $f~ ~> $v hello from f ``` ## Language pragmas: `pragma` {#pragma} The `pragma` special command can be used to set **pragmas** that affect the behavior of the Elvish language. The syntax looks like: ``` pragma = ``` The name must appear literally. The value must also appear literally, unless otherwise specified. Pragmas apply from the point it appears, to the end of the lexical scope it appears in, including subscopes. The following pragmas are available: - The `unknown-command` pragma affects the resolution of command heads, and can take one of two values, `external` (the default) and `disallow`. See [ordinary command](#ordinary-command) for details. **Note**: `pragma unknown-command = disallow` enables a style where uses of external commands must be explicitly via the `e:` namespace. You can also explicitly declare a set of external commands to use directly, like the following: ```elvish pragma unknown-command = disallow var ls = $e:ls~ var cat = $e:cat~ # ls and cat can be used directly; # other external commands must be prefixed with e: ``` # Pipeline A **pipeline** is formed by joining one or more commands together with the pipe sign (`|`). For each pair of adjacent commands `a | b`, the standard output of the left-hand command `a` (IO port 1) is connected to the standard input (IO port 0) of the right-hand command `b`. Both the file and the value channel are connected, even if one of them is not used. Elvish may have internal buffering for both the file and the value channel, so `a` may be able to write bytes or values even if `b` is not reading them. The exact buffer size is not specified. Command redirections are applied before the connection happens. For instance, the following writes `foo` to `a.txt` instead of the output: ```elvish-transcript ~> echo foo > a.txt | cat ~> cat a.txt foo ``` A pipeline runs all of its command in parallel, and terminates when all of the commands have terminated. ## Pipeline exception If one or more command in a pipeline throws an exception, the other commands will continue to execute as normal. After all commands finish execution, an exception is thrown, the value of which depends on the number of commands that have thrown an exception: - If only one command has thrown an exception, that exception is rethrown. - If more than one commands have thrown exceptions, a "composite exception", containing information all exceptions involved, is thrown. If a command threw an exception because it tried to write output when the next command has terminated, that exception is suppressed when it is propagated to the pipeline. For example, the `put` command throws an exception when trying to write to a closed pipe, so the following loop will terminate with an exception: ```elvish-transcript ~> while $true { put foo } > &- Exception: port has no value output [tty 9], line 1: while $true { put foo } > &- ``` However, if it appears in a pipeline before `nop`, the entire pipeline will not throw an exception: ```elvish-transcript ~> while $true { put foo } | nop ~> # no exception thrown from previous line ``` Internally, the `put foo` command still threw an exception, but since that exception was trying to write to output when `nop` already terminated, that exception was suppressed by the pipeline. This can be more clearly observed with the following code: ```elvish-transcript ~> var r = $false ~> { while $true { put foo }; set r = $true } | nop ~> put $r ▶ $false ``` The same mechanism works for builtin commands that write to the byte output: ```elvish-transcript ~> var r = $false ~> { while $true { echo foo }; set r = $true } | nop ~> put $r ▶ $false ``` On UNIX, if an external command was terminated by SIGPIPE, and Elvish detected that it terminated after the next command in the pipeline, such exceptions will also be suppressed by the pipeline. For example, the following pipeline does not throw an exception, despite the `yes` command being killed by SIGPIPE: ```elvish-transcript ~> yes | head -n1 y ``` ## Background pipeline Adding an ampersand `&` to the end of a pipeline will cause it to be executed in the background. In this case, the rest of the code chunk will continue to execute without waiting for the pipeline to finish. Exceptions thrown from the background pipeline do not affect the code chunk that contains it. When a background pipeline finishes, a message is printed to the terminal if the shell is interactive. # Code Chunk A **code chunk** is formed by joining zero or more pipelines together, separating them with either newlines or semicolons. Pipelines in a code chunk are executed in sequence. If any pipeline throws an exception, the execution of the whole code chunk stops, propagating that exception. # Exception and Flow Commands Exceptions have similar semantics to those in Python or Java. They can be thrown with the [fail](builtin.html#fail) command and caught with either exception capture `?()` or the `try` special command. If an external command exits with a non-zero status, Elvish treats that as an exception. Flow commands -- `break`, `continue` and `return` -- are ordinary builtin commands that raise special "flow control" exceptions. The `for`, `while`, and `peach` commands capture `break` and `continue`, while `fn` modifies its closure to capture `return`. One interesting implication is that since flow commands are just ordinary commands you can build functions on top of them. For instance, this function `break`s randomly: ```elvish fn random-break { if eq (randint 2) 0 { break } } ``` The function `random-break` can then be used in for-loops and while-loops. Note that the `return` flow control exception is only captured by functions defined with `fn`. It falls through ordinary lambdas: ```elvish fn f { { # returns f, falling through the innermost lambda return } } ``` # Namespaces and Modules Like other modern programming languages, but unlike traditional shells, Elvish has a **namespace** mechanism for preventing name collisions. ## Syntax Prepend `namespace:` to command names and variable names to specify the namespace. The following code ```elvish e:echo $E:PATH ``` uses the `echo` command from the `e:` namespace and the `PATH` variable from the `E:` namespace. The colon is considered part of the namespace name. Namespaces may be nested; for example, calling `edit:location:start` first finds the `edit:` namespace, and then the `location:` namespace inside it, and then call the `start` function within the nested namespace. ## Special namespaces The following namespaces have special meanings to the language: - `local:` and `up:` refer to lexical scopes, and have been documented above. - `e:` refers to externals. For instance, `e:ls` refers to the external command `ls`. Most of the time you can rely on static resolution rules of [ordinary commands](#ordinary-command) and do not need to use this explicitly, unless a function defined by you (or an Elvish builtin) shadows an external command. - `E:` refers to environment variables. For instance, `$E:USER` is the environment variable `USER`. This **is** always needed, because unlike command resolution, variable resolution does not fall back onto environment variables. - `builtin:` refers to builtin functions and variables. You don't need to use this explicitly unless you have defined names that shadows builtin counterparts. ## Modules Apart from the special namespaces, the most common usage of namespaces is to reference modules, reusable pieces of code that are either shipped with Elvish itself or defined by the user. ### Importing modules with `use` Modules are imported using the `use` special command. It requires a **module spec** and allows a namespace alias: ```elvish use $spec $alias? ``` The module spec and the alias must both be a simple [string literal](#string). [Compound strings](#compounding) such as `'a'/b` are not allowed. The module spec specifies which module to import. The alias, if given, specifies the namespace to import the module under. By default, the namespace is derived from the module spec by taking the part after the last slash. Module specs fall into three categories that are resolved in the following order: 1. **Relative**: These are [relative](#relative-imports) to the file containing the `use` command. 1. **User defined**: These match a [user defined module](#user-defined-modules) in a [module search directory](command.html#module-search-directories). 1. **Pre-defined**: These match the name of a [pre-defined module](#pre-defined-modules), such as `math` or `str`. If a module spec doesn't match any of the above a "no such module" [exception](#exception) is raised. Examples: ```elvish use str # imports the "str" module as "str:" use a/b/c # imports the "a/b/c" module as "c:" use a/b/c foo # imports the "a/b/c" module as "foo:" ``` ### Pre-defined modules Elvish's standard library provides the following pre-defined modules that can be imported by the `use` command: - [edit](edit.html) is only available in interactive mode. As a special case it does not need importing via `use`, but this may change in the future. - [epm](epm.html) - [math](math.html) - [path](path.html) - [platform](platform.html) - [re](re.html) - [readline-binding](readline-binding.html) - [store](store.html) - [str](str.html) - [unix](unix.html) is only available on UNIX-like platforms (see [`$platform:is-unix`](platform.html#platform:is-unix)) ### User-defined modules You can define your own modules in Elvish by putting them under one of the [module search directories](command.html#module-search-directories) and giving them a `.elv` extension (but see [relative imports](#relative-imports) for an alternative). For instance, to define a module named `a`, you can put the following in `~/.config/elvish/lib/a.elv` (on Windows, replace `~/.config` with `~\AppData\Roaming`): ```elvish-transcript ~> cat ~/.config/elvish/lib/a.elv echo "mod a loading" fn f { echo "f from mod a" } ``` This module can now be imported by `use a`: ```elvish-transcript ~> use a mod a loading ~> a:f f from mod a ``` Similarly, a module defined in `~/.config/elvish/lib/x/y/z.elv` can be imported by `use x/y/z`: ```elvish-transcript ~> cat .config/elvish/lib/x/y/z.elv fn f { echo "f from x/y/z" } ~> use x/y/z ~> z:f f from x/y/z ``` In general, a module defined in namespace will be the same as the file name (without the `.elv` extension). There is experimental support for importing modules written in Go. See the [project repository](https://github.com/elves/elvish) for details. ### Circular dependencies Circular dependencies are allowed but has an important restriction. If a module `a` contains `use b` and module `b` contains `use a`, the top-level statements in module `b` will only be able to access variables that are defined before the `use b` in module `a`; other variables will be `$nil`. On the other hand, functions in module `b` will have access to bindings in module `a` after it is fully evaluated. Examples: ```elvish-transcript ~> cat a.elv var before = before use ./b var after = after ~> cat b.elv use ./a put $a:before $a:after fn f { put $a:before $a:after } ~> use ./a ▶ before ▶ $nil ~> use ./b ~> b:f ▶ before ▶ after ``` Note that this behavior can be different depending on whether the REPL imports `a` or `b` first. In the previous example, if the REPL imports `b` first, it will have access to all the variables in `a`: ```elvish-transcript ~> use ./b ▶ before ▶ after ``` **Note**: Elvish caches imported modules. If you are trying this locally, run a fresh Elvish instance with `exec` first. When you do need to have circular dependencies, it is best to avoid using variables from the modules in top-level statements, and only use them in functions. ### Relative imports The module spec may begin with `./` or `../` to introduce a **relative import**. When `use` is invoked from a file this will import the file relative to the location of the file. When `use` is invoked from an interactive prompt, this will import the file relative to the current working directory. ### Scoping of imports Namespace imports are lexically scoped. For instance, if you `use` a module within an inner scope, it is not available outside that scope: ```elvish { use some-mod some-mod:some-func } some-mod:some-func # not valid ``` The imported modules themselves are also evaluated in a separate scope. That means that functions and variables defined in the module does not pollute the default namespace, and vice versa. For instance, if you define `ls` as a wrapper function in `rc.elv`: ```elvish fn ls [@a]{ e:ls --color=auto $@a } ``` That definition is not visible in module files: `ls` will still refer to the external command `ls`, unless you shadow it in the very same module. ### Re-importing Modules are cached after one import. Subsequent imports do not re-execute the module; they only serve the bring it into the current scope. Moreover, the cache is keyed by the path of the module, not the name under which it is imported. For instance, if you have the following in `~/.config/elvish/lib/a/b.elv`: ```elvish echo importing ``` The following code only prints one `importing`: ```elvish { use a/b } use a/b # only brings mod into the lexical scope ``` As does the following: ```elvish use a/b use a/b alias ``` elvish-0.17.0/website/ref/math.md000066400000000000000000000005331415471104000165600ustar00rootroot00000000000000 @module math # Introduction The `math:` module provides mathematical functions and constants. Function usages are given in the same format as in the reference doc for the [builtin module](builtin.html). In particular, all the commands in this module conform to the convention of [numeric commands](builtin.html#numeric-commands). elvish-0.17.0/website/ref/path.md000066400000000000000000000003571415471104000165670ustar00rootroot00000000000000 @module path # Introduction The `path:` module provides functions for manipulating and testing filesystem paths. Function usages are given in the same format as in the reference doc for the [builtin module](builtin.html). elvish-0.17.0/website/ref/platform.md000066400000000000000000000001731415471104000174530ustar00rootroot00000000000000 @module platform # Introduction The `platform:` module provides access to the platform's identifying data. elvish-0.17.0/website/ref/prelude.md000066400000000000000000000003341415471104000172660ustar00rootroot00000000000000The latest version of documents in this section are also available as a [docset](https://elv.sh/ref/docset/Elvish.tgz). You can also [subscribe](dash-feed://https%3A%2F%2Felv.sh%2Fref%2Fdocset%2FElvish.xml) to the feed. elvish-0.17.0/website/ref/re.md000066400000000000000000000012351415471104000162350ustar00rootroot00000000000000 @module re # Introduction The `re:` module wraps Go's `regexp` package. See the Go's doc for supported [regular expression syntax](https://godoc.org/regexp/syntax). Function usages notations follow the same convention as the [builtin module doc](builtin.html). The following options are supported by multiple functions in this module: - `&posix=$false`: Use POSIX ERE syntax. See also [doc](http://godoc.org/regexp#CompilePOSIX) in Go package. - `&longest=$false`: Prefer leftmost-longest match. See also [doc](http://godoc.org/regexp#Regexp.Longest) in Go package. - `&max=-1`: If non-negative, limits the maximum number of results. elvish-0.17.0/website/ref/readline-binding.md000066400000000000000000000011561415471104000210240ustar00rootroot00000000000000 @module readline-binding # Introduction The `readline-binding` module provides readline-like key bindings, such as binding Ctrl-A to move the cursor to the start of the line. To use, put the following in `~/.elvish/rc.elv`: ```elvish use readline-binding ``` Note that this will override some of the standard bindings. For example, Ctrl-L will be bound to a function that clears the terminal screen rather than start location mode. See the [source code](https://github.com/elves/elvish/blob/master/pkg/mods/bundled/readline-binding.elv.go) for details. elvish-0.17.0/website/ref/store.md000066400000000000000000000002421415471104000167600ustar00rootroot00000000000000 @module store # Introduction The `store:` module provides access to Elvish's persistent data store. It is only available in interactive mode now. elvish-0.17.0/website/ref/str.md000066400000000000000000000003231415471104000164340ustar00rootroot00000000000000 @module str # Introduction The `str:` module provides string manipulation functions. Function usages are given in the same format as in the reference doc for the [builtin module](builtin.html). elvish-0.17.0/website/ref/unix.md000066400000000000000000000006201415471104000166070ustar00rootroot00000000000000 @module unix # Introduction The `unix:` module provides access to features that only make sense on UNIX-like operating systems, such as Linux, FreeBSD, and macOS. On non-UNIX operating systems, such as MS Windows, this namespace does not exist and `use unix` will fail. Use the [`$platform:is-unix`](platform.html#platform:is-unix) variable to determine if this namespace is usable. elvish-0.17.0/website/reset.css000066400000000000000000000027771415471104000164010ustar00rootroot00000000000000/* html5doctor.com Reset Stylesheet v1.6.1 Last Updated: 2010-09-17 Author: Richard Clark - http: //richclarkdesign.com Twitter: @rich_clark */ html, body, div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, abbr, address, cite, code, del, dfn, em, img, ins, kbd, q, samp, small, strong, sub, sup, var, b, i, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, figcaption, figure, footer, header, hgroup, menu, nav, section, summary, time, mark, audio, video { margin: 0; padding: 0; border: 0; outline: 0; font-size: 100%; vertical-align: baseline; background: transparent; } body { line-height: 1; } article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { display: block; } nav ul { list-style: none; } blockquote, q { quotes: none; } blockquote:before, blockquote:after, q:before, q:after { content: ''; content: none; } a { margin: 0; padding: 0; font-size: 100%; vertical-align: baseline; background: transparent; } ins { background-color: #ff9; color: #000; text-decoration: none; } mark { background-color: #ff9; color: #000; font-style: italic; font-weight: bold; } del { text-decoration: line-through; } abbr[title], dfn[title] { border-bottom: 1px dotted; cursor: help; } table { border-collapse: collapse; border-spacing: 0; } hr { display: block; height: 1px; border: 0; border-top: 1px solid #cccccc; margin: 1em 0; padding: 0; } input, select { vertical-align: middle; } elvish-0.17.0/website/style.css000066400000000000000000000165341415471104000164130ustar00rootroot00000000000000/** * Global styling. **/ html { /* Prevent font scaling in landscape while allowing user zoom */ -webkit-text-size-adjust: 100%; } body { font-family: "Source Serif", Georgia, serif; font-size: 17px; line-height: 1.4em; } body.has-js .no-js, body.no-js .has-js { display: none !important; } img { max-width: 100%; } a { text-decoration: none; color: #0645ad; } /** * Top-level elements. * * There are two main elements: #navbar and #content. Both have a maximum * width, and is centered when the viewport is wider than that. * * #navbar is wrapped by #navbar-container, a black stripe that always span * the entire viewport. **/ #navbar-container { width: 100%; color: white; background-color: #1a1a1a; padding: 12px 0; } #content, #navbar { max-width: 1024px; margin: 0 auto; padding: 0 4%; } /** * Elements in the navbar. * * The navbar is made up of two elements, #blog-title and ul#nav-list. The * latter contains li.nav-item which contains an a.nav-link. */ #blog-title, #nav-list { display: inline-block; /* Add spacing between lines when the navbar cannot fit in one line. */ line-height: 1.4em; } #blog-title { font-size: 1.2em; margin-right: 0.6em; /* Move the title upward 1px so that it looks more aligned with the * category list. */ position: relative; top: 1px; } #blog-title a { color: #5b5; } .nav-item { list-style: none; display: inline-block; } .nav-link { color: white; } .nav-link > code { padding: 0px 0.5em; } .nav-link:hover { background-color: #444; } .nav-link.current { color: black; background-color: white; } .nav-item + .nav-item::before { content: "|"; } /** * Article header. **/ .timestamp { margin-bottom: 0.6em; } .article-title { padding: 16px 0; border-bottom: solid 1px #667; } /* Extra level needed to be more specific than .article h1 */ .article .article-title h1 { font-size: 1.5em; margin: 0; padding: 0; border: none; } /** * Article content. **/ .article-content { padding-top: 32px; } .article p, .article ul, .article pre { margin-bottom: 16px; } .article li { margin: 0.5em 0; } .article li > p { margin: 1em 0; } /* Block code. */ .article pre { padding: 1em; overflow: auto; } /* Inline code. */ .article p code { padding: 0.1em 0; } .article p code::before, .article p code::after { letter-spacing: -0.2em; content: "\00a0"; } code, pre { font-family: "Fira Mono", Menlo, "Roboto Mono", Consolas, monospace; } .article code, .article pre { background-color: #f0f0f0; border-radius: 3px; } /* This doesn't have p, so that it also applies to ttyshots. */ .article code { font-size: 85%; } /* We only use h1 to h3. */ .article h1, .article h2, .article h3 { line-height: 1.25; } .article h1, .article h2, .article h3 { margin-top: 24px; margin-bottom: 20px; font-weight: bold; } .article h1 { font-size: 1.3em; padding-bottom: 0.4em; border-bottom: 1px solid #aaa; } .article h2 { font-size: 1.2em; } .article h3 { font-style: italic; } .article ul, .article ol { margin-left: 1em; } /** * Table of content. */ .toc { background-color: #f0f0f0; padding: 1em; margin: 0 16px 16px 0; border-radius: 6px; line-height: 1; } /* The first

clears the TOC */ .article-content h1 { clear: both; } #toc-list { margin-left: -0.6em; } @media (min-width: 600px) and (max-width: 899px) { #toc-list { column-count: 2; } } @media (min-width: 900px) { #toc-list { column-count: 3; } } #toc-list li { list-style: none; /* Keep first-level ToC within one column */ break-inside: avoid; } /** * Category content. **/ .category-prelude { padding-top: 4%; margin-bottom: -20px; } .article-list { padding: 4% 0; } .article-list > li { list-style: square inside; padding: 3px; } .article-list > li:hover { background-color: #c0c0c0; } .article-link, .article-link:visited { color: black; display: inline; line-height: 1.4em; border-bottom: 1px solid black; } .article-timestamp { float: right; display: inline-block; margin-left: 1em; } /** * Layout utilities. **/ .clear { clear: both; } .no-display { display: none !important; } /** * Miscellous elements. **/ hr { clear: both; border-color: #aaa; text-align: center; } hr:after { content: "❧"; text-shadow: 0px 0px 2px #667; display: inline-block; position: relative; top: -0.5em; padding: 0 0.25em; font-size: 1.1em; color: black; background-color: white; } .key { display: inline-block; border: 1px solid black; border-radius: 3px; padding: 0 2px; margin: 1px; font-size: 85%; font-family: "Lucida Grande", Arial, sans-serif; } /** Section numbers generated by pandoc */ .header-section-number:after, .toc-section-number:after { content: "."; } /** * TTY shots. */ pre.ttyshot { font-size: 12pt; line-height: 1 !important; border: 1px solid black; display: inline-block; margin-bottom: 0 !important; } pre.ttyshot, pre.ttyshot code { background-color: white; } @media screen and (max-width: 600px) { pre.ttyshot { font-size: 2.6vw; } } /* SGR classes, used in ttyshots. */ .sgr-1 { font-weight: bold; } .sgr-4 { text-decoration: underline; } .sgr-7 { color: white; background-color: black; } .sgr-31 { color: darkred; /* red in tty */ } .sgr-41 { background-color: darkred; /* red in tty */ } .sgr-32 { color: green; /* green in tty */ } .sgr-42, .sgr-7.sgr-32 { background-color: green; /* green in tty */ } .sgr-33 { color: goldenrod; /* yellow in tty */ } .sgr-43, .sgr-7.sgr-33 { background-color: goldenrod; /* yellow in tty */ } .sgr-34 { color: blue; } .sgr-44, .sgr-7.sgr-34 { color: white; /* Hacky hacky, just to make the nav ttyshot work */ background-color: blue; } .sgr-35 { color: darkorchid; /* magenta in tty */ } .sgr-45, .sgr-7.sgr-35 { background-color: darkorchid; /* magenta in tty */ } .sgr-36 { color: darkcyan; /* cyan in tty */ } .sgr-46, .sgr-7.sgr-36 { background-color: darkcyan; /* cyan in tty */ } .sgr-37 { color: lightgray; } .sgr-47, .sgr-7.sgr-37 { background-color: gray; } /** Header anchors. */ .anchor { opacity: 0; font-size: 90%; color: inherit; padding-left: 0.15em; } *:hover > .anchor { opacity: 1; } /** * Dark theme. */ .dark { color: #eee; background: black; } .dark a { color: #6da2fa; } .dark a:visited { color: #7e72ff; } .dark .article-link, .dark .article-link:visited { color: #eee; border-color: white; } .dark .article-list > li:hover { background-color: #333; } .dark .article code, .dark .article pre { background-color: #181818; } .dark .toc { background-color: #181818; } .dark hr { border-color: #eee; } .dark hr:after { color: #eee; background-color: black; } .dark pre.ttyshot, .dark pre.ttyshot code { background: black; } .dark .sgr-7 { color: black; background-color: #eee; } table { border-collapse: collapse; width: 100%; margin-bottom: 16px; } td, th { border: 1px solid #aaa; text-align: left; padding: 0.4em; } .dark td, .dark th { border-color: #444; } elvish-0.17.0/website/template.html000066400000000000000000000140661415471104000172400ustar00rootroot00000000000000 {{ if is "homepage" -}} {{- end }} {{ if is "homepage" -}} {{- else if is "category" -}} {{- else -}} {{- end }} {{ if is "homepage" -}} {{ .BlogTitle }} {{- else if is "category" -}} {{ index .CategoryMap .Category }} {{- else -}} {{ .Title }} - {{ .BlogTitle }} {{- end }} {{ $docsetMode := eq (getEnv "ELVISH_DOCSET_MODE") "1" -}} {{ if not $docsetMode }} {{ end }} {{/* The reference to "content" is a free one and has to be fixed elsewhere. The *-content templates defined below are intended to be used for this. For instance, by adding the following code, this whole template file will function as the template for articles: {{ define "content" }} {{ template "article-content" . }} {{ end }} This snippet can be generated by contentIs("article"). */}} {{ template "content" . }} {{ define "article-content" }}
{{ if not .IsHomepage }}
{{ .Timestamp }}

{{ .Title }}

{{ end }}
{{ .Content }}
{{ end }} {{ define "category-content" }} {{ $category := .Category }}
{{ if ne .Prelude "" }}
{{ .Prelude }}
{{ end }}
{{ end }} elvish-0.17.0/website/tools/000077500000000000000000000000001415471104000156705ustar00rootroot00000000000000elvish-0.17.0/website/tools/check-rellinks.py000066400000000000000000000040041415471104000211360ustar00rootroot00000000000000import dataclasses import glob import os import os.path import sys import urllib.parse import bs4 @dataclasses.dataclass class Link: href: str parsed: urllib.parse.ParseResult def main(args): if len(args) != 2: print('Usage: check-rellinks dir') sys.exit(1) os.chdir(args[1]) filenames = glob.glob('**/*.html', recursive=True) targets = {} rellinks = {} for filename in filenames: with open(filename) as f: soup = bs4.BeautifulSoup(f, 'html.parser') links = [Link(href=e['href'], parsed=urllib.parse.urlparse(e['href'])) for e in soup.find_all('a', href=True)] rellinks[filename] = [link for link in links if link.parsed.scheme == ''] targets[filename] = [e['id'] for e in soup.find_all(id=True)] def check(path, fragment): if path.endswith('.atom') and fragment == '': return True return path in targets and (fragment == '' or fragment in targets[path]) has_broken = False for filename in rellinks: if filename.endswith('-release-notes.html'): continue dirname = os.path.dirname(filename) broken_links = [] for link in rellinks[filename]: path = link.parsed.path if path == '': path = filename else: if os.path.splitext(path)[1] == '': path += '/index.html' if path.startswith('/'): path = path.lstrip('/') else: path = os.path.normpath(os.path.join(dirname, path)) if not check(path, link.parsed.fragment): broken_links.append(link.href) if broken_links: if not has_broken: print('Found broken links:') has_broken = True print(filename) for link in broken_links: print(f' {link}') if has_broken: sys.exit(1) if __name__ == '__main__': main(sys.argv) elvish-0.17.0/website/tools/docset-data/000077500000000000000000000000001415471104000200605ustar00rootroot00000000000000elvish-0.17.0/website/tools/docset-data/Info.plist000066400000000000000000000006711415471104000220340ustar00rootroot00000000000000 CFBundleIdentifier elvish CFBundleName Elvish DocSetPlatformFamily elvish isDashDocset DashDocSetFamily dashtoc elvish-0.17.0/website/tools/md-to-html000077500000000000000000000007221415471104000176010ustar00rootroot00000000000000#!/bin/sh in=$1 out=$2 opts= has() { head -n1 $in | grep "$@" >/dev/null } has toc && { opts="$opts --toc --template=toc-and-body" } has number-sections && { opts="$opts --number-sections" } mydir=$(dirname "$0") $mydir/macros.bin -repo $mydir/../.. -elvdoc $mydir/elvdoc.bin < $1 | $mydir/highlight.bin | pandoc -f gfm+smart+attributes --data-dir=$mydir/pandoc --lua-filter=$mydir/pandoc/header-anchors.lua --metadata title=${1%.md} -o $2 $opts elvish-0.17.0/website/tools/mkdocset000077500000000000000000000013501415471104000174260ustar00rootroot00000000000000#!/bin/sh # Generate docset from reference docs. # # Docset is a format for packaging docs for offline consumption: # https://kapeli.com/docsets # # External dependencies: # # - python3 # - sqlite3 if test $# != 2; then echo "Usage: mkdocset.elv $website $docset" exit 1 fi bindir=$(dirname "$0") website=$1 docset=$2 mkdir -p $docset/Contents/Resources/Documents cp $bindir/../favicons/favicon-16x16.png $docset/icon.png cp $bindir/../favicons/favicon-32x32.png $docset/icon@2x.png cp $bindir/docset-data/Info.plist $docset/Contents cp $website/ref/*.html $docset/Contents/Resources/Documents rm $docset/Contents/Resources/Documents/index.html python3 $bindir/mkdsidx.py $website/ref | sqlite3 $docset/Contents/Resources/docSet.dsidx elvish-0.17.0/website/tools/mkdsidx.py000066400000000000000000000017411415471104000177100ustar00rootroot00000000000000import glob import os import sys import urllib.parse import bs4 PRELUDE = """ DROP TABLE IF EXISTS searchIndex; CREATE TABLE searchIndex(id INTEGER PRIMARY KEY, name TEXT, type TEXT, path TEXT); CREATE UNIQUE INDEX anchor ON searchIndex (name, type, path); """.strip() def main(args): if len(args) != 2: print('Usage: dsindex dir') sys.exit(1) os.chdir(args[1]) print(PRELUDE) for filename in glob.glob('*.html'): with open(filename) as f: soup = bs4.BeautifulSoup(f, 'html.parser') anchors = soup.find_all('a', class_='dashAnchor') for anchor in anchors: name = anchor['name'] entry_type, symbol = name.split('/')[-2:] symbol = urllib.parse.unquote(symbol) print( 'INSERT OR IGNORE INTO searchIndex(name, type, path) VALUES ' ' ("%s", "%s", "%s#%s");' % (symbol, entry_type, filename, name)) if __name__ == '__main__': main(sys.argv) elvish-0.17.0/website/tools/pandoc/000077500000000000000000000000001415471104000171345ustar00rootroot00000000000000elvish-0.17.0/website/tools/pandoc/header-anchors.lua000066400000000000000000000004351415471104000225240ustar00rootroot00000000000000function Header(el) local id = el.identifier if id == '' then return el end local link = pandoc.Link('', '#'..id, '', {['class'] = 'anchor icon-link', ['aria-hidden'] = 'true'}) el.content:insert(link) el.attributes['onclick'] = '' return el end elvish-0.17.0/website/tools/pandoc/templates/000077500000000000000000000000001415471104000211325ustar00rootroot00000000000000elvish-0.17.0/website/tools/pandoc/templates/toc-and-body.html000066400000000000000000000014161415471104000243020ustar00rootroot00000000000000

Table of Content:

$toc$
$body$ elvish-0.17.0/website/tools/ref-deps000077500000000000000000000004321415471104000173220ustar00rootroot00000000000000#!/bin/sh # Outputs the Go files a reference doc may depend on. # # Must be run from the website directory. cat ${1%.html}.md | awk '/^@module/{ if (NF == 3) { print $3 } else { print "pkg/mods/" $2 } }' | sed 's/-//g' | while read dir; do echo ../$dir ../$dir/* done elvish-0.17.0/website/ttyshot/000077500000000000000000000000001415471104000162465ustar00rootroot00000000000000elvish-0.17.0/website/ttyshot/README.md000066400000000000000000000013041415471104000175230ustar00rootroot00000000000000This directory contains "ttyshots" -- they are like screenshots, but taken on terminals. They are taken with Elvish's undocumented `edit:-dump-buf` function. To take one, use the following procedure: 1. Modify `edit:rprompt` to pretend that the username is `elf` and the hostname is `host`: ```elvish edit:rprompt = (constantly (styled 'elf@host' inverse)) ``` 2. Add a keybinding for taking ttyshots: ```elvish edit:insert:binding[Alt-x] = { edit:-dump-buf > ~/ttyshot.html } ``` 3. Make sure that the terminal width is 58, to be consistent with existing ttyshots. 4. Put Elvish in the state you want, and press Alt-X. The ttyshot is saved at `~/ttyshot.html`. elvish-0.17.0/website/ttyshot/completion-mode.html000066400000000000000000000007641415471104000222360ustar00rootroot00000000000000~/go/src/github.com/elves/elvish> vim CONTRIBUTING.md COMPLETING argument CONTRIBUTING.md LICENSE NEXT-RELEASE.md cmd/ go.su Dockerfile Makefile README.md go.mod main. ━━━━━━━━━━━━━ elvish-0.17.0/website/ttyshot/control-structures.html000066400000000000000000000033261415471104000230410ustar00rootroot00000000000000~> if $true { echo good } else { echo bad } good ~> for x [lorem ipsum] { echo $x.pdf } lorem.pdf ipsum.pdf ~> try { fail 'bad error' } except e { echo error $e } else { echo ok } error ?(fail 'bad error') elvish-0.17.0/website/ttyshot/fundamentals/000077500000000000000000000000001415471104000207275ustar00rootroot00000000000000elvish-0.17.0/website/ttyshot/fundamentals/history-1.html000066400000000000000000000003431415471104000234540ustar00rootroot00000000000000~> randint 1 7 elf@host HISTORY #59321 elvish-0.17.0/website/ttyshot/fundamentals/history-2.html000066400000000000000000000003771415471104000234640ustar00rootroot00000000000000~> randint 1 7 elf@host HISTORY #59321 elvish-0.17.0/website/ttyshot/histlist-mode.html000066400000000000000000000025071415471104000217250ustar00rootroot00000000000000~> xiaq@xiaqsmbp HISTORY 13345 make tools/ttyshot 13346 make 13347 ./assets/ 13348 ls 13349 ls 13350 rm *.png 13351 git st 13352 .. 13353 git st 13354 git add . 13355 git st 13356 git commit 13357 git push elvish-0.17.0/website/ttyshot/location-mode.html000066400000000000000000000025141415471104000216700ustar00rootroot00000000000000~> xiaq@xiaqsmbp LOCATION * ~ * ~/go/src/github.com/elves/elvish 110 ~/on/elvish-site/code 62 ~/on/elvish-site/code/src 52 ~/go/src/github.com/elves/elvish/edit 34 ~/on/elvish-site/code/tty 33 ~/on/elvish-site/code/assets 32 ~/go/src/github.com/elves/elvish/eval 26 ~/on/chat-app/code 24 ~/on/elvish-site/code/dst 20 ~/go/src/github.com/elves/md-highlighter 14 ~/on/chat-app/code/public 13 ~/.elvish elvish-0.17.0/website/ttyshot/navigation-mode.html000066400000000000000000000031541415471104000222200ustar00rootroot00000000000000~/go/src/github.com/elves/elvish> xiaq@xiaqsmbp NAVIGATING elvish CONTRIBUTING.md FROM golang:onbuild fix-for-0.7 Dockerfile images Gopkg.lock md-highlighter Gopkg.toml LICENSE Makefile README.md cover daemon edit errors eval getopt elvish-0.17.0/website/ttyshot/pipelines.html000066400000000000000000000031551415471104000211300ustar00rootroot00000000000000~> curl https://api.github.com/repos/elves/elvish/issues | from-json | all (one) | each [issue]{ echo $issue[number]: $issue[title] } | head -n 11 366: Support searching file from elvish directly 364: Ctrl-C in elvish kills Atom in background 357: Asynchronous syntax highlighting 356: In web backend, run commands with pty IO, not pipe 354: Support multi-line prompts from byte output 353: Completers should detect context in a top-down manner 352: Quoted command names are highlighted randomly 351: keep navigation mode open after command 350: Raw mode requires two presses of ^V 344: Elvish won't compile 343: Possible to suppress job control messages? elvish-0.17.0/website/ttyshot/tour/000077500000000000000000000000001415471104000172375ustar00rootroot00000000000000elvish-0.17.0/website/ttyshot/tour/completion-filter.html000066400000000000000000000004711415471104000235630ustar00rootroot00000000000000~/on/elvish> vim 0.16.0-release-notes.md xiaq@macarch COMPLETING argument .md 0.16.0-release-notes.md PACKAGING.md CONTRIBUTING.md README.md elvish-0.17.0/website/ttyshot/tour/completion.html000066400000000000000000000011731415471104000223000ustar00rootroot00000000000000~/on/elvish> vim 0.16.0-release-notes.md xiaq@macarch COMPLETING argument 0.16.0-release-notes.md LICENSE README.md go.sum CONTRIBUTING.md Makefile cmd/ pkg/ Dockerfile PACKAGING.md go.mod tools/ ━━━━ elvish-0.17.0/website/ttyshot/tour/history-list.html000066400000000000000000000010261415471104000225760ustar00rootroot00000000000000~> xiaq@macarch HISTORY (dedup on) 56439 cd ~ 56440 ls 56441 echo foo bar 56442 vim .elvish/rc.elv elvish-0.17.0/website/ttyshot/tour/history-walk-prefix.html000066400000000000000000000003111415471104000240500ustar00rootroot00000000000000~> echo foo bar xiaq@macarch HISTORY #55830 elvish-0.17.0/website/ttyshot/tour/history-walk.html000066400000000000000000000003171415471104000225630ustar00rootroot00000000000000~> vim .elvish/rc.elv xiaq@macarch HISTORY #56395 elvish-0.17.0/website/ttyshot/tour/lastcmd.html000066400000000000000000000003741415471104000215600ustar00rootroot00000000000000~> xiaq@macarch LASTCMD vim .elvish/rc.elv 0 vim 1 .elvish/rc.elv elvish-0.17.0/website/ttyshot/tour/location-filter.html000066400000000000000000000010231415471104000232140ustar00rootroot00000000000000~> xiaq@macarch LOCATION par 19 ~/go/src/github.com/elves/elvish/pkg/parse 7 ~/go/src/github.com/elves/elvish/pkg/parse/cmpd 1 ~/go/src/github.com/elves/elvish/pkg/parse/expr 1 ~/go/src/github.com/elves/elvish/pkg/parse/parseutil elvish-0.17.0/website/ttyshot/tour/location.html000066400000000000000000000010141415471104000217310ustar00rootroot00000000000000~> xiaq@macarch LOCATION 103 ~/go/src/github.com/elves/elvish/website 49 ~/go/src/github.com/elves/elvish/website/learn 47 ~/go/src/github.com/elves/elvish 27 ~/go/src/github.com/elves/elvish/pkg elvish-0.17.0/website/ttyshot/tour/navigation.html000066400000000000000000000031001415471104000222560ustar00rootroot00000000000000~/go/src/github.com/elves/elvish> xiaq@macarch NAVIGATING cirr 0.16.0-release-not This is the draft release n elvi CONTRIBUTING.md 2020-07-01. posi Dockerfile up LICENSE # Breaking changes Makefile NEXT-RELEASE.md - The following commands PACKAGING.md `edit:close-listing`, ` README.md `edit:listing:close`. cmd cover - The `edit:histlist:togg elvish-0.17.0/website/ttyshot/tour/unicode-prompts.html000066400000000000000000000002171415471104000232550ustar00rootroot00000000000000~❱ # Fancy unicode prompts! xiaq✸xiaqsmbp